diff --git a/skills-manifest.json b/skills-manifest.json index 83e8c6c0e..70c62b820 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -22,8 +22,8 @@ "files": 17 }, "hyperframes-animation": { - "hash": "7b3cd6bf281b1141", - "files": 104 + "hash": "b4adace47d4fa1b1", + "files": 121 }, "hyperframes-cli": { "hash": "b1a0725560016894", @@ -62,7 +62,7 @@ "files": 29 }, "product-launch-video": { - "hash": "10e0b80f7040ad1e", + "hash": "9557c9b86f4239b9", "files": 26 }, "remotion-to-hyperframes": { diff --git a/skills/hyperframes-animation/blueprints-index.md b/skills/hyperframes-animation/blueprints-index.md index 55409a4c8..864a68b75 100644 --- a/skills/hyperframes-animation/blueprints-index.md +++ b/skills/hyperframes-animation/blueprints-index.md @@ -2,12 +2,12 @@ > Entry point to the blueprint layer. Read this to find the shape for a frame; read `blueprints/.md` to instantiate it. The Step-4 method (Reproduce / Adapt / Compose, what to write per frame) lives in `visual-design.md` — this file is the menu + the picker. -A **blueprint** is a product-agnostic, **time-coded shot template** — `Scene N (a–b s): …` with `[slots]` and one named **signature move** — reverse-engineered from 50 golden product-launch clips (plus 13 hyperframes-animation blueprints reverse-translated to the same brief format). It encodes a whole shot across its full duration — reveals paced to the spoken line, not dumped at t=0 — so instantiating one structurally keeps content arriving instead of freezing. The full template lives in `blueprints/.md`. **Step 4 (visual design) instantiates one blueprint per frame** (or composes from the motion vocabulary when none fits). +A **blueprint** is a product-agnostic, **time-coded shot template** — `Scene N (a–b s): …` with `[slots]` and one named **signature move** — reverse-engineered from 178 golden product-launch clips across two mining rounds (plus 13 legacy blueprints reverse-translated to the same brief format). It encodes a whole shot across its full duration — reveals paced to the spoken line, not dumped at t=0 — so instantiating one structurally keeps content arriving instead of freezing. The full template lives in `blueprints/.md`. **Step 4 (visual design) instantiates one blueprint per frame** (or composes from the motion vocabulary when none fits). -## The 15 blueprints +## The 22 blueprints - + Flat, centered, bold-type shot where the **motion IS the words changing** — a fixed line swaps tokens in place by hard cut, or a statement builds across full-screen beats (each its own move) onto a spring-pop payoff. The workhorse (6 roles). Reach for it whenever the words carry the shot and there's no set, surface, or click. @@ -15,122 +15,176 @@ Flat, centered, bold-type shot where the **motion IS the words changing** — a A live text caret **types (and edits) a line as a human would**, then collapses it and pops a brand payoff, or holds it under a persistent mark while a sub-line types into the final CTA. Reach for it when "someone is typing this" should be the engine — a relatable typed pain → brand, or a standing logo + a typed CTA rail. - -Pre-placed labeled **stations on one oversized canvas, traversed by a single virtual camera** — repeated lateral/diagonal pans centering each station and revealing a callout, landing held on the last. Reach for it for a milestone timeline panned to "us," or a connected web of pain stations ending in a tangled knot. + +Pre-placed labeled **stations on one oversized canvas, traversed by a single virtual camera** — repeated lateral/diagonal pans centering each station and revealing a callout, landing held on the last. Reach for it for a milestone timeline panned to "us," a connected web of pain stations ending in a tangled knot, or a two-shot concept-decode strip bridged by one lateral pan into a live demo. - -Iconned **nodes spring into a ring around a center**, then resolve on the core — a camera push-IN (depth-of-field collapsing onto it) or a held hub mark with satellites orbiting it. Reach for it for "it connects everything / one hub" or "sits at the center of your stack." + +The real viewport camera is the STORYTELLER — a **multi-leg motivated journey** (dive in → a beat fires → travel to the consequence / reposition → landing push) across one continuous world. Two sub-shapes: **(A) action roundtrip** — dive to a panel, a click/send fires, the camera swoops to where the consequence renders as element motion; **(B) cursorless flight** — pure cinematic 3D flight (motion blur, DoF, tilt-to-flatten), no cursor anywhere. Reach for it when the camera's travel itself tells the cause→effect (or spectacle) story — not when it chases a cursor (cursor-ui-demo) or presents one hero device (device-surface-showcase). + + + +Open TIGHT on one full-bleed detail — a graphic macro or a small UI region — let micro-action play in close-up, then **ONE continuous decelerating zoom-out reveals the containing whole** (design-tool workspace / multi-pane agent workspace); the frame locks and element-level payoff carries on. The zoom-out IS the engine — the structural inverse of the push-in shapes; no zoom-in anywhere. Reach for it to open on a mystery detail that re-scopes into "this is where it lives," or to land a scale/breadth payoff ("that was one corner of everything it did"). + + + +Iconned **nodes spring into a ring around a center**, then resolve on the core — a camera push-IN (depth-of-field collapsing onto it), a held hub mark with satellites orbiting it, or a cursor click that COLLAPSES the orbit and springs the product demo out of it (CTA). Reach for it for "it connects everything / one hub" or "sits at the center of your stack." Third finisher (scatter-drift end card): no ring, no camera — ~20 icons pop in scattered frame-wide around a serif headline and drift slowly outward under a fully static frame. -N items (tiles / cards / logos / list-lines) **self-assemble in a staggered cascade** into a grid or vertical list and hold; an optional camera zoom-OUT reveals the array inside a vaster whole. Reach for it to enumerate breadth at once — a feature grid, an accumulating benefit list, or a logo wall. +N items (tiles / cards / logos / list-lines) **self-assemble in a staggered cascade** into a grid or vertical list and hold; an optional camera zoom-OUT reveals the array inside a vaster whole. Reach for it to enumerate breadth at once — a feature grid, an accumulating benefit list, a logo wall, a self-populating live data board, or a streaming field that clears to a payoff line. - -A brand mark / wordmark **builds itself from parts** (elements assemble/orbit, letters cascade, an outline draws on, or a camera pushes through negative space) and resolves into a centered lockup, optionally extended to a URL/CTA. Reach for it for a wordless premium brand sting, or a logo build leading into the final ask. + +A brand mark / wordmark **comes to exist on screen** — built from parts (elements assemble/orbit, letters cascade, an outline draws on, a camera pushes through negative space), spring-bloomed whole from zero on a cleared stage, morphed in one unbroken chain out of the preceding phrase, absorbed from a pixel streak, or already assembled and settling as satellites clear — and resolves into a centered lockup, optionally extended to a URL/CTA/end card. Reach for it for a wordless premium brand sting, a logo build leading into the final ask, or any brand-outro lockup beat. - -A visible custom **cursor drives a reconstructed app UI** through clicks/hovers/drags so the screen changes state shot-to-shot, while the camera chases each interaction. Reach for it for a first cursor-led look at the surface (Product_Intro) or one workflow demonstrated end-to-end onto the action button (Key_Feature). + +A visible custom **cursor drives a reconstructed app UI** through clicks/hovers/drags so the screen changes state shot-to-shot, while the camera chases each interaction (or holds a locked static stage while element swaps do the "camera work"). Reach for it for a first cursor-led look at the surface (Product_Intro), one workflow demonstrated end-to-end onto the action button (Key_Feature), an ambient multi-cursor canvas hook, or a demo|text|demo Benefits sandwich. - -A **device mockup or floating window held as hero** while its screens cycle through a real flow, presented by a camera ranging from a static hold to a continuous 3D push. Role-narrow (Key_Feature only) but mechanic-rich (static tour · floating-window push-scroll · 3D-hand demo). Reach for it to show a feature experienced *inside its real interface*. + +A **device mockup or floating window held as hero** while its screens cycle through a real flow, presented by a camera ranging from a static hold to a continuous 3D push. Mechanic-rich (static tour · floating-window push-scroll · 3D-hand demo · cursorless stepwise-flow · showcase-carousel); the stepwise-flow variant widens it to Product_Intro. Reach for it to show a feature experienced *inside its real interface*, or a product introduced by *completing its core loop*. - -Numbers and charts are the hero — a **count-up ring/number, trend chart, tilted stat grid** — traversed by a camera that pushes THROUGH (or scrolls across) them to land on one hero metric. Reach for it when the data carries the argument: quantify a worsening problem, or open confidently on "look at the result." + +The AI-era demo shot: a **prompt/query/command types into a real product input and the machine answers** — status theater into a streaming answer / action log / diff cards / chart / generated artifact (full loop), an instant result surface that gets re-queried (search / generated page / preview flip), or the clip cuts at the submit and the ask itself is the show (incl. the install-command CTA end card). Reach for it whenever the beat is "watch me ask, watch it answer" — the keyboard drives, not a clicked-through UI (that's cursor-ui-demo) and not bare typed typography (that's typewriter-reveal). - -The calm **breather/landing beat** — one clean title or single brand/proof card revealed with exactly ONE restrained move (slide-up crossfade, or wipe-away-to-reveal), then a still hold. Low motion is the payload, not a deficiency. Reach for it for a two-line value title, or a busy open wiped to a clean lockup + a "loved by N+ teams" stat. + +Agent work performed as **working-state theater** — a single trigger beat (menu pick, modal click, a scan already running) hands the frame to the machine: loaders spin and status phrases swap while it visibly works, then the receipt cascades in — a checklist/findings card whose rows arrive and CHECK OFF (badge flips, strikethroughs, severity pills), or a conversation thread building message-by-message to a camera push-in on the confirmation. Reach for it to dramatize an agent doing multi-step work where the state mutation IS the demo — no typed prompt, no cursor-driven workflow, no static enumeration. + + + +A bipartite stage — an inspector/editor **panel bound to a target surface** — where a cursor (or caret) continuously manipulates a control (value scrub, unit/codegen dropdown, easing-handle drag, inline retype) and the coupled surface **updates live in the same beat**; the camera holds or punch-and-returns but never loses the couple. Reach for it when the feature IS live editing/inspection — "change this, watch it change" — not a click-through workflow (that's `cursor-ui-demo`). + + + +The frame travels vertically along one LONG full-bleed content surface — an agent transcript, task feed, or analysis document (no device frame) — by camera pan or element scroll, **reading the generated work as evidence**; then ONE focal interaction (file-chip click, quote highlight, row expand) pivots into an artifact reveal (workspace zoom-out, spreadsheet scale-up onto highlighted cells, inline panel). Reach for it when "the AI did a lot of work → here's the deliverable" is the beat — the traversal is the proof, the artifact is the payoff. + + + +Numbers and charts are the hero — a **count-up ring/number, trend chart, tilted stat grid** — traversed by a camera that pushes THROUGH (or scrolls across) them to land on one hero metric. Reach for it when the data carries the argument: quantify a worsening problem, open confidently on "look at the result," cold-open on one exploding statistic, prove a feature with a dark cursor-scrubbed stat montage, or guest-star a single gauge count-up as one beat inside a type relay. + + + +The calm **breather/landing beat** — one clean title or single brand/proof card revealed with exactly ONE restrained move (slide-up crossfade, or wipe-away-to-reveal), then a still hold. Low motion is the payload, not a deficiency. Reach for it for a two-line value title, or a busy open wiped to a clean lockup + a "loved by N+ teams" stat. Also runs as a card CHAIN — 2–3 near-still monochrome cards seamed by instant hard cuts (CTA end-card stack) or blur-snap handoffs (Product_Intro title prelude), terminating on the held logo; chains run 2–3s per card, ~5.5–9.5s total. Two paired items of equal weight enter from opposite wings with **mirrored 3D "book-open" tilts** and hold side-by-side, then an inner-edge pill badge spring-pops on each to punctuate. Reach for it for an A/B or "X + Y together" — two complementary capabilities weighed at once (not >2 items, not sequential steps). - -Overwhelm by accumulation — recognizable surfaces assemble, density-marker icons scatter in, the center one **morphs into the viewer's own avatar**, then elements close in from all sides (surrounded, not zoomed-into). Reach for it when the pain is "you're buried in tools," ending on a claustrophobic crowd. + +Overwhelm by accumulation — recognizable surfaces assemble, density-marker icons scatter in, the center one **morphs into the viewer's own avatar**, then elements close in from all sides (surrounded, not zoomed-into). Reach for it when the pain is "you're buried in tools," ending on a claustrophobic crowd. Second resolution (clutter-shove-to-question): the accumulation runs under a slow zoom-out, then a push-in shoves the clutter to the frame edges and a two-part serif question builds in the opened center — camera-driven, no avatar. A typed lead-in + an accent word cycling through options, then a hero **crashes in from off-screen and physically shoves the text aside** — a collision, not a fade — settling alone. Reach for it when a "could be many things" build should be violently replaced by "this is it." + +One element stays PINNED — a wordmark, composer box, or anchor line that enters once and **never moves** — while the adjacent region (or the entire surrounding theme) cycles through many discrete states: hard-cut label swaps, a vertical carousel, per-word highlight stepping, or in-place theme morphs, cadence often manipulated (steady stepping or a slow→accelerating flurry), resolving on an emphasis beat into a completed lockup. Reach for it to assert breadth around one fixed identity — "everyone says / works everywhere / calling all X" — where the anchor's stillness IS the claim. + + A product video holds center and breathes, then **slides aside to hand its weight to a hero stat**, then both clear and kinetic text types into the vacated center, sealed by a gradient pill. Reach for it for "see the feature → see the impact" where the video must stay visible (slides, never cuts). - -A resting brand mark **condenses at the same center into a brighter CTA**, then a cursor arrives and lands a human-aimed click with feedback. Reach for it for a focused "click here" sign-off that walks the eye from identity to action — no spatial set, no multi-step UI. + +A resting brand mark **condenses at the same center into a brighter CTA**, then a cursor arrives and lands a human-aimed click with feedback. Reach for it for a focused "click here" sign-off that walks the eye from identity to action — no spatial set, no multi-step UI. Role-widened to Hook: the same machinery as an OPENER — a lone widget (pill/chip) on a flat field morphs in place (pill→menu, chip→prompt card), performs its payload, then vanishes to a typed closing title. ## Role → blueprint menu -A **SOFT** menu: story truth comes first. Story-design reaches in **when the product's own beat calls for that shape** — it suggests a proven shape, it never dictates which beats exist. Each role has 2–4 options; if none fits the beat, compose freely (the menu is not a checklist). Each line is the **trigger** that should make you reach for that blueprint. +A **SOFT** menu: story truth comes first. Story-design reaches in **when the product's own beat calls for that shape** — it suggests a proven shape, it never dictates which beats exist. Each role has 4–11 options; if none fits the beat, compose freely (the menu is not a checklist). Each line is the **trigger** that should make you reach for that blueprint. Roles here map 1:1 to the storyboard frame `type` enum: **Hook**=`hook` · **Problem**=`pain_point` · **Product_Intro**=`product_intro` · **Key_Feature**=`feature_showcase` · **Benefits**=`benefit_highlight` · **Social_Proof**=`social_proof` · **CTA**=`cta` · **Brand_Outro**=`branding`. **Hook** -- `kinetic-type-beats` — a punchy rhetorical line / "you keep doing X" callout where the in-place word-swap is the joke, OR an escalating multi-beat statement landing a spring-pop payoff. +- `kinetic-type-beats` — a punchy rhetorical line / "you keep doing X" callout where the in-place word-swap is the joke, an escalating multi-beat statement landing a spring-pop payoff, word beats resolving into a logo reveal, or a centered beat triptych (a beat may be a non-text element). - `typewriter-reveal` — type a relatable line, collapse it, pop the brand (logo or product-UI) — "here's the everyday pain, now here's us." - `spatial-pan-stations` — a timeline of milestones panned to the present ("evolution leading up to us"). - `constellation-hub` — a constellation of tools/nodes + a camera push-in ("it connects everything"). +- `cta-morph-press` — a lone widget on a flat field morphs in place (pill→menu, chip→prompt card), performs, then vanishes to a typed title ("one widget doing one thing" opener). - `ticker-takeover` — a cycling accent word ("could be X, or Y…") violently replaced by a hero crashing in from off-screen. +- `fixed-anchor-cycle` — a static lead line holds while an accent line carousels through an audience/option roll-call beneath it, then clears into statement beats landing the brand line. +- `prompt-type-submit-generate` — "watch me ask": a typed headline → one push-in onto the product's input → the prompt types and the clip ends at the submit; or the whole demo loop runs and a second command starts before the cut. +- `cursor-ui-demo` — an ambient multi-cursor workshop: labeled teammate cursors work a design canvas live (grab-drag-drop, recolor on drop) while a headline builds over the demo — the live workshop itself is the hook. +- `dataviz-countup` — a cold-open counter burst: icons puncture in clustered at center, one dramatic statistic explodes upward in size as the icons fling outward to their marks, closed by a slow lean-in. +- `zoom-out-workspace-reveal` — a full-bleed graphic mystery (blob / blossom / macro) resolved by one unbroken decelerating pull-back through nesting levels into the design-tool workspace that made it; canvas keeps animating after the lock. **Problem** -- `kinetic-type-beats` — 3–5 short pain statements, each landing alone on a bare canvas, no product yet. +- `kinetic-type-beats` — 3–5 short pain statements each landing alone on a bare canvas, or a question/hook phrase relay scale-popping through center (optionally resolving on a product surface as an element move). - `spatial-pan-stations` — pan a connected web of pain "stations" ending in a tangled knot. - `dataviz-countup` — a count-up ring / chart / stat grid pushed-through to dramatize a worsening or large problem. - `overwhelm-surround` — recognizable tools that morph into the viewer, then task bubbles close in from all sides ("you're buried"). **Product_Intro** -- `kinetic-type-beats` — "Introducing…" hard-cut name-drop resolving on the brand name/logo. +- `kinetic-type-beats` — "Introducing…" hard-cut name-drop resolving on the brand name/logo; also a fixed headline with one swapping word-slot, a word-by-word run with per-hero-word effect payoffs, or an anchored wordmark that transforms out. - `logo-assemble-lockup` — a wordless premium brand sting (elements pulse/orbit and assemble around the mark). - `cursor-ui-demo` — first look at the product surface; a cursor sweeps in to introduce the app. - `dataviz-countup` — hard-cut into a data-viz card grid, camera scrolls to a glowing hero metric + a kinetic tagline. - `video-text-pivot` — a product video that slides aside to hand its weight to a hero stat, then yields the center to kinetic impact text. +- `spatial-pan-stations` — a two-shot strip bridged by ONE lateral pan: a static phrase's accent word 3D-flap-decodes (the concept lands), then the camera pans with parallax into a live cursor-typing demo. +- `prompt-type-submit-generate` — the first look at the product is its composer or search bar — a long prompt (or short query) types with attachments / dropdown picks / live autocomplete, steering to the confirming control. +- `device-surface-showcase` — a cursorless end-to-end flow (setup/auth → action → success) completed inside the held surface, bookended by title cards. +- `titlecard-reveal` — a three-beat dark title prelude (logo pop → name+version append → tagline card) chained by blur-snap handoffs before any product UI. **Key_Feature** -- `grid-card-assemble` — a labeled feature tile/pill grid that self-assembles (or glass cards revealed by a camera zoom-out). +- `grid-card-assemble` — a labeled feature tile/pill grid that self-assembles (or glass cards revealed by a camera zoom-out; or a live-populating data board — skeleton fills, tethered cards, post-assembly status flips). - `cursor-ui-demo` — a specific multi-step workflow demonstrated end-to-end, landing on the action button/result. - `device-surface-showcase` — a device/window hero whose screens cycle (static tour · floating-window push-scroll · 3D-hand demo). - `comparison-split` — two paired capabilities side-by-side with mirrored book-open tilts (an A/B / "X + Y together"). - `video-text-pivot` — a feature clip that slides aside to a frame-filling metric, then a typographic impact line. +- `dataviz-countup` — dark-scrub-montage: kinetic headline beats cut-stitched with self-drawing charts and a cursor-scrubbed dashboard (`chart-scrub-readout`). +- `prompt-type-submit-generate` — the capability as one prompt→response round trip: submit into thinking states, then a streaming answer, action log, diff cards, chart, or instant generated artifact. +- `agent-progress-theater` — the feature is the agent WORKING (plan / scan / fix / automation): loader + status theater resolving into a checklist that checks off, or a thread that builds to a confirmation payoff. +- `panel-edit-live-sync` — an inspector/editor panel edit-syncs a bound target live (scrub → it rotates, retype → it resizes, pick → it converts); for features whose value prop is the live coupling itself. +- `camera-journey` — a cursorless cinematic 3D flight over the product surface — motion blur, depth-of-field, tilt-to-flatten — landing violently on the CTA / hero card (sub-shape B). +- `transcript-scroll-artifact-reveal` — a long transcript/feed/document traversed vertically as evidence of generated work, then one interaction pivots into the artifact ("it did all this → here's the deliverable"). **Benefits** -- `kinetic-type-beats` — a rapid-fire staccato montage of 8–12 short value phrases. -- `grid-card-assemble` — a vertical benefit list that accumulates or steps. +- `kinetic-type-beats` — a rapid-fire staccato montage of 8–12 short value phrases, or a slow 2–4-statement relay each held ~1.5s+. +- `grid-card-assemble` — a vertical benefit list that accumulates, steps, or streams past a focal slot, optionally clearing to a payoff line. - `titlecard-reveal` — a calm two-line value title card (a breather/stillness beat). +- `camera-journey` — a small action in one panel pays off in another region, and a real camera swoop physically connects cause to effect (sub-shape A). +- `zoom-out-workspace-reveal` — micro-actions in extreme close-up on one small UI region, then one fast decelerating zoom-out reveals the huge multi-pane agent workspace; the wide holds while the deliverable payoff completes. +- `fixed-anchor-cycle` — one product surface pinned dead-center while its whole theme re-skins per beat ("the same prompt, in every tool"). +- `cursor-ui-demo` — the demo|text|demo sandwich: two static-stage demo beats bridged through a full-screen kinetic/title interlude and back. **Social_Proof** - `constellation-hub` — product mark as the hub, partner logos orbiting it ("works with your stack"). - `grid-card-assemble` — a logo wall that builds then pulls back to reveal a vast ecosystem. - `titlecard-reveal` — wipe a busy open away to a clean brand lockup + a "loved by N+ teams" stat. +- `constellation-hub` — scatter-drift end card: ~20 app icons pop in frame-wide around a serif headline and drift outward under a static frame ("connects to thousands of apps"). +- `dataviz-countup` — one radial-gauge count-up instrument embedded as a single beat inside a kinetic-type relay. **CTA** -- `kinetic-type-beats` — a punchy closing line (or short value stack) snapping beat-by-beat onto the logo/URL. +- `kinetic-type-beats` — a punchy closing line (or short value stack) snapping beat-by-beat onto the logo/URL, or a 3–5-beat chain where each beat carries its own kinetic gag before the logo forms. - `logo-assemble-lockup` — a logo build → camera push-through into the final URL/CTA verb. - `cta-morph-press` — a brand mark that condenses into the CTA at one center, then a cursor lands a human-aimed click. +- `titlecard-reveal` — a monochrome end-card chain (statement → CTA line → wordmark/logo) seamed by instant hard cuts, ending on the logo held to the final frame. +- `constellation-hub` — orbit-collapse: category icons drift around an empty central CTA, a cursor click implodes the orbit toward the click point, and the product demo springs OUT of the collapse. +- `prompt-type-submit-generate` — the install-command end card: headline demotes, a terminal pill springs in, the command types and holds with a blinking cursor. **Brand_Outro** -- `kinetic-type-beats` — a rapid verb barrage resolving on the brand's one defining word. +- `kinetic-type-beats` — a rapid verb barrage resolving on the brand's one defining word, or a relaxed full-frame beat relay terminating in a long-held URL end card. - `typewriter-reveal` — a persistent brand mark with a typed/swapping CTA rail beneath it. - `logo-assemble-lockup` — feature/UI elements clear the stage and the lockup draws itself in. - `ticker-takeover` — options cycle, then the brand mark crashes in and owns the frame. +- `fixed-anchor-cycle` — the wordmark pins while praise quotes / tagline highlights cycle beside it (optionally accelerating), resolving into the finished lockup. -> Coverage: every role has ≥2 options; every blueprint serves ≥1 role. `kinetic-type-beats` is the workhorse (6 roles); `device-surface-showcase` is role-narrow (Key_Feature only) but mechanic-rich. Five shapes — `comparison-split`, `overwhelm-surround`, `ticker-takeover`, `video-text-pivot`, `cta-morph-press` — were added from the hyperframes-animation blueprints. +> Coverage: every role has ≥2 options; every blueprint serves ≥1 role. `kinetic-type-beats` is the workhorse (6 roles); `dataviz-countup` now spans 5; `device-surface-showcase` (once role-narrow) now also serves Product_Intro via the mined stepwise-flow variant. Five shapes — `comparison-split`, `overwhelm-surround`, `ticker-takeover`, `video-text-pivot`, `cta-morph-press` — were added from the hyperframes-animation blueprints; seven more — `prompt-type-submit-generate`, `agent-progress-theater`, `panel-edit-live-sync`, `camera-journey`, `transcript-scroll-artifact-reveal`, `zoom-out-workspace-reveal`, `fixed-anchor-cycle` — were mined from the golden-clip corpus. ## Picking guidance @@ -143,4 +197,6 @@ Roles here map 1:1 to the storyboard frame `type` enum: **Hook**=`hook` · **Pro Every recurring move in the golden vocabulary is backed by this skill's local `rules/` — including five added to round out the corpus: `depth-of-field-blur`, `motion-blur-streak`, `depth-scatter-assemble`, `spring-pop-entrance` (the canonical entrance pop, distinct from the click/press `press-release-spring`), and `ambient-glow-bloom`. Each blueprint's `rule mapping` cites the real rule. +Variant provenance (`from `) names the mined golden shape a variant was reverse-engineered from; the case-level golden map lives with the c2v-bench mining reports (maintainers only — consuming agents need only the shape names). + One genuine out-of-scope special remains: `device-surface-showcase`'s **3D-hand gesture-input + WebGL bloom/portal** needs R3F/Three.js + WebGL — a heavier capability than the rule library. Use it sparingly, or pick a simpler `device-surface-showcase` variant. diff --git a/skills/hyperframes-animation/blueprints/agent-progress-theater.md b/skills/hyperframes-animation/blueprints/agent-progress-theater.md new file mode 100644 index 000000000..90501e4b6 --- /dev/null +++ b/skills/hyperframes-animation/blueprints/agent-progress-theater.md @@ -0,0 +1,76 @@ +# agent-progress-theater — Agent Progress Theater + +**intent**: Agent work performed as WORKING-STATE theater — a short trigger beat hands the frame to the machine, which then visibly _works_: loaders spin, status phrases swap, dots pulse, counters tick — before the receipt arrives as a card whose rows cascade in and CHANGE STATE (badges flip to checks, labels strike through, severity pills read out), or as a conversation thread building message-by-message onto a camera push-in payoff. The subject is the machine performing labor over time. It is NOT a typed prompt awaiting output (no prompt/input is ever typed — the trigger is a click, a menu choice, or an already-running scan); NOT `cursor-ui-demo` (at most ONE igniting click here, then the cursor exits and the UI performs itself); NOT `grid-card-assemble` (rows there assemble into a static enumeration and hold — rows here are alive: they arrive as agent output and then MUTATE, checking off one by one while the viewer watches). + +**roles served** + +- Key_Feature (from `agent-progress-theater`): when the feature is the agent doing multi-step work (build a plan / scan a repo / fix a vulnerability / handle infra for you) and the proof is status theater — a loader lockup with a typed label, status couplets swapping under an `[accent]` spinner, then a checklist/findings card that populates and checks off in front of the viewer. +- Key_Feature (from `message-thread-payoff`): when the agent's work lives inside a conversation or automation thread — user/agent bubbles and tool-call/reply cards popping in sequence, the working state carried by pulsing loading dots or rapidly ticking diff counters, resolved by ONE camera push-in tight on the confirmation line (`[reaction pill]`, "Sent using `[@Bot]`", a thank-you bubble). + +**duration**: 4.2–11.6s (short members are a single card-and-check-off or thread beat at ~4–5s; long members chain trigger → interstitial → status swaps → receipt card at ~9–12s; thread payoff spans 4.2–9.1s) + +**shot structure** (a warm flat canvas — `[off-white / warm beige / near-white bg]`, optional `[faint grid / dot-grid / wavy-line]` texture; white rounded cards with soft drop shadows; ONE `[working accent]` color reserved for the machine (spinner, status words, active step) and one `[done color]` for completion (checks, "Completed"); camera static or ONE slow move — motion is overwhelmingly element-level springs, staggers, and state flips. Two folded sub-shapes — **(A) checklist/findings theater** and **(B) message-thread payoff**.) + +- **Scene 1 (0.0–~1.5s) — the trigger.** Something asks the machine to work, in ONE beat: + - _Variant — option menu (A)_: a centered white pill card poses `[the question]`; it SPRINGS open downward into a rounded menu — `[3–4 option rows]` fade/slide in staggered, each with a number badge. A cursor enters, hover-dances between rows (a pale `[hover fill]` highlight follows it), and CLICKS the chosen row (~press-down spring); the whole menu scales down toward its center and fades out. This is the only cursor appearance in the shot. + - _Variant — modal click (A)_: close-up of a white modal with `[Dismiss]` / `[action button]`; a hand cursor clicks the action (quick press-down spring); the modal fades away. Optionally followed by a serif `[interstitial line]` on the bare canvas — words land staggered, hold, fade out word-staggered as the bg swaps. + - _Variant — already working (A)_: a `[Scan in progress]`-style state — a thin `[accent]` arc spinner rotating over a heading + body copy + a `[Starting…]` pill (cursor resting on it, motionless); only the spinner moves. The whole scene then rapidly scales up and fades — a push-through exit. + - _Variant — workspace push-through (A)_: a rapid camera push-in THROUGH a multi-panel `[workspace: builder / editor / terminal]` — panels scale past the viewport edges and clear away to the bare canvas. + - _Variant — thread opener (B)_: a `[user bubble]` spring-pops in ("`[the ask]`"), OR a stats card pops in whose green/red `[diff counters]` rapidly tick and settle — the automation's opening receipt. + +- **Scene 2 (~1–4s) — the working state (the machine performs).** The frame belongs to the machine; nothing is clickable. Pick 1–3 working motifs and CHAIN them: + - A loader lockup: a spinning `[accent asterisk / arc]` beside a `[working label]` typed on rapidly ("`Buildi` → `Building plan…`"), a left→right shimmer sweep passing through the letters; the spinner may momentarily morph asterisk↔dot and back. + - Status couplets: 2–3 centered pairs — a dark `[action line]` over an `[accent status word]` ("Thinking…", "Noodling…") with its spinner — swapping via quick fades/slides at a steady cadence. + - A `[scan/tool label]` types/expands rightward to its full string, then SHRINKS and DOCKS to the top-left as a fixed corner header (the canvas now belongs to what it produces). + - A status heading flips tense as rows land beneath it ("Using `[Tool]`" → "Used `[Tool]`"), with a gently pulsing "Thinking" and gray meta-lines ("Exploring `[N]` files…") fading in below. + - _Variant — thread machinery (B)_: the `[agent reply]` fades/slides up, then a monospace `[tool_call]` line appears beneath it — small icon + `[tool name]` + three pulsing loading dots; OR an instruction bubble scrolls into view (internal window scroll, frame static) followed by a `[brand logo]` pop-in beside a "Sending message…" row. **The pulse dies the instant the result lands** — dots vanish as the payload arrives. + +- **Scene 3 (~2–4s) — the receipt cascades in (the payoff engine).** The work materializes as a card that BUILDS: + - _Variant — checklist (A)_: a white `[Progress / summary]` pill or card SPRING-pops in with a bounce, then springs open downward (or the summary card glides UP as a taller `[findings]` panel expands beneath it). Rows cascade in one by one — slide-up + fade, staggered — each with `[number badge / severity pill]` + `[label]` + optional gray `[meta line]`. Then the STATE MUTATION runs: badges flip one by one from numbered outline to a solid `[done color]` circle + white checkmark (slight scale bounce), the checked label simultaneously strikes through and dims; pending items keep partially-drawn arc outlines animating. End the run mid-list — some items checked, some still numbered — the work is visibly _ongoing_. + - _Variant — thread payload (B)_: the camera pushes in / pans down centering the `[tool_call]` line as a white payload card expands downward from it — 2–4 light monospace `[key: value]` lines fading in. Then the `[resolution message]` expands into place below (inline `[code chips]` and `[link]` coloring), OR a dark `[thread card]` scales up from a status row to DOMINATE the frame while the background darkens, its `[reply]` expanding into place under a "1 reply" divider. + +- **Scene 4 (final ~1–2.5s) — resolve.** Two endings: + - _Variant — hold / scroll (A)_: the finished (or mid-mutation) card stack holds static to the end, OR the viewport scrolls down the final card (fast in the last beat) revealing `[a second heading + numbered list]`, ending mid-list. A slow continuous zoom into the card may run underneath (the header drifts off the top of frame). + - _Variant — payoff push-in (B)_: ONE camera push-in + pan-down lands tight on the payoff line — "`Sent using [@Bot]`" / the confirmation + `[thank-you bubble]` spring-in — then a `[reaction button]` springs into an active pill with bouncy overshoot and a count. The push eases into a gentle near-imperceptible drift and the clip ends on the close-up. No end card. + +**motion vocabulary**: pill springs open downward into a menu/checklist · option rows fade/slide in staggered · cursor hover-dance (pale highlight fill follows the cursor between rows) · single igniting click with press-down spring · menu scale-down fade exit · modal fade-away · thin `[accent]` arc spinner rotation · spinning asterisk loader · asterisk↔dot morph · typed-on loader label with caret · left→right text shimmer sweep · serif interstitial with word-staggered fade in/out · status couplets swapping via quick fades/slides under an `[accent]` spinner · pulsing "Thinking" label · status heading tense flip (Using→Used) · label types/expands rightward then shrinks and docks as a corner header · scene scale-up/fade push-through exit · rapid camera push-in through a multi-panel workspace · slow continuous zoom into a card (header drifts off frame) · summary card spring pop with bounce · card glides up as a panel expands beneath it · anchored downward panel/payload expansion · rows stagger in (slide-up + fade) · badge flip from numbered outline to solid circle + white checkmark with scale bounce · strikethrough + dim on completion · partially-drawn arc outlines animating on pending items · severity-pill readouts (Critical / High) · viewport scroll down the final card · chat bubble spring scale-up pop-in · reply fade/slide-up · monospace tool-call line with three pulsing loading dots (dots die the instant the result lands) · payload card expands downward from the line · green/red diff counters rapid tick-and-settle · internal window scroll (frame static) · brand logo pop-in beside a status row · card scales up from a row to dominate the frame while the background darkens · reply message expands into place · inline code chips / link coloring · reaction button springs into an active pill with bouncy overshoot + count · camera push-in + pan-down centering the payoff · slight pull-back · gentle end drift · static hold. + +**rule mapping** + +- pill springs open downward into a menu / panel expands beneath a gliding card / payload card expands downward from a tool-call line → `anchored-layout-expand` (edge-anchored container growth: height-masked wrapper + inner counter-translate, container drawn at final size); spring flavor from `spring-pop-entrance` +- option rows / findings rows / task rows stagger in (slide-up + fade) → `spring-pop-entrance` (staggered-group form, ≤500ms cap) or `gsap-effects` (plain fade+translate stagger) — NOT `waterfall-entry` (its binary no-fade arrival law contradicts this dialect's soft fade/slide cascade) +- cursor glides to a row and clicks; hand cursor clicks the modal button → `cursor-click-ripple` (move + press) + `press-release-spring` (the button's press-down spring) +- pale hover-highlight fill following the cursor between rows → `gsap-effects` (a background fill translated row-to-row; no dedicated rule needed) +- menu scale-down fade exit / scene scale-up push-through exit / palette-for-window swap → `scale-swap-transition` +- thin arc spinner rotation / spinning asterisk loader → `svg-icon-enrichment` (rotating internal SVG parts via `setAttribute('transform','rotate(deg cx cy)')`; timeline-driven, finite) +- asterisk↔dot morph and back → `scale-swap-transition` (two elements morphing at the same center) +- typed-on loader label ("Building plan…") / scan label typing to its full string → `discrete-text-sequence` (+ `context-sensitive-cursor` for the caret) +- left→right shimmer sweep through the loader letters → `ambient-glow-bloom` (single-pass traveling sheen) or `css-marker-patterns` (highlight sweep) — pick sheen for light-on-text, marker for a drawn band +- serif interstitial word-staggered fade in/out; status couplets swapping on a cadence → `dynamic-content-sequencing` (phrase windows) + `discrete-text-sequence` (the whole-state swaps); per-word stagger via `gsap-effects` +- pulsing "Thinking" label / three pulsing loading dots (phase-offset) → `sine-wave-loop` (finite repeats; kill the tween at the resolve beat — see doctrine note) +- status heading tense flip (Using→Used) / gray meta-lines fading in / final-token snaps → `discrete-text-sequence` +- label shrinks and docks to the top-left as a fixed corner header → `gsap-effects` (plain scale + translate tween; no dedicated rule needed) +- rapid camera push-in through the multi-panel workspace → `viewport-change` (the push) + `multi-phase-camera` (phasing) + optional `motion-blur-streak` (velocity blur as panels clear the frame) +- slow continuous zoom into the receipt card (header drifts off top) → `multi-phase-camera` (steady-push phase) or `viewport-change` +- summary card / progress pill / chat bubble / brand logo / file chip spring pop-in → `spring-pop-entrance` +- summary card glides up as the findings panel expands beneath → `gsap-effects` (the glide) + `anchored-layout-expand` (the panel) +- badge flip: numbered outline → solid circle + white checkmark with scale bounce → `scale-swap-transition` (outline↔solid swap at same center) + `svg-path-draw` (checkmark draw-in) + `spring-pop-entrance` (the bounce); the pending→active→complete progression itself → `dynamic-content-sequencing` (a snap state machine, per cursor-ui-demo's workflow-approve-press precedent) +- strikethrough + dim on the checked label → `css-marker-patterns` (strike-through draw) + `gsap-effects` (opacity dim) +- partially-drawn arc outlines animating on pending items → `svg-path-draw` (partial dashoffset, held mid-draw) +- viewport scroll down the final card / internal window scroll under a static frame → `gsap-effects` (transform-only content translate inside a masked window) — use `viewport-change` only if the FRAME moves +- green/red diff counters rapid tick-and-settle → `counting-dynamic-scale` (numeric proxy count-up; suppress the scale-growth component — these tick at fixed size) +- dark thread card scales up from a row to dominate the frame → `card-morph-anchor` (row → full-frame morph + handoff) with the background darkening as a `gsap-effects` overlay fade +- reply message / resolution line expands into place → `spring-pop-entrance` (soft overshoot) or `anchored-layout-expand` for a true downward growth +- reaction button springs into an active pill with overshoot + count → `spring-pop-entrance` (the pop) + `press-release-spring` (activation flavor) + `counting-dynamic-scale` (the count, if it ticks) +- camera push-in + pan-down centering the tool call / the payoff line → `coordinate-target-zoom` (non-centered target: scale + counter-translate) or `viewport-change` +- slight pull-back then gentle end drift → `multi-phase-camera` (pull-back phase + continuous micro-drift; keep the drift near-imperceptible) +- static hold on the final stack → no rule needed + +**camera modifier** (default is a STATIC frame — the theater is element-level; at most ONE real move per shot, chosen from): + +- Trigger push-through: a rapid push-in through the opening workspace that clears to the bare canvas → `viewport-change` + `multi-phase-camera`, optional `motion-blur-streak`. +- Receipt zoom: one slow continuous zoom into the checklist card across the whole mutation run, letting the header drift off the top → `multi-phase-camera` (steady push). +- Payoff push-in (sub-shape B's defining move): static through the build, then ONE push-in + pan-down tightening onto the confirmation line, easing to a micro-drift end → `coordinate-target-zoom` / `viewport-change` + `multi-phase-camera` (drift). +- Everything else — swaps, cascades, check-offs, scrolls — happens on a locked frame (any "scroll" is the content translating inside its window, not the camera). + +**doctrine note (idle-motion ban)**: the working-state motifs (spinner rotation, pulsing dots, pulsing "Thinking") brush against motion-doctrine's idle-motion ban — here they are DIEGETIC: the pulse _performs_ "the machine is working" and is the narrative content of Scene 2, not decorative breathing. Keep every loop finite, timeline-driven, and seek-safe (`sine-wave-loop` finite repeats, `svg-icon-enrichment` rotation), and kill it at the exact frame the state resolves — the corpus does this explicitly (the loading dots vanish the instant the payload card expands; the spinner swaps out with the loader lockup). diff --git a/skills/hyperframes-animation/blueprints/camera-journey.md b/skills/hyperframes-animation/blueprints/camera-journey.md new file mode 100644 index 000000000..e1e4e62d0 --- /dev/null +++ b/skills/hyperframes-animation/blueprints/camera-journey.md @@ -0,0 +1,61 @@ +# camera-journey — Camera Journey + +**intent**: The real viewport camera is the STORYTELLER — a multi-leg journey (dive in → a mid-journey beat fires → travel to the consequence / reposition → landing push, at rest) across ONE continuous world, where the travel itself carries the narrative. Two folded sub-shapes: **(A) action roundtrip** — the camera dives into a UI panel, a cursor/typed action fires, and the camera swoops/pans to another region where the consequence renders as element motion; **(B) cursorless flight** — pure cinematic 3D flight (motion blur, depth-of-field, tilt-to-flatten rotations) over static or self-animating content, no cursor anywhere. + +**boundary**: This is NOT `cursor-ui-demo` — there the camera _chases_ the cursor (a servo following the actor); here the camera IS the actor, moving on its own narrative motivation, and in sub-shape A the cursor acts only at the leg hinge (in B it never appears). This is NOT `device-surface-showcase` — there one DEVICE/surface is hero and the camera merely presents it; here no single surface is hero — the journey traverses multiple regions/panels/depth planes and the traversal is the story. This is NOT `spatial-pan-stations` — there pre-placed stations on a flat canvas are visited by repeated pans of the same type; here the legs are heterogeneous (push-in, swoop, pull-back-rotate, whip, dive) and each leg is _motivated_ (by a fired action, or by the reveal it lands on). + +**roles served** + +- Benefits (from `camera-swoop-panel-action-roundtrip`): when the benefit IS a cause→effect round trip — "do this small thing here, get this big thing there" (comment → chart morphs; agent finding → verified commit; chat message → receipt + ledger). The camera physically connects the action to its payoff, so the viewer _travels_ the value chain instead of being told it. +- Key_Feature (from `cursorless-camera-flight`): when the feature should feel cinematic and inevitable — a payout form or a generated content-plan calendar explored by a flying camera (dives, whip sweeps, tilt-to-flatten, violent final push onto the CTA/hero card), the content acting by itself (a dropdown self-selects; keyword cards simply exist in depth) with no hand on the wheel. + +**duration**: 5.6–11.1s (sub-shape A 5.6–9.0s: 001 5.6s · 066 8.6s · 004 9.0s; sub-shape B 6.3–11.1s: Outrank 6.3s · 094 11.1s) + +**shot structure** (one oversized `[world]` — a `[UI canvas: design tool / GitHub + agent panels / phone + desktop ledger]` (A) or a `[3D-laid-out space: floating form card / calendar grid with standing keyword cards]` (B) — wrapped by a single virtual camera; content animates as elements _inside_ the world while the camera travels; every leg is a sequential tween on the same camera state) + +- **Scene 0 (optional, 0.0–~1.8s) — static prologue.** Camera locked on a `[prologue beat: static promo card with a floating 3D product card / typed headline with an accent word / wide establishing shot of the app]`. A typewriter line may finish (`[headline]` types on, accent word in `[accent color]`). The prologue BREAKS by a hard cut or by the headline shrinking and slipping away as the first dive begins — the stillness exists to make the journey's launch land. + +- **Scene 1 (~0.5–2.0s) — LEG 1: dive in.** The camera pushes in FAST and TIGHT onto `[the focal element]`: + - _Sub-shape A_: a flat whole-viewport push onto `[an actionable element: comment box / agent panel / chat bubble]` where `[typed text]` finishes typing or `[response text]` streams in. The header/context leaves the frame — commitment, not a polite zoom. + - _Sub-shape B_: the push lands at an ANGLE — a tilted 3D close-up of `[the form region / the calendar grid]`, foreground elements motion-blurred during the travel, neighbors soft under depth-of-field. A huge `[foreground prop: date number / field label]` may dominate the frame, blurred by speed. + +- **Scene 2 (~1.5–6.0s) — LEG 2: the mid-journey beat (the hinge).** The camera holds, drifts, or pulls slowly while the content ACTS: + - _Sub-shape A — the action fires_: a `[cursor]` clicks `[Send / Create PR]` (or a `[message]` sends implicitly) and the acted element CLEARS/vanishes. Optional theater before the click: a `[status spinner]` cycles `[status words]`, `[to-do items]` strike through, `[response text]` streams. The click is the hinge that _motivates_ the next leg. + - _Sub-shape B — the content self-acts_: a `[dropdown]` expands by itself (pushing `[the field below]` down), shows a `[row hover highlight]` with no cursor, and collapses with the new value selected; OR the flight decelerates INTO FOCUS on `[one card]` — its `[metrics]` sharp, neighboring cards blurred. + +- **Scene 3 (~4.0–8.0s) — LEG 3: travel to the consequence / reposition.** + - _Sub-shape A_: the camera pulls back / swoops / pans to `[region B]` while the CONSEQUENCE builds as element motion — `[bars shrink into the baseline while a node-dotted line draws left→right / a verified commit row slides into the timeline + a reaction pill pops / a receipt card expands row-by-row from a skeleton]`. An optional SECOND leg extends the trip: `[pan up-right to a toolbar → a dropdown cascades open / match cut into an extreme close-up → a fast decelerating zoom-out reveals a ledger table]`. + - _Sub-shape B_: a repositioning move — a slow pull-back that ROTATES the world flat and centered (3D → straight-on 2D), or a heavily motion-blurred WHIP SWEEP that resolves into a flat lateral pan across `[a month calendar / the full card]`. On the flat hold, quiet element beats may play: a thin `[focus outline]` fades in around one `[field]` and sweeps down to the next; the card keeps a near-imperceptible tilt/scale drift so the hold never dies. + +- **Scene 4 (final ~1–2s) — LEG 4: landing.** The journey resolves on the payoff: + - _Sub-shape A_: the camera comes to REST; the `[cursor]` hovers or drifts toward `[the payoff: an open Export menu item / the commit link / the View-transaction button]`; ends still, on the changed state — the world is visibly different from where the trip began. + - _Sub-shape B_: a sudden VIOLENT push-in/dive (motion-blurred) onto `[the CTA button scaled huge in frame / the hero keyword card]`, ending holding tight — or holding MID-DIVE (the last frames are still traveling; the flat overview is explicitly not the final image). + +**motion vocabulary**: whole-viewport camera push-in (fast/tight and slow/subtle); camera pull-back reframe; camera pan up/right/down; dive/swoop between stacked panels; fast decelerating zoom-out to rest; sudden violent push-in onto a button scaled huge; continuous 3D flight through a card grid; dive into an angled 3D close-up; slow pull-back that rotates/flattens the world to straight-on; heavily motion-blurred whip sweep; motion blur on camera travel; depth-of-field with blurred neighbors; decelerate-into-focus; hard cut / match cut into extreme close-up; near-imperceptible tilt/scale drift on holds; typed text finishing in an input; typewriter headline; headline shrinks and slips away as the camera dives; streaming AI response text; status-word spinner cycling labels; to-do strikethrough draw; cursor click; clicked element clears/vanishes; dropdown cascades open / self-expands and collapses with a row hover highlight (displacing the field below); bar-to-line chart morph (bars shrink into the baseline while a node-dotted line draws left→right, labels persist); commit row slide-in on a timeline; reaction pill appears; skeleton→content card build; receipt/label rows expand row-by-row; thin focus outline fades in and sweeps between fields; camera drift toward a button; 3D card subtle float; cursor hover at rest. + +**rule mapping** + +- the multi-leg camera itself — sequential push / pull-back / pan / dive phases on one wrapper, plus the micro-drift that keeps holds alive → `multi-phase-camera` (phase sequencing + drift) over `viewport-change` (the base virtual-camera primitive: single `.world` wrapper, one `cam {scale,x,y}` state — one source of truth for every leg) +- diving TIGHT onto an off-center element (comment box, chat bubble, Send button, one keyword card) → `coordinate-target-zoom` (scale + counter-translate; measure the target, don't hand-derive — a journey amplifies centering error on every leg) +- fast decelerating zoom-out from an extreme close-up to rest (066's ledger reveal) → `coordinate-target-zoom` zoom-out variation / `multi-phase-camera` (pull phase, hard `power4.out`) +- motion blur on camera travel (dive, whip sweep, violent final push) → `motion-blur-streak` (Camera-travel carve-out — the blur envelope rides the `.world` wrapper during a leg: the world never leaves frame, the blur peaks at peak velocity and resolves sharp at each landing) +- depth-of-field on neighbors while one card is in focus; decelerate-into-focus → `depth-of-field-blur` (focal pull + blur-the-cluster-while-pushing-in are explicitly in scope; run the DoF tween at the same position as the camera leg) +- the 3D flight itself (sub-shape B's core) — a perspective camera traveling with `rotateX/rotateY/translateZ` through a 3D-laid-out world: the dive into an angled calendar grid, the tilt-to-flatten pull-back (angled 3D → straight-on 2D), the continuous flight between standing cards → `3d-camera-flight` (perspective wrapper + preserve-3d; the 2D camera rules keep owning any flat legs) +- whip sweep → composition: `nudge-curve` (burst-dominant tuning of the slow-fast-slow slide, applied to the world) + `motion-blur-streak` (camera-travel carve-out) on the same window +- typed text finishing in an input; typewriter headline; streaming AI response text; status spinner cycling `[status words]`; skeleton→content state swap → `discrete-text-sequence` (+ `gsap-effects` typewriter; `context-sensitive-cursor` for the input caret) +- which content appears per leg / receipt rows and findings arriving on script windows → `dynamic-content-sequencing` +- cursor click on `[Send / Create PR]` (sub-shape A's hinge) → `cursor-click-ripple` + `press-release-spring` (or `physics-press-reaction` for a weightier press) +- clicked element clears/vanishes; panel state A → B on the return leg → `scale-swap-transition` / `card-morph-anchor` +- to-do strikethrough draw; row hover highlight → `css-marker-patterns` (strike-through) · `asr-keyword-glow` (accent glow on the hovered/selected row) +- bar-to-line chart morph → composite, decomposes cleanly: `stat-bars-and-fills` (bars `scaleY` → baseline) + `svg-path-draw` (node-dotted line draws left→right) at the same timeline position — no single rule names the coordinated chart-type morph, but no new rule needed +- commit row slide-in; reaction pill appears; receipt rows expand row-by-row → `spring-pop-entrance` (single arrivals) / `waterfall-entry` (the row-by-row cascade) +- dropdown self-expands, displacing the field below (094) → `anchored-layout-expand` (the masked edge-anchored expansion of the dropdown body — never tween `height`) + `reactive-displacement` (the expansion tween drives the sibling's displacement) +- focus-ring travel between fields (094: a thin outline fades in on `From`, then sweeps down onto `Amount`) → `ai-tracking-box` restyled as a plain outline (offsets baked at setup; size morphed via scale, never width/height) +- 3D card subtle float; near-imperceptible tilt/scale drift on holds → `sine-wave-loop` (+ `multi-phase-camera`'s drift for the camera-side micro-motion; the _tilt_ component of the drift belongs to `3d-camera-flight`) +- camera drift toward a button; slow subtle zoom-ins riding a hold → `multi-phase-camera` (steady-push mode, tiny spread) + +**camera grammar** (the defining layer — this blueprint IS its camera): every leg is a tween on ONE camera state (`viewport-change`'s single `.world` wrapper / `cam` object), sequenced by `multi-phase-camera`, aimed by `coordinate-target-zoom`. Legs must be _motivated_: sub-shape A moves because an action fired (click → swoop to the consequence); sub-shape B moves because the next reveal demands it (dive → focus → reposition → final dive). Vary the leg verbs — a journey of four identical pushes reads as a slideshow. Ease law: hard `out`-family on dives and landings (`power4.out` — violent arrival, sharp settle), `power2.inOut` on repositioning legs; spring/back easing on a camera feels wrong (per `multi-phase-camera`). Sub-shape B layers `3d-camera-flight`'s perspective wrapper under the same single-state discipline. + +**Seek-safety (non-negotiable for this much camera):** the entire journey — every leg, every blur envelope, every DoF pull — lives on the ONE paused GSAP timeline, so any frame seek reproduces the exact mid-leg camera pose. One camera state object, transform composed in a single writer (`applyCamera()`), no CSS `transition` anywhere near the wrapper, blur via proxy-tweened attributes / `--dof` vars (both seek-safe), and ending mid-dive is fine — a seek to the last frame just lands mid-tween. Per-leg targets are measured ONCE at setup (after `fonts.ready`) and baked; never `getBoundingClientRect` in `onUpdate`. + +**Overflow (required for a clean `check`):** a traveling camera deliberately moves world content past the frame edges on every leg. Keep `overflow: hidden` on the scene root AND mark the moving `.world` wrapper with `data-layout-allow-overflow` — otherwise `check` reports `text_box_overflow` / `container_overflow` for every panel the journey leaves behind (see the same note on `device-surface-showcase`). diff --git a/skills/hyperframes-animation/blueprints/constellation-hub.md b/skills/hyperframes-animation/blueprints/constellation-hub.md index 0d472861a..1234152fd 100644 --- a/skills/hyperframes-animation/blueprints/constellation-hub.md +++ b/skills/hyperframes-animation/blueprints/constellation-hub.md @@ -8,8 +8,13 @@ - Social_Proof (from `social-proof-orbit-ecosystem`): the product brand mark lands as the center hub and partner logos spring onto a ring and revolve around it — "plugs into / sits at the center of your stack." - CTA (from `cta-orbit-collapse`): the ring resolves by COLLAPSE rather than a push-in — category icons drift around an empty central CTA, a cursor click implodes the orbit toward the click point, and the product demo springs OUT of that collapse as the answer (scope → choice → consequence → product). - Social_Proof (from `proof-logo-chain`): a persistent center logo accrues proofs — its wordmark decodes, a claim ticker swaps, the logo glides to center, then avatars cascade into orbit with drawn connectors while partner logos scroll the bottom strip; four claims read as one statement. +- Social_Proof (from `scatter-drift-finisher`): the ecosystem beat as a + static END CARD — a two-line serif `[headline]` is the center (no hub mark, no ring), `[~20 app +icons]` pop in scattered frame-wide in a quick stagger, then keep drifting very slowly OUTWARD + to the end. "Connects to thousands of apps" said with count and spread, not geometry. -**duration**: 5–8s (Hook 5–6s · Social_Proof 5–8s · CTA orbit-collapse ~6s) +**duration**: 5–8s (Hook 5–6s · Social_Proof 5–8s · CTA orbit-collapse ~6s · Social_Proof +scatter-drift end card ~2.5s as a closing beat) **shot structure** @@ -22,8 +27,15 @@ Consolidated template — nodes ring a center, then one of two finishers resolve - Variant — Hook (push-in finisher): from Scene 3, a continuous smooth CAMERA PUSH-IN toward the center inner cluster — inner nodes scale up and stay sharp while outer nodes are pushed toward the edges and progressively BLUR (depth-of-field), background scales up smoothly; holds magnified on the core. - Variant — Social_Proof (orbit finisher): the center `[brand mark]` snaps in via a quick 3D rotate that decelerates and settles; a thin `[accent]` orbit ring draws around it; `[N partner badges]` spring onto the ring (staggered overshoot) and revolve CLOCKWISE while staying upright, under a continuous slow camera ZOOM-OUT (ecosystem reveal). - Variant — Social_Proof (optional type-push-through opener, prepended before Scene 1): centered `[headline]` types/slides in with a huge transparent-fill OUTLINE copy of the same words behind it; the outline text scales up exponentially toward camera (high-speed dolly / push-through), breaches the frame, then HARD-CUTS to the hub bg of Scene 1. +- Variant — Social_Proof (scatter-drift finisher, no ring): the center is a two-line serif + `[headline]` building in place (not a mark); `[~20 app icons]` pop in SCATTERED across the whole + frame in a quick stagger — no ring geometry, no connectors — then sustain a very slow outward + drift to the end. Camera fully static: no push-in, no zoom-out; the "everything around one + center" reads from the drift vectors pointing away from the headline. Often chained as the end + card of a preceding UI beat (the prior card dissolves into it). -**motion vocabulary**: staggered elastic spring-pop node entrances (~1.15 overshoot); slow gradient-blob drift; connector-line / orbit-ring draw-on; 3D snap-rotate-settle on the hub mark; continuous camera push-in (inner sharp, outer depth-of-field blur, bg scale-up); clockwise orbital revolve of upright badges; continuous slow camera zoom-out (ecosystem reveal); optional outline-text push-through dolly entry. +**motion vocabulary**: staggered elastic spring-pop node entrances (~1.15 overshoot); slow gradient-blob drift; connector-line / orbit-ring draw-on; 3D snap-rotate-settle on the hub mark; continuous camera push-in (inner sharp, outer depth-of-field blur, bg scale-up); clockwise orbital revolve of upright badges; continuous slow camera zoom-out (ecosystem reveal); optional outline-text push-through dolly entry. Scatter-drift finisher: frame-wide scattered icon pop-in (staggered, no ring); sustained slow +outward icon drift; in-place two-line serif headline build; static-frame hold to the end. **rule mapping** (motion verb → `rules/.md`) @@ -40,5 +52,12 @@ Consolidated template — nodes ring a center, then one of two finishers resolve - continuous slow zoom-out (ecosystem reveal) → `multi-phase-camera` (pull-back) / `coordinate-target-zoom` - outline-text push-through dolly opener (Social_Proof) → `3d-text-depth-layers` (outline copy behind) + `multi-phase-camera` (push-through) - depth-of-field blur on outer nodes during push-in → `depth-of-field-blur` (progressive DOF/focus-falloff blur on the off-center outer nodes while the inner core stays sharp) +- frame-wide scattered icon pop-in (no ring) → `spring-pop-entrance` (staggered group) + + `gsap-effects` (stagger recipe); positions pre-baked scattered — NOT `avatar-cloud-network`'s + elliptical ring +- sustained slow outward icon drift → `center-outward-expansion` (outward vectors, slow sustained + register — drift targets sit slightly past the pop-in positions) +- in-place serif headline build → `gsap-effects` (staggered line/word reveal) -**camera modifier**: push-in-with-DOF (Hook) — `multi-phase-camera` PUSH-in targeted via `coordinate-target-zoom` onto the core; the focus-falloff blur half of it is backed by `depth-of-field-blur`. Orbit finisher (Social_Proof) — slow continuous zoom-out via `multi-phase-camera` (pull-back) while satellites revolve. +**camera modifier**: push-in-with-DOF (Hook) — `multi-phase-camera` PUSH-in targeted via `coordinate-target-zoom` onto the core; the focus-falloff blur half of it is backed by `depth-of-field-blur`. Orbit finisher (Social_Proof) — slow continuous zoom-out via `multi-phase-camera` (pull-back) while satellites revolve. Scatter-drift finisher (Social_Proof end card) — none: the frame never moves; the outward drift +is element-level. diff --git a/skills/hyperframes-animation/blueprints/cta-morph-press.md b/skills/hyperframes-animation/blueprints/cta-morph-press.md index 98856763e..cc791c934 100644 --- a/skills/hyperframes-animation/blueprints/cta-morph-press.md +++ b/skills/hyperframes-animation/blueprints/cta-morph-press.md @@ -5,8 +5,16 @@ **roles served** - CTA (from `cta-morph-press`): when the close moves from brand identity to a single user action, two elements share the same center sequentially (a morph, not a cut), and the payoff is a simulated click with physical feedback. Reach for it for a focused "click here" sign-off — no spatial set, no multi-step UI (that's `cursor-ui-demo`). +- Hook (ROLE-WIDENED, from `widget-morph-on-blank-field`): the same + machinery run as an OPENER — a lone `[widget]` (pill / chip lockup) on a flat field transforms + in place, performs its payload, then vanishes to a plain frame that a typed `[title]` resolves. + The click, when present, ignites the morph rather than closing it; there may be no cursor at + all. Reach for it when the product hook IS one widget doing one thing — still no spatial set, + no multi-step UI (that's `cursor-ui-demo`). Mint-reconsideration trigger: if future mining + brings 2+ more widget-morph openers with the vanish → typed-title resolve, promote this variant + to its own blueprint (the beat order is fully inverted by then). -**duration**: 4–6s +**duration**: 4–6s (Hook widget-morph opener 5–7.5s) **shot structure** (a `[bg]` canvas; hero and CTA are flex-centered siblings sharing one `transform-origin`) @@ -14,8 +22,23 @@ - **Scene 2 (~1.4–2.4s) — the morph (signature move).** The hero CONDENSES at the same screen center into a smaller, brighter `[CTA]` (button / card): the outgoing mark shrink-fades exactly as the CTA scales up in its place. Because they share one `transform-origin`, the eye reads it as one element transforming, not a swap. - **Scene 3 (~2.4–3.4s) — approach.** A `[cursor]` arrives from off-stage on a **decelerating** path (it "arrives," it does not pass through) and lands a few px **off** the CTA's geometric center, so the aim reads human, not scripted. - **Scene 4 (~3.4–end) — press.** The cursor lands a physical CLICK — cursor and CTA compress together in lockstep, then release with feedback (an optional ripple / glow bloom). Holds on the clicked state. +- **Variant — Hook (widget-morph opener)** (from `widget-morph-on-blank-field`; + reorders the beats — press first, morph second, title last). **(1) presence**: a lone + `[pill / chip lockup]` sits centered on a flat `[field]`; optionally the `[cursor]` glides in, a + hover pill-background appears behind the chip, and the click lands with the same lockstep press. + **(2) the morph**: the widget transforms IN PLACE — expands downward anchored at its top edge + into a `[menu]`, or spring-morphs outward into a `[prompt card]` with a small overshoot settle — + new content fades/slides into place. **(3) payload**: the transformed state performs — + `[placeholder]` types with a blinking caret, `[user text]` types while a control flips from + muted to its vibrant active color, or the menu snap-collapses back to the pill carrying the + `[new value]` + a checkmark pop; the background may snap to a new color under the persistent + foreground card. **(4) resolve**: the widget VANISHES; a plain frame closes the beat — a + `[closing title]` types on center, or a hold on the flipped solid. -**motion vocabulary**: faint rotation-only resting breath (logo scope only); same-center morph-swap (shrink-fade ↔ scale-up sharing `transform-origin`); cursor decel-arrival from off-stage; off-center human aim; lockstep press compression; release feedback ripple / glow. +**motion vocabulary**: faint rotation-only resting breath (logo scope only); same-center morph-swap (shrink-fade ↔ scale-up sharing `transform-origin`); cursor decel-arrival from off-stage; off-center human aim; lockstep press compression; release feedback ripple / glow. Hook opener: anchored downward expand of a pill into a menu and springy snap-collapse back; +chip-to-card spring morph with overshoot settle; placeholder / user-text typewriter with blinking +caret (may cut mid-word); control color-state flip muted → vibrant; background color snap under a +persistent foreground card; checkmark pop; widget vanish to blank frame; typed closing title. **rule mapping** @@ -24,5 +47,17 @@ - cursor press + release in lockstep (single-target-array so both compress together) → `physics-press-reaction` (PRESS_DOWN + RELEASE portion) - cursor approach (decel from off-stage, off-center landing, hard-cut opacity in) → `gsap-effects` (translate on `power2.out`) - click ripple / release glow → `cursor-click-ripple` (attack-decay ring) and/or `ambient-glow-bloom` (release bloom) +- (Hook) chip → prompt-card spring morph at one center → `scale-swap-transition` (the base morph + contract, run in the expand direction) + `card-morph-anchor` (corner-radius / surface ride-along) +- (Hook) anchored-edge expand / snap-collapse (pill ↔ menu, top edge pinned) → + `anchored-layout-expand` (edge-anchored directional container growth — origin-pinned expansion + with counter-scaled children; `card-morph-anchor` stays for uniform-scale morphs only) +- (Hook) placeholder + user typing, blinking caret, mid-word cut → `gsap-effects` (typewriter) + + `context-sensitive-cursor` (blink) + `discrete-text-sequence` (mid-word cut states) +- (Hook) control color flip muted → vibrant → `press-release-spring` (color-transition variation) +- (Hook) checkmark pop / card-arrival overshoot → `spring-pop-entrance` +- (Hook) hover pill-background + igniting click → the base's `physics-press-reaction` + + `cursor-click-ripple` mappings apply unchanged -**camera modifier**: camera-static — the morph and click happen in element space; a camera move would compete with the click as the climax. +**camera modifier**: camera-static — the morph and click happen in element space; a camera move would compete with the click as the climax. The Hook opener keeps the same contract — even the background color flip is an element-level +snap, not a camera event. diff --git a/skills/hyperframes-animation/blueprints/cursor-ui-demo.md b/skills/hyperframes-animation/blueprints/cursor-ui-demo.md index 7ca0332b2..d47a66779 100644 --- a/skills/hyperframes-animation/blueprints/cursor-ui-demo.md +++ b/skills/hyperframes-animation/blueprints/cursor-ui-demo.md @@ -4,27 +4,41 @@ **roles served** -- Product*Intro (from `product-intro-cursor-ui-demo` / #14 Product_Intro_02, #15 Product_Intro_2, #17 Product_Intro_04): first look at the product surface — the cursor sweeps/hovers to \_introduce* the app and reveal what it is, landing on a hovered hero element or freshly-popped result. Light, exploratory; backdrop steps colors as it goes. -- Key*Feature (from `key-feature-cursor-ui-demo` / #23 Key_Feature_2, #24 Key_Feature_03, #27 Key_Feature_06): one specific multi-step workflow demonstrated \_end-to-end* (edit / configure / select across 2–4 discrete beats), each beat a real edit the UI responds to live, landing locked on the primary action button or the produced result. +- Product_Intro (from `product-intro-cursor-ui-demo`): first look at the product surface — the cursor sweeps/hovers to \_introduce\* the app and reveal what it is, landing on a hovered hero element or freshly-popped result. Light, exploratory; backdrop steps colors as it goes. +- Key_Feature (from `key-feature-cursor-ui-demo`): one specific multi-step workflow demonstrated \_end-to-end\* (edit / configure / select across 2–4 discrete beats), each beat a real edit the UI responds to live, landing locked on the primary action button or the produced result. - Key_Feature (from `workflow-approve-press`): an agency / confirmation workflow framed by a cockpit of 3D-tilted flanks — a step list ticks pending → active → complete (a snap state machine, CSS responding to `[data-state]`), and a flank button takes the PRESS as the payoff (its color flips to success, a checkmark stamps). The click is the climax, not a passing gesture. +- Key_Feature (from `cursor-app-state-tour`): the static-stage STATE TOUR — the cursor drives a reconstructed app through 2–4 discrete feature states on a LOCKED frame; every scene change is a click-triggered element swap/scale (modal springs from center, side panel slides in from the right edge, settings hard-swap, table populates, node-graph builds), never a real camera move; optional `[title card]` Scene 0 in front and a `[brand end beat]` behind. +- Key_Feature (from `drag-field-onto-document`): the DRAG-DROP journey — one continuous zoom-breathing shot of a document workspace: the cursor drags a ghosted `[field chip]` from an inputs sidebar onto the page, drop-snaps it into a placed field, a modal/typing beat completes it, and the placed element is adjusted in close-up before the cursor heads to the `[Finish/CTA]`. +- Product_Intro: the low-event BROWSE — the cursor roams ONE clean page state and the filter controls answer with slight hover updates; no typed input, no title beats, and the shot may end mid-roam. +- Product_Intro (from `hover-inspect-run`): the HOVER-INSPECT run — a click SPAWNS a labeled `[toolbar]`, the camera zooms out from a tight crop to the full page, then the cursor sweeps `[page elements]` while a floating `[inspector panel]` TRACKS the cursor, outline-highlighting and content-snapping per hovered element. (The slice's three-beat dark title prelude, scenes 1–3, belongs to `titlecard-reveal`, not here.) +- Hook: the ambient MULTI-CURSOR canvas — several labeled `[teammate cursors]` work a design canvas simultaneously (grab-drag-drop of components between mockups, recolor/identity swaps on drop) while the canvas group translate-PANS within a static frame and a `[headline]` builds word-group by word-group over the demo; the live workshop itself is the hook. One continuous beat, no cuts, no camera. +- Benefits (from `ui-demo-text-interlude-ui-demo`): the demo|text|demo SANDWICH — two static-stage demo beats of this blueprint bridge through a full-screen kinetic/title interlude and back (cursor acts, UI answers, all "zoom" element scale); the interlude beat is `kinetic-type-beats` material, and the sandwich itself is sequencing above the single-shot unit. -**duration**: 4.0–9.3s (union of Key_Feature 4.0–7.3s and Product_Intro 6.1–9.3s) +**duration**: 4.0–12.9s (Key_Feature 4.0–12.9s — the mined state tours run long, 10.4–12.9s, and the drag-drop journeys 9.8–10.6s, against the original 4.0–7.3s set; Product_Intro 4.5–9.3s — the low-event browse sets the 4.5s floor; Hook ~6.5s; the Benefits demo|text|demo sandwich totals 11.6–12.8s with each demo half ~4–5s) -**shot structure** (a `[product UI surface]` — fixed app window, dashboard/editor, parallax `[content card]` stack, or a `[container object/icon]` — centered over `[bg color/gradient]`, shown `[flat]` or `[3D-isometric]`; a custom `[brand-colored cursor with icon]` is the protagonist and the camera servos to whatever it touches; UI responds _live_ and in sync with each cursor action. Two role-tuned tempos fold in — Product_Intro **sweeps to introduce**, Key_Feature **performs a workflow**.) +**shot structure** (a `[product UI surface]` — fixed app window, dashboard/editor, parallax `[content card]` stack, or a `[container object/icon]` — centered over `[bg color/gradient]`, shown `[flat]` or `[3D-isometric]`; a custom `[brand-colored cursor with icon]` is the protagonist and the camera servos to whatever it touches; UI responds _live_ and in sync with each cursor action. Two role-tuned tempos fold in — Product_Intro **sweeps to introduce**, Key_Feature **performs a workflow** — and the camera spans a spectrum: the full CHASE, one continuous zoom-breathe, or a fully LOCKED static stage where the UI itself does all the moving.) - **Scene 1 (0.0–~Xs) — surface establishes + first touch.** The `[product UI surface]` arrives centered over `[bg color/gradient]` — either it is simply present (fixed window / dashboard / editor), a 3D-parallax stack of `[content cards]`, or a `[container object/icon]` that FLIES IN with a 3D tumble and settles. The custom `[cursor]` enters. The cursor performs the FIRST action on `[cursor target 1]` and the UI responds live in the same beat. Camera holds or begins a slow push-in toward the acted-on region. - _Variant — Product_Intro_: low-commitment first touch — cursor HOVERS/sweeps a control or SWEEP-HIGHLIGHTS a field to `[accent color]`, OR the `[container]` fans open. An optional label/title fades/morphs onto the surface. The point is to _show the surface exists_ and is touchable. - _Variant — Key_Feature_: a concrete edit — cursor DRAGS a scrollbar / TYPES into a field / DRAGS a handle, and the UI responds materially (`[scroll]` / value climbs / region resizes). If the surface opened in `[3D-isometric]`, it may snap perspective-FLAT here to read the workflow. + - _Variant — Key_Feature (static-stage tour)_: an optional Scene 0 — `[title card / kinetic brand word]` on a flat field — hard-cuts or window-SCALES-UP into the surface; the `[app UI]` is fully present from the first frame and the cursor enters and glides to the first control. The camera is LOCKED from the start and stays locked. + - _Variant — Hook (ambient multi-cursor)_: no single protagonist — several labeled `[teammate cursors]` are already at work across `[N mockups]` on a design canvas; the canvas group translate-PANS within the static frame while a `[headline]` builds word-group by word-group over the top. One continuous beat, no cuts. - **Scene 2 (~Xs–~Ys) — camera chases to the next interaction (the engine).** The camera MOVES to the next target — push-in + pan / whip-pan / pan-down to `[cursor target k]` — and the cursor performs action k as the UI updates live. Each beat is a discrete interaction connected by a fast camera move; the surface's inner content SWAPS per interaction. - _Variant — Product_Intro_: navigation is exploratory — a slow camera pan + depth-of-field FOCUS-PULL across a parallax `[content card]` stack, or the `[container]` fanning into `[N option/content cards]` that SPRING to position. As content swaps, the supporting backdrop STEPS its color (`[bg step 1]` → step 2 → …). Typically one or two such moves. - _Variant — Key_Feature_: repeat for `[2–4 beats total]`, each a distinct operation the UI answers — counter COUNTS UP, `[pill/swatch]` SELECTS, a modal SLIDES UP and TYPES — connected by whip-pans / progressive zoom. The workflow visibly advances toward a result. + - _Variant — Key_Feature (static-stage tour)_: the camera never moves — every beat is a click-triggered ELEMENT response: a modal SPRINGS/scales up from center, a `[side detail panel]` SLIDES in from the right edge (a second panel may slide over the first), hamburger→sidebar slide-open, a settings panel HARD-swaps its content, a dropdown fills, a `[table]` populates row-by-row, a formula types into a cell and the range populates on enter, a type-to-filter list live-collapses, a `[block]` pops into the canvas, a node-graph BUILDS (cards + connecting lines radiate from center), a hover drops a `[popover]` below a tag. Any "zoom" is element scale of the UI only. + - _Variant — Key_Feature (drag-drop)_: the cursor GRABS a `[field chip]` from an `[inputs sidebar]`, drags a semi-transparent GHOST across the page, and drops it — it SNAPS into a placed field with bounding box + corner handles; a completion beat follows (a `[modal]` springs up over the dimmed document, a name types letter-by-letter while a live `[cursive preview]` builds per keystroke, confirm click). The whole clip rides one continuous zoom-BREATHING arc (slow zoom-out / gentle zoom-in / final zoom-out) instead of discrete camera beats. + - _Variant — Product_Intro (hover-inspect)_: the cursor's first click SPAWNS a labeled `[toolbar]`, the camera zooms OUT from a tight crop to the full page, then the cursor sweeps `[page elements]` — each hovered element gets an outline and a floating `[inspector panel]` TRACKS the cursor, its content snapping per element. - **Scene 3 (~Ys–end) — payoff state, camera settles, HOLD.** The cursor lands on its final target and the screen reaches the payoff state; the camera comes to rest (static) and holds. - _Variant — Product_Intro_: the cursor HOVERS the hero element — a `[content card]` SCALES UP on hover, a node gets an `[Available]`-style pill, or a `[result card]` POPS/springs in — the "here's the product" payoff. Settles static, holds. - _Variant — Key_Feature_: locked close-up on the OUTCOME — cursor lands on the `[primary action button: Export / Save / Reimburse]` and a `[hover backdrop / highlight]` SPRING-pops in (the climax is the action button / produced result). Holds. + - _Variant — Key_Feature (static-stage tour)_: optional detachable end beat — `[brand text beat / icon-ring lockup / end stat card]` — or the cursor simply comes to REST on the next target and holds (006_claudeai ends with the cursor on a panel's close X, the panel never closing). + - _Variant — Key_Feature (drag-drop)_: close-up on the placed element ADJUSTED — a corner-handle drag proportionally resizes it — then the cursor sweeps toward the `[Finish / CTA]` as the clip ends. + - _Variant — browse / hover-inspect_: no payoff lock at all — the shot ends MID-demo, cursor still roaming (browse and hover-inspect modes). -**motion vocabulary**: cursor-driven click / hover / sweep-highlight / drag / type; per-interaction live UI response (scroll, value climb, region resize, content swap); camera push-in + pan / whip-pan / pan-down servoing to each target; coordinate zoom onto the acted region; press-and-ripple on a clicked control; button press-compress; screen-state swap shot-to-shot; card fan-out to corners (spring); 3D container fly-in & tumble-settle; perspective-flatten (3D→2D snap); paginated/stepped backdrop color advance; depth-of-field focus-pull across a parallax card stack; counter count-up; pill/swatch select; modal slide-up + typing; label/title morph between states; UI-keyword highlight glow; terminal hover-scale or result-card pop-in; spring hover-backdrop on the final action button. +**motion vocabulary**: cursor-driven click / hover / sweep-highlight / drag / type; per-interaction live UI response (scroll, value climb, region resize, content swap); camera push-in + pan / whip-pan / pan-down servoing to each target; coordinate zoom onto the acted region; press-and-ripple on a clicked control; button press-compress; screen-state swap shot-to-shot; card fan-out to corners (spring); 3D container fly-in & tumble-settle; perspective-flatten (3D→2D snap); paginated/stepped backdrop color advance; depth-of-field focus-pull across a parallax card stack; counter count-up; pill/swatch select; modal slide-up + typing; label/title morph between states; UI-keyword highlight glow; terminal hover-scale or result-card pop-in; spring hover-backdrop on the final action button; hard panel swap (no easing); side detail panel slide-in from the right edge (second panel over the first); hamburger→sidebar slide-open; hover popover drop below a tag; element-scale fake zoom (UI window scales in/out on click, camera locked); table populates row-by-row; formula typed into a cell + instant cell-range populate on enter; fill-handle drag auto-fill down rows; type-to-filter list live-collapse; dropdown fill on click; block/element pop-in to canvas; node-graph build (cards + connecting lines radiate from center); character-by-character auto-typing with blinking caret; window scale-up with settle; ghost-chip drag (grip dots + icon) across the page; drop-snap into a placed field with bounding box + corner handles + trash icon; modal spring-up over a dimming document; letter-by-letter typing with a live cursive preview building per keystroke; corner-handle drag with proportional resize; continuous zoom-breathing single shot (zoom-out / zoom-in / zoom-out arcs); cursor sweep toward the CTA at clip end; multiple labeled collaborative cursors moving independently; cursor grab-drag-drop of components between mockups; element recolor/identity swap on drop; canvas-group translate-pan within a static frame; headline building word-group by word-group over the demo; hover-triggered micro content/sidebar update; click spawns a labeled toolbar; floating inspector panel tracking the cursor with per-element content snap; per-element hover outline highlight; motion-blur window fly-in; tight-crop open then zoom-out to full page; brand icon-ring end beat; 3D end-card float on the hold. **rule mapping** @@ -53,5 +67,23 @@ - depth-of-field focus-pull across the parallax card stack → `depth-of-field-blur` (rack-focus / DoF blur transition between near and far cards; `3d-page-scroll` supplies the tilted parallax stack and `viewport-change` the pan) - paginated/stepped backdrop color advance synced to interactions (`[bg step 1]`→step 2→…) → `discrete-text-sequence` (discrete state stepping, here applied to a background-color state rather than text) - modal slide-up + in-modal typing as one combined beat → `card-morph-anchor` / `scale-swap-transition` (the panel slide-in) + `discrete-text-sequence` (the in-modal typed text) +- element-scale fake zoom — the UI window scales, camera locked (static-stage tour) → `coordinate-target-zoom` (applied to the surface wrapper rather than the world) +- side detail panel slide-in from the right edge / hamburger→sidebar slide-open / hover popover drop → `card-morph-anchor` / `scale-swap-transition` (the panel arrival) + `dynamic-content-sequencing` (which content each panel shows per beat) +- hard panel swap / in-panel content snapping through states / hover-triggered micro update / type-to-filter live-collapse / element identity swap on drop → `dynamic-content-sequencing` +- table populates row-by-row / fill-handle auto-fill cascading down rows / log rows cascade in → `waterfall-entry` +- formula typed into a cell / character-by-character auto-typing with blinking caret / letter-by-letter typed name → `discrete-text-sequence` + `context-sensitive-cursor` (the caret) +- node-graph build (cards + connecting lines radiate from center) → `center-outward-expansion` (the cards) + `svg-path-draw` (the connecting lines draw) +- click spawns a labeled toolbar / dropdown fills on click / drop-snap settle of the placed field / window scale-up with settle → `spring-pop-entrance` +- modal spring-up over a dimming document → `spring-pop-entrance` (the modal) + `depth-of-field-blur` (the document dim/blur beneath) +- ghost-chip drag-and-drop / cursor grab-drag of components between mockups / fill-handle drag / corner-handle resize drag → `cursor-drag` (`cursor-click-ripple` covers move+click only) +- floating inspector panel TRACKS the cursor, content snapping per element → `ai-tracking-box` (the per-frame follow mechanics, restyled as an inspector panel) + `dynamic-content-sequencing` (the per-element content) +- live cursive preview building per typed keystroke → `svg-path-draw` (progressive stroke reveal keyed to typing progress) +- continuous zoom-breathing single shot (drag-drop variant) → `multi-phase-camera` (pull-back / focus / push phases + micro-drift) +- motion-blur window fly-in / tight-crop open then zoom-out to full page → `motion-blur-streak` (the fly-in) + `viewport-change` (the zoom-out) +- multiple labeled collaborative cursors moving independently → `multi-cursor-choreography` (N labeled independent cursor actors; the single-actor cursor rules assume one) +- canvas-group translate-pan within a static frame → `viewport-change` (the `.world` translate realizes the pan; semantically the camera stays locked) +- headline builds word-group by word-group over the demo → `waterfall-entry` +- brand icon-ring end beat → `svg-path-draw` (the ring) + `spring-pop-entrance` (the lockup) +- 3D end-card float on the hold → `sine-wave-loop` — CAUTION: motion-doctrine bans idle wobble; prefer a settle-and-hold -**camera modifier**: The defining motion is the camera CHASE — the viewport follows the cursor from target to target via `camera-cursor-tracking` (primary), realized as concrete push-in + pan / whip-pan / pan-down moves under `viewport-change`, sequenced into discrete interaction beats by `multi-phase-camera`, with each beat's destination targeted via `coordinate-target-zoom` (zoom to the acted-on region). Product_Intro biases toward a slow, exploratory pan + focus-pull that sweeps the surface; Key_Feature biases toward snappier whip-pans / progressive zoom that march through the workflow and lock static on the action button. This camera-servo-to-cursor is what separates the blueprint from hands-off camera scrolls (dataviz-scroll-reveal) and static device/window tours. +**camera modifier**: The defining motion is the camera CHASE — the viewport follows the cursor from target to target via `camera-cursor-tracking` (primary), realized as concrete push-in + pan / whip-pan / pan-down moves under `viewport-change`, sequenced into discrete interaction beats by `multi-phase-camera`, with each beat's destination targeted via `coordinate-target-zoom` (zoom to the acted-on region). Product_Intro biases toward a slow, exploratory pan + focus-pull that sweeps the surface; Key_Feature biases toward snappier whip-pans / progressive zoom that march through the workflow and lock static on the action button. This camera-servo-to-cursor is what separates the blueprint from hands-off camera scrolls (dataviz-scroll-reveal) and static device/window tours. The golden set widens this into a spectrum. At one pole the **static-stage state tour** (now the largest member set) LOCKS the camera for the entire clip and lets the UI itself do all the moving — panel slide-ins, element-scale fake zooms, content snaps — with the cursor alone carrying the eye. The **drag-drop** variant replaces discrete chase beats with ONE continuous zoom-breathing arc under `multi-phase-camera`. The **hover-inspect** variant inverts the push-in: a tight-crop open zooms OUT to the full page before the cursor sweep. Pick the pole per brief — chase for workflow marches, locked stage for dense reconstructed dashboards, a single breathe for one-document journeys. With the locked pole absorbed, what separates this blueprint from `device-surface-showcase` is the CURSOR-as-actor, not the camera: a fully static tour still belongs here as long as a visible cursor drives every state change. diff --git a/skills/hyperframes-animation/blueprints/dataviz-countup.md b/skills/hyperframes-animation/blueprints/dataviz-countup.md index cd2aec330..2cea2a1cd 100644 --- a/skills/hyperframes-animation/blueprints/dataviz-countup.md +++ b/skills/hyperframes-animation/blueprints/dataviz-countup.md @@ -4,11 +4,13 @@ **roles served** -- Problem (from `problem-dataviz-pushthrough` / #9 Problem_1): quantifies the pain with real-looking instruments — a count-up ring → a trend chart → a stat grid — the camera pushing THROUGH each object into the next to dramatize a worsening / large-scale problem ("X% of people struggle with…"). -- Product_Intro (from `product-intro-dataviz-scroll-reveal` / #19 Product_Intro_06): a confident "look at the result / the data" open — hard-cut from a hook word into a perspective-tilted grid of data-viz cards, then a hands-off camera scroll lands one glowing hero metric while a kinetic tagline assembles word-by-word. +- Problem (from `problem-dataviz-pushthrough`): quantifies the pain with real-looking instruments — a count-up ring → a trend chart → a stat grid — the camera pushing THROUGH each object into the next to dramatize a worsening / large-scale problem ("X% of people struggle with…"). +- Product_Intro (from `product-intro-dataviz-scroll-reveal`): a confident "look at the result / the data" open — hard-cut from a hook word into a perspective-tilted grid of data-viz cards, then a hands-off camera scroll lands one glowing hero metric while a kinetic tagline assembles word-by-word. - Hook (from `hook-counter-burst`): a cold-open hook on ONE dramatic statistic — the frame opens dark and empty, 3–5 thematic icons puncture in clustered at center, then the headline number EXPLODES upward in size as the icons fling outward to their marks (the count-up and the spread are one beat), closed by a slow camera lean-in. Kinetic from frame 1. +- Key_Feature (from dark-stat-scrub-montage): prove the feature with its own analytics — on a black canvas, kinetic headline beats alternate with self-drawing charts and a 3D-tilted dark dashboard that a cursor SCRUBS (tracking line + live tooltips), stitched by hard cuts and one zoom punch. The one variant where a cursor touches the data. +- Social_Proof (from `gauge-beat`): a single count-up instrument — radial gauge arc-draw + rapidly ticking metric + caption — embedded as ONE BEAT inside a kinetic-typography relay; entered and exited by element-level scale/blur push-throughs on a static frame. The instrument guest-stars; the relay itself belongs to kinetic-type-beats. -**duration**: ~4–12s (Hook ~4s · Product_Intro ~6s · Problem ~11–12s) +**duration**: ~4–12s (Hook ~4s · Product_Intro ~6s · dark-scrub-montage ~7.3–7.75s · Problem ~11–12s · gauge-beat ~2.5s inside a ~10.8s relay) **shot structure** Data-viz field on `[bg color]` (dark or light, soft corner glows); `[gradient A→B]` brand stroke on charts/rings; clean sans-serif white/dark text; a continuous camera move runs underneath that traverses 2–3 data instruments and resolves on a hero metric. One instrument per beat; the camera carries the cut. @@ -18,8 +20,10 @@ - Variant — Problem (push-THROUGH, count-up → trend → grid): Scene 1 is a centered circular progress ring + count-up center number with scattered glowing `[avatar/object]` orbit. Scene 2 is a fast camera PUSH-IN straight through the center of the ring (ring, number, orbiting elements scale up and fly out of frame) into a rounded `[card]` holding `[stat-2 header]` over a `[gradient]` line chart with grid lines + translucent area fill that draws left→right; camera pushes through then settles. Scene 3: camera PANS to a second `[card]` whose number counts up, holding a grid of the `[avatar/object]` elements — a subset dim/blur while the rest receive `[accent]` circular checkmark badges that SPRING-POP; camera settles to the end. The traversal is z-depth push-through between instruments. - Variant — Product_Intro (scroll-to-hero + word-by-word tagline): a brief opener — Scene 0 (~0.0–0.85s): a full-frame `[hero-color orb]` with a bold white `[hook phrase]` over it; static shimmer, then HARD CUT. Scene 1 cuts to a slightly perspective-TILTED grid of `[data-viz / product cards]` (charts, heatmaps, stat cards with deltas + source footers) with `[tagline word 1]` centered; the grid begins SCROLLING (e.g. toward upper-left) with its tilt held. Scene 2: the grid keeps scrolling so the `[hero metric card]` glides into dead-center as off-center cards slide away; `[tagline word 1]` translates out and `[word 2]` rises in from a frame edge. Scene 3: hero card settles centered, `[accent]` glow blooms behind it, camera PUSHES IN slightly; `[word 2]` holds near it. Scene 4: `[word 2]` slides out, the final `[tagline word]` drops in from the opposite edge above the still-glowing hero, push-in peaks. Scene 5: overlay type clears, camera eases BACK OUT to a settled wider tilted composition — hero centered with glow, supporting cards flanking. The traversal is a hands-off camera SCROLL across a tilted card plane (no cursor, no clicks) + a one-word-at-a-time kinetic headline + push-in-then-out bookend. +- Variant — Key_Feature (dark-scrub-montage: kinetic beats × instruments, cut-stitched): on black, `[kinetic word]` beats ALTERNATE with data instruments; hard cuts stitch the beats and the camera is locked per beat — the traversal is a montage, not a continuous move. Beat A: a bold `[heading]` holds while a thick `[trend line]` DRAWS itself left→right inside a dark chart band, rising to break above the band's edge; at the peak a `[accent]` dot pops and a pill tooltip springs in, its label building to `[value + delta]`. Beat B: ONE fast zoom PUNCH lands a close-up, slightly 3D-tilted dark `[analytics dashboard]` (metric cards with deltas, translucent oversized numerals floating behind); a white cursor SCRUBS a chart — a vertical tracking line follows it and `[date: value]` tooltips read out live, then a second chart ACTIVATES with a color flip and its own scrubbing tooltip — while the tilted plane drifts gently sideways; quick pull-away/fade to black. Beat C: a `[glowing wave / typed line / impact word]` beat lands the closing stat LOCKUP — `[title]` + big `[stat]` counting up + `[green delta arrow + context line]` — and holds static to the end. Kinetic words between instruments scale up violently past the frame as element-level push-through transitions (no camera). +- Variant — Social_Proof (gauge-beat inside a relay): a static-camera kinetic-type relay hosts ONE instrument beat — thin concentric `[accent]` arcs radiate from center, a thick `[accent]` progress arc draws clockwise over them, a large `[metric]` rapidly ticks up to `[big value]` with a `[caption]` below; the group slowly scales up (element-level drift), then hard-cuts out to the next text beat. Entry/exit for every beat is scale-up-from-blur in / scale-up-and-blur-past-frame out — a fake push-through with no camera anywhere. Use when social proof is one number and the surrounding beats are typography. -**motion vocabulary** count-up number with transform-scale growth on the value; circular progress-ring sweep; growth bar / progress fill; gradient trend-line + area-fill left→right draw; spring-overshoot pop-in of scattered glowing avatar/object elements; perspective-tilted card grid; directional grid scroll (cards glide in/out of center); hero-card centering; soft accent glow bloom behind the hero; slow continuous zoom-in; fast camera push-IN / push-THROUGH the center of an instrument; lateral/vertical camera pan between cards; gentle push-in that peaks then eases back out to a wider settle; selective dim/blur of a subset + spring-pop checkmark badges; full-frame hook orb → hard cut; kinetic tagline assembled word-by-word (each word drops/rises from a frame edge, prior word slides out). +**motion vocabulary** count-up number with transform-scale growth on the value; circular progress-ring sweep; growth bar / progress fill; gradient trend-line + area-fill left→right draw; spring-overshoot pop-in of scattered glowing avatar/object elements; perspective-tilted card grid; directional grid scroll (cards glide in/out of center); hero-card centering; soft accent glow bloom behind the hero; slow continuous zoom-in; fast camera push-IN / push-THROUGH the center of an instrument; lateral/vertical camera pan between cards; gentle push-in that peaks then eases back out to a wider settle; selective dim/blur of a subset + spring-pop checkmark badges; full-frame hook orb → hard cut; kinetic tagline assembled word-by-word (each word drops/rises from a frame edge, prior word slides out). Dark-scrub-montage additions: self-drawing chart line that breaks above its band; peak dot + pill tooltip spring-pop; cursor chart scrub with vertical tracking line + live date/value tooltip readouts; chart activation color flip; 3D-tilted dark dashboard plane with slow lateral drift; translucent oversized numerals floating behind cards; fast zoom punch-in; pull-away/fade-to-black beat exit; hard-cut beat stitching; kinetic word push-through (element scales up past the frame); typed line with blinking cursor; impact slam word + particle-dissolve punctuation; glowing wave draw; green delta arrow pop; stat lockup hold. Gauge-beat additions: concentric static arcs + thick clockwise progress-arc draw; rapid count-up tick; scale-up-from-blur entrance / scale-up-and-blur-past-frame exit (element-level fake push-through). **rule mapping** (motion verb → `rules/.md`) @@ -37,8 +41,19 @@ - slow continuous zoom-in + push-THROUGH the instruments + lateral/vertical pan between cards + push-in-then-out bookend → `multi-phase-camera` (see camera modifier) - soft accent glow BLOOM behind the hero card → `ambient-glow-bloom` (un-triggered soft glow/bloom behind the static hero element — distinct from `press-release-spring`'s press-triggered glow and `asr-keyword-glow`'s word-timed envelope) - selective dim/blur of a SUBSET of grid items (focus-falloff on the non-highlighted cards) → `depth-of-field-blur` (selective per-element blur/dim to spotlight the highlighted cards — the same focus-falloff rule used in `constellation-hub`) +- cursor chart scrub (cursor-tied vertical tracking line + live data readout in a tooltip) → `chart-scrub-readout` (the tracking line, tooltip pop, and seek-safe live value readout driven by cursor x) +- chart activation color flip (second chart lights up under the scrub) → `gsap-effects` (color/opacity chord at the scrub handoff — basic tween, no dedicated rule needed) +- 3D-tilted dashboard plane + slow lateral drift → `3d-page-scroll` (the tilt framing) + `sine-wave-loop` (the drift; keep amplitude tiny so the scrub stays legible) +- fast zoom punch-in to the dashboard → `multi-phase-camera` (one short aggressive push phase) aimed via `coordinate-target-zoom`; add `motion-blur-streak` at peak velocity +- kinetic word push-through / scale-up-and-blur-past-frame exit / scale-up-from-blur entrance → `kinetic-beat-slam` (the beat grammar) + `motion-blur-streak` (blur peaks at max speed, resolves at the settle — its entrance form runs the blur-in, its exit form the blow-past) +- typed line with blinking cursor → `discrete-text-sequence` + `context-sensitive-cursor` (square-wave blink) +- impact slam word → `kinetic-beat-slam`; its particle-dissolve punctuation → `particle-burst` (glyph→particles dissolve, deterministic) +- glowing wave draw → `svg-path-draw` (the draw) + `ambient-glow-bloom` (the glow envelope) +- green delta arrow pop / peak dot + pill tooltip → `spring-pop-entrance` +- concentric static arcs + clockwise progress-arc draw (gauge beat) → `stat-bars-and-fills` (ring form) → draw mechanics `svg-path-draw` (both already mapped above — the gauge is the existing ring with static concentric chrome behind it) **camera modifier**: The camera is the through-line that traverses the data instruments — one camera wrapper sequenced by `multi-phase-camera`, with each stop targeted via `coordinate-target-zoom` onto the focal instrument/card. - Problem — push-THROUGH: a slow continuous zoom-in (drift overlay) plus a fast PUSH-IN straight through the center of one instrument into the next (`multi-phase-camera`, Steady-push pattern), then a lateral/vertical PAN to the final card. Z-depth push-through is the signature (distinguishes it from a flat pan-tour). - Product_Intro — scroll-to-hero + bookend push: a hands-off directional SCROLL across the tilted card plane (`3d-page-scroll` scroll / `viewport-change` pan) that lands the hero card center, then a gentle push-in that PEAKS and eases BACK OUT to a wider settle (`multi-phase-camera`, Bookend-pull pattern). No cursor, no clicks — the camera does the navigating. +- Key_Feature — montage-cut: the camera is NOT the through-line — hard cuts stitch the instrument beats, the frame is locked inside each beat, and exactly ONE fast zoom punch (`multi-phase-camera` single push phase + `coordinate-target-zoom`) lands the dashboard close-up; exits are pull-away/fade-to-black. Between instruments, ELEMENTS fake the push: kinetic words scale up past the frame (`kinetic-beat-slam` + `motion-blur-streak`). Gauge-beat form drops even the punch — fully static, all push-through element-level. Reach for this mode when the dialect is a dark rapid montage; the Problem/Product_Intro modes remain the default for a single continuous argument. diff --git a/skills/hyperframes-animation/blueprints/device-surface-showcase.md b/skills/hyperframes-animation/blueprints/device-surface-showcase.md index 53adaa0e3..408ce700d 100644 --- a/skills/hyperframes-animation/blueprints/device-surface-showcase.md +++ b/skills/hyperframes-animation/blueprints/device-surface-showcase.md @@ -4,10 +4,12 @@ **roles served** -- Key*Feature (from key-feature-device-screen-tour, key-feature-floating-window-scroll, key-feature-3d-device-hand-demo): show a feature being \_experienced inside its real interface* — the surface houses the action and its screens advance through a flow, rather than enumerating tiles or chasing a cursor across a workflow. (Note: all three drafts are Key_Feature; this blueprint is role-narrow but mechanic-rich — variants differ by MECHANIC, not role.) +- Key_Feature (from key-feature-device-screen-tour, key-feature-floating-window-scroll, key-feature-3d-device-hand-demo): show a feature being \_experienced inside its real interface\* — the surface houses the action and its screens advance through a flow, rather than enumerating tiles or chasing a cursor across a workflow. (Note: the three founding drafts are Key_Feature and variants differ by MECHANIC, not role; the mined stepwise-flow variant widens the blueprint to Product_Intro.) - Key_Feature (from demo-page-scroll-spotlight): the floating-window push-scroll variant carried to a spotlight climax — a real webpage rendered as a tilted 3D card coasts in (power2, like a phone held up — no spring), header keywords flare on a karaoke glow as the VO names them, the page rolls to the demoed section, and one element LIFTS off the surface (translateZ + scale) under a radial spotlight that dims the rest. +- Product_Intro (from stepwise-flow-completion): a compact end-to-end product flow — setup/auth → action → success/confirm — plays out cursorless as successive screen states inside the held surface, capped by a confirming button press; bookended by title-card beats. The surface introduces the product by \_completing its core loop\*, not by touring screens. +- Key_Feature (from `showcase-carousel`): the showcase-carousel — two surfaces in sequence (a widget card cycling brand skins, a phone frame with app screens sliding through it) gated by interstitial claim words; the screen cycle is a breadth carousel ("N brands / N apps"), not a flow. -**duration**: 5–9.6s (page-scroll-spotlight 5–9s · floating-window 7.8s · 3d-hand 7.9s · device-tour 9.6s) +**duration**: 5–11.3s (page-scroll-spotlight 5–9s · floating-window 7.8s · 3d-hand 7.9s · in-device approval 7.9s · stepwise-flow 8.5–9.4s · device-tour 9.6s · showcase-carousel 11.3s) **shot structure** One product surface — a `[device mockup]` or a `[floating browser/app window]` — is the persistent hero on a `[styled backdrop: gradient / radial / stylized 3D void]`; its `[screens/sections]` cycle through a real `[product flow]` while a showcase camera (static-hold, push-in→zoom-out, or one continuous push) presents it. Each screen state holds ~1.0–1.5s. @@ -18,8 +20,10 @@ - Variant — static-tour (key-feature-device-screen-tour, 9.6s): a `[device mockup]` slides in from off-screen and settles (ease-out); an `[accent-color shape]` scales up behind it (spring overshoot). Camera STAYS STATIC the entire clip — all motion is element/UI-level: a tap COMPRESSES a button (95%→100%), the UI scrolls/transitions to the next view (old pushes out, new pulls up), and a `[side headline]` SWAPS beside the device (old slides up + fades, new slides up + in) per screen. Holds on the final screen. No camera move, no cursor. - Variant — floating-window (key-feature-floating-window-scroll, 7.8s): OPENS on a full-frame `[title card]` (a small `[icon]` draws in at center, `[feature name]` below; holds ~2s), which DISSOLVES to a `[macOS-style browser/app window]` floating on a `[vivid gradient]` (traffic-lights + `[URL pill]` + tabs; left nav, central content, right `[sidebar]`). Camera PUSHES IN on a `[target region/sidebar]` (active item highlighted `[accent]`, a cursor drifts down the list), then ZOOMS BACK OUT to re-frame the whole window while the content SCROLLS through `[sections]`; the `[highlighted item]` stays marked. One push-in→zoom-out arc, gated by the title-card opener. - Variant — 3d-hand (key-feature-3d-device-hand-demo, 7.9s): FULLY 3D — a `[3D device]` drifts in a `[stylized 3D void / bloom + particles]`, opening tilted and self-rotating to face the lens nearly flat as ONE CONTINUOUS forward camera push begins (no cuts). A glossy `[3D hand]` rises from the bottom-foreground and GESTURE-DRIVES the surface: it swipes to scroll a `[picker/sidebar panel]` of `[option cards]` and taps `[option]` (while a `[header word]` letter-flips in place); the selection APPLIES — a `[new layout]` grows from center to fill the device face, nav flips, a `[marquee]` scrolls horizontally; the hand swipes again to scroll the page upward through `[sections]`, then drifts out. The camera never stops pushing; the bright device face keeps growing toward the lens until it BLOOMS into a `[light]` wash — a zoom-through "portal" exit that fills the frame. +- Variant — stepwise-flow (Product_Intro, 8.5–9.4s; in-device Key_Feature sub-mode 7.9s): CURSORLESS end-to-end flow — the surface completes `[setup/auth → action → success]` as a narrative arc. Opens on a `[title card]` that fades in/out on an ambient gradient (or a typed `[command]` running character-by-character on a terminal field). The `[flow surface]` arrives (phone mock slides up oversized and settles / bordered log panel replaces the command) and step 1 completes via rapid sequential pops — `[OTP digits]` fill boxes left-to-right capped by a green check, or `[log steps]` pop top-down with highlighted tokens, ending on a trailing-dots waiting state. State advances laterally (old content slides out left, new in from right, chrome persists) or via a dark-to-light scene swap into a white `[detail/confirm card]` whose elements stagger in. COMMIT: the `[CTA button]` is pressed (press dip / spinner "Processing") and a `[success state]` renders with check bullets — in the in-device sub-mode the commit runs a biometric ritual: dim overlay, `[squircle]` spring-pops, a ring draws around an icon, the icon morphs to a checkmark and holds; a slight camera push-in fires ONLY at the state transition (camera punctuates the commit, then re-locks). EXIT: the surface leaves and closing `[title cards]` pop in and ease smaller — the surface exits before the coda instead of holding. Camera otherwise static. For this variant the persistent hero is the FLOW, not one surface: a terminal panel may hand off wholesale to a confirm card. +- Variant — showcase-carousel (Key_Feature, 11.3s): TWO surfaces in sequence on a slowly drifting `[pastel mesh gradient]`, static camera, gated by centered interstitial `[claim words]` (fade in with gentle scale-up, fade out). Act 1: a white `[widget card]` scales in, flips/morphs into a tilted vertical widget and CYCLES `[N brand skins]` (~0.8s each) — one shared layout, per-skin content and accent swaps — while a large `[brand logo]` crossfades below per flip; the widget scales away. Act 2: a `[phone frame]` enters oversized and tilted, settles upright at center; full `[app screens]` slide left through it (~1s each), holding on the last. The screen cycle is a breadth carousel, not a flow — no taps, no cursor, no camera. -**motion vocabulary** surface establish (edge slide-in + settle / tilt drift-in + self-rotate-to-camera / title-card dissolve); accent shape spring behind surface; element-level screen-cycling (scroll-swap, push-in-from-side, scale-swap); button tap-compress; staggered side-headline reveal + copy swap (out-up / in-up); in-place header-word letter-flip; floating browser-window-on-gradient idle float; full-frame title-card opener (icon draw-in + label); camera push-IN on a region; camera zoom-OUT re-frame; content scroll-through; one continuous 3D camera-follow push (no cuts); 3D device drift + self-rotate; stylized-environment bloom/particles; 3D-hand entrance + swipe-scroll + tap (gesture-driven); picker-panel slide-in; template-apply grow-from-center; horizontal marquee scroll; gesture-driven page scroll; zoom-through bloom/portal exit; static-hold (no camera) as the floor of the camera range. +**motion vocabulary** surface establish (edge slide-in + settle / tilt drift-in + self-rotate-to-camera / title-card dissolve); accent shape spring behind surface; element-level screen-cycling (scroll-swap, push-in-from-side, scale-swap); button tap-compress; staggered side-headline reveal + copy swap (out-up / in-up); in-place header-word letter-flip; floating browser-window-on-gradient idle float; full-frame title-card opener (icon draw-in + label); camera push-IN on a region; camera zoom-OUT re-frame; content scroll-through; one continuous 3D camera-follow push (no cuts); 3D device drift + self-rotate; stylized-environment bloom/particles; 3D-hand entrance + swipe-scroll + tap (gesture-driven); picker-panel slide-in; template-apply grow-from-center; horizontal marquee scroll; gesture-driven page scroll; zoom-through bloom/portal exit; static-hold (no camera) as the floor of the camera range. Stepwise-flow additions: title-card bookends (fade-in/out opener; closers pop in then ease smaller); typed terminal command with prompt chevron; sequential top-down log pops with sub-line reveals; animated trailing-dots wait state; sequential digit pops left-to-right + green check confirm; lateral screen slide with persistent chrome; dark-to-light scene swap; staggered card element build-in (fade + slide-up); button press dip + fill flip; spinner processing state; success check-bullet reveal; notification banner spring-in with overshoot; lockscreen fade/blur-away as a card expands to fill the device face; commit-synced micro push-in; dim overlay; squircle spring pop; circular ring draw; icon morph to checkmark; surface exit before a title coda. Showcase-carousel additions: interstitial claim-word gate; brand-skin cycling with per-flip logo crossfade; card flip/morph into a tilted widget; oversized-tilted surface entry settling upright; fast slide-left screen carousel inside a static frame; drifting mesh-gradient backdrop. **rule mapping** (per motion verb → backing rule, or flagged special) @@ -40,6 +44,20 @@ - horizontal `[marquee]` scroll (3d-hand) → `viewport-change` (PAN mode on the marquee strip) — _thin fit; a literal CSS-marquee/translateX loop is closer to a `gsap-effects`/CSS recipe than a named motion rule_ - 3D-hand entrance + swipe + tap as the interaction DRIVER (gesture input that scrolls/selects) → **flagged special — needs a heavier capability beyond the rule library (R3F/Three.js + WebGL), NOT a motion-shape rule.** The 3D hand model + WebGL bloom have a _technique_ backing (`3d.md` — R3F, `useGLTF` HandModel, `--gl=swiftshader` for the shader/bloom), but no motion-shape rule models a 3D hand as the swipe-to-scroll / tap-to-select gesture protocol. `context-sensitive-cursor` / `camera-cursor-tracking` only model a flat typing/pointer cursor, not a 3D gesturing hand. - zoom-through bloom / portal exit (3d-hand) → **flagged special — needs a heavier capability beyond the rule library (WebGL), NOT a named transition rule.** Capability is `techniques.md` → WebGL shader (via `3d.md` headless WebGL: `--gl=swiftshader --concurrency=1`), but no named transition rule covers a bloom/portal fly-through. +- typed terminal command / non-linear log text (stepwise-flow) → `discrete-text-sequence` (typing + threshold state replacement) with `dynamic-content-sequencing` computing each step's window from content length +- sequential top-down log pops / OTP digit pops left-to-right / staggered confirm-card build-in → `spring-pop-entrance` (staggered group form; low overshoot for log lines) +- trailing-dots wait state → `sine-wave-loop` (finite repeats; step the opacity of 3 dots on a shared phase) +- lateral screen slide with persistent chrome → the existing screen-cycling mapping (`3d-page-scroll` translateX form inside the clipped surface); chrome sits outside the sliding layer +- notification banner spring-in / squircle pop (in-device) → `spring-pop-entrance` +- lockscreen fade/blur-away + card expands to fill the device face → `card-morph-anchor` (uniform-scale container morph — never tween width/height) + `depth-of-field-blur` (the blur-away) +- commit-synced micro push-in (camera punctuates the Approve/tap, then re-locks) → `multi-phase-camera` (single short push phase placed at the state transition) +- button press dip + fill flip / Approve press-down spring-back → `press-release-spring` (already mapped; the fill flip is its color-transition variation) +- spinner processing state → `svg-icon-enrichment` (rotating internal element with explicit SVG center) +- success check bullets / biometric ring draw → `svg-path-draw` (check strokes; ring rotated −90° to start at 12 o'clock) + `spring-pop-entrance` for the bullet pops +- icon morph to checkmark (biometric ritual) → **flagged special — SVG path morph, see hyperframes-keyframes (morph)**; no motion-shape rule models it — mechanics live in `techniques.md` / the keyframes skill, same tier as the blueprint's existing WebGL flags +- interstitial claim-word gate (fade + gentle scale-up, then out) → `gsap-effects` (plain fade/scale chord; deliberately quieter than `kinetic-beat-slam`) +- brand-skin cycling with per-flip logo crossfade → `discrete-text-sequence` (whole-state content replacement at thresholds) + `scale-swap-transition` where a flip reads as shrink-out/pop-in; the card→tilted-widget flip/morph → `card-morph-anchor` + `css-3d-transforms` +- drifting mesh-gradient backdrop → `sine-wave-loop` (very-low-amplitude position/hue drift on gradient blobs) **camera modifier**: The showcase camera spans a RANGE keyed by variant, all on a single content-wrapping virtual camera (`viewport-change`): diff --git a/skills/hyperframes-animation/blueprints/fixed-anchor-cycle.md b/skills/hyperframes-animation/blueprints/fixed-anchor-cycle.md new file mode 100644 index 000000000..1752ee454 --- /dev/null +++ b/skills/hyperframes-animation/blueprints/fixed-anchor-cycle.md @@ -0,0 +1,46 @@ +# fixed-anchor-cycle — Fixed Anchor, Cycling World + +**intent**: One element is PINNED — a wordmark, a composer box, an anchor line that enters once and never moves again — while the adjacent region (or the entire surrounding theme) cycles through many discrete states around it, cadence often manipulated (steady stepping, a fast carousel, or a slow→accelerating flurry), resolving on an emphasis beat into a completed lockup or a muted freeze. The stillness of the anchor IS the claim: everything changes, this stays. Distinct from `kinetic-type-beats` sub-shape A, where a word-slot inside a centered line swaps and the sentence itself is the subject — there the anchor is a sentence frame on a bare type field; here the anchor is the PRODUCT identity and what cycles around it can be non-text (whole theme skins, chrome/logo swaps, textured label chips, a carousel list), the cycle asserts breadth ("everyone says / works everywhere / calling all X"), and the resolve completes the anchor into a lockup. Distinct from `ticker-takeover`, whose cycle ends in a collision — a hero crashes in and shoves the text aside; here nothing ever collides with the anchor: the cycle stops, and a final element quietly joins it. + +**roles served** + +- Brand_Outro (from `static-anchor-rapid-text-swaps`): when the sign-off is the brand name sitting immovable while praise quotes / tagline words cycle beside or beneath it — steady per-word highlight stepping, or a hard-cut chip flurry that accelerates — landing on the finished lockup ("bolt.new / prompt, run, edit, deploy / enjoy."; "Opus 4.6 by ANTHROP\C"). +- Benefits: when "works everywhere" is shown literally — one product surface (a prompt composer with one verbatim string) pinned dead-center while its ENTIRE shell morphs in place through N product themes (background, typography, radii, chrome, logos all crossfading at once), ending in a washed-out freeze. +- Hook: when the opener is a roll-call — a static anchor line holds while an accent-colored line beneath it runs as a fast vertical carousel through an audience/option list, then the block clears into follow-up statement beats that land the brand line. + +**duration**: 6.6–11.1s (Benefits shortest ~6.6s at 4 theme beats; Brand_Outro ~9–9.4s; Hook longest ~11s when the anchor-cycle block hands off to follow-up statement beats). The cycle engine itself occupies ~3–5s regardless of role. + +**shot structure** (flat static frame — camera locked in every member; a `[bg]` field, solid or subtly drifting; two folded sub-shapes — **(A) adjacent-region cycle**: the anchor holds and a neighboring slot swaps through N states; **(B) whole-context morph**: the anchor holds and everything AROUND it re-skins in place) + +- **Scene 1 (0.0–~2.0s) — the anchor lands and PINS.** The `[anchor: wordmark / product name / composer box / lead line]` enters once — fade/scale-in centered, word-by-word build, or already present at frame one — at a fixed position it will hold for the entire clip. Zero movement from here on: no drift, no breathe, no re-layout. If the anchor is a UI surface (sub-shape B), it carries a `[verbatim string]` with a blinking cursor. + +- **Scene 2 (~2.0s–~70% of runtime) — the cycle engine (signature move).** The world changes around the unmoved anchor. Choose by sub-shape: + - **Sub-shape A (adjacent-region cycle)**: a region beside/beneath the anchor steps through N discrete states — pick ONE swap mechanic and ONE cadence: + - _swap mechanics_: instant hard-cut label replacement (a `[chip / tape label]` slaps over the old one, texture/highlight shifting slightly, chip width re-fitting each `[phrase]` — growing away from the anchor, never over it); sequential per-word highlight stepping (one word of the `[tagline]` snaps bright/bold while the rest sits dim grey, the highlight walking the line); or a fast vertical carousel (each `[list item]` slide/fades through the accent slot ~0.5s/phrase). + - _cadences_: steady stepping (~0.5–1s/state), or **slow→accelerating flurry** — ~1s beats compressing to ~0.15–0.3s per swap, breadth escalating into a blur of states (12–16 states read as "everyone"; 3–8 read as a roll-call). + - Geometry law: the cycling region NEVER overlaps, touches, or displaces the anchor; size the layout so the longest state still fits inside the frame with clear margins. + - **Sub-shape B (whole-context morph)**: at ~1.3s intervals the entire theme — `[bg color]`, typography, corner radii, toolbar icons, footer `[brand logos]`, contextual lines — morphs in place via quick (~0.3s) crossfades through N `[product skins]`, every property blending simultaneously. No hard cuts, no wipes; the anchor's content string is identical in every skin (chrome details like a `> ` prefix may adapt per skin). + +- **Scene 3 (~70–85%) — the emphasis beat.** The cycle resolves — it does not just stop: + - _Variant — Brand_Outro (highlight stepping)_: the whole `[tagline]` snaps solid bright at once — full-line illumination after the per-word walk. + - _Variant — Brand_Outro (flurry)_: the flurry halts and HOLDS on the `[longest / weightiest phrase]` — a beat of stillness after acceleration. + - _Variant — Benefits (theme morph)_: the final beat mutes — a faint `[dot-grid]` fades in across the background while the UI drops to low opacity, a washed-out blueprint freeze. + - _Variant — Hook (carousel)_: the anchor block clears, handing off to 1–3 centered word-by-word statement beats (kinetic-type-beats territory) that carry toward the close. + +- **Scene 4 (final beat → end) — lockup completion and HOLD.** A final element joins the still-unmoved anchor and the finished composition holds static to the end: a `[closing word]` drops in below, aligned to the last cycled state ("enjoy."); the chip vanishes on a hard cut and the `[brand sign-off]` appears beside the anchor on a shared baseline ("by ANTHROP\C"); or the final `[brand line]` builds word-by-word dead-center and holds ("with Copilot."). Long static hold — the lockup is the payoff, give it 20–30% of the runtime. + +**motion vocabulary**: anchor fade/scale-in entrance; permanently pinned anchor (zero movement, no idle breathe); instant hard-cut label/chip replacement (slap-over with subtle texture/highlight shift); chip width resize-to-fit per phrase (grows away from the anchor); sequential per-word highlight stepping through a line; dim-to-grey line state; whole-line illumination snap; fast vertical carousel slide/fade of one line under a static line; cadence acceleration (slow ~1s beats into a ~0.15–0.3s flurry); hold-on-longest-phrase emphasis beat; in-place theme morph crossfade (~0.3s) blending background/fonts/radii/icons simultaneously; per-beat chrome/logo swap; blinking text cursor; contextual line appearing/disappearing across beats; dot-grid backdrop fade-in; global opacity washout; end freeze; word-by-word phrase build; block clear between scenes; drop-in entrance of a final word; hard cut to final lockup; long static hold. + +**rule mapping** + +- instant hard-cut chip/label/phrase swaps at time thresholds; per-word highlight stepping (color/weight state swaps); dim-line → full-line illumination snap; per-state chip width set (a per-state layout property, set discretely — never tweened) → `discrete-text-sequence` +- fast vertical carousel of the accent line under the static anchor (slide/fade stepped swaps in a masked slot) → `vertical-spring-ticker` (its footer-reveal step unused — Scene 4's lockup takes its place) +- per-phrase state windows computed from a script of N states (praise quotes, audience list, theme beats) → `dynamic-content-sequencing` (Accelerating cadence — for the flurry, pre-compute the beat array with shrinking `hold` values, geometric decay over the state list) +- word-by-word phrase builds (anchor line, follow-up statements, final brand line) → `dynamic-content-sequencing` + `waterfall-entry` (or `kinetic-beat-slam` when the statements should land percussively) +- anchor entrance fade/scale-in; drop-in of the final closing word → `spring-pop-entrance` (restrained overshoot — the register here is editorial, not bouncy) +- blinking cursor in the pinned composer → `context-sensitive-cursor` (color adapts per theme skin at segment boundaries) +- whole-context theme morph → `theme-crossfade-morph` (N pre-styled full-scene layers stacked at the same geometry, opacity-crossfaded, the shared anchor string rendered once on top); the composer shell's radius/surface component alone → `card-morph-anchor` +- subtly drifting background field beneath the cycle → `sine-wave-loop` (bounded drift; the anchor itself gets none) +- dot-grid fade-in + global opacity washout freeze; long static hold → `gsap-effects` (plain opacity tweens) / static hold (no rule needed) + +**camera modifier**: none — every member is fully camera-static; the cycle is the only motion, and the pinned anchor's stillness is load-bearing. Do not add a push-in "for energy"; it would break the anchor contract. diff --git a/skills/hyperframes-animation/blueprints/grid-card-assemble.md b/skills/hyperframes-animation/blueprints/grid-card-assemble.md index 9d490efe2..e7588d447 100644 --- a/skills/hyperframes-animation/blueprints/grid-card-assemble.md +++ b/skills/hyperframes-animation/blueprints/grid-card-assemble.md @@ -8,23 +8,26 @@ - Key_Feature (from key-feature-glass-card-camera-reveal): open TIGHT on 2–3 glowing icons; a camera zoom-OUT unfolds a row of glassmorphism cards that grow from behind the icons (icons shrink to card headers), center card scales forward, the group floats, then sweeps out — a "pillars revealed at once" reveal variant of the same assemble shape. - Benefits (from benefits-vertical-list): short value phrases populate a single vertical list ~1 item/sec, co-resident and accumulating; each line enters via a spring marker-pop + check-draw + pill mask-wipe, OR the whole stack snaps up one slot per beat (slot-machine) so the newest lands in the bright focal slot. - Social_Proof (from social-proof-logo-grid-zoom-out): a wall of partner/app logos builds into a center grid (whole-enter / randomized pop-in / column slide-up), an optional headline + accent-gradient proof-number fills in above, then a continuous camera zoom-OUT shrinks the array to reveal a vast ecosystem; optional fixed HUD/viewfinder brackets; optional grid slide-up fly-out exit. +- Key_Feature (from live-data-populate-board): the array assembles by POPULATING ITSELF — skeleton pills fill and swap to real data, cards spring in tethered to map markers — and its state keeps flipping live after assembly (status pills stepping through states); no cursor, locked frame. The "look how much" beat becomes "look, it's doing it right now." +- Benefits (from item-field-to-payoff-card): a breadth FIELD — a rapidly streaming list past a fixed focal slot, or a chip array with one highlighted hero — plays its breadth motion, then CLEARS to concise centered payoff text (claim / price / URL end card). The array is the argument's setup; the payoff line is its landing. -**duration**: 3.0–10.5s (Social_Proof 3.0–6s · Key_Feature grid 5.8–7.3s · Key_Feature glass-card 6.5s · Benefits list 6.5–10.5s, scaling ~1 item/sec with count) +**duration**: 3.0–10.5s (Social_Proof 3.0–6s · live-populate 4.2–7.8s · Key_Feature grid 5.8–7.3s · Benefits stream/field-to-payoff 5.9–8.4s · Key_Feature glass-card 6.5s · Benefits list 6.5–10.5s, scaling ~1 item/sec with count) **shot structure** (consolidated template — concrete motion verbs, [slots]) - **Scene 1 (0.0–~1.0s) — open + first arrivals.** On a `[gradient / radial / dark background]` (optional `[dot-grid / drifting-watermark]` texture), an empty `[grid or list region]` is established and items begin to ASSEMBLE in a quick staggered cascade (~0.04–0.08s gap; list pacing ~1 item/sec). Each `[item: feature tile / pill / logo tile / benefit line]` fades + slides/scales a short distance directly into its slot (low drama — no scatter, no big bounce; spring overshoot reserved for accent markers). Camera static. An opening `[headline / hook]` may fill in line-by-line above the array, with any `[proof number]` counting up in an `[accent gradient]`. - **Scene 2 (~1.0s–~Xs) — array resolves + holds.** Remaining items finish arriving; layout resolves into the final `[2-col-brick / 3×3 grid / dense mosaic / stacked list]`. The completed array HOLDS, alive but resting: a gentle continuous parallax/sine FLOAT on the tiles and/or a slow camera push-in (faint scale-up). Optional `[accent-color]` glow TRAVELS across/behind the tiles. -- **Scene 3 (~Xs–end) — settle / reveal / exit.** Everything settles and holds to the end, OR the optional camera modifier runs (see below), OR a `[closing line / CTA]` book-ends the array. +- **Scene 3 (~Xs–end) — settle / reveal / exit.** Everything settles and holds to the end, OR the optional camera modifier runs (see below), OR a `[closing line / CTA]` book-ends the array. OR the field CLEARS to payoff copy — the array exits and a concise centered `[claim / price / URL]` lands (price via a very fast character snap-build with a split-second partial state; URL via a left-to-right reveal, holding in `[accent]` and flipping to `[ink]` only in the final beat) — OR the camera PUSHES THROUGH one highlighted `[hero item]` (single rapid accelerating push-in) and crossfades into a second, vaster receding `[word-grid depth field]` that continuously scales down to reveal ever more items before fading to the payoff. Variants (where roles diverge from the template): - **Variant — Key_Feature grid**: items are labeled `[icon + feature-label]` tiles/pills assembling into a 2-col-brick / 3×3 grid; near-static hold with slow push-in + optional traveling-glow sweep; headline book-ends (`[hook]` → `[CTA]`). No camera reveal. - **Variant — Key_Feature glass-card-reveal**: the assemble is CAMERA-DRIVEN, not element-stagger. Open tight on `[2–3 glowing icons]`; camera zoom-OUT grows `[N]` glass cards out from behind the icons (icons shrink ~50% to become card headers), `[center card]` scales ~105% and moves forward to overlap the sides (quick spring); cards hold side-by-side with continuous parallax float; exit = fast motion-blur SWEEP slides the cards off-frame. -- **Variant — Benefits vertical-list**: a single vertical `[benefit-line]` stack, ~1 item/sec, two sub-modes — (a) BUILD: each line stays fully lit; entry = `[marker]` spring-pop + `[check/icon]` draw-in + `[pill]` mask-wipe of the text; (b) SNAP: the whole stack steps up one slot per beat (~0.1s eased) so the newest line lands in the bright focal slot and lines leaving it dim by position. Static camera; optional perpetual `[decorative orbit/disc]` on the opposite side. No camera reveal. +- **Variant — Benefits vertical-list**: a single vertical `[benefit-line]` stack, ~1 item/sec, three sub-modes — (a) BUILD: each line stays fully lit; entry = `[marker]` spring-pop + `[check/icon]` draw-in + `[pill]` mask-wipe of the text; (b) SNAP: the whole stack steps up one slot per beat (~0.1s eased) so the newest line lands in the bright focal slot and lines leaving it dim by position; (c) STREAM: the list scrolls rapidly and continuously past the focal slot — center item opaque `[ink]` and slightly enlarged, neighbors faded/shrunk — then DECELERATES to stop on the `[chosen item]`; optionally split-framed against a fixed static `[label]` on the opposite side; the field then clears to a centered `[payoff line]`. Static camera; optional perpetual `[decorative orbit/disc]` on the opposite side. No camera reveal. +- **Variant — Key_Feature live-populate**: the assemble is a DATA-POPULATION wave, cursorless, frame locked (± one gentle opening zoom-out that makes room for the `[headline]`). Two board shapes — (a) ANCHORED: `[white data cards]` spring in one-by-one, each tethered by a thin line to its `[marker]` on a `[map/board surface]` whose markers pulse (expanding fading rings); (b) TABULAR: new `[columns]` appear as grey skeleton pills, progress fills run left→right staggered top-to-bottom (colored fill with a leading tip), each bar SWAPPING to its real `[value/avatar chip]` on completion. After assembly the array stays LIVE: `[status pills]` flip states in quick snappy swaps (color-coded, several in succession), or the `[headline]` crossfades and a second population wave runs on a newly revealed region — the table content scrolling horizontally beneath a sticky first column to expose it. Hold lands on the fully populated, fully updated final state. - **Variant — Social_Proof logo-wall-zoom-out**: intro beat (`[trusted-by headline]` card OR a `[product screenshot]`) crossfades/cuts to a center logo grid that builds (whole-enter / randomized pop-in / column slide-up); a continuous camera zoom-OUT then shrinks the whole grid toward center to reveal a vast ecosystem and holds; optional fixed HUD/viewfinder brackets; optional exit = whole grid SLIDES UP and flies out through the top. -**motion vocabulary**: item stagger-assemble (fade + short slide/scale into slot) · brick/grid/list layout resolve · randomized pop-in · column slide-up · vertical-list step (slot-machine snap-and-hold) · spring-overshoot marker pop · check/icon draw-in · pill/label mask-wipe reveal · dim-by-position de-emphasis · line-by-line headline fill · accent-gradient number count-up · near-static hold · gentle parallax/sine float on hold · slow camera push-in · camera zoom-OUT reveal (continuous OR phased pull-back) · cards-grow-from-behind-icons · icon-shrink-to-header · center-card scale-up + forward overlap (spring) · traveling-glow sweep · fixed HUD/viewfinder brackets · motion-blur slide-out sweep (exit) · grid slide-up fly-out (exit) · book-end headline fade · perpetual decorative orbit/loop. +**motion vocabulary**: item stagger-assemble (fade + short slide/scale into slot) · brick/grid/list layout resolve · randomized pop-in · column slide-up · vertical-list step (slot-machine snap-and-hold) · spring-overshoot marker pop · check/icon draw-in · pill/label mask-wipe reveal · dim-by-position de-emphasis · line-by-line headline fill · accent-gradient number count-up · near-static hold · gentle parallax/sine float on hold · slow camera push-in · camera zoom-OUT reveal (continuous OR phased pull-back) · cards-grow-from-behind-icons · icon-shrink-to-header · center-card scale-up + forward overlap (spring) · traveling-glow sweep · fixed HUD/viewfinder brackets · motion-blur slide-out sweep (exit) · grid slide-up fly-out (exit) · book-end headline fade · perpetual decorative orbit/loop · skeleton-pill progress fill (left→right, leading tip, color transition) · fill-completes-swap-to-real-data · staggered top-to-bottom fill cascade · live status-pill state flips (color-coded, post-assembly) · tethered-card spring-in (thin line to an anchor marker) · pulsing marker rings · two-wave populate with headline crossfade · sticky-column internal horizontal scroll · rapid vertical stream past a fixed focal slot + deceleration stop · split fixed-label layout · pill-widens-as-label-fills arrival · highlighted hero chip · push-through-the-hero-item exit · receding word-grid depth field · clear-to-payoff coda · price snap-build (split-second partial state) · left-to-right URL reveal + final-beat color flip. **rule mapping** (motion verb → `rule-id`) @@ -50,6 +53,18 @@ Variants (where roles diverge from the template): - traveling-glow sweep across/behind tiles → `ambient-glow-bloom` (one-pass traveling glow sweep across the tiles) - motion-blur slide-out sweep (glass-card exit) → `motion-blur-streak` (directional velocity blur on the fast sweep that carries the cards off-frame) - grid slide-up fly-out exit → `gsap-effects` (plain staggered translate-off-frame; no dedicated rule needed — a basic exit tween, not a missing capability) +- skeleton-pill progress fill → `stat-bars-and-fills` (progress-fill `scaleX` form; the leading tip is a chorded child element) +- fill-completes-swap-to-real-data / live status-pill flips / headline crossfade between waves → `discrete-text-sequence` (whole-state replacement at time thresholds — the pill's states are text states) +- staggered top-to-bottom fill cascade → `gsap-effects` (per-row stagger on the fill tweens) +- tethered-card spring-in → `spring-pop-entrance` (the card) + `avatar-cloud-network` (the thin connection-line-to-anchor layout; anchor coordinates must match the marker exactly) + `svg-path-draw` if the tether draws in +- pulsing marker rings → `cursor-click-ripple` (its expanding-ring + attack-decay opacity envelope, minus the cursor/click, on a bounded repeat) +- sticky-column internal horizontal scroll → `viewport-change` (PAN form on the inner column layer; the sticky column sits outside the panned layer) — mark the moving layer `data-layout-allow-overflow` and clip at the table card +- rapid vertical stream past a focal slot + deceleration stop → `vertical-spring-ticker` (continuous form: one long decelerating translate instead of its stepped tweens; focal-slot emphasis reuses the dim-by-position mapping above) +- pill-widens-as-label-fills → `card-morph-anchor`'s substitution law (uniform `scaleX`/clip-path — never tween `width`) + `discrete-text-sequence` for the label fill +- push-through-the-hero-item exit → `multi-phase-camera` (single accelerating push phase) aimed via `coordinate-target-zoom` at the highlighted chip, crossfading at peak +- receding word-grid depth field → `viewport-change` (one `.world` wrapper, `cam.scale` ↓ continuously — the zoom-OUT reveal grammar pointed at a word field; size/opacity tiers fake the depth) +- price snap-build (split-second partial state) → `discrete-text-sequence` (non-linear typing with bulk additions — exactly its typo/partial-state mechanic) +- left-to-right URL reveal → `techniques.md` (clip-path reveal — same mapping as the pill mask-wipe); the final-beat color flip → `gsap-effects` (a `tl.set` at the beat — basic, no rule needed) **camera modifier — zoom-OUT reveal** (optional; the role-defining move for the glass-card and logo-wall variants): a camera wrapper around the whole array scales DOWN over the hold, revealing the assembled grid/cards sitting inside a larger environment (ecosystem scale, or a row of cards unfolding from tight icons). @@ -59,8 +74,8 @@ Variants (where roles diverge from the template): --- ``` -BLUEPRINT: grid-card-assemble — serves Key_Feature, Benefits, Social_Proof (folded 4 drafts) -RULE GAPS: none — traveling-glow sweep → ambient-glow-bloom; motion-blur slide-out sweep (exit) → motion-blur-streak; grid slide-up fly-out (exit) → gsap-effects (plain translate) +BLUEPRINT: grid-card-assemble — serves Key_Feature, Benefits, Social_Proof (folded 4 drafts + 2 mined clusters: live-data-populate-board, item-field-to-payoff-card) +RULE COVERAGE: complete, no gaps — traveling-glow sweep → ambient-glow-bloom; motion-blur slide-out sweep (exit) → motion-blur-streak; grid slide-up fly-out (exit) → gsap-effects (plain translate); skeleton-fill populate → stat-bars-and-fills + discrete-text-sequence; push-through-hero exit → multi-phase-camera + coordinate-target-zoom ``` Merge tension: `center-outward-expansion` (the natural backing for stagger-assemble) caps cleanly at 3–8 items and explicitly warns 8+ causes mid-flight overlap chaos — but a Social_Proof logo wall is deliberately dense (12+ tiles), so for that variant the items must NOT burst from a shared center; they slide a short distance directly into their own slot (the rule's "starting partially-spread"/short-path form, or a `gsap-effects` per-item stagger), which the consolidated Scene-1 verb already specifies as "short distance directly into its slot." diff --git a/skills/hyperframes-animation/blueprints/kinetic-type-beats.md b/skills/hyperframes-animation/blueprints/kinetic-type-beats.md index 87404d9e7..4d9923351 100644 --- a/skills/hyperframes-animation/blueprints/kinetic-type-beats.md +++ b/skills/hyperframes-animation/blueprints/kinetic-type-beats.md @@ -6,43 +6,72 @@ - Hook (from `hook-kinetic-type-flash`): when one stationary line lands a punchy rhetorical question or "you keep doing X" callout and the in-place token swap itself is the joke. - Hook (from `hook-kinetic-type-escalation`): when ONE statement should escalate across distinct full-screen beats (each a different move) and punctuate on a spring-pop payoff element — a rising-intensity / "transform X into Y" opener. +- Hook (from `kinetic-type-to-logo-reveal`): when rapid centered word beats are the warm-up for a typography-to-brand arc — the swaps resolve into a logo reveal (pop-in whole, or 3D parts assemble and flatten into the flat mark) that hands off to a value card / browser mockup sliding in. +- Hook (from `centered-beat-triptych`): when the open is three center-stage beats on a constant field, each element ALONE on screen — and a beat's payload may be non-text (a logo-lockup rotation-snap, a CTA button spring-pop with a one-shot glow-ring pulse, a benchmark chart that builds and holds); the last beat holds to the end. - Problem (from `problem-kinetic-type-beats`): when the script is 3–5 short pain statements (or a "what-if?" framing) that should each land alone, on a bare canvas, before the next replaces it — no product visible yet. +- Problem (from `centered-phrase-relay-question-hook`): when the pain is an ordered chain of question/hook phrases that scale-pop through center relay-style (each exits as the next arrives) and land a specially-styled climax word — OR resolve the question on a `[product surface]` entering as an element move, never a camera zoom. - Product_Intro (from `product-intro-kinetic-type-namedrop`): when the hook IS the words — hard-cut through "Introducing…" / tagline / value beats and resolve on the brand name or logo. +- Product_Intro (from `fixed-line-word-swap`): when a fixed headline holds and ONLY one word-slot changes — cursor-deleted-and-retyped once (optionally by a labeled collaborative cursor) or rapid-cycled through a `[role]` list — then hands off to the product/brand payoff; the purest sub-shape A. +- Product_Intro (from `flat-field-kinetic-word-run`): when a sentence builds word-by-word on a flat brand-color field and each `[hero word]` earns a bespoke one-shot effect payoff (letter-scramble, chromatic glitch, confetti burst, emoji morph) before a punch-word finale. +- Product_Intro (from `anchored-wordmark-transform`): when the anchored type itself mutates — an "Introducing" / predecessor beat builds or swaps into the `[wordmark]` in place, which then TRANSFORMS (a UI collage rushes outward from behind it, a word morphs into a pulsing icon, the title zoom-blurs away) into a short payoff beat. - Benefits (from `benefits-kinetic-type`): when "what you get" is a rapid-fire staccato montage — 8–12 short value phrases, each flashing and clearing before the next at high tempo. +- Benefits (from `flat-void-statement-relay`): when the value reads as a SLOW statement relay — 2–4 full statements on a flat void, each built by its own engine (typewriter, oversized element-scroll, outline-echo stack, wave-mapped pattern) and held ~1.5s+ before the hard cut; the low-tempo sibling of the staccato montage. - CTA (from `cta-kinetic-type`): when the sign-off is a punchy closing line (or a short stack of value lines) that snaps/fades in beat-by-beat and lands on the brand lockup or URL — no spatial set, no clicked button. +- CTA (from `kinetic-beat-chain-to-logo`): when the sign-off chains 3–5 message beats and each beat carries a DIFFERENT kinetic gag (marquee scroll-through, flash-swap word list, brief 3D letter extrude, spring-bounce prop, one interleaved mock-UI beat) before the logo/URL forms — optionally out of a preceding glow pulse — and holds. - Brand_Outro (from `brand-outro-kinetic-type-resolve`): when the close is a rapid center-channel barrage of single-word verbs asserting breadth, resolving on the brand's one defining word (motion-is-the-message, no logo lockup). +- Brand_Outro (from `centered-beat-relay-to-url`): when the close is a short relay of full-frame beats — fade/scale swaps, a spring shrink-to-0, an `[icon]` bounce, a gradient-swept title card — terminating in a centered `[URL / domain]` end card held for the longest stretch of the shot (~40–75% of runtime); an optional `[product UI]` prologue scales down and fades to the canvas first. -**duration**: 3.4–12s (Benefits fastest ~3.5–4s at 8–12 sub-0.5s beats; Brand_Outro ~3.6s; Problem longest 7–12s; CTA spans 3.6–11.7s with beat count) +**duration**: 3.0–12.9s (Benefits staccato fastest ~3.5–4s at 8–12 sub-0.5s beats, statement-relay Benefits up to ~8.3s; Product_Intro fixed-line as short as ~3.0s; Problem 4.1–12s; CTA spans 3.6–12.9s with beat count; Brand_Outro ~3.6s as a verb barrage, up to ~12.6s when the terminal URL hold carries 40–75% of the runtime) -**shot structure** (flat, fixed center anchor; bold sans-serif text on a solid `[bg color]`; type/tokens are the only subject; camera locked unless a modifier is noted; two folded sub-shapes — **(A) fixed-line token swap** and **(B) multi-beat statement build**) +**shot structure** (flat, fixed center anchor; bold sans-serif text on a solid `[bg color]`; type/tokens are the default subject, though a beat's payload may be ONE non-text center-stage element — a logo lockup, a CTA button, a chart — obeying the same arrive-hold-clear law; camera locked unless a modifier is noted; two folded sub-shapes — **(A) fixed-line token swap** and **(B) multi-beat statement build**) - **Scene 1 (0.0–~1.0s) — first beat lands.** Solid `[bg color]` field. Bold `[type color]` text arrives dead-center via ONE entrance: type-on character-by-character with a trailing blinking caret, OR a hard-cut FLASH-in (no fade/slide), OR a per-word staggered fade/blur, OR an oversized word that smoothly SCALES DOWN to a small centered word. An optional `[accent color]` move plays on the key word(s): a left→right drawn underline / strike-through, a small particle/dot burst from behind the text, or a `[accent color]` selection-box framing the word. - _Variant — Hook (flash)_: just the fixed `[hook line]` (or its first word) parks at center; no escalation move. - _Variant — Hook (escalation)_: `[beat 1 text]` arrives big and scale-downs to centered, OR sits over a glowing `[motif]` with a slow camera push-in (see camera modifier); ends on a hard cut. + - _Variant — Hook (logo reveal)_: centered bold words swap in with quick spring-scale pops on a flat/gradient field while flat `[accent]` circles/dots drift idly; a beat may hard-cut to a contrast bg and enter with an RGB-split glitch stretch that snaps sharp. + - _Variant — Hook (triptych)_: beat 1 may be non-text — a `[logo mark]` rotates in 3D and snaps flat beside a `[version tag]`, or a statement resolves via a horizontal stretch/slice glitch on a subtle `[grid card]` — holds, then clears (scale-down + fade, or hard cut). - _Variant — Problem_: centered `[pain line 1]` reveals in chunks across one or two lines with its `[accent]` underline / particle burst. + - _Variant — Problem (relay)_: `[hook phrase 1]` scale-pops into center with a quick spring on a flat solid OR drifting-gradient field — optionally the background itself morphs open first (a rounded `[accent shape]` expands into the full-bleed gradient); as the phrase holds, its word-spacing spreads slightly. - _Variant — Product_Intro_: bold `[hook word, e.g. "Introducing"]` enters with a typographic accent (split-and-slide apart, drawn underline, or `[accent]` selection-box). + - _Variant — Product_Intro (fixed-line)_: the full fixed headline `[fixed phrase] [swap-slot]` parks centered (a faint `[plexus / ambient pattern]` may drift behind); no escalation move — the slot is the show. + - _Variant — Product_Intro (word-run)_: a field-claiming open — horizontal `[brand color]` stripe wipes reveal the `[logo lockup]` then clear, or a giant blob expands from center repainting the frame in the brand color — before the sentence starts building. + - _Variant — Product_Intro (wordmark transform)_: "Introducing" fades in over ambient sine-wave lines that undulate then snap taut, OR the `[old version wordmark]` holds and swaps away, OR oversized scattered `[gradient]` letters bounce-assemble into the `[name]` while the whole word scales down to center. - _Variant — Brand_Outro_: optional single-frame flash of `[product UI / hero asset]` precedes the verb channel, then `[verb 1]` hard-cuts in centered. + - _Variant — Brand_Outro (relay-to-URL)_: optional prologue — the `[product UI window]` scrolls its content, then scales down and fades out to the flat canvas; the first text beat fades/scales in centered. - **Scene 2..N — beats replace each other in place (the engine).** The center anchor advances one beat at a time; nothing from the prior beat lingers. Choose the swap mechanism by sub-shape: - - **Sub-shape A (fixed-line token swap)**: the line stays fixed and only the variable slot changes by an instant hard CUT (no roll/scroll/blur) — `[token A]` → `[token B]` → `[token C]` — OR the final word(s) backspace out and a new word retypes (`[word A]` → `[word B]`). The rest of the line holds. + - **Sub-shape A (fixed-line token swap)**: the line stays fixed and only the variable slot changes by an instant hard CUT (no roll/scroll/blur) — `[token A]` → `[token B]` → `[token C]` — OR the final word(s) backspace out and a new word retypes (`[word A]` → `[word B]`). The rest of the line holds. The cycle may run a rapid `[role word]` list at the fixed slot, and the delete-retype may be performed by a labeled collaborative cursor; a faint `[plexus / ambient pattern]` may keep drifting behind the fixed line. - **Sub-shape B (multi-beat statement build)**: each full-screen beat hard-cuts to a NEW background/line, and each gets its own distinct entrance/exit MOVE — springy scale-in/scale-out overshoot, 3D letter-tumble (glyphs scatter into a rotating depth cloud, then reassemble into the next phrase), motion-blur fly-in that resolves sharp at center, prior text accelerates/zooms past the camera while fading, letter-spacing collapse, or a bottom-up masked slide. Background may hard-flip `[bg A]`↔`[bg B]` on selected beats with `[type color]` inverting to stay legible. - _Variant — Hook (escalation)_: beat 2 `[beat 2 text]` (more emphatic) snaps in; beat 3 `[beat 3 text]` (climax) holds, then a transition-out move on the type itself — a Z-dolly forward THROUGH an oversized glyph, OR a per-word karaoke highlight sweep lighting words left→right. + - _Variant — Hook (triptych)_: a mid beat may be non-text — a `[CTA button]` spring-pops with overshoot, fires a one-shot blurry glow-ring pulse outward, and settles smaller — the element alone on screen, then cleared like any other beat. - _Variant — Problem_: each `[pain line k]` enters by chunk-reveal or motion-blur fly-in as the prior blurs/zooms off; an optional `[accent color]` interstitial word ("[what-if hook]") scales up from center, holds, then zooms past the camera and fades. + - _Variant — Problem (relay)_: each `[phrase]` scale-pops into center while the prior shrinks and split-slides off toward BOTH left/right edges (clipping off-screen) with fade; an optional emphasis beat lands on a hard-cut contrast bg — a single `[word]` letter-tracking-tightens from wide spacing while scaling up as four thin `[accent]` arrows shoot in diagonally from the corners, converging on it; a left-aligned line-by-line value build may interleave. - _Variant — Product_Intro_: each `[tagline phrase]` is a hard-cut/push-through inverted-text beat with its own one-shot accent (strike-through, slider/toggle shapes sliding in, or a bg-invert cycle white→`[accent]`→black flipping fg/bg). + - _Variant — Product_Intro (word-run)_: the `[sentence]` builds word-by-word with snappy pops (lines re-center as they add; an underline may draw beneath key words), then one beat per `[hero word]` — each lands large and performs its own one-shot effect: a letter-scramble resolve (with thin divider ticks), a chromatic-glitch jitter (offset color copies snapping back clean), a spring bounce + confetti burst that erupts up and drifts down, or a letter-slot swapped for a springing `[emoji / mark]` that morphs; the finale may run an alternating huge/small word scale chain. + - _Variant — Product_Intro (wordmark transform)_: the `[wordmark]` completes in place — staggered part-by-part pop (`[part 1]` then `[part 2]`), an in-place swap replacing "Introducing", or a `[second phrase]` appending — with a gradient hue-sweep across the type that settles to a solid color snap. - _Variant — Benefits_: high tempo (~0.4s/beat) — each `[benefit phrase]` pops via springy scale-in/out or 3D letter-tumble; multiple bg light↔dark flips across the run with text-color invert. + - _Variant — Benefits (statement relay)_: low tempo — each `[statement]` builds by its own engine and HOLDS ~1.5s+ before the hard cut: line 2 types char-by-char under a static line 1; an oversized `[phrase]` element-scrolls right→left through the frame (a moving window onto a wider line, a gradient sweeping the letters); a solid `[word]` holds while stacked outline-only echo copies cycle vertically behind it; a multi-line block builds fast as small `[accent shapes]` fly in from the edges then drift outward and thin (text may form as a masked grey fill, then snap solid). - _Variant — CTA_: each `[value line]` → `[value line]` → `[CTA verb line]` clears by hard cut / zoom-blur cut through near-black / fade-out, then the next pops/fades/slides in. Optional `[accent motif]` draws on behind (rising line-graph trim-path, thin wireframe guides, gutter geometry tiles). + - _Variant — CTA (beat-chain)_: individual beats carry their own gag — a line enters right and marquee-scrolls continuously left across the frame (exiting); a `[use-case word]` list flash-swaps in place; a beat's letters briefly extrude into simple 3D and flatten back; a `[glyph + prop]` group spring-bounces in then slides off; ONE mock `[compose-window / product UI]` beat may interleave without breaking the chain. - _Variant — Brand_Outro_: a centered single `[verb / keyword]` HARD-CUTS to the next at a steady ~0.2s cadence (no fade/scale) over a continuous moving field (see camera modifier). + - _Variant — Brand_Outro (relay-to-URL)_: 2–3 full-frame beats swap wholesale at a relaxed cadence — each fades/scales in and out, or scales up slightly then spring-shrinks to 0%, or an `[icon]` bounce-pops in from 0% and shrinks back out, or a `[title card]` holds with a continuous in-text horizontal gradient sweep before a HARD CUT to the bare canvas. - **Scene N (final beat → end) — resolve and HOLD.** The last beat lands and holds to the end (settle only, no further scale-out). Resolution diverges by role: - _Variant — Hook (flash)_: last token swap lands and holds; optional tiny punctuation/emphasis snap (`?` → `?!`, or fill snaps to `[accent color]`). - _Variant — Hook (escalation)_: resolve on `[payoff bg]` — a `[payoff element]` (colored square / heart-eyes reaction emoji) SPRING-POPS in center; small `[accent motes]` drift outward; subtle settle. + - _Variant — Hook (logo reveal)_: the word beats resolve on the brand — the `[logo]` pops in whole, or floating 3D `[shapes]` assemble and FLATTEN into the flat 2D mark as the `[wordmark]` slides in beside it; then a `[browser mockup / value card]` slides/scales in on a fresh bg and holds (a bottom caption may build). + - _Variant — Hook (triptych)_: the final beat may be non-text — a `[benchmark chart]` fades in its framework and grows bars from zero width in a top-down stagger (the `[hero row]` bold/highlighted), then holds static for the back half of the shot; or a closing statement glitch-reveals and holds. - _Variant — Problem_: final `[pain line]` reveals (left→right swipe with leading-edge blur, OR letters explode radially then the resolving line fades up); holds the pain on screen. + - _Variant — Problem (relay)_: the climax `[word]` scales in with special treatment (gradient fill, slight ~-8° rotation) and holds — OR the question resolves on a `[product surface]` as an ELEMENT move: a `[pill / search bar]` slides in from the right and keeps traveling leftward while its text progressively reveals (may end mid-slide, phrase cropped at the frame edge), or the `[page canvas]` scales down while `[app chrome + side panels]` slide in and frame it. - _Variant — Product_Intro_: resolve on the brand — `[logo mark]` / `[wordmark]` pops in centered (optional sting: liquid/ink splash, blob backing), OR the final value word holds inside an expanding-iris `[accent]` circle that scales to fill frame and hard-cuts the closing word through it. + - _Variant — Product_Intro (wordmark transform)_: with the completed `[wordmark]` anchored dead-center, a dense `[UI-screenshot collage]` rushes in and expands outward from behind the text toward the frame edges with parallax (fast pull-back feel), then clears quickly to a clean `[wordmark]` end card; OR one `[word]` morphs into a pulsing `[icon]` completing an icon+text lockup before the field dissolves to its inverse; OR the title rapidly scales up and zoom-blurs away as the next context fades in. - _Variant — Benefits_: the last `[benefit phrase]` arrives (optionally on the inverted bg) and SETTLES — does not scale/tumble back out. - _Variant — CTA_: land on the lockup — `[logo mark]` SCALES UP small→full and holds, OR a `[logo]`/`[url]` builds segment-by-segment beside its icon. End-card holds dead static. + - _Variant — CTA (glow-preceded formation)_: the prior letters scatter/clear, a soft `[accent]` glow pulses on the empty field, and the `[logo mark]` FORMS out of the glow with the `[url]` wordmark below; holds to the final frame. - _Variant — Brand_Outro_: hard cut to the `[resolve word / brand keyword]` (longest, still centered); HOLDS ~0.5s while the background field keeps moving. + - _Variant — Brand_Outro (relay-to-URL)_: the centered `[URL / domain]` (+ optional CTA line above) fades/scales in and holds — the LONGEST beat of the shot, ~40–75% of the runtime — optionally fading at the very tail. -**motion vocabulary**: hard-cut / flash word swaps; in-place token cycle (instant cut, no roll/scroll/blur); type-on with trailing blinking caret; backspace-and-retype; per-word staggered fade/blur reveal; big→small scale-down; springy scale-in/scale-out overshoot; 3D letter-tumble scatter-and-reassemble; motion-blur fly-in / blur-off; prior text zoom-through-camera; letter-spacing collapse; bottom-up masked slide; drawn-on `[accent]` underline / strike-through; particle/dot burst from text; `[accent]` selection-box frame; bg-invert hard-flip with text-color invert; karaoke per-word highlight sweep; radial letter-explode; expanding-iris circle wipe-to-next; final spring-pop payoff element (square / emoji / logo mark); drifting `[accent]` motes / ambient shapes; segment-by-segment URL/wordmark build; final-token punctuation snap; settle-and-hold. +**motion vocabulary**: hard-cut / flash word swaps; in-place token cycle (instant cut, no roll/scroll/blur); type-on with trailing blinking caret; backspace-and-retype; per-word staggered fade/blur reveal; big→small scale-down; springy scale-in/scale-out overshoot; 3D letter-tumble scatter-and-reassemble; motion-blur fly-in / blur-off; prior text zoom-through-camera; letter-spacing collapse; bottom-up masked slide; drawn-on `[accent]` underline / strike-through; particle/dot burst from text; `[accent]` selection-box frame; bg-invert hard-flip with text-color invert; karaoke per-word highlight sweep; radial letter-explode; expanding-iris circle wipe-to-next; final spring-pop payoff element (square / emoji / logo mark); drifting `[accent]` motes / ambient shapes; segment-by-segment URL/wordmark build; final-token punctuation snap; settle-and-hold; scale-pop phrase relay (prior shrinks + split-slides off both edges with clip-fade); letter-tracking tighten-from-wide while scaling; corner arrows converging on a word; gradient-fill / hue-sweep across type with settle-to-solid snap; in-text traveling gradient sweep; background shape morph-open into a full-bleed field; RGB-split / chromatic-glitch jitter; horizontal stretch/slice glitch reveal; letter-scramble resolve with divider ticks; confetti burst up-and-drift; letter-slot emoji/mark swap + morph; alternating huge/small word scale chain; color-stripe wipes / blob expand frame-repaint; oversized phrase element-scroll (moving window); right→left marquee scroll-through; stacked outline-echo copies cycling behind a solid word; full-frame repeating-word pattern on a rolling 3D wave; accent shapes fly-in then drift-out-and-thin; masked grey fill snapping solid; brief 3D letter extrude-then-flatten; spring-bounce glyph+prop drop-in; glow-pulse-preceded logo formation; 3D shapes assemble-and-flatten into the mark; logo-lockup 3D rotation-snap; one-shot glow-ring pulse; chart bars growing in a top-down stagger; labeled collaborative cursor delete-and-retype; in-place role-word cycle; ambient plexus/pattern drift; spring shrink-to-0 exit / bounce-in from 0%; scattered-letter bounce-assembly with baseline settle; staggered wordmark part pop; phrase append; word→icon morph with continuous pulse; UI-collage rush-out with parallax from behind anchored type; zoom-blur title exit; long-held URL end card. **rule mapping** @@ -69,10 +98,36 @@ - motion-blur fly-in / blur-off / zoom-through-camera streak on type → `motion-blur-streak` (directional velocity blur on a fast fly-in / zoom-through; the heavy motion-blur smear resolves sharp at center) - radial letter-explode (glyphs explode outward radially then resolve) → `depth-scatter-assemble` (radial per-letter explode-and-resolve is in scope alongside the depth-cloud scatter) - 3D letter-tumble depth-cloud scatter-and-reassemble → `depth-scatter-assemble` (free tumbling depth-cloud that flies out and snaps back into the next phrase) +- scale-pop phrase relay → `spring-pop-entrance` (the arriving phrase) + `gsap-effects` (the prior phrase's shrink + split-slide clear toward both edges) +- letter-tracking tighten-from-wide while scaling → `gsap-effects` (letter-spacing tween — the inverse of the letter-spacing collapse mapped above) +- corner arrows converging on a word → `css-marker-patterns` (burst geometry with inverted travel — lines converge instead of radiate) + `gsap-effects` +- gradient-fill climax word / hue-sweep across type / in-text traveling gradient sweep → `gradient-text-sweep` (gradient tweened THROUGH letterforms — position/hue sweep with settle-to-solid snap, seek-safe) +- background shape morph-open into a full-bleed field → `card-morph-anchor` (uniform scale + borderRadius paint tween, then the field takes over) +- RGB-split / chromatic-glitch jitter; horizontal stretch/slice glitch reveal → `chromatic-glitch` (deterministic offset color-copy layers, jitter + snap-clean; covers the stretch/slice glitch reveal) +- letter-scramble resolve with divider ticks → `hacker-flip-3d` (the deterministic glyph-substitution decode, minus the 3D rotation) +- 3D shapes assemble-and-flatten into the mark; scattered-letter bounce-assembly → `depth-scatter-assemble` (scatter-to-clean-layout settle) + `spring-pop-entrance` (the bounce settle) +- logo-lockup 3D rotation-snap → `orbit-3d-entry` (the 3D flip-in entry, skipping the orbit phase) +- one-shot glow-ring pulse; glow-pulse-preceded logo formation → `ambient-glow-bloom` (single-pass bloom-and-fade) + `spring-pop-entrance` (the mark forming out of it) +- chart framework fade-in + bars growing from zero width top-down; radial gauge arc-draw + count-up → `stat-bars-and-fills` (+ `counting-dynamic-scale` for the ticking value) +- labeled collaborative cursor delete-and-retype; in-place role-word cycle → `discrete-text-sequence` + `context-sensitive-cursor` (the labeled-pointer look itself is oversized-cursor doctrine, not a rule) +- ambient plexus/pattern drift; accent shapes drift-out-and-thin → `sine-wave-loop` (finite drift) after a `spring-pop-entrance` arrival +- letter-slot emoji/mark swap + morph; word→icon morph with continuous pulse → `scale-swap-transition` (same-center morph) + `svg-icon-enrichment` (the icon's internal pulse) +- oversized phrase element-scroll; right→left marquee scroll-through → `gsap-effects` (linear translate of an oversized element through a static frame) +- stacked outline-echo copies cycling behind a solid word → `3d-text-depth-layers` (the offset echo stack) + `vertical-spring-ticker` (the vertical cycle) +- brief 3D letter extrude-then-flatten → `3d-text-depth-layers` (build the extrusion offsets, then collapse them) +- full-frame repeating-word pattern on a rolling 3D wave → flagged special — a 3D wave-mapped text field is out of rule scope; `sine-wave-loop` only drives the undulation oscillator +- alternating huge/small word scale chain → `kinetic-beat-slam` (distinct per-beat entrances on the shared beat array) +- color-stripe wipes → `gsap-effects` (masked translate tweens); blob expand frame-repaint → `card-morph-anchor` +- UI-collage rush-out with parallax from behind anchored type → `center-outward-expansion` (clustered-at-center → outward to final positions; vary per-tile rates/scales for the parallax read) +- spring shrink-to-0 exit / bounce-in from 0% → `spring-pop-entrance` (in) / `gsap-effects` `back.in` shrink (out) +- product-surface resolve (pill slide with progressive text reveal; canvas scale-down as chrome frames in) → `nudge-curve` (the slide that reveals during travel) + `gsap-effects` (coordinated scale + panel slides) +- zoom-blur title exit as an in-shot beat handoff → `motion-blur-streak`; as a scene-out into the next scene it belongs to the transition layer +- staggered wordmark part pop / phrase append → `spring-pop-entrance` + `dynamic-content-sequencing` **camera modifier** (optional, layered over the flat shot; most variants are camera-locked) - Slow continuous global zoom-in / uniform push-in running underneath the whole sequence (Problem, Brand_Outro) → `multi-phase-camera` (push phase) — gives parallax between the fixed type and a moving background field. - Camera dolly/zoom forward THROUGH an oversized glyph along Z as a beat transition-out (Hook escalation, Product_Intro push-through) → `coordinate-target-zoom` (target the glyph center) or `multi-phase-camera` (push). - Slow push-in on Scene 1 over a glowing `[motif]` (Hook escalation) → `multi-phase-camera` (push) or `coordinate-target-zoom`. +- Slow continuous card/scene scale-up running UNDER hard-cut beats (Hook triptych) — a push-in feel rendered as element scale on the scene group, never a real dolly → `multi-phase-camera` (push phase) or a plain `gsap-effects` scale tween. - Note: the in-place token swap (sub-shape A) and most Benefits/Hook-flash/CTA variants are fully camera-static — the swap is the only motion. diff --git a/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md b/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md index 7fc2bb793..554e175ab 100644 --- a/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md +++ b/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md @@ -1,6 +1,6 @@ # logo-assemble-lockup — Logo Assemble → Lockup -**intent**: A brand mark / wordmark builds itself from parts (elements assemble or orbit in, letters cascade, an outline draws on, or a camera pushes through negative space) and resolves into a centered logo lockup — optionally extended into a final URL / CTA. +**intent**: A brand mark / wordmark comes to exist on screen and resolves into a centered logo lockup — built from parts (elements assemble or orbit in, letters cascade, an outline draws on, or a camera pushes through negative space), spring-BLOOMED whole from zero on a cleared stage, MORPHED in one unbroken chain out of the preceding phrase / glyph, absorbed from a kinetic streak, or already assembled and settling as decorations clear — optionally extended into a final URL / CTA / end card. **roles served** @@ -9,8 +9,13 @@ - CTA (from cta-button-wordmark-build): The "draws-its-own-outline → wordmark-builds-letter-by-letter" sub-shape — a `[CTA button]` pill strokes its own glowing border, a diagonal-band WIPE flips the frame, and the `[wordmark]` types in beside a slash to land the lockup. Camera static. - Brand_Outro (from brand-outro-assemble-logo-lockup): The closing mark — a formation of `[feature pills / UI elements]` CLEARS the stage off all four edges, then on the empty frame the `[logo mark]` draws itself on stroke-by-stroke and the `[wordmark]` reveals to complete the lockup, then fades out. - Product_Intro (from brand-reveal-assemble-zoom): a context-then-focus reveal — a companion tagline TYPES out to set context, the hero mark pops in beside it, then the companion exits as the layout recenters and the camera pushes IN to a held close-up on the mark (wide composition narrowing to a tight focus). +- Product_Intro (from logo-parts-lockup-assembly): the literal parts build — `[icon parts]` (a glowing dot traces a circle, semi-circles scale up and overlap, strokes rotate in) converge into the `[brand icon]` center-frame on a flat / gradient field, the `[wordmark]` joins (± a `[badge pill]` pops onto the lockup), then a payoff beat: a stepped bottom `[subtitle rail]`, a big `[count-up stat]` over a faint asset grid, or the lockup clears and a `[product UI window]` scales in. Static frame, all element-level. +- CTA (from text-clears-mark-blooms-lockup): the text-clear BLOOM — centered `[serif tagline]` beats (word-by-word staggered fades) hold, then CLEAR themselves to a blank frame; the `[brand mark]` spring-blooms from ZERO at dead center, slides left as the `[wordmark]` reveals to its right, and the balanced lockup holds (near-)still. Constant warm flat bg, static frame. +- Brand_Outro (from phrase-morphs-into-lockup): the MORPH chain — a centered `[phrase]` mutates in place, then collapses / swaps into an `[intermediate glyph]` whose line panels fan-and-flip around a central pivot with visible motion blur (page-flip feel) and interlock into the `[geometric mark]`, which slides apart into the lockup. One unbroken chain of transformation, never a cut-and-replace assembly; the finished lockup holds dead static for the final ~40–50% of runtime. +- Brand_Outro (from lead-text-then-mark-assembles): the parts-arrive build — a `[hand-off line]` holds and departs, then the mark is BUILT from arriving parts (`[icon]` drops in, letters slide in one by one, terminal punctuation lands, a confetti burst pops and instantly shrinks) OR a `[pixel stack]` streaks into full-width multicolor stripes whose tail retracts and is ABSORBED into the pixel mark — finishing as a lockup or a full end card (`[icon tile]` + `[title]` + `[URL pill]` + store badges) held static. +- Brand_Outro (from `settled-lockup-reveal`): the null-assembly boundary — the `[lockup]` is on stage from frame one; `[satellite shapes]` drift outward and fade, an accent underline sweeps beneath the wordmark, and the `[tagline]` wipes in to complete it. Settle-and-reveal: no predecessor beat, no morph, no relay. -**duration**: ~4.6–11.0s (Brand_Outro ~4.6s · brand-reveal ~5s · Product_Intro ~7s · CTA 5.4–11.0s) +**duration**: ~4.4–11.0s (Brand_Outro ~4.4–7.3s · brand-reveal ~5s · CTA text-clear bloom 6.0–8.9s · Product_Intro ~7s orbit sting, 7.0–9.8s parts-assembly · CTA push/build 5.4–11.0s) **shot structure** (one consolidated time-coded template; `[slots]` are product-agnostic) @@ -19,20 +24,35 @@ - _Variant — CTA push_: on a `[bg gradient]`, the `[logo mark]` is settling in object space (a 3D mark with thin wireframe edge-guides + a faint bracket motif behind center); a very slow continuous camera push-in may already be creeping. - _Variant — CTA button-build_: on a `[dark grid bg]`, a rounded `[CTA button "label"]` pill rises / scales into center (a prior headline clearing off the top); its thin border DRAWS ON as an animated glowing outline STROKE, with a small `[accent]` comet / spark icon at its left edge. - _Variant — Brand_Outro_: a PRE-ARRANGED formation of `[feature pills / element grid]` (each `[icon]`+`[label]`) DISPERSES — elements slide outward from their laid-out positions and fly off all four frame edges (edge-clearing drift, NOT a center-origin burst), emptying the frame onto a clean `[bg]`. + - _Variant — Product_Intro parts-assembly_ (from logo-parts-lockup-assembly): optional text hook — a centered "`[Meet product]`" line wipes away right→left — or straight into the build; on a flat / gradient `[bg]`, the first `[icon parts]` arrive: a glowing dot traces a clockwise circle, a gradient semi-circle scales up inside it, or the mark scales-up-with-rotate into center. + - _Variant — CTA text-clear bloom_ (from text-clears-mark-blooms-lockup): a centered `[serif tagline / question]` (± an outlined `[badge pill]`) finishes a left→right word-staggered reveal in the first ~0.5–1s (each word passing light-grey→dark) and HOLDS; optional rolling word-by-word swap to a second `[availability line]`. Then the CLEAR: text exits — shrink-toward-center + fade, or word-by-word left-first fade-out — leaving a blank frame for a beat. + - _Variant — Brand_Outro morph-chain_ (from phrase-morphs-into-lockup): a centered `[phrase]` completes or mutates in place (a vertical slot-machine word swap — one word exits up as its replacement rises from below, rest of the line fixed — or a word-by-word landing) and holds. Nothing clears: the phrase IS the raw material for the mark. + - _Variant — Brand_Outro parts-arrive_ (from lead-text-then-mark-assembles): a centered `[hand-off line: tagline / "Brought to you by"]` holds on a flat canvas, then exits — slides straight down off-frame with fade, or fades away behind the incoming flourish. + - _Variant — Brand_Outro settled-reveal_ (from settled-lockup-reveal): the `[lockup]` is already centered at t=0; `[satellite shapes]` drift slowly outward around it — an INVERTED clear: the decorations leave, the mark stays. - Scene 2 — assemble the mark (~1.0–~Ys): the mark builds itself from parts. - _Variant — Product_Intro_: seed dots SCALE UP into flat `[accent]` shapes arranged on the rings; concentric bands ripple outward (tunneling feel) and the shapes begin to ORBIT / drift around the still-fixed center. - _Variant — CTA push_: the `[wordmark]` CASCADES out from behind the mark (letters left→right with overshoot) into the full `[brand lockup]`; the 3D mark may assemble in beats (a terminal detaches + pops as a spring dot, a part hinges-open-and-snaps-shut elastic). Optional beat: a `[cursor]` arcs in and "clicks" the wordmark, OR a frosted-glass pill holding an intermediate `[CTA line]` springs in while layered mark shells fan to the edges. - _Variant — CTA button-build_: a graphic WIPE flips the frame to `[contrast bg]` — a thin `[accent]` diagonal line sweeps in, swells into a full-frame diagonal BAND, then collapses to a small `[accent]` slash. - _Variant — Brand_Outro_: on the now-clear frame, the `[logo mark]` DRAWS ON via stroke (built arc-by-arc / segment-by-segment). + - _Variant — Product_Intro parts-assembly_: the overlapping parts COMPLETE the `[brand icon]` (a second circle overlaps to close the orb; strokes interlock); the `[wordmark]` slides out from behind the icon or in from its right; a small `[badge pill]` pops onto the lockup. + - _Variant — CTA text-clear bloom_: on the blank frame the `[brand mark]` scales up from ZERO at dead center with a snappy spring ease (slight overshoot, hint of rotation as it grows) — the whole mark at once, no parts. + - _Variant — Brand_Outro morph-chain_: the phrase collapses / wipes horizontally into the mark, OR is instantly swapped at the same center for a line-art `[intermediate icon]` whose strokes split into panels that fan-and-flip around a central pivot with visible motion blur, interlock-settling into the `[geometric mark]`. Never a cut to the finished logo — the transformation must stay unbroken. + - _Variant — Brand_Outro parts-arrive_: the mark is BUILT from arriving parts — the `[icon]` drops in from above, letters slide in one by one, terminal punctuation lands, a tiny confetti burst pops and instantly shrinks — OR a colored `[pixel stack]` pops in at a text edge, shoots horizontally stretching into full-width multicolor stripes, then the stripe tail retracts and is ABSORBED into the `[pixel mark]` (mask retraction). + - _Variant — Brand_Outro settled-reveal_: an accent underline sweeps left→right beneath the `[wordmark]` — the only "build" this variant performs. - Scene 3 — resolve to lockup (~Ys–end): the lockup completes and holds (Product_Intro / Brand_Outro) or is flown into / extended to a CTA (CTA variants). - _Variant — Product_Intro (the ONE camera move)_: the whole system smoothly TILTS from flat top-down into an angled isometric perspective (ease-in-out) with a slight zoom-out — flat shapes become luminous 3D forms, bands become glowing orbit lines, while the central `[logo mark]` does NOT tilt (stays 2D, front-facing, fixed). Camera eases to a stop; elements keep continuous orbit/drift (inner faster than outer); the mark holds its steady glow. Final settled frame. - _Variant — CTA push (the signature)_: a single fast CAMERA PUSH-THROUGH the mark's negative space / through the glass pill — heavy horizontal motion-blur, giant `[CTA]` letters streaking past the lens (cursor drops out). Resolves to the final lockup on a saturated `[bg]`: a `[url badge]` / `[CTA line]` revealed by a left→right WIPE carrying an `[accent]` leading edge (or a clean fade), with solid mark-shapes parallax-sliding in behind. Settles to a dead-static hold (slow zoom-out / settle). - _Variant — CTA button-build_: the `[wordmark]` BUILDS letter-by-letter to the right of the slash, landing on the final "`[slash] [WORDMARK]`" lockup centered on the new bg. Slow settle to static. - _Variant — Brand_Outro_: the `[wordmark]` reveals beside the drawn mark (slide / fade) to complete the `[lockup]`; the lockup holds, then fades to `[black / bg]`. + - _Variant — Product_Intro parts-assembly (the payoff beat)_: the finished lockup holds while a bottom `[subtitle box]` steps through `[tagline fragments]` (swap-in-place); or a big `[count-up stat]` line lands over a faint background asset grid; or the lockup scales-down / fades and a `[product UI window]` scales up on the flat bg (its panel content may swap once). The build hands off to product proof. + - _Variant — CTA text-clear bloom_: the mark slides a short distance LEFT while the `[wordmark]` reveals to its right (letter-by-letter / slide-out wipe with visible partial states); the balanced "`[mark] + [wordmark]`" lockup centers and holds, one member continuing an almost imperceptible slow scale-up through the hold. + - _Variant — Brand_Outro morph-chain_: the mark slides left as the `[wordmark]` is pulled out rightward trailing a motion-blur streak, the pair decelerating into the centered lockup (± a `[sub-line]` fades in below). The hold is LONG — dead static for the final ~40–50% of runtime. + - _Variant — Brand_Outro parts-arrive_: the lockup rests centered and holds; or the full end card completes — a rounded-square `[icon tile]` scales up behind the mark, the `[title]` fades in word-by-word, and a bottom row (`[URL pill]` + `[store badges]`) fades / slides up — then holds static. + - _Variant — Brand_Outro settled-reveal_: the `[tagline]` reveals left→right below the wordmark; the satellites finish drifting out and fade; the lockup holds centered (at most a very slow global zoom-out, no pan). -**motion vocabulary**: ring pulse / expand; background crossfade (light→dark); glow ignite; seed-dot scale-up; continuous orbit / drift (inner faster than outer); single 3D perspective tilt (flat→isometric) + slight zoom-out around a fixed 2D anchor; 3D logo assemble (part detach + spring dot, clapperboard hinge / snap, shell fan-out); wordmark cascade with overshoot (letters left→right); button pill rise / scale-in; animated stroke-outline DRAW + glow (button border AND logo mark); comet / spark accent; diagonal-band wipe (sweep → swell → collapse-to-slash); letter-by-letter wordmark build; pre-formed grid DISPERSE off all four edges; logo-mark stroke-draw (sequential arcs / segments); fast CAMERA PUSH-THROUGH with motion-blur (CTA spine); continuous slow push-in / push-out; cursor arc-in + click; parallax shape slide-in; left→right URL/badge wipe with glowing leading edge; static / fade-out end-lockup hold; optional idle breathe on the held mark. +**motion vocabulary**: ring pulse / expand; background crossfade (light→dark); glow ignite; seed-dot scale-up; continuous orbit / drift (inner faster than outer); single 3D perspective tilt (flat→isometric) + slight zoom-out around a fixed 2D anchor; 3D logo assemble (part detach + spring dot, clapperboard hinge / snap, shell fan-out); wordmark cascade with overshoot (letters left→right); button pill rise / scale-in; animated stroke-outline DRAW + glow (button border AND logo mark); comet / spark accent; diagonal-band wipe (sweep → swell → collapse-to-slash); letter-by-letter wordmark build; pre-formed grid DISPERSE off all four edges; logo-mark stroke-draw (sequential arcs / segments); fast CAMERA PUSH-THROUGH with motion-blur (CTA spine); continuous slow push-in / push-out; cursor arc-in + click; parallax shape slide-in; left→right URL/badge wipe with glowing leading edge; static / fade-out end-lockup hold; optional idle breathe on the held mark; glowing-dot circular path trace; part-overlap icon completion (semi-circles scale up + overlap); scale-up-with-rotate mark entrance; wordmark slide-out-from-behind-icon; badge pill pop onto the lockup; stepped subtitle swap-in-place (bottom rail); count-up stat tick over a faint asset grid; lockup shrink / fade → UI-window scale-up payoff; word-by-word staggered fade-through-grey (in, and left-first out); rolling word-by-word line swap; shrink-toward-center + fade clearing exit; whole-mark spring BLOOM from zero (overshoot + slight rotation); near-imperceptible continuous scale-up through the hold; vertical slot-machine word swap; horizontal phrase collapse / wipe into the mark; instant same-center text→icon swap; line-panel fan-and-flip morph around a central pivot with motion blur (page-flip feel); interlock-settle into the geometric mark; wordmark pull-out trailing a motion-blur streak; lead-line slide-down-off-bottom exit; icon drop-in from above; sequential per-letter slide-in + terminal punctuation landing; confetti burst pop-then-instant-shrink; pixel-stack pop at a text edge; horizontal streak-stretch into full-width stripes; stripe-tail retraction absorbed into the mark (mask retraction); rounded-tile scale-up enclosing the mark; bottom metadata row fade / slide-up (URL pill + store badges); satellite shapes outward drift + fade; left→right underline sweep; left→right tagline wipe-in. **rule mapping** (per motion verb → `rules/.md`) @@ -62,8 +82,40 @@ - left→right URL / badge wipe with glowing leading edge → `techniques.md` clip-path reveal (#12, animate `inset()` left→right); the glowing leading edge → `asr-keyword-glow` - static / fade-out end-lockup hold → no motion rule needed (terminal hold / opacity fade; intentional) - idle breathe on held mark (optional) → `sine-wave-loop` (post-settle breathing) +- glowing-dot circular path trace → `svg-path-draw` (the traced circle draws on) + `techniques.md` MotionPathPlugin (#9) for the leading dot riding the path tip +- part-overlap icon completion / semi-circle scale-up → `spring-pop-entrance` (per-part scale-in; place parts at their final overlap positions from setup — the overlap IS the completed mark) +- scale-up-with-rotate mark entrance → `spring-pop-entrance` (add a rotation from-value to the pop) +- wordmark slide-out-from-behind-icon → recipe `gsap-effects` (x-slide) under a clip / overflow mask via `techniques.md` clip-path reveal (#12); z-order the icon above the sliding text +- badge pill pop onto the lockup → `spring-pop-entrance` +- stepped subtitle swap-in-place (bottom rail) → `discrete-text-sequence` (whole-state replacement at time thresholds); derive the windows via `dynamic-content-sequencing` +- count-up stat tick over a faint asset grid → `counting-dynamic-scale`; the faint grid is a plain opacity fade (no rule needed) +- lockup shrink / fade → UI-window payoff → `scale-swap-transition` (exit cluster shrinks + fades at center; window pops in with `back.out`) +- word-by-word staggered fade-through-grey (in / left-first out) → recipe `gsap-effects` (per-word staggered opacity + color tween). Deliberately a quiet FADE register — do NOT substitute `waterfall-entry` here; its binary-arrival doctrine is the wrong voice for this serif beat +- rolling word-by-word line swap → two overlapping `gsap-effects` word staggers at the same timeline position (old line out left-first, new line in left→right) +- shrink-toward-center + fade clearing exit → `scale-swap-transition` (its exit half; the entrance half is the bloom) +- whole-mark spring BLOOM from zero → `spring-pop-entrance` (single hero, `back.out` overshoot, slight rotation from-value) +- near-imperceptible continuous scale-up through the hold → no motion rule needed (one long linear micro-tween on the held lockup; intentional life-in-the-hold) +- vertical slot-machine word swap → `vertical-spring-ticker` (masked column, stepped tween — one word slot cycles, rest of the line fixed) +- horizontal phrase collapse / wipe into the mark → `scale-swap-transition` (same-center morph) with the collapse via `techniques.md` clip-path reveal (#12) +- instant same-center text→icon swap → no motion rule needed (`tl.set` hard swap; intentional — the chain's continuity lives in the NEXT beat's morph) +- line-panel fan-and-flip morph (page-flip, motion-blurred) → `hacker-flip-3d` (the per-panel 3D rotation axis) + `motion-blur-streak` (the blur) + `techniques.md` CSS-3D; true stroke-interpolation glyph morphs live in `hyperframes-keyframes` (SVG morph) — reach there if panels can't sell it +- interlock-settle into the geometric mark → `center-outward-expansion` machinery run INWARD (per-panel transform offsets tween to 0 in lockstep with one driver) +- wordmark pull-out trailing a motion-blur streak → `motion-blur-streak` (echo / ghost trail collapsing into the lead) on the x-slide +- lead-line slide-down-off-bottom exit → in-scene clearing beat; same doctrine as the grid-disperse row above (offscreen target + out-easing; prefer the harness transition when the exit IS the scene boundary) +- icon drop-in from above → `spring-pop-entrance` (y-offset from-value, overshoot on landing) +- sequential per-letter slide-in + terminal punctuation landing → `waterfall-entry` (staggered arrival cascade on a lateral axis; the punctuation is the cascade's final, heaviest beat) +- confetti burst pop-then-instant-shrink → `press-release-spring` ("release burst" variation) for a small deterministic burst; a true multi-particle confetti field → `particle-burst` +- pixel-stack pop at a text edge → `spring-pop-entrance` (tight stagger down the stack) +- horizontal streak-stretch into full-width stripes → plain `scaleX` stretch via `gsap-effects` (transform-origin at the stack) + `motion-blur-streak` for the streak read +- stripe-tail retraction absorbed into the mark → `techniques.md` clip-path reveal (#12) run in REVERSE (animated `inset()` retraction reading as mask absorption into the mark) +- rounded-tile scale-up enclosing the mark → `spring-pop-entrance` (scale-in BEHIND the mark; z-order only, mark never moves) +- bottom metadata row fade / slide-up → `spring-pop-entrance` (staggered group, ≤500ms cap) +- satellite shapes outward drift + fade → `center-outward-expansion` run OUTWARD (drift targets past frame edge) + opacity tail; if the drift must idle first, seed it with `sine-wave-loop` +- left→right underline sweep → `css-marker-patterns` (highlight sweep re-skinned as an underline) or `stat-bars-and-fills` progress-fill `scaleX` +- left→right tagline wipe-in → the existing "left→right URL / badge wipe" row applies unchanged (clip-path `inset()`) **camera modifier** (the push / tilt) - **CTA push-through** (the CTA spine): a scripted hard zoom phase on a scene-wrapping camera → `multi-phase-camera` ("Steady push" / "Bookend pull" pattern; push phase = the climax). When the mark is OFF-center and the camera must fly through a specific point of negative space, combine with `coordinate-target-zoom` (outer scales, inner counter-translates so the target negative-space point lands at viewport center as scale ramps; measure the offset at setup). The signature heavy horizontal MOTION-BLUR on the streak → `motion-blur-streak` (directional velocity blur on the push); realize with a CSS `filter: blur()` / duplicated-streak layer on the camera during the push window. - **Product_Intro tilt** (the one cinematic move): the flat→isometric perspective tilt + slight zoom-out is a single scripted camera beat → `multi-phase-camera` (scale phase + the "Targeted zoom into off-center element" / drift machinery) for the zoom-out. `multi-phase-camera` is scale+translate+drift only, so the perspective-PLANE rotateX (flat top-down → angled isometric) of the whole stage is the CSS-3D move noted above — approximate via `techniques.md` CSS-3D, animating the stage's `rotateX` (closest reference is `orbit-3d-entry`'s "Tilted orbit plane" variation animated over time). +- **Static-frame variants**: the parts-assembly, text-clear bloom, morph-chain, parts-arrive, and settled-reveal variants are all COMPLETELY static-frame (element-level motion only; settled-reveal tolerates at most a very slow global zoom-out). The camera modifier applies only to the CTA push and the Product_Intro tilt. diff --git a/skills/hyperframes-animation/blueprints/overwhelm-surround.md b/skills/hyperframes-animation/blueprints/overwhelm-surround.md index ad0314332..751d14398 100644 --- a/skills/hyperframes-animation/blueprints/overwhelm-surround.md +++ b/skills/hyperframes-animation/blueprints/overwhelm-surround.md @@ -5,8 +5,13 @@ **roles served** - Problem (from `problem-mockup-overwhelm`): when the problem beat must first show "too many tools / too much surface area" and then put **the viewer inside it** — a literal swap of subject (product → person) followed by a closing-in that feels invasive. Reach for it when the pain is "you're buried," not "this metric is bad" (that's `dataviz-countup`). +- Problem (from `desktop-clutter-accumulation`): when the overwhelm is a **workspace**, not a tool + count — live windows, stickies, and alert toasts pile up until the frame is chaotically full, and + the beat resolves not by closing in but by shoving the clutter aside and asking the question. + Reach for this variant when the pain lands on words ("how can you X… when you spend months on + Y?"), not on a surrounded avatar. -**duration**: 6–9s +**duration**: 6–9s (clutter-shove-to-question variant ~10s) **shot structure** (a `[bg]` canvas; recognizable surfaces first, the viewer's avatar revealed underneath, then a radial crowd) @@ -14,8 +19,20 @@ - **Scene 2 (~1.6–3.0s) — density amplifies.** `[platform icons / logos]` scatter in around the mockups (staggered), used purely as **density markers** — "look how much surface area," not animated dials. - **Scene 3 (~3.0–4.6s) — the morph (signature move).** The CENTER mockup MORPHS: its content fades out, the container reshapes, and the viewer's `[avatar]` is revealed **underneath** — a literal swap of subject, product → person. - **Scene 4 (~4.6–end) — close-in.** `[task bubbles / demands]` close in from ALL sides toward the avatar (radial staggered entry). The avatar **stays put** while the bubbles invade — the claustrophobia comes from being surrounded, never from a camera push. Holds on the crowded state. +- **Variant — clutter-shove-to-question** (replaces Scenes 3–4 and + inverts the camera contract — see modifier): accumulation runs under a **slow steady zoom-out** — + `[sticky notes]` bounce in springy, `[dashboard / editor windows]` pop and slide up, a stack of + `[alert toasts]` slides in at one edge, inner content keeps typing / log-scrolling as live density, + windows overlap until the frame is chaotically full. The camera then REVERSES into a quick + push-in that **shoves the clutter to the frame edges**, opening central negative space where a + `[two-part serif question]` builds word-by-word (line 1 swaps in place to line 2); a `[cursor]` + glides in from off-frame and comes to rest under the text; a very slow forward creep and hold. + No morph, no avatar — the question is the payoff. -**motion vocabulary**: staggered scale-in assembly; resting-scale-preserving low float; density-marker icon scatter; content-fade → container-reshape → reveal-anchor-beneath morph; radial close-in entry from all compass points; held crowded end-state. +**motion vocabulary**: staggered scale-in assembly; resting-scale-preserving low float; density-marker icon scatter; content-fade → container-reshape → reveal-anchor-beneath morph; radial close-in entry from all compass points; held crowded end-state. Clutter-shove variant: slow steady zoom-out under accumulation; reverse quick push-in; clutter +shoved to frame edges opening center negative space; continuous live typing / log scroll inside +windows as ambient density; toast-stack slide-in; word-by-word serif build with in-place line swap; +cursor glide-to-rest; very slow forward creep + hold. **rule mapping** @@ -24,5 +41,18 @@ - center mockup → avatar morph (HF forbids `width`/`height` tweens → drive the reshape on `scaleX`/`scaleY`, anchor = the avatar layer rendered beneath) → `card-morph-anchor` - radial bubble close-in (positions baked once via `cos`/`sin`, staggered entry) → `gsap-effects` (radial layout) + `spring-pop-entrance` (per-bubble arrival) - low-amplitude float on background mockups/icons → `sine-wave-loop` (low-amplitude register — subtle jitter that composes onto each element's resting scale, never a `fromTo` yoyo that re-tweens to its start) +- (variant) zoom-out under accumulation → quick push-in → slow forward creep → `multi-phase-camera` + (pull-back / push / drift as sequential phases on one world wrapper; counter-translate math in + `viewport-change`) +- (variant) clutter shoved to the edges as the push-in lands → `center-outward-expansion` (outward + vectors to edge resting positions), fired at the same timeline position as the camera push so the + shove reads as CAUSED by it (`reactive-displacement` register) +- (variant) word-by-word serif question build → `gsap-effects` (staggered word reveal); the + in-place line-1 → line-2 swap → `discrete-text-sequence` +- (variant) live typing inside windows → `gsap-effects` (typewriter); the continuous inner + log-scroll — composition: looping content translateY via `gsap-effects` (masked) +- (variant) cursor glide-in coming to rest → `cursor-click-ripple` (approach portion only — no click) -**camera modifier**: camera-static — the close-in must read as the world crowding the subject, so the frame holds; a push-in would convert "surrounded" into "zoomed-into" and kill the claustrophobia. +**camera modifier**: camera-static — the close-in must read as the world crowding the subject, so the frame holds; a push-in would convert "surrounded" into "zoomed-into" and kill the claustrophobia. The clutter-shove-to-question variant is the sanctioned exception: there the camera IS the +storyteller (zoom-out ↔ push-in via `multi-phase-camera`), and the claustrophobia comes from +accumulation, not surround — never mix the two resolutions in one shot. diff --git a/skills/hyperframes-animation/blueprints/panel-edit-live-sync.md b/skills/hyperframes-animation/blueprints/panel-edit-live-sync.md new file mode 100644 index 000000000..2eb2fd2f0 --- /dev/null +++ b/skills/hyperframes-animation/blueprints/panel-edit-live-sync.md @@ -0,0 +1,73 @@ +# panel-edit-live-sync — Panel Edit, Live Sync + +**intent**: A bipartite stage — an inspector/editor **panel bound to a target surface** — where a cursor (or text caret) continuously manipulates a control (value scrub, unit/codegen dropdown pick, knob or easing-handle drag, inline retype) and the coupled surface updates **live, in the same beat**: the page button rotates as the value scrubs, preview icons resize per keystroke, the hex readout mirrors every hover, the code block converts on the pick. The motion IS the causality — one gesture, two surfaces changing in the same frame. The camera's job is co-visibility of the couple, not a chase. + +**provenance** (7 mined Key_Feature goldens across 4 products, both dialects — three sync modes): + +- _Write-sync (control → target)_ — the anchor mode: a visual-editor panel scrubs rotation/margin/padding while the live page button rotates and shifts in the same beat (plus unit + font-weight dropdown picks); an inline `className` retype in a glowing code callout resizes the preview icons per keystroke (caret-as-actor, push-in/pull-back roundtrip that must keep BOTH surfaces in frame); a motion editor drags a knob along a dotted motion path and bends easing handles into an S-curve, paying off with a big zoom-out where the finished toggle PERFORMS the edited ease (deferred payoff). +- _Read-sync (target → panel mirror)_: clicking a page button pops a toolbar → "Copy code" → the code editor fills with the element's CSS under one continuous slow zoom-out; hovering palette swatches live-updates a footer hex readout while the grid scrolls. +- _Self-conversion (panel is both control and target)_: unit dropdown conversions inside a 3D-tilted spacing panel snap-convert values in place (rem→px→%, `0,375 rem` → `6 px` → `4,871 %`); a codegen dropdown picks SwiftUI and the CSS block crossfades into SwiftUI under a rapid punch-in. + +> **Concentration caveat**: 4 of 7 members are one video (CSS Scan Pro 2.0). The COUPLING engine is independently attested by 3 more products across 3 more videos and both dialects (Figma Dev Mode, Figma motion editor, bolt.new), each on a different surface pair — page+inspector, canvas+timeline+easing panel, IDE code+app preview — so the shape is real, not one film's house style. What IS CSS-Scan-Pro house style (marked optional below): the dark-slate capability title-card prelude, the oversized black cursor with white outline, the green success-checkmark flip, flash tooltips. Trigger is product-conditional: reach for this shape when the feature itself is live editing/inspection. + +**roles served** + +- Key_Feature (from `panel-edit-live-sync`, all 7 cases): one capability demonstrated as 2–4 edit beats on a single bound element — each beat a continuous manipulation the coupled surface answers in real time, resolving on the last edit held, a zoom-out to the finished product performing the edit, or a callout landing on the result. Three sub-shapes fold in: + - **(A) write-sync** — cursor/caret edits a control; the TARGET transforms live (rotate/shift/stretch/resize/re-animate). + - **(B) read-sync** — cursor selects/hovers the target; the PANEL readout mirrors live (CSS streams in, hex footer updates). + - **(C) self-conversion** — the edit transforms the panel's own readout (units snap-convert, CSS crossfades to SwiftUI). + +**duration**: 5.3–11.9s (read-sync hover demos shortest ~5.3s; multi-beat scrub/edit runs 8.7–11.9s) + +**shot structure** (a `[target surface — webpage / design canvas / IDE + live preview]` sharing the frame with a `[bound panel — floating inspector / docked code panel / timeline + easing editor]`; a `[cursor or caret]` is the actor; every beat pairs ONE manipulation gesture with a SIMULTANEOUS response on the coupled surface; selection chrome declares which element is bound; camera ranges locked → active but always preserves the couple) + +- **Scene 0 (optional, 0.0–2.0s) — capability title card.** Solid dark `[slate/charcoal]` card; a single white line names the capability (`"Edit CSS visually"`, `"Auto measurement units conversion"`, `"Check color palettes"`) — fades/drifts in, holds, then a HARD CUT or a fast motion-blurred zoom-out that settles the stage. (CSS-Scan-Pro-house-leaning; 071/017/080 open cold on the stage, 071 instead springs a giant lowercase `[verb word]` over the preview.) + +- **Scene 1 (~1–3s) — the couple establishes.** The `[target surface]` arrives with the `[bound panel]` docked, floating in subtle 3D tilt, or SLIDING IN from an edge. Selection chrome pops on to declare the binding: `[bounding box + corner handles / red dashed inspection guides / redline measurement chips popping sequentially / green class-name header]`. The cursor enters and glides to the first control. + +- **Scene 2..N (~2s each) — edit beats, gesture + mirror in the same frame (the engine).** Each beat is ONE continuous manipulation and its live answer: + - _Variant — write-sync (A)_: the cursor CLICK-AND-DRAGS a numeric field (value counts up/down: `0°→-10°`, `0→38 px`) while the target `[button/element]` rotates/shifts/stretches in real time; OR drags a `[knob along a dotted motion path / easing handle bending the curve, coords readout updating]`; OR a caret INLINE-RETYPES a value (`1xl→4xl→2xl`) inside a `[glowing magnifier callout]` while `[preview elements]` resize per keystroke. A flash `[tooltip]` may name the gesture. + - _Variant — read-sync (B)_: the cursor CLICKS/HOVERS the target element — a `[floating toolbar]` springs up above it, a menu pick fires (`Copy code` → icon flips to a green checkmark) and the `[code editor]` fills with streaming CSS; or hovered `[swatches]` outline and the `[footer hex]` updates instantly per hover as the grid scrolls. + - _Variant — self-conversion (C)_: the cursor clicks a unit/codegen `[dropdown]` — it opens with hover-highlighted rows + checkmark — and on the pick the readout SNAP-CONVERTS in place (`rem→px`, value recalculates) or the whole `[code block]` crossfades to the new language, heading flipping (`Layout`→`HStack`). + - Camera per beat: LOCKED wide holding both surfaces; or a PUNCH-IN to the acting surface (panel scroll reveals the next section) — but during a write-sync edit both gesture and mirror stay co-visible (071's law: the push-in never crops the preview out). + +- **Scene N (final beat → end) — the edit proves out, HOLD.** Resolution diverges: + - _Variant — last edit held_: the final pick lands (`100 - Thin` selected, `4,871 %` applied) and the state simply HOLDS — never end on the tooltip with the dropdown unopened. + - _Variant — payoff zoom-out_: a big zoom-out reveals the finished product PERFORMING the edited parameter — the toggle slides with the new ease inside the full phone mockup, confetti drifting; or the pull-back returns to the identical full framing while a `[terminal]` appends an hmr line. + - _Variant — callout lands_: a large `[arrow callout]` slides in pointing at the result / the export menu rests open under the cursor; frame drifts subtly outward. + +**signature move**: the **live-sync couple** — a scrubbed/typed/dragged control and its bound surface changing simultaneously, in-frame together, every edit beat. + +**motion vocabulary**: click-and-drag value scrubbing with live target sync (rotate / shift / stretch); per-keystroke live preview resize; inline retype with backspace + blinking caret; instant value snap-conversion; live hex/readout mirror on hover; unit/codegen dropdown with hover-highlight rows + checkmark, instant open/close; font-weight/dropdown row pick; knob drag along a dotted motion path with waypoints; easing-handle drag bending the curve (coords readout updating); playhead scrub; redline measurement chips popping sequentially; bounding box + corner handles; red dashed inspection guides; floating toolbar springs up above the selected element; code panel slides in from an edge; in-panel scroll to a new section; swatch-grid scroll; syntax-highlighted code streams/pastes in; code crossfade (CSS→SwiftUI) with heading flip; glowing magnifier callout over a code token; icon flips to green success checkmark; flash tooltip naming the gesture; oversized black cursor with white outline; grab-cursor drag; dark title-card prelude + hard cut; fast motion-blurred zoom-out settle; ONE continuous slow zoom-out spanning a demo shot; eased push-in → hold → eased pull-back roundtrip; quick punch-in to panel/timeline/code; subtle 3D tilt drift/parallax on a floating panel; big zoom-out to the product payoff; result element re-animates with the edited ease; confetti drift; terminal log append; large arrow callout slide-in; static hold. + +**rule mapping** + +- cursor glide to a control, presses, click feedback → `cursor-click-ripple` +- cursor state flips pointer↔grab over a scrubbable field / draggable handle → `context-sensitive-cursor` +- scrubbed numeric readout counts up/down under the drag → `counting-dynamic-scale` +- **the live-sync couple itself** (control gesture drives a second element's property in the same beat) → `control-target-sync` (concurrent tweens at the SAME timeline position — readout tween + target transform tween sharing one label) +- inline retype with backspace, typos, holds / keystroke thresholds → `discrete-text-sequence` (+ `context-sensitive-cursor` for the caret blink) +- per-keystroke preview resize → `discrete-text-sequence` (keystroke state thresholds) + `control-target-sync` (the coupled scale steps) +- instant value snap-conversion / hex readout swap / heading flip (`Layout`→`HStack`) / status text → `discrete-text-sequence` +- syntax-highlighted code streaming/pasting in, terminal log append → `discrete-text-sequence` (bulk additions are explicitly in-scope) +- dropdown/menu pops open; floating toolbar springs up; tooltip flash; redline chips pop sequentially (staggered, ≤500ms) → `spring-pop-entrance` +- dropdown row hover-highlight stepping and pick sequencing / which edit beat shows what → `dynamic-content-sequencing` +- dashed inspection guides / selection outline draw on → `svg-path-draw`; dotted motion path with waypoints → `svg-path-draw` (the path display) +- knob TRAVEL along the motion path → path following — see `hyperframes-keyframes` (paths) +- easing-handle drag bending the curve (SVG `d` interpolation) → SVG path morph — see `hyperframes-keyframes` (morph; `svg-path-draw` only draws strokes, it cannot morph a path); coords readout beside it → `discrete-text-sequence` +- glowing magnifier callout over a code token (incl. the live enlarged duplicate of a UI token) → composition: `ambient-glow-bloom` (the glow) + `spring-pop-entrance` (the callout pop) +- code panel slides in from an edge / panel docks → `card-morph-anchor` / `scale-swap-transition` (per cursor-ui-demo precedent for panel slide-in) +- code block crossfade CSS→SwiftUI; success-icon flip to green checkmark → `scale-swap-transition` (state swap at the same anchor) +- in-panel scroll / swatch-grid scroll (masked internal translate) → `gsap-effects`; on a 3D-tilted panel → `3d-page-scroll` (tilted plane w/ internal scroll) +- subtle 3D tilt drift/parallax on the floating panel; continuous micro-drift on holds → `multi-phase-camera` (micro-drift phase) +- punch-in to panel/timeline/code and settle → `coordinate-target-zoom` + `multi-phase-camera` +- eased push-in → hold → eased pull-back roundtrip (co-visibility preserved) → `multi-phase-camera` (pull-back / focus / push sequencing) +- ONE continuous slow zoom-out spanning the demo shot; big zoom-out to the product payoff → `viewport-change` (single `.world` composite transform) +- fast motion-blurred zoom-out settle transition → `motion-blur-streak` + `viewport-change` +- result element re-animates with the edited ease (toggle slides with the new S-curve) → `gsap-effects` (custom-ease tween on the payoff element) +- confetti drift on the payoff → `particle-burst` (deterministic confetti) + `sine-wave-loop` (bounded drift) +- large arrow callout slide-in + hold → `gsap-effects` (single slide tween) +- dark title-card prelude (capability line fades/drifts in, hard cut out) → cross-blueprint: `titlecard-reveal` territory; the drift/fade itself → `gsap-effects` — EXIT-N/A as a mapped rule here +- hard cuts between title and demo; final static hold → EXIT-N/A (transition registry / no rule needed) + +**camera modifier**: The camera law is the INVERSE of cursor-ui-demo's chase: it serves **co-visibility of the couple**. Three attested postures — (1) LOCKED: fixed framing for the whole demo, panel + target both in frame, all motion element-level (CSS_39.0, CSS_102.8 after settle); (2) ONE CONTINUOUS MOVE: a single slow zoom-out (or drift) spanning the entire demo shot while edits fire inside it (CSS_10.9, CSS_63.5's tilt-drift) → `viewport-change`; (3) PUNCH-AND-RETURN: eased push-in onto the acting surface, tight hold through the edit, eased pull-back to the identical opening framing (071_bolt, 080_figma, 017_figma) → `multi-phase-camera` + `coordinate-target-zoom` — with the hard constraint that during a write-sync edit the mirror surface is never cropped out. If the camera is chasing the cursor target-to-target with per-beat state swaps, you're in `cursor-ui-demo`, not here. diff --git a/skills/hyperframes-animation/blueprints/prompt-type-submit-generate.md b/skills/hyperframes-animation/blueprints/prompt-type-submit-generate.md new file mode 100644 index 000000000..57fd27e8a --- /dev/null +++ b/skills/hyperframes-animation/blueprints/prompt-type-submit-generate.md @@ -0,0 +1,85 @@ +# prompt-type-submit-generate — Prompt, Submit, Generate + +**intent**: The AI-era demo shot — a `[prompt / query / command]` types character-by-character into a REAL product input (chat composer, search bar, terminal prompt, URL bar, sidebar assistant) and the machine answers: status theater into a streaming answer / agent action log / diff cards / chart / generated artifact — or the clip cuts at the submit and the ask itself is the show. The keyboard is the actor and the product is the responder. Distinct from `typewriter-reveal` (a line typed as bare typography on an empty field — no product surface, nothing answers) and from `cursor-ui-demo` (a cursor clicking a reconstructed UI through states — there the pointer drives every change; here any cursor work only primes the input or lands the submit, and every state change after that is the machine's own doing). + +**roles served** + +- Hook (from `app-window-push-in-prompt-typing`): when the opener is "watch me ask" — typed headline beat(s), ONE eased push-in lands tight on the product's input, the prompt types and the clip ends at / just after submission (sub-shape A). +- Hook (from `typed-command-output-scroll`): when the demo loop ITSELF is the hook — command in, output builds and scrolls, and a second command / retype starts before the cut, ending mid-action (sub-shapes B/C with the restart ending). +- Product_Intro (from `prompt-typing-composer`): when the first look at the product IS its composer — a brand beat opens onto the input surface, a long prompt types with hovers / attachments / dropdown picks, and the camera steers gently toward the input or the confirming control (sub-shape A, occasionally running through to an agent-log payoff). +- Product_Intro (from `search-query-walkthrough`): when the product is introduced through its search affordance — a short `[query]` types with a blinking caret, autocomplete / results populate LIVE, and a confirm click settles the result state (sub-shape C, search skin). +- Key_Feature (from `prompt-type-submit-generate`): when the capability is demoed as ONE prompt→response round trip — submit into thinking/status states, then a streaming answer, action-log rows with brand icons, green diff cards, a chart drawing itself, or an instant generated-app reveal (sub-shapes A/B/C — the family's widest role). +- CTA (from `install-command-end-card`): the install-command end card — the closing `[headline]` DEMOTES (shrinks, grays, lifts) to make room for a `[terminal pill]` that springs in and stretches wide, the `[install command]` types out with a blinking cursor, flanking metadata and a `[tool-icon row]` pop in, and the finished card holds long. No submit, no response — the typed command IS the ask (sub-shape A, terminal skin). + +**duration**: 5.2–12s (A prompt-as-hook 5.2–12s, incl. the ~7.4s CTA end card; B full generate loop 5.45–11.9s; C instant-result surface 5.7–11.9s — a long-form family: most members run 7–12s because the response needs room to arrive) + +**shot structure** (a `[product input]` on/inside a `[product surface — app window, web page, terminal, browser chrome, sidebar]` over `[bg color]`; the input is the gravitational center — the camera makes at most one or two purposeful moves toward or away from it and is otherwise LOCKED; typing is character-by-character behind a visible caret, and response content arrives progressively, never dumped; three folded sub-shapes — **(A) prompt-as-hook**: the clip ends at / just after the submit (or mid-word), the ask is the show; **(B) full generate loop**: submit → status theater → the output builds block by block; **(C) instant-result surface**: the machine answers with a finished surface, often re-queried before the cut) + +- **Scene 0 (optional, 0.0–~2s) — lead-in beat.** ONE establishing move before the input owns the shot: a `[headline]` types on centered and clears; a `[title card]` hard-cuts away; a brand beat (`[logo/mascot]` centered, `[serif title]` building in word groups, logo shrinking-and-rising to dock top-center); an `[orb / mark]` forms with a glowing rim; the `[app window]` flies in with motion blur and settles; or a full-frame `[thumbnail grid]` parts at its vertical centerline to clear the stage. Keep it ≤2s — the input is the star. + - _Variant — Hook_: typed headline beats carry the intro — "[Introducing X]" types on, holds, is replaced by the `[tagline]` typing in the identical style; the typing register is established before the product ever appears. + - _Variant — Key_Feature_: the capability claim types as a bare title ("[Run a task across multiple models.]") then hard-cuts to the surface — or skip Scene 0 entirely and open on the live surface mid-workflow. + - _Variant — CTA_: the `[closing line]` types on in two steps ("[Designed.] [Not generated.]") and holds — it will demote in Scene 2. + +- **Scene 1 (~1–3s) — the input takes focus.** The `[product input]` arrives or is primed: a `[pill bar]` EXPANDS sideways from the mark/chip; a `[prompt palette / card]` SPRINGS in at center with a soft shadow; ONE smooth eased/accelerating push-in crops tight onto the composer inside the `[app window]` (headline chrome slides out of frame); a `[⌘K search modal]` springs to center while the page blurs behind it; a cursor clicks a `[menu row / Assistant button]` and the prompt block appears; or a `[✕ clear button]` empties the previous query back to `[placeholder]`. Optional composer ritual (pick 1–2, before or during typing): an `[attachment]` drags in and settles in a tray below the input; a `[model / option dropdown]` opens beneath the selector, rows hover-highlight, a checkmark lands and the toolbar label updates. + - _Variant — Product_Intro_: the ritual is the introduction — a `[chip grid]` fades in and the cursor arcs across 2–3 hover highlights before clicking the one that opens the composer; the affordances are the tour. + - _Variant — Key_Feature_: the surface already carries an old `[query]` and its previous `[result panel]` — clearing it says "this is a working tool, not a mockup". + +- **Scene 2 (~2–6s) — the prompt types (the engine).** The `[prompt text]` types rapidly character-by-character behind a blinking caret; the input card GROWS downward / wraps as text fills, pushing footer controls and attachments down; a typed `[token]` may convert into an inline `[brand pill]` mid-typing (`[@browser]` → a colored `[Browser]` chip, typing continues around it); the camera may run ONE slow continuous push-in toward the input, decelerating to a near-hold on the typed ask. Sub-shape (A) may END here — cut mid-word with the caret blinking, or held on the finished prompt. + - _Variant — Hook (A)_: the typed ask is the cliffhanger — end on the completed prompt, or on the submit click as the interface DIMS at the cut. + - _Variant — Product_Intro (A)_: the camera dives toward the bottom input while `[option pills]` cascade in above it; the prompt is still mid-word at the cut — the product is introduced as something you talk to. + - _Variant — CTA (A, install end card)_: the Scene-0 headline DEMOTES — scales ~50%, desaturates to gray, lifts upward — as a small `[$ chip]` spring-pops below and STRETCHES horizontally into a wide `[terminal pill]`; the `[install command]` types out inside it; a faint `[repo link]` and a "[Works with]" label fade in quietly; a row of `[tool icons]` pops in one after another with soft spring scale; the finished composition holds long, only the caret blinking. + +- **Scene 3 (~4–7s) — submit + machine theater.** The `[submit control]` is clicked (cursor glide + press dip; the button may have MORPHED state on first keystroke — waveform → up-arrow — and may flip to a `[stop]` control while streaming) or the retype implies enter. The surface answers instantly with a working state: prior content VANISHES (chip grid gone, panel collapses to its slim header, whole layout swaps); then the theater — `[status phrases]` cross-dissolve with a left-to-right shimmer sweep ("[Thinking]" → "[Modeling…]" → "[Planning…]"), a `[spinner]` rotates over a loading strip, a row of `[loading cards]` lines up, or a `[checklist]` populates and its items flip one by one to green checks with strikethrough while a `[status heading]` flips tense ("[Using X]" → "[Used X]"). + - _Variant — (A) status-flare exit_: end the clip ON the theater — "[Generating…]" / the rotating spinner — the flare is the button; the answer is left to the imagination. + +- **Scene 4 (rest) — the answer arrives.** Choose by sub-shape: + - **Sub-shape B (full generate loop)**: the output BUILDS progressively, each block pushing content down — `[answer text]` streams paragraph by paragraph; `[action-log rows]` pop in sequentially, each with a `[brand icon]`; `[diff cards]` expand with green-highlight added lines; `[chart lines]` draw staggered left-to-right from a shared origin; an `[ASCII / summary table]` draws in; live counters tick; the surface auto-scrolls vertically to follow the newest line (page, terminal, or in-card scroll) — often under ONE slow continuous push-in on the result window. + - **Sub-shape C (instant-result surface)**: the machine answers with a finished surface — the matching `[result / article]` renders in place; `[autocomplete chips]` stagger-pop below the bar WHILE the query types (the machine answers every keystroke), then a hover fills the `[Search button]` solid and a click confirms; the `[generated page]` rises as a rounded card and SCROLLS continuously beneath the pinned prompt; a blur-whip resolves onto the `[artifact window]` and a tab click FLIPS code → preview; or a zoom-out reveals the prompt pill was inside a full `[workspace]` where the `[content]` rewrites itself live. + +- **Scene 5 (final beat) — resolve.** Diverges by role and sub-shape: + - _Variant — Key_Feature (hold)_: HOLD on the completed output — chart finished, diff cards + `[action buttons]` fully rendered; no fade-out, no blank end frame. + - _Variant — Product_Intro (confirm)_: the cursor lands the confirming click (`[Create PR]` / `[Generate]` / `[Search]`) as the clip ends, or extras fade and a final push-in leaves the clean end state; one member hard-cuts to a minimal `[end card]` — the submit button alone at dead center with a settle pop. + - _Variant — Hook (restart — the signature)_: a SECOND `[prompt / command]` starts typing at a fresh prompt line, or the query BACKSPACES-AND-RETYPES and the output swaps wholesale to `[result 2]` — the clip ends MID-ACTION, mid-scroll or mid-word: the loop is endless, and that is the point. + - _Variant — CTA_: the end card from Scene 2 simply holds to the last frame; the blinking cursor is the only motion. + +**motion vocabulary**: character-by-character typing with blinking caret / block cursor; typed-headline beats replacing each other; input pill grows / wraps downward into a multi-line box; prompt palette / card springs in; pill bar expands sideways from a mark or chip; orb formation with glowing rim; typed token → inline brand-pill morph mid-typing; placeholder clear; ✕-click query clear; backspace-and-retype query swap; attachment drag-in and tray settle; dropdown open + row hover-highlight + checkmark select + toolbar label update; chip-grid hover dance; cursor glide / arc with hover highlight fills; click press dip; submit-button state morph (waveform→up-arrow, submit→stop); hover fill-state swap on a Search button; content vanish / panel collapse / layout swap on submit; status-phrase cross-dissolves with left-to-right shimmer sweep; pulsing "Thinking"; spinner rotation; loading strip; animated trailing dots; loading model-card row; status-heading tense flip (Using→Used); checklist squares flipping to green checks with strikethrough; action-log rows popping in sequentially with brand icons; streaming text blocks pushing content down; green-highlight diff cards expanding; staggered left-to-right chart line-draws; ASCII / summary table draw-in; count-up ticker; vertical output scroll (page / terminal / in-card) following the newest line; generated page rising as a rounded card and scrolling beneath a pinned prompt; autocomplete chips rapid stagger-pop; code↔preview instant flip on tab click; blur-whip transition; prompt jumps to a heading on submit; zoom-out reveal from prompt pill to full UI window; single eased / accelerating push-in landing on the input; slow continuous push-in on the result window; window fly-in with motion blur; zoom + pan cropping browser chrome; ⌘K modal spring-in with background blur; full-frame grid parting at the vertical centerline; headline demotion (scale-down + desaturate + lift); chip horizontal-stretch into a wide terminal pill; quiet low-contrast metadata fade-ins; sequential spring pop-ins of an icon row; second prompt typing at the cut; interface dim / fade at the cut; long static end hold with blinking cursor. + +**rule mapping** + +- character-by-character typing, placeholder clear, backspace-and-retype, second prompt at the cut, typed-headline beats → `discrete-text-sequence` (typing / typos / holds / backspace) backed by `gsap-effects` (typewriter recipe) +- blinking caret / block cursor (persisting through holds) → `context-sensitive-cursor` +- prompt / status / output phrase windows, script-driven beat durations → `dynamic-content-sequencing` +- input card grows downward / wraps as text fills → `anchored-layout-expand` (top-anchored downward growth, stepped at wrap boundaries) +- typed token → inline brand-pill morph mid-typing → composition: `scale-swap-transition` (token→chip swap at the conversion threshold) + `card-morph-anchor` (the reflow around the chip) +- prompt palette / modal / dropdown springs in; loading cards, log rows, diff cards, autocomplete chips, icon rows arriving staggered → `spring-pop-entrance` (single hero or staggered group) +- pill bar expands sideways from a mark; chip stretches into a wide terminal pill → `card-morph-anchor` (container morph) +- cursor glide to a control, press, ripple → `cursor-click-ripple`; the press dip + recovery → `press-release-spring` (or `physics-press-reaction` for cursor+button compressed together) +- hover highlight fills, Search-button instant solid fill, UI keyword accents → `asr-keyword-glow` (static-timeline glow variant) or `press-release-spring` (color-transition variation) +- attachment drag-in with cursor → `context-sensitive-cursor` (pointer↔grab) + `spring-pop-entrance` (tray settle) +- content vanish / layout swap / panel collapse on submit; code↔preview instant flip → `scale-swap-transition` (paired same-center swap) or a hard `tl.set` state swap via `discrete-text-sequence` semantics +- prompt jumps to a heading on submit → FLIP reposition — see `hyperframes-keyframes` (FLIP); the travel itself via `nudge-curve` (slow-fast-slow group slide) +- status-phrase cross-dissolves with shimmer sweep → `discrete-text-sequence` (phrase swaps) + `ambient-glow-bloom` (Shimmer sweep variation — single-pass traveling sheen, clipped to the text) +- spinner rotation, animated trailing dots, pulsing loader glyphs → `svg-icon-enrichment` (rotating / pulsing internal SVG elements); the bounded "Thinking" pulse → `sine-wave-loop` (finite repeats — this pulse PERFORMS status, it is not idle wobble) +- checklist state flips, status-heading tense flip, status-pill swaps → `discrete-text-sequence` (discrete state stepping); the checkmark stamp → `svg-path-draw` or `spring-pop-entrance` +- streaming text blocks / log rows pushing content down → `dynamic-content-sequencing` (per-block windows) + `spring-pop-entrance` (per-row arrival) +- vertical output scroll following the newest line (page / terminal / in-card) → composition: content translateY keyed to the same timeline as the content windows + a matched `viewport-change` counter-pan when the frame itself travels +- generated page as a rounded card whose internal content scrolls → `3d-page-scroll` (flat variant — internal scroll of a page card) +- staggered chart line-draws → `svg-path-draw` (stroke-dashoffset, staggered starts) +- count-up ticker / live counters → `counting-dynamic-scale`; result bars / fills → `stat-bars-and-fills` +- single eased push-in landing on the input; slow continuous push-in on the result → `multi-phase-camera` (push phase) with the destination framed via `coordinate-target-zoom` +- zoom-out reveal from prompt pill to full workspace; zoom + pan cropping chrome → `viewport-change` (composite pan+scale on the `.world` wrapper) +- window fly-in with motion blur; blur-whip transition → `motion-blur-streak` +- ⌘K modal with background blur → `depth-of-field-blur` (blur the page plane, keep the modal sharp) + `spring-pop-entrance` +- full-frame grid parting at the vertical centerline → `center-outward-expansion` (halves glide outward in lockstep) +- orb formation with glowing rim → `ambient-glow-bloom` + `spring-pop-entrance` +- headline demotion (scale-down + desaturate + lift) → `gsap-effects` (plain composite tween; no dedicated rule needed) +- interface dim at the cut, hard cut to a minimal end card, end mid-word / mid-scroll → exit conventions, no rule needed +- long static end hold with only the caret blinking → `context-sensitive-cursor` (the blink is the sanctioned residual motion) + +**camera modifier** (the camera always serves the ask or the answer; many members are fully camera-static — typing, submit theater, and streaming carry the shot) + +- ONE smooth eased / accelerating push-in that lands tight on the input and LOCKS (Hook, Product_Intro) → `multi-phase-camera` (push) + `coordinate-target-zoom` (target the input) — the defining move of the "watch me ask" opener. +- ONE slow continuous push-in running under the typing or under the output build, decelerating to a near-hold (Product_Intro, Key_Feature) → `multi-phase-camera` — gives the response weight without stealing from it. +- ONE zoom-out reveal — the prompt pill turns out to live inside a full workspace (Key_Feature, sub-shape C) → `viewport-change` (pull-back) — the inverse move; the ask was closer to the product than you thought. +- Entry-only flourishes: window fly-in with motion blur (`motion-blur-streak`), zoom + pan cropping browser chrome (`viewport-change`) — both settle before typing starts. +- Never more than two real viewport moves per shot; the frame is LOCKED during submit theater and streaming (the content scrolls, the camera does not). diff --git a/skills/hyperframes-animation/blueprints/spatial-pan-stations.md b/skills/hyperframes-animation/blueprints/spatial-pan-stations.md index 07cf09120..d3f172e4b 100644 --- a/skills/hyperframes-animation/blueprints/spatial-pan-stations.md +++ b/skills/hyperframes-animation/blueprints/spatial-pan-stations.md @@ -4,8 +4,8 @@ **roles served** -- Hook (from hook-pan-timeline / #1 Hook_02): a horizontal timeline of evenly-spaced milestones, left-panned beat by beat, each marker getting a spring-popped callout, landing on the present moment ("evolution / milestone walk leading up to us"). -- Problem (from problem-camera-pan-stations / #8 Problem_01): a connected web of pain "stations" linked by hand-drawn leading lines, diagonally panned station to station, ending on a tangled scribble knot ("too many disconnected steps — it's a mess"). +- Hook (from hook-pan-timeline): a horizontal timeline of evenly-spaced milestones, left-panned beat by beat, each marker getting a spring-popped callout, landing on the present moment ("evolution / milestone walk leading up to us"). +- Problem (from problem-camera-pan-stations): a connected web of pain "stations" linked by hand-drawn leading lines, diagonally panned station to station, ending on a tangled scribble knot ("too many disconnected steps — it's a mess"). - Product_Intro (from concept-demo-decode-pan): a two-shot strip bridged by ONE lateral pan — shot 1 holds a static phrase whose accent word 3D-flap-DECODES (the concept lands), then the camera pans across the strip (with background parallax) into shot 2, where a cursor drives a live typing demo. Pairs this pan with `cursor-ui-demo`'s focal-locked tracked typing. **duration**: 7–10s (union of Hook 8–10s, Problem ~7s, concept-demo ~7s) diff --git a/skills/hyperframes-animation/blueprints/titlecard-reveal.md b/skills/hyperframes-animation/blueprints/titlecard-reveal.md index 8b1e53ded..90bf56b58 100644 --- a/skills/hyperframes-animation/blueprints/titlecard-reveal.md +++ b/skills/hyperframes-animation/blueprints/titlecard-reveal.md @@ -6,8 +6,16 @@ - Benefits (from `benefits-titlecard-crossfade`, #34): a calm two-line value title card — headline value line, then one slide-up crossfade to a qualifier/elaboration line that holds center. - Social_Proof (from `social-proof-reveal-card`, #35): wipe a busy app-collage open away with one diagonal pill-sweep to reveal a clean brand lockup (icon + wordmark) plus a centered "loved by [N]+ [audience] teams" social-proof line that spring-settles and holds. +- CTA (from `hard-cut-card-stack-to-logo`): a monochrome end-card + CHAIN — statement → CTA / availability line → brand wordmark/logo — separated by instant hard + cuts at full opacity; each card is its own allocated stillness, and the sequence terminates on + the logo held to the final frame. +- Product_Intro (from `title-card-prelude-chain`): a three-beat dark title + PRELUDE before any product UI — `[logo]` pop → `[name]` (a `[version]` appends grey→bright) → + `[tagline]` card — chained by clears and blur-snap handoffs rather than hard cuts. -**duration**: 3–5s (Benefits 3–4s; Social_Proof ~5s / observed 4.7s). +**duration**: 3–5s (Benefits 3–4s; Social_Proof ~5s / observed 4.7s). Card chains run 2–3s per +card, ~5.5–9.5s total. **shot structure** @@ -23,9 +31,24 @@ Scene 2 (~0.4–~1.5s): the ONE move executes — a single restrained reveal tha Scene 3 (~1.5s–end): the revealed/settled card holds to the end (the allocated stillness). At most one subtle live element (a slow breathing pulse on the card, or a very slow camera drift). No second development phase. Variant — Benefits: [benefit line 1] translates up and fades out as [benefit line 2 — qualifier / elaboration] translates up from below center and fades in to take center; holds. (This single slide-up crossfade IS the one move — Benefits front-loads no Scene-2 wipe.) Variant — Social_Proof: the lockup — [logo icon] centered, [wordmark] below, centered [social-proof tagline] "Loved by [N]+ [audience] teams" (the [N]+ may count up) — spring-settles small, then holds. + +Variant — card chain (CTA end-card stack / Product_Intro title prelude): the single-card contract +repeats 2–3 times in sequence. Each card is a complete Scene 1–3 in miniature — arrive (or simply +BE there), at most one restrained move, hold — and the seams between cards are INSTANT hard cuts +at full opacity (no crossfade, no fade-through-black) or, in the prelude flavor, a blur-away → +snap-into-focus handoff. + Card moves stay on budget: a character-by-character type-on with visible partial states, a + right-to-left backspace that resolves the [wordmark] into the small [logo icon], a grey→bright + append ("[name]" gains "[version]"), a blur-snap into focus — or nothing beyond a + barely-perceptible continuous slow scale-up across the hold. + The final card is always the [brand logo / lockup], held static to the last frame. ``` -**motion vocabulary**: single restrained reveal (gentle fade-in + subtle scale-up settle | diagonal clip-path pill-wipe), one slide-up crossfade between two centered lines (Benefits), icon stroke draw-on (Social_Proof), optional "[N]+ teams" count-up, logo+tagline spring-settle-and-hold, subtle breathing on the held card, hold-to-end. Calm register — no spring chains, no tumble, no per-beat flips, no second phase. Camera static (optional very slow drift only). +**motion vocabulary**: single restrained reveal (gentle fade-in + subtle scale-up settle | diagonal clip-path pill-wipe), one slide-up crossfade between two centered lines (Benefits), icon stroke draw-on (Social_Proof), optional "[N]+ teams" count-up, logo+tagline spring-settle-and-hold, subtle breathing on the held card, hold-to-end. Calm register — no spring chains, no tumble, no per-beat flips, no second phase. Camera static (optional very slow drift only). Card-chain register: instant hard cut at full opacity as the only seam, barely-perceptible +continuous slow scale-up across each hold, character-by-character type-on with visible partial +states, right-to-left backspace collapsing the wordmark into the logo icon, grey→bright text +append, blur-away → snap-into-focus card handoff, logo pop with overshoot + glow (prelude opener), +monochrome text-on-solid throughout. **rule mapping** @@ -36,7 +59,22 @@ Scene 3 (~1.5s–end): the revealed/settled card holds to the end (the allocated - "[N]+ teams" count-up (Social_Proof Scene 3, optional) → `rules/counting-dynamic-scale.md` - logo + tagline spring-settle-and-hold (Social_Proof Scene 3) → `rules/spring-pop-entrance.md` (single soft settle; intentionally one beat, not a chain) - subtle breathing on the held card (the one live element during the hold) → `rules/sine-wave-loop.md` +- type-on / backspace / grey→bright append (chain cards) → `rules/discrete-text-sequence.md` + (non-linear typing incl. backspace; drive the version append as a bulk addition) +- wordmark remainder resolves into the logo icon → `rules/scale-swap-transition.md` (same-center + swap fired as the last character deletes) +- barely-perceptible slow scale-up across a hold → the camera-modifier drift + (`rules/multi-phase-camera.md`, micro-drift register) applied per-card +- blur-away → snap-into-focus handoff (prelude flavor) → `rules/depth-of-field-blur.md` (single + pull on the outgoing / incoming card) +- logo pop with overshoot + glow (prelude card 1) → `rules/spring-pop-entrance.md` + + `rules/ambient-glow-bloom.md` +- instant hard cut at full opacity → not a rule: a timeline `tl.set` swap — deliberately NO + transition entry. **camera modifier**: optional — a single very slow drift/push under the hold only → `rules/multi-phase-camera.md`. Default is fully static; do not add unless the held beat would otherwise read as a freeze-frame. -**stillness note**: This is a legitimate allocated-stillness beat. The hold in Scene 3 is the deliverable, not an unanimated gap — do NOT manufacture a development phase, extra swaps, or force-animation. One restrained move + a subtle hold (optionally one breathing element or one slow drift) is the correct and complete shape. +**stillness note**: This is a legitimate allocated-stillness beat. The hold in Scene 3 is the deliverable, not an unanimated gap — do NOT manufacture a development phase, extra swaps, or force-animation. One restrained move + a subtle hold (optionally one breathing element or one slow drift) is the correct and complete shape. The card-chain variant does not break this: each card individually obeys the one-move + hold +contract, and the hard cut is a seam, not a move. Boundary: if the cards flip at sub-second tempo +or each beat carries its own entrance/exit energy, you have left this blueprint — that is +`kinetic-type-beats` (its CTA variant owns the high-tempo value-line stack). diff --git a/skills/hyperframes-animation/blueprints/transcript-scroll-artifact-reveal.md b/skills/hyperframes-animation/blueprints/transcript-scroll-artifact-reveal.md new file mode 100644 index 000000000..c7cc88d4b --- /dev/null +++ b/skills/hyperframes-animation/blueprints/transcript-scroll-artifact-reveal.md @@ -0,0 +1,45 @@ +# transcript-scroll-artifact-reveal — Transcript-Scroll Artifact Reveal + +**intent**: The frame travels vertically along ONE long content surface — an agent transcript, a running task feed, an analysis document, a story draft — rendered full-bleed on a flat canvas (no device frame, no held mockup), by camera pan or element scroll; the traversal itself is the story ("look how much work happened / how much is here"), until ONE focal interaction — a file-chip click, a quote highlight, a collapsible-row expand — pivots the shot into an artifact/detail reveal: the deliverable behind the work. + +**roles served** + +- Key_Feature (modes: `pan-to-workspace` · `feed-rush` · `document-to-artifact` · `selection-pivot`): the x-viral AI-product grammar for "the agent did a lot of work → here's the deliverable." The long surface is the EVIDENCE (tool pills, checked progress items, task rows, headings, comps tables, story paragraphs), read at traversal pace; the artifact is the PAYOFF (full workspace with live mockup, spreadsheet with highlighted cells, inline ask-panel, sub-task stack). Reach for it when the feature's proof is the volume/depth of generated work and the beat should cash that in on one interaction — not a held device tour (`device-surface-showcase`), not a cursor-chased workflow (`cursor-ui-demo`). + +**duration**: 5–11.8s (feed-rush 5.4s · pan-to-workspace 5.0s · selection-pivot 9.3s · document-to-artifact 11.75s) + +**shot structure** One `[long content surface: agent chat transcript / task feed / analysis document / story doc]` sits full-bleed on a `[flat light canvas]` (goldens: warm off-white / cream / beige / plain white — the surface's own background IS the scene background); dark text with small `[accent]` marks (green verb highlights, model-tag pills, check circles, yellow cells). Three acts: TRAVERSE → HINGE → ARTIFACT. Camera discipline is the signature: at most TWO real camera moves in the whole shot, bracketing the hinge; everything else is element motion on a static frame. + +- **Scene 1 (0.0–~40–60% of runtime) — establish + vertical traversal (the evidence).** The surface establishes with one small opener — a `[title]` types on / a centered `[title]` shrinks ~50% and glides to the top-left to dock as a fixed header / the frame opens tight on the `[chat panel]` — then the traversal begins: the frame travels DOWN the content (or the content streams UP through the frame), revealing progressive work in reading order: `[prompt → tool pills → checked progress items → typed summary]`, `[tagged task rows → muted tasks → checklist block]`, `[heading → paragraph → comps table → bullets]`, `[title → story paragraphs → dialogue]`. New rows may cascade in (staggered arrival) before the scroll takes over; a typed line may finish under the moving frame. Traversal texture varies by member: one continuous slow pan, a fast continuous feed rush, stepped scrolls decelerating at each stop (speed-blur between stops, content fading at frame edges), or one smooth scroll easing to a stop. +- **Scene 2 (~1–2s) — the hinge: ONE focal interaction.** The traversal settles and a single interaction pivots the shot: a `[file-attachment chip]` spring-pops in below a typed handoff line and a cursor glides in and CLICKS it; a `[sentence/quote]` gets a selection-highlight sweep and a `[tooltip pill]` spring-pops above it for the click; a `[collapsible row]` reaches the frame center and EXPANDS; or the typed `[verifier summary]` completes as the implicit trigger. This is the only interaction in the shot — the cursor (if any) appears here for the first time. +- **Scene 3 (rest) — artifact reveal + hold.** The hinge cashes in, choosing ONE reveal mechanic: a fast smoothly-DECELERATING zoom-OUT re-frames the whole `[workspace]` (the panel just traversed becomes a sidebar beside a `[live mockup]` and `[tool panel]`); an `[artifact window: spreadsheet]` scales up from small toward full frame, then a slow push-in + lateral pan settles on its `[highlighted cells]`; an `[inline panel]` expands below the highlighted line and a `[follow-up question]` types into it; or the row unfolds into a `[sub-task stack]` and the scroll settles on `[narration text]`. Optional coda: one cursor click instantly swaps a `[screen]` inside the revealed artifact (e.g. a phone tab click). Frame locks; element motion only to the end. + +- Variant — _pan-to-workspace_ (001_claudeai, 5.0s): traversal is a REAL camera pan — opens tight on the chat panel, one single uninterrupted downward glide (never cutting away) over pills → checked list → typing verifier summary; hinge is the summary completing; reveal is ONE rapid decelerating zoom-out to the three-part workspace (chat-as-sidebar / phone mockup / tweaks panel); coda cursor click swaps the phone screen instantly. Exactly two camera moves total. +- Variant — _feed-rush_ (010_perplexity A, 5.4s): NO camera at all — title docks to header, five tagged rows cascade in, then a fast continuous upward ELEMENT scroll races through muted tasks and a checklist to a collapsible row; hinge is the row itself; reveal is the row expanding into a six-item sub-task stack, settling on narration. Cursorless. +- Variant — _document-to-artifact_ (010_perplexity B, 11.75s): traversal is a stepped ELEMENT scroll (static frame) — the document climbs in fast steps, decelerating at each stop, blur/fade between stops, clearing to blank canvas; hinge is a typed handoff line + file-chip pop + cursor click; reveal is the spreadsheet window scaling up then one slow continuous push-in + rightward pan onto the yellow-highlighted forecast columns. +- Variant — _selection-pivot_ (014_OpenAI, 9.3s): typed headline → document builds (bubble prompt + typed title + populating paragraphs) → one smooth upward element scroll eases to a stop; hinge is the selection-highlight sweep + the shot's ONE push-in framing the sentence + tooltip-pill click; reveal is the inline panel expanding below the line with the referenced quote and a rapidly-typed follow-up question. Camera locked at the pushed-in zoom to the end. + +**motion vocabulary** continuous slow downward camera pan; fast continuous upward feed scroll; stepped document scroll decelerating at each stop; smooth scroll easing to a stop; speed-blur between scroll stops; content fade at frame edges; centered title shrinks ~50% and glides to a top-left header dock; task rows cascade in staggered; typed line / typed title / typed follow-up question (caret); green leading-verb highlights and model-tag pills riding past; checked-item strikethroughs riding past; file-attachment chip spring pop-in; tooltip pill spring pop; chat-bubble arrival; cursor glide-in + click; selection-highlight sweep across a sentence; ONE camera push-in onto the selection; fast decelerating zoom-out to the full workspace; artifact window scales up from small; slow push-in + lateral pan settling on highlighted cells; collapsible row expands into a sub-task stack; inline panel expands below the line; phone-screen instant swap on a coda tab click; frame-lock hold. + +**rule mapping** + +- vertical traversal by ELEMENT scroll — fast feed rush / stepped document scroll / smooth scroll-to-stop → `3d-page-scroll` (flat variant: tilt ≈ 0 — the surface's content `translateY`-scrolls to sections; the multi-phase scroll variant covers stepped stops; keep ONE ease family across all steps — `power3.out`/`power4.out` for UI-scroll feel) +- vertical traversal by CAMERA pan (transcript glide) → `viewport-change` (pan mode — the world translates up under a static frame; one continuous tween, no cuts) +- speed-blur between stepped-scroll stops → `motion-blur-streak` (blur peaks at max scroll velocity, resolves to 0 at each settle) +- which content each traversal beat reveals (stop-by-stop sequencing) → `dynamic-content-sequencing` +- centered title shrinks and glides to dock as a fixed header → `gsap-effects` (one simultaneous scale + translate tween; plain two-property move, no named rule required) +- task rows cascade in staggered before the scroll takes over → `waterfall-entry` (arrival cascade; goldens use fade + slide-up — the house rule prescribes binary-opacity whip-in, adopt the house form) or `spring-pop-entrance` (staggered group) for card-like rows +- typed lines — verifier summary, handoff line, document title, follow-up question, opening headline → `discrete-text-sequence` (+ `context-sensitive-cursor` for the trailing caret) +- file-attachment chip pop-in / tooltip pill pop / chat-bubble arrival → `spring-pop-entrance` +- cursor glides in, lands, clicks (hinge and coda) → `cursor-click-ripple` (+ `physics-press-reaction` to compress cursor and target together on the press) +- selection-highlight sweep across the sentence → `css-marker-patterns` (highlight sweep) +- ONE push-in onto the highlighted selection / slow push-in + lateral pan settling on highlighted cells → `coordinate-target-zoom` (measured off-center target — the lateral pan IS the counter-translate component), sequenced under `multi-phase-camera` when it follows the window scale-up +- fast decelerating zoom-OUT to the full workspace → `coordinate-target-zoom` (zoom-out variation: open at the zoomed-in framing, pull to scale 1 with `power3.out`/`power4.out`) or `viewport-change` (single continuous pull on the `cam` object) +- artifact window scales up from small toward full frame on the click → `spring-pop-entrance` (hero arrival scale-up; tune overshoot to ~0 / `power3.out` so the window reads weighty, not bouncy) +- collapsible row expands into a sub-task stack / inline panel expands below the highlighted line → `anchored-layout-expand` (in-flow accordion growth pushing subsequent content DOWN — never tween width/height) + `waterfall-entry` (or `spring-pop-entrance` stagger) on the arriving children +- phone-screen instant swap on the coda tab click → `discrete-text-sequence` (discrete whole-state swap; instant, no in-artifact camera move) +- green verb highlights, model-tag pills, check-circle strikethroughs, yellow forecast cells, edge fade masks → static styling of the surface content — no motion rule needed + +**camera modifier**: The blueprint's camera law: **at most TWO real camera moves, bracketing the hinge** — the goldens are emphatic (their briefs carry CRITICAL camera notes). Pick the traversal mechanic first: camera pan (`viewport-change` pan — pan-to-workspace only) OR element scroll (`3d-page-scroll` flat — all others); never both at once. The reveal then spends the second (or only) move: one zoom-OUT to the workspace or one push-IN to the detail (`coordinate-target-zoom`, phases sequenced by `multi-phase-camera`), after which the frame LOCKS — all remaining motion is element-level (typing, expand, screen swap). The feed-rush variant spends zero camera moves: the whole shot is element scroll + expand. This restraint is what separates the shape from `cursor-ui-demo` (camera servos to every interaction) and from `device-surface-showcase` (a showcase camera presenting a held hero). + +**Overflow (scrolled/panned surfaces — required for a clean `check`):** the traversal deliberately moves content past the frame edges. Clip at the scene (`overflow: hidden`) AND mark the moving inner layer (the `.page-content` / `.world` wrapper carrying the transcript/feed/document) with `data-layout-allow-overflow` — otherwise `check` reports `text_box_overflow` / `container_overflow` for every row that has scrolled off. The clip handles it visually; the attribute tells the layout audit it's intentional. diff --git a/skills/hyperframes-animation/blueprints/zoom-out-workspace-reveal.md b/skills/hyperframes-animation/blueprints/zoom-out-workspace-reveal.md new file mode 100644 index 000000000..384675420 --- /dev/null +++ b/skills/hyperframes-animation/blueprints/zoom-out-workspace-reveal.md @@ -0,0 +1,68 @@ +# zoom-out-workspace-reveal — Zoom-Out Workspace Reveal + +**intent**: Open TIGHT on one full-bleed detail — a graphic macro or a small UI region — let micro-action play in close-up, then ONE continuous decelerating zoom-out reveals that everything seen so far lives inside a containing whole (a design-tool workspace / a multi-pane agent workspace); the frame locks at the wide and element-level payoff carries on. The zoom-out IS the narrative engine and the reveal-of-nesting is the payoff — distinct from `grid-card-assemble`, where a zoom-OUT is an optional camera modifier garnishing an element-stagger assemble; here nothing assembles, the world was whole all along, and the single outward move is what re-scopes its meaning. The structural inverse of every existing push-in shape (`constellation-hub`'s push-in, `device-surface-showcase`'s continuous push, `dataviz-countup`'s push-through). + +**roles served** + +- Hook (from `continuous-zoomout-nesting-reveal`): when the open should be a full-bleed graphic mystery — a blob morphing, a macro blossom blooming — resolved by one unbroken exponentially-decelerating zoom-out that passes THROUGH an intermediate composition (oversized headline / card artwork / web page) before revealing the whole thing is an artboard inside a design tool (panels, layers, inspector, timeline); the frame locks and the canvas keeps animating, ending mid-action. +- Benefits (from `close-up-open-single-zoom-out-reveal`): when the payoff is scale/breadth — micro-actions play in extreme close-up on one small UI region (file rows popping in, a highlight stepping, a guided glide down a list), then ONE fast smoothly-decelerating zoom-out (~0.5–1s) reveals the region was a corner of a huge multi-pane agent workspace (chat + artifact preview + sidebar); the wide holds static to the end while element-level payoff completes the story ("look how much the agent did — and here's the deliverable"). + +**duration**: 6.8–11s (Hook continuous-pull both 6.8s; Benefits dwell-then-snap 10.7–11s — the dwell and the post-lock payoff stretch, the reveal itself does not) + +**HARD RULE — no zoom-in anywhere; camera static outside the single reveal.** Carried verbatim from both Benefits goldens and structurally true of both Hook goldens: the camera's only scale motion is OUTWARD. One zoom-out per shot. Before the reveal the camera either holds, glides/pans along the close-up surface, or is already running the (only) pull-back; after the reveal decelerates to a full stop the frame is LOCKED — every later change (pane swap, pane expansion, cursor travel, playhead scrub, canvas animation) is element/layout motion, never camera. No push-in, no punch, no re-zoom, no second reveal. Violating this collapses the shape back into a generic camera tour. + +**shot structure** (one oversized static world — the full `[whole: workspace]` authored at final layout from frame 0 — with the camera starting scaled far in on the `[detail]`; the reveal is one scale animation on the world; two folded sub-shapes — **(A) continuous nesting pull** (Hook) and **(B) close-up dwell → snap reveal** (Benefits)) + +- **Scene 1 (0.0–~2.5s) — full-bleed detail + micro-action.** Extreme close-up: the `[detail: graphic macro — blob / blossom stem / small UI region — file list / browser corner]` fills the frame edge-to-edge with NO containing chrome, canvas, or neighboring panes visible. The detail PERFORMS in close-up — this beat is never a static hold: + - _Variant — Hook (A)_: the graphic itself moves/morphs/blooms — an organic `[accent]` blob flows across and morphs into an undulating wavy line, or blurred macro forms sharpen as circular petals pop and expand outward into a flat vector `[motif]` — while the pull-back is ALREADY running underneath (the camera never waits). + - _Variant — Benefits (B)_: camera holds (or glides) while UI micro-action plays — `[rows: filenames / list items]` pop in top-to-bottom, a soft `[highlight]` steps down row-by-row, or the camera rides down a list while gently pulling back. Optional blur-to-sharp resolve on the opening frame. + +- **Scene 2 (~2.5s–reveal start) — the middle beat.** Diverges by sub-shape: + - _Variant — Hook (A) — intermediate nesting level_: the continuing zoom-out resolves a mid-level composition, still full-bleed, still no chrome — oversized `[headline]` glyphs descend into frame as partial letterforms and settle centered (the "descent" is pure world-scale: the letters are static in world space, the camera pull produces the motion), or the `[motif]` is revealed living inside a `[card]` in a row of cards on a `[web page]`. The viewer re-scopes once — and still doesn't know the real container. + - _Variant — Benefits (B) — close-up beat advances_: the close-up story develops at the same tightness — the view shifts to an adjacent `[panel]`, a new `[row]` fades/slides in and grows its panel, a `[cursor]` enters and hovers it with a soft highlight. This is the pre-reveal dwell; tension is "we're deep inside something." + +- **Scene 3 (the reveal) — ONE decelerating zoom-out completes; frame LOCKS.** The signature move. The camera pulls back to scale 1 and eases to a full stop, revealing the containing `[whole]`: + - _Variant — Hook (A)_: the pull is the tail of the SAME continuous zoom running since frame 0 (total travel ~4.3–4.5s of a 6.8s shot), with strong exponential deceleration — the `[intermediate composition]` turns out to be `[an artboard / a phone-screen mock]` on a `[design-tool canvas]`: light chrome, left pages/layers panel, right properties inspector, blue selection box, bottom animation timeline with keyframe bars. + - _Variant — Benefits (B)_: the pull is a discrete rapid burst (~0.5–1s) from the held close-up — smooth, heavily decelerating — landing the full `[multi-pane agent workspace]`: left `[chat pane]` with the prompt + status + response, center/right `[artifact pane: spreadsheet / deck preview]`, optional `[sidebar: progress checklist + artifacts + context]`. + - Both: the zoom-out ends BEFORE the shot does — always leave a post-lock act. The deceleration-to-stop is what makes the lock legible. + +- **Scene 4 (lock–end) — element-level payoff on the locked wide.** The reveal is not the ending; the close-up's world keeps living inside the wide. All motion is element/layout: + - _Variant — Hook (A)_: a `[cursor]` enters from off-frame and glides to hover/click the selected element, or a `[playhead]` scrubs left-to-right across the bottom timeline while the canvas artwork animates in sync (petals rotate about their hub, a starburst spins in place, a motif sweeps/shifts). Ends MID-ACTION — the tool is alive. + - _Variant — Benefits (B)_: a `[file-attachment card]` fades in → the cursor clicks `[Open]` → the artifact pane swaps content via a quick white-out → the viewer pane expands full-width over its neighbor (LAYOUT motion, not camera) landing on the `[deliverable: full slide / dashboard]`; or the frame simply holds long and static while the cursor drifts to rest near the `[payoff stat]`. Struck-through checklist items in the sidebar read as completed work. Long hold to the end. + +**motion vocabulary**: one continuous scale-driven zoom-out with exponential/eased deceleration (no cuts) · single fast decelerating zoom-out burst (~0.5–1s) · workspace-lock at zoom end · full-bleed no-chrome opening · blur-to-sharp macro focus resolve · organic blob flow + morph into undulating wavy line · squiggle-underline settle with residual undulation · circular petals popping/expanding outward (bloom) · oversized letters descending into frame as partial glyphs (world-scale, not element motion) · text scaling down through the frame to a centered settle · rows pop in top-to-bottom · selection highlight steps down row-by-row · camera rides/pans down a list while pulling back · new row fades/slides in and grows its panel · cursor hover with soft row highlight · cursor entering from off-frame and gliding to hover/click · timeline playhead scrub left-to-right · in-canvas rotation about a hub / spin-in-place · motif shift/sweep-in · file-attachment card fade-in · cursor click · pane content swap via quick white-out · pane expands full-width over neighbor (layout motion) · checklist items shown struck-through · long static hold · cursor drift to rest · ends mid-action (Hook). + +**rule mapping** (motion verb → `rule-id`) + +- the single decelerating zoom-out on the whole world → `viewport-change` (one `.world` wrapper; `cam` object as single source of truth via `onUpdate`; start `cam.scale` at the reveal ratio with `T = -offset × S` centering the detail, tween scale → 1 and translate → 0 with ONE shared ease — the detail drifts from frame-center to its home slot as the wide takes over, exactly the golden read) +- off-center detail framed at open, zoom-out to wide → `coordinate-target-zoom` ("Zoom out (target → wide view)" variation — nested wrappers, reverse phases: start zoomed on the measured target, tween outer scale → 1 + inner translate → 0 with shared duration/ease; measure the detail's center after `fonts.ready`, never hand-derive) +- pre-reveal glide/ride down a list while gently pulling back (Benefits B) → `viewport-change` (pan + scale composed on the one `cam` object) — sequencing the slow-glide → hold → fast-pull profile → `multi-phase-camera` (phase machinery; this shape runs the same scale-agnostic math at 4–12× outward — see `viewport-change`'s scale-guide range note) +- exponential deceleration-to-stop → ease selection (`expo.out` / `power4.out` on the reveal tween) — parameter guidance, no rule needed; after the stop, NO camera tweens exist on the timeline (hard rule above) +- blur-to-sharp macro resolve chorded to the early pull → `depth-of-field-blur` (refocus/settle variation: `--dof` ramps to 0 as the zoom recedes, same timeline position as the pull) +- oversized partial glyphs descending / text scaling down through the frame → no element tween — authored static in world space; `viewport-change`'s pull produces the motion (author trap: animating the letters separately double-moves them) +- organic blob flow + morph into wavy line → SVG path morph — see `hyperframes-keyframes` (morph); flagged special, like `device-surface-showcase`'s WebGL specials — substitute a non-morph accent when the capability isn't loaded +- squiggle-underline residual undulation → `sine-wave-loop` (finite bounded undulation) +- circular petals pop/expand outward (bloom) → `spring-pop-entrance` (staggered pops) + `center-outward-expansion` (petals expand from the hub to final positions) +- rows pop in top-to-bottom → `spring-pop-entrance` (staggered group, ≤500ms stagger cap) or `gsap-effects` (low-drama fade + short slide stagger) +- selection highlight steps down row-by-row → `gsap-effects` (stepped `tl.set` repositions at time thresholds — instant steps, no glide; trivial, no dedicated rule needed) +- new row fades/slides in → `spring-pop-entrance` (soft variant); its panel growing to fit → `anchored-layout-expand` (one-axis layout expansion) +- cursor enters off-frame → glides → hovers → clicks → `cursor-click-ripple` (move-to-target, co-depress, ripple); soft hover row-highlight → `gsap-effects` (background-color/opacity tween) +- timeline playhead scrub left-to-right → `gsap-effects` (linear `ease:"none"` translateX); in-sync canvas animation = place the artwork tweens at the same timeline position as the scrub (sync is free on one paused timeline) +- in-canvas rotation about a hub / spin-in-place (petal flower, starburst) → `svg-icon-enrichment` (SVG `setAttribute('transform','rotate(deg cx cy)')` for explicit centers) +- motif shift/sweep-in on a card → `gsap-effects` (masked translate) or `techniques.md` clip-path reveal +- file-attachment card fade-in → `spring-pop-entrance` (soft) / `gsap-effects` fade +- pane content swap via quick white-out → `discrete-text-sequence` (whole-state swap at a threshold) + `gsap-effects` (white flash overlay with attack-decay opacity envelope) +- pane expands full-width over neighbor (layout motion) → `anchored-layout-expand` (one-axis layout hand-off; width/height tweens stay forbidden) +- checklist items struck-through / status states → static content, or `discrete-text-sequence` if they check off on screen +- long static hold + cursor drift to rest → hold needs no rule; the drift is a single slow `gsap-effects` translate that ARRIVES somewhere meaningful (rests near the payoff stat) — it performs, it is not idle wobble +- ends mid-action (Hook) → the playhead/canvas tweens simply run to the composition edge — no exit move, no rule + +**camera law — staging the one move** (the camera is the engine here, not a modifier) + +- Build the ENTIRE `[whole]` workspace at final layout inside one `.world` wrapper; there is no second set. The open is `cam.scale = S0` (typically 4–12× — whatever makes the `[detail]` full-bleed) with counter-translate centering the detail; the reveal tweens to `scale 1, translate 0`. `overflow: hidden` on the scene; background on the scene, never the world. +- Crispness constraint: everything visible at open must survive S0 magnification — author the detail as DOM/vector (text, SVG, CSS shapes); any raster inside the close-up needs `sourceResolution ≥ rendered × S0`. +- Sub-shape A: the reveal tween spans ~0–4.5s with `expo.out`-class deceleration — one tween, no phases, no cuts; element beats (morph, bloom, glyph settle) are positioned along it. +- Sub-shape B: optional gentle pre-reveal pan/pull (`viewport-change` pan, or a slow scale ease-out ≤ ~15% travel) during the dwell, then the reveal burst (~0.5–1s, heavy decel) as its own tween; camera fully static after. +- Never: a zoom-in, a second zoom-out, camera motion after the lock, or replacing the reveal with a cut. One outward move is the whole grammar. + +**boundary vs `grid-card-assemble`**: it already carries an optional zoom-OUT reveal modifier (glass-card / logo-wall variants), so the two shapes border each other. The test: if elements ASSEMBLE and the pull-back merely shows the assembled array in context, it's `grid-card-assemble`; if the world is whole from frame 0 and the single decelerating pull-back is itself the story — close-up mystery → nesting reveal → locked-frame payoff — it's this blueprint. Related evidence: a mined profile-page golden runs the same single UI zoom-out/scroll-up reveal at small scale inside a kinetic-type shot, corroborating the move's currency without sharing the shape. diff --git a/skills/hyperframes-animation/rules-index.md b/skills/hyperframes-animation/rules-index.md index 5cf84fd8e..8568d2279 100644 --- a/skills/hyperframes-animation/rules-index.md +++ b/skills/hyperframes-animation/rules-index.md @@ -2,6 +2,21 @@ Atomic motion recipes. Each lives at `rules/.md`. Compose 2-4 per scene with a single paused timeline. +## The contract — every rule assumes this + +Stated once here so individual rules don't repeat it. Every recipe in `rules/`: + +- runs on ONE **paused** GSAP timeline registered on `window.__timelines` (never autoplay, never a second timeline); +- is **seek-safe both directions**: `fromTo` with explicit from-states (t=0 correct under seek; `immediateRender: false` when re-owning a target), absolute values — never relative `+=` tweens; state readable as a pure function of timeline time, no mutable trackers; +- is **deterministic**: no `Math.random()`, no `Date.now()` — index-derived pseudo-random and baked schedules only; finite repeats, never `repeat: -1`; +- animates **transforms and paint-only properties** — `width`/`height`/`top`/`left` tweens are forbidden (use scale/translate proxies, masks, or `anchored-layout-expand`); +- caps group staggers so an arrival reads as one beat (`items × stagger ≤ ~0.5s`); +- puts **no CSS `transition`** on animated elements (they interpolate independently of seek and flicker) and hints compositors with `will-change: transform` where many tweens run at once; +- measures DOM (`offsetHeight`, `getBoundingClientRect`) at build time only in a **single-scene** composition — in a multi-scene montage, later clips may not be laid out yet: use authored CSS-matched constants; +- lives inside a standard scene clip per `hyperframes-core` (`class="clip"` + `data-*` timing) — rule snippets show mechanism DOM only, not the scene scaffold. + +A rule's own **Critical Constraints** section lists only what is SPECIFIC to that rule beyond this contract. + ## Text & Typography @@ -14,6 +29,8 @@ Atomic motion recipes. Each lives at `rules/.md`. Compose 2-4 per scene wi Typing cursor whose `background-color` switches at segment boundaries plus square-wave blink via `(tl.time() % cycle) < cycle/2`. Tags: cursor, color, context, typewriter, styling, segment Pre-compute a flat `[{startTime, endTime, ...}]` array from a script of `{textMain, textAccent, charSpeed, hold}` entries. Each phrase's window = `chars × charSpeed + hold`. Content-driven duration, no hand-tuned offsets. Tags: timeline, sequencing, dynamic, duration, script-driven Percussive kinetic typography — short phrases slam in on ONE shared beat array with DISTINCT per-phrase entrances (scale-slam / side-snap / rise-rotate), optional rhythm chrome (metronome ticks, beat bar), then a locked finale. The recipe for "punchy / rhythmic" taglines. Tags: text, kinetic, typography, beat, rhythm, slam, percussive, punchy +A gradient tweened THROUGH letterforms — `background-clip: text` + an oversized-background `backgroundPosition` tween. Continuous sweep across a held headline, traveling word-to-word highlight (stacked-copy opacity envelopes), or a hue-sweep that settles to a solid via a pixel-identical twin crossfade. Glyphs never move; finite, seek-safe. Tags: gradient, text, sweep, background-clip, highlight, hue, headline +RGB-split / slice glitch that snaps sharp — offset color copies jitter on a deterministic hash of QUANTIZED timeline time (never Math.random), or horizontal slice bands displace and converge under a stepped ease; brief vibration, clean resolve, clamped rest state. Entrance stretch, emphasis burst, and slice-reveal forms. Tags: glitch, rgb-split, chromatic, slice, jitter, stutter, snap ## Data & Stats @@ -21,6 +38,7 @@ Atomic motion recipes. Each lives at `rules/.md`. Compose 2-4 per scene wi Counter whose transform scale grows with the value; seek-safe `onUpdate`, `Math.round`, `tabular-nums`, multi-stat chord. (Also listed under Text & Typography.) Tags: counter, number, stat, count-up Data-viz primitives that pair a number with a graphic — growth bars (CSS `scaleY` stagger), progress fill (bar `scaleX` or measured SVG ring), and fractional star-rating wipe (`clip-path`). Transforms only, seek-safe. Pick single-focus vs split-frame and hold it. Tags: data, stats, chart, bars, progress, ring, stars, rating, infographic +Cursor/playhead scrubs an already-drawn chart — ONE driver moves a vertical tracking line + marker along a baked data polyline while a date/value tooltip steps through the data array (text writes only on index change); second series can activate on cross. Chart arrival belongs to `stat-bars-and-fills` / `svg-path-draw`; this is the read head. Tags: chart, scrub, tooltip, readout, tracking-line, data, playhead ## Camera & Viewport @@ -30,6 +48,7 @@ Atomic motion recipes. Each lives at `rules/.md`. Compose 2-4 per scene wi Two-phase virtual camera that locks the viewport to a moving focal point (typing cursor) — static initial framing then focal-point-locked tracking. Uses browser-native `getBoundingClientRect()` / `ctx.measureText()` after `document.fonts.ready`. Tags: camera, tracking, viewport, two-phase, typing Sequential camera-zoom system (pull-back / focus / push) plus continuous micro-drift. Tags: camera, zoom, phase, drift, scale, cinematic Virtual camera — simulate zoom / pan / focus-lock by transforming a single `.world` wrapper containing all scene content. Single-element composite transform `translate(x,y) scale(S)`; counter-translate math is `T = -offset × S` (DIFFERENT from coordinate-target-zoom's `T = -offset`). Tags: viewport, camera, zoom, pan, focus-lock +<3d-camera-flight path="rules/3d-camera-flight.md">Perspective camera FLIGHT through a 3D-laid-out world — one static `perspective` stage + `preserve-3d` `.world` whose pose (`translate3d` + `rotateX`/`rotateY`) is tweened leg-by-leg from a single camera state object: dive into an angled grid, tilt-to-flatten pull-back, flight past standing cards, decelerate-into-focus. `power4.out` landings, `power2.inOut` repositioning; DoF via depth-of-field-blur on non-focal planes. The only camera rule that rotates/travels in Z (the other three are 2D scale+translate). Tags: camera, 3d, flight, perspective, rotateX, translateZ, dive, tilt Selective rack-focus — GSAP-tween `filter: blur()` (+ slight opacity dim) on off-focus layers via a `--dof` var while the focal element stays sharp; single pull, two-plane rack, or blur-the-cluster-while-pushing-in. Finite, deterministic, seek-safe. Tags: blur, depth-of-field, focus, rack-focus, dim, spotlight @@ -43,6 +62,7 @@ Atomic motion recipes. Each lives at `rules/.md`. Compose 2-4 per scene wi Elements flip in from 3D space (`rotateX` + `rotateY` + `translateZ`) then settle into a continuous elliptical orbit. **Critical**: entry MUST flip in-place at the orbital starting position (`gsap.set` BEFORE phase 1), not at scene center. Tags: orbit, 3d, flip, ellipse, circular, icon, entry, continuous AI detection overlay — yellow `#facc15` L-bracket corners + confidence label (fluctuating 95-99%) following a target on a sine arc path. Box position recomputed per-frame from target position (never tweened separately). Tags: ai, tracking, bounding-box, detection, corner, ml N elements scatter into / reassemble from a rotating 3D depth-cloud — each starts at a deterministic index-derived 3D offset (translateZ + rotateX/Y + scatter) and settles to a clean flat layout; tumble-swap and radial-explode variants. preserve-3d + perspective, transform-only, seek-safe. Tags: 3d, scatter, assemble, tumble, depth, perspective, glyphs +Edge-pinned container grows/collapses along ONE axis and in-flow content reflows — pill springs open into a dropdown, panel grows a sub-task stack, input card steps taller as typed text wraps, pane expands over a neighbor. Transform-only (layout authored expanded; mask + sheet slide, or proxy-driven scaleY + inverse counter-scale) since width/height tweens are forbidden; the push on following content shares the SAME tween so the seam never separates. Tags: expand, collapse, anchored, dropdown, accordion, panel, reflow, push, mask, counter-scale ## SVG & Icons @@ -66,11 +86,16 @@ Atomic motion recipes. Each lives at `rules/.md`. Compose 2-4 per scene wi Tactile button press: linear compression then spring recovery via two adjacent GSAP tweens on the same property. Variations: color transition, shadow depth via CSS vars, release burst, background glow. Tags: spring, press, button, interaction, physics, glow, burst Physical click simulation — two sequential GSAP scale tweens (down to 0.9, up to 1.0) approximate a spring with overshoot. Pass a single targets array `["#cta", "#cursor"]` to compress both together for tactile contact feel. Tags: spring, click, physics, press, interaction, cursor Animated cursor moves to a target, depresses cursor + target together on click, emits an expanding ripple with attack-decay opacity envelope. Element lives in DOM from t=0 with `opacity: 0` (no conditional rendering). Tags: cursor, click, ripple, interaction, mouse, button, keyframes +The drag verb for driven cursors — grab (press dip + lift), travel (semi-transparent ghost rides the cursor in exact lockstep via matched tweens), drop-snap into a placed field with selection chrome. Variants: fill-handle auto-fill (linear travel + stepped `tl.set` cell reveals), corner-handle proportional resize (uniform scale, origin at the anchor corner — never width/height), grab-lift-reorder (tilt + shadow, neighbor springs into the vacated slot). Tags: cursor, drag, drop, ghost, handle, resize, reorder, snap, interaction +N (2–4) labeled independent cursor actors work one canvas simultaneously — collaborative-canvas ambience. Per-actor deterministic waypoint tables (explicit fromTo legs + rests), name-tag pills in distinct colors, grab/drop/hover actions at chorus intensity on an interleaved beat grid (one payoff at a time, zone-partitioned paths, no collisions); camera locked — any pan is the canvas group translating. Tags: cursor, multi-cursor, collaboration, ensemble, canvas, name-tag, choreography, ambient +Live-sync couple — a scrubbed/typed/picked control and its bound target change in the SAME beat: readout tween + target transform tween share one timeline label, duration, and ease (continuous scrub), or one threshold state array carries both sides (per-keystroke / dropdown-pick steps). Distinct from `reactive-displacement` (collision physics, one-shot transition). Tags: control, scrub, live-sync, mirror, panel, editor, readout, ui Coordinated morph between two DOM elements at the same screen center. Exit cluster shrinks + fades; entrance pops in with `back.out(2)` overshoot. Tags: transition, morph, scale, swap Container morphs apparent size + corner radius + surface treatment between two shots, then fades to reveal the real target underneath. HyperFrames substitutes uniform `scale` for the forbidden `width`/`height` tween, plus paint-only `borderRadius`/`background`/`boxShadow`. Tags: morph, anchor, transition, border-radius, container, shape, handoff +Whole-theme in-place morph under a fixed anchor — background, typography, radii, icons, chrome and logos blend simultaneously (~0.3s) through N pre-styled skins while one anchor element never moves. Stacked complete layers + opacity-only crossfade, anchor rendered once on top (or per-layer at identical geometry); static camera. Single container instead → `card-morph-anchor`. Tags: theme, skin, crossfade, morph, anchor, reskin, cycle, ui The canonical ENTRANCE pop — an element (or staggered group) arrives by springing `scale: 0 → 1` with `back.out` overshoot, `fromTo` so it's correct at t=0 under seek. Single hero, staggered group (≤500ms cap), overshoot tuned by personality. Distinct from `press-release-spring` (a click/press reaction). Tags: spring, entrance, pop, scale-in, overshoot, stagger, arrival Fake directional velocity blur on a fast entrance / camera push-through — blur peaks at max speed, resolves to 0 at the settle. Two paths: SVG `feGaussianBlur` stdDeviation on the motion axis (proxy-tweened), or a deterministic echo/ghost trail that collapses into the lead. Entrances / mid-shot only. Tags: motion-blur, streak, velocity, ghost, echo, fast Staggered ARRIVAL cascade — words/elements whip in from below, each starting before the previous settles, an accelerating wave that resolves composed. Title cards, segment openers, list intros. Binary 0→1 opacity via `tl.set` — never fade an arrival. Tags: entrance, cascade, stagger, kinetic-text, title-card, arrival, waterfall +Deterministic particle / confetti events — confetti pop that bursts up and drifts down on gravity (optional instant-shrink), dot burst from behind text, glyph dissolve to particles. Fixed pool, index-seeded launch values, one `ease: "none"` driver whose onUpdate computes each particle as a pure ballistic function of time — scrub-safe mid-flight, ≤ ~40 particles. Tags: particles, confetti, burst, dissolve, ballistic, deterministic, punctuation Slow-fast-slow three-phase group slide (power3.in ramp → linear burst → power4.out tail, 10/65/25 distance, tail ≥3× ramp-in) to reposition a composed group and reveal content during the burst. Tags: slide, reposition, group-motion, nudge, slow-fast-slow diff --git a/skills/hyperframes-animation/rules/3d-camera-flight.md b/skills/hyperframes-animation/rules/3d-camera-flight.md new file mode 100644 index 000000000..08ac4895b --- /dev/null +++ b/skills/hyperframes-animation/rules/3d-camera-flight.md @@ -0,0 +1,180 @@ +--- +name: 3d-camera-flight +description: Perspective camera FLIGHT through a 3D-laid-out world — one static perspective stage + preserve-3d world whose pose (translate3d + rotateX/rotateY) is tweened leg-by-leg from a single camera state object. Dive into an angled grid, tilt-to-flatten pull-back, continuous flight past standing cards, decelerate-into-focus. Hard power4.out landings, power2.inOut repositioning; DoF via depth-of-field-blur on non-focal planes. +metadata: + tags: camera, 3d, flight, perspective, preserve-3d, rotateX, rotateY, translateZ, dive, tilt, world, cinematic +--- + +# 3D Camera Flight + +Every other camera rule here is a **2D camera**: [viewport-change.md](viewport-change.md), [multi-phase-camera.md](multi-phase-camera.md), and [coordinate-target-zoom.md](coordinate-target-zoom.md) simulate the camera with `scale` + `translate` on a flat wrapper — the lens never tilts, and there is no depth axis to travel along. [3d-page-scroll.md](3d-page-scroll.md) is a **static tilt**: one angle held all scene while content scrolls inside. This rule is the missing camera that _flies_ — dives into an angled grid, pulls back while the world rotates flat, streaks past standing cards, decelerates out of a blur into focus: a **perspective camera traveling with `rotateX` / `rotateY` / `translateZ` through a 3D-laid-out world**, under the same single-camera discipline as `viewport-change`: **one perspective wrapper, one camera state object, one transform writer**, every leg a sequenced tween on that state. + +## How It Works + +Five layers, strictly separated: + +1. **The lens** — `perspective: PERSPECTIVE_PX` on a static `.stage` wrapper. Set once, never tweened, never moved. Changing perspective mid-shot reads as the lens itself warping, not the camera moving. +2. **The world** — a `.world` div with `transform-style: preserve-3d`, laid out at final 1× size: the ground surface (grid, form card, canvas) as flat DOM, optional **props** (a giant date number, a floating label) at static `translateZ(PROP_Z)` offsets so travel produces parallax, and **standing cards** counter-tilted to face the camera at their landing pose. +3. **The camera state** — a single object `cam = { x, y, z, rx, ry }` (the world's pose), written to `world.style.transform` by ONE function, `applyCamera()`, in a **fixed order**: `translate3d(x, y, z) rotateX(rx) rotateY(ry)`. With translate composed _outside_ the rotations, `x`/`y`/`z` always move the world along **screen axes** no matter how it is currently tilted — pan is always sideways, `z` is always toward/away from the lens. Put the rotations first and every leg's numbers change meaning as the tilt changes. +4. **The legs** — sequential tweens on `cam`, each one camera move: dive in (`power4.out` — violent arrival, sharp settle), tilt-to-flatten pull-back (`power2.inOut` — a repositioning, no slam), lateral flight, final dive. Camera intent inverts onto the world pose exactly as in `viewport-change`: camera flies **in** → world `z` **increases** (comes toward the lens); camera pans **right** → world `x` **negative**; camera tilts **down** over the surface → world `rx` **positive** (far edge tips away). +5. **Depth cues** — DoF via [depth-of-field-blur.md](depth-of-field-blur.md) `--dof` tweens on the **non-focal planes** (cards, props — leaf elements, never the world itself), and velocity blur on travel legs via [motion-blur-streak.md](motion-blur-streak.md)'s Camera-Travel Carve-Out — applied to the **stage**, never the world (a `filter` on a `preserve-3d` element flattens it). + +Landing poses are **authored, not derived**: set `cam` to candidate values at design time, call `applyCamera()`, screenshot, adjust, bake the numbers as constants. There is no counter-translate formula to get wrong in 3D — the pose IS the design decision. Never measure per-frame (`getBoundingClientRect` in `onUpdate` desyncs under parallel frame sampling), and don't hand-derive 3D projections — your eye at design time beats the math. + +## Recipe + +```html + +
+ +
+
+
{gridCells}
+
{cardA}
+
{cardB}
+
+ +
{propGlyph}
+
+
+``` + +```css +.scene { + overflow: hidden; /* travel legs push world content past the frame on purpose */ + background: {sceneBg}; /* the void the flight exposes at frame edges — must be a + designed surface (deep brand color / soft gradient), never default white */ +} +.stage { + position: absolute; + inset: 0; + perspective: PERSPECTIVE_PX; /* THE LENS — static, never tweened */ + /* travel blur (motion-blur-streak carve-out) attaches HERE, never on .world */ +} +.world { + position: absolute; + inset: 0; + transform-style: preserve-3d; + transform-origin: 50% 50%; + will-change: transform; + /* keep CLEAN: no filter, opacity < 1, overflow, clip-path, or mask — each + flattens preserve-3d. Background on .scene, blur on .stage or leaf cards. */ +} +.surface { + position: absolute; + inset: WORLD_INSET; /* world runs larger than the frame so travel has runway */ + transform-style: preserve-3d; +} +.prop { + position: absolute; + left: var(--px); + top: var(--py); + /* static world-space pose; counter-tilt faces the camera at the dive pose */ + transform: translateZ(PROP_Z) rotateX(PROP_COUNTER_TILT); +} +.layer { + --dof: 0px; /* DoF channel per depth-of-field-blur — leaf elements only */ + filter: blur(var(--dof)); + will-change: filter; +} +``` + +```js +const world = document.getElementById("world"); + +// Camera state — the ONLY source of truth for the world's pose. Every leg +// tweens this object; nothing else touches world.style.transform. +const cam = { x: 0, y: 0, z: WIDE_Z, rx: 0, ry: 0 }; + +function applyCamera() { + // Fixed order: translate OUTSIDE the rotations → x/y/z stay screen-aligned + // at any tilt. Changing this order changes what every baked pose means. + world.style.transform = `translate3d(${cam.x}px, ${cam.y}px, ${cam.z}px) rotateX(${cam.rx}deg) rotateY(${cam.ry}deg)`; +} +applyCamera(); // seed frame 0 so a seek to t=0 renders the opening pose + +// ── LEG 1 — DIVE IN: wide establishing pose → angled close-up on card A. +// fromTo states the opening pose explicitly; power4.out = violent arrival, +// razor-sharp settle. Travel blur: motion-blur-streak carve-out on .stage. +const DIVE_POSE = { x: DIVE_X, y: DIVE_Y, z: DIVE_Z, rx: DIVE_RX, ry: DIVE_RY }; +tl.fromTo( + cam, + { x: 0, y: 0, z: WIDE_Z, rx: 0, ry: 0 }, + { ...DIVE_POSE, duration: DIVE_DUR, ease: "power4.out", onUpdate: applyCamera }, + DIVE_AT, +); +// Decelerate-INTO-FOCUS: non-focal planes' --dof ramps to BLUR_PER_DEPTH × data-depth +// on the SAME window/ease (depth-of-field-blur focal pull); card A stays at --dof: 0. + +// ── LEG 2 — TILT-TO-FLATTEN PULL-BACK: every channel returns to neutral on ONE +// power2.inOut tween — a reposition, not a slam. DoF releases on the same window +// so the flat overview arrives fully crisp. +const FLAT_POSE = { x: 0, y: 0, z: 0, rx: 0, ry: 0 }; +tl.to( + cam, + { ...FLAT_POSE, duration: FLATTEN_DUR, ease: "power2.inOut", onUpdate: applyCamera }, + FLATTEN_AT, +); +tl.to(".layer", { "--dof": "0px", duration: FLATTEN_DUR, ease: "power2.inOut" }, FLATTEN_AT); + +// ── LEG 3 — LATERAL FLIGHT: screen-aligned pan (translate is outside the +// rotations, so x is a pure sideways move even mid-tilt). +tl.to(cam, { x: PAN_X, duration: PAN_DUR, ease: "power2.inOut", onUpdate: applyCamera }, PAN_AT); + +// ── LEG 4 — FINAL DIVE onto card B: same grammar as leg 1; card A racks OUT of +// focus as card B racks in (depth-of-field-blur rack, shared window). +const LAND_POSE = { x: LAND_X, y: LAND_Y, z: LAND_Z, rx: LAND_RX, ry: LAND_RY }; +tl.to( + cam, + { ...LAND_POSE, duration: LAND_DUR, ease: "power4.out", onUpdate: applyCamera }, + LAND_AT, +); +tl.to("#card-a", { "--dof": `${MAX_BLUR}px`, duration: LAND_DUR, ease: "power4.out" }, LAND_AT); +tl.to("#card-b", { "--dof": "0px", duration: LAND_DUR, ease: "power4.out" }, LAND_AT); +// Landing dwell: ≥1 s of stillness on card B — unless ending held mid-dive. +``` + +## Variations + +- **Continuous flight past standing cards** — one long leg instead of dive-land-dive: sustained `z` + `x` travel (2–4 s, `power2.inOut` / `power1.inOut` near-constant cruise) through a corridor of cards and props at staggered `PROP_Z`. Parallax does the work — near props streak past while far ones crawl. Keep ONE plane sharp at a time via staggered `--dof` tweens. Props crossing the camera plane (`cam.z + PROP_Z` approaching `PERSPECTIVE_PX`) blow up to fill the frame and vanish — that IS the fly-past; never let a focal card cross it. +- **End held mid-dive** — give the final leg a window that overruns the composition (`LAND_AT + LAND_DUR > data-duration`); the last frame holds mid-tween — still traveling, blur not fully resolved. Seek-safe by construction (a seek to the last frame lands at a deterministic pose); don't fake it with a shorter leg plus a manual offset. Use when the brief wants momentum at the cut, not rest. +- **Whip sweep** — the heavily motion-blurred lateral whip that resolves into the next region: leg 3 driven by [nudge-curve.md](nudge-curve.md)'s three-phase chain (burst-dominant) on `cam.x`, with [motion-blur-streak.md](motion-blur-streak.md)'s Camera-Travel Carve-Out on the same window — blur ramps through the ramp-in, rides the burst at peak, resolves to 0 through the `power4.out` tail. Full recipe in that carve-out. +- **Hold drift (the hold never dies)** — between legs, fold `multi-phase-camera`-style micro-drift **through the same writer**: a driver tween writes tiny `dx`/`dy`/`drx` into a `drift` object and `applyCamera()` composes `cam.x + drift.dx`, `cam.rx + drift.drx`, etc. Never let drift write `world.style.transform` itself — two writers on one transform is the classic camera bug. Amplitudes per `multi-phase-camera` (2–8 px), rotation drift ≤ 0.5°. + +## Values + +| token | range | notes | +| ------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| PERSPECTIVE_PX | 700–1400 px (moving cam best 800–1200) | smaller = wilder foreshortening, more violent dives; larger = near-orthographic, the flight flattens | +| WORLD_INSET | −50% to −150% per side | world 2–4× the frame so lateral legs have runway | +| PROP_Z | 80–300 px | higher = stronger parallax, earlier fly-past | +| PROP_COUNTER_TILT | ≈ `-LAND_RX` of the leg that reads it | author by eye and bake | +| DIVE_RX / LAND_RX | 30–55° | "angled grid" starts ~30°; \|rx\| ≤ ~65°, \|ry\| ≤ ~30° — beyond that flat planes go edge-on, text unreadable | +| DIVE_Z / LAND_Z | 300–700 px at PERSPECTIVE_PX ≈ 1000 | **Z budget**: `cam.z + PROP_Z ≤ ~0.6 × PERSPECTIVE_PX` for readable content — near the perspective distance, scale blows toward infinity and elements invert/vanish past the camera plane | +| WIDE_Z | −100 to −400 px | negative z = world pushed away = camera wide | +| DIVE_X/Y, LAND_X/Y | read off a screenshot at the baked tilt | screen-aligned (translate outside rotations) | +| DIVE_DUR / LAND_DUR | 0.6–1.0 s | commitment, not a polite zoom; under 0.5 s reads as a cut | +| FLATTEN_DUR | 1.2–2.0 s | the repositioning is the breath between dives | +| PAN_DUR | 0.8–1.5 s plain; 0.5–0.8 s whip | | +| Ease law | `power4.out` dives/landings; `power2.inOut` repositioning/cruise | spring/back on a camera reads as the world wobbling on a string; four identical pushes read as a slideshow — vary the leg verbs | +| Holds | ≥ 0.8 s between legs; final dwell ≥ 1 s | unless ending held mid-dive | +| BLUR_PER_DEPTH / MAX_BLUR | per [depth-of-field-blur.md](depth-of-field-blur.md) | 3–6 px per step, terminal 8–24 px, leaf elements only; travel-blur peak per [motion-blur-streak.md](motion-blur-streak.md) (~18–20 px full-frame, on `.stage`) | + +## Critical Constraints + +- **One lens, one state, one writer** — `perspective` on the static `.stage` only (never on `.world`, never tweened, never a second perspective wrapper inside); every leg tweens the single `cam` object; only `applyCamera()` writes the transform — drift folds into the same writer via additive state. Two writers (or a second transform sneaking in via CSS) is the classic broken-camera bug, five channels of it here. +- **Fixed transform order: translate outside the rotations** — `translate3d(x,y,z) rotateX() rotateY()`. Reorder it and every pose you authored silently means something else. +- **Keep the world CLEAN** — `filter`, `opacity < 1`, `overflow` other than `visible`, `clip-path`, or `mask` on `.world` (or any intermediate wrapper) forces used `transform-style: flat` and collapses every `translateZ` in the scene. Travel blur goes on `.stage`; DoF on leaf cards; fades on children; background on `.scene`. `transform-style: preserve-3d` on `.world` and every intermediate wrapper between it and 3D-positioned children. +- **Camera intent inverts onto the world** — fly in = world z up, pan right = world x negative, tilt down = world rx positive. Same sign law as `viewport-change`, two more axes to get right. +- **Poses authored and baked** — never measured per-frame, never hand-derived projections. +- **First leg is a `fromTo`** AND `applyCamera()` runs once at setup — a seek to t=0 must render the exact establishing pose. +- **Z budget** — only sacrificial props may cross the camera plane. +- **Reads happen at landings** — angled, blurred, flying text is texture; anything the viewer must read gets a near-flat pose or a sharp held close-up ≥ 1 s (the tilt-to-flatten leg exists to hand the surface over for reading). +- **`overflow: hidden` on `.scene` + `data-layout-allow-overflow` on `.world`** — travel legs deliberately push panels past the frame; without the pairing, `check` reports `container_overflow` for every region the flight leaves behind. + +## See also + +[viewport-change.md](viewport-change.md) (2D counterpart, same single-writer law — right when the shot never tilts) · [multi-phase-camera.md](multi-phase-camera.md) (leg-sequencing grammar + hold micro-drift) · [coordinate-target-zoom.md](coordinate-target-zoom.md) (aim math for a flat-hold zoom while `rx`/`ry` are 0) · [depth-of-field-blur.md](depth-of-field-blur.md) (non-focal defocus / racks) · [motion-blur-streak.md](motion-blur-streak.md) (travel blur on the stage) · [nudge-curve.md](nudge-curve.md) (whip-sweep burst tuning) · [3d-page-scroll.md](3d-page-scroll.md) (static-tilt cousin — camera should NOT travel) · [orbit-3d-entry.md](orbit-3d-entry.md) / [depth-scatter-assemble.md](depth-scatter-assemble.md) (elements moving under a still camera — the inverse; don't run both on one beat). Capability background: `../techniques.md` § CSS 3D Transforms. diff --git a/skills/hyperframes-animation/rules/3d-page-scroll.md b/skills/hyperframes-animation/rules/3d-page-scroll.md index b0ab69262..e3a8f8635 100644 --- a/skills/hyperframes-animation/rules/3d-page-scroll.md +++ b/skills/hyperframes-animation/rules/3d-page-scroll.md @@ -7,145 +7,91 @@ metadata: # 3D Page Scroll -A webpage (or long content) presented as a tilted 3D card. Spring-eased scroll reveals specific sections while the static 3D perspective adds physical depth. +A webpage (or long content) presented as a tilted 3D card. Spring-eased scroll reveals specific sections while the static 3D perspective adds physical depth. (For a camera that actually travels/tilts, see [3d-camera-flight.md](3d-camera-flight.md) — this rule's tilt never moves.) ## How It Works Two independent transforms combine: -1. **3D tilt** — Static `rotateY` + `rotateX` with `perspective` on the card. The angle does **not** change during the scene. -2. **Scroll** — The content inside the card translates vertically (`translateY` / `y` in GSAP) within a clipped container, driven by a GSAP tween. Spring-like deceleration via `ease: "power3.out"` or `"power4.out"`. +1. **3D tilt** — static `rotateY` + `rotateX` with `perspective` on the card. The angle does **not** change during the scene. +2. **Scroll** — the content inside the card translates vertically (`y` in GSAP) within a clipped container; spring-like deceleration via `power3.out` / `power4.out`. -Optional layer: +Optional: **spotlight overlay** — a radial-gradient mask dims everything except a focal region after the scroll lands. It sits above the scrolling content, fixed relative to the card, never inside `.page-content`. -3. **Spotlight overlay** — A radial-gradient mask dims everything except a focal region after the scroll lands. Use to draw attention to one section. - -For multi-step scrolling (scroll → pause → scroll), use multiple `tl.to(".page-content", { y: -, ... }, )` calls at different timeline positions. - -## HTML +## Recipe ```html -
-
-
- -
{heroContents}
-
{featuresContents}
-
{targetContents}
-
{ctaContents}
-
- -
+
+
+ +
{heroContents}
+
{featuresContents}
+
{targetContents}
+
{ctaContents}
+
``` -## CSS (hero-frame layout) - ```css -.scene { - position: relative; - width: 100%; - height: 100%; -} - .tilt-card { position: absolute; left: 50%; top: 50%; - /* tilt + perspective set in CSS only if no other transform tween touches - this element. If GSAP also tweens scale on .tilt-card, set the tilt - via gsap.set() to avoid matrix overwrites. */ + /* tilt + perspective in CSS only if no other transform tween touches this + element — if GSAP also tweens scale on .tilt-card, set the tilt via + gsap.set() instead to avoid matrix overwrites */ transform: translate(-50%, -50%) perspective({perspectivePx}) rotateY({tiltYDeg}) rotateX({tiltXDeg}); transform-style: preserve-3d; width: {cardWidth}; height: {cardHeight}; border-radius: 24px; background: {cardBackgroundColor}; - overflow: hidden; /* clip the scrolling content */ + overflow: hidden; /* clip the scrolling content at the rounded corners */ /* shadow X-offset sign must match tiltY sign (negative tiltY ⇒ positive X) */ box-shadow: 40px 30px 80px rgba(0, 0, 0, 0.45); } - .page-content { position: absolute; top: 0; left: 0; width: 100%; - /* height is intrinsic from sections — taller than .tilt-card.height */ + /* height intrinsic from sections — taller than the card */ } - -.page-content section { - height: {sectionHeight}; /* sections sized so cumulative offset = target distance */ - padding: 64px; - /* section-specific styling … */ -} - .spotlight { position: absolute; inset: 0; pointer-events: none; opacity: 0; - background: radial-gradient( - ellipse 60% 35% at 50% 50%, - transparent 50%, - {spotlightDimColor} 100% - ); + background: radial-gradient(ellipse 60% 35% at 50% 50%, transparent 50%, {spotlightDimColor} 100%); } ``` -## GSAP Timeline +```js +// SCROLL_DISTANCE is measured at design time from the real page layout +// (top of .page-content origin to vertical center of #target-section, +// accounting for card height) — NOT a free tunable. +tl.to( + ".page-content", + { y: -SCROLL_DISTANCE, duration: SCROLL_DUR, ease: "power3.out" }, + SCROLL_AT, +); -```html - - +// Spotlight fades in on the target after the scroll settles. +tl.to( + ".spotlight", + { opacity: 1, duration: SPOTLIGHT_FADE_DUR, ease: "power1.inOut" }, + SPOTLIGHT_AT, +); ``` -### Multi-phase scroll variant +## Variations + +**Multi-step scroll (scroll → pause → scroll)** — multiple `y:` tweens at different positions. Distances are both measured from the `.page-content` origin (NOT delta from the previous step); GSAP composes successive `y:` tweens on the same property, each starting from the value the previous one left: ```js -// Scroll to section A → hold → scroll to section B. -// SCROLL_DISTANCE_A and SCROLL_DISTANCE_B are both measured from the -// .page-content origin (NOT delta from previous step). tl.to( ".page-content", { y: -SCROLL_DISTANCE_A, duration: SCROLL_DUR, ease: "power3.out" }, @@ -156,72 +102,34 @@ tl.to( { y: -SCROLL_DISTANCE_B, duration: SCROLL_DUR, ease: "power3.out" }, SCROLL_AT_B, ); +// SCROLL_AT_A + SCROLL_DUR ≤ SCROLL_AT_B — the two scrolls must not fight for y ``` -GSAP composes successive `y:` tweens additively when targeting the same property — each tween starts from the value left by the previous tween. +## Values -## How to Choose Values - -- **tiltYDeg** — static Y rotation in CSS (or via `gsap.set()`). - - Range: -12 to -4 (left-leaning) or 4 to 12 (right-leaning); 0 = no perspective rotation. - - Effects: bigger magnitude = more dramatic 3D; near 0 collapses to a flat panel. - - Constraints: shadow X-offset sign must match (negative tiltY ⇒ positive box-shadow X). -- **tiltXDeg** — static X rotation. - - Range: 0-6 - - Effects: positive tilts the top edge away from the viewer. -- **perspectivePx** — perspective distance. - - Range: 800-2000 px - - Effects: smaller = more dramatic foreshortening; larger = nearly orthographic. -- **cardWidth / cardHeight** — card frame size. - - Constraints: card height < total content height, otherwise scroll has nothing to reveal. -- **sectionHeight** — height of each scrolled section. - - Constraints: sum of all section heights ≥ cardHeight + SCROLL_DISTANCE so the target section ends up within frame after scroll. -- **SCROLL_AT** — timeline second at which the scroll tween begins. - - Constraints: must be ≥ end of any prior fade-in tweens on `.page-content`. -- **SCROLL_DUR** — duration of one scroll tween. - - Range: 0.8-1.8 s - - Effects: shorter feels like a hard cut; longer feels programmatic. -- **SCROLL_DISTANCE** — pixels to translate `.page-content` upward. - - Constraints: measured once at design time from the target section's offset; NOT a free tunable. -- **SPOTLIGHT_AT** — timeline second at which the spotlight begins fading in. - - Constraints: should be ≥ SCROLL_AT + SCROLL_DUR (or slightly earlier for overlapping handoff) so the spotlight reveals the freshly-arrived section. -- **SPOTLIGHT_FADE_DUR** — spotlight opacity fade-in duration. - - Range: 0.4-0.8 s -- Multi-phase variant — **SCROLL_AT_A / SCROLL_AT_B**: must satisfy `SCROLL_AT_A + SCROLL_DUR ≤ SCROLL_AT_B` so the two scrolls don't fight for the y property. - -Ease family — discrete choice: - -- `power3.out` — heavy deceleration; reads as a programmatic scroll that "lands". Default. -- `power4.out` — even heavier; reads as a momentum-driven scroll. -- `power2.inOut` — symmetric; reads as a cinematic camera pan rather than UI scroll. - -Pick one and use it across all scrolls in the scene — mixing easings within one scene reads as jerky. - -## Key Principles - -- **Tilt is static**, not animated. The card holds its angle the whole scene. -- **Shadow direction matches tilt**: a left-leaning card casts shadow to the right (positive X shadow offset). Mismatch breaks the 3D illusion. -- **Page content is real HTML**, not a screenshot. Screenshots can't be individually highlighted or scrolled-to with precision. -- **Use real layout for distances**: scroll target distance comes from the actual cumulative section heights, not estimated pixel values. -- **Spotlight as overlay**, not inside the page-content — overlay sits above scrolling content and stays fixed relative to the card. +| token | range / rule | notes | +| ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| tiltYDeg | −12 to −4 (left-leaning) or 4 to 12 | bigger = more dramatic 3D; near 0 collapses to a flat panel | +| tiltXDeg | 0–6 | positive tilts the top edge away | +| perspectivePx | 800–2000 px | smaller = more foreshortening; larger = nearly orthographic | +| cardWidth / Height | card height < total content height | otherwise the scroll has nothing to reveal | +| sectionHeight | Σ heights ≥ cardHeight + SCROLL_DISTANCE | so the target section lands within frame | +| SCROLL_AT | ≥ end of prior tweens on `.page-content` | | +| SCROLL_DUR | 0.8–1.8 s | shorter feels like a hard cut; longer feels programmatic | +| SCROLL_DISTANCE | measured from the layout | from actual cumulative section heights — never estimated; don't overshoot content end | +| SPOTLIGHT_AT | ≥ SCROLL_AT + SCROLL_DUR (or slightly earlier) | spotlight reveals the freshly-arrived section | +| SPOTLIGHT_FADE_DUR | 0.4–0.8 s | | +| Ease | `power3.out` default; `power4.out` momentum; `power2.inOut` cinematic pan | pick ONE for all scrolls in the scene — mixing easings reads as jerky | ## Critical Constraints -- **`overflow: hidden` on `.tilt-card`** — scrolling content must clip at card boundaries, otherwise it leaks past the rounded corners -- **`transform-style: preserve-3d`** on `.tilt-card` — required for any 3D children (or for combining `perspective` with rotations cleanly) -- **Timeline must be paused**: `gsap.timeline({ paused: true })`. Never `tl.play()` — HF seeks frame-by-frame -- **Registry key = `data-composition-id`**: `window.__timelines["page-scroll-scene"]` must match scene root's `data-composition-id` -- **Finite scroll distance** — compute from actual content geometry; don't use arbitrary values that may overshoot the content end -- **Same easing across multi-phase scroll** — mixing `power3.out` and `power1.inOut` looks jerky; pick one for the scene +- **Tilt is static** — the card holds its angle the whole scene. +- **Shadow direction matches tilt** — a left-leaning card casts shadow to the right (positive X offset); mismatch breaks the 3D illusion. +- **Page content is real HTML, not a screenshot**; scroll distances come from the real layout geometry. +- **`overflow: hidden` + `transform-style: preserve-3d` on `.tilt-card`** — clip at the rounded corners; preserve-3d for any 3D children / clean perspective composition. +- **Spotlight is an overlay above the scrolling content**, never inside `.page-content`. +- **Same easing across a multi-phase scroll**, and non-overlapping scroll windows. -## Combinations +## See also -- [asr-keyword-glow.md](asr-keyword-glow.md) — highlight elements on the page synced to voiceover word timestamps -- [multi-phase-camera.md](multi-phase-camera.md) — overall camera zoom while the page scrolls (zoom-in to target section as it lands) -- [cursor-click-ripple.md](cursor-click-ripple.md) — cursor lands on a UI element within the scrolled-into-view section - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + ease reference; `y:` tween basics -- `/hyperframes-core` — composition wiring, `data-*` attributes -- `/hyperframes-cli` — `hyperframes lint` to verify the registry key + duration +[asr-keyword-glow.md](asr-keyword-glow.md) (on-page keyword highlight synced to VO) · [multi-phase-camera.md](multi-phase-camera.md) (camera zoom while the page scrolls) · [cursor-click-ripple.md](cursor-click-ripple.md) (cursor lands in the scrolled-into-view section) · [3d-camera-flight.md](3d-camera-flight.md) (when the camera itself should travel). diff --git a/skills/hyperframes-animation/rules/3d-text-depth-layers.md b/skills/hyperframes-animation/rules/3d-text-depth-layers.md index 8ebc903c2..5ca2641b0 100644 --- a/skills/hyperframes-animation/rules/3d-text-depth-layers.md +++ b/skills/hyperframes-animation/rules/3d-text-depth-layers.md @@ -7,291 +7,122 @@ metadata: # 3D Text Depth Layers -Renders the same text N times at increasing offsets, with back layers translucent and the front layer fully opaque. Creates a physical "stacked extrusion" depth illusion. Distinct from `text-shadow` (which can't have per-layer hue / opacity / animation) — each layer is a real DOM element. +The same text rendered N times at increasing offsets — back layers translucent, front layer full opacity and brand color — creates a physical "stacked extrusion" depth illusion on large typography. Distinct from `text-shadow` (which can't have per-layer hue / opacity / animation): each layer is a real DOM element. ## How It Works -- N copies of the same text in a single container -- Each copy positioned absolutely with offset `(i * OFFSET_X, i * OFFSET_Y)` -- Back layers (high `i`) use translucent or darkened color -- Front layer (`i = 0`) is full opacity, full brand color -- Optionally: each layer fades in staggered, creating a "building up" depth animation +A build script appends `LAYER_COUNT` copies back-to-front; each back layer sits at `translate(i × OFFSET_X, i × OFFSET_Y)` with alpha stepping down per layer, while the front copy (`i = 0`) is `position: relative` so it defines the container size (back layers stack absolutely behind it). The default entrance cascades the layers' fades back-to-front while a proxy tween grows the offsets from 0 → full, so the depth "builds forward" and lands as the last layer fades in. -## HTML +## Recipe ```html -
-
- -
+ +
+
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; -} .depth-stack { - position: relative; - /* Container size set by the front layer; back layers stack behind */ + position: relative; /* front layer defines size; back layers stack behind */ } .depth-text { - font-family: {font}; - font-weight: 900; + font-weight: 900; /* black weight — thin text loses the illusion */ font-size: HERO_FONT_SIZE; letter-spacing: HERO_LETTER_SPACING; line-height: 1; color: {frontColor}; - text-transform: uppercase; } -/* Back layers — absolute, stacked behind */ .depth-text.is-back { position: absolute; top: 0; left: 0; - pointer-events: none; + pointer-events: none; /* decorative */ } -/* Front layer — relative to define container size */ .depth-text.is-front { position: relative; z-index: 10; } ``` -## GSAP Timeline + Layer Setup +```js +const stack = document.querySelector(".depth-stack"); -```html - - +// Depth grows on entry — offsets interpolate 0 → full +const depthState = { p: 0 }; +tl.to( + depthState, + { + p: 1, + duration: DEPTH_GROW_DUR, + ease: "power2.out", + onUpdate: () => { + stack.querySelectorAll(".depth-text.is-back").forEach((el) => { + const i = Number(el.dataset.layer); + el.style.transform = `translate(${i * OFFSET_X * depthState.p}px, ${i * OFFSET_Y * depthState.p}px)`; + }); + }, + }, + LAYER_CASCADE_START, // align with the cascade so depth lands as the last layer fades in +); ``` ## Variations -### Static depth (no animation, single hero shot) +- **Static depth** (single hero shot) — render all layers at final positions from t=0; optionally fade the whole stack in with a subtle scale (0.94–0.98 → 1, 0.5–0.8s). +- **Dynamic depth pulse** — after the grow completes, modulate the offsets with a sine multiplier `1 + sin(p) × BEAT_AMP` (BEAT_AMP 0.2–0.6; one beat per 0.7–1.5s reads as a heartbeat). +- **Color-shift back layers** — instead of fading to translucent, step hue/lightness per layer: `hsla(HUE_BASE − i × HUE_STEP, SAT_PCT%, LIGHT_BASE − i × LIGHT_STEP%, 1)` (HUE_STEP 4–12°; larger reads as glitch). Depth reads as a colored cast shadow. -Skip the cascade — render all layers in their final positions from t=0, optionally fade the entire stack in: +## Values -```js -tl.from( - stack, - { opacity: 0, scale: STATIC_ENTRY_SCALE, duration: STATIC_ENTRY_DUR, ease: "power3.out" }, - 0, -); -``` - -### Dynamic depth pulse - -Animate `OFFSET_X` / `OFFSET_Y` based on a heartbeat — depth grows and shrinks rhythmically: - -```js -const beat = { p: 0 }; -tl.to( - beat, - { - p: Math.PI * 2 * BEAT_CYCLES, - duration: BEAT_DUR, - ease: "none", - onUpdate: () => { - const mult = 1 + Math.sin(beat.p) * BEAT_AMP; - stack.querySelectorAll(".is-back").forEach((el) => { - const i = Number(el.dataset.layer); - el.style.transform = `translate(${i * OFFSET_X * mult}px, ${i * OFFSET_Y * mult}px)`; - }); - }, - }, - BEAT_START, -); -``` - -### Color-shift back layers - -Instead of fading to translucent, shift to a different hue — depth reads as "casting a colored shadow": - -```js -el.style.color = `hsla(${HUE_BASE - i * HUE_STEP}, ${SAT_PCT}%, ${LIGHT_BASE - i * LIGHT_STEP}%, 1)`; -``` - -## How to Choose Values - -### Layer geometry - -- **LAYER_COUNT** — number of stacked copies (back layers + 1 front). - - Range: 4-6. Below 4 the depth doesn't read as 3D; above 6 the stack visually clutters on tight kerning - - Effects: low end reads as subtle shadow; high end reads as chunky extrusion -- **OFFSET_X / OFFSET_Y** — per-layer translation offset, in px. - - Range: 1-3 px each. Above 4 px reads as a glitch / chromatic aberration rather than depth - - Effects: offset direction implies light direction. `(+x, +y)` = light from upper-left; `(-x, +y)` = light from upper-right. Pick one and keep it consistent across the composition - - Constraints: same sign convention throughout the composition - -### Back-layer color falloff - -- **BACK_ALPHA_MAX** — alpha of the back layer nearest the front. - - Range: 0.6-0.85. Lower than 0.5 makes even the nearest back layer disappear; higher than 0.9 fights the front layer for dominance -- **BACK_ALPHA_STEP** — alpha decrement per layer further back. - - Range: 0.08-0.15. Smaller steps read as a soft gradient; larger steps read as discrete plates - - Constraints: choose so `BACK_ALPHA_MAX − (LAYER_COUNT − 2) × BACK_ALPHA_STEP ≥ BACK_ALPHA_MIN` -- **BACK_ALPHA_MIN** — floor below which back layers stop fading. - - Range: 0.1-0.2. Below 0.1 the deepest layer disappears entirely on dark backgrounds - -### Typography - -- **HERO_FONT_SIZE** — front-layer font size, in px. - - Range: 60 px minimum to read as layered; 200-340 px for full-bleed hero shots. Thin text loses the layered illusion -- **HERO_LETTER_SPACING** — letter spacing. - - Range: −0.03em (tight) to 0 (normal). Negative spacing tightens the stack so the offsets read as depth instead of as repetition -- **{font}** — typeface; pick a black/900 weight family with strong horizontal strokes -- **{frontColor}** — front-layer color; the brand or accent color -- **{backHueRGB}** — RGB triplet for back layers (e.g. matched to a brand glow). Used inside `rgba({backHueRGB}, ${alpha})` - -### Cascade entry (default form) - -- **LAYER_CASCADE_START** — timeline offset where the cascade begins. - - Constraints: ≥ 0; if another beat precedes, ≥ that beat's end -- **LAYER_CASCADE_STEP** — delay between each layer's fade-in. - - Range: 0.04-0.10 s. Smaller feels almost-simultaneous; larger feels stepped and mechanical -- **LAYER_FADE_DUR** — duration of each individual layer's fade-in. - - Range: 0.3-0.6 s - -### Depth-grow tween - -- **DEPTH_GROW_START** — when the offset growth begins. - - Constraints: typically `≈ LAYER_CASCADE_START`; align so the first layer's fade and the depth growth start together -- **DEPTH_GROW_DUR** — duration over which offsets interpolate from 0 to full. - - Range: 0.4-0.8 s. Roughly match `LAYER_FADE_DUR × LAYER_COUNT / 2` so depth lands as the last layer fades in - -### Static-depth variation - -- **STATIC_ENTRY_SCALE** — initial scale before the whole stack fades in. - - Range: 0.94-0.98 — subtle inflation; larger reads as a separate "pop" effect -- **STATIC_ENTRY_DUR** — fade-in duration for the whole stack. - - Range: 0.5-0.8 s - -### Dynamic-pulse variation - -- **BEAT_CYCLES** — number of full beat cycles across `BEAT_DUR`. - - Range: `BEAT_DUR / 1.5s ≤ BEAT_CYCLES ≤ BEAT_DUR / 0.7s` (one beat per 0.7-1.5 s reads as a heartbeat) -- **BEAT_DUR** — pulse tween duration. - - Constraints: tied to the visible window of the depth stack -- **BEAT_AMP** — fractional amplitude of the offset pulse. - - Range: 0.2-0.6. Smaller is a gentle breathing depth; larger reads as a kick-drum thump -- **BEAT_START** — when the pulse begins. - - Constraints: `≥ DEPTH_GROW_START + DEPTH_GROW_DUR` so the pulse modulates a fully-grown stack - -### Color-shift variation - -- **HUE_BASE / HUE_STEP** — base hue (front layer) and per-layer hue rotation. - - Range: `HUE_STEP` 4-12°. Larger steps cycle further around the color wheel and read as glitch -- **SAT_PCT** — fixed saturation for all layers. - - Range: 60-85% -- **LIGHT_BASE / LIGHT_STEP** — base lightness and per-layer darkening. - - Range: `LIGHT_STEP` 3-8 percentage points so back layers darken into the background - -## Key Principles - -- **Layer count 4-6** — fewer than 4 doesn't read as 3D, more than 6 visually clutters on tight kerning -- **Offset 1-3 px per axis** — subtle is dramatic. `OFFSET = 6+` looks like a glitch rather than depth -- **Offset direction implies light direction** — `(+x, +y)` = light from upper-left; `(-x, +y)` = light from upper-right. Pick one and be consistent across the composition -- **Back layers translucent OR darker** — DON'T make them MORE saturated than the front (looks like a halo). Each back layer should be slightly more transparent (`alpha -= BACK_ALPHA_STEP per layer`) or slightly darker -- **Last (front) layer `position: relative`** to define container size; all others `position: absolute` stack behind -- **Bold/black weight + large size** — 900 weight, 60 px+ minimum. Thin text loses the layered illusion -- **Don't apply per-letter animation on top of layers** — character animations (hacker-flip, typewriter) on top of 6-layer depth = chaos. If you need both effects, drop depth to 2-3 layers OR apply layers only to the static post-reveal state +| token | range | notes | +| ------------------- | ------------------------ | ---------------------------------------------------------------------- | +| LAYER_COUNT | 4–6 | <4 doesn't read as 3D; >6 clutters on tight kerning | +| OFFSET_X / OFFSET_Y | 1–3px each | >4px reads as glitch / chromatic aberration, not depth | +| BACK_ALPHA_MAX | 0.6–0.85 | nearest back layer; >0.9 fights the front for dominance | +| BACK_ALPHA_STEP | 0.08–0.15 | small = soft gradient; large = discrete plates | +| BACK_ALPHA_MIN | 0.1–0.2 | floor — below 0.1 the deepest layer vanishes on dark backgrounds | +| HERO_FONT_SIZE | 60px min; 200–340px hero | thin/small text loses the layered illusion | +| HERO_LETTER_SPACING | −0.03em–0 | tighter makes offsets read as depth, not repetition | +| LAYER_CASCADE_STEP | 0.04–0.10s | smaller ≈ simultaneous; larger feels stepped | +| LAYER_FADE_DUR | 0.3–0.6s | per-layer fade | +| DEPTH_GROW_DUR | 0.4–0.8s | ≈ `LAYER_FADE_DUR × LAYER_COUNT / 2` so depth lands with the last fade | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `text-shadow`** alongside layered depth — they compound and over-extrude -- **Use `transform: translate()` for offsets, not `top`/`left`** — translate composes cleanly with parent's centering and avoids reflow -- **`pointer-events: none` on back layers** — they're decorative; don't catch hover or selection -- **Set layer color via `rgba()` not opacity** — opacity on the whole element fades the rendered glyph including any shadow; rgba in `color` fades just the glyph +- **Offset direction implies light direction** — `(+x, +y)` = light upper-left, `(-x, +y)` = upper-right; one sign convention for the whole composition. +- **Back layers translucent OR darker — never more saturated than the front** (reads as a halo, not depth). +- **Set back-layer color via `rgba()` in `color`, not element `opacity`** — opacity fades the whole rendered glyph including any shadow. +- **Front layer `position: relative` defines container size**; back layers absolute with `pointer-events: none`; offsets via `transform: translate()`, never `top`/`left`. +- **No CSS `text-shadow` alongside layered depth** — they compound and over-extrude. +- **No per-letter animation on top of the stack** — hacker-flip / typewriter over 6-layer depth is chaos; drop to 2–3 layers or apply depth only to the static post-reveal state. -## Combinations +## See also -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — render the counter number with depth layers -- [sine-wave-loop.md](sine-wave-loop.md) — idle breathing on the front layer after reveal -- [center-outward-expansion.md](center-outward-expansion.md) — depth-stacked wordmark reveals after burst lands - -## Pairs with HF skills - -- `/hyperframes-animation` — staggered fade-ins + onUpdate for dynamic depth -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`counting-dynamic-scale` (counter rendered with depth layers) · `sine-wave-loop` (idle breathing on the front layer post-reveal) · `center-outward-expansion` (depth-stacked wordmark after the burst lands). diff --git a/skills/hyperframes-animation/rules/ai-tracking-box.md b/skills/hyperframes-animation/rules/ai-tracking-box.md index 05a6dcf44..0ae63af7e 100644 --- a/skills/hyperframes-animation/rules/ai-tracking-box.md +++ b/skills/hyperframes-animation/rules/ai-tracking-box.md @@ -7,82 +7,29 @@ metadata: # AI Tracking Box -A bounding box with corner markers ("L-brackets") that follows a moving target, simulating real-time AI detection. Position and size oscillate on sine paths to mimic continuous re-computation. Conventionally rendered in "AI detection yellow" (`{detectionYellow}`) on a dark background with a confidence label. +A bounding box of four L-bracket corners + a confidence label that follows a moving target, simulating real-time AI detection. Rendered in detection yellow (`#facc15` family) on a dark background — the industry convention (AV HUDs, security CV, ML demos); red reads "warning", green "success", blue "info" — none read "detection." ## How It Works -- Box position `(x, y)` and size `(w, h)` are derived from sine + drift across composition time -- 4 L-bracket corner markers (`
` per corner with two-sided borders) sit ON the box -- Optional label tag above the top-left corner showing class name + confidence percent +ONE `ease: "none"` driver tween advances a phase `p`; its `onUpdate` computes the TARGET's position from trig, then derives the box's position/size FROM the target — every frame, in that order. The box never gets its own position tween: if it trails the target it reads as a broken tracker, not a smart AI. Size jitters a few percent off-tempo (non-integer frequency multiple) to mimic continuous re-fitting, and the confidence label flickers inside [95, 99]. -All driven by GSAP timeline so HF seeks deterministically. - -## HTML +## Recipe ```html -
- -
-
{Brand}
-
{targetGlyph}
-
- - -
-
-
-
-
-
{targetGlyph} {LABEL} · {confidence}%
-
+ +
{targetGlyph}
+
+
+
+
+
+
{LABEL} · {confidence}%
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - background: radial-gradient(ellipse at center, {bgInner} 0%, {bgOuter} 70%); - font-family: {font}; - overflow: hidden; -} -.bg { - position: absolute; - inset: 0; - display: grid; - place-items: center; - gap: 60px; -} -.bg-content { - position: absolute; - top: 120px; - left: 50%; - transform: translateX(-50%); - font-size: 80px; - font-weight: 900; - color: {bgTextColor}; - letter-spacing: 12px; - text-transform: uppercase; -} -.bg-mascot { - position: absolute; - font-size: 240px; - line-height: 1; -} - .track-box { - position: absolute; - /* Position + size set by GSAP onUpdate */ + position: absolute; /* position + size written by the driver's onUpdate */ pointer-events: none; will-change: transform, width, height; } @@ -91,292 +38,102 @@ All driven by GSAP timeline so HF seeks deterministically. width: 48px; height: 48px; } +/* Each corner draws only its two outer borders — .tr/.bl/.br mirror this: */ .corner.tl { top: -8px; left: -8px; border-top: 6px solid {detectionYellow}; border-left: 6px solid {detectionYellow}; } -.corner.tr { - top: -8px; - right: -8px; - border-top: 6px solid {detectionYellow}; - border-right: 6px solid {detectionYellow}; -} -.corner.bl { - bottom: -8px; - left: -8px; - border-bottom: 6px solid {detectionYellow}; - border-left: 6px solid {detectionYellow}; -} -.corner.br { - bottom: -8px; - right: -8px; - border-bottom: 6px solid {detectionYellow}; - border-right: 6px solid {detectionYellow}; -} .label { position: absolute; top: -56px; left: -8px; - padding: 8px 16px; background: {detectionYellow}; - color: {labelTextColor}; - font-family: {monoFont}; - font-size: 24px; - font-weight: 800; - letter-spacing: 2px; - border-radius: 6px; + color: {labelTextColor}; /* near-black on yellow */ + font-family: {monoFont}; /* mono = machine readout */ white-space: nowrap; } ``` -## GSAP Timeline +```js +const box = document.getElementById("track-box"); +const mascot = document.getElementById("mascot"); +const label = document.getElementById("label"); +const C = { x: COMP_WIDTH / 2, y: COMP_HEIGHT / 2 }; -```html - - + }, + TRACK_START, +); ``` -## How to Choose Values - -- **ENTRY_SCALE** — start scale of the box before it pops in - - Range: 0.5-0.9 - - Effects: low end = stronger pop / more "snapping into focus"; high end = subtle reveal - - Constraints: must be < 1 (box scales UP into place) - - Reference: examples use 0.7 - -- **ENTRY_DUR** — duration of the fade-in + scale-up (seconds) - - Range: 0.3-0.8 s - - Effects: low end = snappy / authoritative lock-on; high end = soft / observational - - Constraints: should end before TRACK_START - - Reference: examples use 0.5 - -- **ENTRY_START** — when the entry tween begins (seconds, absolute on timeline) - - Range: 0-2 s typically - - Effects: late start = lets the viewer notice the target first before the AI "finds" it; early start = AI is already watching - - Constraints: ENTRY_START + ENTRY_DUR ≤ TRACK_START - - Reference: examples use 0.5 - -- **ENTRY_BOUNCE** — coefficient passed to `back.out(...)` on entry - - Range: 1.2-2.5 - - Effects: low end = subtle overshoot; high end = exaggerated snap (reads as "aggressive lock-on") - - Constraints: stick to `back.out` family — `elastic` reads as cartoonish, `power` reads as flat - - Reference: examples use 1.4 - -- **TRACK_START** — when continuous tracking begins (seconds) - - Range: ≥ ENTRY_START + ENTRY_DUR - - Effects: gap between entry and tracking = pause for emphasis; no gap = seamless lock + follow - - Constraints: TRACK_START + TRACK_DUR ≤ composition duration - - Reference: examples use 1.0 - -- **TRACK_DUR** — length of the tracking phase (seconds) - - Range: 2-8 s - - Effects: short = "quick scan"; long = "sustained observation" - - Constraints: must accommodate at least one full CYCLE to read as oscillation - - Reference: examples use 4.0 - -- **CYCLES** — number of full sine oscillations of the target across TRACK_DUR - - Range: 0.5-3 - - Effects: low = lazy drift; high = jittery / hyperactive target - - Constraints: CYCLES / TRACK_DUR sets effective Hz of drift; keep < ~0.6 Hz or motion blurs - - Reference: examples use 1.5 - -- **DRIFT_X / DRIFT_Y** — amplitude of target oscillation around screen center (px, 1920×1080 basis) - - Range: 40-200 px - - Effects: small = subtle hover; large = wide chase that pushes the box near the frame edge - - Constraints: SCREEN_CENTER ± DRIFT must keep mascot fully on screen given MASCOT_SIZE - - Reference: examples use 80 / 50 - -- **SIZE_BASE** — mean width/height of the bounding box (px) - - Range: 200-500 px - - Effects: small = "specimen tag"; large = "the box IS the subject" - - Constraints: must visibly enclose the target glyph at all confidence sizes - - Reference: examples use 320 - -- **SIZE_VAR** — half-amplitude of per-frame size jitter (px) - - Range: 5-10% of SIZE_BASE - - Effects: low end = stable / confident detector; high end = jittery / re-fitting detector. Outside this range reads as "broken" (too much) or "static UI" (none) - - Constraints: keep < 0.15 × SIZE_BASE to avoid breaking the L-bracket illusion - - Reference: examples use 30 (~9% of 320) - -- **SIZE_FREQ_MULT** — multiplier on tracking phase for size oscillation - - Range: 1.5-3 - - Effects: 1 = size pulses in lock-step with drift (reads mechanical); irrational ratio = organic re-fitting - - Constraints: avoid integer ratios; non-integer reads as continuous recomputation - - Reference: examples use 2.3 - -- **MASCOT_SIZE** — rendered width of the mascot element (px); used to center it on (mx, my) - - Range: matches CSS `font-size` of `.bg-mascot` - - Effects: must match the actual rendered size or mascot drifts out of the box - - Constraints: MASCOT_SIZE / 2 = offset applied to top/left - - Reference: examples use 240 (matches `font-size: 240px`) - -- **CONFIDENCE_MEAN** — center % shown on the label - - Range: 95-99 - - Effects: < 95 reads "uncertain"; 100 reads "fake-precise". 97 is the sweet spot for "confident AI" - - Constraints: keep CONFIDENCE_MEAN + CONFIDENCE_VAR ≤ 99 - - Reference: examples use 97 - -- **CONFIDENCE_VAR** — flicker half-range around CONFIDENCE_MEAN - - Range: 1-3 - - Effects: 0 = static (looks like a screenshot); >3 = unstable (looks broken) - - Constraints: CONFIDENCE_MEAN ± CONFIDENCE_VAR ⊂ [95, 99] - - Reference: examples use 2 - -- **CONFIDENCE_FREQ_MULT** — multiplier on tracking phase for label flicker - - Range: 3-6 - - Effects: low = synced with drift (mechanical); high = fast nervous flicker (reads "live inference") - - Constraints: keep above SIZE_FREQ_MULT so label flickers faster than the box breathes - - Reference: examples use 4 - -- **COMP_WIDTH / COMP_HEIGHT** — composition pixel dimensions (used to derive SCREEN_CENTER) - - Range: dictated by the HF composition (`data-width` / `data-height`) - - Effects: not a creative choice — match the parent composition - - Constraints: SCREEN_CENTER = (COMP_WIDTH/2, COMP_HEIGHT/2) - - Reference: examples use 1920 × 1080 - -- **{detectionYellow}** — corner-marker + label background color - - This is a **discrete convention**, not a tunable range. AI detection overlays are yellow on dark backgrounds across the industry (autonomous-vehicle HUDs, security CV, ML demos). Red reads as "warning", green as "success", blue as "info" — none read as "detection." - - Recommended: a saturated warm yellow (`#facc15` / `#FCD34D` family) on a dark navy or near-black background. Substituting any other hue loses genre legibility. - - Reference: examples use `#facc15` (Tailwind `yellow-400`) - -- **{bgInner} / {bgOuter}** — radial-gradient background stops - - Should be dark and low-chroma so the yellow markers pop - - Constraints: choose colors with sufficient contrast against {detectionYellow} (the corners and label must remain readable) - - Reference: examples use `#161a3a` (inner) → `#0b0d1f` (outer) - -- **{labelTextColor}** — text color inside the yellow label tag - - Constraints: must contrast against {detectionYellow}; typically the same near-black as {bgOuter} - - Reference: examples use `#0b0d1f` - -- **{font} / {monoFont}** — scene text font and label font - - {font}: sans-serif body font for {Brand} backdrop - - {monoFont}: monospaced font for the confidence label (mono reinforces "machine readout" affordance) - - Reference: examples use `"Inter", sans-serif` and `"JetBrains Mono", monospace` - ## Variations -### Multi-object detection +- **Multi-object**: one driver per box/target pair, phases offset by `π / N` so they don't tick synchronously. +- **Lost-then-reacquired**: fade the box to ~0.2–0.4 opacity, then re-snap with a harder `back.out(1.8–2.5)` and flash a "REACQUIRED · 99%" label via `tl.set`. +- **Tracking-then-zoom**: hand off to [viewport-change.md](viewport-change.md) — "the AI found something, now show it." -Multiple boxes at different phases (each tracking its own mascot). Each is its own onUpdate-driven set; offset their phase by `Math.PI / N` so they don't tick synchronously. +## Values -### Lost-then-reacquired +| token | range | notes | +| -------------------- | ------------------ | ------------------------------------------------------------------------ | +| ENTRY_SCALE | 0.5–0.9 | < 1 — the box snaps UP into focus | +| ENTRY_DUR / \_BOUNCE | 0.3–0.8s / 1.2–2.5 | `back.out` only — elastic reads cartoonish, power reads flat | +| TRACK_START | ≥ entry end | a gap = pause for emphasis; none = seamless lock + follow | +| TRACK_DUR | 2–8s | ≥ one full cycle or the drift never reads as oscillation | +| CYCLES | 0.5–3 | keep effective rate < ~0.6 Hz or the motion blurs | +| DRIFT_X / DRIFT_Y | 40–200px | center ± drift must keep the target fully on screen | +| SIZE_BASE | 200–500px | must visibly enclose the target at all jitter sizes | +| SIZE_VAR | 5–10% of SIZE_BASE | more reads broken, none reads like a screenshot; keep < 0.15× | +| SIZE_FREQ_MULT | 1.5–3, non-integer | integer ratios pulse in lock-step with drift = mechanical | +| CONFIDENCE_MEAN/VAR | 95–99 / 1–3 | mean ± var ⊂ [95, 99]; < 95 "uncertain", 100 "fake-precise"; 97 is sweet | +| CONFIDENCE_FREQ_MULT | 3–6 | > SIZE_FREQ_MULT — label flickers faster than the box breathes | +| MASCOT_SIZE | = rendered size | mismatch drifts the target out of the box | -The box fades to {LOST_OPACITY} (~{LOST_DUR}) then re-snaps to a new position with a "REACQUIRED" label flash: - -```js -tl.to(box, { opacity: LOST_OPACITY, duration: LOST_DUR }, LOST_START); -tl.to( - box, - { opacity: 1.0, duration: REACQUIRE_DUR, ease: `back.out(${REACQUIRE_BOUNCE})` }, - REACQUIRE_START, -); -tl.to(label, { textContent: "REACQUIRED · 99%", duration: 0 }, REACQUIRE_START); -``` - -(LOST_OPACITY ≈ 0.2-0.4; REACQUIRE_BOUNCE ≈ 1.8-2.5 for a snappier re-lock than the initial entry.) - -### Tracking-then-zoom - -After tracking, the camera (via [viewport-change](viewport-change.md)) zooms into the tracked box. Combined effect: "the AI found something, now show it." - -## Key Principles - -- **Yellow on dark background is the detection convention** — see {detectionYellow} entry. Other colors lose the genre signal. -- **Box ALWAYS contains the target** — recompute box position EVERY frame from target position; never trail behind. If the box lags, it reads as "broken tracker," not "smart AI." -- **Subtle size variation (~5-10% of SIZE_BASE)** — too much and the tracker looks confused; just right reads as "real-time recomputation." -- **Corner markers, not full borders** — L-brackets are the genre signature. Full border looks like a generic UI box. -- **Confidence label flickers in a tight range (CONFIDENCE_MEAN ± CONFIDENCE_VAR inside [95, 99])** — outside that range reads as "uncertain"; ≥100 reads as "fake-precise." -- **No CSS animation for the tracking — use timeline onUpdate** — HF seek-by-frame doesn't sync with CSS animation. +Tokens: `{detectionYellow}` `#facc15` family; `{bgInner}/{bgOuter}` dark low-chroma radial so the yellow pops; `{labelTextColor}` near-black; `{monoFont}` for the label. ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS animation on `.track-box` or `.corner`** — must be timeline-driven -- **`will-change: transform, width, height`** on `.track-box` -- **`pointer-events: none`** on `.track-box` — decorative overlay -- **Box position recomputed per-frame from target** — never tween box position separately from target +- **❗ Box recomputed per-frame FROM the target** — one driver computes the target position, then the box derives from it in the same `onUpdate`. Never tween the box's position separately. +- **Corner L-brackets, not a full border** — the genre signature; a full border reads as a generic UI box. +- **Yellow-on-dark** — substituting another hue loses genre legibility. +- **Confidence flickers in a tight band inside [95, 99]**, in a mono font. +- **`pointer-events: none`** on the box — it's a decorative overlay. -## Combinations +## See also -- [viewport-change.md](viewport-change.md) — zoom into the tracked box after detection phase -- [multi-phase-camera.md](multi-phase-camera.md) — wide shot during tracking, push-in on lock -- [sine-wave-loop.md](sine-wave-loop.md) — the mascot itself idle-breathes inside the box - -## Pairs with HF skills - -- `/hyperframes-animation` — onUpdate writing multi-element positions -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`viewport-change` (zoom into the detection) · `multi-phase-camera` (wide during tracking, push-in on lock) · `sine-wave-loop` (the target idle-breathes inside the box). diff --git a/skills/hyperframes-animation/rules/ambient-glow-bloom.md b/skills/hyperframes-animation/rules/ambient-glow-bloom.md index 6423b4c18..e1807bdd9 100644 --- a/skills/hyperframes-animation/rules/ambient-glow-bloom.md +++ b/skills/hyperframes-animation/rules/ambient-glow-bloom.md @@ -7,299 +7,127 @@ metadata: # Ambient Glow Bloom -A soft radial glow that **blooms in behind a hero element** (card, logo, metric) and holds, giving it presence. Unlike `press-release-spring`'s click-triggered burst or `asr-keyword-glow`'s word-timed envelope, this glow is **un-triggered** — it simply blooms on the hero's settle and stays lit. Two forms: a **hero bloom** that swells behind a settling element and then breathes, and a **traveling glow sweep** that translates a soft highlight across a surface exactly once. Both are finite, deterministic, and seek-safe. +A soft radial glow that **blooms in behind a hero element** (card, logo, metric) and holds, giving it presence. Unlike `press-release-spring`'s click-triggered burst or `asr-keyword-glow`'s word-timed envelope, this glow is **un-triggered** — it blooms on the hero's settle and stays lit. Two forms: a **hero bloom** that swells behind a settling element then breathes, and a **traveling sweep** that translates a soft highlight across a surface exactly once. ## How It Works -A radial-gradient layer sits **behind** the hero (`z-index` below it), starting at `opacity: 0`. Over the bloom-in window it ramps `opacity: 0 → peak` with a gentle `scale` swell (the halo "inflates" into place), timed to land on the hero's settle so the two read as one beat. +A radial-gradient layer sits **behind** the hero (glow `z-index: 1`, hero `z-index: 2` — a glow in front occludes it), starting at `opacity: 0`. Over the bloom-in window it ramps `opacity: 0 → peak` with a gentle `scale` swell, timed so `BLOOM_START + BLOOM_DUR` lands on the hero's settle — glow and hero resolve as ONE beat ("powering on"), never glow-then-card. After bloom-in: -Two forms diverge after bloom-in: +1. **Hero bloom** — a **bounded idle breathe** during the hold: a finite `ease: "none"` tween advances a `phase` proxy and `onUpdate` nudges opacity + scale a hair around peak (never a `yoyo` loop). `sin(0) = 0` → the breathe starts exactly at the bloom's resting state. +2. **Traveling sweep** — a narrow highlight band at one edge translates **once** across to the other (`x` off-surface to off-surface), clipped to the surface (`overflow: hidden`). One pass, no return — a repeating sweep reads as a loading shimmer, not a reveal accent (the shimmer-sweep variation below is the sanctioned exception). -1. **Hero bloom** — once lit, the glow does a **bounded idle breathe** during the hold. Drive it with an `onUpdate` reading `tl.time()` (NOT a `repeat: -1` yoyo): a `Math.sin` of elapsed time nudges `opacity` and `scale` a hair around their peak. At `sin(0) = 0` the breathe starts exactly at the bloom's resting state — no jump. -2. **Traveling sweep** — a narrow highlight gradient at one edge of the surface translates **once** across to the other edge (`x` from off-surface to off-surface), a single finite pass. No loop, no return. The sweep layer is clipped to the surface so the highlight only reads where it overlaps. +Peak opacity stays restrained (**≤ 0.45 hard ceiling**) so the glow gives presence without washing the frame; the glow color is **darker + more saturated** than the element it backs (a same-hue, same-lightness glow disappears into the surface). -Peak opacity stays restrained (≤ ~0.45) so the glow gives presence without washing the frame; the glow color is darker / more saturated than the element it backs. - -## HTML +## Recipe ```html -
-
- -
-
{HeroLabel}
-
+ +
+
+ +
{HeroLabel}
+
+ ``` -For the traveling-sweep form, the sweep layer is clipped to the surface it crosses: - -```html -
- -
-
-``` - -## CSS - -```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; -} -.bloom-stage { - position: relative; - display: grid; - place-items: center; -} -.hero-card { - position: relative; - z-index: 2; - width: HERO_WIDTH; - height: HERO_HEIGHT; - display: grid; - place-items: center; - background: {heroBg}; - border-radius: HERO_RADIUS; - font-family: {font}; - font-weight: 900; - font-size: HERO_FONT_SIZE; - color: {heroTextColor}; -} -.bloom-glow { - /* Radial halo behind the hero — extends past it via negative inset */ - position: absolute; - z-index: 1; - inset: GLOW_INSET; - background: {glowGradient}; - opacity: 0; - transform: scale(GLOW_START_SCALE); - transform-origin: 50% 50%; - pointer-events: none; - /* will-change because opacity + scale both animate during bloom AND breathe */ - will-change: transform, opacity; -} - -/* Traveling-sweep form */ -.surface { - position: relative; - overflow: hidden; /* clips the sweep to the surface footprint */ - border-radius: SURFACE_RADIUS; -} -.sweep { - position: absolute; - top: 0; - bottom: 0; - /* A narrow soft band, wider than it needs to be so the falloff is gentle */ - width: SWEEP_WIDTH; - /* Diagonal highlight: angle the gradient so the sweep reads as raked light */ - background: {sweepGradient}; - opacity: 0; - pointer-events: none; - will-change: transform, opacity; -} -``` - -## GSAP Timeline - -```html - - +// ── Form B: TRAVELING SWEEP ── one finite pass, constant glide. +tl.fromTo( + "#sweep", + { x: SWEEP_START_X, opacity: 0 }, + { x: SWEEP_END_X, opacity: SWEEP_PEAK_OPACITY, duration: SWEEP_DUR, ease: "none" }, + SWEEP_START, +); +tl.to("#sweep", { opacity: 0, duration: SWEEP_FADE_DUR, ease: "power1.in" }, SWEEP_FADE_START); ``` ## Variations -### Bloom-and-hold (no breathe) +- **Bloom-and-hold** — for scenes <3s or a hero with its own idle, skip the breathe: the single `fromTo` is the whole recipe. +- **Pulse-on-arrival** — bloom slightly PAST peak (`GLOW_OVERSHOOT_OPACITY`, `scale: 1.06`), then a second adjacent tween eases down to a steady hold — one breath punctuating the landing, no ongoing loop. +- **Multi-hero relay** — stagger per-glow `BLOOM_START` by ~0.15–0.3s across a row; shrink `OPACITY_AMP` / `SCALE_AMP` per the `/√N` rule below. +- **Diagonal raked sweep** — angle `{sweepGradient}` (~105°) across a wordmark: the classic one-pass logo sheen. Narrower `SWEEP_WIDTH`, higher `SWEEP_PEAK_OPACITY`. -For very short scenes (< 3s) or when the hero already has its own idle, skip Phase 2 entirely — bloom to peak and hold flat. The single `fromTo` is the whole recipe; the glow is just lit presence. +### Shimmer sweep (text-clipped status-phrase working-state) -### Pulse-on-arrival (one swell, then settle to a lower hold) - -Bloom slightly **past** peak, then ease back down to a steady hold level — a single breath that punctuates the hero's landing without an ongoing loop. Two adjacent tweens (state continuity, same as `press-release-spring`): +The sweep re-aimed **inside type**: a soft highlight gradient clipped into a status phrase ("Thinking…", "Analyzing dataset…") via `background-clip: text` travels left→right through the letterforms — the grey-on-grey shimmer that says _still working_. Unlike every other form here it legitimately **repeats while the status is live**: the repetition is diegetic working-state, not idle wobble (same defense as a blinking caret — the motion performs status). Two things keep it honest: it is **bounded** (one finite tween whose pass count is computed from the status window, never `repeat: -1`), and it is **killed at resolve** — the moment the status completes, the shimmer stops dead; a shimmer surviving into the answer beat turns a working indicator into decoration. ```js -tl.fromTo( - glow, - { opacity: 0, scale: GLOW_START_SCALE }, - { opacity: GLOW_OVERSHOOT_OPACITY, scale: 1.06, duration: BLOOM_DUR, ease: "power2.out" }, - BLOOM_START, -); +// Status shimmer — N passes as ONE bounded tween. Killed at resolve. +const status = document.getElementById("status-phrase"); +// CSS on #status-phrase: background: {shimmerGradient}; background-size: 300% 100%; +// -webkit-background-clip: text; background-clip: text; color: transparent; +const shimmer = { p: 0 }; +const PASSES = Math.round(STATUS_DUR / PASS_PERIOD); // whole passes, computed up front tl.to( - glow, - { opacity: GLOW_HOLD_OPACITY, scale: 1, duration: SETTLE_DUR, ease: "power2.inOut" }, - BLOOM_START + BLOOM_DUR, + shimmer, + { + p: PASSES, + duration: STATUS_DUR, + ease: "none", + onUpdate: () => { + const t = shimmer.p % 1; // 0→1 within each pass; percent axis inverted → left→right travel + status.style.backgroundPosition = `${(1 - t) * 100}% 50%`; + }, + }, + STATUS_START, ); +tl.set(status, { backgroundPosition: "100% 50%" }, STATUS_START + STATUS_DUR); // resolve: dead. ``` -### Multi-hero relay (staggered blooms behind a row of cards) +Keep it a whisper: `{shimmerGradient}` is the status text's own grey with one slightly-lighter band (highlight stop a step above the base, nothing near white); `background-size` ~300% keeps the band narrow in the glyphs; `PASS_PERIOD` 1.2–1.8s — slower reads as a sheen accent, faster as a spinner. Whole-number `PASSES` lands the band at its start position exactly at the kill frame, so the `tl.set` is visually a no-op. This is the working-state cousin of `gradient-text-sweep`: reach **here** when the sweep _means_ "in progress," **there** when the gradient is the typographic treatment itself. -Bloom each card's glow on a stagger so presence sweeps across the row. Per-glow `BLOOM_START` offset by `STAGGER` (~0.15-0.3s); shrink `OPACITY_AMP` / `SCALE_AMP` per the concurrent-elements rule below so N breathing halos don't compound into a shimmer. +## Values -### Diagonal raked sweep (wordmark sheen) - -Angle `{sweepGradient}` (e.g. a 105° linear gradient) and let the band travel left→right across a wordmark or logo lockup. Reads as light raking across a surface — the classic one-pass logo sheen. Same single-pass timeline; just a narrower `SWEEP_WIDTH` and a higher `SWEEP_PEAK_OPACITY` since it's a tight highlight on a small target. - -## How to Choose Values - -### Glow geometry - -- **GLOW_INSET** — negative inset so the radial halo extends past the hero edges. - - Range: `-200` to `-450` px on a 1920×1080 canvas; larger halo for a bigger hero - - Effects: too small and the glow is a tight rim, not ambient presence -- **GLOW_START_SCALE** — scale at the start of bloom-in (the halo "inflates" to 1). - - Range: 0.80 (clear inflation) → 0.92 (subtle) → 1.0 (no swell, opacity-only bloom) - - Constraints: keep ≤ 1.0 — the swell should grow into place, not shrink - -### Bloom-in dynamics - -- **BLOOM_DUR** — bloom-in duration. - - Range: 0.6-1.4s; longer for a hero that's still settling so they land together - - Effects: shorter → the glow "snaps on"; longer → it suffuses in (the ambient feel) -- **BLOOM_START** — when the bloom begins. - - Constraints: align so `BLOOM_START + BLOOM_DUR` ≈ the hero's settle frame, so glow and hero resolve as one beat — not glow-then-card or card-then-glow -- **GLOW_PEAK_OPACITY** — peak halo opacity. - - Range: 0.15 (subtle) → 0.30 (default) → 0.45 (dramatic) - - **Constraints: ≤ 0.45** — higher washes the whole frame and the hero loses contrast against its own glow - -### Idle breathe (hero-bloom form) - -- **BREATHE_DUR** — breathe tween length. - - Constraints: equals `TOTAL_DURATION − (BLOOM_START + BLOOM_DUR)` to fill the hold with motion -- **BREATHE_CYCLES** — number of full breaths across `BREATHE_DUR`. - - Range: `BREATHE_DUR / 4s ≤ CYCLES ≤ BREATHE_DUR / 2.5s` (a 2.5-4s breath period reads as a slow ambient pulse — glow breathing wants to be slower than element breathing) -- **OPACITY_AMP** — sine amplitude on opacity around the peak. - - **Default: 0.02-0.05** (barely-perceptible pulse — the right answer for most scenes) - - Constraints: `GLOW_PEAK_OPACITY + OPACITY_AMP` must stay ≤ 0.45 -- **SCALE_AMP** — sine amplitude on the halo scale. - - **Default: 0.01-0.03** (the halo "breathes" without visibly resizing) - - Push higher only when the glow is the sole motion in a short isolated scene - -### Traveling sweep - -- **SWEEP_WIDTH** — width of the soft highlight band. - - Range: 15-35% of the surface width (a wide soft band) for a grid sheen; 8-15% for a tight wordmark sheen -- **SWEEP_START_X / SWEEP_END_X** — travel endpoints, both fully off-surface. - - Constraints: start ≈ `-(SWEEP_WIDTH + edge)`, end ≈ `surfaceWidth + edge` — the band must enter from fully off one edge and exit fully off the other, so there's no visible spawn/despawn mid-surface -- **SWEEP_DUR** — single-pass travel duration. - - Range: 0.8-1.6s; one deliberate pass, slow enough to read as light, fast enough not to dominate -- **SWEEP_PEAK_OPACITY** — highlight opacity. - - Range: 0.10 (whisper sheen) → 0.25 (default) → 0.40 (bright rake) - - Constraints: ≤ ~0.45 (same wash limit); tighter sweeps tolerate the high end -- **SWEEP_START / SWEEP_FADE_START / SWEEP_FADE_DUR** — when the pass runs and tails out. - - Constraints: `SWEEP_FADE_START + SWEEP_FADE_DUR ≈ SWEEP_START + SWEEP_DUR` so opacity reaches 0 exactly as the band clears the far edge - -### Tokens - -- **{glowGradient}** — radial-gradient, saturated near center fading to transparent. Color should be **darker + more saturated** than `{heroBg}` — a same-color glow looks washed out (same rule as `press-release-spring`'s burst). -- **{sweepGradient}** — a soft band: `transparent → highlight → transparent`. For a sheen, a near-white or brand-tint highlight at low alpha; angle it (e.g. `linear-gradient(105deg, …)`) for a raked look. -- **{heroBg} / {heroTextColor}** — the hero surface the glow backs; high contrast so the lit hero still reads against its halo. - -## Key Principles - -- **Un-triggered by design** — this glow does NOT wait on a click (`press-release-spring`) or a word timestamp (`asr-keyword-glow`). It blooms on the hero's settle as ambient presence. If you need a triggered burst, reach for one of those rules instead. -- **Glow behind, hero in front** — glow `z-index: 1`, hero `z-index: 2`. A glow in front occludes the hero at peak opacity. -- **Glow color darker + more saturated than the element** — bright hero → dark, saturated halo. A same-hue, same-lightness glow disappears into the surface. -- **Land glow and hero as ONE beat** — time `BLOOM_START + BLOOM_DUR` to the hero's settle. A glow that arrives before or after the card reads as two separate events; arriving together reads as the card "powering on." -- **Restrained peak — default to the LOW end.** `GLOW_PEAK_OPACITY` 0.15-0.30 for most scenes; 0.45 is a hard ceiling. A glow you consciously notice is too strong — it should register as the hero having weight, not as a visible light source. -- **Breathe is BOUNDED, never a loop** — the idle pulse is a finite `onUpdate` tween reading `tl.time()` (via the `phase` proxy), not `repeat: -1` / `yoyo`. `sin(0) = 0` means it starts at the bloom's resting state with no jump. (Same reason as `sine-wave-loop`: an infinite/CSS loop desyncs from the HF seek clock.) -- **Sweep is ONE pass** — the traveling highlight enters off one edge and exits off the other a single time. No return trip, no loop. A repeating sweep reads as a loading shimmer, not a one-time reveal accent. -- **Concurrent halos compound** — N breathing glows in a row add up. Per-glow `OPACITY_AMP` and `SCALE_AMP` ≤ default `/ √N`, and stagger the breathe period (2.6s / 2.9s / 3.3s) so they don't pulse in lockstep. (Same `/√N` discipline as `sine-wave-loop`'s concurrent-elements rule.) -- **Don't combine `boxShadow` glow on the hero with this halo layer** — they compete in the layout pipeline and the result reads muddy. Put the glow on the dedicated `.bloom-glow` layer, not as a shadow on the hero. +| token | range / default | notes | +| ----------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- | +| GLOW_PEAK_OPACITY | 0.15 (subtle) → 0.30 (default) → **0.45 hard ceiling** | higher washes the frame; a glow you consciously notice is too strong | +| GLOW_INSET | −200 to −450px (1920×1080) | negative so the halo extends past the hero; too small reads as a tight rim | +| GLOW_START_SCALE | 0.80–1.0 | ≤1.0 — grow into place, never shrink | +| BLOOM_DUR / BLOOM_START | 0.6–1.4s | `BLOOM_START + BLOOM_DUR` ≈ the hero's settle frame | +| OPACITY_AMP / SCALE_AMP | 0.02–0.05 / 0.01–0.03 default | `PEAK + OPACITY_AMP ≤ 0.45`; push only when the glow is the sole motion | +| BREATHE_CYCLES | period 2.5–4s per breath | glow breathes slower than element breathing | +| SWEEP_WIDTH | 15–35% of surface (grid) / 8–15% (wordmark) | | +| SWEEP_DUR | 0.8–1.6s | one deliberate pass — slow enough to read as light | +| SWEEP_PEAK_OPACITY | 0.10 → 0.25 (default) → 0.40 | same ≤ ~0.45 wash limit; tight sweeps tolerate the high end | +| SWEEP_START_X / END_X | fully off-surface both ends | no visible spawn/despawn mid-surface; fade reaches 0 as the band clears | +| PASS_PERIOD (shimmer) | 1.2–1.8s | with whole-number PASSES | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition`** on the glow / sweep — interpolates independently of HF seek and flickers -- **No `repeat` / `yoyo` / `repeat: -1`** — the breathe is a bounded finite tween; the sweep is one pass -- **No `Math.random` / `Date.now`** — the breathe phase is deterministic (`phase.p` over a fixed duration) -- **GSAP transform aliases only**: `x`, `y`, `scale`, `rotation` — plus `opacity` / `filter`. Never tween `width` / `height` / `left` / `top` (the halo swell is `scale`, the sweep travel is `x`). -- **`will-change: transform, opacity`** on the glow — it animates both during bloom-in and the breathe -- **Glow peak `opacity ≤ 0.45`** — higher washes the composition -- **Sweep endpoints fully off-surface** — band must enter and exit beyond the clipped edges so it never spawns/despawns mid-frame +- **Glow peak opacity ≤ 0.45** — including breathe amplitude; default to the LOW end (0.15–0.30). +- **Glow behind, hero in front**; glow color darker + more saturated than the hero surface. +- **Land glow and hero as one beat** — before or after reads as two separate events. +- **Breathe is bounded, sweep is one pass** — the only sanctioned repetition is the shimmer sweep, bounded and killed at resolve. +- **Concurrent halos compound** — per-glow amps ≤ default `/√N`, stagger breathe periods (2.6s / 2.9s / 3.3s) so they don't pulse in lockstep. +- **Don't combine a `boxShadow` glow on the hero with this halo layer** — they compete and read muddy; the glow lives on the dedicated layer. -## Combinations +## See also -- [sine-wave-loop.md](sine-wave-loop.md) — pair the hero-bloom form with a sine breathe on the hero element itself; the glow breathes on opacity, the hero breathes on scale/y, slightly out of phase for a layered "alive" hold -- [press-release-spring.md](press-release-spring.md) — distinct sibling: that rule's `bg-glow` is **click-triggered**, this one is un-triggered. Don't run both behind the same element -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — bloom the accent halo behind the hero stat card on the count-up's settle (the `dataviz-countup` blueprint's "soft accent glow blooms behind the hero metric" beat) -- [stat-bars-and-fills.md](stat-bars-and-fills.md) — glow blooms behind the hero metric + its paired graphic as they land together -- [center-outward-expansion.md](center-outward-expansion.md) — run the traveling-sweep across the assembled layout once it resolves (the `grid-card-assemble` blueprint's "traveling-glow sweep across the assembled grid") - -## Pairs with HF skills - -- `/hyperframes-animation` — `onUpdate` writing opacity/transform + bounded sine breathe -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`sine-wave-loop` (hero breathes on scale/y while the glow breathes on opacity, out of phase) · `press-release-spring` (the click-triggered sibling — never both behind one element) · `counting-dynamic-scale` / `stat-bars-and-fills` (bloom behind a landing stat) · `center-outward-expansion` (sweep across the assembled grid) · `gradient-text-sweep` (the design-beat gradient counterpart). diff --git a/skills/hyperframes-animation/rules/anchored-layout-expand.md b/skills/hyperframes-animation/rules/anchored-layout-expand.md new file mode 100644 index 000000000..040e9766d --- /dev/null +++ b/skills/hyperframes-animation/rules/anchored-layout-expand.md @@ -0,0 +1,148 @@ +--- +name: anchored-layout-expand +description: Edge-pinned container grows (or collapses) along ONE axis and in-flow content reflows with it — a pill springs open downward into a dropdown, a panel grows a sub-task stack, an input card stretches as typed text wraps, a pane expands over a neighbor. Transform-only (mask + slide, or proxy-driven scaleY + counter-scale) because width/height tweens are forbidden; the push on subsequent content is a matched translate on the same tween. +metadata: + tags: expand, collapse, anchored, dropdown, menu, accordion, panel, reflow, push, mask, counter-scale, layout +--- + +# Anchored Layout Expand + +> The law: **author the layout at its final (expanded) state in CSS, then fake the collapsed state with transforms.** The container never changes size — the _visible_ region does — and everything downstream rides a matched translate. The browser computes layout ONCE; every intermediate frame is pure transform. + +THE one-axis growth primitive: a container pinned at one edge appears to grow along a single axis, and the in-flow content after it moves in perfect contact with the traveling edge — dropdown, sub-task stack, growing composer card, pane widening over a neighbor. Growth and push are ONE motion: if the panel's bottom edge and the pushed content ever separate or overlap, the illusion dies. + +Distinct from [card-morph-anchor.md](card-morph-anchor.md) (a free-floating two-shot morph with no neighbors to push — this rule's container is a live layout participant), [spring-pop-entrance.md](spring-pop-entrance.md) (arrival at a point, no edge travel or reflow), and [reactive-displacement.md](reactive-displacement.md) (displacement by a colliding intruder; here content moves because the container's edge reached it — layout causality, not collision). + +## How It Works + +1. **Mask** — a wrapper at the final body height (`BODY_H`), `overflow: hidden`. Never tweened. +2. **Sheet** — the panel surface + content inside the mask, starting at `y: -BODY_H` (tucked above the mask window, behind the pinned header). +3. **Below** — ONE wrapper holding everything after the container, also starting at `y: -BODY_H`. +4. **Grow** — ONE `fromTo` drives sheet AND below from `y: -BODY_H → 0`. Shared tween ⇒ the descending bottom edge and the pushed content stay in exact contact by construction. Collapse = the same pair tweened back. + +When the surface must visibly **stretch in place** (rows revealed top-first, or a pane growing sideways), use the proxy counter-scale variant below instead. + +## Recipe + +```html + +
+
+
{headerLabel}
+
+
+
{rowA}
+
{rowB}
+
+
+
+ +
{followingContent}
+
+``` + +```css +/* Layout is the EXPANDED end state — no collapsed geometry exists in CSS. */ +.expander-head { + position: relative; + z-index: 2; /* the sheet slides out from UNDER the header */ +} +.expand-mask { + height: BODY_H; /* authored final height — NEVER tweened */ + overflow: hidden; +} +.expand-sheet { + height: BODY_H; + border-radius: 0 0 SHEET_RADIUS SHEET_RADIUS; /* bottom-only — header + sheet read as one grown card */ + will-change: transform; /* + on .below */ +} +``` + +```js +// BODY_H must equal the mask's CSS height exactly — measure once at build. +// (Montage caveat: per the contract, in a multi-scene master use an authored +// CSS-matched constant instead — later clips may not be laid out yet.) +const BODY_H = document.querySelector("#expand-mask").offsetHeight; + +// The grow: ONE tween, BOTH sides of the seam. +tl.fromTo( + ["#expand-sheet", "#below"], + { y: -BODY_H }, + { y: 0, duration: GROW_DUR, ease: GROW_EASE }, + GROW_AT, +); + +// Garnish: rows already ride the sheet; the fade stagger makes them read as "options arriving". +tl.fromTo( + ".expand-row", + { opacity: 0 }, + { opacity: 1, duration: ROW_FADE_DUR, stagger: ROW_STAGGER, ease: "power2.out" }, + GROW_AT + GROW_DUR * 0.25, +); + +// Collapse — same machinery back; faster (closing is a snap decision). +tl.fromTo( + ["#expand-sheet", "#below"], + { y: 0 }, + { y: -BODY_H, duration: COLLAPSE_DUR, ease: "power3.in", immediateRender: false }, + COLLAPSE_AT, +); +``` + +## Variations + +- **Proxy counter-scale — surface stretches in place** (rows revealed top-first holding their screen positions; the "payload card expands from the tool-call line"). Drive mask `scaleY` and the sheet's exact inverse from ONE proxy — two independent tweens are wrong: eased midpoints of `s` and `1/s` are not inverses and the content squashes mid-grow. Net content scale is `s × 1/s = 1` every frame; seek-safe because everything derives from the one interpolated proxy. + + ```js + const grow = { h: COLLAPSED_H }; // 0 for fully collapsed + tl.fromTo( + grow, + { h: COLLAPSED_H }, + { + h: BODY_H, + duration: GROW_DUR, + ease: GROW_EASE, + onUpdate: () => { + const s = Math.max(grow.h / BODY_H, 0.0001); // clamp: no divide-by-zero + gsap.set("#expand-mask", { scaleY: s, transformOrigin: "50% 0%" }); + gsap.set("#expand-sheet", { scaleY: 1 / s, transformOrigin: "50% 0%" }); + gsap.set("#below", { y: grow.h - BODY_H }); + }, + }, + GROW_AT, + ); + ``` + +- **One-axis pane expand (X)**: same machinery rotated 90° — pin the left edge, sheet from `x: -PANE_W` (or proxy `scaleX` + counter-scale, origin `0% 50%`). Decide the neighbor's fate explicitly: **overlap** (pane paints over it, no neighbor tween) or **push** (neighbor rides the same tween). Never both. +- **Typed-wrap growth** — the composer card gets taller as typed text wraps. Quantize: one short step per wrap boundary, each moving the pair by one `LINE_H`; wrap times come from the deterministic typing schedule ([discrete-text-sequence.md](discrete-text-sequence.md)), never measured at render time. Two battle-tested traps: + - **Composer cards have no pinned header** — a composer grows from its TOP edge (the send-button footer stays put), so a plain y-step clips the card's top out of the mask. Combine the proxy counter-scale with the wrap quantization (step the proxy by `LINE_H` at each wrap time) and split the surface into a **sheet** (carries the top radius) + **footer** (carries the bottom radius) so the growth seam stays invisible. + - **Wrap TIME vs wrap POSITION are two different authorities** — the typing schedule decides _when_ a wrap fires, the browser's line-breaking decides _where_ text actually wraps, and with proportional fonts they silently disagree. Author an explicit `\n` in the typed string (with `white-space: pre-wrap`) at the chosen split point so both derive from the same authored fact. +- **Springy open** (rare, explicitly-playful): `back.out(1.2)` — the edge overshoots a few px; the pushed content bounces with the panel (correct — they're in contact). Default stays `power3.out`. +- **Row grows a sub-task stack**: the row is the pinned header, the stack is the sheet, every later row lives in `#below`; chain several scopes for progressive disclosure. +- **FLIP hand-off**: if the container also TRAVELS to a new layout slot while resizing (prompt promoted to heading, card docking into a sidebar), that's a FLIP problem — `/hyperframes-keyframes` (FLIP recipes). This rule stays the in-place one-axis specialist. + +## Values + +| token | range | notes | +| ------------------------ | --------------------------- | --------------------------------------------------------------------- | +| BODY_H | measured / authored | drift from the CSS height = visible gap or overlap at full open | +| GROW_AT | trigger beat + 0–0.1s | growth needs a cause (click / wrap / status beat) or it reads haunted | +| GROW_DUR | 0.35–0.6s | below ~0.3s the pushed content appears to teleport | +| GROW_EASE | `power3.out` default | `back.out(1.1–1.3)` only for the playful register | +| ROW_STAGGER / \_FADE_DUR | 0.04–0.08s / 0.2–0.3s | start rows ~25% into the grow so none flash inside a closed panel | +| COLLAPSE_DUR | 0.2–0.35s, `power3.in` | faster than open | +| STEP_DUR / LINE_H | 0.12–0.2s / CSS line-height | typed-wrap variant; WRAP_TIMES from the typing script | + +## Critical Constraints + +- **NEVER tween `width` / `height` / `top` / `left` / `margin` / `padding`** — the mask's height is a CSS constant; only its children transform. Tweening the mask IS the forbidden move this rule replaces. +- **`data-layout-allow-overflow` on the mask** — the collapsed phase parks the sheet outside the mask's box by construction, which trips the `hyperframes check` layout gate (`container_overflow`). The flag is the sanctioned waiver: this overflow is the technique working as designed, not a bug. +- **Sheet + below share one tween (or one proxy)** — matched-but-separate tweens on the two sides of the contact edge are the classic seam bug. +- **Everything downstream rides `#below`** — content outside the wrapper is overlapped at t=0 and orphaned during the grow. +- **`overflow: hidden` on the mask** — without it the tucked sheet is visible above the header at t=0. +- **Counter-scale needs a proxy**, clamped `s ≥ 0.0001` (a fully-collapsed body divides by zero). +- **Deterministic sizes** — `BODY_H`, `LINE_H`, `WRAP_TIMES` are build-time constants or one-time measurements, never per-frame layout reads. + +## See also + +`cursor-click-ripple` (the igniting click) · `spring-pop-entrance` (richer per-row arrivals) · `discrete-text-sequence` (the typing that drives stepped growth) · `scale-swap-transition` (the grown menu's exit) · `/hyperframes-keyframes` FLIP (grow + travel). diff --git a/skills/hyperframes-animation/rules/asr-keyword-glow.md b/skills/hyperframes-animation/rules/asr-keyword-glow.md index aca91047e..8a0eb9019 100644 --- a/skills/hyperframes-animation/rules/asr-keyword-glow.md +++ b/skills/hyperframes-animation/rules/asr-keyword-glow.md @@ -7,183 +7,84 @@ metadata: # ASR Keyword Glow -Words in a phrase visually activate (glow blur + scale) when "spoken," following an attack-sustain-release (ASR-like) envelope. In a real ASR pipeline these timings come from word-level transcript data; for promotional video, hardcode the timings to control emphasis pacing. The envelope leaves a subtle "rest glow" after the word, creating a breadcrumb of recent emphasis. +Words in a phrase visually activate (glow blur + scale) when "spoken", following an attack-sustain-release envelope over per-word `{ start, end }` timestamps. In a real ASR pipeline the timings come from a word-level transcript (`hyperframes transcribe` — same shape); for promo video, hand-author them to control emphasis pacing. The envelope never falls to zero after a word — it decays to a rest level, leaving a breadcrumb of recent emphasis. ## How It Works -Each word has `{ start, end }` timestamps. At each frame, compute the word's envelope value: +A single linear driver tween (`ease: "none"` — any other ease distorts the per-word envelope; do not change) sweeps scene time; its `onUpdate` loops over ALL words computing each one's envelope: 0 before `start`, linear attack to 1 over `ATTACK_DUR`, sustain at 1 until `end`, decay to `REST_LEVEL` over `RELEASE`, then hold at rest. The envelope drives `text-shadow` blur and `scale` — one driver for the whole phrase, never one tween per word (60+ words would bloat the timeline). -- **Pre-start** → 0 (not yet) -- **Start → peak** → attack (linear ramp 0 → 1) -- **Peak → end** → sustain (stays at 1) -- **End → end+release** → decay (1 → restLevel, typically 0.25) -- **After release** → restLevel (stays subtly highlighted) - -The envelope drives `textShadow` blur radius AND `scale`. Higher blur + bigger scale = "speaking" emphasis. - -## HTML +## Recipe ```html -
-
- - {w1} - {w2} - - {brandWord} -
+ +
+ {w1} + {w2} + + {brandWord}
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {sceneBackgroundColor}; - font-family: {font}; -} .phrase { display: flex; flex-wrap: wrap; - gap: 24px; justify-content: center; - max-width: 1700px; - font-size: 120px; - font-weight: 900; - letter-spacing: 2px; color: {restColor}; - text-align: center; - line-height: 1.2; } .word { - display: inline-block; + display: inline-block; /* required for transform on */ transform-origin: 50% 50%; - /* Initial subtle rest glow */ text-shadow: 0 0 0 {glowColorTransparent}; - will-change: transform, text-shadow; } .word.brand { color: {brandAccentColor}; - letter-spacing: 12px; - text-transform: uppercase; } ``` -## GSAP Timeline +```js +// Per-word spoken windows — one entry per span; brand word 1.5-2× a normal word's window. +const TIMINGS = { + // {w1Key}: { start: …, end: … }, — seconds, local to the scene +}; -```html - - + }, + 0, +); ``` -`glowColorRgba(env)` returns the brand glow color with `env`-modulated alpha (e.g. `rgba({glowR}, {glowG}, {glowB}, ${GLOW_ALPHA_BASE + env * GLOW_ALPHA_RANGE})`). +`glowColorRgba(env)` returns the glow color with `env`-modulated alpha. ## Variations -### Multi-octave glow (more dramatic peaks) - -Combine the envelope-driven blur with a sin pulse during the sustain phase — high-emphasis words breathe at peak. The sine frequency `PULSE_HZ` controls how many breaths fit in the sustain window; amplitude `PULSE_AMPLITUDE` controls how visible the breath is. +- **Karaoke style (RECOMMENDED for video narration)** — the default amplitudes read too subtle in video: inactive words still dominate. Render inactive words DIM and lerp the active word toward bright + larger; at any moment 1–2 words are bright (spoken + lingering rest) and the rest is dim. Use for short phrases (5–10 words) where one word at a time should POP; keep the subtle default for long dense text. Pushes MAX_BLUR, MAX_SCALE_BOOST, and REST↔ACTIVE contrast; everything else identical: ```js -const sustain = env * (1 + Math.sin(driver.t * PULSE_HZ) * PULSE_AMPLITUDE); -const blur = MAX_BLUR * sustain; -``` - -### Color shift on the peak - -The active word lerps from `restColor` → `peakColor` as `env` rises, settling back to `restColor` at rest: - -```js -function lerpChannel(a, b, t) { - return Math.round(a + (b - a) * t); -} -el.style.color = `rgb(${lerpChannel(REST_RGB.r, PEAK_RGB.r, env)}, ${lerpChannel(REST_RGB.g, PEAK_RGB.g, env)}, ${lerpChannel(REST_RGB.b, PEAK_RGB.b, env)})`; -``` - -### Karaoke style (dim-rest + bright-active, RECOMMENDED for video narration) - -Default amplitudes (small MAX_BLUR, small MAX_SCALE_BOOST, rest text full white) read as too subtle in video — the inactive words still dominate. Karaoke style fixes this: **inactive words rendered DIM**, active words **lerp toward bright white + larger scale**: - -```js -// Tunable constants — see How to Choose Values -// REST_RGB — dim color for inactive words -// ACTIVE_RGB — bright color at peak (non-brand) -// BRAND_RGB — bright color at peak (brand word) -// MAX_BLUR, MAX_SCALE_BOOST, REST_LEVEL all pushed higher than default - function lerpChannel(a, b, t) { return Math.round(a + (b - a) * t); } @@ -191,96 +92,37 @@ function colorAt(env, isBrand) { const target = isBrand ? BRAND_RGB : ACTIVE_RGB; return `rgb(${lerpChannel(REST_RGB.r, target.r, env)}, ${lerpChannel(REST_RGB.g, target.g, env)}, ${lerpChannel(REST_RGB.b, target.b, env)})`; } - -// In onUpdate: -el.style.color = colorAt(env, el.classList.contains("brand")); +// in onUpdate: el.style.color = colorAt(env, el.classList.contains("brand")); ``` -Visual result: at any moment 1-2 words are bright + glowing (the spoken word + the recently-spoken one's lingering rest), and the rest of the phrase is dim. This is closer to actual karaoke / lyric video aesthetic than the subtle "everyone half-glowing" baseline. +- **Multi-octave glow** — multiply the sustain by `1 + sin(driver.t × PULSE_HZ) × PULSE_AMPLITUDE` so high-emphasis words breathe at peak. +- **Color shift on the peak** — same channel-lerp from `restColor` → `peakColor` as `env` rises (non-karaoke form). +- **3D pop-out** — add `translateZ(env × MAX_POP_Z)` so the spoken word leans toward camera; requires `perspective` on the parent. +- **From real ASR transcripts** — convert `{ word, start_ms, end_ms }` entries to seconds and feed in identically. -When to use karaoke vs default: short narration phrases (5-10 words) where one word at a time should clearly POP → karaoke. Long dense text where many words emphasize subtly → default subtle. Karaoke pushes MAX_BLUR, MAX_SCALE_BOOST, and contrast between REST_RGB and ACTIVE_RGB; everything else is identical. +## Values -### 3D pop-out - -Combine envelope with `translateZ` for words to "lean toward camera" as they speak: - -```js -const popZ = env * MAX_POP_Z; -el.style.transform = `translateZ(${popZ}px) scale(${scale})`; -``` - -Requires `perspective` on the parent. - -### From real ASR transcripts - -For real ASR-driven scenes, replace hardcoded TIMINGS with transcript JSON (each entry has `word`, `start_ms`, `end_ms`). Convert to seconds and feed in identically. The shape `{ [wordKey]: { start, end } }` is the same whether hand-authored or derived from `hyperframes transcribe`. - -## How to Choose Values - -- **TIMINGS** — per-word `{ start, end }` map. Author one entry per `.word` span. - - Shape: `{ wordKey: { start: number, end: number } }`, all seconds local to the scene. - - Constraints: monotonic non-overlap — every entry's `end < next entry's start` (overlapping windows make the envelope ambiguous). - - Brand word window: typically 1.5-2× the average non-brand word window so the brand sustains. -- **ATTACK_DUR** — seconds for the envelope to ramp 0 → 1 once a word starts. - - Range: 0.1-0.25 s - - Effects: shorter feels punchy and ASR-like; longer feels smoothed-out. - - Constraints: must be < (smallest word's end - start), otherwise the word never reaches 1. -- **RELEASE** — seconds for the envelope to decay 1 → REST_LEVEL after a word ends. - - Range: 0.2-0.5 s -- **REST_LEVEL** — held envelope value after RELEASE. - - Range: 0.15-0.4 (default style); 0.05-0.2 (karaoke style — dimmer rest). - - Effects: lower = quieter breadcrumb; higher = more recently-spoken words stay bright. - - Constraints: must be < 1; should be > 0 to preserve the breadcrumb. -- **MAX_BLUR** — peak `text-shadow` blur radius in px. - - Range: 15-25 px (default style); 30-45 px (karaoke style). - - Effects: bigger reads as "shouting"; smaller reads as "neutral narration". -- **MAX_SCALE_BOOST** — additive scale at peak (e.g. 0.08 ⇒ 1.0 → 1.08). - - Range: 0.03-0.10 (default style); 0.15-0.25 (karaoke style). - - Effects: bigger reads as "bouncy"; smaller reads as "just glowing". -- **SCENE_DURATION** — total seconds for the single driver tween. - - Constraints: must equal the scene's `data-duration` so the driver `t` reaches the end of TIMINGS in sync with HF's seek. -- **REST_RGB / ACTIVE_RGB / BRAND_RGB** (karaoke style) — discrete color choices, not numeric. - - REST_RGB: dim tone of the brand palette's neutral; should read as off-white-ish dim, not black. - - ACTIVE_RGB: brand text color at full readability. - - BRAND_RGB: brand accent color (often the same hue as the glow). -- **PULSE_HZ / PULSE_AMPLITUDE** (multi-octave variation) — sine breath frequency / depth. - - PULSE_HZ range: 4-10 rad/s; PULSE_AMPLITUDE range: 0.1-0.3. -- **MAX_POP_Z** (3D pop-out variation) — max Z translation at peak (px). - - Range: 20-60 px; requires parent `perspective`. - -Ease family — discrete choice: - -- Single linear driver (`ease: "none"`) so `t` maps 1:1 to scene time. Any other ease distorts the per-word envelope shape — do not change. - -## Key Principles - -- **Envelope shape: attack-sustain-decay-rest** — never zero out after a word. The rest level (REST_LEVEL > 0) keeps the recently-spoken words subtly highlighted, creating a "breadcrumb" of attention. -- **Brand word gets longer emphasis (1.5-2× normal)** — the brand is the headline; let it sustain. -- **`display: inline-block`** on each word — required for `transform` to apply to ``. -- **MAX_BLUR and MAX_SCALE_BOOST stay in their default-style ranges unless you commit to karaoke** — picking values between default and karaoke yields awkward "half-loud" emphasis. -- **Per-word `text-shadow`** (not `box-shadow`) — text-shadow is the glow around the GLYPH, which is what reads as "speaking emphasis." Box-shadow would glow around the inline-block bounding box (rectangle). -- **Single driver, multi-word onUpdate** — one tween that loops over all words. Don't create one tween per word — at 60+ words the timeline becomes unwieldy. -- **❗ Climax dwell ≥1s** — after the final word's emphasis, comp continues ≥1s. The last word IS the headline beat. +| token | default style | karaoke style | notes | +| --------------- | -------------------- | ------------- | ---------------------------------------------------------- | +| ATTACK_DUR | 0.1–0.25s | same | must be < the shortest word's window or it never reaches 1 | +| RELEASE | 0.2–0.5s | same | decay to rest | +| REST_LEVEL | 0.15–0.4 | 0.05–0.2 | > 0 (breadcrumb), < 1 | +| MAX_BLUR | 15–25px | 30–45px | bigger = "shouting" | +| MAX_SCALE_BOOST | 0.03–0.10 | 0.15–0.25 | additive at peak (0.08 ⇒ scale 1.08) | +| PULSE_HZ / AMP | 4–10 rad/s / 0.1–0.3 | — | multi-octave variation | +| MAX_POP_Z | 20–60px | — | 3D variation | +| SCENE_DURATION | = `data-duration` | same | driver must end in sync with the scene's seek window | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS animation** on word elements -- **`display: inline-block`** on each `.word` -- **`will-change: transform, text-shadow`** on `.word` -- **Timings monotonic** (later start > earlier end) — overlapping words mess up the envelope +- **Timings monotonic, non-overlapping** — every entry's `end` < the next entry's `start`; overlapping windows make the envelope ambiguous. +- **Brand word window 1.5–2× a normal word** — the brand is the headline; let it sustain. +- **Driver ease stays `"none"`** — any other ease warps every word's envelope timing. +- **`text-shadow`, not `box-shadow`** — the glow must hug the GLYPH (speaking emphasis), not the inline-block rectangle. +- **One driver looping all words** — never one tween per word. +- **Commit to a style** — values between the default and karaoke columns yield awkward "half-loud" emphasis. +- **Climax dwell ≥1s** after the final word's emphasis — the last word IS the headline beat. -## Combinations +## See also -- [3d-text-depth-layers.md](3d-text-depth-layers.md) — the active word gets depth-layered emphasis at peak -- [sine-wave-loop.md](sine-wave-loop.md) — non-active words breathe subtly between emphasis moments -- [context-sensitive-cursor.md](context-sensitive-cursor.md) — typewriter that types each word matching the ASR cadence - -## Pairs with HF skills - -- `/hyperframes-animation` — single driver, multi-element envelope -- `/media-use` — `hyperframes transcribe` outputs real ASR data -- `/media-use` — pair with caption rendering -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`3d-text-depth-layers` (depth on the active word at peak) · `sine-wave-loop` (idle breathe between emphasis moments) · `context-sensitive-cursor` (typewriter matching the ASR cadence) · `/media-use` for `hyperframes transcribe` and caption rendering. diff --git a/skills/hyperframes-animation/rules/avatar-cloud-network.md b/skills/hyperframes-animation/rules/avatar-cloud-network.md index 19cd540ad..7d47ebc9d 100644 --- a/skills/hyperframes-animation/rules/avatar-cloud-network.md +++ b/skills/hyperframes-animation/rules/avatar-cloud-network.md @@ -7,76 +7,29 @@ metadata: # Avatar Cloud Network -Avatars arranged on an elliptical ring around a central element (logo / counter / brand). SVG dashed connection lines from center to each avatar. Staggered spring entry on avatars, then connection lines draw outward — communicates "community" or "social proof." Distinct from [orbit-3d-entry](orbit-3d-entry.md) (which continuously orbits) — avatar-cloud is a static composed reveal. +Avatars on an elliptical ring around a central hub (logo / counter), with SVG dashed lines drawing outward from the hub to each avatar — "community" / social proof. Distinct from [orbit-3d-entry.md](orbit-3d-entry.md) (continuous orbit): this settles into a static composed formation. ## How It Works -Three rendering layers: +Three layers: SVG lines (z-index 1, behind), avatars (z-index 2), hub (z-index 5 — lines terminate AT its edge, never pass through). Avatar positions and lines are built once at setup from ONE shared center; the timeline then runs hub fade → avatar cascade → outward line draw → breathing dwell. Drawing FROM the center is the narrative: "the hub connects to its community." -1. **SVG connection lines** (z-index 1, behind everything) — line from center hub to each avatar's position -2. **Avatars** (z-index 2) — `
` circles on elliptical positions -3. **Center hub** (z-index 5) — brand counter or logo (sits ABOVE the lines that converge on it) - -Animation phases: - -- `HUB_FADE_START → HUB_FADE_START + HUB_FADE_DUR`: hub fades in -- `AVATAR_ENTRY_START → AVATAR_ENTRY_START + (AVATAR_COUNT − 1) × AVATAR_STAGGER + AVATAR_ENTRY_DUR`: avatars cascade in -- `LINES_START → LINES_START + (AVATAR_COUNT − 1) × LINE_STAGGER + LINES_DUR`: connection lines draw outward -- climax dwell: optional idle breathing on avatars (see Variations / sine-wave-loop) - -## HTML +## Recipe ```html -
- - - - - - -
-
-
{counterValue}
-
{counterLabel}
-
- -
- -
{footerLine}
+ + +
+
{counterValue} {counterLabel}
+
``` -Placeholder tokens: - -- `{counterValue}` / `{counterLabel}` — the hub copy (numeric proof + category) -- `{footerLine}` — optional attribution line under the cloud -- `{avatar[i]}` — per-avatar image source (or emoji glyph if using the emoji variation below) - -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - background: {bgColor}; - font-family: {font}; - overflow: hidden; -} .lines { position: absolute; inset: 0; - width: 100%; - height: 100%; - pointer-events: none; z-index: 1; + pointer-events: none; } .hub-wrap { position: absolute; @@ -87,285 +40,106 @@ Placeholder tokens: .hub { position: relative; z-index: 5; - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - padding: 48px 64px; - border-radius: 28px; - background: {hubBg}; - border: 1px solid {hubBorder}; -} -.hub-num { - font-size: HUB_NUM_FONT_SIZE; - font-weight: 900; - color: {textColor}; - letter-spacing: -4px; - line-height: 1; - font-variant-numeric: tabular-nums; -} -.hub-label { - font-size: HUB_LABEL_FONT_SIZE; - font-weight: 800; - letter-spacing: 12px; - color: {accentColor}; - text-transform: uppercase; } .avatar { position: absolute; z-index: 2; - width: AVATAR_SIZE; - height: AVATAR_SIZE; - border-radius: 50%; - border: 3px solid {avatarBorder}; - box-shadow: - 0 12px 32px rgba(0, 0, 0, 0.5), - 0 0 24px {avatarGlow}; - display: grid; - place-items: center; - font-size: AVATAR_GLYPH_SIZE; - background: {avatarBg}; + transform: translate(-50%, -50%); /* centers on the (left, top) the script sets */ will-change: transform, opacity; - /* Top-left positioned by script; transform centers via -50% trick */ - transform: translate(-50%, -50%); -} -.brand { - position: absolute; - bottom: 80px; - left: 50%; - transform: translateX(-50%); - font-size: BRAND_FONT_SIZE; - font-weight: 900; - letter-spacing: 14px; - color: {accentColor}; - text-transform: uppercase; } ``` -## GSAP Timeline +```js +// CENTER_X/Y must equal the hub's RENDERED center exactly — every avatar +// position and line endpoint derives from it. For a place-items:center hub on +// a 1920×1080 canvas: (W/2, H × CENTER_Y_FACTOR). +const C = { x: CENTER_X, y: CENTER_Y }; +const wrap = document.querySelector(".hub-wrap"); +const svg = document.querySelector(".lines"); -```html - - +// Climax dwell — out-of-phase breathing holds the eye on the formed network: +// one phase proxy (0 → 2π·BREATH_CYCLES, ease "none"); onUpdate scales avatar i by +// 1 + sin(p + (i/n)·2π) · BREATH_AMP — sine-wave-loop's multiplicative onUpdate form. +// Keep the -50% centering in the same transform write. ``` -## How to Choose Values - -### Geometry - -- **CENTER_X / CENTER_Y** — px coordinates of the hub center; lines and avatar positions derive from these. - - Constraints: **must equal the hub's actual rendered center** — when this rule is composed with another scene (e.g. a logo that has been recentered), `CENTER_X / CENTER_Y` must be baked from the same source as the hub's final position - - Reference: ../../examples/proof-logo-chain.html uses `(W/2, H × 0.47)` so the cloud sits slightly above the canvas midline -- **RADIUS_X / RADIUS_Y** — ellipse radii in px (RADIUS_X ≥ RADIUS_Y reads as perspective). - - Range: `RADIUS_X` ~ 20-30% of viewport width; `RADIUS_Y` ~ 18-25% of viewport height - - Constraints: `RADIUS_X / RADIUS_Y` ratio between 1.5 and 3.0 reads as natural depth; ratio = 1 (circle) reads as a flat 2D layout - - Reference: ../../examples/proof-logo-chain.html uses `W * 0.25` (`480px`) and `H * 0.22` (`237.6px`) -- **AVATAR_COUNT** — number of avatars distributed around the ring. - - Range: 8-12; fewer feels sparse, more clutters the ellipse - - Reference: ../../examples/proof-logo-chain.html uses `10` -- **AVATAR_SIZE / AVATAR_GLYPH_SIZE** — px diameter of each avatar circle and (optional) inner glyph size. - - Range: `AVATAR_SIZE` ~ 80-120 px at 1920 wide; small enough that 10+ avatars fit the ring without overlap -- **HUB_NUM_FONT_SIZE / HUB_LABEL_FONT_SIZE / BRAND_FONT_SIZE** — hub typography. - - Constraints: hub-num is the focal beat, sized 2-4× the label - -### Hub fade - -- **HUB_FADE_START** — when the hub fades in. - - Range: usually `0` (the hub establishes the focal point); offset if the scene precedes with another beat -- **HUB_FADE_DUR** — hub fade-in duration. - - Range: 0.4-0.6s -- **HUB_BOUNCE** — `back.out(HUB_BOUNCE)` coefficient on the hub's scale entry. - - Range: 1.4 (subtle) → 1.8 (firm) - -### Avatar cascade - -- **AVATAR_ENTRY_START** — when the first avatar pops in. - - Constraints: `≥ HUB_FADE_START + HUB_FADE_DUR × 0.6` so the hub is established before satellites arrive -- **AVATAR_ENTRY_DUR** — per-avatar scale-up duration. - - Range: 0.4-0.7s -- **AVATAR_STAGGER** — delay between consecutive avatar entries. - - Range: 0.06-0.10s; cascade reads as "joining"; simultaneous reads as "all already there" -- **AVATAR_BOUNCE** — `back.out(AVATAR_BOUNCE)` coefficient on each avatar's pop. - - Range: 1.4 (gentle) → 1.8 (firm); slightly firmer than hub for differentiation - -### Connection lines - -- **LINES_START** — when the lines begin drawing outward. - - Constraints: `LINES_START + (AVATAR_COUNT − 1) × LINE_STAGGER` should overlap the last avatar's settle by ~0.1-0.2s so the drawing reads as a consequence of the avatars landing -- **LINES_DUR** — per-line draw duration (strokeDashoffset → 0). - - Range: 0.4-0.7s -- **LINE_STAGGER** — delay between consecutive lines starting. - - Range: 0.02-0.05s; tight stagger reads as a wave outward - -### Idle breathing - -- **BREATH_START** — when idle breathing activates. - - Constraints: `≥ LINES_START + (AVATAR_COUNT − 1) × LINE_STAGGER + LINES_DUR + ~0.2s` (let the lines settle) -- **BREATH_DUR** — total duration of the breathing tween. - - Range: fills the remaining composition window -- **BREATH_CYCLES** — number of full sine cycles across `BREATH_DUR`. - - Range: 1.0-2.0; under 1 reads as a single sigh, over 2 starts to look anxious -- **BREATH_AMP** — sine amplitude on scale (multiplicative). - - Range: 0.02-0.06; smaller for headshots, larger for stylized glyphs - -### Color tokens - -- **{bgColor}** — stage background (typically a dark gradient so the cloud reads as a constellation) -- **{textColor}** — hub-num color (primary copy) -- **{accentColor}** — hub-label + footer (the brand voice) -- **{hubBg} / {hubBorder}** — hub card surfaces; gradient + 1px border reads as elevated -- **{avatarBg} / {avatarBorder} / {avatarGlow}** — avatar circle styling; soft border + glow keeps them legible on dark backgrounds -- **{lineColor}** — SVG stroke color (translucent accent reads as networky) -- **{font}** — base typography stack - ## Variations -### Avatar size variation (organic feel) +- **Size variety**: vary avatar sizes by a small index-keyed array so the ring doesn't read rigidly repetitive. +- **Solid lines**: drop the dash + draw; lines fade in via opacity — more corporate, less networky. +- **Multi-orbit**: inner ring (fewer, larger) connected to the hub; outer ring is an unconnected "halo." +- **Glyph avatars**: flags / emoji / icons instead of faces — reads "global community" or role spread. -Vary avatar sizes by index — e.g. a small index-keyed array of sizes — so the ring doesn't read as rigidly repetitive. +## Values -### Solid lines instead of dashed +| token | range | notes | +| -------------- | ---------------------------- | ---------------------------------------------------------------- | +| AVATAR_COUNT | 8–12 | fewer feels sparse; more clutters the ellipse | +| RADIUS_X / \_Y | ~20–30% W / ~18–25% H | ratio X/Y 1.5–3.0 reads as perspective; 1 (circle) reads flat | +| avatar size | 80–120px @1920 | ring must fit 10+ without overlap | +| HUB_DUR | 0.4–0.6s | HUB_BOUNCE 1.4–1.8 | +| AVATAR_AT | ≥ 0.6 × HUB_DUR | hub established before satellites arrive | +| AVATAR_DUR | 0.4–0.7s | AVATAR_BOUNCE 1.4–1.8, slightly firmer than hub | +| AVATAR_STAGGER | 0.06–0.10s | cascade reads "joining"; simultaneous reads "already there" | +| LINES_AT | overlaps last avatar settle | start ~0.1–0.2s before it — draw reads as consequence of landing | +| LINE_DUR | 0.4–0.7s | LINE_STAGGER 0.02–0.05s = a wave outward | +| BREATH_CYCLES | 1.0–2.0 over the remaining s | under 1 = single sigh; over 2 = anxious. BREATH_AMP 0.02–0.06 | -Drop `stroke-dasharray` and use a solid stroke. Drop the dash-draw animation; lines fade in via opacity instead. More corporate, less networky. - -### Multi-orbit (concentric rings) - -Two layers of avatars: smaller inner ring (fewer avatars, slightly larger size), larger outer ring (more, smaller). Lines connect ONLY inner ring to hub; outer ring is a "halo." - -### Country / role glyphs (geographic or persona spread) - -Replace face images with flags / emoji / iconography. Reads as "global community" or "diverse roles." - -## Key Principles - -- **Hub above lines (`z-index: 5` vs lines `z-index: 1`)** — lines should appear to terminate AT the hub edge, not pass through. Hub must be in front. -- **Lines drawn outward (dash offset 0)** — drawing FROM center is the visual narrative: "the hub connects to its community." -- **8-12 avatars** — fewer feels sparse, more clutters the ellipse. -- **`RADIUS_X > RADIUS_Y`** — horizontal ellipse reads as perspective; equal radii (circle) reads as 2D flat layout. -- **Avatar entry stagger 0.06-0.10s** — cascade reads as "joining"; simultaneous reads as "all already there." -- **Stagger lines AFTER avatars are mostly settled** — line draw starts ~0.1-0.2s before last avatar settles for overlap. -- **Idle breathing post-formation** — each avatar slightly out-of-phase. Holds the eye during climax dwell. -- **❗ Climax dwell ≥1s** — after lines complete, hold for ≥1s so the formed network is readable. +Tokens: dark `{bgColor}` so the cloud reads as a constellation; translucent accent `{lineColor}`; soft border + glow keeps avatars legible on dark. ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS animation** on avatars or lines -- **`will-change: transform, opacity`** on avatars -- **SVG `pointer-events: none`** — decorative overlay -- **`getTotalLength()` not needed for straight lines** — use `Math.hypot` for line length (cheaper, exact) -- **Hub `z-index` > lines z-index** — explicit layering +- **CENTER_X/Y must match the hub's actual rendered center** — when composed with another scene (e.g. a recentered logo), bake them from the same source as the hub's final position, or lines visibly miss the hub. +- **Hub z-index above lines** — lines terminate at the hub edge, never cross it. +- **Lines draw outward** (dashoffset len → 0), starting after avatars are mostly settled. +- **`RADIUS_X > RADIUS_Y`** — a horizontal ellipse reads as perspective; a circle reads flat. +- **Climax dwell ≥ 1s** after lines complete so the formed network is readable. +- Straight lines: `Math.hypot` for length — `getTotalLength()` not needed. -## Combinations +## See also -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — the hub IS a growing counter -- [sine-wave-loop.md](sine-wave-loop.md) — avatar idle breathing pattern -- [3d-text-depth-layers.md](3d-text-depth-layers.md) — hub label with depth layers - -## Pairs with HF skills - -- `/hyperframes-animation` — staggered spring entries + SVG dash draw -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`counting-dynamic-scale` (the hub IS a growing counter) · `sine-wave-loop` (the breathing form) · `orbit-3d-entry` (the continuously-orbiting cousin). diff --git a/skills/hyperframes-animation/rules/camera-cursor-tracking.md b/skills/hyperframes-animation/rules/camera-cursor-tracking.md index 7d4939059..b979bade9 100644 --- a/skills/hyperframes-animation/rules/camera-cursor-tracking.md +++ b/skills/hyperframes-animation/rules/camera-cursor-tracking.md @@ -7,240 +7,127 @@ metadata: # Two-Phase Camera Cursor Tracking -Keeps a horizontally-growing element (e.g. a search bar with typing text, a long URL animating in) visible by switching between two camera modes. +Keeps a horizontally-growing element (a search bar with typing text, a long URL animating in) visible by switching between two camera modes. ## How It Works Separate **World Space** (the full target element with all content) from **Screen Space** (the viewport). Two phases: -- **Phase 1 (Static)** — The world container sits at a fixed initial offset. Camera doesn't move. This anchors the viewer's eye to the composition before tracking begins. -- **Phase 2 (Tracking)** — Activates when the focal point (cursor, highlight, last typed glyph) exceeds a target screen position (e.g. a configurable fraction `CURSOR_TARGET_FRACTION` of viewport width from the left). The world container translates leftward (`x: -`) keeping the focal point pinned at that screen position. +- **Phase 1 (Static)** — the world container sits at a fixed initial offset; the camera doesn't move. Anchors the viewer's eye before tracking begins. +- **Phase 2 (Tracking)** — activates when the focal point (cursor, highlight, last typed glyph) exceeds a target screen position (`CURSOR_TARGET_FRACTION × viewportWidth` from the left). The world translates leftward (`x: -delta`) keeping the focal point pinned at that screen position. -The offset math is **mathematically continuous** at the phase boundary — at the instant tracking starts, the world position equals what the static phase had. So the transition is seamless. - -The piecewise form used in code is: +The offset math is **mathematically continuous** at the phase boundary — at the instant tracking starts, the world position equals what the static phase had, so the transition is seamless. The piecewise form: ``` finalWorldX = Math.min(INITIAL_OFFSET, trackingOffset) ``` -`INITIAL_OFFSET` is the static-phase value; `trackingOffset` is whatever shift keeps the focal point at `CURSOR_TARGET_FRACTION × viewportWidth`. While the focal point hasn't grown past the target screen X, `trackingOffset` exceeds `INITIAL_OFFSET` (it's a less-negative number) and `Math.min` returns the static value. Once the focal point would cross the target, `trackingOffset` overtakes `INITIAL_OFFSET` and tracking takes over. +`INITIAL_OFFSET` is the static-phase value; `trackingOffset` is whatever shift keeps the focal point at the target screen X. While the focal point hasn't grown past the target, `trackingOffset` is a less-negative number and `Math.min` returns the static value; once the focal point would cross the target, `trackingOffset` overtakes and tracking takes over. Do NOT replace this with a hard `if (typingProgress > threshold)` branch — the camera will visibly jump. -## HTML +## Recipe ```html -
-
-
- +
+
+
``` -## CSS (hero-frame layout) - ```css -.scene { - position: relative; - width: 100%; - height: 100%; -} - .viewport { position: absolute; inset: 0; - overflow: hidden; /* clip the world content */ + overflow: hidden; /* clip the world's left edge as it pans off-screen */ display: flex; align-items: center; justify-content: flex-start; - padding-left: VIEWPORT_PAD_LEFT; /* "left margin" — variation: left-aligned init */ + padding-left: VIEWPORT_PAD_LEFT; /* Phase-1 anchor X — must match the JS constant */ } - .world { display: flex; align-items: center; - white-space: nowrap; /* keep text on one line for camera-tracking */ - transform: translateX(0); /* GSAP will animate this */ + white-space: nowrap; /* text must stay on one line for the camera math */ } - -.search-bar { - font-family: {font}; - font-size: BAR_FONT_SIZE; - font-weight: BAR_FONT_WEIGHT; - color: {textColor}; - letter-spacing: BAR_LETTER_SPACING; -} - .search-bar .text { - /* Width grows as more characters reveal */ display: inline-block; overflow: hidden; vertical-align: bottom; } - .search-bar .cursor { - display: inline-block; + display: inline-block; /* inline sibling of the text, NOT absolutely positioned — + absolute positioning misaligns with the camera math */ width: CURSOR_WIDTH; margin-left: CURSOR_GAP; background: {accentColor}; height: CURSOR_HEIGHT_EM; vertical-align: bottom; - /* No `animation: blink` CSS keyframe here — HF renders by seeking a paused - timeline, and CSS animation clocks are NOT synced to that seek. A CSS - blink will flicker non-deterministically. Drive cursor blink as a finite - yoyo tween on the GSAP timeline instead — see GSAP Timeline section. */ + /* no CSS blink animation — CSS clocks don't sync to seek; blink is a GSAP tween below */ } ``` -## GSAP Timeline +```js +// Pre-measure the target text width to compute tracking distance. +// Measure SYNCHRONOUSLY — no fonts.ready gate (see Critical Constraints). +const textEl = document.getElementById("reveal-text"); +const targetCursorScreenX = CURSOR_TARGET_FRACTION * VIEWPORT_WIDTH; +const fullWidth = textEl.scrollWidth; // total text width after full reveal +const trackingDelta = Math.max(0, VIEWPORT_PAD_LEFT + fullWidth - targetCursorScreenX); -```html - - +// Cursor blink — finite GSAP yoyo (never CSS @keyframes; CSS animation clocks +// aren't synced to HF's seek and flicker non-deterministically). +const blinkRepeats = Math.ceil(SCENE_DURATION / BLINK_HALF_PERIOD) - 1; +tl.to( + ".search-bar .cursor", + { opacity: 0, duration: BLINK_HALF_PERIOD, ease: "steps(1)", yoyo: true, repeat: blinkRepeats }, + 0, +); ``` -### Variations +## Variations -- **Centered → Center-Tracked**: set `.viewport { justify-content: center; padding: 0; }`. Camera tracks once the focal point crosses the midline (`CURSOR_TARGET_FRACTION = 0.5`). -- **Left-Aligned → Right-Tracked**: as written above. Best when content exceeds viewport width from the start. -- **Continuous typing driver**: replace the `maxWidth` tween with an `onUpdate` typing clock (`charsTyped = Math.floor(progress)`) plus per-frame `measureNodeWidth` to drive the cursor screen X. Required when the typed text is consumed elsewhere in the scene (e.g. by a parent strip's camera offset). +- **Centered → center-tracked**: `.viewport { justify-content: center; padding: 0; }`, `CURSOR_TARGET_FRACTION = 0.5` — tracks once the focal point crosses the midline. +- **Left-aligned → right-tracked**: as written; best when content exceeds viewport width from the start. +- **Continuous typing driver**: replace the `maxWidth` tween with an `onUpdate` typing clock (`charsTyped = Math.floor(progress)`) plus per-frame `measureNodeWidth` driving the cursor screen X — required when the typed text is consumed elsewhere in the scene (e.g. by a parent strip's camera offset). -## How to Choose Values +## Values -- **VIEWPORT_PAD_LEFT** — left-edge padding of the world inside the viewport (Phase 1 anchor X). - - Range: 0 → ~10% of viewport width - - Effects: 0 hugs the left edge; larger inset feels more like a centered hero frame - - Constraints: must match the CSS `padding-left` on `.viewport` or the camera math drifts -- **VIEWPORT_WIDTH** — the composition's `data-width` in CSS pixels. - - Constraints: must equal the scene root's `data-width`; never tweened -- **CURSOR_TARGET_FRACTION** — fraction of viewport width where the focal point locks during Phase 2. - - Range: 0.5 (center-tracked) → 0.75 (right-leaning, more text visible behind cursor) - - Effects: lower values leave less revealed text in frame; higher values delay tracking -- **BAR_FONT_SIZE** — hero element font size. - - Range: ~8-12% of viewport height; below ~6% reads as a UI widget rather than a cinematic element -- **BAR_FONT_WEIGHT** — weight of the search-bar text. - - Range: discrete; 400 for neutral demo text, 700 for hero / headline framing -- **BAR_LETTER_SPACING** — `letter-spacing` for the bar text. - - Range: slight negative (tighter, more cinematic) → 0 (default) -- **CURSOR_WIDTH** — visual cursor stroke width in CSS pixels. - - Range: ~4-10 px at 1080p; thinner reads as typed text, thicker reads as a block caret -- **CURSOR_GAP** — `margin-left` between text and cursor. - - Range: a few px of breathing room; do not exceed the cursor width or it visually detaches -- **CURSOR_HEIGHT_EM** — cursor height as an em multiple of the font. - - Range: 0.85-1.0; matches the visual height of the typed glyphs -- **REVEAL_START** — when Phase 1 typing begins. - - Constraints: typically 0; if preceded by another phase, ≥ that phase's end + small buffer -- **REVEAL_DUR** — duration of the Phase 1 reveal tween. - - Range: scale with character count (target an average per-character cadence in the 0.05-0.15s range) - - Constraints: must end before `SCENE_DURATION` and ideally overlap slightly with the tracking phase -- **TRACK_START** — when Phase 2 camera motion begins. - - Range: usually before reveal completes so the handoff feels continuous; can equal `REVEAL_START` if the focal point is already past the target at t=0 - - Constraints: `TRACK_START < REVEAL_START + REVEAL_DUR` for a smooth crossfade -- **TRACK_DUR** — duration of the camera pan. - - Range: 0.8-2.0s; under 0.5s reads as a snap, over 2.5s drags -- **SCENE_DURATION** — must match the scene root's `data-duration`. - - Constraints: feeds the blink repeat count; mismatch causes blinks to truncate or run past the end -- **BLINK_HALF_PERIOD** — half-period of the cursor blink (one on-state OR one off-state). - - Range: 0.2-0.4s; 0.3s reads as a natural caret blink - - Constraints: derived value `Math.ceil(SCENE_DURATION / BLINK_HALF_PERIOD) - 1` must be ≥ 0 -- **Ease choices** — discrete: - - Camera pan: `power2.inOut` or `power3.inOut` for cinematic settle; avoid `back.out` (overshoot reads as UI bounce, not camera) - - Reveal: `"none"` for linear typing; any easing distorts the per-keystroke cadence - - Blink: `"steps(1)"` for hard on/off; any easing fades the cursor and breaks the caret feel - -## Key Principles - -- **Measure with `getBoundingClientRect()` / probe nodes**, not by character count × font-size. Proportional fonts have variable glyph widths. -- **`white-space: nowrap`** on the world — text must stay on one line for camera math to work -- **Pre-allocate the world width** by setting `maxWidth` at full target width — prevents layout shift mid-tween -- **Eased camera** (`power2.inOut` / `power3.inOut`), not linear — natural pan feel -- **Spring-like via easing**, not via stiffness/damping params — GSAP doesn't have a built-in spring, but `back.out(${BOUNCE_FACTOR})` or `power4.out` approximate the settling feel +| token | range | notes | +| ---------------------- | --------------------------- | ------------------------------------------------------------------------------- | +| VIEWPORT_PAD_LEFT | 0 → ~10% of viewport width | must match the CSS `padding-left` or the camera math drifts | +| VIEWPORT_WIDTH | = the root's `data-width` | never tweened | +| CURSOR_TARGET_FRACTION | 0.5–0.75 | lower = less revealed text in frame; higher delays tracking | +| CURSOR_WIDTH / GAP | 4–10 px / a few px | gap ≤ cursor width or it visually detaches | +| CURSOR_HEIGHT_EM | 0.85–1.0 em | matches the typed glyph height | +| REVEAL_DUR | chars × 0.05–0.15s | ease `"none"` — any easing distorts the per-keystroke cadence | +| TRACK_START | < REVEAL_START + REVEAL_DUR | overlap the reveal so the handoff feels continuous | +| TRACK_DUR | 0.8–2.0s | `power2.inOut`/`power3.inOut`; `back.out` reads as UI bounce, not camera | +| BLINK_HALF_PERIOD | 0.2–0.4s | `steps(1)` hard on/off; repeats derived from SCENE_DURATION (= `data-duration`) | ## Critical Constraints -- **Build the timeline SYNCHRONOUSLY, no fonts.ready gate** — HF renders frames in parallel workers, each a fresh browser. If you wrap the timeline build in `document.fonts.ready.then(...)`, some workers will seek frames BEFORE the Promise resolves and find no timeline registered → those frames render at CSS initial state (e.g. `max-width: 0` ⇒ empty text), other workers render correctly → visible flicker between empty and filled. Register `window.__timelines[id] = tl` at script-parse time, even if fonts haven't loaded yet — the camera math can tolerate a few percent width error from fallback-font measurement, but worker-race flicker is unacceptable. -- **If precise post-font measurement matters**, re-measure inside the tween's `onUpdate` (still deterministic per-frame seek), not via a Promise gate. Or set `font-display: block` on the @font-face to force the browser to wait for the font before painting any text. -- **Timeline must be paused**: `gsap.timeline({ paused: true })`. Never `tl.play()` -- **Registry key = `data-composition-id`**: `window.__timelines["tracking-scene"]` must match scene root -- **Continuous math at phase boundary**: the world's `x` at the moment tracking starts must equal the static-phase offset. The `Math.min(INITIAL_OFFSET, trackingOffset)` formulation guarantees this; do NOT switch to a hard `if (typingProgress > threshold)` branch or the camera will visibly jump. -- **Inline cursor, not absolutely positioned**: cursor should be a sibling of the text (inline-block) so it follows text flow naturally — absolute positioning misaligns with the camera math -- **`overflow: hidden` on `.viewport`**: clip the world's left edge as it pans off-screen -- **Cursor blink via GSAP, NOT CSS `@keyframes ... infinite`** — HF renders by seeking the paused timeline; CSS animation clocks are NOT synchronized with that seek, so any CSS-driven blink will flicker non-deterministically across frames. Always drive blink as a finite yoyo tween on the paused GSAP timeline (repeat count computed from scene length). +- **Build the timeline SYNCHRONOUSLY — no `fonts.ready` gate.** HF renders frames in parallel workers, each a fresh browser. A `document.fonts.ready.then(...)` wrapper means some workers seek frames BEFORE the Promise resolves and find no timeline → those frames render at CSS initial state (`max-width: 0` ⇒ empty text) while others render correctly → visible flicker. Register the timeline at script-parse time: the camera math tolerates a few percent width error from fallback-font measurement; worker-race flicker is unacceptable. If precise post-font measurement matters, re-measure inside the tween's `onUpdate` (still deterministic per-frame), or set `font-display: block` on the @font-face. +- **Measure with `getBoundingClientRect()` / `scrollWidth` / probe nodes**, never character count × font-size — proportional fonts have variable glyph widths. +- **Continuous math at the phase boundary** — the `Math.min(INITIAL_OFFSET, trackingOffset)` form, never a hard threshold branch. +- **`white-space: nowrap` on the world** and pre-allocated width (tween `maxWidth` to the full target width) — prevents layout shift mid-tween. +- **Cursor is an inline sibling of the text**, and blinks via a finite GSAP yoyo — never CSS `@keyframes … infinite`. +- **`overflow: hidden` on `.viewport`** — clips the world as it pans. -## Combinations +## See also -- [context-sensitive-cursor.md](context-sensitive-cursor.md) — change cursor color/style per text segment during typing -- [discrete-text-sequence.md](discrete-text-sequence.md) — non-linear text reveals that pair with this camera - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + tween API -- `/hyperframes-core` — composition wiring + `data-*` attributes -- `/hyperframes-cli` — `hyperframes lint` to validate the registry key + duration +[context-sensitive-cursor.md](context-sensitive-cursor.md) (cursor color per text segment) · [discrete-text-sequence.md](discrete-text-sequence.md) (non-linear text reveals under this camera). diff --git a/skills/hyperframes-animation/rules/card-morph-anchor.md b/skills/hyperframes-animation/rules/card-morph-anchor.md index 16ce3f91f..3ae8ae1cb 100644 --- a/skills/hyperframes-animation/rules/card-morph-anchor.md +++ b/skills/hyperframes-animation/rules/card-morph-anchor.md @@ -7,261 +7,128 @@ metadata: # Card Morph Anchor -A container smoothly transforms its width, height, border-radius, and (optionally) background between two visual states. The morph itself **IS the shot transition** — no separate transition effect needed. The viewer's eye tracks the morphing container as the anchor between shots. +A free-floating container morphs apparent size, corner radius, and surface treatment between two shots — the morph itself IS the transition; the viewer's eye tracks the persistent container. Distinct from [anchored-layout-expand.md](anchored-layout-expand.md) (an edge-pinned live layout participant that grows along one axis and reflows neighbors — here nothing is pushed) and [theme-crossfade-morph.md](theme-crossfade-morph.md) (a whole-theme reskin under a fixed anchor — here a single container changes shape). ## How It Works -A single GSAP tween animates multiple container properties simultaneously (width / height / border-radius / background). At the same time: +Since `width`/`height` tweens are forbidden, **substitute uniform `scale` for apparent size**; the remaining morph channels are **paint-only**: `borderRadius`, `background`, `boxShadow`. All channels ride ONE tween (one ease, one duration) so the shape morphs in lockstep. Content choreography: old content fades out during the first ~40% of the morph, new content fades in during the last ~40% — the shape-only gap between is the natural "blink." Optionally the morph card itself fades at the very end, revealing the real next-shot element rendered behind it. -1. **Old content** fades out during the first ~40% of the morph -2. **New content** fades in during the last ~40% of the morph -3. **Optional final fade** — the morph container itself fades to 0, revealing the actual next-shot element rendered behind it - -The persistent container provides visual continuity even as content and shape change. - -## HTML +## Recipe ```html -
- -
-
-

{shotOneHeadline}

-

{shotOneSubcopy}

-
-
- logo -
-
- - -
- anchor -
+ + +
anchor
+
+
{shotOneContent}
+
{shotTwoContent}
``` -## CSS (hero-frame layout) - -Card starts as a wide rectangle (shot 1 state). All properties present from the start; only opacities differ: - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; -} - .morph-card { - position: relative; - width: {SHOT_ONE_W}px; - height: {SHOT_ONE_H}px; - border-radius: {SHOT_ONE_RADIUS}px; + width: SHOT_ONE_W; + height: SHOT_ONE_H; /* shot-1 geometry; the morph is scale, never width/height */ + border-radius: SHOT_ONE_RADIUS; background: {surfaceShotOne}; - overflow: hidden; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); + overflow: hidden; /* content must clip during the shape change */ display: grid; place-items: center; + will-change: transform; } - .content-old, .content-new { position: absolute; inset: 0; display: grid; place-items: center; - padding: 32px; -} - -.content-old { - opacity: 1; } .content-new { - opacity: 0; + opacity: 0; /* author its inner sizes at apparent-size ÷ END_SCALE — it scales with the card */ } - .next-shot-anchor { position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - opacity: 0; /* GSAP fades this in as morph card fades out */ - /* Use DOM ORDER for stacking — render .next-shot-anchor BEFORE .morph-card - in markup so the morph card is naturally on top. Do NOT use z-index: -1 - and then snap it positive mid-fade — that causes a visible pop. */ + opacity: 0; /* fades in as the morph card fades out */ } ``` -## GSAP Timeline +```js +const END_SCALE = SHOT_TWO_W / SHOT_ONE_W; // uniform — keep the two shots aspect-matched -```html - - +// Optional handoff — card fades out over the pixel-identical real anchor. +tl.to( + ".morph-card", + { opacity: 0, duration: MORPH_DUR * FINAL_FADE_FRAC, ease: "power1.in", immediateRender: false }, + MORPH_START + MORPH_DUR * (1 - FINAL_FADE_FRAC), +); +tl.to( + ".next-shot-anchor", + { opacity: 1, duration: MORPH_DUR * FINAL_FADE_FRAC, ease: "power1.out" }, + MORPH_START + MORPH_DUR * (1 - FINAL_FADE_FRAC), +); ``` -## Key Properties to Morph +## Morph channels -| Property | Shape of change | Visual effect | -| ------------------ | -------------------------------------------------------------- | ---------------------------- | -| `width` / `height` | `SHOT_ONE_W × SHOT_ONE_H` → `SHOT_TWO_W × SHOT_TWO_H` | wide card shrinks to an icon | -| `borderRadius` | `SHOT_ONE_RADIUS` → `SHOT_TWO_RADIUS` (≤ half of smaller side) | rectangle becomes a circle | -| `background` | `{surfaceShotOne}` → `{surfaceShotTwo}` (solid or gradient) | container identity shifts | -| `boxShadow` | base shadow → accent glow token | emphasis changes | +| channel | how | +| -------------- | ---------------------------------------------------------------------------------------------- | +| apparent size | uniform `scale` — the substitution for the forbidden `width`/`height` tween; aspect preserved | +| `borderRadius` | paint-only; pre-scale units — tween to `APPARENT_RADIUS / END_SCALE`, ≤ half the smaller side | +| `background` | paint-only; gradients interpolate only with equal stop counts (solid→solid: `backgroundColor`) | +| `boxShadow` | paint-only; base shadow → accent glow shifts emphasis | -GSAP tweens all of these simultaneously when included in one `tl.to(...)` call. +## Variations -## How to Choose Values +- **Landing on a non-centered target** (dock icon, sidebar slot): add `x`/`y` to the same tween, computed as the FLIP-style delta between the card's and the target's rects — `getBoundingClientRect()` both at build time (single-scene only, per the contract) and tween the difference. Don't hand-compute from CSS values: paddings, borders, and parent transforms compound, and center-vs-edge arithmetic is the classic off-by-half bug. +- **Aspect change between shots**: uniform scale preserves aspect — morph to the nearest uniform fit and let the crossfade/handoff absorb the small delta, or drop the handoff and hold the card's final state. -- **HOLD_BEAT** — pre-morph dwell so the viewer registers shot 1 before it changes - - Range: 0.6-1.5 s - - Effects: low end feels rushed / glitchy; high end stalls pacing - - Constraints: must be ≥ shot 1's content entry settle time -- **MORPH_START** — when the container morph begins - - Range: equal to `HOLD_BEAT` in the canonical pattern - - Constraints: must be > any shot-1 entry tween end -- **MORPH_DUR** — full length of the simultaneous container morph - - Range: 0.6-1.2 s - - Effects: low end reads as a snap; high end loses momentum - - Constraints: short morphs (<0.5s) cannot fit both old-fade and new-fade -- **SHOT_TWO_W / SHOT_TWO_H** — final container dimensions - - Range: 80-400 px when handing off to an icon-sized anchor - - Constraints: if handing off (`.next-shot-anchor`), MUST match the anchor's dimensions exactly to avoid a visible pop -- **SHOT_TWO_RADIUS** — final corner radius (use to read as circle / pill / soft-rect) - - Range: 0 to `min(SHOT_TWO_W, SHOT_TWO_H) / 2` - - Effects: half-of-smaller-side = perfect circle; smaller = soft rect - - Constraints: > half is visually clamped — wastes the tween -- **OLD_FADE_FRAC** — fraction of `MORPH_DUR` over which shot-1 content fades out, starting at `MORPH_START` - - Range: 0.3-0.5 - - Effects: low end clips shot 1 too early; high end overlaps with shot 2 content - - Constraints: `OLD_FADE_FRAC + NEW_FADE_FRAC ≤ 1` (gap between is the "shape-only" moment) -- **NEW_FADE_FRAC** — fraction of `MORPH_DUR` over which shot-2 content fades in, ending at `MORPH_START + MORPH_DUR` - - Range: 0.3-0.5 - - Effects: symmetric to OLD_FADE_FRAC -- **FINAL_FADE_FRAC** — optional tail fraction during which the morph container itself fades to 0 for handoff - - Range: 0 (no handoff) or 0.1-0.2 - - Constraints: only use when `.next-shot-anchor` matches the morph's final visual exactly -- **Ease family** — discrete choice - - Options: `power2.inOut` (canonical, balanced), `power3.inOut` (snappier), `expo.inOut` (most cinematic but can feel sluggish at low durations) - - Avoid `back.out` / `elastic.out` on the morph itself — overshoot fights the dimensional change +## Values -CSS-side placeholders (`SHOT_ONE_W`, `SHOT_ONE_H`, `SHOT_ONE_RADIUS`, `{surfaceShotOne}`, `{surfaceShotTwo}`) take real values in the example. Pick `{surfaceShotOne}` and `{surfaceShotTwo}` so the gradient/solid stops counts match (GSAP can interpolate background gradients only when stop counts agree). - -## Key Principles - -- **All target properties in one tween** — they share a single ease and duration so they morph in lockstep -- **Old content fades early, new content fades late** — the container shape change happens between, providing a natural "blink" moment -- **Final fade is optional** — use it when the next shot has a real anchor element to hand off to (e.g. avatar that the icon morphed into "is") -- **Same easing for shape and crossfade** — avoid mixing `power2.inOut` morph with `bounce.out` content, looks unsynchronized -- **❗ If you use `.next-shot-anchor` for handoff, its visuals must be pixel-identical to `.morph-card`'s final state** — same `width` / `height`, same `border-radius`, same `background`, same `box-shadow`, same internal icon dimensions. Any visual delta between the two = visible pop during the crossfade. If you can't match exactly, **drop the handoff** and just hold the morph card at its final state (add a breath if needed for life). +| token | range | notes | +| ----------------- | ------------------------- | ------------------------------------------------------------------------------------ | +| HOLD_BEAT | 0.6–1.5s | ≥ shot 1's entry settle; the viewer must register shot 1 first | +| MORPH_DUR | 0.6–1.2s | < 0.5s can't fit both content fades | +| END_SCALE | SHOT_TWO_W / SHOT_ONE_W | icon-sized handoffs typically land at 80–400px apparent width | +| SHOT_TWO_RADIUS | ≤ min(W, H)/2 apparent | half the smaller side = perfect circle; beyond is clamped | +| OLD/NEW_FADE_FRAC | 0.3–0.5 each, sum ≤ 1 | the gap between is the shape-only "blink" | +| FINAL_FADE_FRAC | 0 (no handoff) or 0.1–0.2 | only when a pixel-identical anchor exists | +| ease | `power2.inOut` canonical | `power3`/`expo.inOut` OK; never `back`/`elastic` — overshoot fights the shape change | ## Critical Constraints -- **`overflow: hidden`** on the morph container — content must clip during shape change, otherwise content overflows the morphing border radius -- **Hold a beat before morphing** — let the viewer register shot 1's content before morphing; instant morph reads as glitchy -- **Timeline must be paused**: `gsap.timeline({ paused: true })`. Never `tl.play()` -- **Registry key = `data-composition-id`**: `window.__timelines["morph-scene"]` must match scene root -- **Use `background` tween, not `background-color`**: gradients need `background` (GSAP supports gradient interpolation when targets are gradients with same number of stops). For solid → solid, `backgroundColor` works. -- **`borderRadius` should be ≤ half the smaller dimension** at end state — otherwise the radius is visually clamped and the morph looks abrupt at the boundary -- **❗ Don't snap `z-index` mid-fade** — if you need `.next-shot-anchor` to appear from behind the morph card, use **DOM order** (render `.next-shot-anchor` BEFORE `.morph-card` so the morph card is naturally on top), then crossfade their opacities. A `tl.set({ zIndex: ... })` call during an active opacity tween causes a visible flicker as the stacking order flips before the opacity transition finishes. +- **❗ Uniform-scale substitution** — never tween `width`/`height`; `scale` + the paint-only channels (`borderRadius`, `background`, `boxShadow`) are the ONLY morph properties. +- **❗ Handoff anchor must be pixel-identical to the card's final state** — same apparent size, radius, background, shadow, inner icon dimensions. Any delta = a visible pop during the crossfade. Can't match exactly? Drop the handoff and hold the morph card. +- **❗ Stacking by DOM order, never a z-index snap mid-fade** — render the anchor before the card; a `tl.set({ zIndex })` during an active opacity tween flips stacking before the fade finishes and flickers. +- **`overflow: hidden`** on the card — content must clip as the radius changes. +- **Hold a beat before morphing**; same ease family for shape and crossfade (mixed eases read unsynchronized). -## Variation: Morphing to a target element's position +## See also -When shot 2 isn't centered (e.g. the morph card "lands" on a specific icon in a dock, sidebar, or grid), compute the target `top` / `left` from the **target element's element-position**, not its visual center. Common mistake: subtracting `height/2` to get center, then applying that to the morph-card's `top` — but if `.morph-card` uses absolute positioning with `top` + `margin: 0` (no transform-centering), `top` represents the **element top edge**, not the center. - -Math template (example: morph card lands on icon at bottom dock): - -``` -target_element_top = viewport_height − dock_bottom_offset − dock_padding_y − icon_height - = 1080 − 60 − 22 − 110 = 888 px -``` - -Then tween `.morph-card { top: 888 }` so its element-top aligns with the target icon's element-top. If you mistakenly tween to `888 + icon_height/2 = 943` you'll land below; tweening to a "center" value like `top: 933` (off-by-arithmetic) will be even worse. - -Always **measure the target element with `getBoundingClientRect()`** before the timeline starts, and use those numbers — don't hand-compute from CSS values, since paddings, borders, and parent transforms compound. - -## Combinations - -- [scale-swap-transition.md](scale-swap-transition.md) — simpler morph without dimension change (just scale + content swap) -- [sine-wave-loop.md](sine-wave-loop.md) — gentle breathing on the final state (e.g. final small circular icon idles with a breath) - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + multi-property tween reference -- `/hyperframes-core` — composition wiring, `data-*` attributes -- `/hyperframes-cli` — `hyperframes lint` to verify scene structure +`anchored-layout-expand` (edge-pinned one-axis growth with reflow) · `theme-crossfade-morph` (whole-theme reskin under a fixed anchor) · `scale-swap-transition` (content swap without shape change) · `sine-wave-loop` (a breath on the final state). diff --git a/skills/hyperframes-animation/rules/center-outward-expansion.md b/skills/hyperframes-animation/rules/center-outward-expansion.md index 1c0a291e8..0b6c10638 100644 --- a/skills/hyperframes-animation/rules/center-outward-expansion.md +++ b/skills/hyperframes-animation/rules/center-outward-expansion.md @@ -7,51 +7,24 @@ metadata: # Center-Outward Expansion -Elements begin at a shared center point and radiate outward to their final positions. The expansion can be the entry beat itself, or **driven by another animation's progress** (e.g. a counting number growing) for coordinated motion. +Elements begin at one shared center point and radiate outward to their final positions — the entry beat itself, or motion driven by another animation's progress (a counting number, a beat). Flat 2D cousin of [depth-scatter-assemble.md](depth-scatter-assemble.md) (per-element 3D cloud): here every element shares the SAME origin. ## How It Works -Each element has a `targetX/Y` (its final layout position) and a shared `centerX/Y`. A `progress` value (0→1) interpolates each element between center and target: +Each element carries its final offset as `data-target-x/y`. Its position lerps between center and target: `x = targetX × progress`. Self-centering is baked as `xPercent/yPercent: -50` so the tweened `x`/`y` are pure offsets from the stage center. Standalone burst = per-item staggered `fromTo`; driven burst = one shared proxy (see Variations). -```js -const x = centerX + (targetX - centerX) * progress; -const y = centerY + (targetY - centerY) * progress; -``` - -When `progress = 0` all elements overlap at the center; when `progress = 1` they're at their final spots. - -## HTML +## Recipe ```html -
-
-
{itemA}
-
{itemB}
-
{itemC}
-
{itemD}
-
{itemE}
-
{itemF}
-
+ +
+
{itemA}
+
{itemB}
+
{itemC}
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; -} .burst-wrap { position: relative; width: 100%; @@ -61,167 +34,55 @@ When `progress = 0` all elements overlap at the center; when `progress = 1` they } .burst-item { position: absolute; - /* Items start at the wrap center via the absolute + 50% trick. - We tween translate offsets via GSAP, not left/top. */ top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - width: {itemSize}; - height: {itemSize}; - display: grid; - place-items: center; - background: {itemBgColor}; - border-radius: 28px; - font-family: {font}; - font-weight: 900; - font-size: 96px; - color: {textColor}; + left: 50%; /* GSAP xPercent/yPercent -50 bakes the centering; x/y tween the offset */ will-change: transform; } ``` -## GSAP Timeline - -```html - - +```js +document.querySelectorAll(".burst-item").forEach((el, i) => { + tl.fromTo( + el, + { xPercent: -50, yPercent: -50, x: 0, y: 0, scale: 0.6, opacity: 0 }, + { + x: Number(el.dataset.targetX), + y: Number(el.dataset.targetY), + scale: 1, + opacity: 1, + duration: EXPAND_DUR, + ease: EXPAND_EASE, + }, + ENTRY_AT + i * STAGGER, + ); +}); ``` -## How to Choose Values - -- **ITEM_COUNT** — number of elements in the burst - - Range: 3–8 - - Effects: 3 = sparse; 8 = busy. > 8 causes visual chaos where cards overlap mid-expansion - - Constraints: at low counts, prefer wider angular spread (target positions further apart) - -- **EXPAND_DUR** — duration of each item's center → target tween - - Range: 1.0–1.8 s - - Effects: shorter = snappy burst; longer = floats outward - - Constraints: if driven by a counter, must equal the counter's duration (chord) - -- **EXPAND_EASE** — shared ease across all items - - Discrete choice: `power2.out`, `power3.out`, `expo.out` - - Selection: `power3.out` is the default — fling out then settle. `power2.out` is gentler. `expo.out` makes them stop dramatically. Avoid `in` easings (they read as items being sucked back in mid-air). - - Constraint: if driven by another animation, must be identical to the driver's ease - -- **STAGGER** — gap between successive items' start times - - Range: 0.04–0.08 s - - Effects: < 0.04 = simultaneous chord; > 0.08 feels lazy / arpeggiated - - Constraints: ITEM_COUNT × STAGGER must be < EXPAND_DUR or the last items still moving when others have landed reads as ragged - -- **ENTRY_AT** — offset applied to the whole burst start - - Range: 0 – 0.5 s - - Effects: > 0 gives a beat of compositional quiet before the burst - -- **START_PROGRESS** — fraction of the center→target path where items begin (for partially-spread variant) - - Range: 0 (exact center) – 0.5 - - Effects: 0 = full cluster, dramatic spread; 0.3 = avoids initial pile-up at center - ## Variations -### Synced expansion (driven by a counter) +- **Synced to a driver (chord)**: when the burst shadows a counter / beat, drop the stagger and drive all items from ONE 0→1 proxy tween with the driver's exact duration AND ease; `onUpdate` writes `translate(-50%,-50%) translate(targetX*p, targetY*p)` per item — the two read as one beat. +- **Partially-spread start**: with 6+ items the full cluster piles up — start from `{ x: targetX * START_PROGRESS, ... }`. +- **Idle micro-float**: hand off to [sine-wave-loop.md](sine-wave-loop.md) after landing instead of freezing. -If the burst should mirror a counting animation's progress: +## Values -```js -// Counter tween defines a state.value 0 → TARGET over COUNT_DUR -const counterState = { value: 0 }; -const burstState = { p: 0 }; - -// Shared tween — same duration, same ease — visually a "chord" -tl.to( - counterState, - { - value: COUNT_TARGET, - duration: COUNT_DUR, - ease: COUNT_EASE, - onUpdate: () => (counterEl.textContent = Math.round(counterState.value).toLocaleString()), - }, - 0, -); - -tl.to( - burstState, - { - p: 1, - duration: COUNT_DUR, - ease: COUNT_EASE, - onUpdate: () => - items.forEach((el) => { - const tx = Number(el.dataset.targetX) * burstState.p; - const ty = Number(el.dataset.targetY) * burstState.p; - el.style.transform = `translate(-50%, -50%) translate(${tx}px, ${ty}px)`; - }), - }, - 0, -); -``` - -### Starting partially-spread - -To avoid the initial clustered mess (6+ elements stacked at center), start at `START_PROGRESS`: - -```js -{ x: targetX * START_PROGRESS, y: targetY * START_PROGRESS, scale: 0.4, opacity: 0 } -``` - -### Idle micro-float at final position - -Pair with `sine-wave-loop` after expansion lands — keeps elements alive instead of frozen. - -## Key Principles - -- **Driver vs driven** — if the burst stands on its own, use a per-item stagger; if it shadows another animation (counter, audio beat), share the same eased progress so they read as one beat -- **Stagger inside the 0.04-0.08 s band** — too tight and the cluster never separates visually, too loose and the burst feels lazy -- **Out-easing for the expansion** — out-easing makes items "fling" out then settle. In-easing looks like they're sucked back in mid-air -- **Element count: 3-8** — fewer feels empty, more causes visual chaos at the center where cards overlap mid-expansion -- **❗ Don't put a label below the burst as the "real headline"** — if you do, the eye snaps to the label and ignores the burst. The burst IS the beat. If a label is needed, use big block-caps and reveal it post-burst, in the same stacked layout. +| token | range | notes | +| -------------- | -------------------- | ---------------------------------------------------------------- | +| ITEM_COUNT | 3–8 | > 8 = visual chaos mid-expansion; low counts want wider spread | +| EXPAND_DUR | 1.0–1.8s | must equal the driver's duration in the synced variant | +| EXPAND_EASE | `power3.out` default | `power2.out` gentler, `expo.out` dramatic stop; NEVER `in` eases | +| STAGGER | 0.04–0.08s | tighter = chord; looser = lazy arpeggio | +| ENTRY_AT | 0–0.5s | a beat of compositional quiet before the burst | +| START_PROGRESS | 0–0.5 | 0 = dramatic full cluster; ~0.3 avoids the pile-up | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Use translate, not left/top** — translating composes cleanly with the centering `translate(-50%, -50%)` trick; mutating `left`/`top` fights the centering and causes pixel jitter -- **`will-change: transform`** on burst items — many simultaneous transforms benefit from compositor hints -- **No `position: absolute` parents inside `burst-wrap` other than items themselves** — sibling absolute elements would steal the centered baseline +- **Tween `x`/`y` over the baked `xPercent/yPercent: -50`** — mutating `left`/`top` fights the centering and causes pixel jitter. +- **Out-easing only** — `in` easings read as items being sucked back mid-air. +- **No other absolute-positioned siblings inside `.burst-wrap`** — they'd steal the centered baseline. +- **❗ The burst IS the beat** — don't park a "real headline" label below it (the eye snaps to the label and ignores the burst). If a label is needed, reveal it post-burst in the same stack. +- Synced variant: identical duration + ease as the driver, or the chord falls apart. -## Combinations +## See also -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — counter peak drives the burst peak (chord) -- [sine-wave-loop.md](sine-wave-loop.md) — idle motion after the burst lands -- [card-morph-anchor.md](card-morph-anchor.md) — burst out of a morphed card - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + stagger -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`counting-dynamic-scale` (the classic chord driver) · `depth-scatter-assemble` (3D per-element cloud) · `card-morph-anchor` (burst out of a morphed card) · `sine-wave-loop` (post-landing life). diff --git a/skills/hyperframes-animation/rules/chart-scrub-readout.md b/skills/hyperframes-animation/rules/chart-scrub-readout.md new file mode 100644 index 000000000..527878f17 --- /dev/null +++ b/skills/hyperframes-animation/rules/chart-scrub-readout.md @@ -0,0 +1,151 @@ +--- +name: chart-scrub-readout +description: A cursor/playhead scrubs an already-drawn chart — one driver moves a vertical tracking line and marker along a baked data polyline while a date/value tooltip steps through the data array; a second series can activate on cross. Deterministic data, readout writes only on index change. +metadata: + tags: chart, scrub, readout, tooltip, tracking-line, data, cursor, playhead +--- + +# Chart Scrub Readout + +The chart is already ON screen — this rule **interrogates** it. A vertical tracking line rides the scrub position, a marker dot follows the series, and a live tooltip reads out `date: value` per position, values flickering past like an odometer. It's the "this data is real — look closer" beat: the scrub proves the chart is an instrument, not a picture. + +Boundary with its neighbors: [stat-bars-and-fills.md](stat-bars-and-fills.md) owns the chart's ARRIVAL; [counting-dynamic-scale.md](counting-dynamic-scale.md) owns a single number swelling in place. This rule assumes the graphic already exists and adds a **read head** moving across it. The three chain naturally: the line draws in (svg-path-draw / stat-bars), this rule scrubs it, and the landing value hands off to a count-up lockup. + +## How It Works + +1. **Data baked at setup** — a literal `DATA` array of `{ d, v }` points (or a pure index formula). The polyline's `points` attribute is computed ONCE from `DATA` by pure mapping functions: chart and readout share one source of truth. The argument of the shot is "this data is real" — a random walk regenerated per render breaks both determinism and the rhetorical claim. +2. **One driver tween** `p: 0 → 1` derives everything in its `onUpdate`: tracking-line x, marker x/y, tooltip position. Every output is a pure function of `p` — any seek lands the identical frame. Parallel tweens that merely share timing drift apart under rounding and read as chart chrome, not a read head. +3. **The marker rides the polyline** — its y interpolates between the two neighboring baked points, from the same arrays that built the chart; a separately-keyframed marker inevitably floats off the line. +4. **The readout is threshold-stepped** — the nearest data index derives from `p`, and `textContent` is written ONLY when that index changes (last-index guard). Transforms glide per frame (compositor-cheap); text steps per data point — no per-frame DOM text thrash. The guard is an optimization, not state: any seek recomputes the same index and the same text. + +## Recipe + +```html + +
+ + + + + + +
+ {firstDate} + {firstValue} +
+
+``` + +```css +.tooltip { + position: absolute; + top: 0; + left: 0; + min-width: TIP_MIN_WIDTH; /* fixed — the box must not resize as values change length */ +} +#tip-value { + font-variant-numeric: tabular-nums; /* MANDATORY — digits flicker past; widths must not */ +} +``` + +```js +// Data baked at setup — literal values. +const DATA = [ + { d: "{date1}", v: V1 }, + // ... N points, chronological ... +]; + +// Pure mapping functions — geometry derives from DATA once. +const PAD = CHART_PAD; +const PLOT_W = CHART_W - PAD * 2; +const PLOT_H = CHART_H - PAD * 2; +const vals = DATA.map((p) => p.v); +const V_MIN = Math.min(...vals); +const V_MAX = Math.max(...vals); +const X = (i) => PAD + (i / (DATA.length - 1)) * PLOT_W; +const Y = (v) => PAD + PLOT_H * (1 - (v - V_MIN) / (V_MAX - V_MIN)); + +document + .getElementById("series-a") + .setAttribute("points", DATA.map((p, i) => `${X(i)},${Y(p.v)}`).join(" ")); + +const line = document.getElementById("track-line"); +const marker = document.getElementById("marker"); +const tooltip = document.getElementById("tooltip"); +const tipDate = document.getElementById("tip-date"); +const tipValue = document.getElementById("tip-value"); + +// Tooltip pops in as the scrub begins — a small fromTo scale/opacity spring at SCRUB_AT. + +// ONE driver — line, marker, and tooltip are all projections of p. +const scrub = { p: 0 }; +let lastIdx = -1; +tl.to( + scrub, + { + p: 1, + duration: SCRUB_DUR, + ease: SCRUB_EASE, + onUpdate: () => { + const f = scrub.p * (DATA.length - 1); // fractional index + const i = Math.min(DATA.length - 2, Math.floor(f)); + const t = f - i; + const x = X(i) + (X(i + 1) - X(i)) * t; + const y = Y(DATA[i].v) + (Y(DATA[i + 1].v) - Y(DATA[i].v)) * t; + + // Transforms glide every frame (cheap, deterministic) + line.setAttribute("x1", x); + line.setAttribute("x2", x); + marker.setAttribute("cx", x); + marker.setAttribute("cy", y); + tooltip.style.transform = `translate(${x + TIP_DX}px, ${y - TIP_DY}px)`; + + // Text steps only when the nearest data point changes + const idx = Math.round(f); + if (idx !== lastIdx) { + tipDate.textContent = DATA[idx].d; + tipValue.textContent = `${DATA[idx].v.toLocaleString()} {unitLabel}`; + lastIdx = idx; + } + }, + }, + SCRUB_AT, +); +// End hold: the driver finishes before the scene does — the landed value reads. +``` + +## Variations + +- **Peak stop** — the scrub is the wind-up, the landing is the stat: `SCRUB_EASE: "power3.out"` decelerates onto the final/peak point, then pop the emphasis at landing (`fromTo` marker `scale: 1 → PEAK_POP_SCALE` at `SCRUB_AT + SCRUB_DUR`). Pair with a pill tooltip that springs to its final label ([spring-pop-entrance.md](spring-pop-entrance.md)) — the classic "line breaks above the band" climax. +- **Second-series activation on cross** — series B sits dimmed; at `SCRUB_AT + SCRUB_DUR * CROSS_P` tween its stroke to the lit color (0.25s, `power2.out`), and in the driver's `onUpdate` read from B's array once `scrub.p ≥ CROSS_P` (still index-guarded). The color flip lands ON the cross — same-frame causality. +- **Two-chart glide** — two scrub beats: sweep chart A, glide the cursor/tooltip group across the gutter (a plain `x` tween, no readout — dead travel, not data), then chart B activates with its own driver. One driver per chart. +- **Cursor-led scrub** — an oversized cursor is the visible actor: another projection of the SAME driver (positioned from `x` in the same `onUpdate`, tip at the tracking line's head) — never a second tween that merely matches timing. Cursor look and click grammar from [cursor-click-ripple.md](cursor-click-ripple.md). +- **Playhead form** — no cursor; the tracking line IS the actor (timeline scrubbers, audio waves, session replays). `ease: "none"` — mechanical playback, not a hand. + +## Values + +| token | range / default | notes | +| ----------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| N (data points) | 10–40 | <10 reads as a slideshow; >40 blurs into texture. The flicker is the point — only first and final values must be legible | +| SCRUB_DUR | 1.5–3s | shorter = confident sweep; longer = inspection. Leave ≥0.8s of scene after the driver ends so the landed value holds | +| SCRUB_EASE | `power1.inOut` default | `"none"` playhead form; `power3.out` peak stop. Never `back.out` — a read head that overshoots and re-reads looks broken | +| CROSS_P | 0.55–0.75 | earlier and A never establishes; later and B's readout has no time to live | +| TIP_DX / TIP_DY | 16–48px, up-and-right | flip the sign near the chart's right edge so the tooltip never exits the frame | +| MARKER_R / stroke width | r 6–12 / 4–8px | the marker must dominate the line it rides | +| TIP_MIN_WIDTH | ≥ longest `date: value` state | without it the box breathes as digits change | + +## Critical Constraints + +- **`DATA` is literal at setup**; polyline points derive from it via pure functions — chart and readout share one source of truth. +- **Seed at setup** — call the scrub applier once with `p = 0` right after building (à la `3d-camera-flight`'s `applyCamera()`), or a seek to t=0 before the driver runs shows the tracking line/marker at their HTML-default positions. +- **Single driver** — one `p` tween; all scrub outputs (line, marker, tooltip, any cursor) computed in its `onUpdate`, each a pure function of `p`. +- **Readout writes guarded by index change** — `onUpdate` stays O(1): a few attribute sets, one transform, text only on step. +- **SVG `viewBox` units = CSS pixels** (`viewBox="0 0 W H"` with matching `width`/`height`) — one coordinate space must serve the SVG internals and the HTML tooltip's transform. +- **`tabular-nums` + fixed `min-width`** on the tooltip value. +- **The chart pre-exists** — draw-in belongs to `svg-path-draw` / `stat-bars-and-fills`; sequence it BEFORE the scrub, don't blend them. +- **Land the read** — hold the final value ≥0.8s (or hand off to a count-up lockup). + +## See also + +`svg-path-draw` (the series draws in first) · `stat-bars-and-fills` (surrounding dashboard chrome) · `spring-pop-entrance` (peak dot + pill pop at the landing) · `counting-dynamic-scale` (closing stat lockup) · `cursor-click-ripple` / `context-sensitive-cursor` (the cursor-led form's actor) · `control-target-sync` (the sibling WRITE direction — there a control edits a target; here a scrub reads a dataset). diff --git a/skills/hyperframes-animation/rules/chromatic-glitch.md b/skills/hyperframes-animation/rules/chromatic-glitch.md new file mode 100644 index 000000000..c35713a92 --- /dev/null +++ b/skills/hyperframes-animation/rules/chromatic-glitch.md @@ -0,0 +1,150 @@ +--- +name: chromatic-glitch +description: RGB-split / slice glitch that snaps sharp — offset color copies jitter on a deterministic hash of quantized timeline time (never Math.random), or horizontal slices displace and converge; a brief vibration, then a clean resolve. Entrance or emphasis punctuation; finite, seek-safe. +metadata: + tags: glitch, rgb-split, chromatic, slice, jitter, stutter, text, snap, distortion +--- + +# Chromatic Glitch + +Digital interference as punctuation: for a fraction of a second the element **breaks** — offset color copies shudder behind it, or horizontal slices displace sideways — then it **snaps sharp** and holds clean. The payoff is the resolve; the glitch exists to make the clean state land harder. Two forms: an **RGB-split jitter** (warm + cool ghost copies vibrating behind the base) and a **slice displacement** (horizontal bands that arrive offset and converge). + +Boundaries: [motion-blur-streak.md](motion-blur-streak.md) is velocity blur tied to **travel** — its element is going somewhere fast. A glitching element is **in place**; the disturbance is temporal, not directional. [hacker-flip-3d.md](hacker-flip-3d.md) substitutes **glyphs** (a decode); here the glyphs are fixed and only displaced copies of them move. + +## How It Works + +The subject is stacked: the **base copy on top** (full legibility at every frame), ghost copies behind. All motion comes from one finite **amplitude-envelope** tween read by an `onUpdate`: + +1. **Quantized time** — `const step = Math.floor(tl.time() / JITTER_STEP)`. The stutter comes from offsets that hold for `JITTER_STEP` and then jump. Smoothly interpolated offsets read as wobble, not glitch — **the quantization IS the digital texture**. +2. **Deterministic hash** — offsets are a pure function of `(step, layerIndex)`: + + ```js + const glitchHash = (n) => { + const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; + return x - Math.floor(x); // 0..1, pure — a scrub to any t recomputes the same frame + }; + ``` + +3. **Amplitude envelope** — a proxy tween carries `amp: 1 → 0` over `GLITCH_DUR`. Per-frame offset = `amp × (glitchHash(step * 13 + layer * 7) * 2 − 1) × MAX_SPLIT`. When the envelope hits zero the copies sit at exactly 0 — the snap-sharp is built into the math, and a final `tl.set` clamps the rest state so the hold is bit-exact. + +The **slice form** swaps color copies for `SLICE_COUNT` full copies, each clipped to a horizontal band via `clip-path: inset()`; per-band `x` (and optional `scaleX` stretch) start at hash-derived offsets and converge to 0 under a stepped ease. + +## Recipe + +```html + + +
+ + + {glitchText} +
+``` + +```css +.glitch-stack { + display: grid; /* all copies share one cell — pixel-identical boxes */ +} +.glitch-base, +.glitch-copy { + grid-area: 1 / 1; +} +.glitch-base { + z-index: 2; /* grid items take z-index without position */ + color: {textColor}; +} +.glitch-copy { + z-index: 1; + opacity: 0; /* raised only while the envelope is live */ + will-change: transform; /* updates every frame while live */ + mix-blend-mode: screen; /* additive on dark bg; drop to normal (and lower opacity) on light */ +} +.glitch-copy.warm { + color: {warmSplit}; /* classic: red/orange */ +} +.glitch-copy.cool { + color: {coolSplit}; /* classic: cyan/blue */ +} +``` + +```js +// Form A: RGB-split jitter — envelope snaps to full amplitude, decays to zero. +// All per-frame state derives from tl.time() + the envelope: pure, replays on seek. +const copies = gsap.utils.toArray("#glitch-stack .glitch-copy"); +const amp = { a: 0 }; +tl.set(amp, { a: 1 }, GLITCH_START); +tl.set(copies, { opacity: SPLIT_OPACITY }, GLITCH_START); +tl.to( + amp, + { + a: 0, + duration: GLITCH_DUR, + ease: "power3.in", // most of the violence up front, dying fast + onUpdate: () => { + const step = Math.floor(tl.time() / JITTER_STEP); // quantized — the stutter + copies.forEach((el, layer) => { + const jx = (glitchHash(step * 13 + layer * 7) * 2 - 1) * MAX_SPLIT * amp.a; + const jy = (glitchHash(step * 29 + layer * 11) * 2 - 1) * MAX_SPLIT * 0.35 * amp.a; + gsap.set(el, { x: jx, y: jy }); + }); + }, + }, + GLITCH_START, +); +// The clean resolve: clamp ghosts to exact rest — never rely on the decay +// landing on zero. A ghost left 1px off reads as a bug every frame after. +tl.set(copies, { x: 0, y: 0, opacity: 0 }, GLITCH_START + GLITCH_DUR); + +// Form B: slice displacement — N band copies of the same content converge. +const slices = gsap.utils.toArray("#slice-stack .slice"); +const bandH = 100 / slices.length; +slices.forEach((el, i) => { + gsap.set(el, { clipPath: `inset(${i * bandH}% 0 ${100 - (i + 1) * bandH}% 0)` }); + const dir = glitchHash(i * 3 + 1) > 0.5 ? 1 : -1; + tl.fromTo( + el, + { + x: dir * (SLICE_OFFSET_MIN + glitchHash(i * 5 + 2) * (SLICE_OFFSET_MAX - SLICE_OFFSET_MIN)), + scaleX: 1 + glitchHash(i * 7 + 3) * SLICE_STRETCH, + opacity: 1, + }, + { x: 0, scaleX: 1, duration: SLICE_RESOLVE_DUR, ease: "steps(SLICE_STEPS)" }, + SLICE_START + glitchHash(i * 11 + 4) * SLICE_JITTER_LAG, + ); +}); +``` + +## Variations + +- **Glitch-stretch entrance** — the element ENTERS glitching: layer `fromTo(stack, { scaleX: STRETCH_FROM, opacity: 0 }, { scaleX: 1, opacity: 1, duration: GLITCH_DUR, ease: "power4.out" })` (`STRETCH_FROM` 1.3–1.8) on the whole stack while the envelope runs. Stretch, split, and envelope all die at the same frame — the word is simply _there_, sharp. +- **Emphasis burst on a held word** — a spasm, not an arrival: 2–3 short envelopes (`GLITCH_DUR` ~0.12–0.2s each) separated by clean gaps of ~0.2–0.4s, each its own `set(amp)/to(amp)/set(rest)` triplet. The clean frames between bursts make it read as energy instead of a rendering fault. +- **Slice reveal** — Form B as the arrival itself: bands start opaque but displaced, converge under the stepped ease. Drop the color copies for the monochrome version — the restrained enterprise read of this rule. +- **Card / non-text glitch** — the stacked-copy machinery is content-agnostic (logo lockup, small card). Keep `MAX_SPLIT` proportional (~1% of element width) — oversized splits read as broken layout, not interference. + +## Values + +| token | range | notes | +| -------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- | +| MAX_SPLIT | 4–14px at headline sizes (~0.06–0.1em) | vertical ~35% of horizontal; base must stay legible at peak | +| JITTER_STEP | 1/30–1/12 s | shorter = frantic buzz, longer = VHS stutter; **≥ one render frame** or quantization vanishes | +| GLITCH_DUR | 0.25–0.6s entrance; 0.12–0.2s burst | ≥ ~1s stops reading as an event and starts reading as a broken render | +| SPLIT_OPACITY | 0.5–0.9 (screen on dark) | 0.35–0.6 unblended on light — screen on white is invisible | +| SLICE_COUNT | 4–10 | more = finer tear, diminishing past ~10 | +| SLICE_OFFSET_MIN / MAX | 12–60px | derive per-band values from `glitchHash(i)`, never uniform — equal offsets read mechanical | +| SLICE_STRETCH | 0–0.5 | 0 pure displacement; ~0.3 stretched-scanline read | +| SLICE_RESOLVE_DUR / SLICE_STEPS / JITTER_LAG | 0.2–0.4s / 3–6 / ≤0.08s per band | the stepped ease keeps the settle digital | +| {warmSplit} / {coolSplit} | — | classic red/cyan; any opposing warm+cool brand pair survives | + +## Critical Constraints + +- **Quantize time — the stutter IS the effect.** Offsets hold for `JITTER_STEP` then jump; if the glitch looks like jelly, you interpolated. `JITTER_STEP` ≥ one render frame or the quantization silently disappears. +- **Pure functions of (quantized time, index)** — every per-frame value comes from `glitchHash`; the hash inputs use `tl.time()`, nothing else. +- **Clamp the rest state** — `tl.set({ x: 0, y: 0, opacity: 0 })` on the ghosts at envelope end; never rely on the decay landing exactly on zero. +- **Base on top, always legible** — ghosts vibrate _behind_ the base; a glitch that destroys legibility for more than ~2 frames is a tear-down, not an accent. +- **Brief, then clean** — the clean hold after the snap is the actual beat; `GLITCH_DUR` well under half the element's screen time. Emphasis bursts are separate finite triplets. +- **No CSS `@keyframes` glitch loops** — the classic CSS glitch snippet runs on the wall clock and desyncs from seek; every displacement goes through the timeline's `onUpdate`. +- **Match the register** — RGB-split is a loud consumer/tech gesture; the monochrome slice variant is the only form that belongs in a restrained enterprise composition. + +## See also + +`kinetic-beat-slam` (one beat lands with the glitch-stretch entrance) · `spring-pop-entrance` (pop clean, burst on the stress beat) · `gradient-text-sweep` (gradient carries the hold after the resolve) · `discrete-text-sequence` (state swap masked at max amplitude) · `motion-blur-streak` (the traveling sibling — if it's moving fast, blur it there). diff --git a/skills/hyperframes-animation/rules/context-sensitive-cursor.md b/skills/hyperframes-animation/rules/context-sensitive-cursor.md index 5a2625b33..a75f6da54 100644 --- a/skills/hyperframes-animation/rules/context-sensitive-cursor.md +++ b/skills/hyperframes-animation/rules/context-sensitive-cursor.md @@ -7,251 +7,134 @@ metadata: # Context-Sensitive Cursor -In a typewriter sequence, the cursor's color (and optionally height/blink rate) matches the **active text segment**. If the typewriter is currently typing a brand name, the cursor is the brand accent color; on a placeholder, it dims to gray. Enhances visual cohesion vs a single fixed cursor color across all text states. +In a typewriter sequence, the cursor's color (and optionally height / blink behavior) matches the **active text segment** — brand accent while typing the brand name, dim on placeholders, success color on the completion mark. The eye lands on the keyword being typed because the cursor shifts with it; a fixed single-color cursor is visual noise by comparison. Layers on top of [discrete-text-sequence](discrete-text-sequence.md)'s SEQUENCE pattern. ## How It Works -The text is authored as a SEQUENCE of `{text, t, segment}` entries where `segment` is a string identifier ('main' / 'highlight' / 'brand' / 'success'). The driver tween's onUpdate determines the current segment based on `time`, then sets the cursor's CSS color (and optionally other props) to match that segment's palette. +The text is authored as a SEQUENCE of `{ t, text, segment, color }` entries; a linear driver's `onUpdate` reverse-searches for the current entry and writes both the visible text and the cursor's `background` (the cursor is a colored block, so `background`, NOT `color`). A second linear tween sweeps a phase `p` through `2π × BLINK_CYCLES_PER_SCENE` and gates cursor opacity on `sin(p) > 0` — a deterministic square-wave blink on the timeline. -## HTML +## Recipe ```html -
-
-
$
-
- _ -
+ +
+
$
+
+ _
``` -## CSS - -Placeholders: `{monoFont}` is the project's monospace stack (proportional fonts cause cursor drift mid-segment); `{bgColor}` is the dark backdrop; `{textColor}` is the readable foreground; `{promptColor}` is the segment-default color for the leading prompt glyph. - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; - font-family: {monoFont}; -} .terminal { + font-family: {monoFont}; /* proportional fonts drift the cursor mid-segment */ display: flex; align-items: baseline; - gap: 24px; - font-size: 72px; - font-weight: 800; - color: {textColor}; - white-space: pre; -} -.prompt { - color: {promptColor}; -} -.text-wrap { - display: inline-flex; - align-items: baseline; - min-width: 1200px; + white-space: pre; /* preserve trailing spaces — cursor sits at segment end */ } .text { - color: {textColor}; white-space: pre; } -/* Cursor highlights based on active segment via per-frame background swap */ .cursor { - display: inline-block; + display: inline-block; /* inline ignores width/height */ width: {cursorWidth}px; height: {cursorHeight}px; background: {textColor}; /* default — overridden per segment in onUpdate */ - margin-left: {cursorGap}px; - vertical-align: {cursorBaselineFix}px; + vertical-align: {cursorBaselineFix}px; /* small negative — anchor to baseline, not line-height */ } ``` -## GSAP Timeline +```js +// Adjacent entries usually share a text prefix but may differ in `segment` — +// that's what shifts the cursor color mid-line. +const SEQUENCE = [ + { t: 0, text: "", segment: "main", color: "{mainColor}" }, + { t: T_LEADIN_END, text: "{leadInChunk}", segment: "main", color: "{mainColor}" }, + { t: T_BRAND_IN, text: "{leadInBrandPrefix}", segment: "brand", color: "{brandColor}" }, + { t: T_BRAND_OUT, text: "{leadInBrandFull}", segment: "main", color: "{mainColor}" }, + { t: T_CMD_IN, text: "{leadInCmdPrefix}", segment: "cmd", color: "{cmdColor}" }, + { t: T_SUCCESS, text: "{leadInDone}", segment: "success", color: "{successColor}" }, +]; -```html - - + }, + 0, +); ``` ## Variations -### Non-blinking during active typing - -When letters are being added (driver moved forward in the last `TYPING_GRACE` seconds), suppress blink — cursor stays solid. When no typing activity (`driver.t - lastChangeTime > TYPING_GRACE`), resume blink. +- **Non-blinking during active typing** — suppress blink while letters are appearing (solid cursor), resume on idle. This MUST be a pure function of the driver's time: tracking a mutable `lastChangeTime` in `onUpdate` is not reverse-seek-safe (scrubbing backwards leaves the stale forward-pass value behind and the cursor blinks — or holds solid — at the wrong frames). Bake the change times from the SEQUENCE instead — every entry whose `text` differs from its predecessor is a typing event: ```js -let lastChangeTime = 0, - lastText = ""; -// In onUpdate: -if (entry.text !== lastText) { - lastChangeTime = driver.t; - lastText = entry.text; -} -const isTyping = driver.t - lastChangeTime < TYPING_GRACE; +// Baked once at build time — no runtime state. +const CHANGE_TIMES = SEQUENCE.filter((e, i) => i > 0 && e.text !== SEQUENCE[i - 1].text).map( + (e) => e.t, +); +// In onUpdate — identical result at any seek, either direction: +const isTyping = CHANGE_TIMES.some((t) => t <= driver.t && driver.t - t < TYPING_GRACE); cursorEl.style.opacity = isTyping ? "1" : Math.sin(blink.p) > 0 ? "1" : "0"; ``` -### Cursor HEIGHT shifts on segment +- **Cursor HEIGHT shifts on segment** — larger cursor on the brand segment: `cursorEl.style.height = entry.segment === "brand" ? cursorHeightEmphasis : cursorHeight` (1.1–1.25×; more reads as glitch). +- **Contrast reversal** — a dark-text-on-light segment needs a dark cursor too; keep `entry.color` as the single source of truth and read from it. -Larger cursor on brand segment for emphasis (`cursorHeightEmphasis > cursorHeight`): +## Values -```js -cursorEl.style.height = - entry.segment === "brand" ? `${cursorHeightEmphasis}px` : `${cursorHeight}px`; -``` - -### Cursor reverses contrast on dark text - -If a segment is rendered DARK text on light bg, cursor should swap to dark too. Manage via `entry.color` as the SOURCE OF TRUTH and read from there. - -## Key Principles - -- **Cursor color shifts make brand moments POP** — eye lands on the brand name because the cursor color shifts to brand accent. Without it, cursor is visual noise. -- **`background` property on the cursor div** — NOT `color` (cursor is a colored block, not a glyph) -- **Deterministic blink via sin** — never CSS `@keyframes blink`. HF seek will desync. -- **Cursor `display: inline-block`** — `display: inline` ignores width/height. -- **`vertical-align: -8px`** (or similar) — visually anchor cursor to text baseline, not full line-height. -- **`white-space: pre`** on text and parent — preserve trailing spaces so cursor sits at end of segment, not after collapsed space. -- **Color palette aligned with brand system** — 3-4 colors max for segments (main / brand / cmd / success). More and the segmentation reads as random. - -## How to Choose Values - -- **DURATION** — total scene length in seconds - - Range: 4-8 s for a single typed line; longer if the line is long - - Effects: too short truncates the typing; too long leaves a dead tail after the success state - - Constraints: must be `≥ SEQUENCE[last].t + (closing dwell)` - - Reference: see the corresponding blueprint's example HTML - -- **SEQUENCE entry `t` values** — absolute seconds where each new visible text + segment kicks in - - Range: monotonically increasing; spacing 0.2-0.5 s between micro-additions (per-word or per-token), longer between segment swaps - - Effects: too-tight spacing collapses the typing feel into a slideshow; too-loose drags - - Constraints: ordered ascending; entries do not need uniform spacing — slow down on highlights - - Reference: see the corresponding blueprint's example HTML - -- **Segment palette: mainColor / brandColor / cmdColor / successColor** — the cursor-fill swatches - - Range: 3-4 discrete colors max; each should be distinguishable at small cursor width - - Effects: too many segments and the swaps read as random; too few and the brand moment loses pop - - Constraints: `brandColor` and `successColor` may be similar in hue but should differ in saturation/luminance so a brand→success transition is visible - - Reference: see the corresponding blueprint's example HTML - -- **cursorWidth / cursorHeight / cursorGap / cursorBaselineFix** — cursor block geometry - - Range: cursorWidth 8-24 px; cursorHeight ≈ 0.85-1.0 × fontSize; cursorGap 4-12 px; cursorBaselineFix small negative number to drop below the baseline - - Effects: too-thin cursor disappears in render compression; too-tall cursor visually outranks the text - - Constraints: must use `display: inline-block` (a `width` on `display: inline` is ignored) - - Reference: see the corresponding blueprint's example HTML - -- **cursorHeightEmphasis** (Variations) — height when the active segment is the brand - - Range: 1.1-1.25 × `cursorHeight` - - Effects: subtle bump reads as emphasis; large bump reads as glitch - - Constraints: `cursorHeightEmphasis > cursorHeight` - - Reference: see the corresponding blueprint's example HTML - -- **BLINK_CYCLES_PER_SCENE** — how many full blink cycles span `DURATION` - - Range: choose so the period `DURATION / BLINK_CYCLES_PER_SCENE` ≈ 0.6-1.2 s; e.g. an 8-second scene with ~1 s period uses BLINK_CYCLES_PER_SCENE = 8 - - Effects: short period (many cycles) reads as glitchy / agitated; long period reads as terminal-idle - - Constraints: must be a whole number when `DURATION` is fixed — the sin sweep ends mid-cycle otherwise and the cursor pops on the last frame - - Reference: see the corresponding blueprint's example HTML - -- **TYPING_GRACE** (Variations) — seconds after a text change during which blink is suppressed - - Range: 0.15-0.3 s - - Effects: low end still blinks while letters are still appearing; high end keeps cursor solid through long holds - - Constraints: must be smaller than the shortest dwell between two adjacent SEQUENCE entries — otherwise the cursor never blinks - - Reference: see the corresponding blueprint's example HTML +| token | range | notes | +| ---------------------- | --------------------------- | ----------------------------------------------------------------------------------------------- | +| DURATION | 4–8s per typed line | `≥ SEQUENCE[last].t + closing dwell` | +| entry `t` spacing | 0.2–0.5s micro-additions | ascending, non-uniform — slow down on highlights | +| segment palette | 3–4 colors max | more reads as random; brand vs success should differ in saturation/luminance | +| cursorWidth / Height | 8–24px / 0.85–1.0× fontSize | too thin vanishes in render compression; too tall outranks the text | +| cursorBaselineFix | small negative px | drop the block to the text baseline | +| BLINK_CYCLES_PER_SCENE | period ≈ 0.6–1.2s | **whole number** — otherwise the sin sweep ends mid-cycle and the cursor pops on the last frame | +| TYPING_GRACE | 0.15–0.3s | **< shortest dwell between adjacent entries** — otherwise the cursor never blinks | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS animation** on cursor — must be timeline-driven (blink + color) -- **Cursor `display: inline-block`** — required for width/height -- **`white-space: pre`** on text container and text — preserve trailing space -- **Monospace font** — proportional fonts cause cursor to drift mid-segment +- **Cursor color goes on `background`** — it's a colored block, not a glyph. +- **Blink is timeline-driven sin, pure of any mutable tracker** — the typing-grace variation shows the seek-safe form. +- **`white-space: pre` on text and container** — collapsed trailing spaces park the cursor in the wrong column. +- **Monospace font + `display: inline-block` cursor** — proportional faces drift the cursor mid-segment; inline ignores the block geometry. +- **BLINK_CYCLES_PER_SCENE is a whole number** for the fixed DURATION. -## Combinations +## See also -- [discrete-text-sequence.md](discrete-text-sequence.md) — uses the same SEQUENCE array pattern; this rule adds the cursor styling layer -- [camera-cursor-tracking.md](camera-cursor-tracking.md) — camera tracks the cursor across the typing -- [press-release-spring.md](press-release-spring.md) — after typing completes, a button press confirms the command - -## Pairs with HF skills - -- `/hyperframes-animation` — onUpdate driving cursor color + sin blink -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`discrete-text-sequence` (the underlying SEQUENCE pattern) · `camera-cursor-tracking` (camera follows the cursor) · `press-release-spring` (post-typing confirm press). diff --git a/skills/hyperframes-animation/rules/control-target-sync.md b/skills/hyperframes-animation/rules/control-target-sync.md new file mode 100644 index 000000000..d521589c9 --- /dev/null +++ b/skills/hyperframes-animation/rules/control-target-sync.md @@ -0,0 +1,131 @@ +--- +name: control-target-sync +description: The live-sync couple — a scrubbed/typed/picked control drives a second element's property in the SAME beat. Readout tween + target transform tween share one timeline label (continuous scrub), or one threshold state array carries both sides (discrete steps). Makes "change this, watch it change" read as causality. +metadata: + tags: control, scrub, live-sync, mirror, panel, editor, couple, readout, ui +--- + +# Control-Target Sync + +THE live-editing move: an inspector/editor control is manipulated — a value scrubbed, a field retyped, a dropdown picked — and a **bound second element answers in the same frame**. The button rotates WHILE the rotation value scrubs; icons resize PER KEYSTROKE. The persuasion is causality — one gesture, two surfaces changing together — and this rule is the coupling contract that produces it. + +Nearest precedent is [reactive-displacement.md](reactive-displacement.md): that rule also derives two elements' motion from one source, but it is **collision physics** — an entering intruder displaces an exiting victim, once, as a transition, and the victim leaves. This rule is a **live editing mirror**: the control is manipulated repeatedly across several beats, the target answers every time, and both sides hold the stage throughout. The numeric readout rides [counting-dynamic-scale.md](counting-dynamic-scale.md)'s proxy pattern; discrete steps ride [discrete-text-sequence.md](discrete-text-sequence.md)'s threshold pattern — what this rule adds is the law that binds either of them to the target. + +## How It Works + +An **edit beat** is a set of concurrent tweens at ONE timeline label: `tl.addLabel("edit1", …)`, then the **readout tween** (numeric proxy + `onUpdate` writing `textContent` only) and the **target transform tween** (`rotation` / `x` / `y` / `scale` to the same endpoint), both placed at the label with the same **duration** and **ease**. The two motions are two projections of one gesture — value at 40% ⇒ target at 40%, on every frame, under any seek. That mathematical lockstep reads as "the panel is editing the page," not "two animations happen to overlap." + +For **discrete edits** (per-keystroke retypes, dropdown picks, unit snaps) the couple steps instead of glides: a single threshold state array carries BOTH sides — each state holds the readout text AND the target's property value — and one driver applies whichever state is active. Both sides read from the same state object, so they cannot desync. + +Chain 2–4 edit beats with short holds between, and end on a **landed** edit — the last value applied and holding, never a tooltip with the dropdown unopened. + +## Recipe + +```html + +
+
{buttonLabel}
+
+
{iconA}
+ … +
+
+
+
+ Rotation +
+
+ Classtext-1xl +
+
+``` + +```js +// ---- Continuous couple: ONE label; both tweens share duration AND ease ---- +tl.addLabel("edit1", EDIT1_AT); +const rotState = { v: 0 }; +const rotReadout = document.getElementById("rotation-readout"); +tl.to( + rotState, + { + v: ROT_TARGET, + duration: SCRUB_DUR, + ease: SCRUB_EASE, + onUpdate: () => { + rotReadout.textContent = `${Math.round(rotState.v)}°`; + }, + }, + "edit1", +); +tl.to( + "#target-button", + { rotation: ROT_TARGET, duration: SCRUB_DUR, ease: SCRUB_EASE }, + "edit1", // same label — the mirror answers in the same frame +); + +// ---- Discrete couple: ONE state array carries BOTH sides ---- +const STEPS = [ + { t: 0.0, text: "text-1xl", scale: 1.0 }, // must equal the initial state + { t: 0.4, text: "text-4xl", scale: 1.9 }, + { t: 1.0, text: "text-xl", scale: 0.85 }, // backspace + { t: 1.35, text: "text-2xl", scale: 1.3 }, // lands +]; +const stepAt = (time) => [...STEPS].reverse().find((s) => time >= s.t) ?? STEPS[0]; + +tl.addLabel("edit3", EDIT3_AT); +const classReadout = document.getElementById("class-readout"); +const stepDriver = { t: 0 }; +let lastStep = null; +tl.to( + stepDriver, + { + t: STEPS_TOTAL, + duration: STEPS_TOTAL, + ease: "none", + onUpdate: () => { + const s = stepAt(stepDriver.t); + if (s !== lastStep) { + classReadout.textContent = s.text; // control steps + gsap.set(".preview-icon", { scale: s.scale }); // target steps — same state object + lastStep = s; + } + }, + }, + "edit3", +); +``` + +## Variations + +- **Dropdown pick → instant conversion (self-conversion)** — the pick converts the panel's own readout in place (`tl.set("#padding-readout", { textContent: "6 px" }, "pick")`); control and target collapse into one element. Compose the dropdown from neighbors: menu pops via [spring-pop-entrance.md](spring-pop-entrance.md), row hover-stepping via [dynamic-content-sequencing.md](dynamic-content-sequencing.md). The conversion must be an INSTANT snap — tweening between unit strings reads as broken, and instantness is the feature being sold. +- **Easing-handle drag → target re-animates (deferred mirror)** — the edit authors a _behavior_, so the mirror is a **replay**, not a concurrent transform: beat 1 drags the handle (handle tween + coords readout), then at a later label the target performs its motion with the newly-authored curve (`tl.fromTo("#toggle-knob", { x: 0 }, { x: KNOB_TRAVEL, duration: REPLAY_DUR, ease: AUTHORED_EASE }, "replay")`), often under a zoom-out ([viewport-change.md](viewport-change.md)). The one sanctioned case where the response is not in the gesture's beat; the replay must still be unmistakably the edited parameter. +- **Read-sync mirror (reverse direction)** — the gesture happens ON the target (hovering swatches, selecting an element) and the PANEL readout is the bound side. Same discrete contract — one state array of `{ t, hoverTarget, readout }` drives both the highlight and the text. +- **Color couple** — the readout counts (`0 → 80`) while the target's `backgroundColor` tweens between two palette stops at the same label. Keep it two fixed stops (GSAP interpolates); never derive per-frame hex strings by hand. + +## Values + +| token | range | notes | +| -------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| SCRUB_DUR | 0.8–1.6 s | the viewer must see BOTH sides move — under ~0.6 s the mirror registers subconsciously at best | +| SCRUB_EASE | `power1.inOut` / `power2.inOut` | shared verbatim by both tweens. Never `back.out` / `elastic.out` — an overshooting value reads as a broken hinge; the readout is data | +| edit endpoints | visible but plausible | −10° tilt, 38 px shift, 1xl → 4xl → 2xl; a 2° rotation doesn't demo anything | +| HOLD_BETWEEN | 0.3–0.8 s | each landed value gets a breath; below 0.3 s the beats smear into one gesture | +| BEAT_COUNT | 2–4 | one edit is a moment, not a demo; past 4 the shot reads as a settings tour | +| STEP gaps (discrete) | 0.15–0.5 s | keystroke pacing per discrete-text-sequence; first state must equal the on-load state | +| VALUE_MIN_WIDTH | ≥ longest value's width | without it the panel edge jitters as digit counts change | + +## Critical Constraints + +- **One label, one gesture** — readout tween and target tween share position, duration, AND ease; never sequence readout-then-target, and never stagger the target behind the readout even by 0.1 s — a delayed response reads as an animation following an edit, not a bound surface. A mismatched ease desyncs the mirror mid-tween even when endpoints agree. +- **Discrete steps share one state object** — both sides read the same array entry, so desync is impossible by construction; first entry mirrors the initial DOM state. +- **The readout is data** — no overshoot, no bounce on the settle; the target may carry the gesture's ease but lands exactly on the edited value. +- **Co-visibility is load-bearing** — control and target share the frame for every edit beat; a camera move must never crop the mirror out (punch-and-return around the beats, not through them). +- **`tabular-nums` + fixed `min-width`** on every scrubbed readout; `onUpdate` is O(1) — text writes only, discrete drivers guard writes with a last-state check. +- **End on a landed edit** — the final beat resolves with the value applied and holding (or the deferred-mirror replay); never mid-gesture or on an unopened menu. +- **The gesture's actor is a separate rule** — cursor glide, grab-cursor flip, and click feedback come from the cursor rules; this rule owns only the couple. + +## See also + +`cursor-click-ripple` / `context-sensitive-cursor` (the hand performing the gesture) · `counting-dynamic-scale` (the readout half alone, when there is no bound target) · `discrete-text-sequence` (retypes inside the control field) · `spring-pop-entrance` (dropdowns/chrome around the couple) · `multi-phase-camera` (punch-and-return framing) · `chart-scrub-readout` (the sibling READ direction — a scrub interrogates a chart instead of editing a target). diff --git a/skills/hyperframes-animation/rules/coordinate-target-zoom.md b/skills/hyperframes-animation/rules/coordinate-target-zoom.md index 083add120..a28e80fa6 100644 --- a/skills/hyperframes-animation/rules/coordinate-target-zoom.md +++ b/skills/hyperframes-animation/rules/coordinate-target-zoom.md @@ -11,32 +11,26 @@ A simple `scale > 1` on a wrapper pushes off-center content OFF the visible canv ## How It Works -Two nested wrappers, separated concerns: +Two nested wrappers, separated concerns — never scale and translate on the SAME element (`translate * scale` ≠ `scale * translate` in CSS transform composition): -1. **Outer wrapper** applies `scale` (the zoom) +1. **Outer wrapper** applies `scale` (the zoom) around `transform-origin: 50% 50%` 2. **Inner wrapper** applies `translate(x, y)` (the counter-shift) -The translate is the **negation** of the target's offset from center. The inner translate moves the target back to the outer's transform-origin BEFORE the outer scale fires, so the scale around center maps the target to 0. +The counter-translate is the **negation** of the target's offset from viewport center: ``` T = -offset ``` -Derivation (outer scales the inner-translated content): +Derivation: the inner translate moves the target to `offset + T` in pre-scale units; the outer scale S (around center) maps that to `S × (offset + T)`; landing at center means `S × (offset + T) = 0` → **`T = -offset`**. The formula does NOT depend on S — the translate is identical at 1.5×, 2×, or 3×. A common wrong intuition is `T = -offset × (S - 1)`: it coincidentally matches at S = 2 and is wrong at every other scale. -1. Inner translate moves target by T in pre-scale units → target at `offset + T` -2. Outer scale S (around center 0,0) maps that to `S × (offset + T)` -3. For target to land at viewport center: `S × (offset + T) = 0` → **`T = -offset`** - -Note: the formula does NOT depend on S. The translate amount is the same whether you zoom 1.5×, 2×, or 3× — as long as the OUTER is the scale and the INNER is the translate, and scale uses `transform-origin: 50% 50%`. +⚠️ **This is the NESTED-wrapper formula.** The single-wrapper camera in [viewport-change.md](viewport-change.md) puts `translate(x,y) scale(S)` on ONE element, where CSS applies scale first — there the counter-translate is **`T = -offset × S`**. The two formulas are not interchangeable; match the formula to the wrapper structure. ## Getting the offset `T = -offset` is only as good as `offset`. The #1 way this pattern ships broken is hand-computing `offset` from a layout formula, getting the **sign** or magnitude wrong, and letting the zoom amplify a small error off-screen. **Default to measuring the target's real laid-out center; reserve the formula for symmetric rows.** -### Default — measure the target's actual center (works for ANY layout) - -Read where the target actually is, once, at setup. This is immune to sign errors because it's derived from the rendered DOM, not a mental model: +**Default — measure the actual center (works for ANY layout).** Immune to sign errors because it reads the rendered DOM, not a mental model: ```js await document.fonts.ready; // metrics final; fallback fonts are 10–30px off → tens of px after a 3×+ zoom @@ -45,87 +39,52 @@ const W = 1920, const r = document.getElementById("target-card").getBoundingClientRect(); const TARGET_OFFSET_X = r.left + r.width / 2 - W / 2; const TARGET_OFFSET_Y = r.top + r.height / 2 - H / 2; -// bake these; feed counterX/Y = -TARGET_OFFSET_X/Y to the inner tween ``` -This `getBoundingClientRect` runs **once at setup**, before timeline registration — NOT per-frame (per-frame DOM reads desync under the renderer's parallel sampling; see SKILL universal constraints). Because the measurement is async (`fonts.ready`), build and register the timeline inside the same `async` setup so the baked offset is ready before `window.__timelines[id]` is published. +Measure **once at setup** and bake — never per-frame in `onUpdate`. Because the measurement is async (`fonts.ready`), build and register the timeline inside the same `async` setup so the baked offset is ready before `window.__timelines[id]` is published. -### Shortcut — symmetric equal-width row ONLY - -If (and only if) the target is one of N **equal-width** cards in a centered row with uniform gaps, you may skip measurement: +**Shortcut — symmetric equal-width row ONLY:** ```js const index_offset = targetIndex - (N - 1) / 2; const TARGET_OFFSET_X = index_offset * (CARD_WIDTH + CARD_GAP); ``` -⚠️ This assumes every sibling is the **same width**. The moment the row is asymmetric — a wide companion label beside a narrow chip, a wordmark flanked by unequal elements — it gives the wrong answer, often the wrong **sign**: the heavier side shifts the centered target the _opposite_ way you'd guess. (A real example: `companion(220) + gap + wordmark + gap + chip(110)` puts the wordmark ~55px **right** of center, but the "chip − companion" intuition says left.) For anything but equal cards, **measure**. +⚠️ This assumes every sibling is the **same width**. The moment the row is asymmetric, it gives the wrong answer — often the wrong **sign**: the heavier side shifts the centered target the _opposite_ way you'd guess (e.g. `companion(220) + gap + wordmark + gap + chip(110)` puts the wordmark ~55px **right** of center, but "chip − companion" intuition says left). For anything but equal cards, **measure**. -### Headroom budget — cap the scale from the measured size - -A zoom multiplies any centering error, so leave margin. Keep the target ≤ ~88% of the canvas at peak; derive the cap from the measured size instead of picking a round number by feel: +**Headroom budget — cap the scale from the measured size.** A zoom multiplies any centering error; keep the target ≤ ~88% of the canvas at peak: ```js const maxScale = Math.min((0.88 * W) / r.width, (0.88 * H) / r.height); const ZOOM_SCALE = Math.min(DESIRED_SCALE, maxScale); ``` -A target that fills 97%+ of the frame reads as cut-off the instant its center is even slightly off — and a hand-baked offset always is. (The perception gate flags this as `primary-offscreen`, and `data-layout-allow-overflow` does **not** exempt it.) +A target filling 97%+ of the frame reads as cut-off the instant its center is slightly off — and a hand-baked offset always is. (The perception gate flags this as `primary-offscreen`; `data-layout-allow-overflow` does **not** exempt it.) -## HTML +## Recipe ```html -
-
-
-
- -
-
{label1}
-
{price1}
-
-
-
{label2}
-
{price2}
-
-
-
{targetLabel}
-
{targetPrice}
-
{targetTagline}
-
-
-
{label4}
-
{price4}
-
-
+
+
+
+
{other}
+
{target}
+
{other}
``` -## CSS - ```css .scene { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; /* REQUIRED — see Critical Constraints */ - background: {bgGradient}; + overflow: hidden; /* REQUIRED — at zoom > 1 the scaled content leaks past the frame */ } .zoom-outer { width: 100%; height: 100%; display: grid; place-items: center; - transform-origin: 50% 50%; + transform-origin: 50% 50%; /* center scaling is what the counter-translate math assumes */ will-change: transform; } .zoom-inner { @@ -133,200 +92,47 @@ A target that fills 97%+ of the frame reads as cut-off the instant its center is place-items: center; will-change: transform; } -.content { - display: flex; - gap: CARD_GAP; -} -.card { - width: CARD_WIDTH; - padding: CARD_PADDING; - border-radius: CARD_RADIUS; - background: {cardBg}; - border: 1px solid {cardBorder}; - text-align: center; - font-family: {font}; -} -.card.target { - background: {targetCardBg}; /* slightly brighter than .card */ - border: 2px solid {targetBorder}; - box-shadow: {targetGlow}; -} -.label { - font-size: LABEL_FONT_SIZE; - font-weight: 800; - letter-spacing: 6px; - text-transform: uppercase; - color: {labelColor}; -} -.price { - font-size: PRICE_FONT_SIZE; - font-weight: 900; - color: {textColor}; - margin: 16px 0; - font-variant-numeric: tabular-nums; -} -.tag { - font-size: TAG_FONT_SIZE; - font-weight: 700; - letter-spacing: 4px; - color: {accentColor}; - opacity: 0; -} ``` -## GSAP Timeline +```js +// TARGET_OFFSET_X/Y and ZOOM_SCALE come from "Getting the offset" — measured +// at setup (after fonts.ready), baked. Counter-translation = -offset. +const counterX = -TARGET_OFFSET_X; +const counterY = -TARGET_OFFSET_Y; -```html - - +// Scale and counter-translate MUST share position, duration, AND ease — +// otherwise the target visibly wanders mid-zoom. +tl.to("#zoom-outer", { scale: ZOOM_SCALE, duration: ZOOM_DUR, ease: "power3.inOut" }, ZOOM_AT); +tl.to( + "#zoom-inner", + { x: counterX, y: counterY, duration: ZOOM_DUR, ease: "power3.inOut" }, + ZOOM_AT, +); ``` ## Variations -### Dynamic target lookup via `getBoundingClientRect` +- **Zoom out (target → wide view)**: reverse the phases — start zoomed-in, then tween to `scale: 1` + `x: 0, y: 0`; the "reveal" beat is the panorama. +- **Multi-target zoom sequence**: chain zooms (target A → pause → target B → pull back); each segment needs its own counter-translation pair. -This is now the **default**, not a variation — see [Getting the offset](#getting-the-offset). Always `await document.fonts.ready` before measuring (fallback-font metrics are off by 10–30px, which a 3×+ zoom magnifies into tens of visible px) and measure **once at setup**, never per-frame. +## Values -### Zoom out (target → wide view) - -Reverse the phases — start at zoomed-in, then `scale: 1` + `x: 0, y: 0` to pull back. The "reveal" beat is the panorama. - -### Multi-target zoom sequence - -Chain multiple zooms: target A (1.5-2.5s) → pause → target B (3-4s) → pull back (4.5-5s). Each segment needs its own counter-translation pair. - -## How to Choose Values - -### Layout - -- **CARD_WIDTH / CARD_GAP / CARD_PADDING / CARD_RADIUS** — geometric layout. - - Constraints: `N × CARD_WIDTH + (N-1) × CARD_GAP < viewportWidth` so all cards fit pre-zoom - - Effects: smaller cards → more siblings on screen → busier composition; larger cards → fewer siblings, more emphasis per card -- **LABEL_FONT_SIZE / PRICE_FONT_SIZE / TAG_FONT_SIZE** — typographic hierarchy. - - Range: tag < label < price (price is the focal element after zoom; sizing it largest reinforces this) - -### Reveal phase - -- **REVEAL_START** — when the cards begin fading in. - - Constraints: typically a small offset (~0.2s) for a beat of black before content appears -- **REVEAL_DUR** — per-card fade-up duration. - - Range: 0.4-0.8s -- **REVEAL_Y** — initial vertical offset of each card before fade-up (in px). - - Range: 16-48 px; bigger feels "thrown in," smaller feels gentle -- **REVEAL_STAGGER** — delay between consecutive card reveals. - - Range: 0.06-0.15s; calibrated so all cards finish before `ZOOM_START` - -### Zoom phase - -- **ZOOM_START** — when the zoom begins. - - Constraints: `≥ REVEAL_START + REVEAL_DUR + (N-1) × REVEAL_STAGGER + viewer-scan-time` (give viewer 0.5-1.5s to read the layout before zooming) -- **ZOOM_DUR** — duration of the zoom tween. - - Range: 1.0-2.0s; under 0.8s feels like a teleport, over 2.5s drags - - Constraints: scale tween + counter-translate tween MUST share this duration AND ease -- **ZOOM_SCALE** — final magnification. - - Range: 1.5× (modest emphasis) → 3× (dominant focus) → 5×+ (cinematic extreme) - - Constraints: card content must remain crisp at this scale; raster source media needs `sourceResolution ≥ rendered × ZOOM_SCALE` - - **Headroom budget**: cap from the measured target size so the target stays ≤ ~88% of the canvas at peak — `ZOOM_SCALE = Math.min(DESIRED, 0.88×W/r.width, 0.88×H/r.height)`. Picking a round number by feel (e.g. 3.2× on a 585px wordmark → 1872px = 97% of 1920) leaves no margin, so any centering slop cuts the text off. - -### Target reveal + dwell - -- **TAG_REVEAL_START** — when the target's hidden tag fades in. - - Constraints: `≥ ZOOM_START + ZOOM_DUR` (only reveal after the zoom settles, so viewer's eye is already on the target) -- **TAG_REVEAL_DUR** — tag fade-in duration. - - Range: 0.3-0.6s -- **DWELL_DUR** — post-zoom hold so the viewer reads the target. - - Range: ≥ 1.0s after tag reveals (see "Climax dwell" in Key Principles) - -### Color tokens - -- **{bgGradient}** — typically a dark radial gradient to vignette the cards -- **{cardBg} / {cardBorder}** — non-target cards (subtle, recessive) -- **{targetCardBg} / {targetBorder} / {targetGlow}** — target card visually brighter / haloed so the eye lands there before the zoom even fires -- **{labelColor} / {textColor} / {accentColor}** — hierarchical text colors; `{accentColor}` reserved for the tag (pops on reveal) - -## Key Principles - -- **Measure the offset, don't hand-derive it** — for any layout that isn't a symmetric equal-width row, read the target's real center with `getBoundingClientRect` at setup (after `fonts.ready`) and bake it (see [Getting the offset](#getting-the-offset)). Hand-computed offsets silently get the **sign** wrong on asymmetric layouts, and the zoom amplifies the error off-screen — the single most common way this pattern ships broken. -- **Transform order — outer scales, inner translates** — DO NOT put scale and translate on the SAME element. The transform math becomes tangled (`translate * scale` ≠ `scale * translate` in CSS transform composition). Nested wrappers cleanly separate concerns. -- **Counter-translate = -offset** — independent of scale. Derive from: outer scale around center maps `(offset + T)` to `S × (offset + T)`. Setting that to zero gives `T = -offset`. A common wrong intuition is `T = -offset × (S - 1)` — it happens to give the same answer at S=2 but is wrong for any other S. -- **`transform-origin: 50% 50%` on outer wrapper** — non-center origin causes unpredictable inner offset; always center. -- **`overflow: hidden` on `.scene` REQUIRED** — at zoom > 1, the outer-scaled content can leak beyond the 1920×1080 frame. -- **Tween scale and counter-translate together** — they MUST share `duration` and `ease`. Otherwise the target drifts mid-zoom (visible "wandering"). Easiest: pass identical params to both tweens at the same time position. -- **❗ Climax dwell ≥1s after zoom completes** — see SKILL universal constraints. If zoom ends at t=3.0 in a 3.5s comp, viewer barely sees the target; aim for 1.5-2s post-zoom dwell. +| token | range | notes | +| ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | +| ZOOM_SCALE | 1.5× modest → 3× dominant → 5×+ extreme | cap via the headroom budget; raster media needs `sourceResolution ≥ rendered × ZOOM_SCALE` | +| ZOOM_DUR | 1.0–2.0s | under 0.8s feels like a teleport, over 2.5s drags; both tweens share it | +| ZOOM_AT | after the layout lands + 0.5–1.5s | give the viewer time to scan the layout before the camera commits | +| DWELL | ≥ 1.0s after the zoom settles | 1.5–2s ideal — the viewer must be able to read the target (climax dwell) | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition` on `.zoom-outer` or `.zoom-inner`** — competes with GSAP -- **`will-change: transform`** on both wrappers — the transforms update every frame during the zoom phase -- **`transform-origin: 50% 50%` on `.zoom-outer`** — center-based scaling is what the counter-translate math assumes -- **Target offset baked once, at setup, from measurement** — measure the target center after `fonts.ready` and bake (see [Getting the offset](#getting-the-offset)); never recompute per-frame in onUpdate, and never hand-estimate the offset for a non-symmetric layout -- **Scale within the headroom budget** — keep the target ≤ ~88% of the canvas at peak, derived from the measured size (`maxScale = 0.88 × W / measuredWidth`); a target that fills the frame is cut off the instant the center is slightly off +- **Outer scales, inner translates** — never both transforms on one element; nested wrappers keep the math clean. +- **`transform-origin: 50% 50%` on the outer wrapper** — non-center origin breaks the counter-translate derivation. +- **`overflow: hidden` on the scene root** — zoomed content leaks past the frame otherwise. +- **Scale and counter-translate share duration + ease** at the same timeline position, or the target drifts mid-zoom. +- **Offset measured once at setup** (after `fonts.ready`), baked — never recomputed per-frame, never hand-derived for a non-symmetric layout (wrong sign → target shoved off-frame). +- **Scale within the headroom budget** — target ≤ ~88% of the canvas at peak, derived from the measured size. -## Combinations +## See also -- [multi-phase-camera.md](multi-phase-camera.md) — multi-phase camera that includes a coordinate-target-zoom phase -- [sine-wave-loop.md](sine-wave-loop.md) — idle breathing on the target AFTER zoom settles -- [discrete-text-sequence.md](discrete-text-sequence.md) — text assembly in the target BEFORE zoom completes - -## Pairs with HF skills - -- `/hyperframes-animation` — two coordinated tweens -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +[viewport-change.md](viewport-change.md) (single-wrapper form, `T = -offset × S`) · [multi-phase-camera.md](multi-phase-camera.md) (a zoom phase inside a phased camera) · [sine-wave-loop.md](sine-wave-loop.md) (idle breathing after the zoom settles) · [discrete-text-sequence.md](discrete-text-sequence.md) (text assembly in the target before the zoom). diff --git a/skills/hyperframes-animation/rules/counting-dynamic-scale.md b/skills/hyperframes-animation/rules/counting-dynamic-scale.md index f3d936cf8..5d9da5f30 100644 --- a/skills/hyperframes-animation/rules/counting-dynamic-scale.md +++ b/skills/hyperframes-animation/rules/counting-dynamic-scale.md @@ -7,200 +7,77 @@ metadata: # Counting with Dynamic Scale -A number counts from A → B while its transform scale grows to the final size. The effect preserves escalating visual weight without tweening `font-size` or forcing text layout on every frame. +A number counts from A → B while its transform scale grows to the final size — escalating visual weight ("this is impressive") without tweening `font-size` or forcing text layout on every frame. The final font size is static CSS; only the transform changes. ## How It Works -A single paused timeline drives **two synchronized tweens**: +Two synchronized tweens at the SAME timeline position with the SAME ease: (1) a proxy value rendered as text via `onUpdate` (`Math.round(...).toLocaleString()`), (2) the counter's transform `scale: START_SCALE → 1`, where `START_SCALE = START_SIZE / END_SIZE`. A suffix (`%`, `×`, `+`) slides in AFTER the count lands — the number gets its own beat — and a label fades in early. -1. The numeric value (rendered as DOM text via `onUpdate`) -2. The counter transform (`scale: START_SCALE` → `scale: 1`) - -As the number gets bigger, the text grows in place — visually communicating “this is impressive” while keeping the final font size static in CSS. - -## Easing - -Pick by drama desired (the choice is discrete; coefficient is implicit): - -| GSAP ease | Effect | -| ------------ | --------------------------------------------- | -| `power1.out` | Mild — slight deceleration | -| `power2.out` | Default — ease-out, fast start slow end | -| `power3.out` | Strong — dramatic deceleration ⭐ recommended | -| `expo.out` | Very dramatic — almost stops at the end | - -`power3.out` matches the polynomial `1 - (1-x)^k` family at k ≈ 2.5 — number rushes up then slows dramatically at the peak. - -## HTML +## Recipe ```html -
-
- 0{suffix} -
-
{label}
+ +
+ 0{suffix}
+
{label}
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; -} - .counter-wrap { display: flex; align-items: baseline; justify-content: center; - gap: 8px; - /* Fixed-width container prevents layout shift as digit count changes */ - width: {counterContainerWidth}; - text-align: center; + width: {counterContainerWidth}; /* fixed width — no layout shift as digit count changes */ } - .counter { - font-family: {font}; - font-weight: 900; - color: {textColor}; - /* MANDATORY — tabular-nums keeps digits the same width */ - font-variant-numeric: tabular-nums; - /* Final size is static. GSAP animates transform scale, not font-size. */ + font-variant-numeric: tabular-nums; /* MANDATORY — digits keep equal width */ display: inline-block; - font-size: {endSize}; + font-size: {endSize}; /* final size is static; GSAP animates scale, not font-size */ transform-origin: center center; - letter-spacing: -2px; - line-height: 1; } - .counter-suffix { - font-family: {font}; - font-weight: 800; - color: {accentColor}; - font-size: {suffixSize}; opacity: 0; transform: translateY(20px); } - -.counter-label { - margin-top: 24px; - font-family: {font}; - font-size: {labelSize}; - color: {mutedTextColor}; - text-align: center; -} ``` -## GSAP Timeline +```js +const counter = document.getElementById("counter"); +const state = { value: 0 }; +const START_SCALE = START_SIZE / END_SIZE; -```html - - +// Label fades in early +tl.from(".counter-label", { opacity: 0, y: 12, duration: LABEL_DUR, ease: "power2.out" }, LABEL_AT); ``` -## How to Choose Values - -- **TARGET_VALUE** — the number the counter lands on - - Effects: 2–3 digits reads best at hero size; 4+ digits requires wider container - - Constraints: must fit horizontally at END_SIZE inside the container - -- **START_SIZE / END_SIZE** — design inputs used once to calculate `START_SCALE` - - Range: START_SIZE ≈ 40–60 % of END_SIZE - - Effects: smaller START_SIZE = more dramatic growth; larger = subtler - - Constraints: set CSS `font-size` to END_SIZE; never tween either value. END_SIZE × digit count must fit the container width without clipping - -- **COUNT_DUR** — count + scale tween duration - - Range: 1.2–2.5 s - - Effects: shorter = aggressive; longer = settled, gives reading time - - Constraints: must allow the eye to read the digits scrolling past; below ~0.8 s reads as a flash - -- **COUNT_EASE** — shared ease for the value and transform scale - - Discrete choice: `power2.out`, `power3.out`, `expo.out` (see table above) - - Constraint: avoid `back.out` / `elastic.out` — overshoot reads as unstable data - -- **SUFFIX_DUR** — duration of the suffix slide-in - - Range: 0.3–0.6 s - - Effects: shorter = snap; longer = floats - - Constraints: must fire after the count lands (started at COUNT_DUR), not during - -- **SUFFIX_BOUNCE_FACTOR** — back.out coefficient on the suffix entry - - Range: 1.4–2.0 - - Effects: 1.4 = small overshoot; 2.0 = bouncy - -- **LABEL_AT / LABEL_DUR** — when and how long the label fades in - - Range: LABEL_AT < COUNT_DUR / 2 (label arrives before count peaks); LABEL_DUR 0.4–0.7 s - ## Variations -### Direct `innerText` tween (no proxy object) - -The GSAP inspector reads `innerText` directly, so a number-only counter can skip the `state` proxy: +- **Direct `innerText` tween (no proxy)** — GSAP can tween `innerText` directly for a number-only counter; keep the proxy form when you need locale formatting or suffix logic. The scale tween stays separate either way: ```js tl.to( @@ -210,84 +87,29 @@ tl.to( ); ``` -`snap: { innerText: 1 }` keeps it integer. Keep the proxy-object `onUpdate` form above when you need locale formatting (`toLocaleString`) or suffix logic. In either form, the synchronized scale remains a separate transform tween at timeline position `0`. +- **3D depth entry** — add a `tl.from(".counter", { z: -300, ... }, 0)` push-in; requires `perspective` on `.counter-wrap` and `transform-style: preserve-3d` on the counter. +- **Multi-stat coordinated reveal** — 3 stats counting in parallel share the SAME ease, duration, and start position so they finish together (a chord, not an arpeggio). Each stat usually also needs a paired graphic (bar / ring / stars) — don't stop at the number; see [stat-bars-and-fills.md](stat-bars-and-fills.md). -### 3D depth entry +## Values -Combine with `translateZ` for parallax-style depth on entry: - -```js -tl.from( - ".counter", - { - z: -300, - duration: 0.6, - ease: "power2.out", - // requires parent or .counter itself to have perspective set - }, - 0, -); -``` - -CSS prerequisite: - -```css -.counter-wrap { - perspective: 1000px; -} -.counter { - transform-style: preserve-3d; -} -``` - -### Multi-stat coordinated reveal - -For 3 stats counting in parallel, share the SAME ease and duration so they finish together — visually a chord, not arpeggio. Each stat usually also needs a **paired graphic** (bar / ring / stars) — don't stop at the number; see [stat-bars-and-fills.md](stat-bars-and-fills.md): - -```js -["#stat1", "#stat2", "#stat3"].forEach((sel, i) => { - const obj = { v: 0 }; - tl.to( - obj, - { - v: TARGETS[i], - duration: COUNT_DUR, - ease: COUNT_EASE, - onUpdate: () => (document.querySelector(sel).textContent = Math.round(obj.v)), - }, - 0, - ); // same start position — chord -}); -``` - -## Key Principles - -- **Synchronized value + scale at one timeline position** so the two tweens share an ease and stay coordinated -- **`font-variant-numeric: tabular-nums` is mandatory** — without it digit-count transitions (e.g. 9 → 10 → 100) cause visible jitter as glyph widths change -- **Fixed-width container** as belt-and-suspenders — even with tabular-nums, glyph shape changes can shift baselines -- **Grow in place, don't bounce** — the number should feel weighty, not springy. `power3.out` ends at exact value; `back.out` overshoots and feels cartoonish -- **Start small enough to grow noticeably** (~50 % of final size); end large enough to feel decisive but not clip viewport -- **Never set `fontSize` in `onUpdate`** — final type size is static CSS; only the transform changes per frame -- **Suffix animates AFTER the count, not during** — gives the number its own beat -- **❗ Label is BIG TEXT, not a page-style tiny caption** — for VIDEO, a small paragraph-style caption below a hero-size number reads as visual noise. Use display-size, uppercase, tracked label so the layout is "two-line big-text"; the label is part of the headline, not a footer. +| token | range | notes | +| --------------------- | ------------------------------------------- | ----------------------------------------------------------------------------- | +| TARGET_VALUE | 2–3 digits ideal | 4+ digits needs a wider container; must fit at END_SIZE without clipping | +| START_SIZE / END_SIZE | START ≈ 40–60% of END | design inputs used once for START_SCALE; never tween either | +| COUNT_DUR | 1.2–2.5s | below ~0.8s reads as a flash — the eye must read the digits scrolling past | +| COUNT_EASE | `power2.out` / `power3.out` ⭐ / `expo.out` | shared by value + scale; more `.out` = more dramatic deceleration at the peak | +| SUFFIX_DUR | 0.3–0.6s | fires at `COUNT_DUR`, never during the count | +| SUFFIX_BOUNCE_FACTOR | 1.4–2.0 | overshoot is fine on the suffix (it's punctuation, not data) | +| LABEL_AT / LABEL_DUR | AT < COUNT_DUR/2; 0.4–0.7s | label arrives before the count peaks | ## Critical Constraints -- **`tabular-nums` mandatory** — required CSS for layout stability -- **Timeline must be paused**: `gsap.timeline({ paused: true })`. Never `tl.play()` -- **Registry key = `data-composition-id`**: `window.__timelines["counter-scene"]` must match scene root -- **`onUpdate` mutates DOM**: HF runtime seeks the timeline frame-by-frame, so `onUpdate` runs on every seek call. Keep `onUpdate` work O(1) — set text only, with no style writes or DOM creation -- **`Math.round` not `Math.floor`** — half-way through the final integer should display the final value briefly, not the previous one -- **Avoid `back.out` / `elastic.out`** for the counter itself — overshoot makes the number look unstable (it's data, not decoration) +- **`tabular-nums` mandatory** + fixed-width container as belt-and-suspenders — without them digit-count transitions (9 → 10 → 100) jitter as glyph widths change. +- **Never set `fontSize` in `onUpdate`** — final type size is static CSS; only the transform changes per frame. Keep `onUpdate` O(1): set text only, no style writes or DOM creation. +- **`Math.round`, not `Math.floor`** — halfway through the final integer should already display the final value. +- **Avoid `back.out` / `elastic.out` on the counter itself** — overshoot makes the number look unstable (it's data, not decoration). Grow in place, don't bounce. +- **Label is BIG TEXT, not a page-style caption** — a tiny paragraph under a hero-size number reads as visual noise in video. Display-size, uppercase, tracked: the label is part of the headline. -## Combinations +## See also -- [stat-bars-and-fills.md](stat-bars-and-fills.md) — **the paired graphic beside the number** (growth bars / progress ring / star wipe). A stat scene is usually BOTH rules: the count-up here + a fill there. Give the fill the same ease and duration so number and graphic land as one beat. -- [svg-path-draw.md](svg-path-draw.md) — icons drawing in around the number -- [center-outward-expansion.md](center-outward-expansion.md) — related icons exploding outward synced to count peak - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + `onUpdate` API -- `/hyperframes-core` — composition wiring, `data-*` attributes -- `/hyperframes-cli` — `hyperframes lint` to verify scene +`stat-bars-and-fills` (the paired graphic — give it the same ease/duration so number and fill land as one beat) · `svg-path-draw` (icons drawing in around the number) · `center-outward-expansion` (icons bursting outward at the count peak). diff --git a/skills/hyperframes-animation/rules/css-marker-patterns.md b/skills/hyperframes-animation/rules/css-marker-patterns.md index ad497de06..97ba6c4a0 100644 --- a/skills/hyperframes-animation/rules/css-marker-patterns.md +++ b/skills/hyperframes-animation/rules/css-marker-patterns.md @@ -1,18 +1,12 @@ # CSS Patterns for Marker Highlighting -Pure CSS + GSAP implementations of all five MarkerHighlight.js drawing modes. Use these for deterministic rendering in HyperFrames compositions — no external library dependency, full GSAP timeline control. +Pure CSS + GSAP implementations of all five MarkerHighlight.js drawing modes — no external library dependency, full timeline control. Snippets show mechanism DOM only, inside a standard scene clip (hyperframes-core); assume `tl` exists. -## Contents - -- [1. Highlight Mode](#1-highlight-mode) — Yellow marker sweep behind text -- [2. Circle Mode](#2-circle-mode) — Hand-drawn ellipse around text -- [3. Burst Mode](#3-burst-mode) — Radiating lines from text -- [4. Scribble Mode](#4-scribble-mode) — Chaotic scribble over text -- [5. Sketchout Mode](#5-sketchout-mode) — Rough rectangle outline +Shared scaffold for every mode: the wrap is `position: relative; display: inline`; the text copy is `position: relative` and z-indexed **above** the accent (below it for sketchout, where the lines cross the text). ## 1. Highlight Mode -Yellow marker sweep behind text. The most common mode. +Yellow marker sweep behind text — the most common mode. ```html @@ -22,16 +16,9 @@ Yellow marker sweep behind text. The most common mode. ``` ```css -.mh-highlight-wrap { - position: relative; - display: inline; -} .mh-highlight-bar { position: absolute; - top: 0; - left: -6px; - right: -6px; - bottom: 0; + inset: 0 -6px; /* bleed past the text edges */ background: #fdd835; opacity: 0.35; transform: scaleX(0); @@ -39,113 +26,46 @@ Yellow marker sweep behind text. The most common mode. border-radius: 3px; z-index: 0; } -.mh-highlight-text { - position: relative; - z-index: 1; -} ``` ```js -// Sweep in from left tl.to("#hl-1", { scaleX: 1, duration: 0.5, ease: "power2.out" }, 0.6); - -// Optional: skew for hand-drawn feel -// gsap.set("#hl-1", { skewX: -2 }); -``` - -### Multi-line Highlight - -Stagger bars across multiple lines: - -```js -tl.to( - ".mh-highlight-bar", - { - scaleX: 1, - duration: 0.5, - ease: "power2.out", - stagger: 0.3, - }, - 0.6, -); +// Optional hand-drawn skew: gsap.set("#hl-1", { skewX: -2 }); +// Multi-line: tl.to(".mh-highlight-bar", { scaleX: 1, ..., stagger: 0.3 }, 0.6); ``` ## 2. Circle Mode -Hand-drawn circle around text. Use `border-radius: 50%` with a slight rotation for organic feel. +Hand-drawn ellipse around text — `border-radius: 50%` plus a slight rotation for organic feel. ```html - IMPORTANT + IMPORTANT ``` ```css -.mh-circle-wrap { - position: relative; - display: inline; -} -.mh-circle-text { - position: relative; - z-index: 1; -} .mh-circle-ring { position: absolute; top: 50%; left: 50%; - width: 130%; + width: 130%; /* tight (short words): 150%; rounded-rect: 120% + border-radius: 30% */ height: 160%; transform: translate(-50%, -50%) rotate(-3deg) scale(0); border: 3px solid #e53935; border-radius: 50%; - pointer-events: none; z-index: 0; } ``` ```js -// Circle scales in with a wobble -tl.to( - "#circle-1", - { - scale: 1, - rotation: -3, - duration: 0.6, - ease: "back.out(1.7)", - transformOrigin: "center center", - }, - 0.7, -); -``` - -### Variations - -```css -/* Tighter circle (for short words) */ -.mh-circle-ring.tight { - width: 150%; - height: 180%; -} - -/* Squared circle (rounded rectangle) */ -.mh-circle-ring.rounded { - border-radius: 30%; - width: 120%; - height: 140%; -} - -/* Ellipse (wider than tall) */ -.mh-circle-ring.ellipse { - width: 150%; - height: 130%; - border-radius: 50%; -} +tl.to("#circle-1", { scale: 1, rotation: -3, duration: 0.6, ease: "back.out(1.7)" }, 0.7); ``` ## 3. Burst Mode -Radiating lines from text center. Each line is a positioned div rotated to its angle. +Radiating lines from text center — each line a positioned span rotated to its angle. Use ~12 lines at 30° steps and **vary `--len` (40–80px)**; equal lengths look mechanical. ```html @@ -153,36 +73,19 @@ Radiating lines from text center. Each line is a positioned div rotated to its a - - - - - - - - - - + ``` ```css -.mh-burst-wrap { - position: relative; - display: inline; -} -.mh-burst-text { - position: relative; - z-index: 2; -} .mh-burst-container { position: absolute; top: 50%; left: 50%; width: 0; height: 0; - z-index: 1; + z-index: 1; /* text copy at z-index: 2 */ } .mh-burst-line { position: absolute; @@ -199,7 +102,6 @@ Radiating lines from text center. Each line is a positioned div rotated to its a ``` ```js -// All lines burst outward simultaneously with slight stagger tl.fromTo( "#burst-1 .mh-burst-line", { scaleY: 0, opacity: 0 }, @@ -208,11 +110,9 @@ tl.fromTo( ); ``` -**Vary line lengths** (40-80px range) for an organic, hand-drawn feel. Equal lengths look mechanical. - ## 4. Scribble Mode -Wavy SVG underlines and strikethroughs that draw themselves via `stroke-dashoffset`. +Wavy SVG underline that draws itself via `stroke-dashoffset`. ```html @@ -227,22 +127,14 @@ Wavy SVG underlines and strikethroughs that draw themselves via `stroke-dashoffs stroke-linecap="round" /> -
+ ``` ```css -.mh-scribble-wrap { - position: relative; - display: inline; -} -.mh-scribble-text { - position: relative; - z-index: 1; -} .mh-scribble-svg { position: absolute; left: 0; - bottom: -6px; + bottom: -6px; /* strikethrough variant: top: 50%; transform: translateY(-50%) */ width: 100%; height: 24px; z-index: 0; @@ -250,38 +142,17 @@ Wavy SVG underlines and strikethroughs that draw themselves via `stroke-dashoffs ``` ```js -// Measure path length and set initial dash state -var path = document.querySelector("#scribble-1"); -var len = path.getTotalLength(); +const path = document.querySelector("#scribble-1"); +const len = path.getTotalLength(); gsap.set(path, { strokeDasharray: len, strokeDashoffset: len }); - -// Draw the line -tl.to( - "#scribble-1", - { - strokeDashoffset: 0, - duration: 0.8, - ease: "power1.inOut", - }, - 0.7, -); +tl.to("#scribble-1", { strokeDashoffset: 0, duration: 0.8, ease: "power1.inOut" }, 0.7); ``` -### Strikethrough Variant - -Position the SVG at `top: 50%; transform: translateY(-50%)` instead of `bottom: -6px`. - -### Wavy Path Generator - -Scale the path's viewBox width to match text width. The wave pattern `Q x1,y1 x2,y2` alternates between `y=0` and `y=24` for a natural wobble. Adjust the control points for tighter or looser waves: - -- **Tight waves**: smaller x-increments (25px per half-wave) -- **Loose waves**: larger x-increments (50px per half-wave) -- **Amplitude**: change the y range (0-24 for standard, 0-16 for subtle) +Path tuning: the `Q` control points alternate y between 0 and 24 for a natural wobble. Tighter waves = smaller x-increments (~25px per half-wave); looser = ~50px; subtler amplitude = y range 0–16. ## 5. Sketchout Mode -Cross-hatch lines over de-emphasized text. Multiple angled lines create a "crossed out" effect. +Cross-hatch over de-emphasized text — two angled lines create a "crossed out" effect. ```html @@ -294,22 +165,11 @@ Cross-hatch lines over de-emphasized text. Multiple angled lines create a "cross ``` ```css -.mh-sketchout-wrap { - position: relative; - display: inline; -} -.mh-sketchout-text { - position: relative; - z-index: 0; -} .mh-sketchout-lines { position: absolute; - top: 0; - left: -4px; - right: -4px; - bottom: 0; + inset: 0 -4px; overflow: hidden; - z-index: 1; + z-index: 1; /* text at z-index: 0 — the lines cross OVER it */ } .mh-sketchout-line { position: absolute; @@ -320,7 +180,6 @@ Cross-hatch lines over de-emphasized text. Multiple angled lines create a "cross height: 2px; background: #e53935; transform-origin: left center; - transform: scaleX(0); } .mh-sketchout-fwd { transform: scaleX(0) rotate(-12deg); @@ -331,43 +190,19 @@ Cross-hatch lines over de-emphasized text. Multiple angled lines create a "cross ``` ```js -// Forward slash draws first -tl.to( - "#sketchout-1 .mh-sketchout-fwd", - { - scaleX: 1, - duration: 0.3, - ease: "power2.out", - }, - 1.0, -); - -// Backward slash follows -tl.to( - "#sketchout-1 .mh-sketchout-bwd", - { - scaleX: 1, - duration: 0.3, - ease: "power2.out", - }, - 1.15, -); +// Forward slash first, backward follows +tl.to("#sketchout-1 .mh-sketchout-fwd", { scaleX: 1, duration: 0.3, ease: "power2.out" }, 1.0); +tl.to("#sketchout-1 .mh-sketchout-bwd", { scaleX: 1, duration: 0.3, ease: "power2.out" }, 1.15); ``` ## Combining Modes in Captions -Use mode cycling for visual variety across caption groups: +Cycle modes across caption groups for visual variety — every 2-3 groups for high energy, 3-4 for medium, 4-5 for low: ```js -var MODES = ["highlight", "circle", "burst", "scribble"]; - -GROUPS.forEach(function (group, gi) { - var mode = MODES[gi % MODES.length]; - // Apply the mode's CSS pattern to emphasis words in this group - group.emphasisWords.forEach(function (word) { - applyMode(word.el, mode, tl, word.start); - }); +const MODES = ["highlight", "circle", "burst", "scribble"]; +GROUPS.forEach((group, gi) => { + const mode = MODES[gi % MODES.length]; + group.emphasisWords.forEach((word) => applyMode(word.el, mode, tl, word.start)); }); ``` - -Cycle every 2-3 groups for high energy, every 3-4 for medium, every 4-5 for low. diff --git a/skills/hyperframes-animation/rules/cursor-click-ripple.md b/skills/hyperframes-animation/rules/cursor-click-ripple.md index 007ea18e3..95ef75b98 100644 --- a/skills/hyperframes-animation/rules/cursor-click-ripple.md +++ b/skills/hyperframes-animation/rules/cursor-click-ripple.md @@ -7,80 +7,24 @@ metadata: # Cursor Click Ripple -An animated cursor moves to a target element, performs a click with visual depression, and emits expanding ripple rings from the click point. +An animated cursor moves to a target element, performs a click with visual depression, and emits expanding ripple rings from the click point. Three sequential phases on one timeline: **move** (eased translation to the target's center) → **click** (scale depression on cursor + target together, yoyo back) → **ripple** (1–3 staggered rings expand and fade from the click point). This is a _point event at one location_ — a sustained hold across space is [cursor-drag.md](cursor-drag.md). -## How It Works - -Three sequential phases driven by a single GSAP timeline: - -1. **Move**: eased cursor translation from entry point to the target element's center -2. **Click**: scale depression on both cursor and target (yoyo: shrink then return) -3. **Ripple**: expanding circles radiate outward from the click point with fade-out. 1–3 staggered rings amplify the click feedback - -Use a GSAP timeline because the phase ordering (move → settle → click → ripples) is exactly what timelines express cleanly. - -## HTML +## Recipe ```html -
- - -
- - - -
- - -
-
-
-
+ +
+ +
+
+
``` -## CSS - -Position cursor at the entry point. Button sits at its final position. Ripples are at the click-target center with `scale: 0` and `opacity: 0` so they hold invisible until the timeline trigger: - ```css -.scene { - position: relative; - width: 100%; - height: 100%; -} - -.target-button { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - /* ...button styling (background, color, font from project tokens) */ -} - -.cursor { - position: absolute; - left: 10%; - top: 80%; /* entry corner */ - pointer-events: none; - z-index: 999; -} - .ripple { position: absolute; left: 50%; - top: 50%; /* click target center */ + top: 50%; /* click-target center */ width: 100px; height: 100px; border-radius: 50%; @@ -91,172 +35,70 @@ Position cursor at the entry point. Button sits at its final position. Ripples a } ``` -## GSAP Timeline +```js +// Phase 1 — Move: eased, not linear +tl.to(".cursor", { x: TARGET_X, y: TARGET_Y, duration: MOVE_DUR, ease: MOVE_EASE }, 0); -Build a paused timeline. Register it on `window.__timelines` with the same key as `data-composition-id` on the scene root. All tuning values are named constants — see How to Choose Values below. +// Phase 2 — Click: cursor + target depress together, then return +tl.to( + ".cursor", + { scale: CURSOR_PRESS_SCALE, duration: PRESS_DUR, ease: "power2.in", yoyo: true, repeat: 1 }, + CLICK_AT, +); +tl.to( + ".target-button", + { scale: TARGET_PRESS_SCALE, duration: PRESS_DUR, ease: "power2.in", yoyo: true, repeat: 1 }, + CLICK_AT, +); -```html - - +// Phase 3 — Ripple burst, N rings staggered from the click point +tl.set([".ripple-1", ".ripple-2", ".ripple-3"], { opacity: 1 }, RIPPLE_AT); +tl.to( + [".ripple-1", ".ripple-2", ".ripple-3"], + { + scale: RIPPLE_SCALE, + opacity: 0, + duration: RIPPLE_DUR, + ease: RIPPLE_EASE, + stagger: RIPPLE_STAGGER, + immediateRender: false, // holds scale 0 / opacity 0 until the click moment + }, + RIPPLE_AT, +); ``` -## How to Choose Values - -- **MOVE_DUR** — cursor travel time from entry to target, in seconds - - Range: 0.4–1.0 s - - Effects: short feels darting; long feels deliberate / "considered click" - - Constraints: must end before `CLICK_AT` — otherwise the click fires while the cursor is still moving and reads as a misclick - - Reference: ../../examples/cta-orbit-collapse.html uses 0.5 s - -- **MOVE_EASE** — easing family for the move tween - - Discrete choice. Options: - - `power2.inOut` — symmetric, calm; good for "the user thoughtfully moves the cursor" - - `back.out()` — overshoot landing; good when the click target is a button you want the cursor to "settle onto" with a tiny visible recoil. Pair with a low overshoot coefficient (~1.2–1.4) — higher reads as cartoonish - - `power3.out` — fast start, soft landing; good for a "decisive" move - - Reference: ../../examples/cta-orbit-collapse.html uses `back.out(1.3)` - -- **CLICK_AT** — time the click fires, in seconds - - Range: must be ≥ `MOVE_DUR` (cursor has settled); typically `MOVE_DUR + 0.0–0.3 s` of "decision pause" - - Effects: zero pause reads as autopilot; >0.3 s of pause reads as hesitation - - Reference: ../../examples/cta-orbit-collapse.html clicks 0.2 s after the cursor settles - -- **PRESS_DUR** — half-duration of the depression (the yoyo runs twice this) - - Range: 0.06–0.12 s - - Effects: short feels crisp; long feels mushy - - Constraints: total press = `2 * PRESS_DUR`; must finish before the next scene phase needs the cursor / target back at normal scale - - Reference: ../../examples/cta-orbit-collapse.html uses 0.08 s - -- **CURSOR_PRESS_SCALE / TARGET_PRESS_SCALE** — how far each compresses during the click - - Range: cursor 0.80–0.90; target 0.92–0.97 - - Effects: smaller numbers = stronger "this click counts" feel; values close to 1 read as a gentle tap - - Constraints: cursor compresses MORE than the target — the cursor is the actor, the target is the recipient - - Reference: ../../examples/cta-orbit-collapse.html uses cursor 0.85 / target 0.95 - -- **RIPPLE_AT** — when the rings start expanding, in seconds - - Range: `CLICK_AT + 0.0–0.08 s` - - Effects: simultaneous with the press feels causal; slight delay feels acoustic ("the click happens, then the wave radiates") - - Reference: ../../examples/cta-orbit-collapse.html starts the ripple at `CLICK_AT` exactly - -- **RIPPLE_DUR** — how long each ring takes to fully expand and fade - - Range: 0.5–1.0 s - - Effects: short rings feel sharp; long rings feel like a soft sonar - - Constraints: must complete before any phase that depends on the ring being gone (e.g. a screen wipe) - - Reference: ../../examples/cta-orbit-collapse.html uses 0.7 s - -- **RIPPLE_SCALE** — final scale of each ring before it fades - - Range: 3–6 - - Effects: 3 keeps the ring near the click site; 6 lets it sweep the surrounding area - - Constraints: if the ring would exit the visible frame before opacity reaches 0, lower the scale or shorten the duration - - Reference: ../../examples/cta-orbit-collapse.html uses 5 - -- **RIPPLE_STAGGER** — delay between consecutive rings - - Range: 0.06–0.12 s (or 0 for a single ring; see Variations) - - Effects: below ~0.06 s reads as one thick ring; above ~0.12 s reads as separate events - - Reference: ../../examples/cta-orbit-collapse.html uses a single ring (no stagger) - -- **RIPPLE_EASE** — easing family for the expansion - - Discrete choice. Options: - - `power2.out` — fast start, soft tail; the standard "ping" feel - - `power3.out` — even sharper attack, longer tail - - `expo.out` — almost-instant expansion with a long quiet fade; reads as a strong, distant pulse - - Reference: ../../examples/cta-orbit-collapse.html uses `power2.out` - -- **TARGET_X / TARGET_Y** — pixel offset of the click target from the cursor's CSS-laid origin - - These are layout-derived, not creative knobs — they must match the visual centroid of the actual click target. A 4 px miss reads as missing the button - - Reference: ../../examples/cta-orbit-collapse.html targets the white button at `CENTER_X + 130, CENTER_Y + 15` - ## Variations -- **Single ring** — keep one `.ripple` element, drop the stagger; reads as more elegant when the rest of the scene is busy -- **Keyframed attack-decay** — replace the simple expand-and-fade with a `keyframes` block that ramps opacity 0 → peak → 0 across the duration; gives a clearer "energy radiates and dissipates" envelope (used in ../../examples/cta-orbit-collapse.html) -- **Multi-ring expanding pulse** — 3 rings with 0.08 s stagger feels richer when the click is the climactic moment of the scene +- **Single ring** — one `.ripple`, no stagger; more elegant when the rest of the scene is busy. +- **Keyframed attack-decay** — a `keyframes` block ramps opacity 0 → peak → 0 across the duration; a clearer "energy radiates and dissipates" envelope. +- **Multi-ring expanding pulse** — 3 rings at 0.08 s stagger when the click is the scene's climactic moment. -## Key Principles +## Values -- **Move before click**: trigger the click only after the move tween has settled — clicking mid-motion reads as unintentional -- **Synchronized depression**: cursor + target depress at the same `position` time with the same duration (and both yoyo back) -- **Ripple from click point**: ripples expand from the exact click location (the button's visual center), not from any element's bounding-box origin -- **Subtle scale**: cursor compresses more than the target — see `CURSOR_PRESS_SCALE` / `TARGET_PRESS_SCALE` -- **High z-index cursor**: cursor renders above all content for the entire sequence +| token | range | notes | +| --------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| MOVE_DUR | 0.4–1.0 s | short darts; long reads as a "considered click." Must end before CLICK_AT or it reads as a misclick | +| MOVE_EASE | discrete choice | `power2.inOut` calm · `power3.out` decisive · `back.out(1.2–1.4)` settles onto the button with a tiny recoil (higher reads cartoonish) | +| CLICK_AT | `MOVE_DUR + 0–0.3 s` | zero pause reads as autopilot; >0.3 s reads as hesitation | +| PRESS_DUR | 0.06–0.12 s (half; yoyo ×2) | short crisp, long mushy; must finish before the next phase needs normal scale | +| CURSOR / TARGET_PRESS_SCALE | 0.80–0.90 / 0.92–0.97 | cursor compresses MORE than the target — the cursor is the actor, the target the recipient | +| RIPPLE_AT | `CLICK_AT + 0–0.08 s` | simultaneous feels causal; slight delay feels acoustic | +| RIPPLE_DUR | 0.5–1.0 s | sharp ping vs soft sonar; must complete before anything that needs the ring gone | +| RIPPLE_SCALE | 3–6 | 3 stays near the click site; if the ring would exit the frame before fading, lower it | +| RIPPLE_STAGGER | 0.06–0.12 s (or 0) | below ~0.06 s reads as one thick ring; above ~0.12 s as separate events | +| RIPPLE_EASE | discrete choice | `power2.out` standard ping · `power3.out` sharper attack · `expo.out` strong distant pulse | +| TARGET_X / TARGET_Y | layout-derived | must match the target's visual centroid — a 4 px miss reads as missing the button | + +Reference values: `../../examples/cta-orbit-collapse.html` — 0.5 s move on `back.out(1.3)`, click +0.2 s, press 0.08 s at 0.85/0.95, single ring to 5× over 0.7 s `power2.out`. ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })`. Never call `tl.play()` — HyperFrames seeks the timeline frame-by-frame deterministically -- **Registry key = `data-composition-id`**: `window.__timelines[""]` must match the `data-composition-id` on the scene root exactly -- **`immediateRender: false` on the ripple expand**: holds the initial state (`scale: 0`, `opacity: 0`) until the click moment, otherwise the tween pre-renders and the rings appear at the wrong size at t=0 -- **Finite duration**: verify `tl.duration()` matches the scene's `data-duration` -- **`pointer-events: none` on cursor + ripples**: they're purely visual; never block underlying interactivity (matters for hover-able exports) -- **No CSS transitions / animations**: all motion lives in the GSAP timeline so seek stays deterministic +- **Move before click** — trigger the click only after the move tween settles; clicking mid-motion reads as unintentional. +- **Rings live in DOM from t=0** at the click-target center with `scale: 0` + `opacity: 0` — never conditionally rendered; `immediateRender: false` on the expand so they hold invisible until the trigger. +- **Ripple from the click point** — the button's visual center, not any element's bounding-box origin. +- **Synchronized depression** — cursor + target depress at the same position with the same duration, and both yoyo back. +- **Cursor above all content** (high z-index) for the whole sequence; `pointer-events: none` on cursor + ripples. -## Combinations +## See also -- [orbit-3d-entry.md](orbit-3d-entry.md) — when the click is the pivot that collapses orbiting elements toward the cursor's target -- [center-outward-expansion.md](center-outward-expansion.md) — the click can be the trigger for an outward burst from the click point -- [press-release-spring.md](press-release-spring.md) for stronger physical feel on the target button -- [scale-swap-transition.md](scale-swap-transition.md) for the button's state change after click (button morphs into success state, next view, etc.) - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + tween API reference (eases, stagger, `immediateRender`, etc.) -- `/hyperframes-core` — composition wiring (`data-*` attributes, scene structure, registration contract) -- `/hyperframes-cli` — `hyperframes lint` to verify the registry key + duration match +`orbit-3d-entry` (click as the pivot that collapses orbiters) · `center-outward-expansion` (click triggers an outward burst) · `press-release-spring` (stronger physical feel on the target) · `scale-swap-transition` (the button's post-click state change). diff --git a/skills/hyperframes-animation/rules/cursor-drag.md b/skills/hyperframes-animation/rules/cursor-drag.md new file mode 100644 index 000000000..e24959159 --- /dev/null +++ b/skills/hyperframes-animation/rules/cursor-drag.md @@ -0,0 +1,141 @@ +--- +name: cursor-drag +description: The drag verb for driven cursors — grab, lift, travel, drop-snap. A semi-transparent ghost chip rides the cursor in exact lockstep and snaps into a placed field with selection chrome; variants cover fill-handle auto-fill down rows, corner-handle proportional resize (uniform scale only), and grab-lift-reorder with the neighbor springing into the vacated slot. +metadata: + tags: cursor, drag, drop, ghost, handle, resize, reorder, snap, interaction, mouse +--- + +# Cursor Drag + +> Cursor look, sizing, off-screen entry, and tip-targeting defer to the **oversized-cursor house doctrine** — this rule owns the drag _mechanics_ only. + +THE held-journey verb: the cursor presses down on a payload, carries it, and releases it somewhere else. The load-bearing law is **lockstep**: the cursor tip and the payload's grip point move as one rigid object for the entire travel — a one-frame drift reads as the chip slipping out of the hand. Distinct from [cursor-click-ripple.md](cursor-click-ripple.md) (move → point event at a single location): a drag is a _sustained hold across space_, and the payload is the co-star. Reuse [physics-press-reaction.md](physics-press-reaction.md) for the grab's press dip (cursor + payload compress together); for N simultaneous actors see [multi-cursor-choreography.md](multi-cursor-choreography.md) — this rule is one protagonist performing a workflow beat. + +## How It Works + +Five beats: **approach** (cursor glides to the source chip, `power2.inOut`) → **grab** (press dip on cursor + chip together; on the down-beat `tl.set` reveals the **ghost** — a pre-rendered semi-transparent clone at the chip's position — plus a small lift `fromTo` to `GHOST_LIFT_SCALE` with a soft shadow, `immediateRender: false`) → **travel** (cursor and ghost move as **matched tweens**) → **drop** (ghost off, placed field pops in with selection chrome) → **adjust / exit** (optional handle resize, then the cursor glides to the next target). + +Matched tweens = same timeline position, same duration, same ease, over straight lines — that keeps the pair rigidly locked at every eased midpoint. A shared `[cursor, ghost]` targets array only works when both need identical deltas; with different start points, use two matched `fromTo`s. Rule-specific corollary of the contract's absolute-values law: a relative `+=` travel on either partner breaks the lockstep under seek. + +Measure chip and slot rects at build time — a 4 px miss on the drop line reads as a failed drag (montage: authored CSS-matched constants, per the contract). `TIP_OFFSET_X/Y` aligns the cursor's TIP (not its bbox) with the grip point. + +## Recipe + +```html + +
⋮⋮ {chipLabel}
+
⋮⋮ {chipLabel}
+
+ {placedLabel} + +
+
+``` + +```js +const chipRect = document.querySelector("#source-chip").getBoundingClientRect(); +const slotRect = document.querySelector("#placed-field").getBoundingClientRect(); +const TRAVEL_DX = slotRect.left - chipRect.left; +const TRAVEL_DY = slotRect.top - chipRect.top; + +// Travel — MATCHED tweens: same position, duration, ease; absolute endpoints. +tl.fromTo( + "#drag-ghost", + { x: 0, y: 0 }, + { x: TRAVEL_DX, y: TRAVEL_DY, duration: TRAVEL_DUR, ease: TRAVEL_EASE, immediateRender: false }, + TRAVEL_AT, +); +tl.fromTo( + "#cursor", + { x: chipRect.left + TIP_OFFSET_X, y: chipRect.top + TIP_OFFSET_Y }, + { + x: chipRect.left + TIP_OFFSET_X + TRAVEL_DX, + y: chipRect.top + TIP_OFFSET_Y + TRAVEL_DY, + duration: TRAVEL_DUR, + ease: TRAVEL_EASE, + immediateRender: false, + }, + TRAVEL_AT, +); + +// Drop is a state commit: ghost off + placed field on at the SAME position. +tl.set("#drag-ghost", { opacity: 0 }, DROP_AT); +tl.fromTo( + "#placed-field", + { opacity: 0, scale: 0.92 }, + { opacity: 1, scale: 1, duration: SNAP_DUR, ease: "power3.out" }, + DROP_AT, +); +tl.fromTo( + [".select-box", ".handle"], + { opacity: 0, scale: 0.6 }, + { opacity: 1, scale: 1, duration: 0.18, ease: "power3.out", stagger: 0.02 }, + DROP_AT + SNAP_DUR * 0.4, +); +``` + +## Variations + +- **Corner-handle proportional resize** — width/height tweens are forbidden, so the resize renders as uniform `scale` with `transform-origin` at the **opposite (anchor) corner**: the anchor stays put, the dragged corner travels. The corner's position is _linear in scale_ (`corner = anchor + scale × (corner₀ − anchor)`), so a cursor tween to the corner's end position with the **same duration and ease** stays glued to the handle exactly: + + ```js + tl.to( + "#placed-field", + { scale: RESIZE_SCALE, transformOrigin: "0% 0%", duration: RESIZE_DUR, ease: "power2.inOut" }, + RESIZE_AT, + ); + tl.to( + "#cursor", + { x: CORNER_END_X, y: CORNER_END_Y, duration: RESIZE_DUR, ease: "power2.inOut" }, + RESIZE_AT, + ); + ``` + + One-axis resizes are `scaleX`/`scaleY` on the same origin logic — stretch-safe boxes only; route to [anchored-layout-expand.md](anchored-layout-expand.md)'s counter-scale when content must stay undistorted. + +- **Fill-handle auto-fill** — the spreadsheet verb: the cursor drags a cell's fill handle straight down on a `"none"` (linear) ease; each row commits via a snapped `tl.set` (never a fade) keyed to the handle's linear progress, so the fill edge and cursor never separate: + + ```js + tl.fromTo( + "#cursor", + { y: HANDLE_Y }, + { y: HANDLE_Y + FILL_DIST, duration: FILL_DUR, ease: "none", immediateRender: false }, + FILL_AT, + ); + gsap.utils.toArray(".fill-cell").forEach((cell, i) => { + tl.set(cell, { opacity: 1 }, FILL_AT + ((i + 1) / CELL_COUNT) * FILL_DUR); + }); + ``` + +- **Grab-lift-reorder** — lift = `y: -LIFT_RISE` + `rotation: LIFT_TILT` (sign from index parity) + shadow on; as the carried item crosses the neighbor's midpoint, the **neighbor springs into the vacated slot** (a `fromTo` translate at `TRAVEL_AT + TRAVEL_DUR * 0.5`, `power3.out`); drop = rotation → 0, shadow off, settle. The neighbor's counter-move sells the reorder — without it the list reads as broken. +- **Component grab between surfaces** — a chip dragged mockup-to-mockup, swapping identity on drop (`tl.set` recolor + label swap at `DROP_AT`, tiny settle pop); the drop chrome is just the identity swap, no handles. + +## Values + +| token | range | notes | +| --------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| approach / press | per cursor-click-ripple | approach 0.4–1.0 s; press-dip halves 0.06–0.12 s; cursor compresses more than the payload | +| GHOST_OPACITY | 0.5–0.75 | below 0.5 vanishes on busy documents; ~1.0 reads as the original moving — then hide `#source-chip` at the grab | +| GHOST_LIFT_SCALE / LIFT_DUR | 1.03–1.08 / 0.12–0.2 s | the shadow is the "off the surface" cue; the scale is garnish | +| TRAVEL_DUR / TRAVEL_EASE | 0.6–1.2 s / `power2.inOut` | a considered drag decelerates into the slot; `power1.inOut` for a calmer carry. `TRAVEL_AT ≥ GRAB_AT + 2×PRESS_DUR + LIFT_DUR` | +| DROP_AT / SNAP_DUR | `TRAVEL_AT + TRAVEL_DUR` exactly / 0.2–0.3 s | a gap between arrival and snap reads as the drop failing | +| RESIZE_SCALE / RESIZE_DUR | by story (≈0.4–0.6) / 0.6–1.0 s | `power2.inOut` | +| LIFT_RISE / LIFT_TILT | 6–12 px / 2–4° | reorder pickup; index-derived tilt sign | + +## Critical Constraints + +- **Lockstep is the law** — matched tweens over straight lines (or one shared tween when deltas are identical); verify at the eased midpoint, not just the endpoints. Absolute endpoints on both partners. +- **The ghost is pre-rendered** — a DOM clone at the source position from t=0, `opacity: 0`, revealed by `tl.set`; placed field and chrome likewise. Never cloned at runtime, never conditionally rendered. +- **Grab has weight** — press dip + lift shadow before any travel; a chip departing without a press reads as telekinesis. +- **Drop is a state commit** — ghost off and placed field on at the same timeline position, `DROP_AT = TRAVEL_AT + TRAVEL_DUR`. +- **Resizes are uniform `scale`, origin at the anchor corner** — never width/height; one-axis stretch on stretch-safe boxes only. +- **Linear ease on the fill-handle travel** — the evenly-spaced `tl.set` reveals depend on it; an eased handle bunches them at the ends. +- **One verb per beat** — drag, then resize, then exit; overlapping a travel with a resize turns choreography into mush. +- **`pointer-events: none`** on cursor, ghost, and chrome. + +## See also + +`physics-press-reaction` (the grab's press dip) · `cursor-click-ripple` (a plain click before/after) · `spring-pop-entrance` (the placed field's snap-settle) · `waterfall-entry` (kinetic fill cascade) · `multi-phase-camera` (the zoom-breathing carrier shot golden drag demos ride) · `multi-cursor-choreography` (this verb inside an ensemble). diff --git a/skills/hyperframes-animation/rules/depth-of-field-blur.md b/skills/hyperframes-animation/rules/depth-of-field-blur.md index 702fbc4d2..e1b974a8f 100644 --- a/skills/hyperframes-animation/rules/depth-of-field-blur.md +++ b/skills/hyperframes-animation/rules/depth-of-field-blur.md @@ -7,168 +7,64 @@ metadata: # 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. The motion is `filter: blur(Npx)` plus a small `opacity` dim, tweened from sharp(0) to blurred over the focus-shift window — both seek-safe, since `filter` and `opacity` are paint-only properties HF interpolates correctly frame-by-frame. - -This is the backing rule for the focus-falloff beat the blueprints keep reaching for: the outer nodes blurring during the push-in (`constellation-hub`), the rack-focus across a parallax card stack (`cursor-ui-demo`), and the non-highlighted cards dimming + blurring to spotlight the hero metric (`dataviz-countup`). Each of those flags "no backing rule" for the DoF half of the move — this is it. +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 single 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 tween targets `--dof: 0` (stays sharp); the off-focus layers target a positive blur. +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: 1. **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. -2. **Rack focus** — two adjacent windows on the same property: focus releases plane A (its blur ramps 0 → max) at the same position plane B's blur ramps max → 0. State continuity matters exactly as in `press-release-spring`: A's resting blur after the rack must be the value B held before it, so authoring the two as adjacent tweens on the same `--dof` is what makes the hand-off seamless. -3. **Blur-the-cluster-while-pushing-in** — the DoF tween runs concurrently with a camera push-in (`multi-phase-camera` / `coordinate-target-zoom`): the surrounding cluster blurs + dims on the SAME timeline position as the camera scales toward the focal core, so "the world recedes" and "we push in" read as one move. +2. **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 `--dof` at the same position so the hand-off is seamless. +3. **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. -Because the blur is a tween target (not a CSS `transition`), the renderer can land it at any frame — and because each layer's target is derived from its index / a data attribute (never `Math.random`), the falloff is identical on every seek. - -## HTML +## Recipe ```html -
-
- -
{FocalLabel}
- - -
{Context A}
-
{Context B}
-
{Context C}
-
+
+ +
{FocalLabel}
+ +
{Context A}
+
{Context B}
+
{Context C}
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgGradient}; -} .world { - /* Single wrapper so a concurrent camera push-in (multi-phase-camera) - transforms everything together; DoF is independent of the camera. */ + /* 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 is the px of blur; filter reads it. Starts sharp. */ - --dof: 0px; + --dof: 0px; /* px of blur; filter reads it — starts sharp */ filter: blur(var(--dof)); - /* will-change: filter — promotes the layer so the blur is cheap to - re-rasterize each frame. See the perf note in Key Principles. */ - will-change: filter; - font-family: {font}; - font-weight: 900; - color: {textColor}; + will-change: filter; /* promotes the layer so per-frame re-rasterization is cheap */ } .focal { - /* Sits above the context layers and never blurs. */ - z-index: 2; - font-size: FOCAL_FONT_SIZE; + z-index: 2; /* sharp layer must sit ABOVE the blurred ones, or its crisp + edges read as bleeding into the haze */ } .ctx { - /* The off-focus plane(s). Smaller / grouped so the blur radius can - stay modest yet still read — blurring a small layer is cheap. */ z-index: 1; - font-size: CTX_FONT_SIZE; - opacity: 1; } ``` -## GSAP Timeline - -```html - - -``` - -## Variations - -### Rack focus between two depth planes (foreground ⇄ background) - -Two adjacent tweens on the same `--dof` per plane — focus leaves plane A as it lands on plane B. State continuity: B's _resting_ blur before the rack equals what A holds after, so the hand-off has no jump. - ```js -// Start: A sharp, B pre-blurred (set BEFORE the rack so there's no pop). -gsap.set("#planeA", { "--dof": "0px", opacity: 1 }); -gsap.set("#planeB", { "--dof": `${MAX_BLUR}px`, opacity: DIM_LEVEL }); - -// Rack: A defocuses while B comes into focus, same position + duration. -tl.to( - "#planeA", - { "--dof": `${MAX_BLUR}px`, opacity: DIM_LEVEL, duration: RACK_DUR, ease: "power2.inOut" }, - RACK_START, -); -tl.to( - "#planeB", - { "--dof": "0px", opacity: 1, duration: RACK_DUR, ease: "power2.inOut" }, - RACK_START, -); -``` - -### Blur the cluster while pushing in (DoF + camera, one beat) - -Run the focal-pull tween at the **same timeline position** as a camera push-in so the surrounding cluster recedes into blur exactly as the camera scales toward the core. The camera transforms `.world`; the DoF tweens the layers — independent properties, no conflict. - -```js -// Camera push-in toward the focal core (see multi-phase-camera / coordinate-target-zoom). -tl.to( - "#world", - { scale: PUSH_SCALE, x: PUSH_X, y: PUSH_Y, duration: FOCUS_DUR, ease: "power2.inOut" }, - FOCUS_START, -); -// Cluster blurs + dims on the SAME position — "the world recedes as we push in." -ctx.forEach((el) => { +// 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, + opacity: DIM_LEVEL, // dim, not gone duration: FOCUS_DUR, ease: "power2.inOut", }, @@ -177,137 +73,40 @@ ctx.forEach((el) => { }); ``` -### Spotlight a hero metric in a card grid (dim + blur the rest) +## Variations -The `dataviz-countup` beat: a subset of grid cards stays sharp (the hero metric) while the remainder dim + blur. Tag the hero(es) and skip them; everything else defocuses on one shared window. +- **Rack focus between two depth planes** — `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. +- **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 `--dof` back to `0px` / opacity 1 over the tail (`REFOCUS_START + REFOCUS_DUR ≤ DURATION`). +- **Bounded focus-breathing on the focal layer (optional)** — a finite `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. -```js -gsap.utils.toArray(".card:not(.hero)").forEach((el) => { - tl.to( - el, - { "--dof": `${GRID_BLUR}px`, opacity: DIM_LEVEL, duration: FOCUS_DUR, ease: "power2.out" }, - FOCUS_START, - ); -}); -``` +## Values -### Refocus / settle (release the blur before the scene ends) +| 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 | -If the beat resolves back to "everything visible" (or hands off to a crossfade that needs a clean outgoing frame), ramp the blur back to 0 over the tail so the scene settles sharp instead of mid-defocus. - -```js -ctx.forEach((el) => - tl.to( - el, - { "--dof": "0px", opacity: 1, duration: REFOCUS_DUR, ease: "power2.inOut" }, - REFOCUS_START, - ), -); -``` - -### Bounded focus-breathing on the focal layer (optional) - -For a subtle "rack settling" feel, let the focal layer's blur breathe a hair around 0 during its hold — a _finite_ `ease:"none"` driver writing `sin()` into `--dof` (never `repeat:-1`, never a CSS animation). Keep the amplitude well under 1px or it reads as "still focusing." - -```js -const drift = { p: 0 }; -tl.to( - drift, - { - p: Math.PI * 2 * BREATH_CYCLES, - duration: BREATH_DUR, - ease: "none", - onUpdate: () => { - const b = Math.max(0, Math.sin(drift.p)) * FOCAL_BREATH_PX; // ≤ ~0.6px - document.getElementById("focal").style.setProperty("--dof", `${b}px`); - }, - }, - BREATH_START, -); -``` - -## How to Choose Values - -### Geometry / layout - -- **FOCAL_FONT_SIZE / CTX_FONT_SIZE** — focal vs context sizing. - - Range: focal is the visual lead; context layers smaller so a modest blur radius still reads as "out of focus." - - Effects: small context layers let you use a smaller `BLUR_PER_DEPTH` (cheaper) yet still look soft. -- **z-index** — focal `z-index: 2`, context `z-index: 1`. - - Constraints: the sharp focal layer must sit **above** the blurred ones, or its crisp edges read as bleeding into the haze. - -### Blur amounts - -- **BLUR_PER_DEPTH** — px of blur added per depth step (`data-depth`). - - Range: 3-6 px per step (a 3-plane stack tops out at ~9-18 px) - - Effects: low → gentle DoF; high → strong miniature/tilt-shift falloff - - Constraints: keep **per-layer blur ≤ ~24 px on large layers** — radius cost grows with both blur and area; large radius over a full-frame element is the expensive case (see Key Principles) -- **MAX_BLUR** — terminal blur for a fully-defocused plane (rack / focal-pull peak). - - Range: 8 (soft) → 16 (default) → 24 (heavy) px - - Constraints: above ~24 px on a big surface, prefer scaling the layer down or grouping its contents so the blurred footprint shrinks -- **GRID_BLUR** — blur on dimmed grid cards (spotlight variation). - - Range: 6-12 px — enough to push them back without losing the grid's shape - -### Dim amounts - -- **DIM_LEVEL** — opacity of off-focus layers at full defocus. - - Range: 0.4 (strong push-back) → 0.55 (default) → 0.7 (subtle) - - Effects: lower → context recedes hard / near-spotlight; higher → still legibly present, just secondary - - Constraints: rarely below 0.35 — fully dark off-focus layers read as "removed," not "defocused" - -### Timing - -- **FOCUS_START / FOCUS_DUR** — when the focal pull begins and how long the rack takes. - - Range: `FOCUS_DUR` 0.5-1.2 s — a rack/pull is a deliberate move, not a snap - - Effects: shorter → urgent "snap focus"; longer → languid cinematic rack -- **RACK_START / RACK_DUR** — rack-focus window (foreground ⇄ background). - - Constraints: both planes' tweens share `RACK_START` and `RACK_DUR` so they cross at the midpoint; `gsap.set` the pre-blurred plane BEFORE `RACK_START` -- **REFOCUS_START / REFOCUS_DUR** — settle-back window. - - Constraints: `REFOCUS_START + REFOCUS_DUR ≤ DURATION` so the scene actually reaches sharp before it ends / hands off -- **PUSH_SCALE / PUSH_X / PUSH_Y** (cluster-while-pushing-in variation) — camera move on `.world`. - - Constraints: shares `FOCUS_START` + `FOCUS_DUR` with the DoF tween so move and defocus read as one beat; counter-translate math lives in `coordinate-target-zoom` / `viewport-change` -- **BREATH_CYCLES / BREATH_DUR / FOCAL_BREATH_PX** (focus-breathing variation). - - Range: `FOCAL_BREATH_PX ≤ 0.6` px; period 2-3 s; this is a barely-there nicety, default to omitting it - -### Tokens - -- **{bgGradient}** — typically dark so the sharp focal layer reads as lit and forward -- **{textColor}** — high-contrast on `{bgGradient}`; the blur softens edges, so don't rely on hairline contrast -- **{font}** — display weight; blurred copy needs heavy weight to stay shape-legible when defocused - -## Key Principles - -- **`--dof` drives the blur; tween the variable, never a CSS `transition`.** Reading `filter: blur(var(--dof))` and animating `--dof` on the GSAP timeline keeps the blur on the HF seek clock. A CSS `transition` on `filter` interpolates on the browser's own clock and flickers/desyncs under frame-by-frame seek. -- **Blur the SMALL / GROUPED layers, not the giant one.** Filter-blur cost scales with both radius and the blurred element's pixel area. A 20 px blur on a full-frame background is the worst case; the same blur on a smaller context card, or on a single grouped wrapper, is cheap. Prefer pushing the focal plane _forward and sharp_ over cranking the background blur radius. -- **`will-change: filter`** on every layer that animates its blur — promotes it to its own layer so the re-rasterization each frame is cheap. Drop it once the blur settles if the layer also does heavy transform work. -- **Keep the radius modest.** ≤ ~24 px on large surfaces; lean on the `opacity` **dim** to do the "push it back" work alongside a smaller blur, rather than blur alone. Dim + modest blur reads more like real DoF than blur cranked to the max. -- **Focal layer stays genuinely sharp** — its `--dof` is `0` and untouched (or breathes ≤0.6 px). Any visible blur on the focal element kills the "this is the thing" read. -- **State continuity on a rack** — the plane coming OUT of focus must start the rack at the blur the incoming plane _was_ holding, and vice-versa; author both as tweens on the same `--dof` at the same position so the cross is seamless (same rule as `press-release-spring`'s press↔release). -- **DoF is independent of the camera** — blur the layers, transform `.world` for the push-in. They're different property channels, so they compose without fighting. Don't try to fake DoF with the camera transform or vice-versa. -- **Settle sharp before a hand-off** — if the next beat is a crossfade/push, refocus to `--dof:0` in the tail so the outgoing frame is crisp; handing off mid-defocus reads as "the render glitched." +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 -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition`** on `filter` / `opacity` — animate `--dof` and `opacity` on the timeline instead -- **No `repeat` / `yoyo` / infinite tweens** — the focus pull is a finite tween; any breathing is a bounded `onUpdate` reading the driver phase (or a finite tween), never `repeat:-1` -- **No `Math.random` / `Date.now`** — per-layer blur is derived from `data-depth` / element index so every seek is identical -- **Tween `filter` (blur) + `opacity` only here** — both paint-only and seek-safe. Use GSAP transform aliases (`x`, `y`, `scale`) for any concurrent camera move; never tween `width` / `height` / `left` / `top` -- **`will-change: filter`** on layers whose blur animates; keep the blurred footprint small -- **Per-layer blur radius ≤ ~24 px on large surfaces** — beyond that the cost (and visible banding) climbs; shrink/group the layer instead +- **Tween the `--dof` variable on the timeline** — reading `filter: 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 `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). +- **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 `--dof` at the same position. +- **DoF is independent of the camera** — blur the layers, transform `.world` for the push-in; don't fake DoF with the camera transform or vice-versa. +- **Settle sharp before a hand-off** — refocus to `--dof: 0` in 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`). -## Combinations +## See also -- [multi-phase-camera.md](multi-phase-camera.md) — the push-in / push-through whose focus-falloff this rule supplies; run the DoF tween at the same position as the PUSH phase -- [coordinate-target-zoom.md](coordinate-target-zoom.md) — zoom onto the focal core while the off-center layers blur (the `constellation-hub` hook) -- [viewport-change.md](viewport-change.md) — pan across a tilted card plane with a rack-focus between near and far cards (the `cursor-ui-demo` focus-pull) -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — the hero metric counts up sharp while the surrounding cards dim + blur (the `dataviz-countup` spotlight) -- [3d-page-scroll.md](3d-page-scroll.md) — the parallax card stack whose planes you rack focus between -- [sine-wave-loop.md](sine-wave-loop.md) — the focal layer idle-breathes after the rack settles (keep idle amplitude and focus-breath both tiny) - -## Pairs with HF skills - -- `/hyperframes-animation` — tweening a CSS custom property + multi-tween coordination -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +[multi-phase-camera.md](multi-phase-camera.md) (the push-in this rule's falloff accompanies) · [coordinate-target-zoom.md](coordinate-target-zoom.md) (zoom onto the focal core — the `constellation-hub` hook) · [viewport-change.md](viewport-change.md) (pan + rack across a tilted card plane) · [counting-dynamic-scale.md](counting-dynamic-scale.md) (hero metric counts up sharp — the `dataviz-countup` spotlight) · [3d-page-scroll.md](3d-page-scroll.md) (the parallax stack to rack between) · [sine-wave-loop.md](sine-wave-loop.md) (post-rack idle; keep both amplitudes tiny). diff --git a/skills/hyperframes-animation/rules/depth-scatter-assemble.md b/skills/hyperframes-animation/rules/depth-scatter-assemble.md index ffe9424b8..8d1968088 100644 --- a/skills/hyperframes-animation/rules/depth-scatter-assemble.md +++ b/skills/hyperframes-animation/rules/depth-scatter-assemble.md @@ -7,297 +7,133 @@ metadata: # Depth Scatter ↔ Assemble -N elements (glyphs, cards, icons, logo fragments) fly in from a rotating 3D depth-cloud and lock into a clean on-screen layout — or the reverse. Each element starts at a **deterministic** 3D offset (translateZ depth + rotateX/rotateY + an x/y scatter derived from its index), then tweens to its assembled flat position (`z: 0, rotation: 0`). Because every scattered position is computed by trig on the element's index — never `Math.random` — it renders identically every frame. - -Distinct from `orbit-3d-entry` (flip-in then a continuous orbit) and `center-outward-expansion` (a flat 2D burst from one shared center): here each element has its **own** point in a 3D cloud, and the resolve is a flat assembled layout, not an orbit or a radial spray. +N elements (glyphs, cards, logo fragments) fly in from a rotating 3D depth-cloud and lock into a flat layout — or the reverse. Each element has its OWN index-derived point in the cloud (translateZ depth + rotateX/Y tumble + x/y scatter). Distinct from `orbit-3d-entry` (flip-in then continuous orbit) and `center-outward-expansion` (flat burst from one shared center): here the resolve is a flat assembled layout. ## How It Works -Each element resolves to a flat layout position (`targetX/Y`, set once in CSS or via `data-*`). Its **scattered** state is derived from its index `i`: +Each element's flat target lives in `data-target-x/y`; its scattered state is pure trig on its index — golden-angle spread, stepped depth — so the cloud is byte-identical every render with no `Math.random`: ```js -const GOLDEN = Math.PI * (3 - Math.sqrt(5)); // ~2.39943 rad — even angular spread, no clumping -const a = i * GOLDEN; // this element's angle in the cloud -const scatterX = Math.cos(a) * RADIUS; // index-derived, deterministic +const GOLDEN = Math.PI * (3 - Math.sqrt(5)); // ~2.39943 rad — even spread, no clumping +const a = i * GOLDEN; +const scatterX = Math.cos(a) * RADIUS; const scatterY = Math.sin(a) * RADIUS; -const scatterZ = Z_NEAR - (i / (n - 1)) * (Z_NEAR - Z_FAR); // stepped depth across the cloud -const rotX = Math.sin(a) * TUMBLE; // tumble orientation, also from the angle +const scatterZ = Z_NEAR - (i / (n - 1)) * (Z_NEAR - Z_FAR); // stepped depth +const rotX = Math.sin(a) * TUMBLE; const rotY = Math.cos(a) * TUMBLE; ``` -A single 0→1 `progress` proxy interpolates each element between scattered and assembled (lerp every channel). At `progress = 0` the elements form the depth-cloud; at `progress = 1` they sit flat in the layout. Run it forward and it's **assemble**; the cloud itself slowly rotates (a stage `rotateY` tween) so the scatter has life before it locks. +Elements are PARKED at their scatter points (`gsap.set`, opacity 0) before any tween, then each tweens to its flat target while the whole stage slowly rotates so the scatter has life before it locks. Requires `perspective` on the scene root and `preserve-3d` on the stage AND each element, or depth + tumble flatten to a 2D scale. -Requires `perspective` on the stage and `transform-style: preserve-3d` on the stage AND each element, or the z-depth and tumble flatten to a 2D scale. - -## HTML +## Recipe ```html -
- -
-
{glyph1}
-
{glyph2}
-
{glyph3}
-
{glyph4}
-
{glyph5}
-
+ +
+
{glyph1}
+
{glyph2}
+
``` -For a logo lockup, `targetX/Y` describe the parts' resting layout; for kinetic type, one `.frag` per glyph (inject spans from the phrase string at setup so width is exact — see Variations). - -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; +.scene-root { display: grid; place-items: center; - background: {bgColor}; - perspective: 1400px; /* REQUIRED — without it, z-depth + tumble read as flat 2D scale */ + perspective: 1400px; /* REQUIRED */ } .cloud-stage { position: relative; - width: 100%; - height: 100%; display: grid; place-items: center; - transform-style: preserve-3d; /* REQUIRED — preserves child 3D context */ + transform-style: preserve-3d; will-change: transform; } .frag { position: absolute; - /* Live at stage center; GSAP translates each one to its layout / cloud point. */ top: 50%; left: 50%; - display: grid; - place-items: center; - font-family: {font}; - font-weight: 900; - font-size: 120px; - color: {textColor}; - transform-style: preserve-3d; /* each fragment keeps its own 3D context */ + transform-style: preserve-3d; backface-visibility: hidden; /* hides the mirrored face mid-tumble */ will-change: transform, opacity; } ``` -## GSAP Timeline +```js +const frags = Array.from(document.querySelectorAll(".frag")); +const n = frags.length; +const GOLDEN = Math.PI * (3 - Math.sqrt(5)); -```html - - +}); ``` ## Variations -### Tumble-swap (mid-shot hand-off between two phrases) +- **Tumble-swap** (the beat-change hand-off): two glyph sets share the cloud; ONE shared 0→1 progress tween drives both in its `onUpdate` — outgoing lerps layout→cloud with `opacity: 1−p`, incoming lerps cloud→layout with `opacity: p`. Two separate tweens drift out of phase under seek and the cross stops reading as one hand-off. Inject per-glyph spans per phrase at setup (measure advance widths after `document.fonts.ready` — single-scene only). +- **Radial letter-explode → resolve**: flat-plane special case — `Z_NEAR = Z_FAR = 0`, small `TUMBLE`; reverse the assemble for the explode. Pure in-plane. +- **Scatter-OUT**: reverse assemble (layout → cloud, opacity 1→0) ONLY as the composition's final beat — mid-shot it reads as the shot ending. +- **Parallax lockup**: back layers get deeper `|Z_FAR|` + longer `ASSEMBLE_DUR`, foreground shallower/shorter — depth-speeded slide-in that locks into the logo. -The signature for `kinetic-type-beats` beat changes: one phrase's glyphs scatter **into** the cloud at the same moment the next phrase's glyphs assemble **out** of it — a 3D hand-off between two states, never an empty frame. Two glyph sets share the cloud; drive both with one shared 0→1 `progress` so they cross deterministically. +## Values -```js -// outgoing[] and incoming[] are two glyph arrays, each with precomputed scatter[] (above). -const swap = { p: 0 }; -tl.to( - swap, - { - p: 1, - duration: SWAP_DUR, - ease: "power2.inOut", - onUpdate: () => { - const p = swap.p; - outgoing.forEach((el, i) => { - // 1 → 0: layout → cloud (scatters AWAY) - const s = outScatter[i]; - const tx = Number(el.dataset.targetX); - const ty = Number(el.dataset.targetY); - el.style.opacity = String(1 - p); - el.style.transform = - `translate(-50%,-50%) translate3d(${tx + (s.x - tx) * p}px,${ty + (s.y - ty) * p}px,${s.z * p}px)` + - ` rotateX(${s.rotationX * p}deg) rotateY(${s.rotationY * p}deg)`; - }); - incoming.forEach((el, i) => { - // 0 → 1: cloud → layout (assembles IN) - const s = inScatter[i]; - const tx = Number(el.dataset.targetX); - const ty = Number(el.dataset.targetY); - el.style.opacity = String(p); - el.style.transform = - `translate(-50%,-50%) translate3d(${s.x + (tx - s.x) * p}px,${s.y + (ty - s.y) * p}px,${s.z * (1 - p)}px)` + - ` rotateX(${s.rotationX * (1 - p)}deg) rotateY(${s.rotationY * (1 - p)}deg)`; - }); - }, - }, - SWAP_AT, -); -``` - -Inject a per-glyph span set for each phrase at setup (so `targetX` per glyph is the exact laid-out advance width — measure after `document.fonts.ready`), and hide each set's opacity to 0 until its window. - -### Radial letter-explode → resolve - -A flat-plane special case (the `kinetic-type-beats` "letters explode radially then resolve" GAP): set `Z_NEAR = Z_FAR = 0` and `TUMBLE` small so the cloud is a 2D ring, then reverse the assemble for the explode — fragments fling out to `scatter[i]` then snap back to layout. Pure in-plane, no depth. - -### Scatter-OUT (final-frame exit only) - -Reverse the assemble (layout → cloud, opacity 1→0) ONLY as the composition's last beat. A scatter-out mid-shot reads as an exit and breaks the shot — keep entrances and hand-offs as assemble or tumble-swap. - -### Parallax depth slide-in (logo lockup) - -For `logo-assemble-lockup`, give back layers a larger `|Z_FAR|` and a longer `ASSEMBLE_DUR`, foreground parts a shallower depth and shorter duration — parts at different depths slide in at different apparent speeds (parallax) and lock into the lockup. - -## How to Choose Values - -- **n (ELEMENT_COUNT)** — fragments / glyphs in the cloud - - Range: 4–14 (glyph sets follow the word length; for fragments/cards stay 4–9) - - Effects: few reads as deliberate assembly; many reads as a dense swarm condensing - - Constraints: above ~14 the cloud crowds the center and individual paths stop reading - -- **RADIUS** — cloud spread in the x/y plane, px - - Range: 250–700 px - - Effects: small = a tight knot that barely separates; large = fragments arrive from the frame edges - - Constraints: keep the farthest scatter inside frame at the chosen `perspective`, or fragments pop in from off-screen with no travel read - -- **Z_NEAR / Z_FAR** — depth band of the cloud, px (front / back) - - Range: Z_NEAR +150 to +450; Z_FAR −150 to −500 - - Effects: a wide band (e.g. +400 / −400) gives strong fly-toward / recede-from camera depth; a narrow band keeps it nearly flat - - Constraints: very large `|z|` against a short `perspective` over-distorts (fragments smear huge then tiny) — widen `perspective` to match - -- **TUMBLE** — peak rotateX/rotateY of scattered fragments, deg - - Range: 40–110° - - Effects: low = fragments drift in nearly upright; high = they tumble through space and rotate upright on arrival - - Constraints: with `backface-visibility: hidden`, glyphs past 90° show blank mid-tween (intended for the tumble); for cards with content on one face, cap near 80° - -- **ASSEMBLE_DUR** — per-fragment cloud → layout tween, s - - Range: 0.7–1.4 s - - Effects: short = snappy lock-in; long = a floating condense - - Constraints: `(n − 1) × STAGGER + ASSEMBLE_DUR` must fit the scene's assembly window - -- **ASSEMBLE_EASE** — shared ease across fragments - - Discrete choice: `power3.out`, `expo.out`, `back.out(1.4)` - - Selection: `power3.out` default (fly in, settle). `expo.out` snaps hard at the end. `back.out` adds a small overshoot as parts seat. Avoid `in` easings — fragments look sucked backward into the cloud mid-air. - -- **STAGGER** — gap between successive fragments' assembly starts, s - - Range: 0.03–0.09 s - - Effects: < 0.03 = a single chord (whole cloud collapses at once); > 0.09 = a slow drip that loses the "swarm" read - - Constraints: `n × STAGGER` should stay below `ASSEMBLE_DUR` so the cloud is collapsing as one motion, not a queue - -- **CLOUD_SPIN_DEG / CLOUD_SPIN_DUR** — stage rotateY over the assembly, deg / s - - Range: 15–60° over a duration ≥ `ASSEMBLE_DUR` - - Effects: a gentle spin gives the scatter life so it doesn't read as a frozen explosion diagram; too fast competes with the assembly - - Constraints: keep finite and ending by settle — no `repeat` - -- **SWAP_DUR / SWAP_AT** (tumble-swap) — hand-off length / when it fires, s - - Range: SWAP_DUR 0.5–1.0 s; SWAP_AT on the beat boundary - - Effects: shorter = a hard cross; longer = a visible dissolve-through-cloud - - Constraints: outgoing and incoming MUST share one `progress` (one tween) so they cross at the same instant - -## Key Principles - -- **`perspective` on the scene root + `preserve-3d` on stage AND each fragment** — without all three, z-depth and tumble collapse to a flat scale -- **Every scattered value is index-derived** — `cos/sin(i × GOLDEN)`, stepped `z` by `i/(n−1)`. The golden angle spreads points evenly with no clumps and (critically) **no `Math.random`**, so the cloud is byte-identical every render -- **`gsap.set` the cloud BEFORE adding tweens** — park each fragment at its scatter point with `opacity: 0` first; the assemble tweens FROM there. Skipping the set leaves frame 0 showing the assembled layout, then a teleport when the first tween starts -- **Resolve flat** — the settled state is `z: 0, rotationX: 0, rotationY: 0` in the layout. A cloud that resolves still-tilted reads as unfinished -- **Assemble / hand-off only; scatter-OUT is an exit** — fragments leaving for the cloud mid-shot reads as the shot ending. Use forward assemble for entrances, tumble-swap for beat changes; reserve scatter-out for the final frame -- **Depth ordering is automatic** — inside `preserve-3d`, paint order follows actual Z, so nearer fragments correctly occlude farther ones with no manual z-index (unlike the orbit case, where the orbit is faked in 2D and needs capped z-index) +| token | range | notes | +| ---------------------- | --------------------- | ----------------------------------------------------------------------------- | +| n | 4–14 (fragments 4–9) | above ~14 individual paths stop reading | +| RADIUS | 250–700px | keep the farthest scatter in frame or fragments pop in with no travel | +| Z_NEAR / Z_FAR | +150…+450 / −150…−500 | large `\|z\|` needs a wider `perspective` or fragments smear | +| TUMBLE | 40–110° | past 90° glyphs show blank mid-tween (intended); cap ~80° for one-faced cards | +| ASSEMBLE_DUR | 0.7–1.4s | | +| ASSEMBLE_EASE | `power3.out` default | `expo.out` snaps, `back.out(1.4)` seats with overshoot; never `in` | +| STAGGER | 0.03–0.09s | `n × STAGGER < ASSEMBLE_DUR` — one collapsing motion, not a queue | +| CLOUD_SPIN_DEG / \_DUR | 15–60° over ≥ dur | gentle life; too fast competes with the assembly | +| SWAP_DUR | 0.5–1.0s | on the beat boundary; shorter = hard cross | ## Critical Constraints -- **No `Math.random` / `Date.now`** — derive every scatter coordinate from the index (golden-angle trig + stepped depth). This is the whole point of the rule: a randomized cloud renders differently each frame and the seek breaks -- **No CSS `transition`** — all motion is GSAP tweens on the paused timeline -- **No `repeat` / `yoyo` / infinite** — the cloud spin and every assemble are finite, one-shot tweens that end before settle -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Transform aliases only** — `x`, `y`, `z`, `scale`, `rotation`/`rotationX`/`rotationY`. Never `width`/`height`/`left`/`top`; `x`/`y` compose with the `xPercent/yPercent -50` self-centering -- **`will-change: transform`** on stage + fragments — many simultaneous 3D transforms benefit from compositor hints -- **In tumble-swap, one shared `progress` for both glyph sets** — two separate tweens can drift out of phase under seek and the cross stops looking like a single hand-off +- **Every scattered value is index-derived** — `cos/sin(i × GOLDEN)` + stepped `z`. The golden angle spreads points evenly with no clumps and no `Math.random`. +- **`gsap.set` the cloud BEFORE adding tweens** — skipping it leaves frame 0 showing the assembled layout, then a teleport when the first tween starts. +- **`perspective` + `preserve-3d` on stage AND each fragment** — missing any one flattens the depth. +- **Resolve flat** — settled state is `z: 0`, rotations 0; a still-tilted resolve reads unfinished. +- **Tumble-swap: one shared progress for both glyph sets.** +- **Depth ordering is automatic** inside `preserve-3d` (paint order follows actual Z) — no manual z-index, unlike the orbit case's capped band. -## Combinations +## See also -- [orbit-3d-entry.md](orbit-3d-entry.md) — alternative 3D entrance (settles into a continuous orbit instead of a flat lockup); shares the `perspective` + `preserve-3d` stage setup -- [hacker-flip-3d.md](hacker-flip-3d.md) — per-glyph 3D flip/decode as the fragments seat; layer for a "letters tumble in AND decode on arrival" read -- [3d-text-depth-layers.md](3d-text-depth-layers.md) — give the assembled wordmark a stacked extrusion once it locks -- [center-outward-expansion.md](center-outward-expansion.md) — flat 2D cousin (single shared center, no depth) when perspective isn't wanted -- [press-release-spring.md](press-release-spring.md) — a spring settle on the assembled lockup once the cloud resolves -- [sine-wave-loop.md](sine-wave-loop.md) — idle breathe on the resolved layout instead of a frozen hold - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + `onUpdate` API (the shared-progress tumble-swap) -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`orbit-3d-entry` (settles into a continuous orbit instead) · `hacker-flip-3d` (glyphs decode on arrival) · `3d-text-depth-layers` (extrude the locked wordmark) · `center-outward-expansion` (flat 2D cousin) · `sine-wave-loop` (idle breathe on the resolved layout). diff --git a/skills/hyperframes-animation/rules/discrete-text-sequence.md b/skills/hyperframes-animation/rules/discrete-text-sequence.md index 3045b3a04..2d3ba111c 100644 --- a/skills/hyperframes-animation/rules/discrete-text-sequence.md +++ b/skills/hyperframes-animation/rules/discrete-text-sequence.md @@ -7,149 +7,99 @@ metadata: # Discrete Text Sequence -Instead of character-by-character typewriter, replace entire string states at time thresholds. Enables non-linear effects (typos, bulk additions, pauses, "thinking" gaps) that smooth per-char typing can't achieve. +Instead of character-by-character typewriter, replace entire string states at time thresholds — enabling non-linear effects (typos, backspaces, bulk paste, "thinking" gaps) that smooth per-char typing can't achieve. If your effect is "type each character, no edits", this rule is overkill — use the smooth-slice variation below. ## How It Works -An array of `{ text, t }` pairs where `t` is a time in seconds. On every onUpdate, scan the array for the latest entry whose `t` has passed and render that text. The display jumps between states; no animation between them. +The typing is authored as a sparse array of `{ t, text }` states; on every `onUpdate` a **reverse search** finds the latest entry whose `t` has passed and renders its text. Display jumps between states with no animation between them — the realism comes from the schedule shape: fast keystroke clusters (0.06–0.20s apart), pauses at word breaks (0.3–0.6s), a typo, backspaces peeling back to the fork, then a bulk paste replacing many chars in one entry. A block cursor blinks via a deterministic sin square wave on the same timeline. -For continuous per-char typewriter (no pauses, no edits), use the **smooth-slice** variation at the bottom. - -## HTML +## Recipe ```html -
-
-
$
-
- | - _ -
+ +
+
$
+
+ _
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; - font-family: {monoFont}; /* monospace is required — see Critical Constraints */ -} .terminal { + font-family: {monoFont}; /* monospace required — proportional jitters even in a fixed box */ display: flex; align-items: baseline; - gap: GUTTER; - font-weight: 800; font-size: TERMINAL_FONT_SIZE; - color: {textColor}; -} -.prompt { - color: {accentColor}; } .text-wrap { display: inline-flex; align-items: baseline; - /* Fixed-width container prevents the right side from jittering as - content changes length. Choose width ≥ longest state's width. */ - min-width: TEXT_WRAP_MIN_WIDTH; + min-width: TEXT_WRAP_MIN_WIDTH; /* ≥ widest state — stops right-edge jitter */ white-space: nowrap; } -.text { - color: {textColor}; -} .cursor { - display: inline-block; + display: inline-block; /* inline ignores width */ width: CURSOR_WIDTH; - color: {accentColor}; - margin-left: CURSOR_GAP; } ``` -## GSAP Timeline + Discrete State Logic +```js +// Each entry shows from its t until the NEXT entry's t. +// Shape: keystrokes → typo → backspace to the fork → bulk paste → completion mark. +const SEQUENCE = [ + { t: 0.0, text: "" }, + { t: T_K1, text: "{p1}" }, // first keystrokes (~3-5 chars, 0.1-0.2s apart) + { t: T_K2, text: "{p1 + ' ' + p2_typo}" }, // continuation containing a typo + { t: T_BS, text: "{p1 + ' ' + p2_partial}" }, // backspace(s) — peel back to the fork + { t: T_BULK, text: "{fullCorrectedText}" }, // bulk paste — many chars in one jump + { t: T_DONE, text: "{fullCorrectedText + ' ✓'}" }, // completion marker +]; -```html - - + }, + 0, +); ``` ## Variations -### Smooth character slice (continuous typewriter — no pauses, no edits) - -For straight-forward typewriter without the non-linear chaos: +- **Smooth character slice** (continuous typewriter — no pauses, no edits): faster to author but uniformly "machine-typed", missing the human realism: ```js const fullText = "{fullPhrase}"; @@ -168,106 +118,29 @@ tl.to( ); ``` -This is faster to author but produces a uniform "machine-typed" feel — missing the human-typing realism. +- **Thinking pause** — hold one state for `THINK_HOLD_DUR` (0.8–2.0s; under 0.5s reads as a stutter, not thought) simply by leaving a gap before the next entry's `t`. +- **State pulse on completion** — when the final state lands, `tl.to(".text", { scale: 1.03–1.08, duration: 0.15–0.3, yoyo: true, repeat: 1 }, T_DONE)`. +- **Per-state color shift** — in `onUpdate`, branch on `driver.t` vs the milestones: success color after `T_DONE`, dim mid-edit, normal while typing. -### Thinking pause (extended hold on a key state) +## Values -Insert a state that holds for `THINK_HOLD_DUR` seconds without changes — feels like the user paused to think: - -```js -{ t: T_PRE_PAUSE, text: '{partialPhrase}' }, // last state before the pause -// ... no entries for THINK_HOLD_DUR seconds ... -{ t: T_PRE_PAUSE + THINK_HOLD_DUR, text: '{resumedPhrase}' }, -``` - -### State pulse on completion - -When the final state lands (e.g. "✓"), pulse-scale the line briefly for emphasis: - -```js -tl.to( - ".text", - { scale: COMPLETION_PULSE_SCALE, duration: COMPLETION_PULSE_DUR, yoyo: true, repeat: 1 }, - T_DONE, -); -``` - -### Per-state color shift - -Color-code states by phase (e.g. dim during edit, success color after the completion marker, optional warning color on typo): - -```js -// In onUpdate after setting textContent: -if (driver.t > T_DONE) textEl.style.color = "{successColor}"; -else if (driver.t < T_K2) - textEl.style.color = "{textColor}"; // normal typing -else textEl.style.color = "{mutedColor}"; // mid-edit dim -``` - -## How to Choose Values - -### Layout - -- **TERMINAL_FONT_SIZE** — font size of the typing line. - - Range: 48-96 px for full-bleed compositions; smaller for terminal-style detail - - Constraints: combined with `TEXT_WRAP_MIN_WIDTH` must fit within viewport -- **TEXT_WRAP_MIN_WIDTH** — fixed-width container holding the text. - - Constraints: must be `≥ widthOf(longest SEQUENCE state) at TERMINAL_FONT_SIZE`. Measure with a hidden probe after `document.fonts.ready` if unsure - - Effects: too small → right edge jitters as states change length; too large → unused horizontal whitespace pads the composition -- **GUTTER** — flex gap between prompt glyph (`$`, `>`) and text. - - Range: ~0.3-0.5× `TERMINAL_FONT_SIZE` -- **CURSOR_WIDTH / CURSOR_GAP** — block cursor dimensions. - - Range: width ~0.3× `TERMINAL_FONT_SIZE`; gap small (single-digit px) so the cursor feels attached to the text - -### Sequence timing - -- **TOTAL_DURATION** — composition length. - - Constraints: must be ≥ `T_DONE` + ~1s climax dwell so viewer sees the completion marker -- **T_K1 / T_K2 / T_BS / T_BULK / T_DONE** — milestone timestamps within the SEQUENCE. - - Range: keystrokes 0.06-0.20s apart for "human typing"; pauses 0.3-0.6s at natural word breaks; bulk paste jumps multiple characters in a single entry - - Constraints: monotonically increasing; `T_DONE ≤ TOTAL_DURATION - dwell` -- **TYPE_DUR** (smooth-slice variation) — total typing duration for continuous typewriter. - - Range: `chars × 0.06s` (fast) to `chars × 0.12s` (relaxed) -- **THINK_HOLD_DUR** (thinking-pause variation) — hold time between two SEQUENCE states. - - Range: 0.8-2.0s; under 0.5s reads as a stutter rather than thought -- **COMPLETION_PULSE_SCALE / COMPLETION_PULSE_DUR** (pulse variation). - - Range: scale 1.03-1.08 (subtle), duration 0.15-0.30s - -### Cursor - -- **BLINK_CYCLES** — number of full blink cycles across `TOTAL_DURATION`. - - Range: `TOTAL_DURATION / 0.8s ≤ BLINK_CYCLES ≤ TOTAL_DURATION / 0.5s` (cycle every 0.5-0.8s reads as a natural cursor) - -### Color tokens - -- **{bgColor} / {textColor} / {accentColor} / {successColor} / {mutedColor}** — discrete choices, not numeric ranges. Pick from the composition's palette; the prompt + cursor share `{accentColor}` so they read as the same "system" element. - -## Key Principles - -- **Threshold sequence drives realism** — group fast successive keystrokes (0.1-0.2s apart), then pause on word breaks (0.3-0.5s), bulk-paste in single jumps (one entry replaces many chars), include a typo or two for human-typing feel -- **Reverse-search the array each frame** — O(n) per frame, where n is small (≤30 typical). Don't try to index by frame; the sequence is sparse -- **Fixed-width container is mandatory** — without `min-width`, the right edge of the text wrap jitters as state length changes. Set width ≥ longest expected state -- **Cursor must be deterministic** — sin-based or sequence-driven blink, NOT a CSS animation. HF seeks frame-by-frame; CSS animations desync -- **No `transition` on the text element** — discrete jumps should be INSTANT. A CSS transition turns the jump into a smear and ruins the "typing" feel -- **❗ Distinguish discrete from smooth** — if your effect is "type each character, no edits" → use the smooth-slice variation. Discrete sequence is overkill for that case. Use discrete only when you need non-linear states (typos, pauses, bulk paste) +| token | range | notes | +| ------------------- | -------------------------------------------- | ---------------------------------------------------------------------- | +| TERMINAL_FONT_SIZE | 48–96px | full-bleed comps; smaller for terminal-style detail | +| TEXT_WRAP_MIN_WIDTH | ≥ widest state | measure with a hidden probe after `document.fonts.ready` if unsure | +| milestone `t`s | keystrokes 0.06–0.20s apart; pauses 0.3–0.6s | monotonically increasing; `T_DONE ≤ TOTAL_DURATION − ~1s` climax dwell | +| TYPE_DUR (smooth) | `chars × 0.06–0.12s` | fast → relaxed | +| BLINK_CYCLES | one cycle per 0.5–0.8s | `TOTAL_DURATION / 0.8 ≤ BLINK_CYCLES ≤ TOTAL_DURATION / 0.5` | +| CURSOR_WIDTH | ~0.3× font size | gap to text single-digit px so the cursor feels attached | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition`** on the text or any of its parents -- **Cursor `display: inline-block`** — `display: inline` ignores width/transform -- **Monospace font** for terminal-style effects — proportional fonts cause visual jitter even with fixed-width container -- **Whitespace: nowrap** on text wrap — wrapping mid-state breaks the illusion +- **Reverse-search the array each frame** — O(n) with small n (≤30 typical); don't index by frame, the sequence is sparse. +- **`min-width` on the text wrap is mandatory** — without it the right edge jitters as state length changes. +- **Discrete jumps must be INSTANT** — any transition on the text turns the jump into a smear and kills the "typing" feel. +- **Cursor blink is sin/sequence-driven on the timeline**, `display: inline-block`, monospace font, `white-space: nowrap` (wrapping mid-state breaks the illusion; trailing spaces must survive). +- **Discrete vs smooth** — use discrete only for non-linear states (typos, pauses, bulk paste); plain typing takes the smooth-slice variation. -## Combinations +## See also -- [3d-text-depth-layers.md](3d-text-depth-layers.md) — discrete text rendered with layered depth (heavy, dramatic) -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — discrete text for the LABEL while counter animates smoothly -- [press-release-spring.md](press-release-spring.md) — after the sequence completes, the line "presses" like a button confirming success - -## Pairs with HF skills - -- `/hyperframes-animation` — onUpdate-driven discrete state lookup -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`context-sensitive-cursor` (same SEQUENCE pattern + segment-colored cursor) · `3d-text-depth-layers` (discrete text with layered depth) · `counting-dynamic-scale` (discrete label beside a smooth counter) · `press-release-spring` (post-completion press beat). diff --git a/skills/hyperframes-animation/rules/dynamic-content-sequencing.md b/skills/hyperframes-animation/rules/dynamic-content-sequencing.md index 14ab6ea7d..259cc6361 100644 --- a/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +++ b/skills/hyperframes-animation/rules/dynamic-content-sequencing.md @@ -7,301 +7,143 @@ metadata: # Dynamic Content Sequencing -A utility pattern (not a motion rule in itself) for scenes that show a SEQUENCE of items (cards, phrases, stats). Each item's duration is calculated from its content length + a per-item config; the sequencer assigns absolute start/end times automatically. Distinct from [discrete-text-sequence](discrete-text-sequence.md) (which is one text element changing states) — this rule swaps between distinct content blocks. +A utility pattern (not a motion rule in itself) for scenes that show a SEQUENCE of items (cards, phrases, stats): each item's duration is computed from its content length + per-item config, and the sequencer assigns absolute start/end times automatically — no hardcoded offsets per item. Distinct from [discrete-text-sequence](discrete-text-sequence.md) (one text element changing states) — this rule swaps between distinct content blocks. ## How It Works -1. Define a content array — each entry has `{ text, speedFactor, hold }` (or arbitrary fields) -2. Pre-compute absolute start times: `start[i] = sum of durations 0..i-1` -3. In onUpdate, find which entry is active (last entry whose `start ≤ time`) and render it +A content array of `{ eyebrow, title, body, speedFactor, hold }` entries is reduced once at build time into a flat `TIMELINE` of `{ …entry, start, end }` — duration per entry is `BASE_DURATION + body.length × SEC_PER_CHAR + hold`, so longer text earns more reading time. A single linear driver's `onUpdate` reverse-searches the active entry and swaps the DOM **only on transitions** (a `lastTitle` guard — per-frame `textContent` writes flicker in render); an optional progress bar fills 0→100% across the whole run. -The "dynamic" part: items with longer text get more screen time (formula: `baseDuration + textLength * msPerChar`). No hardcoded `from` / `durationInFrames` per item. - -## HTML +## Recipe ```html -
-
-
{eyebrow}
-
-
-
-
-
— {Brand}
+ +
+
+
+
+
``` -## CSS - -Placeholders: `{font}` is the project sans-serif stack; `{bgColor1}`/`{bgColor2}` make the dark backdrop gradient; `{accentColor}` highlights the eyebrow / brand / progress fill; `{textColor}` is the primary readable foreground. - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: radial-gradient(ellipse at center, {bgColor1} 0%, {bgColor2} 70%); - font-family: {font}; -} -.display { - display: flex; - flex-direction: column; - align-items: center; - gap: 32px; - text-align: center; - max-width: 1400px; -} -.eyebrow { - font-size: 32px; - font-weight: 800; - letter-spacing: 14px; - color: {accentColor}; - text-transform: uppercase; -} -.title { - font-size: 120px; - font-weight: 900; - letter-spacing: -2px; - line-height: 1; - color: {textColor}; -} .body { - font-size: 48px; - font-weight: 500; - line-height: 1.4; - color: {accentColor}; - opacity: 0.9; - min-height: 160px; /* reserve space so layout doesn't jump */ -} -.progress-bar { - width: 600px; - height: 4px; - background: {accentColor}26; /* ~15% alpha */ - border-radius: 2px; - margin-top: 16px; - overflow: hidden; + min-height: 160px; /* reserve space — content height varies; without this, layout jumps */ } .progress-fill { height: 100%; - background: linear-gradient(90deg, {accentColor} 0%, {accentColor2} 100%); width: 0%; } -.brand { - position: absolute; - bottom: 80px; - left: 50%; - transform: translateX(-50%); - font-size: 32px; - font-weight: 900; - letter-spacing: 12px; - color: {accentColor}; -} ``` -## GSAP Timeline +```js +// N entries, each with its own pacing (optionally a speedFactor multiplier); +// the final entry uses a larger hold (closing beat). +const CONTENT = [ + { eyebrow: "{eyebrow1}", title: "{title1}", body: "{body1}", hold: HOLD_MID }, + // … + { eyebrow: "{eyebrowN}", title: "{titleN}", body: "{bodyN}", hold: HOLD_FINAL }, +]; -```html - - + }, + 0, +); ``` ## Variations -### Crossfade between items (not hard cut) +- **Crossfade between items** — return BOTH adjacent entries during an overlap window (`time ≥ e.start − overlap && time ≤ e.end + overlap`, overlap ≈ 0.3s) and render them with opacities computed from distance to the boundary. +- **Per-item motion variation** — map an `entry.style` key to an existing rule per chapter (e.g. `3d-text-depth-layers` → `hacker-flip-3d` → `counting-dynamic-scale`); the sequencer only orchestrates timing. +- **Auto-extend composition duration** — you can set `data-duration` from the computed `TOTAL_DURATION` in script, but HF reads `data-duration` at composition load and setting it after init may not take effect — author the duration manually from a rough total. -Add `overlap` to the find function — return BOTH the previous and next entry during the overlap window, render with crossfade opacity: +### Accelerating cadence (geometric hold decay) + +For rhetorical escalation — "everyone says…", a roll-call, a praise flurry — the beat grid itself accelerates: early entries hold ~1s (read speed), then windows shrink geometrically into a ~0.15–0.3s flurry, braking on an emphasis state before the resolve. The acceleration is pre-computed into the same flat `TIMELINE` — still content-driven, still deterministic, no speed-up tween anywhere: ```js -function activeEntries(time, overlap = 0.3) { - const result = []; - TIMELINE.forEach((e) => { - if (time >= e.start - overlap && time <= e.end + overlap) result.push(e); - }); - return result; -} +// Geometric decay on the hold, clamped at a flurry floor; the brake state holds longest. +const HOLDS = CONTENT.map((entry, i) => Math.max(FLURRY_FLOOR, HOLD_START * Math.pow(DECAY, i))); +HOLDS[CONTENT.length - 1] = HOLD_FINAL; + +let cumulative = 0; +const TIMELINE = CONTENT.map((entry, i) => { + // Past ~0.5s states are glanced as motion texture, not read — + // drop the per-char term or you never reach flurry speed. + const readable = HOLDS[i] >= READ_THRESHOLD; + const dur = HOLDS[i] + (readable ? entry.body.length * SEC_PER_CHAR : 0); + const start = cumulative; + cumulative += dur; + return { ...entry, start, end: cumulative }; +}); ``` -Then render the two adjacent entries with computed opacities based on distance from boundary. +Worked example — **praise-chip flurry**: ~16 short quotes hard-cut through a chip beside a pinned wordmark. First 3 states at `HOLD_START = 1.0` (each reads fully); `DECAY = 0.8` shrinks every following window until `FLURRY_FLOOR = 0.2` catches it (≈12 states over ~2.5s — a churn of acclaim, individually glanced); the longest phrase takes `HOLD_FINAL ≈ 1.6` as the brake before the closing lockup. -### Per-item motion variation +Values: `HOLD_START` 0.8–1.2s; `DECAY` 0.75–0.88 (higher = longer runway before the flurry bites); `FLURRY_FLOOR` 0.15–0.3s (below ~0.15s swaps strobe); `READ_THRESHOLD` ~0.5s; brake ≥ 4× the floor or the stop doesn't register as a beat. The 3–6 entry guidance relaxes here — 12–18 states are legal precisely because flurry states aren't individually read. The hard-cut discipline (`lastTitle` guard, instant swaps) is what lets 0.2s states render clean. -Each entry has its own motion style. Map `entry.style` to one of the existing rules: chapter 1 uses [3d-text-depth-layers](3d-text-depth-layers.md), chapter 2 uses [hacker-flip-3d](hacker-flip-3d.md), chapter 3 uses [counting-dynamic-scale](counting-dynamic-scale.md). The sequencer just orchestrates timing; per-entry rendering uses the appropriate rule. +## Values -### Auto-extend composition duration +| token | range | notes | +| ------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------- | +| BASE_DURATION | 0.6–1.5s | minimum per entry regardless of length — even one-word entries get read time | +| SEC_PER_CHAR | 0.03–0.06 s/char | ≈17–33 chars/sec; uniform across the sequence so the pace reads as one engine; lean high for wide-character languages | +| HOLD_MID | 0.5–1.0s | dwell on a non-final entry; `< HOLD_FINAL` | +| HOLD_FINAL | 1.0–2.0s | climax dwell — must exceed HOLD_MID by a clear margin so the close reads as a beat | +| SPEED_FACTOR | 0.5–2.0 (default 1.0) | per-entry only; if every entry shares a factor, fold it into SEC_PER_CHAR | +| TAIL_PAD | 0.0–1.0s | quiet beat after the last entry; prefer 0 when the next composition owns the breath | +| CONTENT N | 3–6 entries | <3 isn't a sequence; >6 drags (accelerating cadence relaxes this — see above) | -If you don't know upfront how long the sequence will be (dynamic content count), bind `data-duration` to the computed `TOTAL_DURATION`. Do this in script BEFORE the timeline registers: - -```js -document - .querySelector("[data-composition-id]") - .setAttribute("data-duration", String(Math.ceil(TOTAL_DURATION))); -``` - -(Caveat: HF reads `data-duration` at composition load; setting after init may not take effect — author the duration manually based on a rough TOTAL calc.) - -## Key Principles - -- **Pre-compute timeline once, not per-frame** — building absolute start/end at script init means onUpdate is O(log n) reverse-search, not O(n²). -- **Per-item duration formula: `BASE_DURATION + body.length × SEC_PER_CHAR + hold`** — longer text needs more reading time. The formula is the load-bearing teaching of this rule; ranges for each const are in How to Choose Values. -- **Reserve `min-height` on body element** — content height varies per item; without reservation, layout jumps and downstream elements (progress bar, brand) jitter. -- **DOM update on transition, not every frame** — track `lastTitle` (or whatever key) and only call `textContent =` when it changes. Per-frame textContent assignment causes flicker in HF render. -- **Optional progress indicator** — a thin bar at the bottom showing 0-100% completes the "this is a sequence" framing. -- **Climax dwell longer than mid-sequence dwell** — the outro's `hold` (HOLD_FINAL) should exceed the in-sequence `hold` (HOLD_MID) so the final brand/CTA lands. - -## How to Choose Values - -- **BASE_DURATION** — minimum visible time of an entry regardless of content length - - Range: 0.6-1.5 s - - Effects: low end snaps through short entries too fast for the eye; high end stalls on short titles - - Constraints: ensures even one-word entries have time to read - - Reference: see `../../examples/messaging-multi-phrase.html` (and any blueprint that uses this rule) - -- **SEC_PER_CHAR** — extra time added per body character - - Range: 0.03-0.06 s/char (≈ 17-33 chars/sec read pace for video) - - Effects: low end feels rushed for paragraph-style bodies; high end feels slow when bodies are short - - Constraints: should be uniform across the sequence so the pace reads as one engine; for languages with wider characters, lean to the high end - - Reference: see `../../examples/messaging-multi-phrase.html` (and any blueprint that uses this rule) - -- **HOLD_MID** — dwell after the typing of a non-final entry completes - - Range: 0.5-1.0 s - - Effects: low end feels rushed; high end feels lazy - - Constraints: `HOLD_MID < HOLD_FINAL` - - Reference: see `../../examples/messaging-multi-phrase.html` (and any blueprint that uses this rule) - -- **HOLD_FINAL** — dwell on the last entry (outro / climax) - - Range: 1.0-2.0 s - - Effects: low end truncates the closing beat; high end overstays - - Constraints: must exceed HOLD_MID by a clear margin so the close reads as a beat, not another mid-sequence pause - - Reference: see `../../examples/messaging-multi-phrase.html` (and any blueprint that uses this rule) - -- **SPEED_FACTOR** — per-entry pacing multiplier - - Range: 0.5-2.0 (default 1.0) - - Effects: <1 stretches an entry's body-driven duration (good for high-density passages); >1 compresses it - - Constraints: discrete choice — use 1.0 unless one entry needs special pacing; if every entry uses the same factor, fold it into SEC_PER_CHAR instead - - Reference: see `../../examples/messaging-multi-phrase.html` (and any blueprint that uses this rule) - -- **TAIL_PAD** — seconds added to `TOTAL_DURATION` after the last entry's `end` - - Range: 0.0-1.0 s - - Effects: 0 ends the driver exactly at the last `hold` completion; >0 leaves a quiet beat (useful before a transition to the next composition) - - Constraints: if downstream is another composition, prefer 0 and handle the breath at the composition seam - - Reference: see `../../examples/messaging-multi-phrase.html` (and any blueprint that uses this rule) - -- **CONTENT length (N)** — number of entries in the sequence - - Range: 3-6 entries - - Effects: <3 isn't a sequence (use a static scene); >6 drags - - Constraints: each entry's `title` must fit one line at the chosen `.title` fontSize; bodies should fit within `min-height` after wrapping - - Reference: see `../../examples/messaging-multi-phrase.html` (and any blueprint that uses this rule) +Reference: `../../examples/messaging-multi-phrase.html`. ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Pre-compute the TIMELINE array** — don't recompute in onUpdate -- **`min-height` on body** for layout stability -- **DOM swap only on entry transition** — use lastTitle/lastKey guard -- **Sequential only** — for parallel tracks, use a different reduction (this rule is sequential) +- **Pre-compute the TIMELINE once at build** — never recompute in `onUpdate`; the reverse search over the flat array is the whole per-frame cost. +- **DOM swap only on entry transition** (`lastTitle`/key guard) — per-frame `textContent` assignment flickers in HF render. +- **`min-height` on the body element** — without reservation, downstream elements (progress bar, brand) jitter as content height varies. +- **Sequential only** — for parallel tracks use a different reduction. +- **Titles fit one line at the chosen size; bodies fit inside `min-height` after wrapping.** -## Combinations +## See also -- [discrete-text-sequence.md](discrete-text-sequence.md) — per-entry typewriter on the body -- [context-sensitive-cursor.md](context-sensitive-cursor.md) — cursor color per chapter segment -- [vertical-spring-ticker.md](vertical-spring-ticker.md) — animated word transitions between items (instead of hard cut) -- [scale-swap-transition.md](scale-swap-transition.md) — visual morph between entries - -## Pairs with HF skills - -- `/hyperframes-animation` — single driver, reverse-search dispatch -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`discrete-text-sequence` (per-entry typewriter on the body) · `context-sensitive-cursor` (cursor color per chapter) · `vertical-spring-ticker` (animated word swap instead of hard cut) · `scale-swap-transition` (visual morph between entries). diff --git a/skills/hyperframes-animation/rules/gradient-text-sweep.md b/skills/hyperframes-animation/rules/gradient-text-sweep.md new file mode 100644 index 000000000..e633dad56 --- /dev/null +++ b/skills/hyperframes-animation/rules/gradient-text-sweep.md @@ -0,0 +1,136 @@ +--- +name: gradient-text-sweep +description: A gradient tweened THROUGH letterforms — background-clip:text + a backgroundPosition tween. Three forms: a continuous horizontal sweep inside a held headline, a traveling word-to-word highlight, and a hue-sweep that settles to a solid. Glyphs never move; finite, deterministic, seek-safe. +metadata: + tags: gradient, text, sweep, background-clip, highlight, hue, typography, headline +--- + +# Gradient Text Sweep + +Color that lives **inside the glyphs**: the headline's fill is an oversized gradient clipped into the letterforms (`background-clip: text`), and the motion is the gradient sliding **through** the type — the letters never move. Three forms: a **continuous sweep** across a held title card, a **word-to-word highlight** that lights a line left→right, and a **hue-sweep** that settles to a solid. + +Boundaries: [asr-keyword-glow.md](asr-keyword-glow.md) is word-timed emphasis railed to ASR timestamps — this rule is a design beat with no audio rail. [ambient-glow-bloom.md](ambient-glow-bloom.md)'s traveling sweep is a sheen riding **over a surface**; here the gradient is masked **into the type** (its "Shimmer sweep" variation is this mechanism re-aimed as a working-state loop). [css-marker-patterns.md](css-marker-patterns.md) draws accents _around_ text, never fills. + +## How It Works + +The text carries a gradient background **wider than its own box** (`background-size: SWEEP_SPAN 100%`, e.g. `300% 100%`) clipped into the glyphs, so tweening `backgroundPosition` slides the gradient through the visible letterforms. Two gotchas own this rule: + +- **`background-position` percentages only produce travel when `background-size` exceeds 100%** — at 100% the image is pinned and the tween is a silent no-op. +- **The percent axis runs opposite to the perceived travel** — tweening `"100% 50%"` → `"0% 50%"` moves the highlight left→right through the text. + +1. **Continuous sweep (held title card)** — one long **linear** `backgroundPosition` tween spanning the hold. First and last color stops equal, so the travel has no visible seam and reads as endless while remaining a single finite tween. +2. **Word-to-word highlight** — each word is two pixel-identical stacked copies: a base copy in the resting color and a gradient-clipped copy at `opacity: 0`. A per-word opacity envelope (rise, then fall as the next word rises) passes the highlight along on an index-derived stagger — an **envelope, not a moving mask**: no per-word position measurement. +3. **Hue-sweep → solid** — the gradient holds position while a `filter: hue-rotate()` tween sweeps its hues; the settle is a stacked-copy crossfade to a solid twin — never a color-stop tween (gradients with different stops don't interpolate reliably). + +## Recipe + +```html + + +
+

{headlineText}

+

{headlineText}

+
+ + +

+ {word1}{word1} + {word2}{word2} +

+``` + +```css +.headline-stack, +.word { + display: grid; /* twins share one cell — pixel-identical boxes */ +} +.headline, +.w-base, +.w-hot { + grid-area: 1 / 1; +} +.gradient-fill, +.w-hot { + background-image: {gradient}; /* {sweepGradient} A/C, {highlightGradient} B */ + background-size: SWEEP_SPAN 100%; /* MUST exceed 100% or the position tween is dead */ + background-position: 100% 50%; /* start; tween toward 0% for left→right travel */ + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.solid-twin { + color: {settleColor}; +} +.w-base { + color: {restColor}; +} +.w-hot { + opacity: 0; /* the envelope raises it as the highlight passes */ +} +``` + +```js +// Form A: continuous sweep. 100% → 0% reads left→right (percent axis inverted); +// ease "none" — an eased sweep reads as an object, not light. +tl.fromTo( + "#headline", + { backgroundPosition: "100% 50%" }, + { backgroundPosition: "0% 50%", duration: SWEEP_DUR, ease: "none" }, + SWEEP_START, +); + +// Form B: traveling highlight — per-word rise/fall envelopes, index stagger. +gsap.utils.toArray(".w-hot").forEach((el, i) => { + const at = HIGHLIGHT_START + i * WORD_LAG; + tl.fromTo(el, { opacity: 0 }, { opacity: 1, duration: HOT_RISE, ease: "power2.out" }, at); + tl.to(el, { opacity: 0, duration: HOT_FALL, ease: "power2.in" }, at + WORD_LAG); +}); + +// Form C: hue-sweep, then crossfade to the solid twin (never tween color stops). +tl.fromTo( + "#headline", + { filter: "hue-rotate(0deg)" }, + { filter: `hue-rotate(${HUE_RANGE}deg)`, duration: HUE_DUR, ease: "power1.inOut" }, + HUE_START, +); +tl.to( + "#headline", + { opacity: 0, duration: SETTLE_SNAP_DUR, ease: "power2.in" }, + HUE_START + HUE_DUR, +); +``` + +## Variations + +- **Title-card crawl** — Form A stretched across a long terminal hold (3–8s end card): seamless-ended gradient, `ease: "none"`, `SWEEP_DUR` = the whole hold. One tween, no loop. +- **One-pass sheen inside type** — gradient is the resting fill everywhere except one narrow highlight band (≤ ~25% of the span); one `backgroundPosition` pass carries the band through and the text returns to rest with no crossfade. +- **Karaoke settle** — Form B with the fall tweens skipped: the line lights cumulatively left→right and holds fully lit; settle color = the hot state, base copies start dimmer. +- **Gradient climax word** — one emphasized word (often ~-8° rotated) carries the gradient while the line stays solid; static gradient + a short Form C hue shift on landing, settling to the brand accent. Pairs with a `kinetic-beat-slam` arrival. + +## Values + +| token | range | notes | +| ------------------- | ---------------------- | ------------------------------------------------------------------------------------ | +| SWEEP_SPAN | 200–400% | must exceed 100%; wider = softer/slower feel, narrower = busier color per glyph | +| SWEEP_DUR | 1.2–3s | match the card's hold exactly; slower than ~4s stops registering as motion | +| WORD_LAG | 0.25–0.5s | HOT_FALL starts exactly WORD_LAG after the rise so envelopes cross — a gap = a blink | +| HOT_RISE / HOT_FALL | 0.15–0.3s / 0.25–0.45s | fall slightly longer — the highlight "trails" | +| HUE_RANGE / HUE_DUR | 40–180° / 0.8–1.6s | past ~180° the palette dissociates from itself mid-sweep | +| SETTLE_SNAP_DUR | 0.1–0.35s | the goldens snap (~0.15s) | +| {settleColor} | — | one of the gradient's own stops (or the brand ink) so the settle reads as resolution | + +## Critical Constraints + +- **`background-size` > 100%** on any element whose `backgroundPosition` is tweened — otherwise the tween is a silent no-op. +- **Percent axis is inverted** — left→right perceived travel is `100% → 0%`. +- **Both `-webkit-background-clip: text` AND `background-clip: text`, with `color: transparent`** — missing the prefix renders a solid gradient block over the text in the capture browser. +- **`ease: "none"` on position sweeps** — this is supposed to read as light, not an accelerating object. +- **Seamless ends for a crawl** — first and last stops equal, or the wrap point flashes a hard edge mid-hold. +- **Stacked copies pixel-identical** — same box, font, weight, tracking, one grid cell; any metric drift makes the crossfade a double-exposure. +- **`data-layout-allow-occlusion` on the twin** — pixel-identical stacked copies trip `hyperframes check`'s `text_occluded` gate by construction; the flag is the sanctioned waiver for this mechanism. +- **Settle by crossfade, never by tweening stops**; and the glyphs never move — if the type must travel, that's a separate rule on the wrapper. +- **No CSS `@keyframes` shimmer** — wall-clock animation desyncs from seek; every sweep is a timeline tween. + +## See also + +`kinetic-beat-slam` (slam lands the climax word, hue settle finishes it) · `spring-pop-entrance` (pop in solid, sweep after) · `discrete-text-sequence` (swap-slot under a riding crawl) · `ambient-glow-bloom` (surface-level sibling) · `css-marker-patterns` (strokes around text; fills here). diff --git a/skills/hyperframes-animation/rules/gsap-effects.md b/skills/hyperframes-animation/rules/gsap-effects.md index 44b956649..0dcf6954f 100644 --- a/skills/hyperframes-animation/rules/gsap-effects.md +++ b/skills/hyperframes-animation/rules/gsap-effects.md @@ -1,33 +1,26 @@ # GSAP Effects for HyperFrames -Drop-in animation patterns. Each effect is self-contained (HTML + CSS + JS) and follows the HyperFrames seek-driven contract — deterministic, no randomness, timeline registered on `window.__timelines`. +Drop-in animation patterns. Snippets show mechanism only, inside a standard scene clip (hyperframes-core); assume `tl` exists. -## Index - -- [Typewriter](#typewriter) — character-by-character text reveal with optional cursor / backspace / word rotation +- [Typewriter](#typewriter) — character-by-character reveal with optional cursor / backspace / word rotation - [Audio Visualizer](#audio-visualizer) — pre-extract audio data, drive Canvas/DOM rendering from the timeline ---- - ## Typewriter -Reveal text character by character using GSAP's TextPlugin. - -### Required Plugin +Requires GSAP's TextPlugin alongside the core script: ```html - ``` -### Basic Typewriter +### Basic ```js const text = "Hello, world!"; -const cps = 10; // chars per second: 3-5 dramatic, 8-12 conversational, 15-20 energetic +const cps = 10; // chars per second — see timing table tl.to( "#typed-text", { text: { value: text }, duration: text.length / cps, ease: "none" }, @@ -35,13 +28,9 @@ tl.to( ); ``` -### With Blinking Cursor +### Blinking Cursor -Three rules: - -1. **One cursor visible at a time** — hide previous before showing next. -2. **Cursor must blink when idle** — after typing, during pauses. -3. **No gap between text and cursor** — elements must be flush in HTML. +Three rules: **one cursor visible at a time** (hide previous before showing next); **cursor must blink when idle** (after typing, during holds); **no gap between text and cursor** (elements flush in HTML). ```html | @@ -70,7 +59,7 @@ Three rules: } ``` -Pattern: blink → solid (typing starts) → type → solid → blink (typing done). +Pattern: blink → solid (typing starts) → type → blink (typing done): ```js tl.call(() => cursor.classList.replace("cursor-blink", "cursor-solid"), [], startTime); @@ -78,9 +67,11 @@ tl.to("#typed-text", { text: { value: text }, duration: dur, ease: "none" }, sta tl.call(() => cursor.classList.replace("cursor-solid", "cursor-blink"), [], startTime + dur); ``` +Multi-line handoff: hide previous cursor → blink new → brief pause (~0.5s) → solid when typing. Never go `hidden → solid` (skips the idle blink). + ### Backspacing -TextPlugin removes from front — wrong for backspace. Use manual substring removal: +TextPlugin removes from the front — wrong for backspace. Use manual substring removal: ```js function backspace(tl, selector, word, startTime, cps) { @@ -88,9 +79,7 @@ function backspace(tl, selector, word, startTime, cps) { const interval = 1 / cps; for (let i = word.length - 1; i >= 0; i--) { tl.call( - () => { - el.textContent = word.slice(0, i); - }, + () => (el.textContent = word.slice(0, i)), [], startTime + (word.length - i) * interval, ); @@ -101,72 +90,26 @@ function backspace(tl, selector, word, startTime, cps) { ### Spacing With Static Text -When a typewriter word sits next to static text, use `margin-left` on a wrapper span. Don't use flex `gap` (it spaces the cursor from the text) and don't put a trailing space in the static text (it collapses when the dynamic span is empty). - -```html -
- Ship something - | -
-``` +A typewriter word next to static text (`Ship something|` in a baseline-aligned flex row): use `margin-left` on the wrapper span. Don't use flex `gap` (it spaces the cursor from the text) and don't put a trailing space in the static text (it collapses when the dynamic span is empty). ### Word Rotation -Type → hold → backspace → next word. Cursor blinks during every idle moment (holds, after backspace). +Type → hold → backspace → next word; cursor blinks during every idle moment: ```js let offset = 0; words.forEach((word, i) => { const typeDur = word.length / 10; - tl.call(() => cursor.classList.replace("cursor-blink", "cursor-solid"), [], offset); + // cursor: solid while typing, blink during holds (same call pattern as above) tl.to("#typed-text", { text: { value: word }, duration: typeDur, ease: "none" }, offset); - tl.call(() => cursor.classList.replace("cursor-solid", "cursor-blink"), [], offset + typeDur); offset += typeDur + 1.5; // hold - - if (i < words.length - 1) { - tl.call(() => cursor.classList.replace("cursor-blink", "cursor-solid"), [], offset); - const clearDur = backspace(tl, "#typed-text", word, offset, 20); - tl.call(() => cursor.classList.replace("cursor-solid", "cursor-blink"), [], offset + clearDur); - offset += clearDur + 0.3; - } + if (i < words.length - 1) offset += backspace(tl, "#typed-text", word, offset, 20) + 0.3; }); ``` ### Appending Words -Build a sentence word-by-word into the same element: - -```js -let accumulated = ""; -let offset = 0; -words.forEach((word) => { - const target = accumulated + (accumulated ? " " : "") + word; - const newChars = target.length - accumulated.length; - tl.to("#typed-text", { text: { value: target }, duration: newChars / 10, ease: "none" }, offset); - accumulated = target; - offset += newChars / 10 + 0.3; -}); -``` - -### Multi-Line Cursor Handoff - -Handing off between typewriter lines: hide previous → blink new → pause → solid when typing. Never go `hidden → solid` (skips the idle blink). - -```js -tl.call( - () => { - prevCursor.classList.replace("cursor-blink", "cursor-hide"); - nextCursor.classList.replace("cursor-hide", "cursor-blink"); - }, - [], - handoffTime, -); - -const typeStart = handoffTime + 0.5; // brief blink pause -tl.call(() => nextCursor.classList.replace("cursor-blink", "cursor-solid"), [], typeStart); -tl.to("#next-text", { text: { value: text }, duration: dur, ease: "none" }, typeStart); -tl.call(() => nextCursor.classList.replace("cursor-solid", "cursor-blink"), [], typeStart + dur); -``` +Build a sentence word-by-word into the same element: keep an `accumulated` string, each step tweens `text: { value: accumulated + " " + word }` with `duration: newChars / cps`, then advances the offset. ### Timing Guide @@ -177,59 +120,40 @@ tl.call(() => nextCursor.classList.replace("cursor-solid", "cursor-blink"), [], | 15-20 | Fast, energetic | Tech demos, code | | 30+ | Near-instant | Filling long blocks | ---- - ## Audio Visualizer -Pre-extract audio data, drive Canvas / DOM rendering from a single `tl.call(...)` per frame. **Do not** use the Web Audio API at render time — there's no playback during seek. +Pre-extract audio data, drive Canvas / DOM rendering from the timeline. **Do not use the Web Audio API at render time** — there's no playback during seek. ### Extract Audio Data -Use the bundled extractor (requires `ffmpeg` and Python `numpy`): +Bundled extractor (requires `ffmpeg` + Python `numpy`): ```bash python skills/hyperframes-creative/scripts/extract-audio-data.py audio.mp3 -o audio-data.json python skills/hyperframes-creative/scripts/extract-audio-data.py video.mp4 --fps 30 --bands 16 -o audio-data.json ``` -### Data Format +Output: `{ "fps": 30, "totalFrames": 5415, "frames": [{ "time": 0.0, "rms": 0.42, "bands": [0.8, 0.6, 0.3] }] }` — `rms` (0-1) is overall loudness; `bands[]` (0-1) are frequency magnitudes, index 0 = bass, each band normalized independently. -```json -{ - "fps": 30, - "totalFrames": 5415, - "frames": [{ "time": 0.0, "rms": 0.42, "bands": [0.8, 0.6, 0.3] }] -} -``` +### Loading (Synchronously) -- **`rms`** (0-1) — overall loudness, normalized across the track. -- **`bands[]`** (0-1) — frequency magnitudes. Index 0 = bass, higher index = treble. Each band normalized independently. - -### Loading the Data (Synchronously) +Inline the JSON for small files (< ~500 KB), or sync XHR for large ones: ```js -// Option A — inline (small files, under ~500 KB) -var AUDIO_DATA = { - /* paste audio-data.json contents */ -}; - -// Option B — sync XHR (large files; must be synchronous for deterministic timeline construction) -var xhr = new XMLHttpRequest(); -xhr.open("GET", "audio-data.json", false); +const xhr = new XMLHttpRequest(); +xhr.open("GET", "audio-data.json", false); // synchronous — deliberate xhr.send(); -var AUDIO_DATA = JSON.parse(xhr.responseText); +const AUDIO_DATA = JSON.parse(xhr.responseText); ``` -**Do NOT use async `fetch()`.** HyperFrames reads `window.__timelines` synchronously after page load — building the timeline inside `.then()` means the timeline isn't ready when capture starts. +**Do NOT use async `fetch()`** — HyperFrames reads `window.__timelines` synchronously after page load; building the timeline inside `.then()` means it isn't ready when capture starts. ### Driving the Timeline -**Canvas 2D** — most common (bars, waveforms, circles, gradients): +Canvas 2D is the workhorse (bars, waveforms, circles, gradients) — one `tl.call` per frame: ```js -const canvas = document.getElementById("viz"); -const ctx = canvas.getContext("2d"); - +const ctx = document.getElementById("viz").getContext("2d"); for (let f = 0; f < AUDIO_DATA.totalFrames; f++) { tl.call( () => { @@ -243,9 +167,7 @@ for (let f = 0; f < AUDIO_DATA.totalFrames; f++) { } ``` -**WebGL / Three.js** — HyperFrames patches `THREE.Clock` for deterministic time. Update uniforms from audio data each frame. - -**DOM elements** — fine for fewer than ~20 elements, slower than Canvas for many. +WebGL / Three.js: HyperFrames patches `THREE.Clock` for deterministic time — update uniforms from audio data each frame. DOM elements: fine under ~20 elements, slower than Canvas beyond that. ### Smoothing @@ -254,46 +176,21 @@ let prev = null; const smoothing = 0.25; // 0.1-0.2 snappy, 0.3-0.5 flowing function smooth(f) { const raw = AUDIO_DATA.frames[f]; - if (!prev) { - prev = { rms: raw.rms, bands: [...raw.bands] }; - return prev; + if (!prev) prev = { rms: raw.rms, bands: [...raw.bands] }; + else { + prev = { + rms: prev.rms * smoothing + raw.rms * (1 - smoothing), + bands: raw.bands.map((b, i) => prev.bands[i] * smoothing + b * (1 - smoothing)), + }; } - prev = { - rms: prev.rms * smoothing + raw.rms * (1 - smoothing), - bands: raw.bands.map((b, i) => prev.bands[i] * smoothing + b * (1 - smoothing)), - }; return prev; } ``` -### Spatial Mapping +### Design Guide -- **Horizontal**: bass left, treble right (iterate bands left-to-right) -- **Vertical**: bass bottom, treble top -- **Circular**: bass at 12 o'clock, wrap clockwise; mirror for a full circle - -### Motion Principles - -- **Bass drives big moves** — scale, glow, position shifts. -- **Treble drives detail** — shimmer, flicker, edge effects. -- **RMS drives globals** — background brightness, overall energy. -- Pick 2-3 properties to animate. More looks noisy. -- Keep minimums above zero — quiet sections still need life. - -### Band Count - -| Bands | Detail | Good for | -| ----- | --------- | -------------------------- | -| 4 | Low | Background glow, pulsing | -| 8 | Medium | Bar charts, basic spectrum | -| 16 | High | Detailed EQ (default) | -| 32 | Very high | Dense radial layouts | - -### Layering - -Layer multiple canvases with CSS `z-index` for depth — a background layer driven by bass/rms and a foreground layer driven by individual bands creates depth without per-element complexity. - -```html - - -``` +- **Spatial mapping** — horizontal: bass left, treble right; vertical: bass bottom; circular: bass at 12 o'clock, wrap clockwise (mirror for a full circle). +- **Bass drives big moves** (scale, glow, position); **treble drives detail** (shimmer, flicker, edges); **RMS drives globals** (background brightness, overall energy). +- Pick 2-3 animated properties — more looks noisy. Keep minimums above zero so quiet sections still have life. +- **Band count**: 4 = background glow/pulse, 8 = bar charts, 16 = detailed EQ (default), 32 = dense radial layouts. +- **Layering**: stack canvases with `z-index` — a background layer driven by bass/rms under a foreground layer driven by individual bands gives depth without per-element complexity. diff --git a/skills/hyperframes-animation/rules/hacker-flip-3d.md b/skills/hyperframes-animation/rules/hacker-flip-3d.md index f831dce92..04c23cca3 100644 --- a/skills/hyperframes-animation/rules/hacker-flip-3d.md +++ b/skills/hyperframes-animation/rules/hacker-flip-3d.md @@ -7,217 +7,117 @@ metadata: # Hacker Flip 3D Reveal -Characters flip down from 90° in 3D while cycling through random glyphs, then settle on the target character. Creates a "decryption" or airport flap-display reveal. +Characters flip down from 90° in 3D while cycling through pseudo-random glyphs, then settle on the target character — a "decryption" / airport flap-display reveal. Resolves to a short target word (typically a brand or label). ## How It Works -Each character gets its own per-char tween from `rotateX: 90deg` (hidden) to `rotateX: 0deg` (revealed), staggered across the word. During the flip: +Each character gets its own per-char tween from `rotateX: 90deg` (hidden, hinged at the bottom edge) to `0deg` (upright), staggered across the word. Below `REVEAL_THRESHOLD` progress the char displays a seeded pseudo-random glyph that reshuffles every few frames; past it, the real target character clicks into place — so the eye catches the right letter just as the flip settles. A hidden ghost copy of the full word reserves layout width so narrow flicker glyphs never shift the line. -1. **Phase A (0 → ~`REVEAL_THRESHOLD` progress)**: character displays a randomly-substituted glyph that flickers (changes every `FLICKER_RATE` frames) -2. **Phase B (`REVEAL_THRESHOLD` → 1.0 progress)**: character displays the REAL target character, settling into its final upright position - -The `REVEAL_THRESHOLD` separates "scrambled" from "revealed" — by the time the flip is mostly done, viewer sees the correct letter clicking into place. - -## HTML +## Recipe ```html -
-
- -
+ +
+
``` -`{phrase}` is the target word the flip resolves to (typically a brand or short label). - -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; - perspective: 1500px; /* REQUIRED — without this rotateX renders flat */ -} - +/* the scene root (or nearest 3D ancestor) MUST set perspective: 1500px */ .hacker-text-wrap { - font-family: {monoFont}; /* monospace recommended so flicker glyphs hold width */ + font-family: {monoFont}; /* monospace so flicker glyphs hold width */ font-weight: 900; font-size: HACKER_FONT_SIZE; - color: {textColor}; - letter-spacing: 4px; - display: flex; - /* Ghost / live chars are absolutely stacked; container reserves layout width */ - position: relative; + position: relative; /* ghost stacks absolutely behind the live row */ } - .hacker-char { display: inline-block; - /* Hinge at the bottom edge — flap-display look */ - transform-origin: bottom; + transform-origin: bottom; /* flap-display hinge */ transform-style: preserve-3d; - /* Will-change improves render perf */ - will-change: transform, opacity; } - -/* Ghost placeholder is hidden but reserves width for variable-glyph fonts. - Without this, narrow target glyphs collapse width when displayed and - characters shift horizontally during flicker. */ .hacker-ghost { opacity: 0; pointer-events: none; + position: absolute; + inset: 0 auto auto 0; } ``` -## GSAP Timeline + Random Glyph Logic +```js +const wrap = document.getElementById("hacker-text"); +const targetWord = wrap.dataset.target; +const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*"; -```html - - + }, + i * CHAR_STAGGER, + ); +}); ``` -## How to Choose Values - -- **HACKER_FONT_SIZE** — font-size of the flip text in px. - - Range: 6-10% of viewport min-dimension; the flip text is the focal beat, scale accordingly - - Constraints: ghost row must use the identical size so layout width stays stable mid-flicker - - Reference: ../../examples/proof-logo-chain.html uses `163px` at 1920×1080 -- **FLIP_DURATION** — per-character flip tween duration. - - Range: 0.4-1.0s; under 0.4s the random-glyph phase has no time to flicker, over 1.0s drags - - Effects: shorter feels snappy and modern; longer feels mechanical / typewriter - - Reference: ../../examples/proof-logo-chain.html uses `0.55s` -- **CHAR_STAGGER** — delay between consecutive characters starting their flips, in seconds. - - Range: 0.03-0.08s; too fast and chars overlap visually, too slow and the effect feels labored - - Constraints: total decode time = `CHAR_STAGGER × (charCount − 1) + FLIP_DURATION`; ensure this fits the phase budget - - Reference: ../../examples/proof-logo-chain.html uses `0.033s` (≈2 frames at 60fps) -- **REVEAL_THRESHOLD** — progress at which a glyph swaps from random → real. - - Range: 0.5-0.7; lower reveals too early (no decode tension), higher feels like a hard reveal at the end - - Effects: this is a discrete tuning of when the eye locks onto the real letter - - Reference: ../../examples/proof-logo-chain.html uses `0.6` -- **FLICKER_RATE** — frames between glyph reshuffles during the random phase. - - Range: 3-6; lower than 3 looks like noise, higher than 6 looks like discrete typing instead of flicker - - Constraints: must be ≥ ~3 frames (see Critical Constraints) - - Reference: ../../examples/proof-logo-chain.html uses an equivalent of `3` (one shuffle every 3 internal-clock frames) -- **{bgColor} / {textColor}** — stage background and live-character color tokens. -- **{monoFont}** — monospace family preferred so flicker glyphs don't change width per swap; if a proportional font is required, the ghost placeholder makes the cost recoverable. -- **{phrase}** — the target word the flip resolves to. Length feeds the total decode duration via `CHAR_STAGGER`. - ## Variations -- **Top-down hinge** — swap `transform-origin: bottom` to `top` for a falling-flap look. +- **Top-down hinge** — `transform-origin: top` for a falling-flap look. - **Center spin** — `transform-origin: center` reads as a barrel roll, not a flap. - **Number-only pool** — restrict `GLYPHS` to digits for a price / countdown decode. -- **Two-pass decode** — chain two `FLIP_DURATION` tweens with different glyph pools (e.g. symbols → letters → real) for a longer reveal. +- **Two-pass decode** — chain two `FLIP_DURATION` tweens with different glyph pools (symbols → letters → real) for a longer reveal. -## Key Principles +## Values -- **Threshold at ~`REVEAL_THRESHOLD`** for swap from random → real glyph — close enough to settled that viewer's eye catches the right letter -- **Hinge at `transform-origin: bottom`** for flap-display look (vs `top` for top-down, vs `center` for spin) -- **Deterministic random** via seeded hash — HF runtime seeks frame-by-frame, so the same frame must show the same glyph (no `Math.random()`) -- **Ghost placeholder** sits behind the live chars with identical content + same font, reserving width — without it, narrow glyphs shift the layout mid-flicker -- **Stagger in the 0.04-0.08s range** per char — too fast and chars overlap visually, too slow and effect feels labored -- **Center the flip dead-center via `display: grid; place-items: center;`** on the scene root — and DO NOT add decorative headers/footers (timestamp lines, "// AUTH" tags, small status dots). The flip text IS the focal beat; surrounding clutter dilutes it. If a secondary label is necessary, promote it to BIG typography in the same stacked layout (56-72px caps + tracking), not a tiny corner annotation. +| token | range | notes | +| ---------------- | ------------------------------- | ---------------------------------------------------------------------------------- | +| HACKER_FONT_SIZE | 6–10% of viewport min-dimension | the flip IS the focal beat; ghost must use the identical size | +| FLIP_DURATION | 0.4–1.0s | under 0.4s the flicker phase has no time; over 1.0s drags | +| CHAR_STAGGER | 0.03–0.08s | total decode = `CHAR_STAGGER × (chars − 1) + FLIP_DURATION` — fit the phase budget | +| REVEAL_THRESHOLD | 0.5–0.7 | lower reveals too early (no tension); higher reads as a hard end-reveal | +| FLICKER_RATE | 3–6 frames per glyph swap | <3 looks like noise; >6 looks like discrete typing | + +Reference: `../../examples/proof-logo-chain.html` (163px, 0.55s, 0.033s, 0.6). ## Critical Constraints -- **`perspective` on scene root REQUIRED** — without parent perspective, `rotateX` looks like a 2D scale, not a 3D flip -- **`transform-style: preserve-3d` on each char** — keeps 3D context intact when chars have their own transforms -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Deterministic randomness**: don't use `Math.random()`. Use a seed derived from char index + frame group so seek determinism holds -- **`onUpdate` writes to DOM**: HF seeks every frame, so this runs many times — keep work O(1) per char per frame -- **Flicker rate ≥ ~3 frames per glyph swap**: faster looks like noise, slower looks like discrete typing +- **`perspective` on the scene root REQUIRED** — without parent perspective, `rotateX` renders as a 2D squash, not a 3D flip; `transform-style: preserve-3d` on each char. +- **Ghost placeholder** with identical content + font must back the live chars — without it, narrow glyphs shift the layout mid-flicker (monospace preferred; the ghost makes a proportional face recoverable). +- **Flicker seed = char index + quantized progress** — the same frame must show the same glyph. +- **Flicker rate ≥ ~3 frames per swap**; `onUpdate` work stays O(1) per char per frame. +- **Center the flip dead-center and add NO decorative chrome** (timestamp lines, "// AUTH" tags, status dots) — the flip is the beat. A necessary secondary label is BIG typography (56–72px caps + tracking) in the same stack, never a tiny corner annotation. -## Combinations +## See also -- [card-morph-anchor.md](card-morph-anchor.md) — pair: hacker-flip reveals a phrase, then card morphs into the next shot -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — counterpart for numeric reveals (text vs number) - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + per-char stagger + `onUpdate` -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`card-morph-anchor` (flip reveals a phrase, card morphs into the next shot) · `counting-dynamic-scale` (the numeric counterpart). diff --git a/skills/hyperframes-animation/rules/kinetic-beat-slam.md b/skills/hyperframes-animation/rules/kinetic-beat-slam.md index 5ada2370a..c82b1595c 100644 --- a/skills/hyperframes-animation/rules/kinetic-beat-slam.md +++ b/skills/hyperframes-animation/rules/kinetic-beat-slam.md @@ -7,43 +7,25 @@ metadata: # Kinetic Beat Slam -Short phrases hit one at a time on a **steady beat**, each with a _different_ entrance, then stack into a locked finale. This is the recipe for "punchy / rhythmic" text-forward pieces (taglines, manifestos, hype intros). The difference between generic and rhythmic is (1) one shared **onset array** driving every element, (2) **distinct** entrances per phrase rather than one reused helper, and (3) optional **rhythm chrome** that visibly keeps the beat. +Short phrases hit one at a time on a **steady beat**, each with a _different_ entrance, then stack into a locked finale — the recipe for "punchy / rhythmic" text-forward pieces (taglines, manifestos, hype intros). The difference between generic and rhythmic is (1) one shared **onset array** driving every element, (2) **distinct** entrances per phrase rather than one reused helper, and (3) optional **rhythm chrome** that visibly keeps the beat. ## How It Works -1. **Define the beat once.** A single `BEATS = [t0, t1, t2, …]` array (seconds) is the rhythmic spine. Every phrase entrance, accent, and chrome tick reads its time from this array — so the whole piece locks to one pulse instead of drifting hand-tuned offsets. -2. **Vary the entrances.** Phrase 1 slams (scale + blur), phrase 2 snaps from the side, phrase 3 rises and rotates. Same _energy_, different _form_ — reusing one `punchIn()` for all three reads as flat. -3. **Land a finale.** All phrases lock into a left-aligned or centered stack; an accent underline sweeps in; optionally a continuous low-amplitude pulse holds the last beat. +A single tempo grid — `PULSE` seconds per sub-beat, `BEATS = [t0, t1, t2, …]` on that grid — is the rhythmic spine; every phrase entrance, accent, and chrome tick reads its time from it, so the piece locks to one pulse instead of drifting hand-tuned offsets. Each phrase gets a different transform axis (scale+blur slam / side snap / rise+rotate) with short attacks (0.35–0.6s on the hit), then the stack holds with a finite low-amplitude breath. -## Beat & Easing - -Pick the entrance easing by attack character (the choice is discrete): - -| GSAP ease | Attack feel | -| ------------- | ------------------------------------------- | -| `power4.out` | Hard slam, fast settle ⭐ default for a hit | -| `expo.out` | Hardest snap (side-snaps, whip-ins) | -| `back.out(2)` | Overshoot pop — accents, not body words | -| `circ.out` | Heavy rise with momentum | - -Use **at least 3 distinct easings** across the piece (entrances are its "tone of voice"). Keep durations short — 0.35–0.6s on the hit, ≤0.25s on the exit — so the beat stays percussive. - -## HTML +## Recipe ```html -
-
-
Notice more.
-
Decide faster.
-
Act now.
-
- - -
+ +
+
Notice more.
+
Decide faster.
+
Act now.
+
+ + ``` -## CSS - ```css .kbs-stage { position: absolute; @@ -51,9 +33,7 @@ Use **at least 3 distinct easings** across the piece (entrances are its "tone of display: flex; flex-direction: column; justify-content: center; - gap: 8px; padding: 120px 160px; /* title-safe margin */ - box-sizing: border-box; } .kbs-line { font-family: "Archivo Black", "League Gothic", sans-serif; /* embedded display face */ @@ -61,11 +41,10 @@ Use **at least 3 distinct easings** across the piece (entrances are its "tone of line-height: 0.96; letter-spacing: -0.03em; color: #f5f5f5; - will-change: transform, filter, opacity; } .kbs-line .verb { - color: #ff5b2e; -} /* one accent hue */ + color: #ff5b2e; /* exactly one accent hue */ +} .kbs-metronome { position: absolute; bottom: 64px; @@ -82,102 +61,77 @@ Use **at least 3 distinct easings** across the piece (entrances are its "tone of } ``` -## GSAP Timeline +```js +// ONE tempo grid drives everything — phrases AND the metronome read it. +const PULSE = 0.4; // seconds per sub-beat +const BEATS = [PULSE * 1, PULSE * 5, PULSE * 9]; // phrase onsets, on the grid -```html - - +// Finale hold: floor (not ceil) so the repeat never overshoots data-duration; +// max(0,…) so a short hold never yields a negative repeat (GSAP reads negative as -1 = infinite). +const holdStart = BEATS[2] + 0.7, + cycle = 1.6, + holdDur = SCENE_DURATION - holdStart; +tl.to( + ".kbs-stage", + { + scale: 1.01, + duration: cycle / 2, + ease: "sine.inOut", + yoyo: true, + repeat: Math.max(0, Math.floor(holdDur / cycle) - 1), + }, + holdStart, +); ``` -## How to Choose Values +## Variations -- **BEATS spacing** — 1.2–1.8s between hits reads as a confident beat; <0.8s feels frantic, >2.5s loses the pulse. Keep spacing even (it's a _beat_). -- **Entrance duration** — 0.35–0.6s. The hit must resolve before the next beat. -- **Distinct entrances** — assign a different transform axis per phrase (scale / x / y+rotate). Reuse the _ease family_, vary the _motion_. -- **Accent hue** — exactly one (the verbs). The rest is mono white/near-black. -- **Rhythm chrome** — optional but high-impact for "rhythmic": a 5-tick metronome, a center beat bar, or a `// label` monospace tag pulsing on-beat. Mark any decorative that must survive a shader transition per `../../transitions/overview.md` rules. +- **Entrance easing by attack character** — `power4.out` hard slam ⭐ default hit · `expo.out` hardest snap (side-snaps, whip-ins) · `back.out(2)` overshoot pop (accents only, not body words) · `circ.out` heavy rise with momentum. Use **at least 3 distinct easings** across the piece. +- **Rhythm chrome alternatives** — a center beat bar or a `// label` monospace tag pulsing on-beat instead of the 5-tick metronome; mark any decorative that must survive a shader transition per `../../transitions/overview.md`. +- **Finale dressing** — stack + accent underline sweep ([css-marker-patterns](css-marker-patterns.md)); don't just leave the last phrase sitting. -## Key Principles +## Values -- **One beat array, not scattered offsets** — every element times off `BEATS[]`. This is the single biggest lever for "rhythmic." -- **Different entrance per phrase** — a reused `punchIn()` for all lines is the flat-but-competent tell. -- **Short attacks** — percussive means fast in, brief, decisive. Long fades kill the beat. -- **One accent hue, heavy weight** — embedded display faces (Archivo Black, League Gothic, Oswald) at 150px+; see `hyperframes-creative/references/typography.md`. -- **Finale earns the hold** — stack + underline sweep + optional breath; don't just leave the last phrase sitting. +| token | range | notes | +| ----------------- | -------------------- | -------------------------------------------------------------------------------------------- | +| BEATS spacing | 1.2–1.8s | <0.8s frantic, >2.5s loses the pulse; keep spacing even — it's a beat | +| entrance duration | 0.35–0.6s | the hit must resolve before the next beat; exits ≤0.25s | +| accent hue | exactly 1 | the verbs; the rest mono white / near-black | +| display face | 150px+, heavy weight | Archivo Black / League Gothic / Oswald — see `hyperframes-creative/references/typography.md` | ## Critical Constraints -- **Timeline paused**: `gsap.timeline({ paused: true })`. Never `tl.play()`. -- **No infinite repeats** on the hold/chrome — use `repeat: Math.max(0, Math.floor(dur / cycle) - 1)` (no `repeat: -1`). Use **`Math.floor`, not `Math.ceil`** — `ceil` overshoots `data-duration` and trips the `gsap_repeat_ceil_overshoot` lint rule; the `Math.max(0, …)` guards against a negative repeat (which GSAP reads as `-1` = infinite = non-deterministic) when the hold is shorter than two cycles. -- **No banned exit animations** between scenes — if this is one of several scenes, the _transition_ is the exit (see `../../transitions/overview.md`); only a final scene may fade out. -- **Display font must be embedded** or it silently falls back at render (Anton/Bebas-as-literal are NOT embedded — `Bebas Neue` aliases to League Gothic; verify in `typography.md`). -- **Registry key = `data-composition-id`** on the root. +- **One beat array, not scattered offsets** — every element times off `BEATS[]` / `PULSE`; this is the single biggest lever for "rhythmic". +- **Different entrance per phrase** — a reused `punchIn()` for all lines is the flat-but-competent tell. Vary the motion axis, reuse the ease _family_. +- **Finale repeat math**: `repeat: Math.max(0, Math.floor(dur / cycle) - 1)` — `Math.ceil` overshoots `data-duration` and trips the `gsap_repeat_ceil_overshoot` lint rule; a negative repeat is read by GSAP as `-1` (infinite). +- **No banned exit animations between scenes** — in a montage the _transition_ is the exit (`../../transitions/overview.md`); only a final scene may fade out. +- **Display font must be embedded** or it silently falls back at render — Anton / Bebas-as-literal are NOT embedded (`Bebas Neue` aliases to League Gothic; verify in `typography.md`). -## Combinations +## See also -- [3d-text-depth-layers.md](3d-text-depth-layers.md) — extruded depth on the slammed words -- [css-marker-patterns.md](css-marker-patterns.md) — underline sweep / circle on the finale -- [sine-wave-loop.md](sine-wave-loop.md) — the finale breath/pulse - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + easing vocabulary (`../../adapters/gsap-easing-and-stagger.md`) -- `/hyperframes-creative` — `references/video-composition.md` (foreground rhythm chrome), `references/typography.md` (embedded display fonts) -- `/hyperframes-core` — composition wiring, determinism (finite repeats) +`3d-text-depth-layers` (extruded depth on the slammed words) · `css-marker-patterns` (finale underline/circle) · `sine-wave-loop` (the finale breath) · `../adapters/gsap-easing-and-stagger.md` (easing vocabulary). diff --git a/skills/hyperframes-animation/rules/motion-blur-streak.md b/skills/hyperframes-animation/rules/motion-blur-streak.md index 438437a9c..d631de626 100644 --- a/skills/hyperframes-animation/rules/motion-blur-streak.md +++ b/skills/hyperframes-animation/rules/motion-blur-streak.md @@ -7,322 +7,124 @@ metadata: # Motion-Blur Streak -Real per-frame motion blur isn't available to a seeked renderer (it integrates over shutter time, which a paused timeline has no concept of), so this rule **fakes** it for a fast element fly-in or a hard camera push-through. The blur **peaks at maximum velocity and resolves to 0 at the settle** — the element reads as streaking in then snapping sharp on arrival. 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. +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: -Two implementation paths, both finite, deterministic, and seek-safe: +- **(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. -- **(A) Directional SVG blur** — an inline `` with `` (X on the axis of motion, 0 across it). GSAP tweens `X` from high → 0 through a proxy that calls `setAttribute` each frame. Cleanest, one element, true directional smear. -- **(B) Echo / ghost trail** — 2–4 duplicate copies at decreasing opacity, offset backward along the motion vector, collapsing into the lead element as it settles. No filter cost; reads as a "speed-line" stutter trail. Better when you want the streak _colored_ or _stylized_ rather than a literal optical blur. - -This is for **entrances and mid-shot moves only** — a fast arrival, a beat that zooms past camera, a card slamming into a grid slot, a logo punching through into a lockup. **Never an exit on a non-final frame** (a blurred element fleeing off-frame mid-composition reads as a glitch, and a hard exit between scenes is the transition's job, not a per-element blur). +**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 move has a velocity profile: it accelerates off the start, peaks, then decelerates into the settle. An `out` ease (`expo.out`, `power4.out`) front-loads that — velocity is highest right at the start and bleeds to zero at the end. The fake works by mapping a **blur (or echo) envelope onto that same curve**: +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. -1. **Position tween** — the element travels from an off-frame / pushed-back start to its resting transform (`x`/`y` for a fly-in, `scale` for a push-through) on a fast `out` ease over `MOVE_DUR`. -2. **Blur envelope** — in lockstep over the **same window and ease**, the smear goes from `PEAK_BLUR` → `0`. Because the ease front-loads velocity and the envelope shares it, max blur coincides with max speed, and blur hits exactly `0` as the element lands. -3. **Settle is sharp** — by `MOVE_START + MOVE_DUR` the element is at its resting transform with blur `0` (path A) or all echoes collapsed onto the lead (path B). It then holds, fully crisp, for the climax dwell. - -Path A tweens the filter's `stdDeviation` attribute (a non-DOM-style numeric attribute) via a **proxy object** — GSAP can't tween an SVG attribute directly, so you tween a plain `{ v: PEAK_BLUR }` and write it back with `setAttribute` in `onUpdate`. Path B places the ghosts at deterministic backward offsets (`i * ECHO_STEP_PX`) and fades/collapses them on the same envelope. - -## HTML - -### Path A — directional SVG blur (recommended default) +## Recipe ```html -
- - - -
-
{phrase}
-
-
+ + +
{phrase}
+ ``` -### Path B — echo / ghost trail +```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 -```html -
-
- - - - -
{phrase}
-
-
-``` +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); -## CSS - -```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {sceneBg}; - font-family: {font}; - overflow: hidden; /* the smear/echo extends past the resting position before settling */ -} -.streak-stage { - position: relative; - display: grid; - place-items: center; -} -.streak-el { - position: relative; - z-index: 2; - font-size: EL_FONT_SIZE; - font-weight: 900; - letter-spacing: EL_TRACKING; - color: {textColor}; - /* Path A only — reference the directional filter. (Omit for Path B.) */ - filter: url(#streak); - will-change: transform, filter; -} - -/* Path B ghosts — identical glyphs behind the lead, decreasing opacity */ -.streak-ghost { - position: absolute; - inset: 0; - display: grid; - place-items: center; - z-index: 1; - font-size: EL_FONT_SIZE; - font-weight: 900; - letter-spacing: EL_TRACKING; - color: {textColor}; - opacity: 0; - will-change: transform, opacity; - pointer-events: none; -} -``` - -## GSAP Timeline - -### Path A — directional SVG blur - -```html - - -``` - -### Path B — echo / ghost trail - -```html - - +}); ``` ## Variations -### Vertical streak (rise / drop-in) +- **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. -Swap the motion axis: use `y` instead of `x` for the position tween, `stdDeviation="0 Y"` for Path A (blur on Y, 0 on X), and `ENTER_FROM_Y` / vertical echo offsets for Path B. A phrase that streaks _up_ into place pairs with `kinetic-type-beats`' rise-rotate beat. +## Camera-Travel Carve-Out -### Camera push-through (scale streak into a lockup) +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: -Instead of translating, the element rushes the camera: `scale: SCALE_FROM → 1` on the fast `out` ease, with a **radial / zoom blur** feel approximated by a symmetric `stdDeviation="B B"` envelope (blur on both axes since the smear is depth-wise, not directional). This is the `logo-assemble-lockup` push-through — the wordmark punches forward out of soft focus and snaps crisp at the lock. +- **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.fromTo( - "#streak-el", - { scale: SCALE_FROM, opacity: 0 }, - { scale: 1, opacity: 1, duration: MOVE_DUR, ease: MOVE_EASE }, - MOVE_START, +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( - blurProxy, - { - v: 0, - duration: MOVE_DUR, - ease: MOVE_EASE, - onUpdate: () => blurNode.setAttribute("stdDeviation", `${blurProxy.v} ${blurProxy.v}`), - }, - MOVE_START, + 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); ``` -### Staggered grid streak-in (cards assemble) +## Values -For `grid-card-assemble`: each card streaks into its slot from its own backward offset, staggered. Drive every card off the same ease/window with a per-index delay; derive the entrance offset and start time from the card's index (no `Math.random`). Each card is sharp the instant it lands in its slot. - -```js -gsap.utils.toArray(".grid-card").forEach((card, i) => { - const at = MOVE_START + i * CARD_STAGGER; - tl.fromTo( - card, - { x: ENTER_FROM_X, opacity: 0 }, - { x: 0, opacity: 1, duration: MOVE_DUR, ease: MOVE_EASE }, - at, - ); - // + a per-card blur proxy tween at the same `at` (Path A), or per-card ghosts (Path B) -}); -``` - -### Hold-the-streak (whip emphasis on a single beat) - -For a single kinetic phrase that "zooms past," keep the streak slightly visible a frame or two longer by easing the blur on a marginally _slower_ curve than the position (e.g. position `expo.out`, blur `power3.out`) — the element arrives, then the last wisp of smear resolves. Use sparingly; the default is locked envelopes. - -## How to Choose Values - -### Motion - -- **MOVE_EASE** — shared ease for position and blur/echo. - - Range: `expo.out` (hardest snap), `power4.out` (hard slam, default), `power3.out` (firm but softer) - - Effects: harder `out` → velocity more front-loaded → blur reads as a sharper streak that resolves later in the window - - Constraints: must be an `out`-family ease (velocity front-loaded). An `inOut` or `in` ease puts peak speed mid/late and the blur-speed coupling breaks. **Position and blur must use the SAME ease** (except the deliberate Hold-the-streak variation). -- **MOVE_DUR** — travel + blur-resolve duration. - - Range: 0.25–0.6 s - - Effects: shorter → more violent whip; longer → a glide, the streak loses punch - - Constraints: a streak is _fast_ — over ~0.7 s it stops reading as velocity blur and looks like a focus pull -- **MOVE_START** — timeline position of the entrance. - - Constraints: leave **≥1 s of dwell** after `MOVE_START + MOVE_DUR` before the composition ends (climax dwell — a streak that lands at `t = DURATION − 0.2 s` reads as "flashed and gone") -- **ENTER_FROM_X / ENTER_FROM_Y** — off-frame start offset along the motion axis. - - Range: 40–120% of the element's own dimension on that axis (far enough to read as "came from off-frame") - - Effects: larger → longer travel → the streak has more runway to read; too small and there's no sense of speed - -### Path A — SVG blur - -- **PEAK_BLUR** — `stdDeviation` at maximum velocity (start of the window). - - Range: 8 (subtle) → 18 (default) → 30 (extreme whip) - - Effects: higher → heavier smear at peak speed; too high erases the glyph entirely at the start frame - - Constraints: ≤ ~30 — beyond that the element is unreadable for the first several frames and reads as "missing then appearing"; the filter region (`x/y/width/height` on ``) must be large enough (≥`-50% … 200%`) or the smear clips at the box edge -- **SCALE_FROM** (push-through variation) — starting scale for a camera push. - - Range: 1.3 (gentle push) → 2.5 (aggressive punch-through) - -### Path B — echo trail - -- **N (ghost count)** — number of ghosts behind the lead (set by how many `.streak-ghost` you author). - - Range: 2–4 - - Effects: more ghosts → a longer, smoother smear; >4 reads as a stutter / strobe rather than a streak -- **ECHO_STEP_PX** — backward offset per ghost along the motion vector. - - Range: 12–40 px - - Effects: larger → a more spread-out, visible trail; smaller → a tight blur-like cluster - - Constraints: `(N) × ECHO_STEP_PX` should be ≲ `ENTER_FROM_X` so the furthest ghost still starts within the travel runway -- **GHOST_BASE_OPACITY** — opacity of the nearest ghost (`i = 1`); falls off as `BASE / i`. - - Range: 0.3 (faint) → 0.6 (pronounced) - - Constraints: ≤ ~0.6 — opaque ghosts read as duplicate elements, not a trail - -### Layout & type - -- **EL_FONT_SIZE / EL_TRACKING** — the streaking element's type weight (when it's a phrase). - - Constraints: heavy display weight (≥120 px at 1080p, ≥800 weight) so the smear has mass to streak; thin type smears into invisibility -- **CARD_STAGGER** (grid variation) — delay between consecutive cards. - - Range: 0.05–0.12 s — tight enough to read as one assembling wave, not separate arrivals - -### Tokens - -- **{sceneBg}** — background; a streak reads best against a solid / low-detail field (a busy bg fights the smear) -- **{font}** — typographic stack (embedded display face if the streaking element is text — see typography reference) -- **{textColor}** — element color; for Path B the ghosts inherit this, so a slightly desaturated trail can be had by tinting `.streak-ghost` separately -- **{phrase}** — the word / glyph / wordmark that streaks in - -## Key Principles - -- **Blur peaks at peak speed, resolves to 0 at the settle** — this is the whole rule. Share the ease and window between the position tween and the blur/echo envelope so they're locked. A blur that lingers after the element stops, or peaks after it's already slow, reads as a focus pull, not velocity. -- **`out`-family ease, always** — velocity must be front-loaded (fast off the start, decelerating in). `expo.out` / `power4.out` / `power3.out`. An `in` or `inOut` ease puts peak speed in the wrong place and the coupling falls apart. -- **Directional blur on the motion axis** (Path A) — `stdDeviation="X 0"` for horizontal, `"0 Y"` for vertical, `"B B"` only for a depth/scale push. A symmetric blur on a sideways move looks like defocus, not speed. -- **Tween a proxy, write the attribute** (Path A) — GSAP tweens the plain `{ v }` object; `onUpdate` calls `setAttribute("stdDeviation", …)`. You cannot tween the SVG attribute directly, and you must **seed it once at setup** so a seek to `t=0` shows the streaked start. -- **Ghosts are deterministic, by index** (Path B) — offset `i * ECHO_STEP_PX`, opacity `BASE / i`. Never `Math.random` for the trail; index drives all per-ghost variation so every seek is identical. -- **Entrances only, never a mid-composition exit** — a streak is an _arrival_. A blurred element leaving on a non-final frame reads as a glitch; scene-to-scene exits are the transition's job (see `../../transitions/overview.md`). -- **Earn the sharp hold** — after the snap, the crisp element must dwell ≥1 s. The contrast between the violent streak and the still, sharp settle _is_ the effect. -- **Heavy element, solid background** — thin type or a busy backdrop both swallow the smear. Big bold mass on a clean field reads. +| 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 -- **Timeline must be paused**: `gsap.timeline({ paused: true })`. Never `tl.play()`. -- **Registry key = `data-composition-id`** on the root. -- **No CSS `transition`** on the streaking element (or ghosts) — it interpolates independently of HF seek and causes flicker. Only GSAP drives the move and the blur. -- **No `repeat` / `yoyo` / infinite** — a streak is a single finite arrival. Finite tweens only. -- **No `Math.random` / `Date.now`** — ghost offsets/opacities and any stagger derive from the element index; deterministic every seek. -- **GSAP transform aliases only**: `x`, `y`, `scale`, `rotation`. Never tween `width` / `height` / `left` / `top`. Tweening `filter` (the proxy → `stdDeviation`) and `opacity` is seek-safe and fine. -- **Seed the SVG `stdDeviation` at setup** (Path A) — write it once before play so a seek to the first frame renders the streaked start, not a momentarily-sharp pre-frame. -- **Filter region must be generous** (Path A) — `` so the smear doesn't clip at the element's box edge. -- **`overflow: hidden` on the scene** — the smear / furthest ghost extends past the resting position during travel; contain it so it doesn't bleed outside the frame. +- 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. -## Combinations +## See also -- [kinetic-beat-slam.md](kinetic-beat-slam.md) — use this streak as the entrance for one phrase in a beat sequence (the scale-slam beat _is_ a motion-blur fly-in); reads its onset from the shared `BEATS[]` array -- [center-outward-expansion.md](center-outward-expansion.md) — the grid streak-in is center-expansion with a velocity-blur envelope on each element's travel -- [3d-text-depth-layers.md](3d-text-depth-layers.md) — extruded depth on the phrase that streaks in (depth layers ride the lead's transform) -- [scale-swap-transition.md](scale-swap-transition.md) — alternative for a SAME-footprint state swap (this rule is for a fast ARRIVAL from off-frame / depth, not a morph) - -## Pairs with HF skills - -- `/hyperframes-animation` — `out`-family easing, proxy-driven `onUpdate` attribute tweens, and locked-envelope coordination (`../../adapters/gsap-easing-and-stagger.md`) -- `/hyperframes-creative` — `references/typography.md` (embedded display face for a text streak), `references/video-composition.md` (solid field behind the smear) -- `/hyperframes-core` — composition wiring, determinism (finite tweens, no `Math.random`) -- `/hyperframes-cli` — `hyperframes lint` / `hyperframes check` (`check` catches a missing `#streak-blur` node or an unreferenced filter) +`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). diff --git a/skills/hyperframes-animation/rules/multi-cursor-choreography.md b/skills/hyperframes-animation/rules/multi-cursor-choreography.md new file mode 100644 index 000000000..bd5895557 --- /dev/null +++ b/skills/hyperframes-animation/rules/multi-cursor-choreography.md @@ -0,0 +1,133 @@ +--- +name: multi-cursor-choreography +description: N labeled independent cursor actors work one canvas simultaneously (collaborative-canvas ambience) — per-cursor deterministic waypoint schedules, name-tag pills in distinct colors, grab/drop actions on an interleaved beat grid so paths and actions never collide; the camera stays locked, the liveness itself is the message. +metadata: + tags: cursor, multi-cursor, collaboration, ensemble, canvas, name-tag, choreography, ambient, teamwork +--- + +# Multi-Cursor Choreography + +> **The camera never chases anyone.** No real camera — any "pan" is the canvas group translating inside a static frame. And per the motion doctrine's idle-motion ban, every cursor must **perform**: travel to a target, act, then rest still. Scheduled rest is stillness; aimless wander loops are wobble. + +THE ensemble primitive: **two to four labeled cursor actors** — each an arrow plus a name-tag pill in its own color — work one shared canvas at the same time. No single interaction is the subject; the **simultaneous liveness is** ("a team is in here, working"), usually as ambience under a headline building over the top. Distinct from [cursor-click-ripple.md](cursor-click-ripple.md) and [cursor-drag.md](cursor-drag.md): those are **one protagonist** the viewer follows click-by-click; here the actors are chorus, not lead — each action smaller and quieter than a solo cursor's, the value in the interleaving. Also distinct from [camera-cursor-tracking.md](camera-cursor-tracking.md): that locks the _viewport_ to one focal cursor; this rule forbids exactly that — the frame is static and the eye roams freely. + +## How It Works + +Everything hangs off one data table: + +1. **The actor table** — a literal `ACTORS` array: per actor a name, a color, and a **waypoint schedule** (`{ x, y, at, dur }` legs plus action beats). All coordinates and times are hand-authored constants — the choreography is data: deterministic, seekable, and auditable for collisions before a single frame renders. +2. **Legs as explicit `fromTo`s** — each leg tweens the actor wrapper from the previous waypoint to the next at an absolute position. Gaps between legs are **rests**: the cursor sits still exactly where it landed. +3. **Actions** — a leg can end in a grab (press dip; the payload rides the next leg in lockstep — [cursor-drag.md](cursor-drag.md) mechanics at chorus intensity), a drop (`tl.set` identity swap + tiny settle pop), or a hover (a highlight fades in under the tip, once, then holds). +4. **The interleaved beat grid** — actions land on **alternating beats** (~1.2 / 2.6 / 4.0 s): at any moment at most one action lands while the others glide or rest. Each actor owns a home **zone** of the canvas; only one actor at a time leaves its zone, so paths never cross near-simultaneously. (Short specimens under ~5s can compress beat spacing to ~0.3–0.9s — zones still prevent collisions; the ≥1s spacing is for ambience-length shots.) +5. **Ambience staging** — cursors may already be mid-canvas at t=0 (the team was working before we arrived — the collaborative-canvas idiom), or enter off-frame on staggered starts. The canvas group may slowly translate-pan under the ensemble (element translate, not a camera). + +## Recipe + +```html + +
+
{mockupA}
+
{chipLabel}
+
+
+ + {actorName1} +
+``` + +```js +// The choreography IS this table — all literals; read the `at` columns to +// verify beats interleave. Each actor owns a zone. +const ACTORS = [ + { + id: "#actor-1", // zone: left mockup + legs: [ + { from: { x: 180, y: 420 }, to: { x: 320, y: 300 }, at: 0.2, dur: 0.9 }, + { to: { x: 340, y: 480 }, at: 2.0, dur: 0.8 }, // rest 0.9s between legs + ], + }, + { + id: "#actor-2", // zone: center mockup + legs: [ + { from: { x: 900, y: 200 }, to: { x: 820, y: 360 }, at: 0.6, dur: 1.0 }, + { to: { x: 980, y: 380 }, at: 3.4, dur: 0.7 }, + ], + }, + { + id: "#actor-3", // zone: right panel — enters from off-frame + legs: [{ from: { x: 1980, y: 520 }, to: { x: 1560, y: 460 }, at: 1.4, dur: 1.1 }], + }, +]; + +ACTORS.forEach((actor) => { + let prev = actor.legs[0].from; + tl.set(actor.id, { x: prev.x, y: prev.y }, 0); // on stage (or off) from t=0 + actor.legs.forEach((leg) => { + tl.fromTo( + actor.id, + { x: prev.x, y: prev.y }, + { x: leg.to.x, y: leg.to.y, duration: leg.dur, ease: "power2.inOut", immediateRender: false }, + leg.at, + ); + prev = leg.to; + }); +}); + +// Actions at chorus intensity — actor 1 grabs the chip: press dip, then the +// chip rides leg 2 in lockstep (matched tween: same position, duration, ease). +tl.to("#actor-1", { scale: 0.88, duration: 0.07, ease: "power2.in", yoyo: true, repeat: 1 }, 1.1); +tl.fromTo( + "#chip-1", + { x: 0, y: 0 }, + { x: CHIP_DX, y: CHIP_DY, duration: 0.8, ease: "power2.inOut", immediateRender: false }, + 2.0, // = actor-1 leg 2 `at` and `dur`, exactly +); +// Drop: identity swap + tiny settle — quieter than a solo cursor's snap +tl.set("#chip-1", { backgroundColor: "{chipSwapColor}" }, 2.8); +tl.fromTo( + "#chip-1", + { scale: 1.06 }, + { scale: 1, duration: 0.2, ease: "power3.out", immediateRender: false }, + 2.8, +); + +// Optional ambient canvas pan (element translate, NOT a camera) +tl.fromTo("#canvas-group", { x: 0 }, { x: PAN_DX, duration: 6.0, ease: "none" }, 0.3); +``` + +## Variations + +- **Ambient collaborative canvas (the Hook register)** — the default: actors mid-canvas at t=0, canvas slowly panning, a headline building over the top ([waterfall-entry.md](waterfall-entry.md)). The demo is set-dressing for the words; keep every action small and the beat grid loose. +- **One labeled editor (N = 1, still ensemble-styled)** — a single labeled teammate cursor performs one visible edit (deletes and retypes a headline word via [discrete-text-sequence.md](discrete-text-sequence.md), or drops one component). The name tag is the point: _a person_ did this. +- **Featured beat inside the ensemble** — one actor briefly becomes the lead: full [cursor-drag.md](cursor-drag.md) grab-carry-drop with chrome while the others explicitly REST for that window. Freeze the chorus; two things moving with intent at once splits the eye. +- **Staggered entrances** — cursors enter from off-frame at `ENTER_AT + i * ENTER_STAGGER`, each gliding to its zone ("the team assembles"); entry vectors from different edges, per the house cursor entry law. + +## Values + +| token | range | notes | +| ------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| ACTOR_COUNT | 2–4 | one is a solo rule's job; five+ reads as noise — no viewer tracks five pointers | +| leg `dur` | 0.6–1.2 s, `power2.inOut` | human, considered mouse movement; sub-0.5 s across long distances reads as a teleport | +| rest gaps | 0.5–1.5 s | rests make the ensemble read as people; zero-rest actors read as screensavers | +| action beat spacing | ≥ 1.0 s | while one acts, others may glide but must not act — audit by sorting all `at` values | +| zones | one per actor | only the acting actor crosses zones; two cursors within ~80 px reads as a glitch — check waypoint pairs at overlapping times | +| PAN_DX | ~40–80 px, linear | parallax life, not a camera move; omit for busier ensembles | +| tag / arrow size | smaller than a solo lead | the oversized-cursor treatment is for protagonists; tags must stay legible at render resolution | +| colors | one saturated hue each | from the palette's accent range; tag pill and arrow fill share the hue | + +## Critical Constraints + +- **The table is the choreography** — all waypoints, times, and actions are literal data. If you can't verify non-collision by reading the `at` columns, the schedule is too clever. +- **Every leg is an explicit `fromTo`** with the previous waypoint as the from-state, `immediateRender: false` on all but each actor's initial placement — chained `.to()`s on shared properties capture stale starts under seek. +- **Interleave, never chord** — at most one action landing at any moment; simultaneous travel is fine (that's the liveness), simultaneous _payoffs_ compete. +- **Chorus intensity** — every action is a quieter version of its solo rule: smaller dips, subtler snaps, no ripple bursts; save full treatment for a featured beat. +- **Rest is stillness** — between legs a cursor holds exactly where it landed: no idle drift, no yoyo wander on any actor. +- **Payload lockstep** — a carried chip's tween matches its actor's leg exactly (position, duration, ease), per the cursor-drag law. +- **The wrapper moves, never the parts** — arrow + name tag are one element; tweening them separately shears the actor apart under seek. +- **Camera locked** — no viewport zoom/pan tweens; the only large-scale motion is the linear canvas-group translate. Never zoom to an actor (that's a solo-cursor shot). +- **Actors are people** — human-speed glides, pauses, one thing at a time; `pointer-events: none` on all actors. Check `tl.duration()` — ensembles accumulate long tails from late rests. + +## See also + +`cursor-drag` (full-treatment featured beat) · `cursor-click-ripple` (chorus click — press only, skip the ripple) · `discrete-text-sequence` (a labeled actor's retype edit) · `viewport-change` (the canvas-group translate math) · `spring-pop-entrance` (components popping in as drop results). diff --git a/skills/hyperframes-animation/rules/multi-phase-camera.md b/skills/hyperframes-animation/rules/multi-phase-camera.md index 3bbe74211..9b03ce7cf 100644 --- a/skills/hyperframes-animation/rules/multi-phase-camera.md +++ b/skills/hyperframes-animation/rules/multi-phase-camera.md @@ -7,192 +7,79 @@ metadata: # Multi-Phase Camera -A camera wrapper around the entire scene that progresses through discrete zoom phases at scripted triggers. Continuous sine-driven micro-drift overlays so the camera never feels static between phases. Distinct from a single linear zoom — multi-phase creates "cinematic pacing" (anticipation → reveal → settle). +A camera wrapper around the ENTIRE scene that progresses through discrete zoom phases at scripted triggers, with continuous sine-driven micro-drift overlaid so the camera never feels static between phases. Distinct from a single linear zoom — multi-phase creates cinematic pacing (anticipation → reveal → settle). ## How It Works -The camera is a single wrapping `
` whose `transform: scale() translate(x, y)` is driven by: +The camera is one wrapping `
` whose `transform: scale() translate(x, y)` is composed from two channels inside a single `onUpdate` writer: -1. **Phase scale** — a stepwise scale value that advances through phases at trigger times (e.g. `PHASE_1_SCALE` at t=0 → `PHASE_2_SCALE` at PHASE_2_AT → `PHASE_3_SCALE` at PHASE_3_AT) -2. **Drift offset** — a continuous sine-based `translateX` / `translateY` (small amplitude, slow frequency) ADDED to the phase transform +1. **Phase scale** — a proxy object `{ scale }` stepped through phases at trigger times (`PHASE_1_SCALE` at t=0 → `PHASE_2_SCALE` at `PHASE_2_AT` → `PHASE_3_SCALE` at `PHASE_3_AT`). +2. **Drift offset** — a continuous sine-based `translateX` / `translateY` (small amplitude, slow frequency) ADDED to the phase transform. X and Y run at slightly different frequencies (`DRIFT_FREQ_RATIO ≈ 1.3`) — equal frequencies produce a perfect diagonal that reads mechanical; ~1.3 gives an organic Lissajous. -Both run inside the GSAP timeline so HF seeks frame-by-frame deterministically. - -## HTML +## Recipe ```html -
-
-
-
{Brand}
-
{tagline}
-
{ctaText}
-
+
+
+
{Brand}
+
{tagline}
+
{ctaText}
``` -## CSS - ```css .scene { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - background: {sceneBgColor}; + overflow: hidden; /* REQUIRED — any phase scale < 1 exposes the content's edges */ + background: {sceneBgColor}; /* background on .scene, NOT .camera — a camera-borne + background warps/translates with the transform and reveals the outer void */ } .camera { position: absolute; inset: 0; display: grid; place-items: center; - transform-origin: 50% 50%; + transform-origin: 50% 50%; /* off-center origin creates phase-to-phase drift */ will-change: transform; } -.content { - display: flex; - flex-direction: column; - align-items: center; - gap: 32px; - text-align: center; -} -.hero { - font-family: {font}; - font-weight: 900; - font-size: {heroSize}; - letter-spacing: 8px; - color: {textColor}; - text-transform: uppercase; -} -.tagline { - font-family: {font}; - font-weight: 600; - font-size: {taglineSize}; - color: {accentColor}; -} -.cta { - font-family: {monoFont}; - font-weight: 700; - font-size: {ctaSize}; - letter-spacing: 6px; - color: {accentColor}; - text-transform: uppercase; -} ``` -## GSAP Timeline +```js +const camera = document.getElementById("camera"); -```html - - +// Content reveals happen INSIDE the camera frame (hero/tagline/cta beats). ``` -## How to Choose Values - -- **PHASE_1_SCALE / PHASE_2_SCALE / PHASE_3_SCALE** — three-step zoom values - - Range: PHASE_1 0.88–0.96; PHASE_2 0.98–1.02; PHASE_3 1.04–1.15 - - Effects: tighter spread = subtler camera; wider = more cinematic - - Constraints: at PHASE_1_SCALE < 1, `.scene` MUST have `overflow: hidden` or the inner content's edges leak outside the frame - -- **PHASE_2_AT / PHASE_2_DUR** — when the focus phase starts and how long it takes - - Range: PHASE_2_AT 0.3–1.0 s; PHASE_2_DUR 1.0–1.8 s - - Effects: longer DUR = slower settle, more cinematic - -- **PHASE_3_AT / PHASE_3_DUR** — when the push phase starts and how long it takes - - Range: PHASE_3_AT 2.0–4.0 s; PHASE_3_DUR 1.0–2.0 s - - Constraints: PHASE_3_AT must be ≥ PHASE_2_AT + PHASE_2_DUR (otherwise focus is preempted) - -- **PHASE_2_EASE / PHASE_3_EASE** — ease per transition - - Discrete choice: `power2.out`, `power3.out`, `power2.inOut` - - Selection: cinematic feel; spring/back easing on a camera feels uncomfortable. Each later phase should imply more settling than the previous (longer dur OR more out-easing). - -- **TOTAL_DURATION** — composition's total runtime (matches `data-duration`) - - Reference: the drift tween must span the whole composition - -- **DRIFT_CYCLES** — number of sine cycles across TOTAL_DURATION - - Range: 1–3 - - Effects: 1 = one slow breath; 3 = noticeably busier - - Constraints: high values read as mechanical wobble rather than organic drift - -- **DRIFT_AMP_X / DRIFT_AMP_Y** — peak drift offset in pixels - - Range: DRIFT_AMP_X 2–8 px; DRIFT_AMP_Y 1–4 px - - Effects: per-frame imperceptible, visible over time. If drift is a discrete shake, it's too much. - -- **DRIFT_FREQ_RATIO** — multiplier on the Y-axis sine frequency - - Range: 1.2–1.5 - - Effects: 1.0 = perfect diagonal (reads mechanical); ~1.3 = organic Lissajous - -- **HERO_AT / TAGLINE_AT / CTA_AT** — content reveal beats - - Constraints: HERO_AT should land AFTER PHASE_1 settles via PHASE_2 (otherwise the hero feels like it's flying away while camera is still pulling back) - ## Phase Patterns -| Pattern | Scale Sequence (Phase 1 → 2 → 3) | Feel | When to use | +| Pattern | Scale sequence (1 → 2 → 3) | Feel | When to use | | ------------------- | --------------------------------- | ------------------------------- | ----------------------------- | | **Focus-in** | back → neutral → slight push | Approach → settle → slight push | Default product reveal | | **Dramatic reveal** | push → neutral → pull | Wide → focus → settle back | Hero shot with breathing room | @@ -201,73 +88,42 @@ Both run inside the GSAP timeline so HF seeks frame-by-frame deterministically. ## Variations -### Phase trigger by content beat (not time) - -If the composition has content phases (e.g. an entry completes, then orbit starts), align the camera tween start time with the content tween's end time rather than using a fixed clock value. - -### Camera shake (panic / impact) - -For a brief shake instead of drift, replace the drift tween with a higher-amplitude, higher-frequency one over a short window: +- **Phase trigger by content beat**: align a camera tween's start with a content tween's end (entry completes → push begins) rather than a fixed clock value. +- **Camera shake (panic / impact)**: a brief higher-amplitude, higher-frequency drift tween over a short window — same `drift` mechanism with `SHAKE_AMP` / `SHAKE_CYCLES` / `SHAKE_DUR` at `SHAKE_AT`. +- **Targeted zoom into an off-center element**: combine scale with counter-translation so the target lands at viewport center — divide the measured offset by the current scale before feeding it into the writer: ```js -tl.to( - drift, - { - p: Math.PI * 2 * SHAKE_CYCLES, - duration: SHAKE_DUR, - ease: "none", - onUpdate: () => { - const dx = Math.sin(drift.p) * SHAKE_AMP_X; - const dy = Math.sin(drift.p * SHAKE_FREQ_RATIO) * SHAKE_AMP_Y; - camera.style.transform = `scale(${phase.scale}) translate(${dx}px, ${dy}px)`; - }, - }, - SHAKE_AT, -); -``` - -### Targeted zoom into off-center element - -If the climax should zoom into a non-centered element, combine scale with counter-translation. Compute the offset so the target ends at viewport center after scale: - -```js -const target = document.querySelector(".cta"); -const tRect = target.getBoundingClientRect(); -const viewportCenter = { x: STAGE_W / 2, y: STAGE_H / 2 }; -const offsetX = (viewportCenter.x - (tRect.left + tRect.width / 2)) / phase.scale; -const offsetY = (viewportCenter.y - (tRect.top + tRect.height / 2)) / phase.scale; +const tRect = document.querySelector(".cta").getBoundingClientRect(); +const offsetX = (STAGE_W / 2 - (tRect.left + tRect.width / 2)) / phase.scale; +const offsetY = (STAGE_H / 2 - (tRect.top + tRect.height / 2)) / phase.scale; // then in onUpdate: translate(offsetX + dx, offsetY + dy) ``` -## Key Principles +(Full counter-translate doctrine: [coordinate-target-zoom.md](coordinate-target-zoom.md).) -- **Drift is imperceptible per-frame, visible over time** — if drift reads as discrete shake, the amplitude is too high -- **Drift X and Y at slightly different frequencies** — `DRIFT_FREQ_RATIO ≈ 1.3` prevents perfect-diagonal motion, which reads as mechanical -- **Phase springs softer than UI springs** — `power2.inOut` or `power3.out` for cinematic feel; spring/back easing on a camera feels uncomfortable -- **Each later phase settles "deeper"** — phase 2 ease should imply more settling than phase 1 (longer duration OR more out-easing). Wakes up → settles → settles deeper -- **Camera wraps EVERYTHING in the scene** — applying camera per-element creates parallax bugs and breaks "this is one viewpoint" -- **❗ overflow: hidden on .scene** — phases that pull back (`scale < 1`) reveal edges of the inner content. Without `overflow: hidden`, those edges leak outside the stage frame and HF renders them as visible content -- **❗ Hero reveal starts AFTER initial pullback ease lands** — if the camera is still pulling back when the headline fades in, the headline feels like it's flying away +## Values + +| token | range | notes | +| --------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | +| PHASE_1 / 2 / 3_SCALE | 0.88–0.96 / 0.98–1.02 / 1.04–1.15 | tighter spread = subtler camera; scale < 1 REQUIRES `overflow: hidden` on `.scene` | +| PHASE_2_AT / PHASE_2_DUR | 0.3–1.0s / 1.0–1.8s | longer DUR = slower settle, more cinematic | +| PHASE_3_AT / PHASE_3_DUR | 2.0–4.0s / 1.0–2.0s | PHASE_3_AT ≥ PHASE_2_AT + PHASE_2_DUR or focus is preempted | +| PHASE_2_EASE / PHASE_3_EASE | `power2.out` `power3.out` `power2.inOut` | spring/back easing on a camera feels uncomfortable; each later phase settles deeper | +| TOTAL_DURATION | = `data-duration` | the drift tween must span the whole composition | +| DRIFT_CYCLES | 1–3 | 1 = one slow breath; high values read as mechanical wobble | +| DRIFT_AMP_X / DRIFT_AMP_Y | 2–8 px / 1–4 px | imperceptible per-frame, visible over time — if it reads as a shake, it's too much | +| DRIFT_FREQ_RATIO | 1.2–1.5 | 1.0 = perfect diagonal (mechanical); ~1.3 = organic Lissajous | +| HERO_AT (etc.) | after Phase-2 settle lands | a hero fading in mid-pull-back feels like it's flying away | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition` on `.camera`** — competes with the GSAP transform -- **`transform-origin: 50% 50%`** on camera — off-center origin creates unpredictable phase-to-phase drift -- **`will-change: transform`** on `.camera` — the camera transform updates every frame -- **`overflow: hidden` on `.scene`** — required when any phase scale < 1 -- **Scene background on `.scene`, not `.camera`** — if background is on camera, scaling/translating it reveals the outer void +- **Camera wraps EVERYTHING in the scene** — a per-element camera creates parallax bugs and breaks the "one viewpoint" read. +- **One writer**: phase scale and drift compose inside the single drift `onUpdate`; nothing else touches `camera.style.transform`. +- **`overflow: hidden` on `.scene`** — required whenever any phase scale < 1. +- **`transform-origin: 50% 50%` on `.camera`** — off-center origin creates unpredictable phase-to-phase drift. +- **Scene background on `.scene`, not `.camera`** — otherwise scaling/translating reveals the outer void. +- **Hero reveal starts AFTER the initial pull-back ease lands** — otherwise the headline feels like it's flying away. -## Combinations +## See also -- [orbit-3d-entry.md](orbit-3d-entry.md) — orbit motion inside a slowly drifting camera -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — climax phase push synced to counter peak -- [3d-text-depth-layers.md](3d-text-depth-layers.md) — depth-stacked hero with cinematic camera moves -- [sine-wave-loop.md](sine-wave-loop.md) — element idle inside the camera (compound motion) - -## Pairs with HF skills - -- `/hyperframes-animation` — multi-phase tween + drift onUpdate -- `/hyperframes-core` — composition wiring, scene wrapper -- `/hyperframes-cli` — `hyperframes lint` +[coordinate-target-zoom.md](coordinate-target-zoom.md) (counter-translate math for the targeted variation) · [orbit-3d-entry.md](orbit-3d-entry.md) (orbit inside a drifting camera) · [counting-dynamic-scale.md](counting-dynamic-scale.md) (climax push synced to counter peak) · [3d-text-depth-layers.md](3d-text-depth-layers.md) (depth-stacked hero under camera moves) · [sine-wave-loop.md](sine-wave-loop.md) (element idle inside the camera). diff --git a/skills/hyperframes-animation/rules/orbit-3d-entry.md b/skills/hyperframes-animation/rules/orbit-3d-entry.md index 8d6203f47..ef46b771c 100644 --- a/skills/hyperframes-animation/rules/orbit-3d-entry.md +++ b/skills/hyperframes-animation/rules/orbit-3d-entry.md @@ -7,295 +7,142 @@ metadata: # Orbit with 3D Entry -Elements flip in from 3D space (rotateX + rotateY + translateZ) then transition into a continuous elliptical orbit around a focal point. Distinct from one-shot reveals — the orbit keeps running. +Elements flip in from 3D space (`rotateX` + `rotateY` + negative `z`) then settle into a continuous elliptical orbit around a center label. Distinct from one-shot reveals — the orbit keeps running, driven by a 0→1 progress tween INSIDE the timeline (never rAF). ## How It Works -Two phases per element: +Per element, two phases: (1) a `back.out` flip from a hidden 3D orientation to flat — **in place at its orbital starting position** (see Critical Constraints); (2) a continuous orbit where `onUpdate` computes `x/y` from `cos/sin(initialAngle + p·2π)` on the ellipse. The stage needs `perspective` on the scene root and `preserve-3d` on stage + items, or the flip flattens to a 2D scale. -1. **Entry (per element)**: GSAP tween from hidden 3D orientation (`rotateX`, `rotateY`, negative `z`) to flat (`rotateX: 0, rotateY: 0, z: 0`). Spring-like ease (`back.out`) for the flip-in. -2. **Orbit (after entry)**: Continuous trigonometric position around a center point. The element's `x` and `y` translate are driven by `cos(t)` and `sin(t)` at a slow angular speed. - -The orbit runs **inside the timeline** — not via `requestAnimationFrame` — so HF seek-by-frame stays deterministic. - -## HTML +## Recipe ```html -
-
-
{glyph1}
-
{glyph2}
-
{glyph3}
-
{glyph4}
-
{glyph5}
-
{glyph6}
-
{centerLabel}
-
+ +
+
{glyph1}
+
{glyph2}
+ +
{centerLabel}
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; +.scene-root { display: grid; place-items: center; - background: {sceneBackground}; - perspective: 1800px; /* REQUIRED — without perspective, rotateX/Y flatten */ + perspective: 1800px; /* REQUIRED */ } .orbit-stage { position: relative; - width: 1000px; - height: 700px; display: grid; place-items: center; transform-style: preserve-3d; } .orbit-item { position: absolute; - /* Items live at stage center; GSAP translates them along the orbit. */ top: 50%; left: 50%; - width: 140px; - height: 140px; - display: grid; - place-items: center; - background: {accentColor}; - border-radius: 50%; - font-family: {font}; - font-weight: 900; - font-size: 64px; - color: {itemTextColor}; transform-style: preserve-3d; will-change: transform; - box-shadow: 0 12px 36px {accentShadowColor}; } .orbit-center { position: relative; - z-index: 5; - font-family: {font}; - font-weight: 900; - font-size: 96px; - letter-spacing: 8px; - color: {centerTextColor}; - text-transform: uppercase; + transform: translateZ(220px); /* wins paint order inside preserve-3d */ + z-index: 9999; } ``` -## GSAP Timeline +```js +const items = document.querySelectorAll(".orbit-item"); +const RADIUS_Y = RADIUS_X * Y_TO_X_RATIO; // perspective-flattened ellipse -```html - - -``` + // 3) Continuous orbit — each item gets its OWN progress tween (own initialAngle) + const orbit = { p: 0 }; + tl.to( + orbit, + { + p: 1, + duration: ORBIT_DURATION, + ease: "none", + onUpdate: () => { + const a = a0 + orbit.p * Math.PI * 2; + const x = Math.cos(a) * RADIUS_X; + const y = Math.sin(a) * RADIUS_Y; + // capped z-index band [1, 50] — see center-label clearance below + el.style.zIndex = String(1 + Math.round(((y + RADIUS_Y) / (2 * RADIUS_Y)) * 49)); + el.style.transform = `translate(-50%, -50%) translate(${x}px, ${y}px)`; + }, + }, + i * STAGGER + ENTRY_DUR, + ); +}); -## How to Choose Values - -- **RADIUS_X** — horizontal radius of the orbit ellipse, in px - - Range: 300–900 px - - Effects: small radius reads as a tight cluster; large radius spreads the ring across the frame and lets a large center element breathe - - Constraints: must clear the center element horizontally at every angle — see Key Principles for the `RADIUS_X * min(|cos(θ)|) ≥ L_w + I_w + breathing_room` rule - - Reference: ../../examples/cta-orbit-collapse.html uses 480 - -- **Y_TO_X_RATIO** — `RADIUS_Y / RADIUS_X`, the orbit's perspective flattening - - Range: 0.4–0.7 - - Effects: low values read as a near-horizontal disc seen from above; values approaching 1 read as a flat plane facing the camera - - Constraints: keep < 1 — the orbit should look like a tilted ring, not a frontal halo - - Reference: ../../examples/cta-orbit-collapse.html uses ≈ 0.58 - -- **ORBIT_DURATION** — seconds for one full revolution - - Range: 4–25 s (longer for ambient backdrop, shorter for active feature motion) - - Effects: short durations look frenetic; long durations read as drifting / calm - - Constraints: must be ≥ the time the orbit is on screen, otherwise the tween ends and items stop - - Reference: ../../examples/cta-orbit-collapse.html uses ~25 s effective (orbit speed 0.25 rad/s) - -- **ENTRY_DUR** — per-element flip-in duration - - Range: 0.4–0.8 s - - Effects: short feels punchy; long feels stately - - Constraints: must be ≤ the gap between the first and last element's start so the cascade doesn't overlap to incoherence - - Reference: ../../examples/cta-orbit-collapse.html uses 0.55 s - -- **STAGGER** — delay between consecutive element entries - - Range: 0.06–0.12 s - - Effects: below ~0.06 s reads as "popcorn"; above ~0.12 s reads as plodding - - Constraints: total cascade `(n - 1) * STAGGER` should still complete before the next scene phase begins - - Reference: ../../examples/cta-orbit-collapse.html uses 0.10 s - -- **FLIP_BACK** — `back.out()` overshoot for the flip-in - - Range: 1.2–2.0 - - Effects: low end is a soft arrive; high end snaps with visible overshoot - - Constraints: pair with a calmer `CENTER_BACK` if both fire close together — competing overshoots cancel each other - - Reference: ../../examples/cta-orbit-collapse.html uses 1.4 - -- **CENTER_BACK** — `back.out()` overshoot for the center label fade-in - - Range: 1.2–1.8 - - Effects: low end keeps the label calm under the busy orbit; high end gives it a small "pop" of arrival - - Reference: ../../examples/cta-orbit-collapse.html uses 1.4 - -- **CENTER_FADE_AT** — when the center label fades in, in seconds - - Range: just after the first 2–4 elements have landed - - Effects: too early competes with the cascade; too late leaves a hole at the center of the orbit - - Reference: ../../examples/cta-orbit-collapse.html starts the center brand near the front of the scene - -- **ROTATE_X_FROM / ROTATE_Y_FROM / Z_FROM / SCALE_FROM** — initial 3D orientation - - Range: rotateX ±60° to ±120°; rotateY ±45° to ±120°; z −200 to −400; scale 0.2–0.6 - - Effects: higher absolute rotation + deeper negative z = more dramatic "card flipping out of depth"; lower = subtle reorientation - - Constraints: pick a direction consistent with the scene's perspective; mixing positive and negative rotateY across items reads as noise - - Reference: ../../examples/cta-orbit-collapse.html uses rotateX 90, rotateY −45, z −100, scale 0 - -## Variations - -### Collapse to center - -To reverse — orbit then collapse inward — interpolate `RADIUS_X` and `RADIUS_Y` to 0 in a final phase by multiplying both radii by a 1→0 driver: - -```js -const collapse = { r: 1 }; -tl.to( - collapse, - { - r: 0, - duration: COLLAPSE_DUR, - ease: "power3.inOut", - onUpdate: () => - items.forEach((el) => { - const a = (Number(el.dataset.angle) / 360) * Math.PI * 2; - const x = Math.cos(a) * RADIUS_X * collapse.r; - const y = Math.sin(a) * RADIUS_Y * collapse.r; - el.style.transform = `translate(-50%,-50%) translate(${x}px,${y}px) scale(${collapse.r})`; - }), - }, - COLLAPSE_AT, +tl.from( + ".orbit-center", + { opacity: 0, scale: 0.6, duration: ENTRY_DUR, ease: `back.out(${CENTER_BACK})` }, + CENTER_FADE_AT, ); ``` -### Tilted orbit plane +## Variations -For a more dramatic 3D orbit, rotate the entire `.orbit-stage` on the X axis: +- **Collapse to center**: a final 1→0 driver multiplies both radii (and item scale) in `onUpdate` — the ring condenses into the center element; pairs with a CTA "click" igniting the collapse. +- **Tilted orbit plane**: `rotateX(25deg)` on `.orbit-stage` — items visibly arc through the plane. -```css -.orbit-stage { - transform: rotateX(25deg); -} -``` +## Values -Items rendered above/below the equator visually arc through the plane. - -## Key Principles - -- **`perspective` on scene root REQUIRED** — without it, rotateX/Y read as 2D scale and the flip-in looks flat -- **`transform-style: preserve-3d`** on both the stage and each item — preserves the 3D context as items have their own transforms -- **Stagger entries** — cascade reads as "swarm forming," simultaneous reads as "popcorn." See `STAGGER` in How to Choose Values -- **Element count 4-12** — fewer feels empty, more crowds the center -- **❗ Center label clearance — translateZ + capped item z-index** — `z-index` ALONE is unreliable inside a `transform-style: preserve-3d` stage (paint order follows Z position, not stacking-context z-index). For the orbit to NEVER occlude the headline: - 1. Push the center label forward: `transform: translateZ(220px); z-index: 9999;` - 2. Cap orbit-item dynamic z-index in `[1, 50]` so bottom-of-orbit items still read as "in front of" top-of-orbit items, but **never above the center label**. e.g.: `el.style.zIndex = String(1 + Math.round((y + RADIUS_Y) / (2 * RADIUS_Y) * 49));` - 3. **Choose `RADIUS_X` so items also clear the center label HORIZONTALLY at all angles.** If the label's half-width is `L_w` and the item's half-width is `I_w`, then `RADIUS_X` must satisfy `RADIUS_X * min(|cos(θ_minimum)|) ≥ L_w + I_w + breathing_room`. For a 6-item orbit with 60° angular spacing, the worst case is `cos(30°) ≈ 0.866` between items. Scale `RADIUS_X` with the center label's width — a heavier wordmark needs a wider ring. -- **❗ Center element is the headline** — the orbit is ornamental motion around it. If the orbit dominates the eye, increase center element size or fade orbit items down +| token | range | notes | +| ----------------------- | ----------------------------- | ------------------------------------------------------------------- | +| RADIUS_X | 300–900px | must also clear the center label horizontally (see below) | +| Y_TO_X_RATIO | 0.4–0.7 | keep < 1 — a tilted ring, not a frontal halo | +| ORBIT_DURATION | 4–25s per revolution | ≥ time on screen, or the tween ends and items freeze | +| ENTRY_DUR | 0.4–0.8s | | +| STAGGER | 0.06–0.12s | below reads "popcorn", above reads plodding | +| FLIP_BACK / CENTER_BACK | 1.2–2.0 / 1.2–1.8 | calm the center pop if both fire close together | +| CENTER_FADE_AT | after 2–4 items land | too early competes; too late leaves a hole | +| ROTATE_X/Y_FROM, Z_FROM | ±60–120°, ±45–120°, −200…−400 | one consistent rotation direction across items; mixed signs = noise | +| SCALE_FROM | 0.2–0.6 | | +| item count | 4–12 | fewer feels empty, more crowds the center | ## Critical Constraints -- **No `requestAnimationFrame`** — orbit must run inside the timeline so HF seeks frame-by-frame deterministically -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Each item gets its OWN orbit tween** — don't share one tween with `targets: '.orbit-item'` because each starts at a different `initialAngle` -- **`will-change: transform`** — many simultaneous orbital transforms benefit from compositor hints -- **Don't animate `left`/`top`** — use `translate()` (composes with `translate(-50%, -50%)` centering) -- **❗ Entry must flip IN PLACE at orbital position, NOT at center** — a fromTo whose "from" and "to" both have `x: 0, y: 0` keeps the item at the stage center during phase 1, so it collides with the center label during flip-in (and then snaps to orbit on phase 2 start — a visible teleport). +- **❗ Entry must flip IN PLACE at the orbital position, NOT at center** — `gsap.set` each item at `(cos(a0)·RADIUS_X, sin(a0)·RADIUS_Y)` with `opacity: 0` BEFORE adding tweens, then phase 1 animates only rotation/opacity/scale. A fromTo that keeps `x/y: 0` flips at the stage center, collides with the center label, then teleports to the orbit when phase 2 starts. +- **❗ Center-label clearance** — `z-index` alone is unreliable inside `preserve-3d` (paint order follows actual Z): push the label forward with `translateZ(220px)` + `z-index: 9999`, cap item z-index to `[1, 50]`, AND size the ring so items clear the label horizontally at every angle: `RADIUS_X × min|cos(θ)| ≥ L_w + I_w + breathing_room` (label/item half-widths; for 6 items the worst case is `cos(30°) ≈ 0.866`). A heavier wordmark needs a wider ring. +- **Each item gets its OWN orbit tween** — a shared `targets: ".orbit-item"` tween can't carry per-item `initialAngle`. +- **The center element is the headline** — the orbit is ornament; if it dominates, grow the center or fade the items down. - The correct pattern (see GSAP Timeline above) is to `gsap.set()` each item at `(cos(initialAngle)*RADIUS_X, sin(initialAngle)*RADIUS_Y)` with `opacity: 0` BEFORE adding tweens, then have phase 1 animate only rotation/opacity/scale — NOT translate. The item fades in IN PLACE at its orbital starting point, and phase 2 picks up the orbit smoothly from there. +## See also -## Combinations - -- [center-outward-expansion.md](center-outward-expansion.md) — alternative entry pattern (burst, not orbit); also the reversed driver for an orbit-collapse finish -- [cursor-click-ripple.md](cursor-click-ripple.md) — pairs naturally when the center element is a CTA the user "clicks" to trigger the collapse -- [sine-wave-loop.md](sine-wave-loop.md) — per-item idle wobble layered on top of the orbit - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + `onUpdate` API -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`center-outward-expansion` (burst entry; reversed driver = the collapse finish) · `cursor-click-ripple` (the click that triggers a collapse) · `depth-scatter-assemble` (3D entrance that resolves flat instead of orbiting). diff --git a/skills/hyperframes-animation/rules/particle-burst.md b/skills/hyperframes-animation/rules/particle-burst.md new file mode 100644 index 000000000..01a96e4a8 --- /dev/null +++ b/skills/hyperframes-animation/rules/particle-burst.md @@ -0,0 +1,149 @@ +--- +name: particle-burst +description: Deterministic particle / confetti events — a confetti pop that bursts up and drifts down (optionally instant-shrinking away), a dot burst from behind text, or a glyph dissolving to particles. Every particle's state is a pure ballistic function of timeline time from index-seeded values, so a scrub to any t shows the correct mid-flight frame. +metadata: + tags: particles, confetti, burst, dissolve, celebration, ballistic, deterministic, punctuation +--- + +# Particle Burst + +Discrete flying particles as a one-shot event: a **confetti pop** that erupts upward and drifts back down on gravity, a **dot burst** radiating from behind a landing word, or a **glyph dissolve** where text breaks into particles that scatter and die. Particles are ephemeral garnish — born from a beat, fly, gone; they never become layout. + +Boundaries: [css-marker-patterns.md](css-marker-patterns.md)'s burst mode is radiating **drawn lines** — a static accent, no flight. [press-release-spring.md](press-release-spring.md)'s release burst is **one blurred radial layer** faking an explosion — enough when a single glow pop will do. [center-outward-expansion.md](center-outward-expansion.md) moves **real layout elements** to final resting slots; particles have no destination, only physics and a death. + +## How It Works + +The whole event is **one driver tween and one formula**: + +1. **Seeded setup** — a fixed pool of `PARTICLE_COUNT` small divs is created once at composition setup (a deterministic loop — setup-time generation is fine; per-frame DOM creation is not). Each particle `i` derives everything from a pure hash: + + ```js + // angle, speed, size, spin, color (palette[i % palette.length]) — all from prand(i * k) + const prand = (n) => { + const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; + return x - Math.floor(x); // 0..1, pure function of n + }; + ``` + +2. **Ballistic formula** — a proxy tween advances `T: 0 → 1` over `FLIGHT_DUR` with `ease: "none"`; `onUpdate` positions every particle as a **pure function of T**: + + ``` + x(T) = vx · T·FLIGHT_DUR + y(T) = vy · T·FLIGHT_DUR + ½ · G · (T·FLIGHT_DUR)² + rot(T) = spin · T·FLIGHT_DUR + ``` + + Gravity `G` supplies the rise-decelerate-fall arc for free. Because position is computed from `T` (never accumulated per frame), a seek to any moment renders the exact mid-flight state — this is what makes DOM particles seek-safe. The driver's `ease: "none"` is load-bearing: the physics lives in the formula; an eased driver warps gravity and the arc stops reading as thrown objects. + +3. **Death** — an opacity tail inside the same formula (fade over the last `FADE_FRAC` of flight), or the confetti signature: a separate **instant-shrink** tween scaling the pool to 0 in a blink at flight end. Either way the particles end invisible and stay invisible. + +## Recipe + +```html + +
+
+
{heroWord}
+
+``` + +```css +/* .burst-stage: position: relative; display: grid; place-items: center. + .burst-hero: z-index: 2 — particles fly BEHIND the word. */ +.particle-field { + position: absolute; + z-index: 1; + left: 50%; + top: 50%; /* the launch origin — offset to taste (e.g. the word's baseline) */ + width: 0; + height: 0; +} +.particle { + position: absolute; + left: 0; + top: 0; + border-radius: 2px; /* confetti chip; 50% for dots */ + opacity: 0; /* invisible until the event fires */ + will-change: transform, opacity; +} +``` + +```js +// Setup: deterministic pool, generated ONCE. +const field = document.getElementById("particle-field"); +const palette = ["{accentA}", "{accentB}", "{accentC}"]; // 3-5 brand tokens +const parts = []; +for (let i = 0; i < PARTICLE_COUNT; i++) { + const el = document.createElement("div"); + el.className = "particle"; + const size = SIZE_MIN + prand(i * 3 + 1) * (SIZE_MAX - SIZE_MIN); + el.style.width = `${size}px`; + el.style.height = `${size * 0.7}px`; // slightly oblong = confetti chip + el.style.background = palette[i % palette.length]; + field.appendChild(el); + // Index-seeded launch parameters — the particle's whole life, fixed here. + const angle = -Math.PI / 2 + (prand(i * 5 + 2) * 2 - 1) * CONE; // upward cone + const speed = SPEED_MIN + prand(i * 7 + 3) * (SPEED_MAX - SPEED_MIN); + parts.push({ + el, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, // negative = up + spin: (prand(i * 11 + 4) * 2 - 1) * SPIN_MAX, + }); +} + +// Confetti pop — one driver, pure ballistic formula. +const drive = { T: 0 }; +tl.fromTo( + drive, + { T: 0 }, + { + T: 1, + duration: FLIGHT_DUR, + ease: "none", // physics lives in the formula, not the ease + onUpdate: () => { + const t = drive.T * FLIGHT_DUR; // seconds of flight — pure function of T + const fade = Math.min(1, (1 - drive.T) / FADE_FRAC); // opacity tail + parts.forEach((p) => { + const x = p.vx * t; + const y = p.vy * t + 0.5 * G * t * t; // rise, stall, drift down + p.el.style.transform = `translate(${x}px, ${y}px) rotate(${p.spin * t}deg)`; + p.el.style.opacity = String(drive.T === 0 ? 0 : fade); // T===0 guard covers seeks before the event + }); + }, + }, + BURST_AT, +); +``` + +## Variations + +- **Confetti pop, then instant-shrink** — the playful signature: full burst, gravity drift, then every chip scales to 0 in a blink: `FADE_FRAC` near 0, plus `tl.to(".particle", { scale: 0, duration: SHRINK_DUR, ease: "power2.in" }, BURST_AT + FLIGHT_DUR - SHRINK_DUR)` with `SHRINK_DUR` 0.15–0.25s. Keep the whole event tiny relative to the subject — a garnish measured in a few dozen pixels, not a screen-filling cannon. +- **Dot burst behind a landing word** — radial instead of a cone: `angle = prand(i) * Math.PI * 2`, `G` near 0, short flight (0.4–0.7s), round dots (`border-radius: 50%`), pool z-indexed behind the word. Fire at the word's settle frame. +- **Glyph dissolve** — seed each particle's **origin** across the glyph block's box (`ox = (prand(i*13) - 0.5) * BLOCK_W`, same for `oy`, added inside the transform), gentle outward drift with low `G`; text fades out over the first ~30% of flight while particles fade in from its silhouette. Color every particle `{textColor}` so the swarm reads as the text's own material. (True per-pixel dissolves are Canvas-2D territory — `techniques.md`; this DOM version sells it up to ~40 particles.) +- **Two-stage burst (pop + stragglers)** — split the pool: 70% on the main driver, 30% on a second driver ~0.12s later with lower speeds; the split is index-derived (`i % 10 < 3`). Same formula, two windows. + +## Values + +| token | range | notes | +| --------------------- | -------------------------------------------- | ------------------------------------------------------------------------------- | --- | ----------------------- | +| PARTICLE_COUNT | 10–18 pop/dots; 24–40 dissolve | **cap ~40** — per-frame style writes; past that, seek perf and register degrade | +| G | 900–1600 px/s² confetti; 0–200 dots/dissolve | natural fall vs drift | +| SPEED_MIN / SPEED_MAX | 250–700 px/s | per-particle via `prand`, never uniform | +| CONE | 0.35–0.8 rad (~20–45°) | wider = splash, narrower = fountain | +| FLIGHT_DUR | 0.7–1.4s | arc should peak ~35–45% of flight: check ` | vy | / G ≈ 0.4 × FLIGHT_DUR` | +| SIZE_MIN / SIZE_MAX | 5–14px chips; 4–8px dots | on a 1080p frame | +| SPIN_MAX | 180–720 deg/s confetti; 0 dots | tumble | +| FADE_FRAC | 0.2–0.35 | near 0 when using instant-shrink | +| BURST_AT | on a cause | the word's settle, a click, a lockup completing — an uncaused burst is noise | + +## Critical Constraints + +- **Position is a pure function of time, driver ease `"none"`** — `x(T)`, `y(T)`, `rot(T)` computed from the driver value every frame, never accumulated (`+=`) per tick (accumulation breaks the moment the renderer seeks); gravity is the ease — an eased driver bends the parabola. +- **Fixed pool, no per-frame DOM** — all particles exist after setup with `opacity: 0`; the event only writes `transform` / `opacity`. **`PARTICLE_COUNT ≤ ~40`** — per-frame style writes scale linearly; keep the event cheap. +- **Particles start AND end at `opacity: 0`** — the `drive.T === 0` guard covers seeks to before the event; the tail/shrink covers after. A chip frozen mid-air at driver end is a bug every subsequent frame. +- **Particles are punctuation** — one event per beat, fired on a cause, small relative to the subject, dead before the next beat; z-ordered behind or around the word it celebrates, never over it. A persistent particle system is a background, and that's not this rule. + +## See also + +`spring-pop-entrance` (confetti fires on the hero's settle frame) · `kinetic-beat-slam` (one beat earns the confetti payoff) · `press-release-spring` (single-layer glow alternative, or compose both) · `css-marker-patterns` (drawn-line burst when the accent should feel hand-annotated) · `scale-swap-transition` (glyph dissolve covers the exit). diff --git a/skills/hyperframes-animation/rules/physics-press-reaction.md b/skills/hyperframes-animation/rules/physics-press-reaction.md index 8eb79e9b6..9fec01708 100644 --- a/skills/hyperframes-animation/rules/physics-press-reaction.md +++ b/skills/hyperframes-animation/rules/physics-press-reaction.md @@ -7,344 +7,95 @@ metadata: # Physics Press Reaction (Cursor + Element Synced) -Models a real click: a cursor approaches a button, lands, and both compress IN SYNC, then release together. Two distinct timing events (down-frame and up-frame) bound by spring forces. Distinct from [press-release-spring](press-release-spring.md) (which has no cursor — just a press happening); this rule is the COMBINED cursor + element behavior. +Models a real click: a cursor approaches a button, lands, and both compress IN SYNC, then release together. Distinct from [press-release-spring.md](press-release-spring.md) (no cursor — just a press happening); this rule is the COMBINED cursor + element behavior. A single `PRESS_INTENSITY` drives both: press down compresses both to `1 - PRESS_INTENSITY` via **one targets array**, release springs both back to 1.0 with overshoot. The cursor translates to the button's center BEFORE the press starts; after release it may move on or hold. -## How It Works - -A single `PRESS_INTENSITY` value drives both cursor and button together: - -- **press down**: both compress to `1 - PRESS_INTENSITY` -- **release**: both spring back to 1.0 with overshoot - -The cursor ALSO translates to the button's center during the approach phase BEFORE press starts. After release, the cursor may move on (next interaction) or hold. - -## HTML +## Recipe ```html -
-
- -
{Brand}
-
- - - - -
+ + + ``` -## CSS +```js +gsap.set("#cursor", { x: CURSOR_START_X, y: CURSOR_START_Y }); // off-screen / far corner -```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {sceneBg}; - font-family: {font}; - overflow: hidden; -} -.stack { - display: flex; - flex-direction: column; - align-items: center; - gap: STACK_GAP; -} -.btn { - display: flex; - align-items: center; - gap: BTN_INNER_GAP; - padding: BTN_PADDING_V BTN_PADDING_H; - background: {btnBg}; - border: none; - border-radius: BTN_RADIUS; - color: {btnTextColor}; - font-family: {font}; - font-weight: 900; - font-size: BTN_FONT_SIZE; - letter-spacing: BTN_TRACKING; - text-transform: uppercase; - cursor: pointer; - box-shadow: {btnRestingShadow}; - transform-origin: 50% 50%; - will-change: transform; -} -.btn-icon { - font-size: BTN_ICON_SIZE; - line-height: 1; -} -.brand { - font-size: BRAND_SIZE; - font-weight: 800; - letter-spacing: BRAND_TRACKING; - color: {brandColor}; - text-transform: uppercase; -} -/* Cursor — absolute, positioned by GSAP */ -.cursor { - position: absolute; - width: CURSOR_SIZE; - height: CURSOR_SIZE; - pointer-events: none; - z-index: 100; - /* initial position is set by gsap.set() */ - transform-origin: 0 0; /* arrow point is the click point */ - filter: {cursorDropShadow}; -} -``` +// Phase 1 — approach +tl.to( + "#cursor", + { x: BUTTON_CENTER_X, y: BUTTON_CENTER_Y, duration: APPROACH_DUR, ease: "power2.inOut" }, + APPROACH_START, +); -## GSAP Timeline +// Phase 2 — coordinated press down: ONE targets array, same scale +tl.to( + ["#btn", "#cursor"], + { scale: 1 - PRESS_INTENSITY, duration: PRESS_DOWN_DUR, ease: "power1.in" }, + PRESS_DOWN_AT, +); -```html - - +// Cursor optionally exits after the press settles +tl.to( + "#cursor", + { x: CURSOR_EXIT_X, y: CURSOR_EXIT_Y, duration: CURSOR_EXIT_DUR, ease: "power2.out" }, + CURSOR_EXIT_AT, +); ``` ## Variations -### Multiple-element chain press +- **Multiple-element chain press** — press button A → A triggers a swap → cursor moves to button B → presses again; each press is one full down-release sub-routine. +- **Hold press (continuous pressure)** — insert a `HOLD_DUR` window between press-down and release: both scales stay at `1 - PRESS_INTENSITY`, inner glow stays on. Suggests "thinking" or "loading." +- **Synchronized inner-glow pulse** — during the hold, pulse the inset glow with a sine driver: a `{ p: 0 }` proxy tweened to `Math.PI * GLOW_PULSE_CYCLES * 2` on `ease: "none"`, `onUpdate` writing `boxShadow` with `alpha = GLOW_BASE_ALPHA + sin(p) * GLOW_PULSE_AMP`. Suggests "processing." -Cursor presses button A → button A triggers swap → cursor moves to button B → presses again. Each press is one full down-release sub-routine. +## Values -### Hold press (continuous pressure) - -Insert a `HOLD_DUR` window between press-down and release. Cursor scale stays at `1 - PRESS_INTENSITY`, button scale stays at `1 - PRESS_INTENSITY`, inner glow stays on. Suggests "thinking" or "loading." - -### Synchronized inner-glow pulse - -During the hold phase, the inner glow pulses (sin-driven). Suggests "processing": - -```js -const holdGlow = { p: 0 }; -tl.to( - holdGlow, - { - p: Math.PI * GLOW_PULSE_CYCLES * 2, - duration: HOLD_DUR, - ease: "none", - onUpdate: () => { - const alpha = GLOW_BASE_ALPHA + Math.sin(holdGlow.p) * GLOW_PULSE_AMP; - document.getElementById("btn").style.boxShadow = - `inset 0 0 GLOW_BLUR rgba(255, 255, 255, ${alpha})`; - }, - }, - HOLD_START_AT, -); -``` - -## How to Choose Values - -### Timing (seconds) - -- **APPROACH_START** — when the cursor begins moving toward the button. - - Range: 0-0.3 s (small lead-in is fine; long delays read as a dead frame) -- **APPROACH_DUR** — cursor approach duration. - - Range: 0.7-1.3 s; faster reads as urgent, slower as deliberate -- **PRESS_DOWN_AT** — when the press fires. - - Constraints: MUST equal `APPROACH_START + APPROACH_DUR` so the cursor arrives exactly when the press begins (avoids "tapping on air") -- **PRESS_DOWN_DUR** — compression duration. - - Range: 0.1-0.25 s -- **RELEASE_AT** — when the release fires. - - Constraints: must be > `PRESS_DOWN_AT + PRESS_DOWN_DUR`; an optional brief hold (0.05-0.4 s, or `HOLD_DUR` for the Hold-press variation) for "thinking" interactions -- **RELEASE_DUR** — release spring duration. - - Range: 0.4-0.7 s (long enough for the overshoot to settle) -- **BRAND_REVEAL_AT** — when the brand line fades in. - - Constraints: must be < `PRESS_DOWN_AT` (context precedes interaction) -- **BRAND_REVEAL_DUR** — brand fade-in duration. - - Range: 0.4-0.8 s -- **CURSOR_EXIT_AT / CURSOR_EXIT_DUR** — optional outbound cursor motion after release. - - Constraints: `CURSOR_EXIT_AT` must be ≥ `RELEASE_AT + RELEASE_DUR` so the cursor exits AFTER the press settles, not during - -### Physics - -- **PRESS_INTENSITY** — how deep the press compression goes. - - Range: 0.05 (subtle) - 0.10 (standard) - 0.15 (heavy) - - Applied as `scale: 1 - PRESS_INTENSITY` on both cursor and button (single GSAP target array) -- **BOUNCE_FACTOR** — `back.out(${BOUNCE_FACTOR})` overshoot on the release. - - Range: 1.6 (soft) - 2.0 (firm) - 2.4 (cartoony) - -### Positioning - -- **CURSOR_START_X / CURSOR_START_Y** — initial cursor position in composition coordinates. - - Constraints: off-screen or in a corner far from the button so the approach reads as motion-in, not a teleport -- **BUTTON_CENTER_X / BUTTON_CENTER_Y** — the button's measured screen-space center. - - Source: measured at composition coordinates; for `place-items: center` at 1920×1080 this is `(960, 540)` -- **CURSOR_EXIT_X / CURSOR_EXIT_Y** — where the cursor moves after release (if used). - - Range: any off-stage or out-of-the-way position -- **BRAND_REVEAL_Y_PX** — brand initial y offset. - - Range: 8-20 px - -### Layout / typography - -- **STACK_GAP** — gap between button and brand line. - - Range: 40-96 px -- **BTN_PADDING_V / BTN_PADDING_H** — button padding. - - Range: V 24-40 px, H 60-100 px (horizontal padding 2-3× vertical reads as pill-shaped CTA) -- **BTN_INNER_GAP** — gap between icon and label inside the button. - - Range: 16-32 px -- **BTN_RADIUS** — button corner radius. - - Range: 20-40 px, or `BTN_PADDING_V + BTN_FONT_SIZE/2` for fully rounded ends -- **BTN_FONT_SIZE / BTN_ICON_SIZE** — typographic sizes inside the button. - - Range: font 60-100 px at 1080p; icon ~1.0-1.1× font size -- **BTN_TRACKING** — letter-spacing on uppercase button text. - - Range: 4-12 px -- **BRAND_SIZE / BRAND_TRACKING** — brand line typography. - - Range: 40-60 px, tracking 8-16 px -- **CURSOR_SIZE** — cursor SVG size. - - Range: 48-96 px at 1080p - -### Hold-press variation - -- **HOLD_DUR** — hold window between press down and release. - - Range: 0.3-0.8 s -- **HOLD_START_AT** — when the glow pulse begins. - - Constraints: typically equal to `PRESS_DOWN_AT + PRESS_DOWN_DUR` -- **GLOW_PULSE_CYCLES** — number of full sine cycles across `HOLD_DUR`. - - Range: 1-4 (more cycles read as faster "processing") -- **GLOW_BASE_ALPHA** — center of the alpha pulse. - - Range: 0.15-0.3 -- **GLOW_PULSE_AMP** — peak deviation from `GLOW_BASE_ALPHA`. - - Range: 0.1-0.2; must satisfy `GLOW_BASE_ALPHA - GLOW_PULSE_AMP ≥ 0` -- **GLOW_BLUR** — inset glow blur radius (px). - - Range: 24-48 px - -### Tokens - -- **{sceneBg}** — background gradient/color -- **{font}** — typographic stack -- **{btnBg}** — button background (typically gradient toward an accent hue) -- **{btnTextColor}** — button text color -- **{btnRestingShadow}** / **{btnPressedShadow}** — outer + inset box-shadow strings for the resting and pressed states -- **{brandColor}** — accent brand color -- **{cursorFill}** / **{cursorStroke}** — cursor SVG fill and stroke -- **{cursorDropShadow}** — `filter: drop-shadow(...)` value for cursor depth -- **{Brand}** — brand line copy -- **{ctaCopy}** / **{ctaIcon}** — button label and inline icon glyph - -## Key Principles - -- **Same press scale on cursor AND button** — physical synchronicity. If only the button scales, the cursor appears to "tap on air"; if only the cursor scales, the button feels disconnected. -- **Cursor arrives BEFORE press starts** — there must be a clear moment of "cursor over target" before scale change. Otherwise the press is unattributed. -- **`back.out(${BOUNCE_FACTOR})` for release** — both elements need spring overshoot together. Linear release loses the tactile feel. -- **Inner glow appears DURING press, fades on release** — visual confirmation of contact. Outer shadow shrinks (pushed-in), inner glow appears (energy concentrated). -- **Cursor `pointer-events: none`** — the cursor is decorative; if it captures events, hover/click behaviors on button below break. -- **Cursor `transform-origin: 0 0`** — the arrow's tip is the click point, not its center. Scale around the tip keeps the click point stable. -- **Climax dwell ≥1 s** — after release, the comp must continue ≥1 s. The press is a beat; viewer needs time to see the result. +| token | range / rule | notes | +| ------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------- | +| APPROACH_START | 0–0.3 s | long delays read as a dead frame | +| APPROACH_DUR | 0.7–1.3 s | faster = urgent, slower = deliberate | +| PRESS_DOWN_AT | `= APPROACH_START + APPROACH_DUR` | cursor arrives exactly as the press begins — avoids "tapping on air" | +| PRESS_DOWN_DUR | 0.1–0.25 s | | +| RELEASE_AT | > `PRESS_DOWN_AT + PRESS_DOWN_DUR` | optional 0.05–0.4 s hold (or `HOLD_DUR` 0.3–0.8 s) for "thinking" interactions | +| RELEASE_DUR | 0.4–0.7 s | long enough for the overshoot to settle | +| PRESS_INTENSITY | 0.05 subtle · 0.10 standard · 0.15 heavy | applied to both cursor and button via the single targets array | +| BOUNCE_FACTOR | 1.6 soft · 2.0 firm · 2.4 cartoony | | +| CURSOR_START / EXIT | off-screen or far corner | the approach must read as motion-in, not a teleport; exit ≥ `RELEASE_AT + RELEASE_DUR` | +| BUTTON_CENTER | measured | for `place-items: center` at 1920×1080: `(960, 540)` | +| BRAND_REVEAL_AT | < `PRESS_DOWN_AT` | context precedes interaction | +| glow pulse | 1–4 cycles; base α 0.15–0.3; amp 0.1–0.2 | `GLOW_BASE_ALPHA − GLOW_PULSE_AMP ≥ 0` | +| CURSOR_SIZE | 48–96 px at 1080p | | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition`** on either cursor or button — competes with GSAP -- **Cursor SVG with `pointer-events: none`** -- **`will-change: transform`** on button (and cursor if desired) -- **`up-frame > down-frame`** — release MUST come after press; otherwise the comp shows release without press -- **Don't use real `mouseenter` / `click` events** — HF is a render context, not a UI; everything must run via the timeline +- **Same press scale on cursor AND button** (one targets array) — only the button scaling makes the cursor "tap on air"; only the cursor scaling makes the button feel disconnected. +- **Cursor arrives BEFORE the press starts** — a clear "cursor over target" moment, or the press is unattributed. +- **`back.out(BOUNCE_FACTOR)` on the release, for both together** — a linear release loses the tactile feel; release MUST come after press. +- **Inner glow appears DURING press, fades on release** — outer shadow shrinks (pushed in), inner glow appears (energy concentrated). +- **Cursor `transform-origin: 0 0`** — the arrow's tip is the click point; scale around the tip keeps it stable. `pointer-events: none` on the cursor. +- **Climax dwell ≥ 1 s** — after release the composition must continue ≥ 1 s; the press is a beat, the viewer needs time to see the result. +- **No real `mouseenter` / `click` events** — HF is a render context; everything runs via the timeline. -## Combinations +## See also -- [press-release-spring.md](press-release-spring.md) — the BUTTON-only press variant; this rule layers cursor on top -- [cursor-click-ripple.md](cursor-click-ripple.md) — adds a ripple effect at the click point -- [scale-swap-transition.md](scale-swap-transition.md) — the press TRIGGERS the swap - -## Pairs with HF skills - -- `/hyperframes-animation` — coordinated multi-target tweens via array -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`press-release-spring` (the BUTTON-only press; this rule layers the cursor on top) · `cursor-click-ripple` (adds a ripple at the click point) · `scale-swap-transition` (the press TRIGGERS the swap). diff --git a/skills/hyperframes-animation/rules/press-release-spring.md b/skills/hyperframes-animation/rules/press-release-spring.md index 4e6fb0a5a..3c6eac967 100644 --- a/skills/hyperframes-animation/rules/press-release-spring.md +++ b/skills/hyperframes-animation/rules/press-release-spring.md @@ -7,290 +7,102 @@ metadata: # Press-Release Spring Chain -Separates input (linear compression) from output (spring recovery) to create tactile feel. The overshoot is a natural byproduct of the spring config, not manually coded. Pairs with secondary motion (shadow shrink, release burst, background glow) layered on the same trigger frame. +Separates input (linear compression) from output (spring recovery) to create tactile feel: the overshoot is a natural byproduct of the spring config, not manually coded, with secondary motion (shadow shrink, release burst, background glow) layered on the same trigger frame. This is a **reaction on an element already resting on screen** — an arrival that springs in from nothing is [spring-pop-entrance.md](spring-pop-entrance.md); add a visible cursor actor and it becomes [physics-press-reaction.md](physics-press-reaction.md). -## How It Works - -Two distinct phases split at the **release** moment: +Two phases split at the **release**: 1. **Press**: linear ease → compression (`scale: 1 → PRESS_SCALE`, shadow shrinks). Linear, not spring — the dip must read as instant/tactile, not squishy. -2. **Release**: `back.out(${BOUNCE_FACTOR})` spring → elastic pop back to `1.0` (overshoot proportional to `BOUNCE_FACTOR`). Optional burst glow ring expands behind the button; optional background environmental glow fades in. +2. **Release**: `back.out(BOUNCE_FACTOR)` spring back to 1.0. Optional burst glow ring expands behind the button; optional environmental glow fades in. -State continuity is critical: the release tween's start value MUST equal the press tween's end value, or the spring snaps to a different position. GSAP threads this automatically when both tweens target the same property at adjacent positions on the same timeline. +State continuity is critical: the release tween's start value MUST equal the press tween's end value, or the spring snaps to a different position. GSAP threads this automatically when both tweens target the same property at **adjacent positions** — `RELEASE_START = PRESS_START + PRESS_DUR`; a gap or overlap breaks it. -## HTML +## Recipe ```html -
-
-
-
- -
+
+
+ +
+
``` -## CSS - -```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; -} -.press-stage { - position: relative; - display: grid; - place-items: center; -} -.btn { - position: relative; - z-index: 2; - /* Visual weight: ≥4% of canvas for the press to read on a 1080p frame */ - width: BTN_WIDTH; - height: BTN_HEIGHT; - background: {btnBg}; - border: none; - border-radius: BTN_RADIUS; - font-family: {font}; - font-weight: 900; - font-size: BTN_FONT_SIZE; - letter-spacing: BTN_LETTER_SPACING; - color: {btnTextColor}; - text-transform: uppercase; - /* Anchor compression on the center — see Critical Constraints */ - transform-origin: 50% 50%; - /* Initial floating shadow — large + diffuse */ - box-shadow: {btnRestShadow}; -} -.burst { - /* Sits BEHIND the button, same footprint */ - position: absolute; - z-index: 1; - inset: 0; - width: BTN_WIDTH; - height: BTN_HEIGHT; - background: {burstGradient}; - filter: blur(BURST_BLUR); - opacity: 0; - transform: scale(1); - pointer-events: none; -} -.bg-glow { - /* Full-stage radial — extends beyond the stage with negative inset */ - position: absolute; - inset: BG_GLOW_INSET; - background: {bgGlowGradient}; - opacity: 0; - pointer-events: none; -} -``` - -## GSAP Timeline - -```html - - -``` - -## Variations - -### Subtle press (status save / muted CTA) - -Less compression, gentler overshoot, smaller burst. `PRESS_SCALE` toward the high end of its range (~0.96), `BOUNCE_FACTOR` toward the low end (~1.4), `BURST_PEAK_SCALE` and `BURST_PEAK_OPACITY` reduced. - -### Dramatic press (hero CTA / "ship it" moment) - -Deeper compression, more overshoot, larger burst. `PRESS_SCALE` toward the low end (~0.88), `BOUNCE_FACTOR` toward the high end (~2.5), `BURST_PEAK_SCALE` and `BURST_PEAK_OPACITY` maxed. - -### Color shift during press - -Darken the button mid-press, return on release. Same timeline positions as the scale tweens — interpolated `backgroundColor` on `#btn`. State continuity rule still applies: the release-color tween's start equals the press-color tween's end. - ```js -tl.to("#btn", { backgroundColor: "{btnPressedColor}", duration: PRESS_DUR }, PRESS_START); -tl.to("#btn", { backgroundColor: "{btnRestColor}", duration: RELEASE_DUR }, RELEASE_START); -``` - -### State change at release (approve / confirm pattern) - -When the press signals confirmation, swap the button's resting color to a success token at `RELEASE_START` (instead of returning to `{btnRestColor}`), then pop a checkmark via a separate `back.out(${CHECK_BOUNCE})` tween at the same position. The button is now in its terminal state — no further presses expected. - -```js -tl.to("#btn", { backgroundColor: "{successColor}", duration: RELEASE_DUR }, RELEASE_START); +// Phase 1 — press (linear compression) tl.to( - ".btn-check", - { scale: 1, duration: CHECK_POP_DUR, ease: `back.out(${CHECK_BOUNCE})` }, + "#btn", + { scale: PRESS_SCALE, boxShadow: "{btnPressedShadow}", duration: PRESS_DUR, ease: "power1.in" }, + PRESS_START, +); + +// Phase 2 — release (spring back; start scale == PRESS_SCALE by adjacency) +tl.to( + "#btn", + { + scale: 1, + boxShadow: "{btnRestShadow}", + duration: RELEASE_DUR, + ease: `back.out(${BOUNCE_FACTOR})`, + }, + RELEASE_START, +); + +// Phase 3 — burst glow pops behind the button, then fades +tl.fromTo( + "#burst", + { scale: 1, opacity: 0 }, + { + scale: BURST_PEAK_SCALE, + opacity: BURST_PEAK_OPACITY, + duration: BURST_GROW_DUR, + ease: "power2.out", + }, + RELEASE_START, +); +tl.to("#burst", { opacity: 0, duration: BURST_FADE_DUR, ease: "power2.in" }, BURST_FADE_START); + +// Phase 4 — environmental glow fades in after release +tl.to( + "#bg-glow", + { opacity: BG_GLOW_PEAK_OPACITY, duration: BG_GLOW_FADE_DUR, ease: "power2.out" }, RELEASE_START, ); ``` -## How to Choose Values +## Variations -### Geometry +- **Subtle press** (status save / muted CTA): `PRESS_SCALE` ~0.96, `BOUNCE_FACTOR` ~1.4, burst scale/opacity reduced. +- **Dramatic press** (hero CTA / "ship it"): `PRESS_SCALE` ~0.88, `BOUNCE_FACTOR` ~2.5, burst maxed. +- **Color shift during press** — darken mid-press, return on release; interpolated `backgroundColor` at the same timeline positions as the scale tweens. Same state-continuity rule. +- **State change at release** (approve / confirm) — instead of returning to the rest color, swap to `{successColor}` at `RELEASE_START` and pop a checkmark via a separate `back.out(CHECK_BOUNCE)` tween (1.4–2.0, firmer than the button's bounce — a punctuating "stamp"; pop 0.3–0.6 s) at the same position. The button is now terminal — no further presses expected. -- **BTN_WIDTH / BTN_HEIGHT** — button footprint. - - Range: button area ≥ 3-5% of canvas (a 320×68 button at 1080p is ~1% and reads as visually insignificant) - - Effects: smaller → press barely reads; larger → press dominates the frame - - Constraints: `BTN_WIDTH × BTN_HEIGHT / (canvasW × canvasH) ≥ 0.03` -- **BTN_RADIUS** — corner radius. - - Range: `BTN_HEIGHT × 0.15` (sharp/modern) → `BTN_HEIGHT / 2` (pill) -- **BTN_FONT_SIZE / BTN_LETTER_SPACING** — typographic weight. - - Range: `BTN_FONT_SIZE ≈ BTN_HEIGHT × 0.4-0.5`; letter-spacing 4-10 px reads as "actionable label" +## Values -### Press dynamics +| token | range | notes | +| -------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ | +| button footprint | ≥ 3–5% of canvas area | a 320×68 button at 1080p is ~1% and the press reads as visually insignificant | +| PRESS_SCALE | 0.88 dramatic · 0.92 default · 0.96 subtle | never <0.85 (broken) or >0.98 (no perceptible dip) | +| PRESS_DUR | 0.10–0.30 s | shorter = snappier; must be shorter than `RELEASE_DUR` (input faster than spring recovery) | +| RELEASE_DUR | 0.40–0.90 s | shorter = tight pop; longer = loose, wobbly settle | +| BOUNCE_FACTOR | 1.4 soft · 2.0 firm · 2.8 cartoony | or `elastic.out(amplitude, period)` for a rubbery oscillation instead of one overshoot | +| RELEASE_START | `= PRESS_START + PRESS_DUR` | adjacency = automatic state continuity | +| BURST_PEAK_SCALE | 3 subtle · 6 default · 8 max | beyond ~8 the radial gradient pixelates visibly | +| BURST_PEAK_OPACITY | 0.4–1.0 | grow ≈ fade, 0.4–0.7 s each; blur 40–100 px (hard ring → ambient haze) | +| BG_GLOW_PEAK_OPACITY | 0.1 subtle · 0.25 default · 0.45 max | higher washes the whole composition; fade-in 0.6–1.0 s; inset −300…−500 px at 1080p | -- **PRESS_SCALE** — compression depth. - - Range: 0.88 (dramatic) → 0.92 (default) → 0.96 (subtle) - - Effects: lower → more tactile / weightier; higher → barely-there acknowledgment - - Constraints: never <0.85 (button feels broken) or >0.98 (no perceptible dip) -- **PRESS_DUR** — compression duration. - - Range: 0.10-0.30 s - - Effects: shorter → snappier / "instant-feeling"; longer → slow squish - - Constraints: shorter than `RELEASE_DUR` (input is faster than spring recovery) -- **RELEASE_DUR** — spring recovery duration. - - Range: 0.40-0.90 s - - Effects: shorter → tight pop; longer → loose, wobbly settle -- **BOUNCE_FACTOR** — `back.out(BOUNCE_FACTOR)` overshoot strength. - - Range: 1.4 (soft) → 2.0 (firm pop) → 2.8 (cartoony) - - Effects: low end barely overshoots; high end reads as cartoonish; tune by feel - - Alternative: switch to `elastic.out(amplitude, period)` for a rubbery oscillation instead of a single overshoot -- **PRESS_START / RELEASE_START** — timeline positions. - - Constraints: `RELEASE_START = PRESS_START + PRESS_DUR` (state continuity — see Critical Constraints) - -### Burst glow - -- **BURST_PEAK_SCALE** — radial pop max scale. - - Range: 3 (subtle) → 6 (default) → 8 (dramatic) - - Constraints: ≤ ~8 — beyond that the radial gradient pixelates visibly -- **BURST_PEAK_OPACITY** — burst max opacity. - - Range: 0.4 (subtle) → 0.8 (default) → 1.0 (dramatic) -- **BURST_GROW_DUR / BURST_FADE_DUR** — grow vs. fade timing. - - Range: 0.4-0.7 s each; default grow ≈ fade -- **BURST_BLUR** — gaussian blur on the burst layer. - - Range: 40-100 px; smaller reads as a hard ring, larger as ambient haze - -### Background glow - -- **BG_GLOW_PEAK_OPACITY** — peak environmental glow. - - Range: 0.1 (subtle) → 0.25 (default) → 0.45 (dramatic) - - Constraints: ≤ 0.45 — higher washes the whole composition -- **BG_GLOW_FADE_DUR** — fade-in duration. - - Range: 0.6-1.0 s -- **BG_GLOW_INSET** — negative inset so the radial extends past the stage edges. - - Range: typically `-300` to `-500` px on a 1920×1080 canvas - -### Optional "approve" variation - -- **CHECK_BOUNCE** — checkmark pop overshoot. - - Range: 1.4-2.0; firmer than the button's main `BOUNCE_FACTOR` to read as a punctuating "stamp" -- **CHECK_POP_DUR** — checkmark scale-up duration. - - Range: 0.3-0.6 s - -### Tokens - -- **{btnBg} / {btnRestColor} / {btnPressedColor}** — primary button surface; pressed darker than rest -- **{btnRestShadow} / {btnPressedShadow}** — rest shadow is large + diffuse; pressed is small + tight (the button "sinks toward the surface") -- **{burstGradient}** — radial; saturated near center, fading to transparent (color should be darker + more saturated than `{btnBg}` — same-color glow looks washed out) -- **{bgGlowGradient}** — full-stage radial, low-opacity tint of `{btnBg}`'s hue family -- **{successColor}** — confirmation green / brand-success for the approve variation - -## Key Principles - -- **State continuity** — release start value MUST exactly match press end value. With a GSAP timeline, the first tween's end value automatically becomes the second tween's start when they target the same property at adjacent times. -- **Visual weight** — button area should be **≥3-5% of canvas**. Smaller and the press reads as visually insignificant. -- **Linear press, spring release** — the compression is `power1.in/out`, the recovery is `back.out`. Both spring → squishy; both linear → mechanical / no overshoot punch. -- **Anchor compression on center** — `transform-origin: 50% 50%` (default). Otherwise the button collapses asymmetrically. -- **Burst behind, not in front** — burst `z-index: 1`, button `z-index: 2`. If burst sits in front, it occludes the button at peak opacity. -- **Glow color darker + more saturated than element** — bright surface → dark, saturated glow. Same-color glow looks washed out. -- **Don't tween `boxShadow` and `filter` together on the same element** — they compete in the layout pipeline; pick one. Shadow on the button, blur on a separate burst layer. -- **Climax beats need dwell time** — after the burst peak + label/wordmark reveal, the composition must run for **≥1s more** (≥2s for "dramatic" variants) before ending. A reveal at `t=DURATION−0.2s` reads as "flashed and gone." +Color tokens: pressed surface darker than rest; rest shadow large + diffuse, pressed small + tight (the button "sinks toward the surface"); burst gradient darker + more saturated than `{btnBg}` — same-color glow looks washed out; bg glow a low-opacity tint of the button's hue family. ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition`** on the button — those interpolate independently of HF seek and cause flicker -- **`will-change: transform`** if the button compounds with other animation layers -- **`RELEASE_START = PRESS_START + PRESS_DUR`** — adjacency on the same property is what makes state continuity automatic; gap or overlap breaks it -- **Burst max scale ≤ ~8** — beyond that the radial gradient pixelates visibly -- **Background glow `opacity ≤ 0.45`** — higher and it washes the whole composition -- **GSAP transform aliases only**: `x`, `y`, `scale`, `rotation`. Never tween `width` / `height` / `left` / `top`. +- **State continuity** — release start value exactly equals press end value; enforced by same-property adjacency at `RELEASE_START = PRESS_START + PRESS_DUR`. +- **Linear press, spring release** — both spring → squishy; both linear → mechanical, no overshoot punch. +- **Anchor compression on center** (`transform-origin: 50% 50%`) or the button collapses asymmetrically. +- **Burst behind, not in front** — burst `z-index: 1`, button `z-index: 2`; in front it occludes the button at peak opacity. +- **Don't tween `boxShadow` and `filter` on the same element** — they compete in the layout pipeline; shadow on the button, blur on the separate burst layer. +- **Climax dwell** — after the burst peak + reveal, the composition must run ≥ 1 s more (≥ 2 s for dramatic variants); a reveal at `t = DURATION − 0.2 s` reads as "flashed and gone." -## Combinations +## See also -- [sine-wave-loop.md](sine-wave-loop.md) — idle micro-float on the button BEFORE the press (slight breathing, sells "ready") -- [center-outward-expansion.md](center-outward-expansion.md) — burst of badges outward synced to the press release -- [cursor-click-ripple.md](cursor-click-ripple.md) — cursor click that triggers the press - -## Pairs with HF skills - -- `/hyperframes-animation` — `back.out` ease + multi-tween coordination -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`spring-pop-entrance` (the ENTRANCE counterpart — arrival, not reaction) · `physics-press-reaction` (this press with a visible cursor actor) · `cursor-click-ripple` (the cursor click that triggers the press) · `sine-wave-loop` (idle micro-float BEFORE the press) · `center-outward-expansion` (badge burst synced to the release). diff --git a/skills/hyperframes-animation/rules/reactive-displacement.md b/skills/hyperframes-animation/rules/reactive-displacement.md index d8c0b0b5f..ae4b84f9a 100644 --- a/skills/hyperframes-animation/rules/reactive-displacement.md +++ b/skills/hyperframes-animation/rules/reactive-displacement.md @@ -7,271 +7,86 @@ metadata: # Reactive Displacement -Exit animation of element A is mathematically DERIVED from the entry spring of element B. Creates a causal link: "A moves _because_ B hit it." Distinct from [scale-swap-transition](scale-swap-transition.md) (which overlaps but isn't causal) and [card-morph-anchor](card-morph-anchor.md) (which uses one container morphing dimensions). +Exit animation of element A is mathematically DERIVED from the entry spring of element B — a causal link: "A moves _because_ B hit it." Distinct from [scale-swap-transition.md](scale-swap-transition.md) (which overlaps but isn't causal) and [card-morph-anchor.md](card-morph-anchor.md) (one container morphing). -## How It Works +A single 0→1 driver tween (the "entry spring") feeds three concurrent derived motions in one `onUpdate`: -A single 0→1 driver tween (the "entry spring") feeds two derived motions: +- **Intruder** (B, entering): position interpolated off-stage → settled over the full driver, plus tilt settling to 0° and a sharp early opacity reveal. +- **Victim** (A, exiting): position interpolated settled → off-stage in the OPPOSITE direction, completing at `VICTIM_FRACTION` (~0.4–0.5) of the driver — NOT 1.0. -- **Intruder** (B, entering): position interpolated from off-stage to settled -- **Victim** (A, exiting): position interpolated from settled to off-stage in the OPPOSITE direction, but completing at a fraction `VICTIM_FRACTION` of the driver (not 1.0) +The victim finishing BEFORE the intruder's entry creates the "hit then settle" rhythm; sharing one eased driver makes the impact moment mathematically synchronized. -The fact that the victim's exit finishes BEFORE the intruder's entry creates the "hit then settle" rhythm. Both motions share the same eased driver, so the impact moment is mathematically synchronized. +## Recipe -## HTML +```js +// Both cards absolutely centered; overflow: hidden on the scene (off-stage travel); +// will-change: transform, opacity on both; intruder z-index ABOVE victim. +const INTRUDER_START_X = STAGE_W; // off-stage right +const VICTIM_END_X = -STAGE_W; // off-stage left — SAME axis, opposite direction -```html -
-
-
-
{victimHeadline}
-
{victimSubline}
-
-
-
{intruderHeadline}
-
{intruderSubline}
-
-
-
-``` +gsap.set("#victim", { x: 0, opacity: 1, rotation: 0 }); +gsap.set("#intruder", { x: INTRUDER_START_X, opacity: 0, rotation: -INTRUDER_TILT }); -## CSS +const driver = { p: 0 }; +tl.to( + driver, + { + p: 1, + duration: DRIVER_DUR, + ease: `back.out(${BOUNCE_FACTOR})`, // the intruder spring + onUpdate: () => { + // Intruder: full 0→1 progress maps enter (off-stage → center) + const intruderX = INTRUDER_START_X * (1 - driver.p); + const intruderOpacity = Math.min(1, driver.p * FADE_IN_SHARPNESS); + const intruderRot = -INTRUDER_TILT * (1 - driver.p); // settles to 0° + const intruder = document.getElementById("intruder"); + intruder.style.transform = `translate(-50%, -50%) translateX(${intruderX}px) rotate(${intruderRot}deg)`; + intruder.style.opacity = String(intruderOpacity); -```css -.scene { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - background: radial-gradient(ellipse at center, {bgColor} 0%, {bgColorDeep} 70%); - font-family: {font}; -} -.stage { - position: absolute; - inset: 0; - display: grid; - place-items: center; -} -.card { - position: absolute; - /* both at center; transform translates them */ - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 24px; - padding: 64px 80px; - border-radius: 28px; - will-change: transform, opacity; -} -.victim { - background: linear-gradient(160deg, {victimTint} 0%, {bgColorDeep} 70%); - border: 1px solid {victimTint}; - z-index: 1; -} -.intruder { - background: linear-gradient(160deg, {intruderTint} 0%, {bgColorDeep} 70%); - border: 2px solid {intruderBorder}; - box-shadow: 0 28px 96px {intruderTint}; - z-index: 2; -} -.card-title { - font-size: 200px; - font-weight: 900; - color: {textColor}; - line-height: 1; - letter-spacing: -4px; -} -.card-sub { - font-size: 36px; - font-weight: 800; - letter-spacing: 10px; - text-transform: uppercase; - color: {accentColor}; - text-align: center; -} -``` - -## GSAP Timeline - -```html - - + }, + DRIVER_AT, +); +// Climax dwell — intruder holds centered for ≥ DWELL_MIN before the scene ends. ``` -## How to Choose Values - -- **DRIVER_AT** — when the entry spring begins - - Range: phase-dependent (typically a few seconds in) - - Effects: too early skips setup beats; too late stalls the cut - - Constraints: must allow ≥ DWELL_MIN of climax dwell before composition ends - - Reference: example schedules the displacement after the prior reading beat resolves - -- **DRIVER_DUR** — full intruder entry duration - - Range: 0.6-1.4 s - - Effects: short = zippy/punchy impact; long = heavy/landed impact - - Constraints: tune against `BOUNCE_FACTOR` — higher bounce on long durations reads as floaty - - Reference: see the corresponding blueprint / example - -- **BOUNCE_FACTOR** — `back.out()` coefficient on the intruder spring - - Range: 1.2-2.0 (discrete choice within `back.out` family) - - Effects: low ≈ firm settle; high ≈ overshoot/bounce - - Constraints: ease family stays `back.out` (or upgrade to `elastic.out` if you want oscillation); changing family rewrites the feel - - Reference: examples typically sit between 1.4 and 1.6 - -- **VICTIM_FRACTION** — fraction of `DRIVER_DUR` over which the victim completes its exit - - Range: 0.4-0.5 - - Effects: < 0.4 victim disappears before impact reads; > 0.5 motion feels parallel, not causal - - Constraints: hard upper limit ~0.6; beyond that the collision metaphor breaks - - Reference: this rule's pattern uses ~0.5 - -- **STAGE_W** — stage width in pixels, used to place elements off-stage - - Range: equal to the composition's `data-width` - - Effects: smaller values leave the off-stage element partially visible at start - - Constraints: must be ≥ composition width - - Reference: examples use the project's render width directly - -- **INTRUDER_TILT** — initial rotation (degrees) the intruder rotates from as it settles to 0° - - Range: 5-15° - - Effects: low = clean glide; high = visible "spin-and-plant" - - Constraints: keep sign consistent with entry direction (matches momentum transfer) - - Reference: ~10° is a typical mid-impact tilt - -- **FADE_IN_SHARPNESS** — multiplier controlling how quickly intruder opacity reaches 1 - - Range: 3-8 (intruder reaches opacity 1 at `1/FADE_IN_SHARPNESS` of progress) - - Effects: low = soft fade alongside motion; high = pops in early and reads as solid - - Constraints: > 1; below 1 means intruder is still transparent at center - - Reference: most examples use a sharp early reveal - -- **DWELL_MIN** — minimum climax dwell after the intruder settles - - Range: ≥ 1.0 s - - Effects: shorter feels rushed and unreadable; longer stalls the comp - - Constraints: post-impact dwell is where the new content gets read — do not skip - - Reference: 1.0-1.5 s is typical - ## Variations -### Impact rotation on victim +- **Impact rotation on victim** — the victim also rotates as it slides: `const victimRot = victimP * -VICTIM_KICK_DEG;` appended to its transform. `VICTIM_KICK_DEG` 15–25°, magnitude matched to the perceived intruder weight. +- **Vertical collision** — intruder from top, victim displaced downward; same math on Y. Reads as "weight dropped on it." +- **Wobble after settle** — after the intruder centers, a damped sine wobble (`±WOBBLE_AMP_DEG` rotation, linearly decaying over `WOBBLE_DUR` via a second `ease: "none"` driver at `DRIVER_AT + DRIVER_DUR`) before stillness — "impact aftermath." +- **Multi-victim ripple** — the intruder displaces multiple aligned cards, each victim's `victimP` on a slightly offset driver phase (cascade ripple). -The victim doesn't just slide off — it ALSO rotates from the impact angle: +## Values -```js -const victimRot = victimP * -VICTIM_KICK_DEG; // rotates as it slides -victim.style.transform = `translate(-50%, -50%) translateX(${victimX}px) rotate(${victimRot}deg)`; -``` - -`VICTIM_KICK_DEG` is typically 15-25°; pick magnitude to match the perceived intruder weight. - -### Vertical collision - -Intruder enters from top, victim displaced downward. Same math with Y instead of X. Visual feels like "weight dropped on it." - -### Wobble after settle - -After the intruder centers, a damped sine wobble (`±WOBBLE_AMP_DEG` rotation, decaying over `WOBBLE_DUR`) before stillness. Adds "impact aftermath" before climax dwell. - -```js -const wobble = { p: 0 }; -tl.to( - wobble, - { - p: Math.PI * WOBBLE_CYCLES * 2, - duration: WOBBLE_DUR, - ease: "none", - onUpdate: () => { - const rot = - Math.sin(wobble.p) * WOBBLE_AMP_DEG * (1 - wobble.p / (Math.PI * WOBBLE_CYCLES * 2)); // linear decay - intruder.style.transform = `translate(-50%, -50%) rotate(${rot}deg)`; - }, - }, - DRIVER_AT + DRIVER_DUR, -); -``` - -### Multi-victim ripple - -Intruder displaces multiple aligned cards, each victim getting a slightly delayed exit (cascade ripple). Each victim's `victimP` uses a different driver phase offset. - -## Key Principles - -- **Single driver = single source of truth** — the entry spring drives BOTH motions. Independent tweens for intruder and victim destroy the causal link; they'd just happen to be near each other in time, not collided. -- **Victim completes at a fraction of driver** — by the time the intruder reaches center, the victim is GONE. The "hit" is the moment they overlap; after that the victim is just exiting space the intruder will fill. -- **Directional momentum transfer** — intruder from positive X → victim moves negative X. Same axis. If they move on different axes, it looks like they passed each other, not collided. -- **Intruder z-index ABOVE victim** — during overlap, the intruder should appear in FRONT (it's the "winner" of the collision). Otherwise the victim looks like it tunneled through. -- **Intruder enters with rotation, settles flat** — adds momentum visualization. A small initial tilt → 0° at settle reads as "spinning in then planting." -- **Climax dwell after impact** — the impact is the headline beat. Post-impact dwell is where the new content gets read. +| token | range | notes | +| ----------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- | +| DRIVER_AT | phase-dependent | after the prior reading beat resolves; must leave ≥ DWELL_MIN of climax dwell before the scene ends | +| DRIVER_DUR | 0.6–1.4 s | short = zippy punch, long = heavy landed impact; higher bounce on long durations reads as floaty | +| BOUNCE_FACTOR | 1.2–2.0 (typ. 1.4–1.6) | stay in the `back.out` family (or `elastic.out` for oscillation) — changing family rewrites the feel | +| VICTIM_FRACTION | 0.4–0.5 | <0.4 the victim disappears before the impact reads; >0.5 feels parallel, not causal; hard cap ~0.6 | +| STAGE_W | ≥ composition width | smaller leaves the off-stage element partially visible at start | +| INTRUDER_TILT | 5–15° (typ. ~10°) | low = clean glide, high = "spin-and-plant"; sign consistent with entry direction (momentum transfer) | +| FADE_IN_SHARPNESS | 3–8 | intruder reaches opacity 1 at `1/FADE_IN_SHARPNESS` of progress; must be > 1 or it's transparent at center | +| DWELL_MIN | ≥ 1.0 s (typ. 1.0–1.5) | post-impact dwell is where the new content gets read — do not skip | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Single driver, multiple derived values in same onUpdate** — don't tween intruder and victim with separate `tl.to()` calls; use ONE driver and compute both inside its onUpdate -- **`overflow: hidden` on `.scene`** — off-stage motion exceeds the frame -- **`will-change: transform, opacity`** on both cards -- **Intruder z-index > victim z-index** — explicit, not relying on DOM order alone +- **Single driver = single source of truth** — both motions computed inside ONE driver's `onUpdate`, never separate `tl.to()` calls per element; independent tweens destroy the causal link (they'd merely be near each other in time). +- **Victim completes at a fraction of the driver** — the "hit" is the overlap moment; after it the victim is just vacating space the intruder will fill. +- **Directional momentum transfer** — same axis, opposite directions; different axes read as passing, not colliding. +- **Intruder z-index above victim** — explicit, not DOM order; otherwise the victim looks like it tunneled through. +- **Intruder enters tilted, settles flat** — small initial tilt → 0° reads as "spinning in then planting." +- **Climax dwell after impact** — the impact is the headline beat; hold the settled intruder ≥ DWELL_MIN. +- **`overflow: hidden` on the scene** — off-stage motion exceeds the frame. -## Combinations +## See also -- [hacker-flip-3d.md](hacker-flip-3d.md) — intruder text reveals via hacker-flip during the entry phase -- [sine-wave-loop.md](sine-wave-loop.md) — idle breathing on intruder during climax dwell -- [vertical-spring-ticker.md](vertical-spring-ticker.md) — intruder is a ticker that "shoves" the previous content out - -## Pairs with HF skills - -- `/hyperframes-animation` — single driver, multi-value onUpdate -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`control-target-sync` (the live-editing mirror — repeated coupled edits, nothing exits) · `hacker-flip-3d` (intruder text reveal during entry) · `sine-wave-loop` (idle breathing during the dwell) · `vertical-spring-ticker` (a ticker that "shoves" the previous content out). diff --git a/skills/hyperframes-animation/rules/scale-swap-transition.md b/skills/hyperframes-animation/rules/scale-swap-transition.md index 799e93f35..cc3505435 100644 --- a/skills/hyperframes-animation/rules/scale-swap-transition.md +++ b/skills/hyperframes-animation/rules/scale-swap-transition.md @@ -7,292 +7,82 @@ metadata: # Scale-Swap Transition -Simulates a "morph" between two DOM elements by overlapping exit and entrance scale animations. Lighter weight than [card-morph-anchor](card-morph-anchor.md) (which morphs container dimensions) and easier than SVG path interpolation. +Simulates a "morph" between two DOM elements by overlapping exit and entrance scale animations. Lighter weight than [card-morph-anchor.md](card-morph-anchor.md) (which morphs container dimensions — use that for SHAPE changes; this rule is for SAME-shape state swaps) and easier than SVG path interpolation. -## How It Works +At a single trigger, two coordinated tweens fire: -At a single trigger time, two coordinated tweens fire: +1. **Outgoing**: scale `1.0 → EXIT_SCALE` + opacity `1 → 0`, fast `power2.in` (rushing away). +2. **Incoming**: scale `EXIT_SCALE → 1.0` + opacity `0 → 1`, `back.out(BOUNCE_FACTOR)` (arriving with weight). -1. **Outgoing element**: scale `1.0 → EXIT_SCALE` + opacity `1 → 0` (fast `power2.in`) -2. **Incoming element**: scale `EXIT_SCALE → 1.0` + opacity `0 → 1` (bouncy `back.out(${BOUNCE_FACTOR})` with overshoot) +A small `OVERLAP` window during which both are mid-tween creates the morph illusion; the incoming sits on top via z-index so the outgoing's fade-tail doesn't bleed through. -A small `OVERLAP` window during which both are mid-tween creates the "morph" illusion. Incoming sits on top via z-index so the outgoing's fade-tail doesn't bleed through. - -## HTML +## Recipe ```html -
-
-
-
-
{outgoingIcon}
-
{outgoingLabel}
-
-
-
{incomingIcon}
-
{incomingLabel}
-
{incomingSubline}
-
-
-
{Brand}
+ +
+
{outgoingIcon} {outgoingLabel}
+
+ {incomingIcon} {incomingLabel} +
{incomingSubline}
``` -## CSS +```js +// Outgoing: shrink + fade fast +tl.to( + "#outgoing", + { scale: EXIT_SCALE, opacity: 0, duration: EXIT_DUR, ease: "power2.in" }, + TRIGGER, +); -```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {sceneBg}; - font-family: {font}; -} -.stack { - display: flex; - flex-direction: column; - align-items: center; - gap: STACK_GAP; -} -.swap-wrap { - position: relative; - width: SWAP_WRAP_W; - height: SWAP_WRAP_H; -} -.card { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: CARD_INNER_GAP; - border-radius: CARD_RADIUS; - padding: CARD_PADDING; - /* Both elements share transform-origin so they "morph" around the same anchor */ - transform-origin: 50% 50%; - will-change: transform, opacity; -} -.card .icon { - font-size: ICON_SIZE; -} -.card .title { - font-size: TITLE_SIZE; - font-weight: 900; - letter-spacing: TITLE_TRACKING; - text-transform: uppercase; -} -.card .sub { - font-size: SUB_SIZE; - font-weight: 700; - color: {accentColor}; - opacity: 0; -} -.outgoing { - z-index: 1; - background: {outgoingBg}; - border: 1px solid {outgoingBorder}; - color: {textColor}; -} -.incoming { - /* Incoming starts hidden + smaller, will pop in */ - z-index: 2; - background: {incomingBg}; - border: 1px solid {incomingBorder}; - color: {textColor}; - opacity: 0; - transform: scale(EXIT_SCALE); -} -.brand { - font-size: BRAND_SIZE; - font-weight: 900; - letter-spacing: BRAND_TRACKING; - text-transform: uppercase; - color: {brandColor}; -} -``` +// Incoming: pops in with overshoot, starting OVERLAP before the exit finishes +tl.to( + "#incoming", + { scale: 1.0, opacity: 1, duration: ENTER_DUR, ease: `back.out(${BOUNCE_FACTOR})` }, + TRIGGER + EXIT_DUR - OVERLAP, +); -## GSAP Timeline - -```html - - +// Inner content reveals AFTER the incoming settles +tl.fromTo( + "#sub", + { opacity: 0, y: SUB_REVEAL_Y_PX }, + { opacity: 1, y: 0, duration: SUB_REVEAL_DUR, ease: "power3.out" }, + TRIGGER + EXIT_DUR + SUB_REVEAL_DELAY, +); ``` ## Variations -### Delayed inner content reveal +- **Delayed inner content reveal** — the classic pattern above: morph the container, then reveal inner text once it settles; the 0.2–0.4 s gap lets the eye land on the new shape before reading. +- **Triple swap (3-state cycle)** — chain A→B→C with triggers `TRIGGER_AB` / `TRIGGER_BC`; each transition is its own tween pair, the previous incoming becoming the next outgoing. State-evolution narratives (early → mid → final labels). +- **Color-shift transition (no scale)** — for a flat morph between same-shape states, drop the scale and keep opacity + a brief background hue tween; less dramatic, more product-UI tone. -The classic pattern: morph the container, then reveal inner text once the container has settled (as in the example above with `.sub`). The 0.2-0.4s gap between morph end and content reveal lets the viewer's eye land on the new container shape before reading the content. +## Values -### Triple swap (3-state cycle) - -Chain: A→B→C with two triggers `TRIGGER_AB` and `TRIGGER_BC`. Each transition needs its own pair of tweens, and the previous incoming becomes the next outgoing. Useful for state evolution narratives (e.g. early-state → mid-state → final-state labels). - -```js -tl.to("#stateA", { scale: EXIT_SCALE, opacity: 0, duration: EXIT_DUR }, TRIGGER_AB); -tl.to( - "#stateB", - { scale: 1.0, opacity: 1, duration: ENTER_DUR, ease: `back.out(${BOUNCE_FACTOR})` }, - TRIGGER_AB + EXIT_DUR - OVERLAP, -); -tl.to("#stateB", { scale: EXIT_SCALE, opacity: 0, duration: EXIT_DUR }, TRIGGER_BC); -tl.to( - "#stateC", - { scale: 1.0, opacity: 1, duration: ENTER_DUR, ease: `back.out(${BOUNCE_FACTOR})` }, - TRIGGER_BC + EXIT_DUR - OVERLAP, -); -``` - -### Color-shift transition (no scale) - -For a flat morph between two same-shape states, drop the scale and keep only opacity + a brief background hue tween. Less dramatic but matches a more product-UI tone. - -## How to Choose Values - -### Timing (seconds) - -- **TRIGGER** — when the swap fires. - - Constraints: must be ≥ the outgoing element's settled time + a presence-dwell so the outgoing "lands" before transforming -- **EXIT_DUR** — outgoing shrink + fade duration. - - Range: 0.3-0.5 s -- **ENTER_DUR** — incoming pop-in duration. - - Range: 0.45-0.7 s (longer than `EXIT_DUR` to let the overshoot settle) -- **OVERLAP** — how much the entrance starts before the exit finishes. - - Range: 0.1-0.2 s - - Constraints: too much (>0.3 s) makes both clearly visible together (no morph); too little (<0.05 s) leaves a visible empty gap -- **SUB_REVEAL_DELAY** — gap between incoming settle and subline reveal. - - Range: 0.2-0.4 s; reveals during the morph compete with the swap for attention -- **SUB_REVEAL_DUR** — subline fade-in. - - Range: 0.3-0.5 s -- **BRAND_REVEAL_AT** — when the brand/context line fades in. - - Constraints: must be < `TRIGGER` (brand is context for the swap, not synchronous with it) -- **BRAND_REVEAL_DUR** — brand fade-in duration. - - Range: 0.4-0.8 s - -### Physics - -- **EXIT_SCALE** — target scale for outgoing (and starting scale for incoming). - - Range: 0.6-0.8; smaller exits feel more dramatic but risk reading as "vanish" instead of "morph" -- **BOUNCE_FACTOR** — `back.out(${BOUNCE_FACTOR})` overshoot on the incoming. - - Range: 1.4 (soft) - 1.8 (firm) - 2.2 (cartoony) - -### Positioning offsets - -- **SUB_REVEAL_Y_PX** — subline initial y offset (positive = below resting). - - Range: 8-20 px -- **BRAND_REVEAL_Y_PX** — brand initial y offset. - - Range: 10-24 px - -### Layout - -- **STACK_GAP** — gap between swap container and brand line. - - Range: 40-96 px -- **SWAP_WRAP_W / SWAP_WRAP_H** — fixed swap container dimensions; both cards `inset: 0` inside. - - Constraints: pick dimensions that fit both states' content; the wrap does not resize during the swap -- **CARD_INNER_GAP** — gap between icon and title inside a card. - - Range: 16-32 px -- **CARD_RADIUS / CARD_PADDING** — card corner radius and inner padding. - - Range: radius 24-40 px; padding 32-64 px -- **ICON_SIZE / TITLE_SIZE / SUB_SIZE / BRAND_SIZE** — typographic sizes. - - Constraints: titles dominate (~80-120 px at 1080p); sub and brand are accent-sized -- **TITLE_TRACKING / BRAND_TRACKING** — letter-spacing on uppercase labels. - - Range: 4-16 px (uppercase reads better with positive tracking) - -### Tokens - -- **{sceneBg}** — background gradient/color -- **{font}** — typographic stack -- **{textColor}** / **{accentColor}** / **{brandColor}** — semantic color tokens -- **{outgoingBg}** / **{outgoingBorder}** — outgoing card surface + border (typically warm or pre-action hue) -- **{incomingBg}** / **{incomingBorder}** — incoming card surface + border (typically cool or post-action hue) -- **{outgoingIcon}** / **{incomingIcon}** — single glyph/emoji per state -- **{outgoingLabel}** / **{incomingLabel}** — state labels -- **{incomingSubline}** — supporting copy that fades in after the incoming settles -- **{Brand}** — brand line shown beneath the swap - -## Key Principles - -- **Incoming z-index ABOVE outgoing** — without this, the outgoing's fade-tail (opacity 0.3-0.5) bleeds through the incoming's lower opacity and creates a "double-exposed" muddy frame -- **Both elements share `transform-origin: 50% 50%`** — different origins make the morph feel like one thing teleporting somewhere else -- **`OVERLAP` in the 0.1-0.2 s window** — too much overlap and both are clearly visible together (no morph); too little and there's a visible empty gap -- **Bouncy ease ONLY for the incoming** — outgoing uses `power2.in` (rushing away), incoming uses `back.out(${BOUNCE_FACTOR})` (arriving with weight). Reverse it and the swap feels mechanical -- **Inner content reveals AFTER container settles** — see `SUB_REVEAL_DELAY`. Reveals during the morph compete for attention and lose -- **Climax dwell ≥1 s after final state lands** — see SKILL universal constraints. After incoming + subline both settle, hold for ≥1 s -- **Brand reveal early, not at the swap** — context (brand, eyebrow) sets the stage; the swap is the headline. If brand reveals AT the swap, it competes +| token | range | notes | +| ---------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| TRIGGER | ≥ outgoing settled + a presence-dwell | the outgoing must "land" before transforming | +| EXIT_DUR | 0.3–0.5 s | | +| ENTER_DUR | 0.45–0.7 s | longer than `EXIT_DUR` so the overshoot can settle | +| OVERLAP | 0.1–0.2 s | >0.3 s both are clearly visible together (no morph); <0.05 s leaves a visible empty gap | +| EXIT_SCALE | 0.6–0.8 | smaller exits feel dramatic but risk reading as "vanish" instead of "morph" | +| BOUNCE_FACTOR | 1.4 soft · 1.8 firm · 2.2 cartoony | | +| SUB_REVEAL_DELAY | 0.2–0.4 s | reveals during the morph compete with the swap for attention | +| BRAND_REVEAL_AT | < TRIGGER | context (brand, eyebrow) sets the stage early; revealed AT the swap it competes with the headline beat | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition`** on either swap element — competes with GSAP -- **`will-change: transform, opacity`** on both swap elements -- **Both elements use `position: absolute; inset: 0`** in the same wrapper — they occupy the same footprint, swap fades one out and pops one in -- **Don't `display: none` the outgoing** after fade — leave it at `opacity: 0` so layout doesn't reflow +- **Incoming z-index ABOVE outgoing** — otherwise the outgoing's fade-tail (opacity 0.3–0.5) bleeds through and double-exposes the frame. +- **Both elements share `transform-origin: 50% 50%`** — different origins make the morph read as one thing teleporting elsewhere. +- **Bouncy ease ONLY on the incoming** — outgoing `power2.in`, incoming `back.out`; reversed, the swap feels mechanical. +- **Both cards `position: absolute; inset: 0`** in the same fixed-size wrapper (sized to fit both states; the wrap never resizes). +- **Don't `display: none` the outgoing** after the fade — leave it at `opacity: 0` so layout doesn't reflow. +- **Inner content reveals after the container settles**; **climax dwell ≥ 1 s** after the final state + subline land. -## Combinations +## See also -- [press-release-spring.md](press-release-spring.md) — button press TRIGGERS the swap (cause and effect) -- [sine-wave-loop.md](sine-wave-loop.md) — idle breathing on the final state -- [card-morph-anchor.md](card-morph-anchor.md) — alternative for SHAPE-changing transitions (this rule is for SAME-shape state swaps) - -## Pairs with HF skills - -- `/hyperframes-animation` — two coordinated tweens with overlap -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`press-release-spring` (a button press TRIGGERS the swap — cause and effect) · `card-morph-anchor` (shape-changing alternative) · `reactive-displacement` (when the replacement should read as a causal collision) · `sine-wave-loop` (idle breathing on the final state). diff --git a/skills/hyperframes-animation/rules/sine-wave-loop.md b/skills/hyperframes-animation/rules/sine-wave-loop.md index 81aff5ecb..6619603b7 100644 --- a/skills/hyperframes-animation/rules/sine-wave-loop.md +++ b/skills/hyperframes-animation/rules/sine-wave-loop.md @@ -7,272 +7,79 @@ metadata: # Sine Wave Loop (subtle jitter / bounded ambient) -> **Reach for this last.** Per the motion doctrine (`references/motion-language.md`): **circular breathing — scaling text/cards up and down to look "alive" — is cheap, the agent's reflexive cheat, and reads weak.** "I'd rather have NO motion than BAD motion." Before using this rule to keep a held frame alive, prefer (1) **sequential reveal timed to the voiceover** — reveal the next line/element when the VO says it, across the back ~50% of the scene; that, not ambient motion, is what fills a shot. If a frame has genuinely settled and still needs a touch of life, the **sanctioned move is subtle jitter** — a small, low-amplitude jitter (this rule, at the LOW end of its amplitude range). A full breathing loop is reserved for the rare case where a single held hero genuinely needs bounded ambient; keep it de-emphasized and small. +> **Reach for this last.** Per the motion doctrine (`references/motion-language.md`): circular breathing — scaling text/cards up and down to look "alive" — is cheap, the agent's reflexive cheat, and reads weak. "I'd rather have NO motion than BAD motion." First fill the back of a shot with **sequential reveal timed to the VO**; if a frame has genuinely settled and still needs life, the **sanctioned move is subtle jitter** — this rule at the LOW end of its amplitude range. A full breathing loop is the rare last resort on a single held hero, never stamped on every element. -Keeps a settled element from feeling dead — as **subtle jitter** or, rarely, a single bounded ambient breath — using `Math.sin` driven by a finite timeline tween. This is the implementation behind the "subtle jitter" move in the motion vocabulary; it is **not** a license to breathe every hero. +Keeps a settled element from feeling dead using `Math.sin` on the timeline clock. Two forms: -## How It Works +- **Yoyo form** — one `sine.inOut` tween with `yoyo: true` and a **finite** `repeat` count. Preferred when the idle stands alone on a property nothing else touches. +- **onUpdate form** — one long `ease: "none"` tween drives a `phase` proxy `0 → 2π·CYCLES`; `onUpdate` maps `Math.sin(phase)` into the transform. Required when the offset multiplies/adds onto another live value (compound transforms, amplitude envelopes, multi-octave). -A long tween advances a `phase` value from 0 → 2π (or 0 → some multiple thereof). On every onUpdate, the phase feeds into `Math.sin()` to produce a small periodic offset added to the element's transform (`scale`, `translateY`, `rotate`). +Either way, idle begins where the entry settled: at `phase = 0`, `sin(0) = 0` — the offset is zero, so there is no jump from the entry's resting state. -The trick to a "no jump" transition from entry to idle: at `phase = 0`, `sin(0) = 0` — the offset is zero, so the element starts at its post-entry resting state. - -## HTML - -```html -
-
-
{HeroLabel}
-
-
-
-``` - -## CSS - -```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgGradient}; -} -.stack { - display: flex; - flex-direction: column; - align-items: center; - gap: STACK_GAP; -} -.hero { - font-family: {font}; - font-weight: 900; - font-size: HERO_FONT_SIZE; - letter-spacing: HERO_LETTER_SPACING; - color: {textColor}; - text-transform: uppercase; - /* Element gets its post-entry resting transform; idle only ADDS to it */ - will-change: transform; -} -.dot { - width: DOT_SIZE; - height: DOT_SIZE; - border-radius: 50%; - background: {accentColor}; - box-shadow: {accentGlow}; -} -``` - -## GSAP Timeline - -```html - - -``` - -## Variations - -### Multiple offset frequencies (organic multi-octave breathing) - -Combining frequencies feels more alive than pure sine: - -```js -const primary = Math.sin(phase.p) * SCALE_AMP_PRIMARY; -const secondary = Math.sin(phase.p * OCTAVE_RATIO) * SCALE_AMP_SECONDARY; // higher-frequency overlay -const scale = 1 + primary + secondary; -``` - -### Conditional activation (only after entry settles) - -If entry is interactive or skippable, gate the idle: - -```js -const idleActive = entryProgress >= GATE_THRESHOLD; -const scale = idleActive ? 1 + Math.sin((time - IDLE_START_TIME) / PERIOD) * SCALE_AMP : 1; -``` - -### Settle and fade (long-idle gate — strongly recommended when `IDLE_DUR > 6s`) - -Drive amplitude through an envelope that fades to zero over the last ~20% of idle, so the scene visibly settles before the inter-scene transition lands: +## Recipe ```js +// onUpdate form — phase-driven, composable. const phase = { p: 0 }; -const FADE_FRAC = 0.2; // last 20% of idle = amplitude ramps to 0 tl.to( phase, { p: Math.PI * 2 * CYCLES, duration: IDLE_DUR, - ease: "none", + ease: "none", // sine provides the easing; a non-linear phase tween distorts the wave onUpdate: () => { - const t = phase.p / (Math.PI * 2 * CYCLES); // 0 → 1 across idle - const env = t < 1 - FADE_FRAC ? 1 : (1 - t) / FADE_FRAC; // 1 → 0 in tail - const scale = 1 + Math.sin(phase.p) * SCALE_AMP * env; - const y = Math.sin(phase.p) * Y_AMP_PX * env; - hero.style.transform = `translateY(${y}px) scale(${scale})`; + const s = Math.sin(phase.p); + hero.style.transform = `translateY(${s * Y_AMP_PX}px) scale(${1 + s * SCALE_AMP})`; + // secondary elements: offset by Math.PI / 2 — synced motion looks mechanical + dot.style.transform = `scale(${1 + Math.sin(phase.p + Math.PI / 2) * DOT_SCALE_AMP})`; }, }, IDLE_START_TIME, ); + +// Yoyo form — standalone property, finite repeats. +tl.to( + "#badge", + { y: -Y_AMP_PX, duration: PERIOD / 2, ease: "sine.inOut", yoyo: true, repeat: REPEATS }, + IDLE_START_TIME, +); ``` -The element is in motion for the first 80% of idle, then comes to rest in the last 20%. Pairs naturally with break-boundary Tier-B transitions (the outgoing visual is static when the crossfade/push begins). +## Variations -### Period vs cycle math - -For an exact cycle of N seconds: +- **Multi-octave** (organic): stack a higher-frequency overlay — `1 + Math.sin(p) * AMP_PRIMARY + Math.sin(p * OCTAVE_RATIO) * AMP_SECONDARY`, with `AMP_SECONDARY < AMP_PRIMARY` and the combined max inside the normal SCALE_AMP range. +- **Settle and fade** (strongly recommended when `IDLE_DUR > 6s`): ramp amplitude to zero over the last ~20% of idle so the scene visibly settles before the inter-scene transition, instead of handing off mid-drift: ```js -const divisor = (idleDurationSec * fps) / (Math.PI * 2); -const value = Math.sin(frame / divisor) * amplitude; +const t = phase.p / (Math.PI * 2 * CYCLES); // 0 → 1 across idle +const env = t < 1 - FADE_FRAC ? 1 : (1 - t) / FADE_FRAC; // FADE_FRAC ≈ 0.2 +const scale = 1 + Math.sin(phase.p) * SCALE_AMP * env; ``` -For HF (`onUpdate` doesn't expose frame directly), use the tween's `phase` value: drive `p: Math.PI * 2 * cyclesWanted` over `duration: idleDurationSec`. +This is the single biggest fix when finalize snapshots show "everything's still moving at the end"; it pairs naturally with break-boundary transitions (the outgoing visual is static when the crossfade/push begins). -## How to Choose Values +## Values -### Layout / typography - -- **STACK_GAP** — vertical gap between hero and dot. - - Range: 0.2-0.4× `HERO_FONT_SIZE` -- **HERO_FONT_SIZE / HERO_LETTER_SPACING** — typographic emphasis. - - Range: 100-240 px for full-bleed compositions; spacing 0.04-0.06em -- **DOT_SIZE** — accent indicator size. - - Range: ~0.15-0.25× `HERO_FONT_SIZE` so the dot reads as accent, not a peer -- **{accentGlow}** — `box-shadow` halo on the dot; typically `0 0 (DOT_SIZE) rgba(accentColor, 0.5-0.7)` - -### Entry phase - -- **ENTRY_Y / ENTRY_SCALE** — initial state before fade-up. - - Range: `ENTRY_Y` 16-32 px (subtle rise), `ENTRY_SCALE` 0.94-0.98 (subtle inflation) -- **ENTRY_DUR** — hero fade-up duration. - - Range: 0.6-1.2s; bigger heroes want a longer settle -- **DOT_ENTRY_START** — when the dot pops in relative to hero. - - Constraints: typically `≈ 0.4-0.6× ENTRY_DUR` so the dot lands while the hero is still settling, not after -- **DOT_ENTRY_DUR** — dot back-out pop duration. - - Range: 0.4-0.7s -- **BOUNCE_FACTOR** — `back.out(BOUNCE_FACTOR)` overshoot strength on the dot pop. - - Range: 1.4 (soft) → 2.0 (firm) → 2.8 (cartoony) - -### Idle phase - -- **IDLE_START_TIME** — when breathing begins. - - Constraints: `≥ ENTRY_DUR + small buffer (~0.1s)` so the breath doesn't fight the entry tail. `sin(0) = 0` at this moment, so the offset is exactly the entry's resting state — no jump -- **IDLE_DUR** — breath tween length. - - Constraints: must equal `TOTAL_DURATION − IDLE_START_TIME` to fill the composition with motion -- **CYCLES** — number of full breath cycles across `IDLE_DUR`. - - Range: `IDLE_DUR / 3s ≤ CYCLES ≤ IDLE_DUR / 1.5s` (cycle period 1.5-3s reads as natural breathing) -- **SCALE_AMP** — sine amplitude on scale (hero). - - **Default: 0.008-0.015** (barely-perceptible breath — the right answer for most scenes) - - Push to 0.02-0.04 only when the element is **alone on canvas**, the scene is **short (< 6s)**, or the brief explicitly calls for **kinetic / playful** register - - See Key Principles for the long-idle / concurrent-element scaling rules -- **Y_AMP_PX** — sine amplitude on y translation (hero). - - **Default: 2-3 px** (barely-perceptible — the right answer for most scenes) - - Push to 4-6 px only when isolated / short / kinetic — same gating as `SCALE_AMP` -- **DOT_SCALE_AMP** — sine amplitude on dot scale (offset by π/2 for out-of-phase motion). - - Range: 0.04-0.12 — larger than hero amplitude is fine because the dot is a small accent -- **PERIOD** (conditional-activation variation) — seconds per cycle when using the `(time - IDLE_START_TIME) / PERIOD` form. - - Range: 1.5-3s -- **GATE_THRESHOLD** (conditional-activation variation) — entryProgress required to start idle. - - Range: 0.85-1.0; lower gates start idle slightly before entry completes for an overlap - -### Multi-octave variation - -- **SCALE_AMP_PRIMARY / SCALE_AMP_SECONDARY** — amplitudes of the two stacked sines. - - Constraints: `SCALE_AMP_PRIMARY > SCALE_AMP_SECONDARY` (secondary is a higher-frequency overlay, not a peer); combined max amplitude should stay within the SCALE_AMP range above -- **OCTAVE_RATIO** — frequency multiplier of secondary relative to primary. - - Range: 2.0-4.0 (whole-number-ish ratios feel musical/coherent; non-integer ratios feel organic/unpredictable) - -### Color tokens - -- **{bgGradient}** — typically a dark radial gradient so the lit hero pops -- **{textColor}** — high-contrast against `{bgGradient}` -- **{accentColor}** — single accent reserved for the dot; the glow color in `{accentGlow}` is the same hue - -## Key Principles - -- **Prefer reveal, then jitter, then breath** — circular breathing as "aliveness" is cheap and reads weak; "no motion over bad motion." First fill the back of a shot with **sequential reveal timed to the VO**; if it's genuinely settled and still feels dead, use this rule at the **LOW end of its amplitude range as subtle jitter**; a full breathing loop is the rare last resort on a single held hero, never stamped on every element. -- **`sin(0) = 0`** — at the moment idle begins, the offset must be zero so there's no visible jump from the entry's settled state to idle. Start the phase tween at `phase = 0`. -- **Amplitude subtlety — default to the LOW end of the range.** Scale `0.008-0.015` (push to 0.02-0.04 only when isolated / short scene / kinetic brief), rotation `±0.3-0.8°` (rarely needed at all), translation `±2-3px` (push to 4-6px only when isolated). Bigger and idle reads as "still animating" instead of "alive but resting" — and a viewer watching 5+ consecutive scenes at the upper end will read the whole film as "shimmering." -- **Cycle duration: 2.5-4s per breath when idle is long, 1.5-3s otherwise** — 2.5-3s is a comfortable breathing cadence; under 1.5s feels frantic in a long-idle window; over 4s feels lifeless in a short one. -- **Long idle window (`IDLE_DUR > 6s` OR idle proportion > 30% of composition):** halve `SCALE_AMP` and `Y_AMP_PX`, slow `CYCLES` so each breath is 3-4s. Consider gating amplitude to fade to zero over the last ~20% of idle so the scene actually **settles before the transition**, instead of handing off mid-drift. This is the single biggest fix when finalize snapshots show "everything's still moving at the end." -- **Concurrent idle on N elements** (triptych columns, card grid, multi-stat row, side-by-side panels): per-element amplitude ≤ default `/ √N`. Three columns each at `±6px` visually adds to `±18px+` of competing motion; three at `±2-3px` reads as one collective breath. Stagger the **period** between elements (2.1s / 1.9s / 2.4s) for organic feel — but the **amplitude** must also be smaller, not just the period. -- **Different elements at different phases** — offset secondary elements by `Math.PI / 2` (90° offset) so they're not all moving in sync. Synced motion looks mechanical; out-of-phase looks alive. -- **Compose, don't replace** — idle motion ADDS to the element's resting transform, not replace it. If the entry settled at `translateY(0)`, idle should produce `translateY(0 + sin*4)`. Don't overwrite the entry's final translation. -- **❗ Don't use CSS `@keyframes` for the idle loop** — CSS animation runs on the browser's render clock, which is independent of the HF seek clock. HF seeks frame-by-frame and a CSS-driven idle will flicker/desync. Drive idle inside the GSAP timeline. +| token | range / default | notes | +| --------------- | ------------------------------------ | -------------------------------------------------------------------------- | +| SCALE_AMP | **0.008–0.015 default** | push to 0.02–0.04 only when isolated on canvas / scene <6s / kinetic brief | +| Y_AMP_PX | **2–3px default** | 4–6px only under the same gating; rotation ±0.3–0.8° rarely needed at all | +| period | 1.5–3s (2.5–4s when idle is long) | <1.5s frantic; >4s lifeless in a short window | +| CYCLES | `IDLE_DUR/3 ≤ CYCLES ≤ IDLE_DUR/1.5` | derive from the period, not the other way round | +| IDLE_START_TIME | ≥ entry settle + ~0.1s | `sin(0)=0` at this moment → no jump off the entry tail | +| IDLE_DUR | `TOTAL_DURATION − IDLE_START_TIME` | one long tween fills the hold — never restarted | +| DOT_SCALE_AMP | 0.04–0.12 | small accents tolerate more than the hero | +| OCTAVE_RATIO | 2.0–4.0 | integer-ish reads musical; non-integer reads organic | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `animation`** for idle — must be timeline-driven -- **`will-change: transform`** if the idle compounds with other tweens on the same element -- **Phase tween `ease: 'none'`** — sine itself provides the easing; tweening the phase non-linearly produces non-sinusoidal motion -- **Don't restart the idle tween** — it's a single long tween from start to end of composition idle window +- **Prefer reveal, then jitter, then breath** — the doctrine order above; default to the LOW end of every amplitude range. At the upper end across 5+ consecutive scenes the whole film reads as "shimmering". +- **Long idle window** (`IDLE_DUR > 6s` OR idle > 30% of composition): halve `SCALE_AMP` / `Y_AMP_PX`, slow the period to 3–4s, and add the settle-and-fade tail. +- **Concurrent idle on N elements** (columns, card grid, stat row): per-element amplitude ≤ default `/ √N`, AND stagger the periods (2.1s / 1.9s / 2.4s). Three columns at ±6px compound to ±18px of competing motion; three at ±2–3px read as one collective breath. +- **Compose, don't replace** — idle ADDS to the element's resting transform; never overwrite the entry's final translation. +- **Phase tween `ease: "none"`** — sine itself is the curve. +- **No CSS `@keyframes` for idle** — CSS animation runs on the browser's render clock, independent of the HF seek clock; a CSS-driven idle flickers/desyncs. Drive idle inside the timeline. -## Combinations +## See also -- After [press-release-spring.md](press-release-spring.md) — button idle-breathes after release settles -- After [counting-dynamic-scale.md](counting-dynamic-scale.md) — final number breathes -- After [card-morph-anchor.md](card-morph-anchor.md) — settled card idle-bobs -- After [orbit-3d-entry.md](orbit-3d-entry.md) — center label idle-breathes while items orbit - -## Pairs with HF skills - -- `/hyperframes-animation` — `onUpdate` writing transform -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`ambient-glow-bloom` (the glow-layer counterpart, same bounded-breathe discipline) · `press-release-spring` / `counting-dynamic-scale` / `card-morph-anchor` / `orbit-3d-entry` (settled elements this can follow) · `spring-pop-entrance` (the arrival that precedes any idle). diff --git a/skills/hyperframes-animation/rules/split-tilt-cards.md b/skills/hyperframes-animation/rules/split-tilt-cards.md index 2908901a2..def415a99 100644 --- a/skills/hyperframes-animation/rules/split-tilt-cards.md +++ b/skills/hyperframes-animation/rules/split-tilt-cards.md @@ -7,53 +7,31 @@ metadata: # Split Tilt Cards -Two cards positioned side-by-side, each rotated in opposite Y directions. Creates a symmetric "book-open" 3D effect — natural fit for comparisons, before/after, or feature pairs. +Two cards side-by-side with opposing `rotateY` (left `+TILT`, right `−TILT`) — a symmetric "book-open" 3D split for comparisons, before/after, feature pairs. Each card slides in from its own side (reinforcing "they came from their own worlds and met here"), then the pair idles in counter-phase. ## How It Works -- Left card rotates `+Y` (faces toward the right viewer angle) -- Right card rotates `-Y` (faces toward the left viewer angle) -- Both share the same `perspective` parent → opposing rotations balance visually -- Each card enters from outside (left card slides in from the left, right card from the right) to reinforce its identity -- Idle phase: gentle counter-phase float (`Math.PI` offset on sine) — cards bob in opposition +`perspective` on the scene root (REQUIRED — without it `rotateY` flattens to a 2D layout) and `transform-style: preserve-3d` on the stage and both cards. Entry starts each card off-axis with `TILT + TILT_OVERSHOOT`, settling to `TILT` — a pivot-into-place. Idle is a gentle counter-phase y-bob (the two yoyo tweens run in opposite directions); copy fades up during the cards' settle, not after. -## HTML +## Recipe ```html -
-
-
-
{leftEyebrow}
-
{leftHeadline}
-
{leftBody}
-
-
-
{rightEyebrow}
-
{rightHeadline}
-
{rightBody}
-
+ +
+
+
{leftEyebrow}
+
{leftHeadline}
+
{leftBody}
+
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; +.scene-root { display: grid; place-items: center; - background: {bgGradient}; - perspective: SCENE_PERSPECTIVE; /* REQUIRED — without perspective rotateY flattens */ + perspective: SCENE_PERSPECTIVE; /* REQUIRED */ } .split-stage { display: flex; @@ -62,216 +40,84 @@ Two cards positioned side-by-side, each rotated in opposite Y directions. Create } .card { width: CARD_WIDTH; - min-height: CARD_MIN_HEIGHT; - padding: CARD_PADDING; - display: flex; - flex-direction: column; - gap: CARD_INNER_GAP; - border-radius: CARD_RADIUS; - background: {cardSurface}; - border: 1px solid {cardBorder}; - color: {textColor}; - font-family: {font}; transform-style: preserve-3d; will-change: transform; } +/* Shadow falls WITH the facing direction: left card faces right → shadow right. */ .card-left { - /* Faces right → shadow falls right */ - box-shadow: - -CARD_SHADOW_OFFSET CARD_SHADOW_DROP CARD_SHADOW_BLUR {shadowColor}, - 0 0 CARD_GLOW_BLUR {accentGlowColor}; + box-shadow: -CARD_SHADOW_OFFSET CARD_SHADOW_DROP CARD_SHADOW_BLUR {shadowColor}; } .card-right { - /* Faces left → shadow falls left */ - box-shadow: - CARD_SHADOW_OFFSET CARD_SHADOW_DROP CARD_SHADOW_BLUR {shadowColor}, - 0 0 CARD_GLOW_BLUR {accentGlowColor}; -} -.card-eyebrow { - font-size: EYEBROW_FONT_SIZE; - font-weight: 800; - letter-spacing: EYEBROW_LETTER_SPACING; - text-transform: uppercase; - color: {accentColor}; -} -.card-headline { - font-size: HEADLINE_FONT_SIZE; - font-weight: 900; - line-height: 1; - letter-spacing: HEADLINE_LETTER_SPACING; -} -.card-body { - font-size: BODY_FONT_SIZE; - font-weight: 500; - line-height: 1.3; - color: {bodyColor}; - opacity: BODY_OPACITY; + box-shadow: CARD_SHADOW_OFFSET CARD_SHADOW_DROP CARD_SHADOW_BLUR {shadowColor}; } ``` -## GSAP Timeline +```js +// Entry — from outside, opposing tilts settle with a small pivot +tl.fromTo( + ".card-left", + { x: -ENTRY_SLIDE_DIST, rotateY: TILT + TILT_OVERSHOOT, opacity: 0 }, + { x: 0, rotateY: TILT, opacity: 1, duration: ENTRY_DUR, ease: "power3.out" }, + LEFT_AT, +); +tl.fromTo( + ".card-right", + { x: ENTRY_SLIDE_DIST, rotateY: -TILT - TILT_OVERSHOOT, opacity: 0 }, + { x: 0, rotateY: -TILT, opacity: 1, duration: ENTRY_DUR, ease: "power3.out" }, + RIGHT_AT, +); -```html - - +// Copy fades up during the settle +tl.from( + ".card-eyebrow, .card-headline, .card-body", + { opacity: 0, y: COPY_RISE, stagger: COPY_STAGGER, duration: COPY_DUR, ease: "power2.out" }, + COPY_REVEAL_AT, +); ``` -## How to Choose Values - -### Layout / typography - -- **SCENE_PERSPECTIVE** — perspective on the scene root. - - Range: 1000-2400 px - - Effects: lower exaggerates the tilt (more cone-like); higher reads as a near-isometric, flatter rotation -- **STAGE_GAP** — horizontal gap between the two cards. - - Range: 40-120 px (≈0.06-0.15× `CARD_WIDTH`) - - Effects: small gap reads "fused pair"; large gap reads "compared but separate" -- **CARD_WIDTH** — width of each card. - - Range: 480-820 px at 1920×1080 - - Constraints: `2 * CARD_WIDTH + STAGE_GAP ≤ 0.95 * stageWidth` so both cards stay on-screen at full tilt -- **CARD_MIN_HEIGHT** — minimum card height. - - Range: ≈0.75-1.05× `CARD_WIDTH` (square-ish reads as balanced; very tall reads as poster) -- **CARD_PADDING / CARD_INNER_GAP / CARD_RADIUS** — interior chrome. - - Range: padding 40-72 px; inner gap 24-48 px; radius 24-40 px -- **CARD_SHADOW_OFFSET / CARD_SHADOW_DROP / CARD_SHADOW_BLUR** — drop-shadow geometry. - - Constraints: offset's sign matches tilt direction (see Key Principles); drop > 0 grounds the card on the scene - - Range: offset 16-28 px; drop 20-32 px; blur 40-80 px -- **CARD_GLOW_BLUR** — secondary inner glow blur radius (`box-shadow` 0 0 blur). - - Range: 16-32 px (subtle accent rim; larger competes with the card content) -- **EYEBROW_FONT_SIZE / EYEBROW_LETTER_SPACING** — small uppercase label. - - Range: 22-32 px; spacing 6-12 px (uppercase reads cleaner with positive letter-spacing) -- **HEADLINE_FONT_SIZE / HEADLINE_LETTER_SPACING** — the main one-line punch. - - Range: 72-104 px at 1920×1080; spacing -1 to -3 px (tight tracking for display weight) -- **BODY_FONT_SIZE / BODY_OPACITY** — supporting copy. - - Range: 28-40 px; opacity 0.8-0.92 - - Constraints: body limited to ≤2 lines (see Critical Constraints — tilted long paragraphs blur) - -### Entry / tilt - -- **TILT** — static `rotateY` magnitude in degrees (left `+`, right `−`). - - Range: 10-18° (under 10 reads almost flat; over 18 the cards fold shut and body copy becomes hard to read) -- **TILT_OVERSHOOT** — extra degrees added to the starting `rotateY` before settling to `TILT`. - - Range: 4-12° - - Effects: gives the entry a slight pivot-into-place feel -- **ENTRY_SLIDE_DIST** — pixels each card slides from off-axis. - - Range: 200-500 px (≈0.3-0.6× `CARD_WIDTH`) -- **ENTRY_DUR** — per-card slide-in duration. - - Range: 0.6-1.2 s -- **LEFT_AT** — left card entry start. - - Range: 0.0-0.4 s -- **RIGHT_AT** — right card entry start. - - Range: `LEFT_AT + 0.0` to `LEFT_AT + 0.3` s (zero stagger feels mechanical; large stagger fragments the pair) - -### Idle bob - -- **FLOAT_AMP** — sine amplitude on idle y bob (px). - - Range: 3-8 px (see sine-wave-loop rule — subtle is the point) -- **FLOAT_DURATION** — full yoyo round-trip duration (one breath). - - Range: 1.6-3.2 s (≈breathing cadence) -- **IDLE_START** — when idle bob begins. - - Constraints: `≥ max(LEFT_AT, RIGHT_AT) + ENTRY_DUR` so idle doesn't fight the entry tail - -### Copy reveal - -- **COPY_REVEAL_AT** — when eyebrow/headline/body fade in. - - Constraints: usually starts during the cards' settle (overlaps the entry tail) — content shouldn't pop in after the cards are already idle -- **COPY_DUR / COPY_STAGGER / COPY_RISE** — fade-up shape. - - Range: duration 0.4-0.7 s; stagger 0.04-0.10 s; rise 12-24 px - -### Color / typography tokens - -- **{bgGradient}** — radial or linear gradient behind the cards; darker than `{cardSurface}` so cards lift -- **{cardSurface}** — card background (typically a low-saturation gradient layered over the scene) -- **{cardBorder}** — 1 px border color, usually `{accentColor}` at low alpha -- **{shadowColor}** — drop-shadow color, typically near-black at 0.5-0.7 alpha -- **{accentGlowColor}** — inner glow color, typically `{accentColor}` at low alpha -- **{accentColor}** — eyebrow + accent rim color (single hue per scene) -- **{textColor}** — primary headline color, high contrast against `{cardSurface}` -- **{bodyColor}** — body copy color, slightly desaturated vs `{textColor}` -- **{font}** — display font stack for all card copy - ## Variations -### Mid-tilt zoom-through (combined with camera move) +- **Badges / floating labels**: position them on the PARENT, never inside a card — inside they inherit the `rotateY` and tilt off-axis. +- **3+ cards**: center card stays flat (`rotateY: 0`), outer two tilt inward — "old way / nothing / our way." +- **Zoom-through**: a separate camera tween scaling `.split-stage` reads as the viewer crossing the gap between the tilted pair. -If a separate camera tween scales `.split-stage`, the cards' tilt reads as the viewer crossing through the gap between them. +## Values -### Asymmetric content density (badge / label / icon) - -Add a floating badge near each card for additional context. Position absolutely on the parent — not inside the card, so the badge doesn't inherit the 3D rotation: - -```html -
{leftBadge}
-
{rightBadge}
-``` - -### Stacked variants (3+ cards) - -For 3 cards, the center card stays flat (`rotateY 0`) and the outer two tilt inward — useful for "your old way / nothing in between / our way" comparisons. - -## Key Principles - -- **`perspective` on scene root REQUIRED** — without it rotateY flattens and the split-tilt collapses to a flat side-by-side layout -- **`transform-style: preserve-3d`** on both the stage and each card — preserves the 3D plane as cards have their own transforms -- **Shadow direction must match tilt** — left card faces right, shadow falls right (positive X), and vice versa. Wrong shadow direction reads as "broken 3D" -- **Symmetric content weight** — both cards same width, same vertical center, similar line counts. Asymmetric content breaks the comparison metaphor -- **Counter-phase float (`Math.PI` offset)** — left bobs up while right bobs down. Synchronized bob looks like both cards are on the same conveyor belt; counter-phase looks alive -- **Slide-in from the outside** — left card from left, right card from right — reinforces "they came from their own worlds and met here" -- **❗ Tilt magnitude 10-15°** — under 10° looks like a slight perspective offset (almost flat), over 18° looks like the cards are folding shut and copy becomes hard to read +| token | range | notes | +| ----------------- | -------------------------------- | ------------------------------------------------------- | +| SCENE_PERSPECTIVE | 1000–2400px | lower exaggerates the tilt; higher reads near-isometric | +| TILT | 10–18° | < 10 reads almost flat; > 18 folds shut and copy blurs | +| TILT_OVERSHOOT | 4–12° | the pivot-into-place feel | +| STAGE_GAP | 40–120px (~0.06–0.15×CARD_WIDTH) | small = fused pair; large = compared-but-separate | +| CARD_WIDTH | 480–820px @1920 | `2×CARD_WIDTH + STAGE_GAP ≤ 0.95×stage` at full tilt | +| ENTRY_SLIDE_DIST | 200–500px (~0.3–0.6×CARD_WIDTH) | | +| ENTRY_DUR | 0.6–1.2s | | +| RIGHT_AT | LEFT_AT + 0–0.3s | zero feels mechanical; large fragments the pair | +| FLOAT_AMP | 3–8px | subtle is the point | +| FLOAT_DURATION | 1.6–3.2s round trip | breathing cadence; IDLE_START ≥ entry end | +| COPY_REVEAL_AT | during the entry tail | copy popping in after cards are idle reads disconnected | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No `requestAnimationFrame`** for the idle float — drive it inside the timeline so seek is deterministic -- **Don't put badges inside the card divs** — they'd inherit the rotateY and tilt off-axis with the card. Float them on the parent -- **Body copy ≤ 2 lines per card** — tilted text becomes hard to read; long paragraphs collapse into a perspective blur +- **`perspective` on the scene root is REQUIRED**; `preserve-3d` on the stage AND each card. +- **Shadow direction matches tilt** — left card faces right → shadow falls right (and mirrored). Wrong sign reads as broken 3D. +- **Counter-phase idle** — the two bobs run with opposite signs at the same position. +- **Badges outside the card divs** (they'd inherit the rotation). +- **Body copy ≤ 2 lines per card** — tilted long paragraphs collapse into perspective blur. +- **Symmetric weight** — same width, same vertical center, similar line counts; asymmetry breaks the comparison metaphor. -## Combinations +## See also -- [card-morph-anchor.md](card-morph-anchor.md) — both cards could morph into a single unified shape afterward -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — numbers as the headline content for each side - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + `yoyo` for the idle bob -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`card-morph-anchor` (the pair can morph into one unified shape afterward) · `counting-dynamic-scale` (numbers as each side's headline) · `sine-wave-loop` (the idle form). diff --git a/skills/hyperframes-animation/rules/spring-pop-entrance.md b/skills/hyperframes-animation/rules/spring-pop-entrance.md index 9911cd83a..2debc1eee 100644 --- a/skills/hyperframes-animation/rules/spring-pop-entrance.md +++ b/skills/hyperframes-animation/rules/spring-pop-entrance.md @@ -7,80 +7,31 @@ metadata: # Spring-Pop Entrance -> **Smooth beats bouncy.** Per the motion doctrine (`references/motion-language.md`), this entrance **defaults to a smooth long-tail settle — `power3.out` (or `expo.out` for a faster arrival)** that decelerates cleanly into the resting size with **no overshoot**. Bouncy `back.out` overshoot is the **#1 instant turn-off** in agent-made videos and is almost never executed well; it is demoted here to a **rare, explicitly-playful exception** (a consumer / fun brand), never the default. When unsure, settle smoothly. +> **Smooth beats bouncy.** This entrance defaults to a smooth long-tail settle — `power3.out` (or `expo.out` for a faster front) — that decelerates cleanly into the resting size with **no overshoot**. Bouncy `back.out` is the **#1 instant turn-off** in agent-made videos and is almost never executed well; it is a rare, explicitly-playful exception (consumer / fun brand), never the default. When unsure, settle smoothly. -THE entrance primitive: an element (or a staggered group of them) arrives on screen by springing from nothing — `scale: 0 → 1`, optionally with a small `y` rise — riding a **smooth long-tail ease (`power3.out` default)** so it grows confidently into its resting size and settles without bouncing. This is **arrival**, not reaction. - -Explicitly distinct from [press-release-spring.md](press-release-spring.md): that rule is a click/press → release feedback chain (a press phase, then a spring recovery to `1.0`). This one has **no press phase** — there is no prior resting state, the element did not exist on screen, it springs into being. Many blueprints used to borrow `press-release-spring` to fake an entrance; reach for this instead. +THE entrance primitive: an element (or staggered group) arrives by springing from nothing — `scale: 0 → 1`, optional small `y` rise — and settles without bouncing. This is **arrival**, not reaction: distinct from [press-release-spring.md](press-release-spring.md) (a click/press → release feedback chain on an element that already rests on screen). Many blueprints used to borrow that rule to fake an entrance; reach for this instead. ## How It Works -A single `fromTo` carries the whole arrival: +One `fromTo` carries the whole arrival: from `{ scale: 0, opacity: 0 }` (explicit, so t=0 is correct under seek) to `{ scale: 1, opacity: 1, ease: "power3.out" }`. For a **group**, the same `fromTo` runs per element at `i * STAGGER`, capped so the group reads as one arriving beat. The `scale` grow is load-bearing; the `y` rise is garnish — drop everything else and it must still read as a clean entrance. Let the ease produce the settle: never hand-key a `scale: 1.1` mid-state (it double-bounces against the curve). -1. **From-state**: `{ scale: 0, opacity: 0 }` — the element is collapsed to a point and invisible. Stated explicitly in the `from` object so a seek to `t=0` lands the element in this exact state (never rely on a CSS-hidden start — see Critical Constraints). -2. **To-state (default)**: `{ scale: 1, opacity: 1, ease: "power3.out" }` — a long-tail decel that grows the element into its resting size and **settles smoothly, no overshoot**. Use `expo.out` instead for a punchier, faster-front arrival (still no bounce). This smooth settle is the house style; the bouncy `back.out` variant is the rare playful exception (see Variations). - -For a **group**, the same `fromTo` runs per element with a **deterministic, index-derived stagger** (`i * STAGGER`), and the total entry window is **capped** (`ITEM_COUNT × STAGGER ≤ ~0.5s`) so the group reads as one arriving beat, not a slow arpeggio. - -A small `y` rise (`y: 24 → 0`) layers a subtle "lifts into place" on top of the pop — optional garnish; the `scale` grow on a smooth ease is the load-bearing motion. (A `rotation` settle belongs only to the playful overshoot variant below.) - -## HTML +## Recipe ```html - -
-
{heroLabel}
-
+ +
{heroLabel}
- -
-
-
{itemA}
-
{itemB}
-
{itemC}
-
{itemD}
-
{itemE}
-
{itemF}
-
+
+
{itemA}
+
{itemB}
+
{itemC}
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; -} -.pop-hero { - display: grid; - place-items: center; - width: {heroSize}; - height: {heroSize}; - background: {heroBg}; - border-radius: HERO_RADIUS; - font-family: {font}; - font-weight: 900; - font-size: HERO_FONT_SIZE; - color: {heroTextColor}; - /* Pop scales around the center — see Critical Constraints */ - transform-origin: 50% 50%; +.pop-hero, +.pop-item { + transform-origin: 50% 50%; /* in-place pop; move to the source point for the anchored variation */ will-change: transform; } .pop-grid { @@ -89,84 +40,38 @@ A small `y` rise (`y: 24 → 0`) layers a subtle "lifts into place" on top of th gap: GRID_GAP; place-items: center; } -.pop-item { - display: grid; - place-items: center; - width: {itemSize}; - height: {itemSize}; - background: {itemBg}; - border-radius: ITEM_RADIUS; - font-family: {font}; - font-weight: 800; - font-size: ITEM_FONT_SIZE; - color: {itemTextColor}; - transform-origin: 50% 50%; - will-change: transform; -} ``` -## GSAP Timeline +```js +// Single hero pop — smooth long-tail settle, no overshoot. +tl.fromTo( + "#hero", + { scale: 0, opacity: 0 }, + { scale: 1, opacity: 1, duration: POP_DUR, ease: "power3.out" }, + ENTRY_AT, +); -```html - - +}); ``` ## Variations -### Calm settle (refined / enterprise / "premium calm") — default - -`power3.out`, no rotation, drop the `y` rise or keep it tiny (~12px). Reads as a confident, weighted settle — right for a hero wordmark or a single product shot landing. The safe default for premium / enterprise brands. - -### Firm settle (default product reveal) — default - -The everyday entrance. `power3.out` (or `expo.out` for a punchier front), optional `Y_RISE` ~24px. Clear, deliberate arrival that decelerates clean — the safe default for cards, icons, and callouts. **No overshoot.** - -### Bouncy pop (RARE — explicitly-playful only) - -The exception, not the default. **Only** for a deliberately playful register (a consumer / fun brand, a toy-like icon set) where a bounce is clearly the intent — never for product / enterprise / serious launch tone. Bouncy is the #1 turn-off and the agent rarely lands it, so reach for this knowingly and sparingly. Swap `power3.out` for `back.out(OVERSHOOT)` and (optionally) add a `rotation` settle so each element looks hand-placed: +- **Calm settle** (premium / enterprise): `power3.out`, no rotation, `Y_RISE` 0–12px — a weighted, confident landing for a hero wordmark or product shot. +- **Firm settle** (everyday default): `power3.out` or `expo.out` for a punchier front, `Y_RISE` ~24px — cards, icons, callouts. +- **Exact-physics settle**: when the settle IS the shot, swap the ease for `springEase({ response: 0.4 })` (critically damped) from `../adapters/gsap-easing-and-stagger.md` → Spring Eases; take `duration` from the helper. +- **Origin-anchored pop**: a callout growing out of a specific point (marker, pointer tip) sets `transform-origin` to that point (e.g. `0% 100%`) so `scale: 0 → 1` reads as "emerging from the source", not "inflating in place". +- **Pop into a held slot**: land the pop and hold still — no idle loop baked into the entrance. If the held frame genuinely needs life, hand off to [sine-wave-loop.md](sine-wave-loop.md) for subtle jitter on a separate later tween; prefer revealing the next element on its VO cue. +- **Bouncy pop (RARE — explicitly-playful only)**: swap the ease for `back.out(OVERSHOOT)` and optionally settle a small `rotation: ROT_FROM → 0` so elements look hand-placed. Only for a deliberately playful register — never product / enterprise / serious tone: ```js -// Playful exception only — default to power3.out (see above). tl.fromTo( el, { scale: 0, opacity: 0, rotation: ROT_FROM }, @@ -175,100 +80,28 @@ tl.fromTo( ); ``` -Keep `OVERSHOOT` modest even here (≤ ~2) — past that it reads as a cartoon wobble, not an arrival. Better still: the baked spring at `dampingFraction: 0.6–0.7` (`../adapters/gsap-easing-and-stagger.md` → Spring Eases) gives ~5–10% overshoot with a second-order settle that reads physical where `back.out` reads cartoon. +Even here keep `OVERSHOOT ≤ ~2` — past that it reads as cartoon wobble. Better still: the baked spring at `dampingFraction: 0.6–0.7` (same adapters doc) gives ~5–10% overshoot that reads physical where `back.out` reads cartoon. -### Origin-anchored pop (callout springs from a pointer / source) +## Values -When a callout should appear to grow out of a specific point (e.g. a station marker or pointer tip), set `transform-origin` to that point instead of center, so the `scale: 0 → 1` reads as "emerging from the source" rather than "inflating in place." - -```css -.callout { - transform-origin: 0% 100%; /* bottom-left = pointer tip; match to the anchor */ -} -``` - -### Pop into a held slot — then hold (jitter at most) - -When a popped element then **holds** an ongoing slot (a constellation node, a persistent badge), do **not** bake an idle loop into this entrance — it must stay finite. Land the pop and let it hold still; if the held frame genuinely needs life, hand off to [sine-wave-loop.md](sine-wave-loop.md) for **subtle jitter** (low amplitude) on a separate, later tween — not a breathing loop. Prefer revealing the next element on its VO cue over keeping this one animating. - -## How to Choose Values - -- **EASE** — the settle curve (the load-bearing decision) - - Default: **`power3.out`** — a smooth long-tail settle, no overshoot; the house style for product / enterprise / serious tone. Use `expo.out` for a punchier, faster-front arrival (still smooth). - - Exact-physics option: `springEase({ response: 0.4 })` (critically damped, ζ=1) from `../adapters/gsap-easing-and-stagger.md` → Spring Eases — the curve `power3.out` approximates, with a harder front and a longer settle tail; take `duration` from the helper. Use when the settle IS the shot (a wordmark landing, a final lockup). - - Playful exception only: `back.out(OVERSHOOT)` — see the Bouncy pop variation; reach for it only when a bounce is clearly the brand intent. - -- **OVERSHOOT** — `back.out(OVERSHOOT)` overshoot strength — **only used in the rare bouncy variant**; the smooth default has no overshoot dial - - Range (playful only): ~1.3 (barely) → ~2.0 (clearly bouncy) - - Constraints: keep ≤ ~2 — past that the overshoot exceeds the element's bounds and reads as a cartoon wobble, not an arrival. If you're not in the explicitly-playful case, don't use this — use `power3.out`. - -- **POP_DUR** — duration of each element's `scale: 0 → 1` tween - - Range: 0.4 – 0.7 s - - Effects: shorter = tight snap; longer = a looser, more floating pop - - Constraints: the main subject must be visible by **`t ≤ 0.5s`** — keep `ENTRY_AT + POP_DUR`'s readable midpoint early; don't let the hero finish arriving after the half-second mark - -- **STAGGER** — gap between successive items' start times (group only) - - Range: 0.04 – 0.08 s - - Effects: < 0.04 reads as a simultaneous chord; > 0.08 feels lazy / arpeggiated - - Constraints: **`ITEM_COUNT × STAGGER ≤ ~0.5s`** (the cap) — beyond that the group stops reading as one beat. Cap the per-item stagger for large groups: `STAGGER = min(0.06, 0.5 / ITEM_COUNT)` - -- **ITEM_COUNT** — number of elements in a group pop - - Range: 3 – 9 - - Effects: 3 = sparse; 9 = full grid. More than ~9 forces `STAGGER` so small the stagger vanishes — switch to a wipe/sweep reveal instead - -- **Y_RISE** — optional upward offset the element lifts from (`y: Y_RISE → 0`) - - Range: 0 (pure pop) – 32 px - - Effects: adds a subtle "lifts into place"; keep small so the `scale` pop stays dominant - - Constraints: 0 for the calm-settle variant; never large enough to read as a slide-up (that's a different primitive) - -- **ROT_FROM** — optional starting rotation, **playful (bouncy) variant only** (`rotation: ROT_FROM → 0`) - - Range: −10° – +10° - - Effects: a small tilt that resolves makes the element look hand-placed - - Constraints: derive sign/size deterministically from index if you want alternating tilt (e.g. `i % 2 ? 6 : -6`) — never `Math.random` - -- **ENTRY_AT / GROUP_ENTRY_AT** — timeline offset before the (group's) pop begins - - Range: 0 – 0.4 s - - Effects: > 0 gives a beat of quiet before the arrival; keep small so the subject still lands by `t ≤ 0.5s` - -### Geometry & tokens - -- **{heroSize} / {itemSize}** — footprints. A hero entrance should occupy a clearly readable share of the frame; group items size down so the grid fits with `GRID_GAP` breathing room. -- **HERO_RADIUS / ITEM_RADIUS** — `height × 0.15` (sharp) → `height / 2` (pill). -- **{heroBg} / {itemBg} / {\*TextColor}** — surface + label tokens; inherit from the composition palette. - -## Key Principles - -- **Smooth beats bouncy** — default to `power3.out` (or `expo.out`): a long-tail settle into `scale: 1`, no overshoot. Bouncy `back.out` is the rare, explicitly-playful exception (the #1 turn-off, and the agent rarely lands it). When unsure, settle smoothly. -- **fromTo, always** — the collapsed `{ scale: 0, opacity: 0 }` start is stated in the `from` object so a seek to `t=0` lands it exactly there. An entrance built on a CSS-hidden start (e.g. `opacity:0` in CSS + a `.to()`) flickers under HF seek — the element renders visible before the tween claims it. -- **Easing carries the motion, not keyframes** — let the ease produce the settle for free. Don't hand-key a `scale: 1.1` mid-state; that double-bounces and fights the curve. (And in the playful variant, the overshoot is a byproduct of `back.out`, not a hand-keyed bounce.) -- **The grow is the motion** — `scale` is load-bearing; the `y` rise (and, in the playful variant, the `rotation` settle) is garnish layered on top. If you drop everything but the `scale` grow, it should still read as a clean entrance. -- **Cap the stagger window** — a group must arrive inside ~0.5s total or it stops reading as one beat and starts reading as a slow list reveal. Derive the stagger from `ITEM_COUNT` so it self-caps. -- **Deterministic per index** — all stagger and any rotation/tilt variation comes from the loop index, never `Math.random` — the renderer must produce the identical frame on every seek. -- **Visible early** — the main subject must be on screen by `t ≤ 0.5s`. A hero that finishes arriving at `t=1s` wastes the opening beat. -- **Don't bake an idle loop here** — this entrance is finite. If the element then holds a slot, hand off to `sine-wave-loop` on a later tween; an infinite `repeat`/`yoyo` here breaks seek. +| token | range | notes | +| ---------- | ----------------------------------------- | ---------------------------------------------------------------- | +| EASE | `power3.out` default; `expo.out` punchier | `back.out(OVERSHOOT)` only in the playful variant | +| POP_DUR | 0.4–0.7s | shorter = tight snap; hero must be visible by **t ≤ 0.5s** | +| STAGGER | 0.04–0.08s | `min(0.06, 0.5 / ITEM_COUNT)` — self-caps the window | +| ITEM_COUNT | 3–9 | >9 makes the stagger vanish — switch to a wipe/sweep reveal | +| Y_RISE | 0–32px | small; never large enough to read as a slide-up | +| ROT_FROM | −10°–+10° | playful variant only; alternate sign by index (`i % 2 ? 6 : -6`) | +| ENTRY_AT | 0–0.4s | a beat of quiet, but keep the subject landing by t ≤ 0.5s | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Entrances use `fromTo`** — explicit `{ scale: 0, opacity: 0 }` from-state; never rely on a CSS-hidden starting state -- **No CSS `transition`** on popped elements — those interpolate independently of HF seek and cause flicker -- **No `repeat` / `yoyo` / infinite tweens** — this is a finite arrival; idle motion is a separate `sine-wave-loop` tween -- **No `Math.random` / `Date.now`** — stagger and tilt are index-derived and deterministic -- **GSAP transform aliases only**: `x`, `y`, `scale`, `rotation`. Never tween `width` / `height` / `left` / `top` -- **`transform-origin: 50% 50%`** for an in-place pop (default); set it to the source point only for the origin-anchored variation -- **Default ease `power3.out`** (smooth, no overshoot); `back.out(OVERSHOOT)` only in the explicitly-playful variant, and there keep **`OVERSHOOT ≤ ~2`** — beyond that it reads as a cartoon wobble, not an arrival -- **`ITEM_COUNT × STAGGER ≤ ~0.5s`** — the group must land inside one beat -- **`will-change: transform`** on popped elements, especially groups — many simultaneous spring tweens benefit from compositor hints +- Default ease `power3.out` (no overshoot); `back.out` only in the explicitly-playful variant, and there `OVERSHOOT ≤ ~2`. +- `ITEM_COUNT × STAGGER ≤ ~0.5s` — the group must land inside one beat. +- Entrances state the collapsed from-state in `fromTo` — never rely on a CSS-hidden start (it renders visible before the tween claims it under seek). +- `transform-origin: 50% 50%` for an in-place pop; the source point only for the anchored variation. +- This is a finite arrival — idle motion on a held element is a separate, later `sine-wave-loop` tween. -## Combinations +## See also -- [sine-wave-loop.md](sine-wave-loop.md) — at most **subtle jitter** on a held node/badge AFTER its pop lands (don't bake any loop into the entrance; and prefer a VO-timed reveal over ambient motion — see that rule's caution) -- [center-outward-expansion.md](center-outward-expansion.md) — elements pop in as they radiate from center to their slots -- [press-release-spring.md](press-release-spring.md) — the reaction counterpart: once popped in, a button can take a press→release; this rule supplies the arrival, that one the click feedback - -## Pairs with HF skills - -- `/hyperframes-animation` — `power3.out` settle (smooth default), `fromTo` entrances, deterministic stagger -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`center-outward-expansion` (pop while radiating to slots) · `press-release-spring` (the click-feedback counterpart) · `sine-wave-loop` (post-arrival jitter, sparingly). diff --git a/skills/hyperframes-animation/rules/stat-bars-and-fills.md b/skills/hyperframes-animation/rules/stat-bars-and-fills.md index 3201f02d2..58679e72c 100644 --- a/skills/hyperframes-animation/rules/stat-bars-and-fills.md +++ b/skills/hyperframes-animation/rules/stat-bars-and-fills.md @@ -16,9 +16,11 @@ The graphics that give a stat **visual weight** beside its number: a small bar c Don't mix blueprints between stats in one piece — that reads as inconsistent. -## 1 — Growth Bars (CSS `scaleY` stagger) +## Recipe -Bars grow from the baseline with a stagger; the last bar is the accent. +### 1 — Growth Bars (CSS `scaleY` stagger) + +Bars grow from the baseline with a stagger; the last bar is the accent. Heights are authored in CSS (inline height per bar); GSAP only reveals `scaleY: 0 → 1` — never animate `height`. ```css .bars { @@ -34,18 +36,15 @@ Bars grow from the baseline with a stagger; the last bar is the accent. transform-origin: bottom center; /* grow UP from the baseline, not from center */ } .bar:last-child { - background: #ffc300; -} /* accent the final/current bar */ + background: #ffc300; /* accent the final/current bar */ +} ``` ```js -// Heights are authored in CSS (e.g. inline height per bar); GSAP only reveals scaleY 0→1. tl.to(".bar", { scaleY: 1, duration: 0.7, ease: "power3.out", stagger: 0.08 }, 0.3); ``` -> Use `scaleY` (a transform), never animate `height` — height tweens are forbidden by the runtime. Set each bar's final height in CSS, scale from 0. - -## 2 — Progress Fill +### 2 — Progress Fill **Bar form** — `scaleX` from a left origin: @@ -73,7 +72,7 @@ const PCT = 0.92; // 92% tl.to(".fill", { scaleX: PCT, duration: 1.0, ease: "power2.out" }, 0.3); ``` -**Ring form** — measured stroke draw (delegates to [svg-path-draw.md](svg-path-draw.md)): +**Ring form** — measured stroke draw (mechanics in [svg-path-draw.md](svg-path-draw.md)): ```js const ring = document.querySelector("#ring"); @@ -84,7 +83,7 @@ ring.style.strokeDashoffset = LEN; // empty tl.to(ring, { strokeDashoffset: LEN * (1 - 0.92), duration: 1.1, ease: "power2.out" }, 0.3); ``` -## 3 — Star-Rating Fill (fractional) +### 3 — Star-Rating Fill (fractional) A gold star row revealed left-to-right to a fractional value (e.g. 4.6 / 5) via a clip wipe over a gold layer sitting on a gray layer. @@ -123,34 +122,24 @@ tl.to( ); ``` -## How to Choose Values +## Values -- **Bar count** — 4–6 reads as "a trend" without clutter; the last bar is the current/accent value. -- **Fill duration** — 0.8–1.2s, matched to the paired count-up so number and graphic land together (share the ease). -- **Accent hue** — exactly one; bars/fill/stars all use the same accent, the rest is muted. -- **Stagger** — 0.06–0.1s on bars; larger feels sluggish, 0 loses the build. - -## Key Principles - -- **Transforms only** — `scaleY` / `scaleX` / `clipPath`, never `width`/`height` tweens (runtime-forbidden). -- **Match the number's timing** — the fill and the count-up should peak together (same start + ease), so the stat resolves as one beat, not two. -- **Measure, don't hard-code** — ring length via `getTotalLength()`; a hard-coded circumference breaks if the radius changes. -- **One accent hue, consistent blueprint** — see `hyperframes-creative/references/data-in-motion.md`. +| token | range | notes | +| ------------- | ----------- | ----------------------------------------------------------------------------------- | +| bar count | 4–6 | reads as "a trend" without clutter; the last bar is the current/accent value | +| fill duration | 0.8–1.2s | matched to the paired count-up so number and graphic land together (share the ease) | +| stagger | 0.06–0.1s | larger feels sluggish, 0 loses the build | +| accent hue | exactly one | bars/fill/stars all use the same accent, the rest is muted | ## Critical Constraints -- **Timeline paused**; build synchronously; registry key = `data-composition-id`. -- **`onUpdate` (if pairing a counter) must be O(1)** — the runtime seeks frame-by-frame (see [counting-dynamic-scale.md](counting-dynamic-scale.md)). -- **No `height`/`width` tweens, no `repeat: -1`** — transforms + finite repeats only. -- **`transform-origin`** must be `bottom` (bars grow up) / `left` (bars/fills grow right) — default center origin scales from the middle and looks wrong. +- **`scaleY` / `scaleX` / `clipPath`, never `height`/`width` tweens** — author each bar's final height in CSS and scale from 0. +- **`transform-origin`** must be `bottom` (bars grow up) / `left` (fills grow right) — the default center origin scales from the middle and looks wrong. +- **`.fill` needs `width: 100%`** — a zero-width fill scaled by any factor is still invisible, and automated gates may miss it. +- **Measure, don't hard-code** — ring length via `getTotalLength()`; a hard-coded circumference breaks if the radius changes. +- **Match the number's timing** — the fill and the count-up peak together (same start + ease) so the stat resolves as one beat, not two; a paired counter's `onUpdate` must be O(1) (see [counting-dynamic-scale.md](counting-dynamic-scale.md)). +- **One accent hue, consistent blueprint** — see `hyperframes-creative/references/data-in-motion.md`. -## Combinations +## See also -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — the number beside the graphic (pair them; same ease/duration) -- [svg-path-draw.md](svg-path-draw.md) — the progress-ring draw mechanics - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + transform tweens -- `/hyperframes-creative` — `references/data-in-motion.md` (stat layout + visual weight) -- `/hyperframes-core` — composition wiring; the no-`width`/`height`-tween rule +`counting-dynamic-scale` (the number beside the graphic — same ease/duration) · `svg-path-draw` (progress-ring draw mechanics) · `hyperframes-creative/references/data-in-motion.md` (stat layout + visual weight). diff --git a/skills/hyperframes-animation/rules/svg-icon-enrichment.md b/skills/hyperframes-animation/rules/svg-icon-enrichment.md index 0be00b6fd..073d0fd0b 100644 --- a/skills/hyperframes-animation/rules/svg-icon-enrichment.md +++ b/skills/hyperframes-animation/rules/svg-icon-enrichment.md @@ -7,323 +7,135 @@ metadata: # SVG Icon Enrichment -Treats an SVG icon as a composition of animated PARTS, not an opaque image. Each meaningful internal element (a clock hand, scissor blade, recording dot, data line) gets its own GSAP-driven micro-animation. Distinct from [svg-path-draw](svg-path-draw.md) (which animates the OUTLINE drawing) — enrichment animates INTERNAL PARTS, ideally after the outline has drawn. +Treats an SVG icon as a composition of animated PARTS, not an opaque image. Each meaningful internal element (a clock hand, scissor blade, recording dot, data line) gets its own micro-animation, targeted by id. Distinct from [svg-path-draw](svg-path-draw.md) (which animates the OUTLINE drawing) — enrichment animates INTERNAL PARTS, ideally after the outline has drawn. -## How It Works +Four signature patterns: -The SVG is authored with named ``, ``, ``, or `` children. The GSAP timeline targets these by selector and applies one of 4 signature motion patterns: +| Pattern | Use For | Math | Tip | +| ----------- | ---------------------------------- | ------------------------------------- | ---------------------------------- | +| Rotation | Clock, gear, loader, dial | `rotate(deg cx cy)` attribute, linear | see the transform-center gotcha | +| Oscillation | Scissors, wings, toggle | `rotate(±sin·amp)` on opposing groups | opposite signs on the two parts | +| Pulse | Recording dot, heart, notification | `scale(1 + sin·amp)` + opacity | ring lags dot by π/2 for ripple | +| Dash flow | Cutting line, data stream | `strokeDashoffset` linear via time | negative for L→R, positive for R→L | -1. **Rotation** — clock hand, gear, loading spinner (`transform: rotate(deg)`) -2. **Oscillation** — scissor blades, wing flap, toggle (`transform: rotate(±sin*amp)` on opposing groups) -3. **Pulse** — recording dot, heart, notification (`scale + opacity` via sin) -4. **Dash flow** — moving dashes along a stroke, like a data stream (`strokeDashoffset` linear) +## ❗ The transform-center gotcha -All run inside the paused GSAP timeline so HF seeks deterministically. +**For rotation around an explicit point inside an SVG, use the SVG `transform` ATTRIBUTE, not CSS transform**: `el.setAttribute("transform", `rotate(${deg} ${cx} ${cy})`)`. The CSS combination `transform: rotate(...)` + `transform-origin: 60px 60px` + `transform-box: fill-box` interprets the origin in the element's OWN **bbox-local** coordinates, NOT viewBox coordinates. For a thin `` (whose bbox is the line's narrow envelope), `60 60` bbox-local is a point OUTSIDE the line — the hand flies along an off-center arc instead of rotating in place. Same trap for small inner shapes (a dot circle whose bbox is the small circle, not the full viewBox). -## HTML +**Scaling around a center point**: same attribute route — `el.setAttribute("transform", `translate(${cx} ${cy}) scale(${s}) translate(-${cx} -${cy})`)`. + +## Recipe ```html -
-
-
- - - - - - - - - - - - - - - - - - - -
-
{brandPhrase}
-
-
+ + + + + + + + ``` -## CSS +```js +// Pattern 1 — Rotation. Proxy tween → SVG transform attribute (explicit center, see gotcha). +const hand = document.getElementById("hand-min"); +const minState = { deg: 0 }; +tl.to( + minState, + { + deg: 360 * MIN_REVOLUTIONS, + duration: TOTAL_DURATION, + ease: "none", // linear motion is the point + onUpdate: () => hand.setAttribute("transform", `rotate(${minState.deg} 60 60)`), + }, + 0, +); +// second hand: same shape with SEC_REVOLUTIONS (visibly faster). -```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; - font-family: {font}; -} -.stack { - display: flex; - flex-direction: column; - align-items: center; - gap: 80px; -} -.row { - display: flex; - gap: 120px; -} -.icon-svg { - width: 320px; - height: 320px; - filter: drop-shadow(0 12px 32px {shadowColor}); -} -.clock-hand { - /* transform-origin in SVG must be in viewBox units, not pixels */ - transform-origin: 60px 60px; - transform-box: fill-box; -} -.brand { - font-size: 64px; - font-weight: 900; - letter-spacing: 14px; - text-transform: uppercase; - color: {textColor}; -} +// Pattern 3 — Pulse. One phase proxy drives dot + ring, ring offset by π/2. +const dot = document.getElementById("rec-dot"); +const ring = document.getElementById("rec-ring"); +const pulse = { p: 0 }; +tl.to( + pulse, + { + p: Math.PI * 2 * PULSE_CYCLES, + duration: TOTAL_DURATION, + ease: "none", // sine handles the curve + onUpdate: () => { + const sD = 1 + Math.sin(pulse.p) * PULSE_DOT_AMP; + const sR = 1 + Math.sin(pulse.p + Math.PI / 2) * PULSE_RING_AMP; + dot.setAttribute("transform", `translate(60 60) scale(${sD}) translate(-60 -60)`); + ring.setAttribute("transform", `translate(60 60) scale(${sR}) translate(-60 -60)`); + ring.style.opacity = String( + PULSE_RING_OPACITY_BASE + Math.sin(pulse.p) * PULSE_RING_OPACITY_AMP, + ); + }, + }, + 0, +); + +// Pattern 4 — Dash flow. Linear offset tween on a dashed stroke. +const flowState = { offset: 0 }; +tl.to( + flowState, + { + offset: DASH_FLOW_TOTAL_OFFSET, // negative = L→R + duration: TOTAL_DURATION, + ease: "none", + onUpdate: () => { + document.getElementById("data-flow").style.strokeDashoffset = String(flowState.offset); + }, + }, + 0, +); ``` -## GSAP Timeline - -```html - - -``` - -## How to Choose Values - -- **MIN_REVOLUTIONS** — minute-hand revolutions across TOTAL_DURATION - - Range: 0.5–2.0 (continuous; faster reads as time-lapse) - - Constraints: avoid integer revolutions if visible end frame matters (lands back at start) - -- **SEC_REVOLUTIONS** — second-hand revolutions across TOTAL_DURATION - - Range: 4–10 (should be visibly faster than the minute hand) - - Constraints: SEC_REVOLUTIONS > MIN_REVOLUTIONS × 3 for the speed difference to read - -- **PULSE_CYCLES** — number of pulse cycles across TOTAL_DURATION - - Range: 2–4 over a 3–5 s comp - - Effects: ≥ 5 reads as anxious flicker; ≤ 1 reads as forgotten - -- **PULSE_DOT_AMP** — dot scale amplitude - - Range: 0.05–0.20 - - Effects: 0.05 = breathing; 0.20 = throbbing - -- **PULSE_RING_AMP** — ring scale amplitude (typically lower than DOT_AMP) - - Range: 0.04–0.12 - - Constraints: must be < PULSE_DOT_AMP or ring overshadows dot - -- **PULSE_RING_OPACITY_BASE / PULSE_RING_OPACITY_AMP** — ring opacity baseline + sine amplitude - - Range: BASE 0.4–0.6; AMP 0.3–0.5 - - Constraints: BASE − AMP ≥ 0 and BASE + AMP ≤ 1 - -- **DASH_FLOW_TOTAL_OFFSET** — total stroke-dashoffset change across TOTAL_DURATION - - Range: −400 to −100 (negative for L→R) or +100 to +400 (R→L) - - Effects: |large| = fast flow; |small| = slow drift - - Constraints: must be an integer multiple of the dash period (dash + gap) or the loop end frame shows a phase jump - -- **BRAND_AT** — when the brand phrase fades in - - Range: 0.3–1.0 s - - Effects: too early competes with icon entries; too late feels appended - -- **Ease family choices**: rotation = `none` (linear motion is the point); pulse driver = `none` (sine handles the curve); reveal of brand = `power3.out` - -## Signature Motion Patterns - -| Pattern | Use For | Math | Tip | -| ----------- | ---------------------------------- | ------------------------------------------------ | ----------------------------------- | -| Rotation | Clock, gear, loader, dial | `transform: rotate(deg)`, linear via sec-counter | `transform-origin` in viewBox units | -| Oscillation | Scissors, wings, toggle | `rotate(±sin*amp)` on opposing groups | Opposite signs on the two parts | -| Pulse | Recording dot, heart, notification | `scale(1 + sin*amp)` + opacity | Ring lags dot by π/2 for ripple | -| Dash flow | Cutting line, data stream | `strokeDashoffset` linear via time | Negative for L→R, positive for R→L | - ## Variations -### Stroke draw → enrichment chain +- **Stroke draw → enrichment chain** — draw the outline first via [svg-path-draw](svg-path-draw.md) (phase 1, `0 → OUTLINE_DUR`), then start enrichment at `OUTLINE_DUR`: the icon "wakes up" after assembly. +- **Per-icon entry stagger** — for a row of icons, each icon's enrichment starts as it fades in, not synchronized. -Draw the icon outline first (via [svg-path-draw](svg-path-draw.md)), THEN activate enrichment. The internal animation feels like "the icon woke up" after assembly. +## Values -```js -// Phase 1: outline draws (0 → OUTLINE_DUR) -tl.fromTo( - "#icon-outline", - { strokeDashoffset: 360 }, - { strokeDashoffset: 0, duration: OUTLINE_DUR, ease: "power2.inOut" }, - 0, -); -// Phase 2: enrichment starts at OUTLINE_DUR -``` - -### Per-icon entry stagger - -For a row of icons all animating, stagger their entries. Each icon's enrichment starts as it fades in, not synchronized — feels organic. - -## Key Principles - -- **❗ For rotation around an explicit point inside SVG, use the SVG `transform` attribute, NOT CSS transform** — `el.setAttribute('transform', `rotate(${deg} ${cx} ${cy})`)`. The CSS combination `transform: rotate(...)` + `transform-origin: 60px 60px` + `transform-box: fill-box` interprets the origin in the element's OWN bbox-local coordinates, NOT in viewBox coordinates. For a thin `` (whose bbox is the line's narrow envelope), `60 60` in bbox-local refers to a point OUTSIDE the line, so the hand flies along an off-center arc instead of rotating in place. Same trap for small inner shapes (rec-dot circle whose bbox is the small circle, not the full viewBox). -- **For scaling around a center point inside SVG**, use `el.setAttribute('transform', `translate(${cx} ${cy}) scale(${s}) translate(-${cx} -${cy})`)`. Same reason — avoids the CSS bbox-local origin trap. -- **Run continuous animations inside the timeline** — never CSS `@keyframes` or `requestAnimationFrame`. Both desync from HF's frame-by-frame seek. -- **Amplitudes subtle** — icons are decorative, not headlines. Pulse scale within the ranges above; rotation speeds calibrated against composition length, not absolute time. -- **Multiple parts of the same icon at different phases** — clock minute vs second hand at different speeds, ring vs dot pulse offset by π/2. Pure-sync looks mechanical; phase-offset looks alive. -- **❗ Climax dwell ≥ 1 s** — if the enrichment is the headline beat, the composition must continue ≥ 1 s after the most dramatic moment. +| token | range | notes | +| ------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------- | +| MIN_REVOLUTIONS | 0.5–2.0 | avoid integer revolutions if the end frame is visible (lands back at start) | +| SEC_REVOLUTIONS | 4–10 | > MIN × 3 or the speed difference doesn't read | +| PULSE_CYCLES | 2–4 over a 3–5s comp | ≥5 reads as anxious flicker; ≤1 reads as forgotten | +| PULSE_DOT_AMP | 0.05–0.20 | 0.05 = breathing; 0.20 = throbbing | +| PULSE_RING_AMP | 0.04–0.12 | must be < PULSE_DOT_AMP or the ring overshadows the dot | +| PULSE_RING_OPACITY_BASE / \_AMP | 0.4–0.6 / 0.3–0.5 | BASE − AMP ≥ 0 and BASE + AMP ≤ 1 | +| DASH_FLOW_TOTAL_OFFSET | ±100–400 | must be an integer multiple of the dash period (dash + gap) or the end frame shows a phase jump | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `animation`** on SVG children — must be timeline-driven -- **`transform-origin` matters per child** — set explicitly per animated element -- **`stroke-linecap: round`** on flowing/dashed lines for clean dash edges -- **Target SVG children by id** — `document.getElementById` is fine; selector chains into `` work the same as HTML +- **The transform-center gotcha above** — SVG `transform` attribute for any rotation/scale around an explicit interior point; never CSS `transform-origin` + `transform-box: fill-box` on thin lines or small inner shapes. +- **No `requestAnimationFrame`** — like CSS animation, it desyncs from HF's frame-by-frame seek; continuous motion lives inside the timeline as linear proxy tweens. +- **Amplitudes subtle** — icons are decorative, not headlines; calibrate rotation speed against composition length, not absolute time. +- **Phase-offset the parts** — minute vs second hand at different speeds, ring lagging dot by π/2. Pure sync looks mechanical. +- **`stroke-linecap: round`** on flowing/dashed lines for clean dash edges. +- **Climax dwell ≥1s** — if the enrichment is the headline beat, the composition continues ≥1s after the most dramatic moment. -## Combinations +## See also -- [svg-path-draw.md](svg-path-draw.md) — outline draws first, enrichment activates second -- [orbit-3d-entry.md](orbit-3d-entry.md) — orbiting items are enriched icons (clock orbits a brand label) -- [sine-wave-loop.md](sine-wave-loop.md) — entire icon floats while internal parts animate - -## Pairs with HF skills - -- `/hyperframes-animation` — onUpdate writes transform/opacity per SVG child -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`svg-path-draw` (outline draws first, enrichment second) · `orbit-3d-entry` (orbiting items are enriched icons) · `sine-wave-loop` (the whole icon floats while internal parts animate). diff --git a/skills/hyperframes-animation/rules/svg-path-draw.md b/skills/hyperframes-animation/rules/svg-path-draw.md index 8dca3753d..218fc6214 100644 --- a/skills/hyperframes-animation/rules/svg-path-draw.md +++ b/skills/hyperframes-animation/rules/svg-path-draw.md @@ -7,205 +7,68 @@ metadata: # SVG Path Draw -Reveals an SVG shape by animating its stroke as if a pen were tracing it. The line appears to be drawn in real-time. +Reveals an SVG shape by animating its stroke as if a pen were tracing it. Two stroke properties together: **`stroke-dasharray = `** makes the entire path one dash; **`stroke-dashoffset`** starts at the path length (dash shifted fully out of view → invisible) and tweens to `0` (fully drawn). The length comes from the DOM API `path.getTotalLength()` — measured, never guessed. -## How It Works +Works on anything with a stroke: ``, ``, ``, ``, ``, ``, ``. -The trick uses two SVG stroke properties together: - -1. **`stroke-dasharray = `** — sets the dash pattern to a single dash equal to the path's total length, so the entire path is "one dash" -2. **`stroke-dashoffset`** — controls how much of the dash is shifted out of view. Start at `pathLength` (entire path is offset out → invisible), animate to `0` (no offset → fully drawn) - -The path length is computed via the DOM API `path.getTotalLength()`. - -## HTML +## Recipe ```html -
- - - - - - -
{Brand}
-
+ + + + + + ``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; - gap: 32px; -} - -.logo-mark { - width: 320px; - height: 320px; -} - .logo-mark path { - fill: none; + fill: none; /* outline-only draw — a fill would appear immediately and ruin the reveal */ stroke: {accentColor}; stroke-width: 12; - stroke-linecap: round; /* soften endpoints */ + stroke-linecap: round; /* softer endpoints */ stroke-linejoin: round; - /* Initial state: invisible. GSAP fills strokeDasharray + strokeDashoffset - based on each path's measured length. */ -} - -.brand-line { - font-family: {font}; - font-weight: 700; - font-size: 48px; - color: {textColor}; - opacity: 0; /* fades in after stroke completes */ - letter-spacing: 0.04em; } ``` -## GSAP Timeline +```js +// Setup: measure each path and set its dash pattern. Real measured geometry, not a magic number. +document.querySelectorAll(".logo-mark path").forEach((p) => { + const len = p.getTotalLength(); + p.style.strokeDasharray = `${len}`; + p.style.strokeDashoffset = `${len}`; +}); -```html - - +// Companion wordmark fades in only after the last stroke settles. +tl.to( + ".brand-line", + { opacity: 1, duration: BRAND_FADE_DUR, ease: "power1.out" }, + BRAND_FADE_START, +); ``` -## How to Choose Values - -- **SEGMENT_DRAW_DUR** — per-segment stroke duration - - Range: 0.3-0.8s - - Effects: low end reads as a fast snap (good for short segments); high end reads as a deliberate pen trace (good for long curves) - - Constraints: must be short enough that the total chain (last segment finish) ends before BRAND_FADE_START; longer than ~1s feels sluggish for a logo reveal - - Reference: short outline segments use ~0.5s - -- **FINAL_SEGMENT_DUR** — duration of the shortest / final segment - - Range: 0.25-0.6s - - Effects: should be proportional to segment length — a short connector drawn at SEGMENT_DRAW_DUR appears slower than its longer siblings - - Constraints: typically 60-80% of SEGMENT_DRAW_DUR when the segment is visibly shorter than the others - - Reference: a mid-bar that is roughly 2/3 the length of the verticals uses ~0.35s - -- **SEG_1_START** — first segment start time - - Range: 0-0.4s - - Effects: 0 starts immediately on play; >0 gives a brief beat of empty stage before motion - - Constraints: should be ≥ 0 - - Reference: a small lead-in of ~0.2s lets the viewer settle before motion - -- **SEG_2_START** — second segment start time - - Range: SEG_1_START + (0.5 \* SEGMENT_DRAW_DUR) to SEG_1_START + SEGMENT_DRAW_DUR - - Effects: closer to SEG_1_START + 0.5\*SEGMENT_DRAW_DUR feels rapid/overlapping; closer to SEG_1_START + SEGMENT_DRAW_DUR feels sequential - - Constraints: stagger ~70-80% of SEGMENT_DRAW_DUR reads as continuous motion (not 3 isolated animations) - - Reference: SEG_1_START + ~0.25s (about half of SEGMENT_DRAW_DUR) - -- **SEG_3_START** — third segment start time - - Range: SEG_2_START + (0.5 \* SEGMENT_DRAW_DUR) to SEG_2_START + SEGMENT_DRAW_DUR - - Effects: same as SEG_2_START — controls perceived rhythm - - Constraints: should preserve the same stagger ratio used between SEG_1 and SEG_2 - - Reference: SEG_2_START + ~0.4s - -- **BRAND_FADE_DUR** — wordmark fade-in duration - - Range: 0.3-0.8s - - Effects: low end snaps in (urgent); high end glides in (premium / branded) - - Constraints: must finish before the composition's `data-duration` ends - - Reference: a calm logo lockup uses ~0.5s - -- **BRAND_FADE_START** — wordmark fade-in start time - - Range: max(SEG_3_START + FINAL_SEGMENT_DUR, …) to that value + 0.4s - - Effects: starting exactly at last stroke end feels tightly chained; adding a small beat gives the strokes a moment to "settle" before the wordmark joins - - Constraints: MUST be ≥ SEG_3_START + FINAL_SEGMENT_DUR (otherwise wordmark appears during the draw and competes with it) - - Reference: SEG_3_START + FINAL_SEGMENT_DUR + ~0.2s - -Ease families used here are discrete choices, not tunable scalars: - -- **stroke draws** use `power2.out` — gentle deceleration mimics a hand lifting at end of stroke. Do NOT use `back.out` or `elastic.out` (pens don't bounce). -- **brand fade** uses `power1.out` — soft tail on an opacity tween. -- For a constant-speed "real pen" tracing feel, swap to `none` (see Variations). - ## Variations -### Rotation start point (start from top instead of 3 o'clock) - -By default, `` and `` start their stroke at 3 o'clock. Rotate the element to start from top: +- **Ring starting at 12 o'clock** — `` / `` strokes start at 3 o'clock by default; rotate the element `-90deg` so a progress ring draws from the top: ```html ` and `` start their stroke at 3 o'clock. Rotate the cy="100" r="60" id="ring" - style="transform-origin: 100px 100px; transform: rotate(-90deg);" + style="transform-origin: 100px 100px; transform: rotate(-90deg)" /> ``` -### Linear (constant-speed) draw - -Use `ease: 'none'` for steady-rate drawing (like an actual pen tracing): - -```js -tl.to("#path", { strokeDashoffset: 0, duration: SEGMENT_DRAW_DUR, ease: "none" }, SEG_1_START); -``` - -### Draw then fill - -For SVG shapes that have a fill color, animate fill opacity to come in AFTER the stroke completes: +- **Linear (constant-speed) draw** — `ease: "none"` for a steady-rate "real pen" trace. +- **Draw then fill** — for filled shapes, tween `fillOpacity: 0 → 1` AFTER the stroke completes (requires `fill-opacity: 0` initially and a real `fill` in CSS): ```js tl.to( @@ -242,33 +96,26 @@ tl.to( ); ``` -Requires `fill-opacity: 0` initially and a real `fill` color in CSS. +## Values -## Key Principles +| token | range | notes | +| ----------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- | +| SEGMENT_DRAW_DUR | 0.3–0.8s | fast snap vs deliberate pen trace; >~1s feels sluggish for a logo reveal | +| FINAL_SEGMENT_DUR | 60–80% of SEGMENT_DRAW_DUR | proportional to segment length — a short connector at full duration reads slower than its siblings | +| SEG_N_START | previous start + 70–80% of its duration | reads as continuous motion, not N isolated animations | +| SEG_1_START | 0–0.4s | a small ~0.2s lead-in lets the viewer settle before motion | +| BRAND_FADE_START | ≥ last stroke end (+ ~0.2s beat) | earlier and the wordmark competes with the draw | +| BRAND_FADE_DUR | 0.3–0.8s | snap (urgent) vs glide (premium) | -- **Set `strokeDasharray` to the path's `getTotalLength()` value**, not an arbitrary number — guessing means stroke will animate but not match the geometry -- **Start `strokeDashoffset` at the same length**, animate down to `0` -- **Measure inside the timeline setup, not at module top** — SVG may not be rendered when module code runs in some environments. In HF runtime this works at top because SVG is inline, but be safe -- **`stroke-linecap: round`** for softer endpoints (less abrupt finish) -- **For sequential multi-path draws, stagger by ~70-80% of the previous segment's duration** — eye reads it as continuous motion, not N separate animations -- **Don't pair with `back.out` or `elastic.out`** — bouncing strokes feel wrong (the pen wouldn't bounce) +Ease families are discrete choices: **stroke draws** use `power2.out` (a hand lifting at end of stroke) or `none` for constant speed — never `back.out` / `elastic.out` (pens don't bounce). **Fades** use `power1.out`. ## Critical Constraints -- **`fill: none` in CSS for outline-only draws** — otherwise the fill area appears immediately and ruins the reveal -- **Path length is measured in the browser**: requires SVG to be in the DOM. HF inline SVG is fine; loaded `` SVGs may not be -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **Works on**: ``, ``, ``, ``, ``, ``, `` (anything with a stroke) -- **For complex paths**, if `getTotalLength()` looks wrong, overestimate `strokeDasharray` slightly (e.g. `len * 1.05`) — too large is invisible during animation start (no visible gap), too small clips the end +- **`fill: none`** for outline-only draws — otherwise the fill appears immediately. +- **Dasharray/dashoffset = the measured `getTotalLength()`**, set at setup; requires the SVG in the DOM (inline SVG is fine; a loaded `` SVG is not). +- **Complex paths**: if `getTotalLength()` looks wrong, overestimate slightly (`len * 1.05`) — too large is invisible at animation start; too small clips the end. +- **Stagger multi-path draws at ~70–80%** of the previous segment's duration. -## Combinations +## See also -- [counting-dynamic-scale.md](counting-dynamic-scale.md) — pair: stroke draws an icon while a number counts up beside it -- [hacker-flip-3d.md](hacker-flip-3d.md) — pair: SVG logo draws, then a hacker-flipped wordmark reveals under it - -## Pairs with HF skills - -- `/hyperframes-animation` — timeline + stroke property tween -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`svg-icon-enrichment` (internal parts animate after the outline draws) · `counting-dynamic-scale` (stroke draws an icon while a number counts up) · `hacker-flip-3d` (logo draws, wordmark decodes beneath). diff --git a/skills/hyperframes-animation/rules/theme-crossfade-morph.md b/skills/hyperframes-animation/rules/theme-crossfade-morph.md new file mode 100644 index 000000000..0621633e2 --- /dev/null +++ b/skills/hyperframes-animation/rules/theme-crossfade-morph.md @@ -0,0 +1,126 @@ +--- +name: theme-crossfade-morph +description: Whole-theme in-place morph under a fixed anchor — background, typography, corner radii, icons, chrome and logos all blend simultaneously (~0.3s) through N pre-styled skins while one anchor element never moves. Recipe = stacked full layers + opacity crossfade, anchor rendered once on top. Seek-safe by construction. +metadata: + tags: theme, skin, crossfade, morph, anchor, reskin, cycle, ui, stacked-layers +--- + +# Theme Crossfade Morph + +The whole world re-skins while one thing holds still. A composer box cycles through four IDE themes; a checkout widget flips through brand skins — background, typography, corner radii, toolbar icons, footer logos all change **at once**, in place, in ~0.3s, N times — and through every flip one anchor element (the prompt string, the widget layout, the wordmark) **never moves**. The anchor's stillness is the rhetorical claim: _everything changes, this doesn't._ + +Boundary: [card-morph-anchor.md](card-morph-anchor.md) morphs **one container** between two shots — its dimensions, radius, and surface tween continuously. This rule re-skins an **entire scene** through **N discrete states**: nothing tweens property-by-property (fonts, icons, and logos can't interpolate); the "morph" is a fast simultaneous crossfade of complete pre-styled layers. ([scale-swap-transition.md](scale-swap-transition.md) swaps an element at center; here the surroundings swap and the element holds.) + +## How It Works + +1. **One skin = one complete layer.** Each theme state is a fully pre-styled, full-bleed layer (`position: absolute; inset: 0`) containing everything that changes: background, shell/chrome, toolbar icons, footer logos, typography. All `N_SKINS` layers exist in the DOM from `t=0`, stacked; skin 0 starts visible, the rest at `opacity: 0`. +2. **The morph is a crossfade.** At each boundary, two opposing opacity tweens run at the same timeline position over `MORPH_DUR` (~0.3s): outgoing `1 → 0`, incoming `0 → 1`. Because both layers are complete, every property "blends" simultaneously for free — including the un-tweenable ones (font families, icon glyphs, logos), which read as morphing precisely because everything else is mid-blend around them. +3. **The anchor renders once, on top.** The element that must not move lives in its own layer above all skins and is **excluded from every skin layer**. No transforms, no re-parenting, no per-skin restyle. +4. **Windows are precomputed.** `T_k = CYCLE_START + k × (SKIN_HOLD + MORPH_DUR)`. Steady cadence by default; hold the final skin longest when it's the resolve. + +The only animated property is `opacity` — which is why this rule is seek-safe with zero special machinery. + +## Recipe + +```html + +
+ +
…terminal chrome, mono type, footer badge…
+
+
…rounded composer, sans type, toolbar pills, logo…
+
+
…dark shell, its own chrome and footer…
+ + +
{anchorText}
+
+``` + +```css +.theme-stage { + position: absolute; + inset: 0; +} +.skin { + position: absolute; + inset: 0; + opacity: 0; + /* Each skin fully self-styled: its own background, fonts, radii, + icons, chrome, logos. Nothing inherited across skins. */ +} +.skin-0 { + opacity: 1; /* the opening state — matches the timeline's fromTo */ +} +.shell { + /* CRITICAL: shared geometry. The shell box (and any element that + "persists" across skins — toolbar row, footer row) sits at the SAME + coordinates in every skin, so mid-blend frames read as one UI + changing clothes, not two UIs ghosting. */ + position: absolute; + left: SHELL_LEFT; + top: SHELL_TOP; + width: SHELL_WIDTH; + height: SHELL_HEIGHT; +} +.anchor { + position: absolute; + z-index: 10; /* above every skin */ + left: ANCHOR_LEFT; + top: ANCHOR_TOP; + /* No transforms, no transitions — the stillness is load-bearing. */ +} +``` + +```js +const skins = gsap.utils.toArray(".skin"); + +// Boundary k→k+1 at T_k: outgoing fades down as incoming fades up — +// ONE simultaneous crossfade, everything blends at once. +skins.forEach((skin, k) => { + if (k === 0) return; // skin-0 is the opening state + const at = CYCLE_START + k * (SKIN_HOLD + MORPH_DUR); + tl.fromTo(skin, { opacity: 0 }, { opacity: 1, duration: MORPH_DUR, ease: "power2.inOut" }, at); + tl.to( + skins[k - 1], + { opacity: 0, duration: MORPH_DUR, ease: "power2.inOut" }, + at, // same position — the blend is simultaneous, never sequential + ); +}); + +// The anchor gets NO tweens. Its absence from the timeline is the point. +``` + +## Variations + +- **Anchor-typography reskin (per-layer copies)** — when the anchor's own type treatment must change with the theme (mono in the terminal skin, sans in the editor skin), each skin carries its own copy of the anchor at **pixel-identical geometry** and there is no separate top layer; the invariant shifts from "one element" to "one geometry." Verify the copies overlay exactly (screenshot two skins at 50% opacity) — a 2px baseline drift reads as the anchor flinching, which breaks the whole claim. +- **Skin-cycle tour with logo relay** — a large brand logo outside the anchored shell crossfades **in the same windows** as the skins (logo k with skin k, same `MORPH_DUR`). The paired swap sells "same product, every brand." +- **Washout finale** — after the last skin, a final low-key layer (faint dot-grid, blueprint wash) fades in while the last shell drops to ~0.25 opacity — the cycle resolves into a held diagram of itself. One extra window; the anchor may fade with the shell or hold full-strength. +- **Emphasis brake** — steady cadence for `N−1` skins, then hold the final skin 2–3× `SKIN_HOLD`; the cycle demonstrates breadth, the brake lands the resolve. Precompute the hold array; don't drift the cadence without cause. + +## Values + +| token | range | notes | +| --------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| N_SKINS | 3–5 | two is a before/after (consider `card-morph-anchor`); past five the cycle pads | +| SKIN_HOLD | 0.8–1.5s | long enough to register the logo/footer identity, short enough to keep the churn rhetorical | +| MORPH_DUR | 0.25–0.4s, ~0.3s canonical | faster reads as a hard cut; slower reads as a mushy dissolve with lingering double-exposure | +| CYCLE_START | ≥ anchor settle + a beat | after the anchor and skin-0 have fully registered | +| SHELL geometry | — | shell / toolbar / footer coordinates identical across skins; contents inside the slots differ freely | +| ANCHOR position | — | identical to the pixel across the scene (per-layer form: identical in every skin) | +| washout / brake | shell ~0.2–0.3 opacity; hold 2–3× SKIN_HOLD | — | + +## Critical Constraints + +- **The anchor never moves.** No transforms, no opacity dips, no re-parenting, no restyle — the contrast between total churn and total stillness is the entire device; one flinch and the shot becomes a slideshow. +- **Nothing tweens but `opacity`** — no `borderRadius` / `background` tweens; radii and colors change by being different in the next layer. Visibility via `opacity` only, never `display` / `visibility` toggles (they can't blend mid-fade). +- **Pixel-align the shared geometry** — mid-blend both skins are partially visible; aligned shells read as one UI changing clothes, misaligned shells ghost into two UIs. +- **Pre-style everything** — each skin is complete and static; no class toggling, no runtime restyle mid-tween. +- **Outgoing and incoming tweens share one timeline position** — a staggered blend flashes the stage background between skins. +- **Adjacent windows only** — skin k crossfades with k+1, never k+2; at no frame are three skins partially visible. +- **Camera static — always.** A push-in on top of a theme cycle destroys the stillness that makes the anchor read. +- **Hard cuts are the cheaper sibling** — if the states should _snap_, that's `discrete-text-sequence` territory; the ~0.3s blend is specifically the "morph" read. + +## See also + +`context-sensitive-cursor` (caret color switches at each `T_k`) · `discrete-text-sequence` (type the anchor first; or the hard-cut alternative) · `card-morph-anchor` (the single-container sibling) · `spring-pop-entrance` (the lockup that joins the anchor at the resolve) · `sine-wave-loop` (drifting field under the cycle — never on the anchor). diff --git a/skills/hyperframes-animation/rules/vertical-spring-ticker.md b/skills/hyperframes-animation/rules/vertical-spring-ticker.md index 88bbd17ec..55e22ca56 100644 --- a/skills/hyperframes-animation/rules/vertical-spring-ticker.md +++ b/skills/hyperframes-animation/rules/vertical-spring-ticker.md @@ -7,233 +7,98 @@ metadata: # Vertical Spring Ticker (Slot Machine) -Multiple spring tweens are ADDED TOGETHER to produce total Y translation. Each spring contributes one discrete "step." The combined motion has snappy distinct moves with natural settling — instead of a single linear scroll, you get the slot-machine "click click click" rhythm. +Multiple spring tweens are ADDED TOGETHER to produce total Y translation — each spring contributes one discrete "step", so instead of a single linear scroll you get the slot-machine "click click click" rhythm with natural settling. Distinct from a continuous marquee: this rule's semantics are discrete steps that land; for endless linear motion see [sine-wave-loop.md](sine-wave-loop.md). ## How It Works -Container has fixed height `ITEM_HEIGHT`, `overflow: hidden`. Inside is a vertical stack of items, each also `ITEM_HEIGHT` tall. The translate of the inner stack is computed as: +A masked window of fixed height `ITEM_HEIGHT` (`overflow: hidden`) holds a vertical stack of items, each exactly `ITEM_HEIGHT` tall. Each spring holds a 0→1 progress; a shared `onUpdate` sums them and applies `translateY(-sum × ITEM_HEIGHT)`. Springs fire sequentially with overlap (`STEP_SPACING ≤ STEP_DUR`), so each step snaps in while the previous is still settling — that overlap is what makes them additive, and the `back.out` overshoot is what makes each step read as a "click". -``` -translateY = -ITEM_HEIGHT * sum(spring_i.progress for each spring) -``` - -Each spring fires at a different time, settles, then the next fires. When summed, the stack snaps forward step-by-step. The "spring" easing gives each step a tiny overshoot/settle that distinguishes it from a linear marquee. - -## HTML +## Recipe ```html -
-
-
{eyebrow}
-
-
- -
{item0}
-
{item1}
-
{item2}
-
{item3}
-
{itemN}
-
-
-
{footerLine}
+ +
+
+
{item0}
+
{item1}
+
{itemN}
``` -## CSS - ```css -.scene { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; - background: {bgColor}; - font-family: {font}; -} -.stack { - display: flex; - flex-direction: column; - align-items: center; - gap: STACK_GAP; -} -.eyebrow { - font-size: EYEBROW_FONT_SIZE; - font-weight: 800; - letter-spacing: 14px; - text-transform: uppercase; - color: {accentColor}; -} -/* MANDATORY: container height matches the per-item height exactly */ .ticker { width: TICKER_WIDTH; - height: ITEM_HEIGHT; /* MUST match .item height */ - overflow: hidden; - border-top: 2px solid {dividerColor}; - border-bottom: 2px solid {dividerColor}; - position: relative; + height: ITEM_HEIGHT; /* MUST match .item height exactly */ + overflow: hidden; /* the mask is the window */ } .stack-inner { display: flex; - flex-direction: column; /* MANDATORY for vertical ticker */ - will-change: transform; + flex-direction: column; /* mandatory — vertical stacking */ } .item { - height: ITEM_HEIGHT; /* MUST equal .ticker height */ + height: ITEM_HEIGHT; /* MUST equal .ticker height */ display: flex; align-items: center; justify-content: center; - font-size: ITEM_FONT_SIZE; - font-weight: 900; - letter-spacing: 8px; - text-transform: uppercase; - color: {textColor}; /* font-variant-numeric: tabular-nums; — for numeric tickers */ } -.brand { - font-size: BRAND_FONT_SIZE; - font-weight: 800; - letter-spacing: 10px; - color: {accentColor}; - text-transform: uppercase; +``` + +```js +const innerEl = document.getElementById("stack-inner"); +const springs = Array.from({ length: STEPS }, () => ({ p: 0 })); + +function applyTransform() { + const sumP = springs.reduce((acc, s) => acc + s.p, 0); + innerEl.style.transform = `translateY(${-sumP * ITEM_HEIGHT}px)`; } -``` +applyTransform(); // initial state -## GSAP Timeline - -```html - - +}); ``` -## How to Choose Values - -- **ITEM_HEIGHT** — px height of each ticker slot AND the masked window. - - Range: ~`ITEM_FONT_SIZE × 1.25`; the line must hold capital descenders without clipping - - Constraints: **`.ticker` height MUST equal `.item` height** exactly — mismatched values cause partial items to peek above/below the mask - - Reference: ../../examples/proof-logo-chain.html uses `204px` -- **TICKER_WIDTH** — px width of the masked window. - - Range: wide enough to hold the longest item without ellipsis; typically 30-60% of viewport width -- **STEPS** — number of additive springs (number of state transitions, not number of items). - - Range: typically 1-4; each step = one "click" in the slot-machine cadence - - Constraints: `STEPS ≤ itemCount − 1` (you can only roll as far as there are items below the visible one) - - Reference: ../../examples/proof-logo-chain.html uses `1` (single roll between two states) -- **STEP_DUR** — duration of each spring tween. - - Range: 0.3-0.7s; under 0.3 the overshoot is invisible, over 0.7 the click reads as a slide - - Reference: ../../examples/proof-logo-chain.html uses `0.45s` -- **STEP_SPACING** — seconds between consecutive springs' start times. - - Range: 0.3-0.5s; closer and the steps blur together (looks like linear scroll), further and the ticker feels lazy - - Constraints: `STEP_SPACING ≤ STEP_DUR` so the previous step is still settling when the next fires (this is what makes them "additive") -- **STEP_START** — when the first spring fires. - - Range: 0+; gate behind any preceding beat -- **BOUNCE_FACTOR** — `back.out(BOUNCE_FACTOR)` overshoot strength per step. - - Range: 1.4 (gentle click) → 2.0 (firm click) → 2.5+ (cartoony spin-and-land for a climax step) - - Effects: low end reads as polished UI, high end reads as casino / game show -- **BRAND_DELAY** — gap after the final step before the footer line reveals, in seconds. - - Range: 0.2-0.5s; lets the final overshoot settle before the next element competes for attention -- **BRAND_FADE_DUR** — footer fade-in duration. - - Range: 0.4-0.7s -- **BRAND_Y** — initial vertical offset of the footer before fade-up (in px). - - Range: 8-24 px; bigger feels "punched in," smaller feels gentle -- **EYEBROW_FONT_SIZE / ITEM_FONT_SIZE / BRAND_FONT_SIZE / STACK_GAP** — typographic + layout scaling. - - Constraints: items are the focal beat, sized 4-8× larger than eyebrow/footer -- **{bgColor} / {accentColor} / {textColor} / {dividerColor}** — semantic color tokens; accent reserved for the eyebrow and footer so the ticker items stay neutral. -- **{font}** — base typography stack. For numeric tickers add `font-variant-numeric: tabular-nums` so digit widths stay constant. - ## Variations -### Numeric ticker (price / counter rolling) +- **Numeric ticker (price / counter rolling)** — items are the digit sequence; run the same spring-step pattern per decimal position. `font-variant-numeric: tabular-nums` required. +- **Reverse direction (countdown)** — flip the sign (`translateY(${sumP * ITEM_HEIGHT}px)`) and arrange items in reverse order. +- **Pause between groups** — several fast steps (small `STEP_SPACING`), a long pause, then one dramatic final step with a bigger `BOUNCE_FACTOR`. The pause is where the eye locks in. +- **Continuous infinite ticker** — NOT this rule (this rule is discrete steps); a looping news ticker is a single linear tween with duplicated items — see [sine-wave-loop.md](sine-wave-loop.md) for continuous-motion semantics. -Replace text items with the digit sequence and use the same spring-step pattern per decimal position (units, tens, hundreds...). Add `font-variant-numeric: tabular-nums` for digit-width stability. +## Values -### Reverse direction (counting down) +| token | range | notes | +| ------------- | --------------------- | ------------------------------------------------------------------------------------- | +| ITEM_HEIGHT | ~`fontSize × 1.25` | must hold capital descenders; `.ticker` height MUST equal it exactly | +| TICKER_WIDTH | 30–60% viewport width | wide enough for the longest item without ellipsis | +| STEPS | 1–4 | number of transitions, not items; `STEPS ≤ itemCount − 1` | +| STEP_DUR | 0.3–0.7s | under 0.3 the overshoot is invisible; over 0.7 the click reads as a slide | +| STEP_SPACING | 0.3–0.5s | **≤ STEP_DUR** so springs overlap (additive); wider gaps read as a lazy linear scroll | +| BOUNCE_FACTOR | 1.4–2.5 | 1.4 gentle click / 2.0 firm / 2.5+ casino spin-and-land for a climax step | -Swap the sign on the translate: `transform: translateY(${sumP * ITEM_HEIGHT}px)` and arrange items in reverse order. Reads as a countdown. - -### Continuous infinite ticker (no settling) - -Loop forever (e.g. news ticker) — use linear ease on a single long tween, duplicate the items list, reset when translation exceeds total height. NOT this rule — see [sine-wave-loop](sine-wave-loop.md) pattern for continuous motion vs this rule's discrete-step semantics. - -### Pause between groups - -For dramatic "spin then land" feel, group several fast spring steps (`STEP_SPACING` small) + a long `BRAND_DELAY`-style pause + a final dramatic step with bigger `BOUNCE_FACTOR`. The pause is where the eye locks in. - -## Key Principles - -- **Container height MUST equal item height** — otherwise items don't snap cleanly into the visible window. If container is 200px and items are 220px, every step shows a partial item edge above/below. -- **`overflow: hidden` on container, NOT on inner stack** — the mask is the window; the stack inside is free to extend below. -- **`flex-direction: column` on inner stack** — required for vertical stacking; row would make items horizontal. -- **Step spacing tighter than step duration** — overlap is what makes the springs additive and gives the "click click" cadence; non-overlapping steps read as a linear scroll. -- **`back.out` per step** — the overshoot is what makes each step feel like a "click." Linear ease or out-only ease loses the slot-machine feel. -- **Sum the springs in onUpdate, don't tween the final position directly** — this is the "additive" trick; each spring contributes its OWN snap, which is the slot-machine pacing. -- **❗ Don't update items via `innerHTML` between steps** — the ticker moves the SAME items via translate; replacing content makes the previous item visible AS the new one (broken illusion). -- **❗ Climax dwell ≥1s after final step** — see SKILL universal constraints. +Reference: `../../examples/proof-logo-chain.html` (204px, 1 step, 0.45s). ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition`** on stack-inner — competes with the additive transform -- **`will-change: transform`** on stack-inner — many small transform updates per second -- **All items same height (pixel-exact)** — mismatched heights cause cumulative drift -- **For numeric: `font-variant-numeric: tabular-nums`** — variable digit widths break alignment +- **Container height = item height, pixel-exact, all items equal** — mismatches show partial item edges above/below the mask and accumulate drift across steps. +- **`overflow: hidden` on the container, not the inner stack**; `flex-direction: column` on the stack. +- **Sum the springs in `onUpdate` — never tween the final position directly.** Each spring contributing its OWN snap is the slot-machine pacing. +- **Overlap steps and keep `back.out` per step** — non-overlapping steps or an out-only ease collapse into a linear scroll. +- **Never update items via `innerHTML` between steps** — the ticker moves the SAME items via translate; swapping content shows the previous item AS the new one (broken illusion). +- **Climax dwell ≥1s after the final step** (SKILL universal constraint). +- **`tabular-nums` for numeric tickers** — variable digit widths break alignment. -## Combinations +## See also -- [reactive-displacement.md](reactive-displacement.md) — ticker is "pushed" by an incoming element -- [scale-swap-transition.md](scale-swap-transition.md) — ticker scales out after settling on final state, scaled-in subtitle replaces it -- [press-release-spring.md](press-release-spring.md) — button press TRIGGERS the ticker spin - -## Pairs with HF skills - -- `/hyperframes-animation` — additive spring tweens via shared onUpdate -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +`reactive-displacement` (ticker pushed by an incoming element) · `scale-swap-transition` (ticker scales out after settling) · `press-release-spring` (button press triggers the spin). diff --git a/skills/hyperframes-animation/rules/viewport-change.md b/skills/hyperframes-animation/rules/viewport-change.md index 19df966bd..fe6faac8f 100644 --- a/skills/hyperframes-animation/rules/viewport-change.md +++ b/skills/hyperframes-animation/rules/viewport-change.md @@ -7,161 +7,74 @@ metadata: # Viewport Change (Virtual Camera) -Simulates camera effects (zoom / pan / focus-lock on a moving element) by transforming a wrapper around ALL scene content. The "world" moves opposite to the perceived camera. Distinct from [multi-phase-camera](multi-phase-camera.md) (which is 2-3 discrete phases + drift) — viewport-change is a single continuous zoom/pan, often used for focus-lock following a moving element. +Simulates camera effects (zoom / pan / focus-lock on a moving element) by transforming a wrapper around ALL scene content. The "world" moves opposite to the perceived camera. Distinct from [multi-phase-camera](multi-phase-camera.md) (2-3 discrete phases + drift) — viewport-change is a single continuous zoom/pan, often used for focus-lock following a moving element. ## How It Works -Camera intent → world transform: +Camera intent → world transform. Camera **pans right** → world `translateX(-distance)`; camera **zooms in** → world `scale(>1)`; camera **follows element X** → world `translateX(viewportCenter - elementWorldX)` per-frame. Get the sign right or everything moves the wrong way. The single `.world` wrapper holds the camera transform; elements inside are positioned in world space, unchanged. -- Camera **pans right** → world `translateX(-distance)` -- Camera **zooms in** → world `scale(>1)` -- Camera **follows element X** → world `translateX(viewportCenter - elementWorldX)` updated per-frame - -The wrapper holds the camera transform; the elements inside are positioned in "world space" unchanged. - -**Single-element composite transform (this rule's form).** Both scale and translate live on ONE wrapper as `translate(x, y) scale(S)`. CSS applies scale FIRST, then translate (right-to-left matrix composition), so a point at world offset `(ox, oy)` lands on screen at `(S × ox + x, S × oy + y)`. To map the target to viewport center: +**Single-element composite transform (this rule's form).** Both scale and translate live on ONE wrapper as `translate(x, y) scale(S)`. CSS applies scale FIRST, then translate (right-to-left matrix composition), so a point at world offset `(ox, oy)` lands on screen at `(S × ox + x, S × oy + y)`. To map the target to viewport center, solve `S × offset + T = 0`: ``` T = -offset × S ``` -This is **different from [coordinate-target-zoom](coordinate-target-zoom.md)**, which uses two nested wrappers (outer scales, inner translates) and derives `T = -offset` (independent of S). Use this rule's single-wrapper form when you want one source of truth for camera state (`cam.scale`, `cam.x`, `cam.y`) updated via `onUpdate`; use nested wrappers when scale and translate can tween independently with shared ease. +This is **different from [coordinate-target-zoom](coordinate-target-zoom.md)**, which uses two nested wrappers (outer scales, inner translates) and derives `T = -offset` (independent of S). Mixing up the two forms drifts the target off-center as scale changes. Use this single-wrapper form when you want one source of truth for camera state (`cam.scale`, `cam.x`, `cam.y`) written via `onUpdate`; use nested wrappers when scale and translate can tween independently with shared ease. -## HTML +## Recipe ```html -
-
-
-
{Brand}
-
{tagline}
-
-
{ctaUrl}
-
-
+
+
+
{Brand}
+
{tagline}
+
{ctaUrl}
``` -## CSS - ```css .scene { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - background: {bgGradient}; - font-family: {font}; + overflow: hidden; /* REQUIRED — any non-1.0 scale reveals edges or pushes content off-frame */ + background: {bgGradient}; /* on .scene, NOT .world — a world-borne background warps with the camera */ } .world { position: absolute; inset: 0; display: grid; place-items: center; - transform-origin: 50% 50%; + transform-origin: 50% 50%; /* centered scaling is what the math assumes */ will-change: transform; } -.content { - display: flex; - flex-direction: column; - align-items: center; - gap: CONTENT_GAP; - text-align: center; -} -.hero { - font-size: HERO_FONT_SIZE; - font-weight: 900; - letter-spacing: HERO_LETTER_SPACING; - text-transform: uppercase; - color: {textColor}; -} -.tagline { - font-size: TAGLINE_FONT_SIZE; - font-weight: 600; - color: {labelColor}; -} -.cta { - display: inline-block; - padding: CTA_PADDING_Y CTA_PADDING_X; - font-family: {monoFont}; - font-size: CTA_FONT_SIZE; - font-weight: 700; - letter-spacing: CTA_LETTER_SPACING; - color: {accentColor}; - text-transform: uppercase; - background: {ctaBg}; - border: 1px solid {ctaBorder}; - border-radius: CTA_BORDER_RADIUS; -} ``` -## GSAP Timeline +```js +const world = document.getElementById("world"); -```html - - +tl.to( + cam, + { + scale: TARGET_SCALE, + y: counterY, + duration: ZOOM_DUR, + ease: "power3.inOut", + onUpdate: applyCamera, + }, + ZOOM_START, +); ``` ## Scale Value Guide @@ -174,32 +87,34 @@ This is **different from [coordinate-target-zoom](coordinate-target-zoom.md)**, | Dramatic | 1.5 - 2.5 | Element fills screen | | Full-screen | 3.0+ | Element covers viewport | -| Perception threshold | Result | -| -------------------- | -------------------- | -| < 5% | Imperceptible | -| 10-15% | Comfortable emphasis | -| > 30% | Cinematic / dramatic | +Perception: < 5% scale change is imperceptible; 10-15% is comfortable emphasis; > 30% is cinematic/dramatic. For a natural product feel, prefer 1.05-1.15× over 2-3s; save big > 1.3× zooms for dramatic narrative moments. + +### Extreme range — 4–12× outward (workspace reveal) + +The same single-cam math runs far past the table: a zoom-out workspace reveal opens punched-in at **4–12×** on one detail (a single cell, message, or button) and pulls out to the full workspace in one continuous move. The mechanics don't change — one `cam` object, `T = -offset × S`, one `applyCamera()` writer — only the authoring direction does: + +- **Build the workspace at its final (1×) layout and OPEN scaled-in** (`cam.scale = 8`, counter-translate aiming the opening detail; state it in a `fromTo` / seed via `applyCamera()` so a seek to t=0 lands punched-in). The wide landing frame is then everything at native design size — text crisp, raster assets at source resolution. +- **Never the inverse** — authoring the close-up at 1× and scaling the world down to 0.08–0.25 for the wide frame drops every label below legible pixel size and softens raster media; the reveal lands on mush. +- **Measure the opening target** — at S = 8, a 1 px error in the baked offset is 8 px on screen at the opening pose. Take the offset from the target's real laid-out center (`getBoundingClientRect` after `fonts.ready`, once at setup — the measuring doctrine in [coordinate-target-zoom.md](coordinate-target-zoom.md)), never from a layout formula. +- **The opening detail must survive ×S** — it renders at `S ×` its design size on the first frames (vector/DOM text is safe; raster needs `sourceResolution ≥ rendered × S`). ## Variations -### Focus-lock (camera follows moving cursor/character) - -For an element moving across the world, keep it at fixed screen X. Compute world offset per-frame: +- **Focus-lock (camera follows a moving cursor/character)** — keep the element at a fixed screen X by computing the world offset per-frame inside the driver's `onUpdate`: ```js const focusEl = document.querySelector(".moving-cursor"); -const targetScreenX = VIEWPORT_WIDTH * FOCUS_SCREEN_X_FRAC; +const targetScreenX = VIEWPORT_WIDTH * FOCUS_SCREEN_X_FRAC; // 0.4–0.7; 0.5 = dead center const focusUpdate = { p: 0 }; tl.to( focusUpdate, { p: 1, - duration: FOLLOW_DUR, + duration: FOLLOW_DUR, // matches how long the focused element is in motion ease: "power2.inOut", onUpdate: () => { const rect = focusEl.getBoundingClientRect(); - const focusWorldX = rect.left + rect.width / 2; - cam.x = targetScreenX - focusWorldX; + cam.x = targetScreenX - (rect.left + rect.width / 2); applyCamera(); }, }, @@ -207,143 +122,27 @@ tl.to( ); ``` -### Composite scale (multi-phase) +- **Composite scale (multi-phase)** — two proxy tweens multiplied through one writer: `cam.scale = scaleUp.v * scaleDown.v; applyCamera()`. Combine a slow push-in (~1.15) with a brief release (~0.9) for a breath/punch shape. +- **Camera mode transition (centered → follow)** — crossfade two camera modes via a 0→1 weight tween; intermediate frames interpolate between the modes' offsets. -Multiply two scale tweens for compound effects: +## Values -```js -const scaleUp = { v: 1 }; -const scaleDown = { v: 1 }; -function applyCompositeCamera() { - cam.scale = scaleUp.v * scaleDown.v; - applyCamera(); -} -tl.to( - scaleUp, - { v: SCALE_UP_TARGET, duration: SCALE_UP_DUR, onUpdate: applyCompositeCamera }, - SCALE_UP_START, -); -tl.to( - scaleDown, - { v: SCALE_DOWN_TARGET, duration: SCALE_DOWN_DUR, onUpdate: applyCompositeCamera }, - SCALE_DOWN_START, -); -``` - -### Camera mode transition (centered → follow) - -Crossfade between two camera modes via a 0→1 weight tween. At weight 0, mode A; at weight 1, mode B; intermediate is interpolated. - -## How to Choose Values - -### Layout (CSS) - -- **CONTENT_GAP** — vertical gap between hero, tagline, and CTA. - - Range: 16-48 px - - Effects: small → tightly stacked (logo-lockup feel); large → airy, editorial -- **HERO_FONT_SIZE / TAGLINE_FONT_SIZE / CTA_FONT_SIZE** — typographic hierarchy. - - Range: hero >> tagline > CTA (hero is the brand mark, CTA is the actionable footer) - - Constraints: hero must remain readable when scaled DOWN at neutral camera AND when scaled UP during the zoom — pick the size at neutral camera, the zoom only enlarges it -- **HERO_LETTER_SPACING / CTA_LETTER_SPACING** — uppercase tracking. - - Range: 4-10 px for uppercase display type; 0 for sentence case -- **CTA_PADDING_X / CTA_PADDING_Y / CTA_BORDER_RADIUS** — pill geometry around the CTA text. - - Constraints: `CTA_BORDER_RADIUS ≥ CTA_FONT_SIZE` to keep the pill ends fully rounded - -### Phase 1 — Content reveal - -- **HERO_START** — when the hero begins fading in. - - Range: 0.2-0.5s (small offset for a beat of black before content appears) -- **HERO_DUR** — hero fade-up duration. - - Range: 0.6-1.2s -- **HERO_Y** — initial Y offset of hero before fade-up (in px). - - Range: 16-48 px -- **TAGLINE_START** — when the tagline begins fading in. - - Constraints: `≥ HERO_START + 0.3` (let the hero land first so the eye reads top-down) -- **TAGLINE_DUR / TAGLINE_Y** — same shape as hero, typically smaller (`TAGLINE_Y` half of `HERO_Y`). - -### Phase 2 — Zoom - -- **TARGET_OFFSET_Y** — measured Y offset (in px) of the CTA from viewport center at neutral camera. - - Constraints: derived from layout, NOT a free parameter. Measure via `getBoundingClientRect()` OR compute from `CONTENT_GAP + (HERO_HEIGHT + TAGLINE_HEIGHT) / 2`. Sign matters — positive = below center. -- **TARGET_SCALE** — final magnification of the world. - - Range: 1.3× (modest) → 1.6-2.0× (typical CTA zoom) → 3×+ (cinematic) - - Constraints: raster source media needs `sourceResolution ≥ rendered × TARGET_SCALE`; text remains crisp at any scale -- **ZOOM_START** — when the zoom begins. - - Constraints: `≥ TAGLINE_START + TAGLINE_DUR + viewer-scan-time` (give viewer ~0.5s after content lands before camera moves) -- **ZOOM_DUR** — duration of the zoom tween. - - Range: 1.0-2.0s; under 0.8s feels like a teleport, over 2.5s drags - -### Phase 3 — CTA reveal + dwell - -- **CTA_REVEAL_START** — when the CTA pops in. - - Constraints: `≥ ZOOM_START + ZOOM_DUR × 0.9` (start near the end of the zoom so the CTA "lands" with the camera) -- **CTA_REVEAL_DUR** — CTA fade-in / pop duration. - - Range: 0.4-0.8s -- **CTA_REVEAL_SCALE** — initial scale of the CTA before pop. - - Range: 0.85-0.95 (sub-1 → grows into place); >1.0 inverts to a shrink-into-place feel -- **BOUNCE_FACTOR** — overshoot coefficient for `back.out(${BOUNCE_FACTOR})`. - - Range: 1.2-2.5; lower = subtle settle, higher = pronounced overshoot. The ease family (`back.out`) is the choice; this number tunes its intensity. - - Reference: ease family options: `back.out` (overshoot then settle), `elastic.out` (oscillation), `power3.out` (clean decel, no overshoot) -- **DWELL_DUR** — implicit hold after `CTA_REVEAL_START + CTA_REVEAL_DUR` until `data-duration` ends. - - Range: ≥ 1.0s (see "Climax dwell" in Key Principles) - -### Focus-lock variation - -- **VIEWPORT_WIDTH** — composition width in px. Real value (`data-width` on the root); not abstract. -- **FOCUS_SCREEN_X_FRAC** — where on screen to lock the focused element. - - Range: 0.4-0.7 (rule of thirds positions); 0.5 is dead center -- **FOLLOW_START / FOLLOW_DUR** — when the follow-cam engages and for how long. - - Constraints: `FOLLOW_DUR` matches the duration the focused element is in motion - -### Composite-scale variation - -- **SCALE_UP_TARGET / SCALE_DOWN_TARGET** — multiplicative factors composed via `cam.scale = scaleUp.v * scaleDown.v`. - - Effects: combine a slow push-in (`SCALE_UP_TARGET` ~1.15) with a brief release (`SCALE_DOWN_TARGET` ~0.9) for a breath/punch shape -- **SCALE_UP_START / SCALE_UP_DUR / SCALE_DOWN_START / SCALE_DOWN_DUR** — phase timing for each multiplicand. - -### Color tokens - -- **{bgGradient}** — scene background (typically a dark radial vignette so edges fall off as zoom reveals them) -- **{textColor}** — hero text; highest contrast against `{bgGradient}` -- **{labelColor}** — tagline / secondary copy; one step softer than `{textColor}` -- **{accentColor}** — CTA text + border; reserved hue that pops on reveal -- **{ctaBg} / {ctaBorder}** — semi-transparent fills derived from `{accentColor}` (typical `rgba` at 10-15% / 35-45% alpha) - -### Font tokens - -- **{font}** — sans-serif body / hero stack (e.g. `"Inter", sans-serif`) -- **{monoFont}** — monospace CTA stack (e.g. `"JetBrains Mono", monospace`); reserved for the URL/code-like CTA so it reads as actionable - -## Key Principles - -- **World moves opposite to perceived camera** — pan camera right = `translateX(-x)` on the world wrapper. Get this sign right, otherwise everything moves the wrong way. -- **Single-wrapper transform order matters** — `translate(x, y) scale(S)` applies scale first; counter-translate is `T = -offset × S`. Mixing this up with the nested-wrapper form (`T = -offset`) drifts the target off-center as scale changes. -- **`overflow: hidden` on `.scene` REQUIRED** — at any non-1.0 scale the world transform reveals edges or pushes content off-frame. -- **`transform-origin: 50% 50%`** on the world wrapper — centered scaling is what the math assumes. -- **Background on `.scene`, NOT on `.world`** — if background is on the world, transforming the world warps/translates the background. -- **Single source of truth via `cam` object + `applyCamera()`** — when scale and translate both change, write them in ONE place. Otherwise the transform string composition order is unpredictable. -- **Subtle continuous motion > big sudden zoom** — for a feel-natural product video, use 1.05-1.15× zoom over 2-3s. Big > 1.3× zooms read as dramatic narrative moments, save them. -- **Climax dwell >=1s** — after the zoom settles, the comp must continue for >=1s so the viewer can read the focal point. +| token | range | notes | +| --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------- | +| TARGET_OFFSET_Y | measured, not a free parameter | target's offset from viewport center at neutral camera; measure via `getBoundingClientRect` | +| TARGET_SCALE | 1.3× modest → 1.6–2.0× typical → 3×+ | raster media needs `sourceResolution ≥ rendered × TARGET_SCALE` | +| ZOOM_START | content landed + ~0.5s scan time | let the viewer read before the camera moves | +| ZOOM_DUR | 1.0–2.0s | under 0.8s teleports, over 2.5s drags | +| DWELL | ≥ 1.0s after the zoom settles | the viewer must be able to read the focal point (climax dwell) | +| VIEWPORT_WIDTH | = the root's `data-width` | real value, not abstract | ## Critical Constraints -- **Timeline must be paused**: `gsap.timeline({ paused: true })` -- **Registry key = `data-composition-id`** -- **No CSS `transition` on `.world`** — competes with GSAP -- **`will-change: transform`** on `.world` -- **`overflow: hidden` on `.scene`** -- **`transform-origin: 50% 50%` on `.world`** -- **Background on `.scene`** — never on `.world` -- **Scale and translate share one `onUpdate`** — both read from `cam` and write the composite transform string together; never split them across tweens that touch `world.style.transform` directly +- **One `.world` wrapper carries the whole camera** — every scene element lives inside it; a second transformed wrapper is a second camera. +- **Single source of truth via the `cam` object + `applyCamera()`** — when scale and translate both change, write them in ONE place; never split them across tweens that touch `world.style.transform` directly (the transform string composition order becomes unpredictable). +- **Single-wrapper counter-translate is `T = -offset × S`** — don't import the nested-wrapper `T = -offset` formula. +- **`overflow: hidden` on `.scene`**; **`transform-origin: 50% 50%` on `.world`**; **background on `.scene`, never on `.world`**. -## Combinations +## See also -- [multi-phase-camera.md](multi-phase-camera.md) — viewport-change inside one phase of a multi-phase camera -- [coordinate-target-zoom.md](coordinate-target-zoom.md) — alternative for off-center zoom (nested wrappers, `T = -offset` form) -- [sine-wave-loop.md](sine-wave-loop.md) — idle micro-drift after viewport settles - -## Pairs with HF skills - -- `/hyperframes-animation` — single tween writing composite transform -- `/hyperframes-core` — composition wiring -- `/hyperframes-cli` — `hyperframes lint` +[coordinate-target-zoom.md](coordinate-target-zoom.md) (nested-wrapper alternative, `T = -offset`) · [multi-phase-camera.md](multi-phase-camera.md) (viewport-change inside one phase) · [sine-wave-loop.md](sine-wave-loop.md) (idle micro-drift after the viewport settles). diff --git a/skills/product-launch-video/references/story-design.md b/skills/product-launch-video/references/story-design.md index c1f683eca..7f7dbfc81 100644 --- a/skills/product-launch-video/references/story-design.md +++ b/skills/product-launch-video/references/story-design.md @@ -84,7 +84,7 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig ## The script bank — what each beat's VO sounds like -> Proven product-launch clips, each reversed into the one spoken line it implies. Grouped by **role → blueprint**. Real product names kept (swap in your own). Draft your beat's VO in the SHAPE of the matching pattern. +> Proven product-launch clips, each reversed into the one spoken line it implies. Grouped by **role → blueprint**. Real product names kept (swap in your own). Draft your beat's VO in the SHAPE of the matching pattern. Kept 1:1 with the role declarations in `../hyperframes-animation/blueprints-index.md` — when a blueprint gains a role there, add its script shape here. ### HOOK @@ -117,6 +117,37 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig - Notion — "A doc? A database? A wiki? — no, it's all of them, in one place." - _Pattern:_ a "could be X, or Y, or Z?" cycle on one swapping word, then a hero claim crashes in and replaces it — "actually, this is what it is." +**prompt-type-submit-generate** — watch me ask + +- "Build me a landing page for my coffee brand — dark, minimal, launch-ready." +- "What if you could just… ask?" +- _Pattern:_ the VO speaks (or frames) the ask itself while the prompt types live — the question is the whole hook; the answer stays off-screen, or a second ask starts before the cut. + +**zoom-out-workspace-reveal** — the mystery re-scopes + +- "This isn't a finished animation — it's your canvas." +- _Pattern:_ near-silence or one slow tease over the close-up mystery, with the landing line timed to the zoom-out — the words answer "what am I looking at?" exactly when the workspace does. + +**fixed-anchor-cycle** — a roll-call around a pinned line + +- "For founders, for designers, for marketers, for teams — for anyone who ships." +- _Pattern:_ one static claim holds while its audience/option list cycles fast beneath it — the list is the sentence's swapping object, and the brand line lands after the cycle clears. + +**cursor-ui-demo** — a canvas already alive + +- "Your whole team is already in here — designing, commenting, shipping." +- _Pattern:_ a "we're all here working" line over an ambient multi-cursor canvas — presence, not features; the live activity is the proof. + +**dataviz-countup** — cold-open on one exploding stat + +- "One million users. Ninety days." +- _Pattern:_ ONE statistic counts up as the VO speaks it — scale alone carries the tension; no product, no context yet. + +**cta-morph-press** — a lone widget does its trick + +- "It starts as a search bar. It becomes your whole workflow." +- _Pattern:_ one small widget on a bare field morphs in place and performs its payload while the VO names the transformation — small thing, big claim, hand off to the title. + ### PROBLEM **kinetic-type-beats** — pain lands alone on a bare canvas @@ -170,6 +201,26 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig - "Watch it run — then look at what it saved: 14 hours, every week." - _Pattern:_ the product video plays, then slides aside to hand the frame to one impact stat — "see it work, now see what it's worth." +**prompt-type-submit-generate** — something you talk to + +- "Meet Ada — describe what you need, attach what you have, and let it run." +- _Pattern:_ the first look IS the composer — the VO introduces the product as a conversation partner while a long prompt types with attachments and option picks, ending on (or just after) the submit. + +**spatial-pan-stations** — decode the idea, then land on it live + +- "Capture, structure, publish — that's the idea. Here it is running." +- _Pattern:_ a labeled concept strip read station by station, the final pan bridging into the live demo — "here's the idea → here it is working." + +**device-surface-showcase** — introduced by completing its core loop + +- "Open a space, drop in your notes, hit share — that's it, that's the product." +- _Pattern:_ the product introduced by DOING its core loop once inside its real interface, stepwise and cursorless — the VO narrates the steps plainly and lands on "that's it." + +**titlecard-reveal** — a near-still title prelude + +- "Relay. Docs that write themselves. Let's look inside." +- _Pattern:_ 2–3 near-still cards seamed by blur-snap handoffs — name, one-line claim, then into the product; each card carries one short spoken phrase. + ### KEY_FEATURE **grid-card-assemble** — enumerate breadth at once @@ -204,6 +255,36 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig - "Here's the editor in action — and the result: a publish-ready cut in minutes." - _Pattern:_ a feature clip runs, then yields the frame to a metric / impact line — the clip proves it, the number lands it. +**prompt-type-submit-generate** — one ask, one answer + +- "Ask for revenue by region — and the chart draws itself." +- _Pattern:_ ONE prompt→response round trip — the VO names the ask, lets the status theater breathe a beat, then calls the result as it streams in. + +**agent-progress-theater** — the machine visibly works + +- "Kick off the scan — it checks every file, flags what's broken, and fixes what it can. Watch." +- _Pattern:_ trigger → working theater → receipt: the VO hands the work over, then reads the findings as rows land and check off — present tense, the machine is the subject. + +**panel-edit-live-sync** — one gesture, two surfaces + +- "Drag the value — the button follows. Pick a unit — the code converts. Live." +- _Pattern:_ 2–4 short cause→effect couplets, each pairing a gesture with its live mirror ("do X — Y answers"), ending held on the last edit. + +**transcript-scroll-artifact-reveal** — the work, then the deliverable + +- "It planned, researched, built, and tested — and left you the spreadsheet to prove it." +- _Pattern:_ a "look how much happened" line read down the transcript at traversal pace, then one pivot line cashes it in on the artifact — evidence first, payoff last. + +**camera-journey** — a cinematic flight over the result + +- "A month of content — planned, scheduled, and ready before you sat down." +- _Pattern:_ a flying camera explores the generated artifact while the VO makes one calm, sweeping claim; the content acts by itself — no hands, no clicks. + +**dataviz-countup** — the numbers prove the feature + +- "Response time, down 40%. Coverage, 3×. Every claim, a number." +- _Pattern:_ the feature proven by its metrics — a stat montage scrubbed / counted up while the VO reads each number as it lands. + ### BENEFITS **kinetic-type-beats** — rapid-fire value montage @@ -223,12 +304,33 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig - CSS Scan Pro — "A smart color picker — with instant tints and shades." - _Pattern:_ one clean two-line value title, one slide-up crossfade, then held still. Low motion is the point. +**camera-journey** — travel the value chain + +- "Leave one comment here — and the forecast updates over there." +- _Pattern:_ a cause→effect round trip: the VO's first half lands on the action, its second half on the payoff the camera travels to — "do this small thing here, get this big thing there." + +**zoom-out-workspace-reveal** — this is just one corner + +- "That one file it fixed? A corner of everything it already did." +- _Pattern:_ the VO tells the micro-story during the close-up dwell, then the scale line lands with the zoom-out — the benefit is breadth, revealed in one move. + +**fixed-anchor-cycle** — everything changes, this stays + +- "Same prompt. Every tool you own." +- _Pattern:_ one pinned claim while the entire surface re-skins around it — a short "works everywhere" line, then let the cycling themes speak. + +**cursor-ui-demo** — demo, value line, demo + +- "Watch it draft the reply — that's an hour back — now watch it file the ticket." +- _Pattern:_ a demo|text|demo sandwich — the VO alternates showing and telling, naming the value in the text beat between two live interactions. + ### SOCIAL_PROOF **constellation-hub** — the hub at the center of your stack - kyvos — "On any BI tool — Tableau, Looker, Power BI — Kyvos sits at the center of your stack." -- _Pattern:_ the product mark is the hub and partner logos orbit it — "sits at the center of everything you use." +- "Connects to thousands of apps — including every one you already use." +- _Pattern:_ the product mark is the hub and partner logos orbit it — "sits at the center of everything you use." In the scatter-drift end-card variant there is no hub and no ring: a serif claim holds center while app icons pop in scattered and drift slowly outward — breadth said with count and spread, not geometry. **grid-card-assemble** — a logo wall pulling back to a vast ecosystem @@ -242,6 +344,11 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig - Trumpet — "We supply the trumpet, you bring the band — loved by 1,000+ sales, success, and marketing teams." - _Pattern:_ wipe a busy open away to a clean lockup plus a "loved by N+ teams" line that settles and holds. +**dataviz-countup** — the numbers vouch + +- "Twelve thousand teams. 4.9 stars. 99.98% uptime." +- _Pattern:_ proof by count-up — adoption, rating, and scale metrics tick up as the VO reads them; the biggest number lands last. + ### CTA **kinetic-type-beats** — punchy closing line beat-by-beat @@ -263,6 +370,21 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig - Linear — "This is Linear. Start building — it's free." - _Pattern:_ the brand mark condenses straight into the single thing you click — "here's us → click here," no spatial set. +**prompt-type-submit-generate** — the command is the ask + +- "One command — npm install relay — and you're live." +- _Pattern:_ the closing invitation IS a typed install command — the headline demotes, the terminal pill types it out, and the VO speaks the command (or one short line over it); the card holds with only the caret blinking. + +**constellation-hub** — the orbit collapses into action + +- "Everything you use, one hub — click, and it's yours." +- _Pattern:_ the ecosystem ring collapses onto the core on one click that springs the product open — the VO turns "it connects everything" into the action line. + +**titlecard-reveal** — a calm end-card stack + +- "Try it free. No credit card. relay.app." +- _Pattern:_ 2–3 near-still cards hard-cut in sequence, one short closing phrase each, terminating on the held logo/URL — the calm is the confidence. + ### BRAND_OUTRO **kinetic-type-beats** — a verb barrage resolving on one word @@ -286,6 +408,12 @@ Step 3 only TAGS the candidate id and writes the shaped VO. Step 4 (visual desig - "For notes, for tasks, for plans, for teams — [brand] holds it all." - _Pattern:_ closing use-cases/verbs cycle through one slot, then the brand mark crashes in and owns the final frame. +**fixed-anchor-cycle** — the anchor holds, the words cycle + +- bolt.new — "Prompt, run, edit, deploy — enjoy." +- Anthropic — "Opus 4.6 — by Anthropic." +- _Pattern:_ the brand name sits immovable while tagline words or praise quotes cycle beside it — per-word highlight stepping, or an accelerating flurry — landing on the completed lockup. + --- ## VO_MODE handling