Every SKILL.md already declared the new name — name: frames, frames-cli, frames-core, and so on — but the directories were never moved, so the tree carried frames-* in content and hyperframes-* on disk. Everything that resolves a skill by path missed: the core-skill list, the plugin marketplace pin, and the docs the CLI reads at runtime. Eight tests failed on main because the files they open do not exist. The directories now match what the files say they are, and the guide the claude-design contract reads takes the name that contract already used.
8.7 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| depth-of-field-blur | Selective-focus rack-focus — pull the eye to a focal element by GSAP-tweening filter blur (+ a small opacity dim) on the off-focus layers while the focal one stays sharp. Drive blur via a `--dof` CSS var; finite tweens, no CSS transition, deterministic. Covers single focal pull, rack-focus between two depth planes, and blur-the-cluster-while-pushing-in. |
|
Depth-of-Field Blur (Selective Focus / Rack Focus)
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. filter and opacity 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 (constellation-hub), rack-focus across a parallax card stack (cursor-ui-demo), non-highlighted cards dimming to spotlight a hero metric (dataviz-countup).
How It Works
Every 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:
- Focal pull — one window: off-focus layers go sharp(0) → blurred while the focal layer holds at 0. The eye is pulled to the only thing still crisp.
- Rack focus — two adjacent windows on the same property: plane A's blur ramps 0 → max at the same position plane B's ramps max → 0. State continuity matters exactly as in
press-release-spring: A's resting blur after the rack must equal what B held before it — author both as tweens on the same--dofat the same position so the hand-off is seamless. - Blur-the-cluster-while-pushing-in — the DoF tween runs at the SAME timeline position as a camera push-in (
multi-phase-camera/coordinate-target-zoom): "the world recedes" and "we push in" read as one move.
Recipe
<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,
);
});
Variations
- Rack focus between two depth planes —
gsap.setplane B pre-blurred BEFORE the rack (no pop), then two tweens sharingRACK_START+RACK_DUR: A →MAX_BLUR+DIM_LEVEL, B →0px+1. Shared window makes them cross at the midpoint. - Blur the cluster while pushing in — run the focal-pull tweens at the same position + duration as a camera tween on
#world(scale/x/y,power2.inOut). Camera transforms the world; DoF tweens the layers — independent property channels, no conflict. - Spotlight a hero metric in a card grid —
gsap.utils.toArray(".card:not(.hero)")all defocus (GRID_BLUR+DIM_LEVEL) on one shared window; heroes are skipped. - Refocus / settle — if the beat resolves back to "everything visible" (or hands off to a crossfade needing a clean outgoing frame), ramp all
--dofback to0px/ opacity 1 over the tail (REFOCUS_START + REFOCUS_DUR ≤ DURATION). - Bounded focus-breathing on the focal layer (optional) — a finite
ease:"none"driver writesMath.max(0, Math.sin(p)) * FOCAL_BREATH_PXinto the focal--dofduring a hold. Keep it ≤ ~0.6px or it reads as "still focusing"; default to omitting it.
Values
| 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.
Critical Constraints
- Tween the
--dofvariable on the timeline — readingfilter: blur(var(--dof))keeps the blur on the HF seek clock. - Blur the SMALL / GROUPED layers, not the giant one. Filter cost scales with radius × pixel area; a 20 px blur on a full-frame background is the worst case. Keep per-layer radius ≤ ~24 px on large surfaces and lean on the
opacitydim to do the push-back work — dim + modest blur reads more like real DoF than blur cranked to the max. will-change: filteron every layer whose blur animates (drop it after settle if the layer also does heavy transform work).- Focal layer stays genuinely sharp —
--dof: 0, untouched (or breathing ≤ 0.6 px). Any visible blur on the focal element kills the "this is the thing" read. - State continuity on a rack — the outgoing plane starts at the blur the incoming plane was holding, and vice-versa; adjacent tweens on the same
--dofat the same position. - DoF is independent of the camera — blur the layers, transform
.worldfor the push-in; don't fake DoF with the camera transform or vice-versa. - Settle sharp before a hand-off — refocus to
--dof: 0in the tail if the next beat is a crossfade/push; handing off mid-defocus reads as "the render glitched." - Sharp focal layer above blurred layers (
z-index).
See also
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).