Skip to content

feat: canvas-based live preview for crop, rotation, and color#1546

Open
smirk-dev wants to merge 1 commit into
magic-peach:mainfrom
smirk-dev:feat/canvas-live-preview-smirk
Open

feat: canvas-based live preview for crop, rotation, and color#1546
smirk-dev wants to merge 1 commit into
magic-peach:mainfrom
smirk-dev:feat/canvas-live-preview-smirk

Conversation

@smirk-dev
Copy link
Copy Markdown

Summary

The editor now renders a hardware-accelerated canvas live preview that reflects crop/framing, rotation, and brightness/contrast/saturation changes in real time — no full FFmpeg export needed to see results. The export pipeline is completely untouched: the canvas is preview-only, FFmpeg still produces the final output.

Refs #653

Technical details

Canvas/video wiring — a single requestAnimationFrame loop (useCanvasPreview) mirrors the existing <video> onto a <canvas> with ctx.drawImage every frame. Drawing every frame (not just on timeupdate) means slider changes update instantly even while paused, and playback stays smooth. The native <video> is kept in the DOM as the frame source (and audio), visually hidden while live; play/pause works via click or Space, seeking via the existing thumbnail strip.

Rotation & aspect ratio — geometry mirrors buildVideoFilter exactly so the preview matches the export: rotate (transpose) → fit/fill scale + pad/crop → colour. Rotation is applied with canvas transforms (translate+rotate, sharper than a CSS element rotate and integrated with the letterbox math); 90/270° swap the source axes. The output frame is letterboxed at the selected preset's aspect ratio inside the 16:9 container, and within that frame the video is fit (letterbox bars) or fill (cover + crop) — matching FFmpeg's force_original_aspect_ratio=decrease+pad vs increase+crop.

Colour filterseq=brightness:contrast:saturation is mapped to a filter string: additive brightness b → brightness(1+b), contrast/saturation pass through 1:1. Applied via ctx.filter where supported (so the black letterbox pad stays pure black, like the export), with a CSS filter fallback on the canvas element for older browsers.

Decoupled from export — none of src/lib/ffmpeg.ts changed. The geometry helpers live in src/lib/previewGeometry.ts and are shared only by the preview.

Performance considerations

  • One requestAnimationFrame loop per preview instance — no setInterval, no nested timers; cancelled on unmount / when live preview is disabled.
  • The recipe is read through a ref, so moving sliders never re-creates the loop or triggers React re-renders per frame.
  • Container size is cached via ResizeObserver and only re-read on resize — no per-frame getBoundingClientRect, so no layout thrashing.
  • DPR-aware: internal buffer scales with devicePixelRatio while CSS size stays stable; canvas buffer/CSS writes are skipped when unchanged.

Files changed

File Responsibility
src/lib/previewGeometry.ts New. Pure, unit-tested geometry/colour helpers mirroring buildVideoFilter (rotate → fit/fill scale+pad/crop → eq colour).
src/lib/__tests__/previewGeometry.test.ts New. 14 unit tests for rotation axis-swap, fit/fill scaling, letterbox math, and colour-filter mapping.
src/hooks/useCanvasPreview.ts New. The requestAnimationFrame render loop (recipe-via-ref, ResizeObserver, DPR-aware buffer, ctx.filter with CSS fallback).
src/components/VideoPreview.tsx Canvas live-preview surface + Live toggle + click/Space play-pause; native <video> retained as frame source. Overlay control buttons regrouped into flex rows.

useVideoEditor.ts already exposes recipe + videoRef to VideoPreview (the integration point #653 calls out); the canvas consumes them through it, so no change was needed there.

Screen recording

Testing

  • bun run lint — clean
  • bun run test — 135 passing (14 new for the geometry/colour helpers)
  • bun run build — static export succeeds
  • Manual: rotation 0/90/180/270, brightness/contrast/saturation sliders, and aspect fit/fill all reflected live

Acceptance criteria

  • Rotation 0/90/180/270 reflected without exporting
  • Brightness/contrast/saturation update the preview in real time
  • Aspect ratio framing (fit/fill) visually indicated
  • Performance: single requestAnimationFrame, no layout thrashing
  • Export still uses FFmpeg (canvas is preview-only)
  • Screen recording attached (to be added before merge)

Note: the colour mapping is a faithful visual approximation of FFmpeg's eq (CSS brightness() is multiplicative vs FFmpeg's additive term), so extreme values may differ a few percent from the exported result — expected for a real-time preview; the export remains the source of truth.

Mirror the source <video> onto a <canvas> via a single requestAnimationFrame
loop so crop/framing, rotation and brightness/contrast/saturation changes are
reflected instantly, without running a full FFmpeg export. The export pipeline
is untouched — the canvas is preview-only.

- src/lib/previewGeometry.ts: pure, unit-tested geometry/colour helpers that
  mirror buildVideoFilter (rotate -> fit/fill scale+pad/crop -> eq colour).
- src/hooks/useCanvasPreview.ts: rAF render loop; recipe read via ref (no
  per-change re-subscribe), container size cached via ResizeObserver (no layout
  thrash), DPR-aware buffer, ctx.filter for colour with CSS-filter fallback.
- src/components/VideoPreview.tsx: render the canvas as the live preview with a
  Live toggle and click/space play-pause; native <video> retained as the frame
  source. Grouped the overlay control buttons into flex rows.

Refs magic-peach#653

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 7, 2026

@smirk-dev is attempting to deploy a commit to the magic-peach1's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 7, 2026

👋 Thanks for your PR, @smirk-dev!

Welcome to Reframe — a browser-based video editor built for everyone 🎬

What happens next

  1. 🤖 Automated checks — build & TypeScript typecheck will run automatically
  2. Vercel preview — a preview deployment will be created (requires maintainer authorization for fork PRs)
  3. 👀 Code review — a maintainer will review your changes
  4. 🚀 Merge — once approved, your PR will be merged!

Quick checklist

  • PR title follows Conventional Commits (e.g. feat: add dark mode)
  • Linked the issue this PR closes (e.g. Closes #123)
  • Tested the changes locally (bun run dev)
  • Build passes (bun run build)

Useful links

Happy coding! 🎉

@github-actions github-actions Bot added level:advanced Advanced level - 55 pts type:design UI/UX design type:performance Performance type:testing Testing labels Jun 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 7, 2026

⚠️ PR Format Issues — @smirk-dev

Please fix the following before your PR can be reviewed:

  • ⚠️ No linked issue found. Add Closes #<issue-number> to your PR description.

Push new commits after fixing — this comment will update automatically.

📖 CONTRIBUTING.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:advanced Advanced level - 55 pts type:design UI/UX design type:performance Performance type:testing Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant