diff --git a/docs/docs.json b/docs/docs.json index 93c96f02c0f..90b5c44470f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1001,6 +1001,7 @@ "concepts/frame-adapters", "concepts/determinism", "guides/html-in-canvas", + "guides/motion-blur", "guides/vfx-chain", "guides/figma" ] diff --git a/docs/guides/motion-blur.mdx b/docs/guides/motion-blur.mdx new file mode 100644 index 00000000000..dc83da5e3ae --- /dev/null +++ b/docs/guides/motion-blur.mdx @@ -0,0 +1,87 @@ +--- +title: "Motion Blur" +description: "Smear fast-moving elements along their path, in Studio preview and in renders. Off until you turn it on." +--- + +Capture motion blur gives fast-moving elements a real shutter smear. For each frame, HyperFrames reads where every mover was across the shutter interval and redraws it along that path. A box sliding across the frame leaves a streak instead of a sharp copy at one position. + +It is **opt-in**. Nothing blurs until you add `data-motion-blur="on"` to the composition root. + +## Turn it on + +```html +
+ ... +
+``` + +That is all. The same attribute drives Studio preview and `hyperframes render`, so what you see is what you render. + +| Attribute | Default | Meaning | +| --- | --- | --- | +| `data-motion-blur` | none | `on` enables blur. `off` opts out, on the root or on any subtree. | +| `data-motion-blur-shutter` | `180` | Shutter angle in degrees, 0–720. Larger is a longer smear. | +| `data-motion-blur-samples` | `16` | Sub-frame samples per frame, 2–64. More is smoother and slower. | +| `data-motion-blur-events` | off | `on` also runs timeline callbacks (`onUpdate` and similar) at each sample. Use it when the motion is driven by callbacks, which blur cannot otherwise see. | + +## What it blurs + +Elements that move quickly in 2D: position, scale and rotation changes from GSAP, CSS animations and transitions, and the Web Animations API. Slow movers and static content are left sharp. A composition with nothing moving is never wrapped and does no per-frame blur work; the one-time discovery scan still runs on first play. + +Blur is an image-space approximation. A mover's box and its contents move together, and glows or shadows outside its box stay sharp. 3D movers are skipped. + +## Requirements + +Blur draws through Chrome's [HTML in Canvas](/guides/html-in-canvas) API. + +- **Renders** use the render browser, which HyperFrames launches with the API enabled. +- **Studio preview** uses your own browser. We saw Chrome 157 and 158 expose the API without a flag, but only in headless and Chrome-for-Testing builds; whether it ships in stable Chrome is unverified. Chrome 155 and earlier need `chrome://flags/#html-in-canvas`. Preview detects the API and plays sharp when it is missing, so check your own browser rather than relying on a version number. + +If Studio's browser cannot blur, Studio tells the render, and the render also skips blur so the output matches what you saw. + +## When blur turns itself off + +html-in-canvas captures a few things wrongly. A wrongly blurred frame is worse than a sharp one, so when HyperFrames finds one of these it turns blur off **for the whole composition, from the first frame**, instead of switching mid-render. This covers every defect the timeline scan can see. A defect the scan misses (a WebGL canvas added late, a root shake between scan samples, a face that flips hidden briefly) is caught per frame instead: that one frame renders sharp, blur resumes on the next, and the count is reported as `failedFrames`. + +| Found in the composition | Why | +| --- | --- | +| A WebGL or WebGPU canvas anywhere in the document | Captured frozen. | +| `backface-visibility: hidden` inside a 3D context (including from an animation) | Drawn mirrored. | +| A transform on the root element | Ignored by the capture. | +| Opacity below 1, or `mix-blend-mode`, on the root element | Not applied by the capture. | +| An `iframe`, `object`, `embed` or custom element in the root | Not captured faithfully. | +| A nested capture canvas, such as a VFX block | Hangs the renderer. | +| An HDR render | The HDR compositor's layer mask does not reach the blur canvases. | +| A distributed render | Chunks cannot share the single-page capture. | + +Large see-through movers (a full-frame translucent sweep, say) are skipped too, because blurring them would drag the content underneath along. Their fast children still blur. + +### Find out why it is off + +```bash +npx hyperframes check +``` + +| Finding | Severity | Meaning | +| --- | --- | --- | +| `motion_blur_off` | warning | Blur is on but was switched off. The message names the cause and a fix. | +| `motion_blur_no_movers` | info | Blur is on but nothing moves fast enough to blur. | +| `motion_blur_unverified` | info | The check could not confirm blur in this browser. | + +In the page, `window.__hfMotionBlur.report()` returns `state`, `reason`, `reasonCode` and `movers`. A render also logs one line per session saying whether blur ran and why not, plus a session-end line when any frames fell back to sharp (`failedFrames > 0`). + +## Cost and limits + +- **Render time.** About 1.7× for a blurred composition (median over 20 real compositions). Compositions where blur is off or nothing moves add only the one-time discovery scan (tens of milliseconds on a 25-mover composition). +- **Editing.** The root is wrapped in the capture canvas only while playing or rendering. Paused, it is plain DOM, so click-to-select in Studio works as usual. +- **Preview rate.** Blurred preview can run at half the frame rate on heavy compositions. +- **Video.** A `