Skill 18 · Hyperframes Animation
Subchapter 18.77
rules/depth-of-field-blur.mdMarkdown9 KBView on GitHub
Pulls the eye to one focal element by blurring (and slightly dimming) everything around it while the focal layer stays sharp — the camera’s depth-of-field falling off the background, or a rack-focus shifting which plane is in focus. and are paint-only, so both tween seek-safe. This is the backing rule for the focus-falloff beat the blueprints reach for: outer nodes blurring during a push-in (), rack-focus across a parallax card stack (), non-highlighted cards dimming to spotlight a hero metric ().
filteropacityconstellation-hubcursor-ui-demodataviz-countupEvery layer carries a --dof custom property (px of blur), read by filter: blur(var(--dof)), plus its own opacity. A GSAP tween advances each layer’s --dof from 0 to its target blur and its opacity from 1 to a dim level over the focus-shift window. The focal layer’s --dof stays 0. Per-layer targets derive from data-depth / index, so the falloff is identical on every seek.
Three mechanics, same primitive:
press-release-spring: A’s resting blur after the rack must equal what B held before it — author both as tweens on the same --dof at the same position so the hand-off is seamless.multi-phase-camera / coordinate-target-zoom): “the world recedes” and “we push in” read as one move.<div class="world" id="world">
<!-- Focal layer — stays sharp -->
<div class="layer focal" id="focal">{FocalLabel}</div>
<!-- Off-focus layers — blur + dim; data-depth orders near→far -->
<div class="layer ctx" data-depth="1">{Context A}</div>
<div class="layer ctx" data-depth="2">{Context B}</div>
<div class="layer ctx" data-depth="3">{Context C}</div>
</div>.world {
/* single wrapper so a concurrent camera push-in transforms everything
together; DoF is independent of the camera */
position: relative;
width: 100%;
height: 100%;
transform-origin: 50% 50%;
}
.layer {
--dof: 0px; /* px of blur; filter reads it — starts sharp */
filter: blur(var(--dof));
will-change: filter; /* promotes the layer so per-frame re-rasterization is cheap */
}
.focal {
z-index: 2; /* sharp layer must sit ABOVE the blurred ones, or its crisp
edges read as bleeding into the haze */
}
.ctx {
z-index: 1;
}// Mechanic 1 — FOCAL PULL. Blur scales with data-depth so far planes blur
// more than near ones; the focal layer (--dof: 0, opacity: 1) is untouched.
gsap.utils.toArray(".ctx").forEach((el) => {
const depth = Number(el.dataset.depth) || 1;
tl.to(
el,
{
"--dof": `${BLUR_PER_DEPTH * depth}px`,
opacity: DIM_LEVEL, // dim, not gone
duration: FOCUS_DUR,
ease: "power2.inOut",
},
FOCUS_START,
);
});gsap.set plane B pre-blurred BEFORE the rack (no pop), then two tweens sharing RACK_START + RACK_DUR: A → MAX_BLUR + DIM_LEVEL, B → 0px + 1. Shared window makes them cross at the midpoint.#world (scale/x/y, power2.inOut). Camera transforms the world; DoF tweens the layers — independent property channels, no conflict.gsap.utils.toArray(".card:not(.hero)") all defocus (GRID_BLUR + DIM_LEVEL) on one shared window; heroes are skipped.--dof back to 0px / opacity 1 over the tail (REFOCUS_START + REFOCUS_DUR ≤ DURATION).ease:"none" driver writes Math.max(0, Math.sin(p)) * FOCAL_BREATH_PX into the focal --dof during a hold. Keep it ≤ ~0.6px or it reads as “still focusing”; default to omitting it.| token | range | notes |
|---|---|---|
| BLUR_PER_DEPTH | 3–6 px per depth step | a 3-plane stack tops out ~9–18 px; low = gentle DoF, high = tilt-shift falloff |
| MAX_BLUR | 8 soft → 16 default → 24 heavy px | terminal blur for a fully-defocused plane; above ~24 px on a big surface, shrink/group the layer instead |
| GRID_BLUR | 6–12 px | pushes cards back without losing the grid’s shape |
| DIM_LEVEL | 0.4 strong → 0.55 default → 0.7 subtle | rarely below 0.35 — fully dark reads as “removed,” not “defocused” |
| FOCUS_DUR | 0.5–1.2 s | a rack/pull is a deliberate move, not a snap; shorter = snap focus, longer = languid |
| RACK_START / RACK_DUR | shared by both planes | gsap.set the pre-blurred plane BEFORE RACK_START |
| FOCAL_BREATH_PX | ≤ 0.6 px, period 2–3 s | barely-there nicety |
| FOCAL vs CTX sizing | context smaller / grouped | small context layers let a modest radius still read as “out of focus” — and blur cheaply |
Tokens: dark {bgGradient} so the sharp focal layer reads as lit and forward; heavy display {font} weight — blurred copy needs it to stay shape-legible.
--dof variable on the timeline — reading filter: blur(var(--dof)) keeps the blur on the HF seek clock.opacity dim to do the push-back work — dim + modest blur reads more like real DoF than blur cranked to the max.will-change: filter on every layer whose blur animates (drop it after settle if the layer also does heavy transform work).--dof: 0, untouched (or breathing ≤ 0.6 px). Any visible blur on the focal element kills the “this is the thing” read.--dof at the same position..world for the push-in; don’t fake DoF with the camera transform or vice-versa.--dof: 0 in the tail if the next beat is a crossfade/push; handing off mid-defocus reads as “the render glitched.”z-index).multi-phase-camera.md (the push-in this rule’s falloff accompanies) · coordinate-target-zoom.md (zoom onto the focal core — the constellation-hub hook) · viewport-change.md (pan + rack across a tilted card plane) · counting-dynamic-scale.md (hero metric counts up sharp — the dataviz-countup spotlight) · 3d-page-scroll.md (the parallax stack to rack between) · sine-wave-loop.md (post-rack idle; keep both amplitudes tiny).