Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@
"concepts/frame-adapters",
"concepts/determinism",
"guides/html-in-canvas",
"guides/motion-blur",
"guides/vfx-chain",
"guides/figma"
]
Expand Down
87 changes: 87 additions & 0 deletions docs/guides/motion-blur.mdx
Original file line number Diff line number Diff line change
@@ -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
<div
id="root"
data-composition-id="main"
data-width="1920"
data-height="1080"
data-duration="10"
data-motion-blur="on"
>
...
</div>
```

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 `<video>` drawn through the capture looks about 2–3% darker in Studio preview than in normal playback. Renders are not affected.
- **Worker count.** Output is not bit-identical between worker counts (around 60 dB PSNR), about the size of the difference chunking already causes.
- **Not handled.** Pose errors under CSS `zoom`, `position: fixed` and `foreignObject`; a very short Web Animations API animation that falls between scan samples; movers made see-through only by CSS backgrounds, masks or `clip-path`.
2 changes: 1 addition & 1 deletion skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"files": 2
},
"hyperframes": {
"hash": "bf9e65d52f3ffdd6",
"hash": "66d328f5c449b403",
"files": 77
},
"hyperframes-cli": {
Expand Down
1 change: 1 addition & 0 deletions skills/hyperframes/references/motion.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Measured frame by frame from the official HyperFrames launch films. These are defaults: a reference the person gave, or a Moment that does otherwise on purpose, wins for its own beat.

- **Moves**: a fast move takes 0.3–0.6 s and carries motion blur on its own ease, peaking at its fastest frame: 6–12 px on text-sized elements, 15–20 px full-frame, horizontal-only on a horizontal move. One move is one tween.
- **Render-time blur**: for fast 2D movers, `data-motion-blur="on"` on the composition root smears them along their real path in preview and render (opt-in; also `data-motion-blur-shutter`, `-samples`, `-events="on"` for callback-driven motion). Suggest it when the person asks for blur or a filmic look and the piece moves fast; skip it when the composition has a WebGL canvas, a 3D flip with `backface-visibility`, a root transform or opacity, or an HDR render, because blur turns itself off there. After adding it run `hyperframes check`: `motion_blur_off` names the cause. About 1.7x render time. Guide: /guides/motion-blur.
- **Arrive slow, leave clean**: long-tailed ease-outs on arrivals; landing takes longer than leaving. An exit is one smooth motion, never slide, stop, then leave. Entrances that are too snappy read as glitches; slowing the whole video by about a second often fixes it.
- **Ease by role**: an arrival lands on a long tail (`expo.out`, `power4.out`), a camera move or a colour flood eases both ways (`power2.inOut`), a zoom through or an exit accelerates (`power3.in`). The same `power2.out` at 0.5 s on everything is the flat default that makes a film feel generated. Linear only for rotation, progress and constant speed that is the point.
- **Springs** are named by damping ratio: 1.0 never bounces, 0.7-0.85 reads premium, 0.4-0.6 playful, under 0.3 a cartoon. Position and size may bounce; opacity and colour never do. GSAP's `elastic.out(1, 0.5)` and `back.out(1.4)` are the nearest built-ins; a closed-form spring of `t` keeps any frame renderable on its own.
Expand Down
Loading