Skip to main content

Documentation 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.

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 with 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:
better-flow/
├── app/                          # Next.js App Router pages
│   ├── api/
│   │   ├── cleanup-cache/        # Cache cleanup endpoint
│   │   ├── export/               # Server-side image export (FFmpeg)
│   │   ├── image-proxy/          # R2 image proxy (CORS bypass)
│   │   ├── upload-url/           # R2 presigned URL generation
│   │   └── upload-video/         # Chrome extension video upload
│   ├── editor/                   # Main editor page
│   ├── layout.tsx                # Root layout
│   ├── page.tsx                  # Landing page
│   └── globals.css               # Global styles + CSS theme tokens

├── components/
│   ├── canvas/                   # Canvas rendering components
│   │   ├── ClientCanvas.tsx      # Main HTML/CSS canvas renderer (client-only)
│   │   ├── EditorCanvas.tsx      # Wrapper: shows upload UI or canvas
│   │   ├── EditorStoreSync.tsx   # Syncs useImageStore → useEditorStore
│   │   ├── frames/               # Browser toolbar & 3D frame overlays
│   │   │   ├── BrowserToolbar.tsx
│   │   │   └── Frame3DOverlay.tsx
│   │   ├── html/                 # Individual HTML rendering layers
│   │   │   ├── HTMLBackgroundLayer.tsx
│   │   │   ├── HTMLMainImageLayer.tsx
│   │   │   ├── HTMLTextOverlayLayer.tsx
│   │   │   ├── HTMLImageOverlayLayer.tsx
│   │   │   ├── HTMLPatternLayer.tsx
│   │   │   ├── HTMLNoiseLayer.tsx
│   │   │   ├── HTMLBlurRegionLayer.tsx
│   │   │   └── SVGAnnotationLayer.tsx
│   │   └── overlays/             # 3D perspective overlay
│   │       └── Perspective3DOverlay.tsx
│   ├── controls/                 # Editor control panels (border, shadow, 3D…)
│   ├── editor/                   # Layout: panels, header, sections
│   ├── export/                   # Export dialogs and progress UI
│   ├── overlays/                 # Image/sticker overlay management
│   ├── text-overlay/             # Text layer controls
│   ├── timeline/                 # Animation timeline, tracks, playback
│   │   ├── TimelineEditor.tsx
│   │   ├── TimelineControls.tsx
│   │   ├── TimelineRuler.tsx
│   │   ├── TimelineTrack.tsx
│   │   ├── TimelinePlayhead.tsx
│   │   ├── KeyframeMarker.tsx
│   │   ├── AnimationPresetGallery.tsx
│   │   └── hooks/
│   │       └── useTimelinePlayback.tsx
│   ├── templates/                # Template system
│   └── ui/                       # Shared Radix-based UI primitives

├── lib/                          # Core libraries and utilities
│   ├── store/
│   │   └── index.ts              # useImageStore & useEditorStore
│   ├── animation/
│   │   ├── interpolation.ts      # Keyframe interpolation & easing functions
│   │   └── presets.ts            # 20+ named animation presets + clonePresetTracks
│   ├── export/
│   │   ├── export-service.ts     # Image export (compositing pipeline)
│   │   ├── ffmpeg-encoder.ts     # FFmpeg WASM H.264/GIF encoder
│   │   ├── webcodecs-encoder.ts  # WebCodecs H.264 + mp4-muxer
│   │   ├── video-encoder.ts      # MediaRecorder WebM wrapper
│   │   └── export-utils.ts       # Shared helpers
│   ├── export-slideshow-video.ts # Video export orchestrator
│   ├── constants/                # Backgrounds, presets, fonts, colors
│   ├── canvas/                   # Canvas utility helpers
│   ├── image-storage.ts          # localStorage image persistence
│   ├── export-storage.ts         # Export preferences persistence
│   └── draft-storage.ts          # IndexedDB draft auto-save

├── hooks/                        # Custom React hooks
│   ├── useExport.ts
│   ├── useAspectRatioDimensions.ts
│   └── useAutosaveDraft.ts

├── types/                        # TypeScript type definitions
│   ├── canvas.ts
│   ├── editor.ts
│   └── animation.ts              # Keyframe, AnimationTrack, TimelineState…

└── public/
    ├── assets/                   # Demo images
    ├── overlays/                 # Overlay images
    └── backgrounds/              # Background images

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
  • imageBorder configuration (frame type, width, color, padding, title, opacity)
  • imageShadow configuration (blur, offsetX, offsetY, spread, color, opacity)
  • perspective3D transforms (perspective, rotateX, rotateY, rotateZ, translateX, translateY, translateZ, scale, skewX, skewY)
  • imageFilters (brightness, contrast, grayscale, blur, hueRotate, invert, saturate, sepia)
  • Aspect ratio selection (selectedAspectRatio) and customDimensions
  • 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:
// lib/store/index.ts (simplified)
export function useEditorStoreSync() {
  const imageStore = useImageStore();
  const editorStore = useEditorStore();

  React.useEffect(() => {
    // Sync uploaded image URL → canvas screenshot source
    if (imageStore.uploadedImageUrl !== editorStore.screenshot.src) {
      editorStore.setScreenshot({ src: imageStore.uploadedImageUrl });
    }

    // Parse gradient string → extract colorA, colorB, direction
    if (bgConfig.type === 'gradient') {
      const { colorA, colorB, direction } = parseGradientColors(gradientStr);
      editorStore.setBackground({ mode: 'gradient', colorA, colorB, gradientDirection: direction });
    }

    // Mirror imageBorder → frame, imageShadow → shadow …
  });
}

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:
┌─────────────────────────────────────┐
│  3 · Overlay Layer (html2canvas)    │  ← text & image overlays composited on top
├─────────────────────────────────────┤
│  2 · User Image Layer (HTML/CSS)    │  ← image transforms, shadow, border, 3D
├─────────────────────────────────────┤
│  1 · Background Layer (HTML / CSS)  │  ← gradient, solid, image bg → html2canvas
└─────────────────────────────────────┘
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.
Why the hybrid approach? Pure CSS gives the best fidelity for backgrounds (gradients, blur, noise, blend modes). HTML image elements give precise, GPU-accelerated rendering for user images. Keeping overlays as real DOM elements means full font and emoji rendering without re-implementing a text layout engine in canvas.

Export Pipeline

Image export follows a deterministic multi-step compositing pipeline implemented in lib/export/export-service.ts:
1

Export Background (html2canvas)

Clone the background element, apply CSS blur and noise overlay, then call html2canvas to produce a full-resolution background bitmap.
2

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.
3

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.
4

Composite Layers

Draw each layer onto a single output canvas in z-order:
  1. Background (bottom)
  2. User image (middle)
  3. Overlays (top)
5

Add Watermark

Optionally stamp the Betterflow watermark onto the composited canvas.
6

Convert to Blob / DataURL

Call canvas.toBlob() with the chosen format and quality settings, then trigger the browser download and save preferences to localStorage.

Data Flow Diagrams

Upload flow

1

User drops or picks a file

react-dropzone in UploadDropzone.tsx validates file type and size.
2

Blob URL created

URL.createObjectURL(file) produces an immediate preview URL.
3

useImageStore updated

setUploadedImageUrl(blobUrl) stores the URL in the main design store.
4

EditorStoreSync propagates the change

The sync hook detects the new URL and calls editorStore.setScreenshot({ src: blobUrl }).
5

ClientCanvas re-renders

The HTML image layer loads from the new src and the canvas refreshes.
6

Persisted to localStorage

For images under 500 KB, saveImageBlob() in lib/image-storage.ts serialises the blob to base64 under the key betterflow-img-<id>.

Export flow

1

User clicks Export

The export dialog calls the useExport hook.
2

Canvas container reference acquired

The hook reads the canvas container ref registered by ClientCanvas.
3

exportElement() called

All current state (background config, overlays, watermark flag, scale) is passed in.
4

Pipeline runs (steps 1–6 above)

Background → user image layer → overlays → composite → watermark → blob.
5

File downloaded

URL.createObjectURL(blob) triggers a programmatic <a> click.
6

Preferences saved

saveExportPreferences() persists format, qualityPreset, and scale to localStorage under betterflow-export-preferences.

State update flow

1

User adjusts a control

e.g. drags the border-width slider in BorderControls.tsx.
2

Control calls store setter

setImageBorder({ width: newValue }) is dispatched to useImageStore.
3

Zustand store updates

Zundo records the previous state for undo support, then the store updates.
4

Subscribed components re-render

Any component that reads imageBorder from the store re-renders automatically.
5

EditorStoreSync mirrors the change

The sync hook propagates the updated border config to useEditorStore.frame.
6

ClientCanvas re-renders

The HTML canvas layer picks up the new frame configuration and redraws.

Animation Engine

The animation engine lives in lib/animation/ and drives both the timeline preview and video frame generation.

Interpolation (lib/animation/interpolation.ts)

FunctionPurpose
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)

FunctionPurpose
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 value t ∈ [0, 1] and return an eased value in the same range:
// lib/animation/interpolation.ts
export const easingFunctions: Record<EasingFunction, (t: number) => number> = {
  linear:           (t) => t,
  'ease-in':        (t) => t * t,
  'ease-out':       (t) => 1 - (1 - t) * (1 - t),
  'ease-in-out':    (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
  'ease-in-cubic':  (t) => t * t * t,
  'ease-out-cubic': (t) => 1 - Math.pow(1 - t, 3),
  'ease-in-expo':   (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10),
  'ease-out-expo':  (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
};

Timeline state

Timeline state is stored inside useImageStore as a TimelineState slice:
// types/animation.ts
interface TimelineState {
  duration: number;       // total timeline length in ms
  playhead: number;       // current position in ms
  isPlaying: boolean;
  isLooping: boolean;
  tracks: AnimationTrack[];
  zoom: number;
  snapToKeyframes: boolean;
}
The 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 in lib/export-slideshow-video.ts selects an encoder based on the requested format and browser capability:
Requested format

       ├── GIF ──────────────────────────────→ FFmpeg WASM
       │                                        (ffmpeg-encoder.ts)

       ├── WebM ─────────────────────────────→ MediaRecorder
       │                                        (video-encoder.ts)

       └── MP4 ──── WebCodecs available? ──Yes→ WebCodecs + mp4-muxer
                           │                    (webcodecs-encoder.ts)
                           No

                           └──────────────────→ FFmpeg WASM fallback
                                               (ffmpeg-encoder.ts)
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

PresetBitrateCRF
High25 Mbps18
Medium10 Mbps23
Low5 Mbps28

Encoder summary

EncoderFileBest for
FFmpeg WASMffmpeg-encoder.tsH.264 and GIF; uses SharedArrayBuffer for multi-threading
WebCodecswebcodecs-encoder.tsHardware-accelerated H.264 via browser GPU; muxed with mp4-muxer
MediaRecordervideo-encoder.tsNative WebM recording; zero additional dependencies

Browser Storage

Betterflow stores data across two mechanisms — IndexedDB for draft auto-save and localStorage for images and smaller preferences.

IndexedDB

DatabaseStoreKeyValue
betterflow-dbdrafts'betterflow-draft'Full EditorState + ImageState snapshot with timestamp
The 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

KeyContents
betterflow-img-<id>Base64-encoded image blob (≤ 500 KB; larger images are blob-URL only)
betterflow-export-preferences{ format, qualityPreset, scale } JSON object
Images larger than 500 KB are not persisted to localStorage to avoid quota errors. They exist only as ephemeral blob URLs for the current session.

Tech Stack Summary

CategoryLibraryVersionRole
FrameworkNext.js16.xApp Router, API routes, SSR
UIReact19.xComponent model; React Compiler enabled
LanguageTypeScript5.9Full static typing
StylingTailwind CSS4.xUtility-first CSS with CSS theme variables
UI PrimitivesRadix UIvariousAccessible, unstyled component base
DOM Capturehtml2canvas1.4Background and overlay layer export
3D Capturemodern-screenshot4.7CSS perspective transform capture
StateZustand5.xLightweight stores with selector subscriptions
Undo/RedoZundo2.xTemporal middleware wrapping useImageStore
Video — WASMFFmpeg (@ffmpeg/ffmpeg)0.12H.264 and GIF encoding in-browser
Video — NativeWebCodecs + mp4-muxerHardware H.264; mp4-muxer 5.x for muxing
Video — FallbackMediaRecorderBrowser APINative WebM recording
StorageCloudflare R2Object storage for backgrounds and assets
Local StorageIndexedDBBrowser APIDraft auto-save via betterflow-db
IconsHugeicons1.xIcon library
Animation UIFramer Motion / GSAP12.x / 3.xLanding page and UI micro-animations

Build docs developers (and LLMs) love