The Tech Behind ASOShot: Building a Canvas-Based Screenshot Engine
Technical deep-dive into ASOShot's canvas-based screenshot rendering engine. React 19, 2D-canvas rendering, 3D projection, and client-side export architecture.
The Tech Behind ASOShot: Building a Canvas-Based Screenshot Engine
Technical deep-dive into ASOShot's canvas-based screenshot rendering engine. React 19, 2D-canvas rendering, 3D projection, and client-side export architecture.
The Architecture
ASOShot is built with:
- •React 19 + TypeScript for the UI layer
- •React Router 7 in framework mode for routing and SSR
- •TailwindCSS + @ninna-ui/* for styling
- •Lucide React for icons
- •JSZip for ZIP batch export
- •Custom 2D-canvas rendering engine for screenshot export
Imported images and screenshot rendering stay in the browser. The translation feature is separate and sends only caption text and selected language choices to the configured translation service.
The Canvas Export Engine
The most interesting technical challenge was rendering screenshots at full resolution. We initially used html-to-image (SVG foreignObject approach), but it couldn't rasterize CSS 3D transforms: Chrome rasterizes preserve-3d content at composited size, causing blur under transform scale.
So we built a custom 2D-canvas renderer:
- •projection.ts: 3D point projection, perspective-warped image rendering, rounded rect tracing
- •drawDevice.ts: flat + 3D device drawing with 40-slice extrusion for realistic edges
- •drawBackground.ts: solid, gradient, image, and panorama strip slicing with patterns and orbs
- •drawCaption.ts: text wrapping, shadow, stroke, background pills, letter spacing via ctx.letterSpacing
- •drawBadge.ts: store badge pills
- •index.ts: orchestrates all renderers at unit=1 for full-resolution export
The preview and export engine share device geometry and scene settings to keep their output aligned.
Fluid Canvas Preview
The on-screen preview uses a fluid canvas approach:
- •ScreenshotCanvas is a fluid aspectRatio box (width 100%, no fixed px)
- •It self-measures rendered width via ResizeObserver + useLayoutEffect
- •Every px-derived value is multiplied by a unit factor (1.0 at export resolution)
- •No ancestor transform: scale is used, avoiding Chrome's preserve-3d rasterization issues
- •CanvasPreview cards size by width only, maintaining consistent aspect ratios
This approach avoids scaling an already composited 3D ancestor and keeps the preview responsive across editor sizes.
3D Device Rendering
3D device frames use a custom rendering approach:
- •On-screen: CSS 3D transforms with preserve-3d for real-time interaction
- •Export: 2D-canvas slice renderer with 40-slice extrusion + perspective-warped front face
- •Edge depth: 16px at export resolution (scaled by unit factor)
- •Perspective: 1200px focal distance for a natural-looking 3D effect
- •Frame radius: parsed from device specs (e.g., '14%/6.5%' for iPhone outer/inner radius)
The 2D-canvas approach produces full-resolution perspective exports from the same scene settings used by the preview.
On-Canvas Drag Interaction
Users can drag device frames and captions directly on the canvas:
- •Screen pixels are converted to export pixels via division by the unit factor
- •SelectionRing with corner handles for visual feedback
- •Only the active card in CanvasPreview is interactive
- •Position changes propagate through useProject's updateScreenshot function
This makes the editing experience feel direct and intuitive: no separate panels or controls needed for positioning.
Try the Canvas Engine
Experience the fluid canvas preview and full-resolution export for yourself. Free, no sign-up.
Try ASOShot (Free)