Betterflow is a browser-only screenshot editor built on Next.js 16 with the App Router. Its architecture centres on a hybrid rendering model: all layers — background, user image, and overlays — are pure HTML and CSS elements captured withDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/betterspacx/app/llms.txt
Use this file to discover all available pages before exploring further.
html2canvas or modern-screenshot. Two coordinated Zustand stores keep UI controls and the canvas renderer in lockstep, and a multi-encoder export pipeline lets users produce PNG, JPG, MP4, WebM, and GIF entirely client-side.
Project Structure
The repository follows a feature-grouped layout. Top-level directories map cleanly to concerns:State Management
Betterflow uses a dual-store pattern with Zustand. The two stores have clearly separated responsibilities and are kept in sync by a dedicated component.useImageStore — main design state
useImageStore is the source of truth for everything the user configures. It wraps Zustand’s temporal middleware from Zundo to provide undo/redo support.
It manages:
- Uploaded image URL and metadata (
uploadedImageUrl,imageName) - Background configuration (
backgroundConfig:type,value,opacity) - Background effects (
backgroundBlur,backgroundNoise) - Text overlays array (
textOverlays: TextOverlay[]) - Image overlays array (
imageOverlays: ImageOverlay[]) - Mockups array (
mockups: Mockup[]) - Annotation shapes (
annotations) and blur regions (blurRegions) - Image transformations:
imageScale,imageOpacity,borderRadius,backgroundBorderRadius imageBorderconfiguration (frame type, width, color, padding, title, opacity)imageShadowconfiguration (blur, offsetX, offsetY, spread, color, opacity)perspective3Dtransforms (perspective, rotateX, rotateY, rotateZ, translateX, translateY, translateZ, scale, skewX, skewY)imageFilters(brightness, contrast, grayscale, blur, hueRotate, invert, saturate, sepia)- Aspect ratio selection (
selectedAspectRatio) andcustomDimensions - Export settings (
exportSettings: quality, format, fileName) - Timeline state:
duration,playhead,isPlaying,isLooping,tracks,zoom,snapToKeyframes - Animation clips: preset-based clips with
startTime,duration, color - Slides: multi-image slideshow support (
slides,activeSlideId,slideshow)
useEditorStore — canvas rendering state
useEditorStore holds derived/runtime values that the HTML canvas renderer (ClientCanvas) needs to render efficiently without re-running complex calculations on every frame:
screenshot—{ src, scale, offsetX, offsetY, rotation, radius }background—{ mode, colorA, colorB, gradientDirection }(parsed from image store’s gradient string)shadow—{ enabled, elevation, side, softness, spread, color, intensity, offsetX, offsetY }pattern—{ enabled, type, scale, spacing, color, rotation, blur, opacity }frame—{ enabled, type, width, color, padding, title, opacity }canvas—{ aspectRatio, padding }noise—{ enabled, type, opacity }
EditorStoreSync — keeping stores in sync
The useEditorStoreSync hook (rendered at the editor root via EditorStoreSync.tsx in components/canvas/) uses React.useEffect to propagate changes from useImageStore into useEditorStore. This one-directional sync keeps the canvas renderer decoupled from the full design state while still reacting to user changes:
Canvas Rendering Architecture
The editor canvas uses a three-layer hybrid rendering strategy implemented entirely with HTML and CSS — no third-party canvas library is used:- Background Layer
- User Image Layer
- Overlay Layer
The background is a plain HTML
div styled with Tailwind and CSS theme variables. It receives gradients, solid colors, or url() image values from getBackgroundCSS() in lib/constants/backgrounds.ts.During export, the background div is cloned, blur and noise effects are applied, then html2canvas converts it to a canvas bitmap. This approach preserves full CSS fidelity — including multi-stop gradients and backdrop-filter — with no custom rendering code.Export Pipeline
Image export follows a deterministic multi-step compositing pipeline implemented inlib/export/export-service.ts:
Export Background (html2canvas)
Clone the background element, apply CSS blur and noise overlay, then call
html2canvas to produce a full-resolution background bitmap.Export User Image Layer (html2canvas / modern-screenshot)
Capture the HTML image layer at a high pixel ratio. When a 3D perspective transform is active,
modern-screenshot (domToCanvas) is used instead of html2canvas to correctly render CSS perspective transforms. Scale the resulting bitmap to the final export dimensions.Export Overlays (html2canvas)
Create a temporary off-screen DOM container, render all text overlays (with fonts loaded) and image overlays into it, then capture with
html2canvas to a transparent bitmap.Composite Layers
Draw each layer onto a single output canvas in z-order:
- Background (bottom)
- User image (middle)
- Overlays (top)
Data Flow Diagrams
Upload flow
EditorStoreSync propagates the change
The sync hook detects the new URL and calls
editorStore.setScreenshot({ src: blobUrl }).Export flow
Canvas container reference acquired
The hook reads the canvas container ref registered by
ClientCanvas.exportElement() called
All current state (background config, overlays, watermark flag, scale) is passed in.
Pipeline runs (steps 1–6 above)
Background → user image layer → overlays → composite → watermark → blob.
State update flow
Subscribed components re-render
Any component that reads
imageBorder from the store re-renders automatically.EditorStoreSync mirrors the change
The sync hook propagates the updated border config to
useEditorStore.frame.Animation Engine
The animation engine lives inlib/animation/ and drives both the timeline preview and video frame generation.
Interpolation (lib/animation/interpolation.ts)
| Function | Purpose |
|---|---|
findSurroundingKeyframes(keyframes, time) | Locate the previous and next keyframes around a given timestamp |
getInterpolatedProperty(track, time, key, default) | Interpolate a single property from a track at time |
getInterpolatedProperties(tracks, time) | Interpolate all properties across all tracks (single-clip, legacy) |
getClipInterpolatedProperties(clips, tracks, time) | Multi-clip aware interpolation — later clips override earlier ones for the same property |
getClipLocalInterpolatedProperties(clipTracks, localTime, ...) | Handle stretched / compressed clips by scaling time internally |
applyEasing(progress, easing) | Apply a named easing function to a normalised progress value |
lerp(start, end, progress) | Linearly interpolate between two numeric values |
Preset utilities (lib/animation/presets.ts)
| Function | Purpose |
|---|---|
clonePresetTracks(preset, options) | Deep-clone preset tracks with fresh unique IDs for timeline placement |
getPresetById(id) | Look up a preset by its string ID |
Easing functions
All eight easing functions take a normalised progress valuet ∈ [0, 1] and return an eased value in the same range:
Timeline state
Timeline state is stored insideuseImageStore as a TimelineState slice:
useTimelinePlayback hook in components/timeline/hooks/ drives the animation loop via requestAnimationFrame, reads getClipInterpolatedProperties() on every tick, and writes the results back into useImageStore as perspective3D and imageOpacity.
Animatable properties
perspective, rotateX, rotateY, rotateZ, translateX, translateY, scale, imageOpacity
Video Export Encoder Selection
The export orchestrator inlib/export-slideshow-video.ts selects an encoder based on the requested format and browser capability:
FFmpeg uses JPEG frames for intermediate processing (approximately 5× faster than PNG). The WASM binary is cached via the browser’s Cache API so it is only downloaded once across sessions.
Quality presets
| Preset | Bitrate | CRF |
|---|---|---|
| High | 25 Mbps | 18 |
| Medium | 10 Mbps | 23 |
| Low | 5 Mbps | 28 |
Encoder summary
| Encoder | File | Best for |
|---|---|---|
| FFmpeg WASM | ffmpeg-encoder.ts | H.264 and GIF; uses SharedArrayBuffer for multi-threading |
| WebCodecs | webcodecs-encoder.ts | Hardware-accelerated H.264 via browser GPU; muxed with mp4-muxer |
| MediaRecorder | video-encoder.ts | Native WebM recording; zero additional dependencies |
Browser Storage
Betterflow stores data across two mechanisms — IndexedDB for draft auto-save andlocalStorage for images and smaller preferences.
IndexedDB
| Database | Store | Key | Value |
|---|---|---|---|
betterflow-db | drafts | 'betterflow-draft' | Full EditorState + ImageState snapshot with timestamp |
draft-storage.ts module manages a singleton IDBDatabase connection with automatic reconnect on unexpected close. Drafts older than 7 days and exceeding 50 MB are cleaned up automatically.
LocalStorage
| Key | Contents |
|---|---|
betterflow-img-<id> | Base64-encoded image blob (≤ 500 KB; larger images are blob-URL only) |
betterflow-export-preferences | { format, qualityPreset, scale } JSON object |
Tech Stack Summary
| Category | Library | Version | Role |
|---|---|---|---|
| Framework | Next.js | 16.x | App Router, API routes, SSR |
| UI | React | 19.x | Component model; React Compiler enabled |
| Language | TypeScript | 5.9 | Full static typing |
| Styling | Tailwind CSS | 4.x | Utility-first CSS with CSS theme variables |
| UI Primitives | Radix UI | various | Accessible, unstyled component base |
| DOM Capture | html2canvas | 1.4 | Background and overlay layer export |
| 3D Capture | modern-screenshot | 4.7 | CSS perspective transform capture |
| State | Zustand | 5.x | Lightweight stores with selector subscriptions |
| Undo/Redo | Zundo | 2.x | Temporal middleware wrapping useImageStore |
| Video — WASM | FFmpeg (@ffmpeg/ffmpeg) | 0.12 | H.264 and GIF encoding in-browser |
| Video — Native | WebCodecs + mp4-muxer | — | Hardware H.264; mp4-muxer 5.x for muxing |
| Video — Fallback | MediaRecorder | Browser API | Native WebM recording |
| Storage | Cloudflare R2 | — | Object storage for backgrounds and assets |
| Local Storage | IndexedDB | Browser API | Draft auto-save via betterflow-db |
| Icons | Hugeicons | 1.x | Icon library |
| Animation UI | Framer Motion / GSAP | 12.x / 3.x | Landing page and UI micro-animations |