diff --git a/docs/guides/motion-blur.mdx b/docs/guides/motion-blur.mdx index dc83da5e3ae..dd248e157d4 100644 --- a/docs/guides/motion-blur.mdx +++ b/docs/guides/motion-blur.mdx @@ -3,7 +3,7 @@ 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. +Motion blur gives fast-moving elements a real shutter smear. For each frame, HyperFrames reads where every mover was across the shutter interval and blurs 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. @@ -22,35 +22,33 @@ It is **opt-in**. Nothing blurs until you add `data-motion-blur="on"` to the com ``` -That is all. The same attribute drives Studio preview and `hyperframes render`, so what you see is what you render. +That is all. The same attribute drives Studio preview and `hyperframes 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. | +| `data-motion-blur-shutter` | `240` | Shutter angle in degrees, 0–720. Larger is a longer smear. | +| `data-motion-blur-samples` | at least 32 | A fixed number of samples per frame, 2–64. Left unset, each mover gets at least 32, and about one per 2 px of smear when that is more, so a fast move never shows steps. | +| `data-motion-blur-events` | off | `on` runs timeline callbacks (`onUpdate` and similar) while blur looks for movers and at each sample. Needed when the motion is set from a callback, such as a `render(t)` function, which blur cannot otherwise see. | +| `data-motion-blur-tier` | `auto` | `capture` or `filter` forces one of the two ways blur is drawn (below). | ## 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. +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 mover's glow and shadow travel with it, and the background behind it stays where it is. -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. +## Two ways it is drawn -## Requirements +**Capture** copies each mover out of the page with Chrome's [HTML in Canvas](/guides/html-in-canvas) API and redraws it along its path. It draws turns and zooms truthfully: a spinning tile smears along its arc and its centre stays sharp, and a zoom streaks outward. -Blur draws through Chrome's [HTML in Canvas](/guides/html-in-canvas) API. +**Filter** puts an SVG filter on each mover: copies of the element along its path, averaged, and softened along the motion. It works in any Chrome, over a WebGL canvas, and costs about what a render without blur costs. Turns and zooms are only softened, not streaked. -- **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. +With `data-motion-blur-tier="auto"` (the default), blur uses the filter tier when every mover only translates and capture when anything turns or scales, because for translation the two look the same and the filter tier is much cheaper. -If Studio's browser cannot blur, Studio tells the render, and the render also skips blur so the output matches what you saw. +Capture needs a browser with HTML in Canvas. Chrome 157 and 158 builds exposed it without a flag in headless and Chrome-for-Testing builds; whether stable Chrome does is unverified. Chrome 155 and earlier need `chrome://flags/#html-in-canvas`. -## When blur turns itself off +Blur uses the filter tier instead of capture when capture cannot run: -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 | +| Found in the composition | Why capture cannot draw it | | --- | --- | | A WebGL or WebGPU canvas anywhere in the document | Captured frozen. | | `backface-visibility: hidden` inside a 3D context (including from an animation) | Drawn mirrored. | @@ -58,12 +56,13 @@ html-in-canvas captures a few things wrongly. A wrongly blurred frame is worse t | 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. | +| A browser without HTML in Canvas, or three failed captures in a row | Capture is unavailable. | + +This is decided once for the whole composition, from the first frame. A defect the scan misses (a WebGL canvas added late, say) is caught per frame instead: that one frame is rendered sharp and counted in `failedFrames`. -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. +Blur is off, and stays off, for HDR renders, distributed renders, and renders that use the engine's own sub-frame blur. -### Find out why it is off +### Find out which one is drawing ```bash npx hyperframes check @@ -71,17 +70,17 @@ 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. | +| `motion_blur_filter_fallback` | info | Capture could not run, so the filter tier draws. The message names the cause and how to get capture back. | +| `motion_blur_no_movers` | info | Blur is on but nothing moves fast enough to blur. If the motion is set from a callback, add `data-motion-blur-events="on"`. | +| `motion_blur_unverified` | info | The check browser cannot run capture, so it drew with the filter tier. Render once to see capture. | -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`). +In the page, `window.__hfMotionBlur.report()` returns `state`, `tier`, `reason`, `reasonCode` and `movers`. A render logs one line per session with the tier, and 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 `