--- name: motion-blur-streak description: Fake directional velocity blur on a fast entrance or camera push-through — blur peaks at max speed and resolves to 0 at the settle, so the element streaks in then snaps sharp. Two paths — SVG feGaussianBlur on the motion axis, or an echo/ghost trail that collapses into the lead. metadata: tags: motion-blur, velocity, streak, entrance, fly-in, ghost, echo, svg-filter, kinetic, camera, snap --- # Motion-Blur Streak Real motion blur isn't available to a seeked renderer (it integrates over shutter time), so this rule **fakes** it for a fast fly-in or hard camera push-through. The whole point is the _coupling_: the blur envelope rides the **same ease and window** as the position tween, so peak blur lands exactly on peak speed and the element is razor-sharp the instant it stops. Two paths: - **(A) Directional SVG blur** — inline `` (X on the motion axis, 0 across it), tweened via a proxy. Cleanest; a true directional smear. - **(B) Echo / ghost trail** — 2–4 duplicates at decreasing opacity, offset backward along the motion vector, collapsing into the lead as it settles. No filter cost; a stylized "speed-line" trail. **Entrances and mid-shot moves only — never a mid-composition exit.** A blurred element fleeing off-frame mid-composition reads as a glitch; a hard exit between scenes is the transition's job (`../../transitions/overview.md`). One sanctioned scope extension: the envelope may ride the **camera wrapper** during a travel leg — see the Camera-Travel Carve-Out. ## How It Works A fast `out`-eased move front-loads velocity — fastest off the start, bleeding to zero at the settle. Map the blur/echo envelope onto that same curve: position travels from an off-frame / pushed-back start to rest over `MOVE_DUR`; in lockstep on the same window and ease the smear goes `PEAK_BLUR → 0` (A) or the ghosts collapse onto the lead (B). By the settle the element is fully crisp and dwells ≥1 s — the contrast between violent streak and still, sharp settle IS the effect. GSAP can't tween an SVG attribute directly: tween a plain `{ v }` proxy and write `setAttribute("stdDeviation", …)` in `onUpdate`, seeding it once at setup so a seek to t=0 shows the streaked start. ## Recipe ```html
{phrase}
``` ```js // Path A — proxy-tweened directional blur. const blurNode = document.getElementById("streak-blur"); const blurProxy = { v: PEAK_BLUR }; const writeBlur = () => blurNode.setAttribute("stdDeviation", `${blurProxy.v} 0`); // X axis only writeBlur(); // seed frame 0 — a seek to t=0 must show the streaked start, not a sharp pre-frame tl.fromTo( "#streak-el", { x: ENTER_FROM_X, opacity: 0 }, { x: 0, opacity: 1, duration: MOVE_DUR, ease: MOVE_EASE }, MOVE_START, ); tl.to(blurProxy, { v: 0, duration: MOVE_DUR, ease: MOVE_EASE, onUpdate: writeBlur }, MOVE_START); // Path B — ghosts on the SAME window/ease; per-ghost variation by index. gsap.utils.toArray(".streak-ghost").forEach((g) => { const i = Number(g.dataset.i); // 1..N-1, set in HTML tl.fromTo( g, { x: ENTER_FROM_X - i * ECHO_STEP_PX, opacity: GHOST_BASE_OPACITY / i }, { x: 0, opacity: 0, duration: MOVE_DUR, ease: MOVE_EASE }, MOVE_START, ); }); ``` ## Variations - **Vertical streak** — swap axes: `y`, `stdDeviation="0 Y"`, vertical echo offsets. - **Camera push-through** — `scale: SCALE_FROM → 1` with a symmetric `"B B"` envelope (depth-wise smear, not directional): the wordmark punches out of soft focus and snaps crisp at the lock. - **Staggered grid streak-in** — each card streaks into its slot at `MOVE_START + i * CARD_STAGGER` with its own blur proxy / ghosts; sharp the instant it lands. - **Hold-the-streak** — blur on a marginally slower curve than position (position `expo.out`, blur `power3.out`) so the last wisp resolves just after arrival. Sparingly; default is locked envelopes. ## Camera-Travel Carve-Out The envelope is also sanctioned at **wrapper level**: on the `.world` / camera wrapper of a virtual-camera scene ([viewport-change.md](viewport-change.md), [multi-phase-camera.md](multi-phase-camera.md), [3d-camera-flight.md](3d-camera-flight.md)) during a **travel leg** — a dive, a whip sweep, a violent final push. This does **not** violate "never a mid-composition exit": the world never leaves frame — the camera travels _through_ it, and every leg ends with the world at rest, sharp, inside the frame. Each leg is an **arrival** at the next pose, so the entrance doctrine applies leg by leg. Three deltas from the element-level recipe: - **Envelope follows the leg's ease.** An `out` leg (dive, final push) uses the base recipe unchanged. An `inOut` repositioning leg peaks mid-leg: split the envelope at the velocity peak — `0 → PEAK` on the in-half ease over the first half, `PEAK → 0` on the out-half over the second. Seed the proxy at **0** for these (the streaked state lives mid-leg, not at t=0; seed-at-`PEAK_BLUR` belongs to the entrance shape, where the first frame IS the fastest). - **Filter placement.** 2D camera: `filter: url(#streak)` on the `.world` wrapper. 3D flight: on the **perspective stage** above the 3D context — a `filter` on a `preserve-3d` element flattens it and collapses every `translateZ`. Never per-element inside the world: one frame-wide envelope, not N desynced ones. - **Full-frame blur is heavy** — cap `PEAK_BLUR` ~18–20 at wrapper level (vs 30 for one element); a brief whip may touch ~24. Axis rule as usual: `"X 0"` for a lateral whip/pan, `"B B"` for a dive/push. ### Whip sweep (named composition) The heavily-blurred lateral whip that resolves into the next region — two rules on one window: 1. **Position** — [nudge-curve.md](nudge-curve.md)'s three-phase chain on the camera state, tuned burst-dominant (tail still ≥3× ramp-in in time). 2. **Blur** — `0 → PEAK` across the ramp-in, held at `PEAK` through the linear burst (constant velocity = constant smear), `PEAK → 0` across the tail. Swap or reveal the next region's content DURING the burst — the smear masks the change; the `power4.out` tail lands it sharp. Reveal during the burst, read after the tail. ```js tl.to(cam, { x: WHIP_X * 0.1, duration: 0.12, ease: "power3.in", onUpdate: applyCamera }, WHIP_AT); tl.to( cam, { x: WHIP_X * 0.75, duration: 0.1, ease: "none", onUpdate: applyCamera }, WHIP_AT + 0.12, ); tl.to( cam, { x: WHIP_X, duration: 0.35, ease: "power4.out", onUpdate: applyCamera }, WHIP_AT + 0.22, ); tl.to(blurProxy, { v: PEAK_BLUR, duration: 0.12, ease: "power3.in", onUpdate: writeBlur }, WHIP_AT); // blur holds at PEAK through the linear burst (no tween needed — value rests at PEAK) tl.to(blurProxy, { v: 0, duration: 0.35, ease: "power4.out", onUpdate: writeBlur }, WHIP_AT + 0.22); ``` ## Values | token | range | notes | | ------------------ | -------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | MOVE_EASE | `expo.out` / `power4.out` (default) / `power3.out` | `out`-family ONLY — `in`/`inOut` puts peak speed in the wrong place; position and blur share it | | MOVE_DUR | 0.25–0.6s | over ~0.7s reads as a focus pull, not velocity | | ENTER_FROM_X/Y | 40–120% of the element's own dimension | enough runway for the streak to read | | PEAK_BLUR | 8–30 (default 18) | >30 erases the glyph at the start; ~18–20 cap at wrapper level | | SCALE_FROM | 1.3–2.5 | push-through variation | | N (ghosts) | 2–4 | >4 reads as strobe, not streak | | ECHO_STEP_PX | 12–40px | `N × step ≲ ENTER_FROM` so the furthest ghost starts inside the runway | | GHOST_BASE_OPACITY | 0.3–0.6 | opaque ghosts read as duplicate elements | | CARD_STAGGER | 0.05–0.12s | one assembling wave, not separate arrivals | ## Critical Constraints - Blur peaks at peak speed and resolves to 0 at the settle — share the ease and window between position and envelope. A blur that lingers after the stop reads as a focus pull. - Entrances / mid-shot arrivals only — never a mid-composition exit; wrapper-level use only per the carve-out. - Seed `stdDeviation` at setup: at `PEAK_BLUR` for the entrance shape, at 0 for a whip / `inOut` leg. - Generous filter region (`x="-50%" y="-50%" width="200%" height="200%"`) or the smear clips at the element's box edge. - Directional axis: `"X 0"` horizontal, `"0 Y"` vertical, `"B B"` only for a depth/scale move — symmetric blur on a sideways move looks like defocus. - Dwell ≥1 s sharp after the snap; a streak landing at the last beat reads as "flashed and gone". - Heavy element on a solid field — thin type (< ~120px / 800 weight) or a busy backdrop swallows the smear. - `overflow: hidden` on the scene — the smear / furthest ghost extends past the resting position during travel. ## See also `kinetic-beat-slam` (streak as one beat's entrance) · `center-outward-expansion` (grid streak-in) · `scale-swap-transition` (same-footprint morph — not an arrival) · `nudge-curve` (the whip sweep's position half) · `3d-camera-flight` / `viewport-change` (the carve-out's wrappers).