From 8b16f442b30e0d4f1b79d507381684f0f59f7a79 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 25 Aug 2026 13:01:06 -0700 Subject: [PATCH] feat: add vox explainer skill --- README.md | 1 + skills/vox-explainer/SKILL.md | 232 +++++++++ .../references/animation-overlap.md | 112 +++++ .../references/motion-continuity.md | 176 +++++++ .../references/plain-sight-script.md | 69 +++ .../references/plain-sight-topics.md | 49 ++ .../references/render-safe-seams.md | 64 +++ skills/vox-explainer/references/seam-gate.md | 104 ++++ .../velocity-matched-transitions-gsap.md | 264 ++++++++++ .../velocity-matched-transitions.md | 269 ++++++++++ .../references/vox-collage-layout.md | 107 ++++ .../references/vox-collage-motion.md | 78 +++ .../references/vox-text-overlays.md | 56 +++ skills/vox-explainer/scripts/seam-gate.mjs | 468 ++++++++++++++++++ skills/vox-explainer/scripts/seam-stamp.mjs | 152 ++++++ 15 files changed, 2201 insertions(+) create mode 100644 skills/vox-explainer/SKILL.md create mode 100644 skills/vox-explainer/references/animation-overlap.md create mode 100644 skills/vox-explainer/references/motion-continuity.md create mode 100644 skills/vox-explainer/references/plain-sight-script.md create mode 100644 skills/vox-explainer/references/plain-sight-topics.md create mode 100644 skills/vox-explainer/references/render-safe-seams.md create mode 100644 skills/vox-explainer/references/seam-gate.md create mode 100644 skills/vox-explainer/references/velocity-matched-transitions-gsap.md create mode 100644 skills/vox-explainer/references/velocity-matched-transitions.md create mode 100644 skills/vox-explainer/references/vox-collage-layout.md create mode 100644 skills/vox-explainer/references/vox-collage-motion.md create mode 100644 skills/vox-explainer/references/vox-text-overlays.md create mode 100644 skills/vox-explainer/scripts/seam-gate.mjs create mode 100644 skills/vox-explainer/scripts/seam-stamp.mjs diff --git a/README.md b/README.md index 04937ec..fbac1a4 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ directory under `skills/` is independent and can be installed on its own. | Skill | What it does | | --- | --- | | [`p5-paint-animation`](skills/p5-paint-animation/) | Turns text, photos, and short clips into deterministic p5.js handwriting, paint-on, and living-painting animations. | +| [`vox-explainer`](skills/vox-explainer/) | Builds 60–90 second, collage-style HyperFrames explainers from a topic, document, or link. | ## Install skills diff --git a/skills/vox-explainer/SKILL.md b/skills/vox-explainer/SKILL.md new file mode 100644 index 0000000..f33835e --- /dev/null +++ b/skills/vox-explainer/SKILL.md @@ -0,0 +1,232 @@ +--- +name: vox-explainer +description: "Create a 60–90 second, collage-style HyperFrames explainer from a hiding-in-plain-sight topic or supplied documents and links. Use for requests such as 'make a plain-sight explainer', 'Vox-style history of X', 'why is X everywhere?', or 'turn this document into an explainer'. Covers topic or source routing, research, script, static design approval, voiceover timing, motion continuity, build, and numeric quality gates." +--- + +# Plain-Sight Explainer (Gateway Workflow) + +Build against the HyperFrames composition contract (clips, `data-*` timing, +and the `#root` schema). Install the upstream HyperFrames skills with +`npx skills add heygen-com/hyperframes`, or scaffold with +`npx hyperframes init`, and read the generated project documentation first. + +Make a 60–90s explainer about something the viewer sees daily but has never +looked at. The film manufactures recognition first, then pays it off with a +causal history. Grammar measured from Vox's Cooper Black film; motion from the +same frame-by-frame calibration process. + +This is a Vox-inspired design vocabulary, not an affiliation or endorsement. +Do not use Vox logos or imply that Vox produced the result. + +## Trust boundary and requirements + +- Require Node.js 22 or newer for the bundled seam scripts. When they start a + preview server, `npx` may download the exact HyperFrames version pinned in + `scripts/seam-gate.mjs` from the npm registry. +- Read only the files and URLs the user supplies. Source mode may fetch public + citations and public-domain or openly licensed assets from declared sources. + Never send private documents, credentials, or private URLs to another + service. +- Ask before using any TTS or transcription API. State the provider, data sent, + required credential, and possible cost before the call. +- Write project source, downloaded assets, audio, contact sheets, and renders + only inside the user's chosen project directory. Do not publish or upload the + result automatically. +- The seam verifier starts a local preview server and an isolated Chrome + profile. Its `--url` and `--comp-url` inputs accept localhost only. A preview + may still load remote assets declared by the user's composition. + +## Reference map + +Read only the references needed for the current stage: + +- Topic selection: [plain-sight-topics.md](references/plain-sight-topics.md) +- Script and captions: [plain-sight-script.md](references/plain-sight-script.md) +- Layout and assets: [vox-collage-layout.md](references/vox-collage-layout.md) +- Text and highlights: [vox-text-overlays.md](references/vox-text-overlays.md) +- Collage motion: [vox-collage-motion.md](references/vox-collage-motion.md) +- Motion law and seam gate: [motion-continuity.md](references/motion-continuity.md) +- Transition catalog and code: [velocity-matched-transitions.md](references/velocity-matched-transitions.md) + and [velocity-matched-transitions-gsap.md](references/velocity-matched-transitions-gsap.md) +- Multi-stage element motion: [animation-overlap.md](references/animation-overlap.md) +- Render-safe seam mechanics: [render-safe-seams.md](references/render-safe-seams.md) +- Seam ledger and script usage: [seam-gate.md](references/seam-gate.md) + +## Pipeline (run in order; each gate blocks the next) + +1. **Entry router — two modes, one gate.** Read + [plain-sight-topics.md](references/plain-sight-topics.md). + - **Topic mode** (the user names a subject, or asks for ideas): the + four-part filter governs IDEATION and tie-breaking only. If a + user-chosen topic fails it, name which part fails and what the film + loses, offer passing alternatives — then build the film they asked for + if they confirm. The filter never refuses a chosen subject. + - **Source mode** (the user supplies material — documents: a memo, + strategy doc, report, paper, deck, transcript, PDF, or a folder of them; + or links: an article, docs page, repo, announcement): the filter does + not apply at all. After confirming the allowed network scope, read every + supplied file, fetch every supplied URL, and follow at most one public + citation hop when needed for a claim. When a citation is dead or truncated + (common in exported PDFs), do not guess the source — verify the claim + independently on the open web before it becomes a receipt, or cut it. + Then run the storyline mine. Person+date and PD-archive requirements are + waived; the mechanism beat is not. The audience is the + source's INTENDED audience, not "any viewer" — the FAMILIAR beat + calibrates to what they see daily. The VO never mentions the source — + see [plain-sight-script.md](references/plain-sight-script.md). + + **The storyline mine (source mode).** The source is ore, not an + outline — never film it section by section. Extract five things: + - **The on-ramp** — the recognition the intended audience already has, in + the source's own world (a habit, a number they see weekly, a thing on + their screen right now). + - **The tension** — the sharpest claim in the source that contradicts what + that audience assumes. + - **The turn** — the DIG's landing point: a person+date when the source + has one, otherwise the decision, number, or moment the story pivots on. + - **The mechanism** — the one claim the viewer can verify on screen + unaided. If the source only asserts, BUILD the demonstration (two-panel + compare, dots redistributed two ways, before/after at equal scale). + - **The receipts** — real artifacts from the source plus the open web + (charts, screenshots, filings, on-screen attributed quotes). Real-assets + rule still holds. + + Map those onto the structural spine; beats the source can't fill honestly + get cut, not faked. Multiple sources — or ONE source carrying + several candidate storylines — mine each, then pick ONE spine: a film + gets one storyline; everything else demotes to receipts. Deliver the + beat map for approval before drafting script. +2. **Script** — read [plain-sight-script.md](references/plain-sight-script.md). + Draft VO + beat map. GATE: every line passes the deletion test; + opener/closer rules hold. If ghost-writing for a specific person, their + voice skill outranks this one. +3. **Design pass** — read + [vox-collage-layout.md](references/vox-collage-layout.md). Build a contact + sheet (one frame per beat, 960×540 tiles, HTML → headless-Chrome screenshot) + from REAL assets. Deliver the sheet BEFORE building any comp. Iterate here + — it is 10x cheaper than comp notes. + + **LOOK GATE — check before delivering the sheet.** Use this collage grammar + without implying Vox affiliation. Verify, don't assume: + - Palette is the measured set — ink #1a1a1a, yellow #FFE619, process blue + #66CFFF, specimen blue #58BCEC, greys #F2F2F2 / #8d9399, ground #fbfaf8. + Treat bright blues as fills and accents. Use #3E87A8 or darker for small + text on white, and pass the renderer's contrast checks. + - Headlines are Archivo Black. + - Every frame is a named recipe from the layout reference (evidence stack, + zoom-isolation, two-panel compare, specimen grid, lower-third, newsprint + layering, circle reveal). Label each frame with its recipe on the sheet. + - The yellow circle appears as the recurring visual carrier. + + **The trap:** when the source document belongs to a company with its own + design system — or the project folder already holds a brand capture, + `frame.md`, or an earlier build in that brand — the pull is to inherit + those tokens. Do not. `brand-faithful` is a DIFFERENT workflow for + product-launch films. A brand skin here is an explicit user decision, never + a default. Ask; don't substitute. +4. **VO** — record or TTS. The TTS input file is a PRONUNCIATION script: + numbers written out ("eighteen thirty-nine"), names spelled as spoken. + Transcribe the result for word timestamps — audio is the clock; every cut, + pop, and highlight keys to a word time. A replacement VO = a full retime. + Captions transcribe the PERFORMED read verbatim, not the script doc. + TTS playbook: `npx hyperframes tts` first; if its bundled speech + stack fails (old Python, espeakng-loader abort), install espeak-ng and + call the Kokoro model directly with the CLI's cached model files. Use a + TTS API only after the user approves its provider, data sharing, credentials, + and cost. Then transcribe whatever produced the audio; the + timestamps are the clock regardless of the engine. +5. **Build** — one HyperFrames comp, one clip group per beat, quiet caption + rail (suppress cues wherever promoted on-screen text carries the words). + Read [vox-text-overlays.md](references/vox-text-overlays.md) and + [vox-collage-motion.md](references/vox-collage-motion.md) before writing timelines. +6. **QC gates (all numeric, all mandatory)** + - lint/check: 0 errors. + - Dead-time sweep: MAD every consecutive frame pair of the RENDER; any + still run >3s is a planning bug — add a staged reveal, not wobble. + - Seam measurement: for any zoom-isolation cut, render the last pre-swap + and first post-swap frames, detect the carrier in both, verify centers + within ~30px and sizes within ~10%. Fix by measuring, not eyeballing. + - Event-density: per beat, largest gap between authored timeline events + ≤3.0s (the motion reference explains why the MAD sweep cannot see + creep-only holds). + - Composition integrity: audit the final keyframe sheet against the layout + reference's hard rules, item by item, logged. + - Keyframe sheet: screenshot one frame per beat; look at it. + +## Technique floor (anti-slideshow gate — declare at beat-map time) + +Passing the defect gates does not make a film. A build that resolves every +beat to cards, side-by-sides, and lower-third text passes lint, seams, and +dead-time and still reads as a slideshow — the collage idiom lives in +techniques a cautious agent will never volunteer. So techniques are DECLARED +in the beat map, not improvised at build time: every beat names its layout +recipe AND its motion treatment from the motion reference's catalog, and the +film's distribution must clear this floor: + +- ≥1 zoom-isolation swap — the camera pushes INTO a photograph, isolates the + carrier, and cuts through it (numerically aligned per the motion reference). +- ≥1 inverse zoom-through arrival, spent on a payoff beat. +- ≥2 drive-pasts or scale traversals that exit at/through frame-fill. +- ≥1 background-dropped cutout (subject lifted off its plate) doing motion a + flat card cannot — docking into a grid, riding a drive-past, anchoring an + iso swap. +- ≥1 newsprint layering moment; the circle reveal (the brand carrier). +- CAPS: static-card / side-by-side layouts ≤ one third of beats; no two + consecutive beats share the same layout recipe or the same treatment. +- When a photograph with interior depth is on frame, the camera enters it + rather than drifting over the card row — flat drift over cards is the + slideshow tell. Entering is governed by the zoom-in contract below. + +**The zoom-in contract.** Pushing into a photograph promises the viewer the +payoff is INSIDE the image. Exactly two legal continuations: +1. **Background dropout** — the subject lifts off its plate and becomes the + carrier (iso swap, dock, drive-past); +2. **Full-frame hold + callouts** — the image holds at/near full-bleed while + annotations draw ON it (labels, arrows, attention windows). +A push into a photo that then cuts to an unrelated composition is banned — +that is a transition wearing a technique's clothes. If neither payoff serves +the beat, do not enter the photo. + +Contract boundaries (what counts as "entering"): a sustained push whose +origin sits inside the image and that reaches near-full-bleed. Seam-scale +zooms (~0.2s whole-scene mechanics) and sub-10% camera creeps are different +categories and are not entries. An element driving AT the camera is the +mirror of an entry and obeys the same contract (it must already be a +dropped-out carrier, never a flat card flying at the lens). A traversal may +exit through frame-fill only into the same image or into the beat its own +motion caused — never into an unrelated composition. + +QC: the beat map logs the treatment distribution table; the final keyframe +audit verifies each declared treatment actually appears on screen (the seam +verifier already measures the zoom-isolation numerically). A film that +cannot clear the floor honestly is a planning bug: re-plan the beats, do not +pad with decoration. When the floor's minimums and the zoom-in contract +conflict, the contract wins and the technique is cut — a missing technique is +a smaller defect than an unmotivated one. + +## Structural spine (weights of a 60s cut) + +This weighted spine is authoritative. THE FAMILY (see the script reference) is +the back half of FAMILIAR — more instances of the recognition, not a new beat. +THE ARTIFACT BEAT sits between STORY and MECHANISM in the script reference's +spine too; when the two references seem to disagree, this list wins. In source +mode, spine beats the source cannot fill honestly (SECOND WIND, the person in +DIG) are CUT and their weight redistributed — the cut-don't-fake rule outranks +the spine's completeness. + +FAMILIAR 15% → DIG (person + date named) 5% → STORY 15% → THE ARTIFACT BEAT +10% → MECHANISM the viewer verifies on screen 20% → SECOND WIND (a technology +removes a constraint) 15% → THESIS in the yellow circle 10% → CLOSE mirrors +the open 10%. + +## Non-negotiables inherited from the motion layer + +- [motion-continuity.md](references/motion-continuity.md) governs seams. Make + hard-looking swaps land inside a verified continuation seam; a literal + static-to-static cut is not exempt from the seam gate. +- Real assets only — PD archives (Wikimedia Commons API, LOC), flagged + recreations when a scan is missing. Recreating artifacts with shapes reads + as slop. +- Highlights always animate on per the text-overlay reference. +- Elements step at 12fps over smooth eases; camera moves stay smooth + per the collage-motion reference. diff --git a/skills/vox-explainer/references/animation-overlap.md b/skills/vox-explainer/references/animation-overlap.md new file mode 100644 index 0000000..19b6e23 --- /dev/null +++ b/skills/vox-explainer/references/animation-overlap.md @@ -0,0 +1,112 @@ +# Keyframe Overlap — the intra-object velocity law + +`motion-continuity`'s vector law says velocity must not die at a seam between SCENES. +This is the same law one level down: velocity must not die between two STAGES of one +element's own motion. Overlap the stages and an L-shaped move stops having a corner and +starts having an arc. + +**Why it exists.** A single element doing two things in sequence is the most common +motion unit in any film, and the default scheduling — stage two begins on the frame +stage one lands — puts a dead frame in the middle of it. That frame is what reads as +cheap. Overlapping costs nothing: same elements, same eases, one changed start time. + +## The rig + +One wrapper per stage, nested. The artwork is the leaf. Each wrapper owns exactly one +property so the transforms never fight. + +```html +
+
+
+
+
+``` + +In After Effects these wrappers are parented nulls (`Null 191` → `Null 190` → shape). +Convert an AE rig by mapping one null to one wrapper, in order. + +## The two invariants + +| | Value | Notes | +|---|---|---| +| **Overlap** | **39% of a stage** | stage 2 starts when stage 1 is 61% through (29f into a 75f stage @60) | +| **Ease** | **`cubic-bezier(.857, 0, .143, 1)`** | symmetric; peak velocity **7.0×** the average | + +**The ease is not optional and it is the half everyone drops.** After Effects' stock +Easy Ease is 33% influence — peak/mean **1.49**. Overlap two eases that gentle and you +get mush, not the effect. GSAP's `expo.inOut` (peak/mean `10·ln2` = 6.93) is a valid +drop-in for the measured curve. + +**Reading any speed graph into a bezier.** For a symmetric `cubic-bezier(a, 0, 1−a, 1)`, +midpoint velocity ÷ mean velocity = `1/(1−a)` exactly. Measure a graph's peak-to-mean +height ratio, invert, done. A 7× spike is `a = 0.857`. + +**Overlap range.** Below ~25% the corner hardens back toward a stop. Past ~50% the two +stages stop reading as two and become one diagonal. 35–45% is the usable band. + +## It is property-agnostic — this is the whole point + +Nothing in the law mentions position, corners, or the number two. Any two animatable +properties hand off, in either order: + +- position → position (any two directions, including both axes in one stage = a diagonal) +- position → scale / rotation / skew +- scale → rotation +- position → letter-spacing, blur, colour +- **and backwards**: rotation → position, letter-spacing → position + +Chain three or more stages by overlapping each with the previous. Non-transform +properties (tracking, blur, colour) apply to the leaf instead of a wrapper; the +scheduling is identical. + +## Make stage two do a job + +The strongest use is not decorative. Pick a second stage that earns its place in the +layout or the story: a word slides in, then **lifts to clear the space** the next lines +reveal into. The motion and the layout become one decision. Reach for a purely +decorative second stage last. + +## Apply it between elements too + +The same offset works across a boundary: start the next element's entry before the +current element finishes its move. That removes the dead beat between an arrival and the +reveal that follows it, and it is how a single-element technique scales to a scene. +Cascades: see [velocity-matched-transitions.md](velocity-matched-transitions.md) §6. + +## Implementation + +Drive stages from one analytic clock, not from chained tweens — nothing accumulates +across frames, so a cold seek lands on the same state the render produces. + +```js +var MOVE = 1.25, OVERLAP = 0.39, START2 = MOVE * (1 - OVERLAP); // 0.7625s +var EASE = cubicBezierEase(0.857, 0, 0.143, 1); // or "expo.inOut" + +function draw(t) { + var p1 = EASE(clamp01(t / MOVE)); + var p2 = EASE(clamp01((t - START2) / MOVE)); + inner.style.transform = "translate3d(" + (DX * (1 - p1)).toFixed(3) + "px,0,0)"; + outer.style.transform = "translate3d(0," + (DY * p2).toFixed(3) + "px,0)"; +} +``` + +Paint one frame synchronously at init. `requestAnimationFrame` does not fire while +`document.visibilityState` is `hidden`, and unset transforms render as a broken frame. + +## Verify + +**The composite speed between the two stages must never reach zero.** Sample the leaf's +position per frame across the hand-off and check the minimum: a butt-joined rig passes +through exactly 0, an overlapped one bottoms out well above it. That number is the +technique; "looks smoother" is not a check. + +Do not measure an ease off player seek timestamps — seeks snap to the frame grid +(`seek(1.422)` lands at 1.400), which manufactures error that is not there. Compare the +ease function, or sample on-grid only. + +## Provenance + +Derived by measuring an After Effects tutorial's rig, easing, and overlap frame +by frame. Treat the numeric values as a calibrated starting point and verify the +result in the current composition. diff --git a/skills/vox-explainer/references/motion-continuity.md b/skills/vox-explainer/references/motion-continuity.md new file mode 100644 index 0000000..a9d0c63 --- /dev/null +++ b/skills/vox-explainer/references/motion-continuity.md @@ -0,0 +1,176 @@ +# Motion Continuity (Gateway) + +Read this before composing any animation. It decides WHAT happens at every seam and how +every scene performs; the technique skills implement it. These rules supersede generic / +upstream motion guidance. The failure this prevents: scenes authored in isolation — the +eye's momentum dies at every cut, and scenes wobble in place between entry and exit. + +## Route map + +| Decision (this skill) | Implementation skill | +|---|---| +| Seam transition choice + parameters + code | [velocity-matched-transitions.md](velocity-matched-transitions.md) §1–5 | +| Text / element entry cascades | [velocity-matched-transitions.md](velocity-matched-transitions.md) §6 | +| In-scene group repositioning (no cut) | [velocity-matched-transitions.md](velocity-matched-transitions.md) §7 | +| ONE element running two+ animations in sequence | [animation-overlap.md](animation-overlap.md) | +| Seam render mechanics / white-flash guard | [render-safe-seams.md](render-safe-seams.md) | + +Authoring order: **vector ledger (`ledger.json`) → STAMP the master seams from it +(`scripts/seam-stamp.mjs --ledger ledger.json --write index.html`) → sustained-motion +route per phase → carriers and causes → build comps → VERIFY (`scripts/seam-gate.mjs`).** +Hand-author only Tier-A morphs/match-cuts. Stamped seams satisfy the generator's +hard invariants, but warnings and rendered output still require review. + +--- + +# Part 1 — The Seam Law + +## The Vector Law + +> How Scene A exits determines how Scene B enters: same axis, same direction, matched +> speed, cut mid-motion on both sides. + +1. **Axis** — x stays x, y stays y, Z stays Z. Never trade axes across a cut. +2. **Direction** — never mirror. On Z, direction = the SIGN of scale change: growing = + push (camera forward), shrinking = pull (camera back). A receding exit answered by a + grow-from-small entry is a mirrored vector — the most common violation, because + grow-from-small is the default element entrance. +3. **Speed** — entry initial velocity ≈ exit final velocity, via mirrored eases (exit + `power4.in` + entry `power4.out`, same distance and duration; the incoming side picks + up ≥50% through the notional path). See + [velocity-matched-transitions.md](velocity-matched-transitions.md). +4. **Phase** — the cut lands mid-motion on BOTH sides. Settling to rest before the cut, + or starting from rest after it, is a dead beat. + +## The Current + +Every film picks ONE dominant direction (house default: LEFT). Every ordinary seam uses +it. Other vectors are RESERVED — spending one means something: + +| Vector | Meaning | +|--------|---------| +| The current (LEFT) | "next beat" — neutral forward progress | +| Upward | elevation — a conclusion or reveal rises above what came before | +| Z forward (zoom-through) | pushing deeper into the same thought | +| Z backward (inverse zoom) | ARRIVAL — something bigger lands | +| Scale-burst (explode out) | leaving a world — a surface blasts past camera | + +- Never run consecutive seams in opposing directions — ping-pong reads as an error. +- A direction change needs a visible cause (click / bounce / impact) or a chapter boundary. + +## The Vector Ledger + +Write it before authoring any master timeline — as **`ledger.json` at the project root** +(schema: [seam-gate.md](seam-gate.md)). One row per seam: cut time, exit and entry vectors +(axis + signed direction; Z rows carry the scale sign), selectors, technique. Exit and +entry must match; if a row mismatches, fix the plan, not the easing. The verifier checks +row consistency statically before any runtime sampling. + +## Carriers + +The eye follows objects, not abstractions. The strongest seams hand a concrete carrier +across the cut at matched position AND velocity: a cursor mid-path, a container that +shrinks/docks into the next layout, a mark that flies into its exact slot, the word group +of a staggered word-cut. With no natural carrier, the scene heroes carry it (partial travel + +early fade, entry mid-flight). Never a crossfade — it has no carrier at all. + +## Causal Motion + +Chain motion so each move is visibly launched by the last: click → squash → release +spring → flight → impact → recoil → reveal. + +- Effects start ON the causing frame — same timeline position, never "shortly after." +- Reactions scale with implied mass: big elements rebound slower, small ones snap. +- A force is a license to change direction; an uncaused flip is a ping-pong. + +## the seam verification gate (build gate — run the verifier, exit 0 or the seam is not done) + +```bash +node /scripts/seam-stamp.mjs --ledger ledger.json --write index.html # generate +node /scripts/seam-gate.mjs verify --ledger ledger.json --project . # verify +``` + +The script (usage + ledger schema: [seam-gate.md](seam-gate.md)) numerically enforces, per +seam: ledger-row consistency, exit still moving at the cut, entry mid-flight (never from +rest), measured direction = ledger direction, entry/exit speed match (WARN), **zero +overlap** (one side visible per frame — the cut is not a dissolve), the **Z sign** rule +(d(scale)/dt same sign both sides; the incoming scene's own entrances are scanned for +sign-fighting), and carrier rect continuity with ancestor scale included. Use +`seam-gate.mjs probe --t ` to find each seam's true carrier selectors when authoring +the ledger. + +Rules the script cannot check — still yours: + +1. **Edits re-open the seam.** Any change to a scene's first/last ~1s (including + re-timing to new VO) invalidates that boundary's audit — re-run the verifier. +2. **Audio is the clock.** Re-time scenes to the VO's real word timestamps; never rush a + read to fit a slot. A VO regen re-opens its seams. +3. **Clip-gating gotcha** (the usual cause of a zero-overlap FAIL): a clip whose + `data-start` precedes its entry tween is un-hidden at its initial opacity — set + initial `autoAlpha: 0` AND `data-start` = the cut time, never earlier. + +--- + +# Part 2 — Performance (the scene keeps performing) + +## No idle wobble + +Idle sine loops (breathe, float, drift, glow pulse) are BANNED as sustained motion — they +read as "the video is waiting." A scene that finishes entering with seconds left is a +planning bug: add story, not wobble. Every phase between entry and exit is owned by one +of these routes (name the route in the plan): + +| Route | What it is | +|---|---| +| **Staged reveals** | Hold content back; pay it off on narration beats — the frame keeps gaining information (default for ≥2 content groups) | +| **Camera with intent** | A mapped scale+pan path: establish wide → travel → arrive on the subject | +| **Sequenced UI life** | The product behaves over time: progress advances, highlights step, counts tick | +| **Animated sequences** | Elements act out a beat: a card files into a stack, an item gets dragged, a result assembles | + +Test: pause at any second — something meaningful must be mid-flight (a reveal landing, +the camera traveling, the UI doing what the narration says). + +**Film-texture exemption:** deterministic, seeded, sub-pixel-scale media texture — grain, +stop-motion boil on cutouts, gate weave + exposure flicker on archival photos — is GRADE, +not motion, and does not count as idle wobble or as a sustained-motion route. It never +touches text, never exceeds ~1px / ±0.05 stop, and obeys its own mode rules (gate weave: +footage mode / full-bleed only — 0.5px @ 8Hz + +±0.04 stop @ 10Hz, ignite at zoom start). A scene may not use texture to pass the +pause test — something narrative must still be mid-flight. + +## Stillness before climax + +Schedule a **0.3–0.75s pause** between the major action and its result — the dramatic +comma. A scene that jumps straight from action to result loses it. + +## Timing intents + +- Single entry ≤ ~800ms; longer buildup = multi-element stagger, not one slow element. +- Exit ≈ 75% of entry. Exception: velocity-matched-transitions inverts this (entry ~127% of exit). +- Total stagger ≤ 500ms; with 8+ elements, tighten per-item delay or stagger the first few. +- Forbidden eases: `bounce.out` / `elastic.out`. Entry overshoot `back.out(1.4–1.7)` is fine. +- Similar elements share one ease+duration intent — never a unique pair per element. + +## Transition vocabulary + +Use only 2–3 inter-scene transitions per film and repeat them; the default boundary is +**velocity-matched transitions in the current's direction**. Hand-written shared-element morphs +(`intent: morph`) don't count against the budget. + +--- + +## Anti-Patterns + +| Don't | Instead | +|-------|---------| +| Author each scene's entrance in isolation | Write the vector ledger first | +| Crossfade between scenes | Cut-the-curve in the current's direction | +| Exit completes, THEN the scene changes | Cut mid-motion on both sides | +| Entry starts from rest after a cut | Enter ≥50% through the notional path | +| Inverse-zoom exit → grow-from-small entry (or push → oversized retraction) | Match the scale-velocity sign (seam verification gate 7) | +| Incoming scene's own pop-in intro under a Z-seam handoff | Hold its opening frame composed, or match the sign | +| Idle wobble / breathe / float to fill time | Assign a sustained-motion route; or add story | +| Direction flip without a cause | Spend a force, or keep the current | +| Reserved vectors used as variety | Default to the current; spend them on meaning | +| Reaction a few frames after its cause | Same-frame ignition | +| Action jumps straight to result | Schedule stillness-before-climax (0.3–0.75s) | diff --git a/skills/vox-explainer/references/plain-sight-script.md b/skills/vox-explainer/references/plain-sight-script.md new file mode 100644 index 0000000..e99aeec --- /dev/null +++ b/skills/vox-explainer/references/plain-sight-script.md @@ -0,0 +1,69 @@ +# Plain-Sight Script + +~150 words per 60s at a casual read. A full 9-beat spine at honest +density is ~225 words ≈ 90s — that is the CEILING. If the tightest honest +cut still exceeds it, cut a beat (cut-don't-fake); never speed the read +past ~1.1x to fit. Write the observation first, then the +sentence in front of it. When ghost-writing for a named person, load their +voice skill first — it outranks everything here except facts. + +## Beat spine + +1. **FAMILIAR** — [ordinary moment, nothing at stake] → [the noticing]. The + on-ramp must be a habit anyone could swap into ("check your last ten + texts", "have you ever seen the map of your district"). Never invented + biography (no named relatives, hometowns, trips). The "I" is a camera any + viewer can climb into; specificity lives in the artifacts. +2. **THE FAMILY** — 3-4 more instances, one per phrase; nicknames and real + labels do the delight work. +3. **THE DIG** — one sentence: "So I looked into X - and it comes back to + [person]." The cut IS the transition; name the person here. Source mode + with person waived: the dig lands on the artifact instead — the code, + the map, the number ("...and it comes back to a zoning map drawn in + [year]"). Same shape, same cut. +4. **STORY** — date + place + the three facts, WITH the why (intent and + method before result: "redraws the districts to capture votes - packing + the opposition..."). +5. **THE ARTIFACT** — the one physical receipt held on screen (a patent page, + a scan, a first edition) while the VO states only what the eye confirms. +6. **MECHANISM** — the viewer-verifiable beat, written as what happens on + screen ("Draw the lines one way... draw them the other way"). +7. **SECOND WIND** — a constraint removed, stated as facts. Never a + personification pivot ("the map met the computer") or an adjective doing a + transition's job ("the trick got surgical"). +8. **THESIS** — the earned generalization, ≤2 sentences, zero metaphors. +9. **CLOSE** — end on facts the viewer doesn't have, mirroring the open. + Mirror-punchlines that restate the reveal ("It isn't an accident") are + banned — same disease as scaffolding openers. + +## The source is invisible + +When the input is a supplied document (memo, strategy doc, report, deck), the VO never +refers to it. No "the memo says", "there's a table in it", "according to the doc", no +quoting the source AS a source. Facts and quoted lines come through as the film's own +assertions; attributed quotes live on screen with the speaker's name, never in narration. +Test: if a line only parses for someone who has read the original, cut it. + +The document is the agent's input, not the viewer's context. + +## Banned everywhere (the tells) + +- Reveal-declaratives: "Nobody signed these", "It's a system". +- Source callbacks: "the memo", "the doc", "as written", "the four rows". +- Fake-candid openers: "Here's the trick", "Here's the thing". +- Restating what the demo just proved ("Different map." after showing it). +- Editorializing qualifiers: "the best answer ever given", "the honest way", + "even America". +- Anything failing the deletion test: delete each sentence; if the reader + loses no underivable fact, it stays deleted. + +## VO production + +- TTS input = pronunciation script: numbers as words ("nineteen sixty-three"), + names spelled phonetically. Display text keeps real spellings. +- Whisper-transcribe the TTS output: it is both the timing source AND a cheap + pronunciation audit. +- A replacement VO (e.g. a human ElevenLabs read) = a FULL retime; captions + update to the performed wording verbatim, including ad-libs. +- Single-line edits: TTS the one sentence, splice the audio, shift all + downstream times by the delta (raw step arrays too, not just wrapped times). diff --git a/skills/vox-explainer/references/plain-sight-topics.md b/skills/vox-explainer/references/plain-sight-topics.md new file mode 100644 index 0000000..cece6c6 --- /dev/null +++ b/skills/vox-explainer/references/plain-sight-topics.md @@ -0,0 +1,49 @@ +# Plain-Sight Topics + +The engine of these films is manufactured recognition: the viewer must be able +to say "wait, I see that every day" in the first ten seconds. Pick topics with +the filter, not by interestingness. + +**Scope:** this filter is for CHOOSING among candidate topics — ideation and +tie-breaking only. It is not an admission test: never use it to refuse or +stall a subject the user has picked, and it does not apply at all in source +mode (documents or links supplied — run the storyline mine in +[the gateway](../SKILL.md) instead). A failed filter on a chosen topic is a note +about what the film loses, not a block. + +## The four-part filter (all four required) + +1. **Hiding in plain sight.** The subject is ubiquitous in the viewer's own + life and has never been looked at. Test: could any viewer swap themselves + into the opener's on-ramp without changing a detail? Fonts, road signs, + the word OK, QWERTY, district maps: yes. Obscure subjects: no — they need + a different film. + A famous-but-MISDATED subject (everyone knows it, everyone dates it + wrong — driverless cars, "new" foods, "modern" slang) also passes, with + the hook inverted: "you assume this is new." Same recognition machinery, + opposite direction. +2. **A person and a date.** The dig must land on a maker within one beat — + three plot-relevant facts, no biography. (Oswald Cooper 1920, Margaret + Calvert 1963, Charles Gordon Greene 1839, Elbridge Gerry 1812.) +3. **A mechanism the viewer verifies on screen.** One beat must let the eye + confirm the claim unaided: the blur test that kills words but not + pictograms, ten dots redistricted two ways, the F drawn inside its + rectangle. If the core claim can only be asserted, the topic fails. +4. **Free archival assets.** The receipts must exist as PD/CC material + (Commons, LOC, patents, court records). Every scale claim becomes a real + artifact on screen. No assets → no film. + +## Bonus signals (break ties) + +- The origin is a SCENE (Woodland draws the barcode in beach sand; the + Gazette prints a monster) — scenes beat facts. +- A factual delight beat exists (the cow named Patience, the duck-shaped G, + hired actors pushing shopping carts). +- A technology later removed a constraint (phototype, the telegraph, + redistricting software) — that is the second-wind beat. +- The newsprint texture becomes literal (word/name coined in a newspaper). + +## Worked shortlist form + +Pitch topics as: subject — the recognition hook — person+date — the +verifiable mechanism — the assets. One line each. Ten at a time. diff --git a/skills/vox-explainer/references/render-safe-seams.md b/skills/vox-explainer/references/render-safe-seams.md new file mode 100644 index 0000000..e8e4f60 --- /dev/null +++ b/skills/vox-explainer/references/render-safe-seams.md @@ -0,0 +1,64 @@ +# Seam Craft — render prerequisites for scene-to-scene transitions + +This is the **render-correctness rules** for scene-to-scene seams: the +prerequisites and master-timeline mechanics that make any transition composite +correctly, independent of which specific transition is chosen. The per-transition +catalog (crossfade, push-slide, zoom-through, velocity-matched transitions, …) lives in +[velocity-matched-transitions.md](velocity-matched-transitions.md); this page contains +the rules underneath all of them. + +The transitions these rules govern are **Tier-B-ready**: pure transform / opacity / +filter on the two scene **clip wrappers** (`#el-`), no injected overlay DOM, no +per-scene cooperation. Overlay families (staggered blocks, blinds, light leak, grid +dissolve, page burn) and shader transitions are deferred to later phases. + +## Stage ground prerequisite (white-flash guard) + +Several templates open a window where the two wrappers' summed opacity < 1 (the +velocity-matched-transitions mid-window cut, zoom-through's 0.15 floor, plain crossfade's +power-curve dip). Whatever is BEHIND the wrappers shows through during that +window. If the assembled `index.html` `#root` has no opaque background, the +renderer composites the dip over its default **white** page → a white flash at +every seam, especially on dark films. +**The assembler must paint the stage:** `#root { background: +var(--canvas-deep, var(--canvas, #000)) }`. Any assembler or consumer of these +templates owns the same guarantee. + +## How the injector applies a transition + +At a `break` boundary between scene _i_ (`from`) and scene _i+1_ (`to`), the +injector: + +1. Extends `#el-` wrapper `data-duration` by `duration_s` (holds its final + frame — verified: `core/src/runtime/init.ts:1393-1410` external-slot branch). +2. Pulls `#el-` wrapper `data-start` earlier by `duration_s` (creates the + overlap window). +3. Reassigns **all** clip `data-track-index` as a 0/1 ping-pong so the two + overlapping wrappers never share a track (same-track overlap is illegal — + `core/src/lint/rules/composition.ts`). Higher track composites on top. +4. Stamps the `gsap_template` into `window.__timelines["main"]` at `T = overlap-start`. + +Verified by prototype render (2026-05-31): the master-timeline wrapper tween is +seeked and rendered (no double-seek with the sub-comp's own paused timeline — +the runtime drives them independently), the extended wrapper holds scene _i_'s +final frame, and the higher-track incoming wrapper composites over + blends with +the outgoing one. + +## Template placeholders + +The injector substitutes these tokens in each `gsap_template` line: + +| Token | Meaning | +| ---------------------------------- | ------------------------------------------------------------------------ | +| `__OLD__` | `"#el-"` — outgoing clip wrapper selector (quoted) | +| `__NEW__` | `"#el-"` — incoming clip wrapper selector (quoted) | +| `__T__` | overlap-start time in seconds (master clock) | +| `__DUR__` | `duration_s` for this boundary | +| `__DX__` | horizontal travel for directional types: `-1920` (LEFT) / `1920` (RIGHT) | +| `__DY__` | vertical travel: `-1080` (UP) / `1080` (DOWN) | +| `__ORIGIN_OUT__` / `__ORIGIN_IN__` | transformOrigin pair for `squeeze` | + +`filter` / `scaleX` / `transformOrigin` are lint-clean on the master timeline +(verified: `core/src/lint/rules/gsap.ts` has no per-property whitelist and scopes +its checks to `data-composition-id` ranges; the x/y/scale/rotation/opacity +whitelist is a _scene-worker_ prompt rule only — it does not bind index.html). diff --git a/skills/vox-explainer/references/seam-gate.md b/skills/vox-explainer/references/seam-gate.md new file mode 100644 index 0000000..e4bc6d9 --- /dev/null +++ b/skills/vox-explainer/references/seam-gate.md @@ -0,0 +1,104 @@ +# seam-stamp.mjs + seam-gate.mjs — usage + ledger schema + +Generate-and-verify pair for the seam verification gate. The scripts have zero npm +dependencies and require Node.js 22 or newer plus local Chrome. The verifier finds a +Puppeteer-cached Chrome or system Chrome, launches it with an isolated temporary profile, +and accepts only localhost preview URLs. + +```bash +# STAMP: write the master seam block (base sets + all wrapper tweens) from the ledger. +# Replaces the // … // block (inserts after the +# window.__timelines["main"] registration if markers are absent). Stamped seams pass +# the gate by construction. match-cut/morph rows get visibility sets only — the +# carrier handoff stays hand-authored. +node /scripts/seam-stamp.mjs --ledger ledger.json --write index.html +``` + +Run the stamper from the project root. For safety, `--write` must resolve inside +the current project directory and refuses symlink targets. + +Per-seam stamp options in the ledger: `exit.dur` / `entry.dur` (durations), +`entry.travel` (xPercent/yPercent offset, default 10 — use 8 for a soft entry), +`blur` (Z seams, default 18px full-frame / set 10 for text-scale). + +```bash +# verify every seam in the ledger (exit 0 = gate passed) +node /scripts/seam-gate.mjs verify --ledger ledger.json --project + +# reuse a RUNNING preview server (restart it after comp edits — stale bundle!) +node /scripts/seam-gate.mjs verify --ledger ledger.json --url http://localhost:5244 + +# discover movers around a cut time (use to author/fix ledger rows) +node /scripts/seam-gate.mjs probe --t 44.8 --project +``` + +`--project` runs the exact HyperFrames CLI version pinned in `seam-gate.mjs`, starts a +fresh preview server with `HYPERFRAME_RUNTIME_URL` unset, and kills it afterward. `npx` +may download that package from the npm registry when it is not cached. To avoid that +network access, start a compatible preview yourself and use +`--url http://localhost:`. Use `--json` for machine output; `--fps 30` is the +default. + +## ledger.json + +Lives at the project root. One row per seam; this is the vector ledger as data. + +```json +{ + "fps": 30, + "seams": [ + { "id": "hook→claim", "cut": 4.2, "technique": "velocity-matched-transitions LEFT", + "exit": { "selector": "#el-hook", "axis": "x", "dir": -1 }, + "entry": { "selector": "#el-claim", "axis": "x", "dir": -1 } }, + + { "id": "claim→payoff", "cut": 10.2, "technique": "inverse zoom-through", + "exit": { "selector": "#el-claim", "axis": "z", "dir": -1 }, + "entry": { "selector": "#el-payoff", "axis": "z", "dir": -1, "scanRoot": "#el-payoff" } }, + + { "id": "ui→player (match cut)", "cut": 60.6, "type": "match-cut", + "carrier": { "out": "#resting-card", "in": "#product-video" } } + ] +} +``` + +- `cut` — seconds on the master clock, the frame the incoming side ignites. +- `type` — `"cut"` (default; full vector checks), `"match-cut"` / `"morph"` + (carrier-continuity + overlap only; motion may start AT the boundary). +- `axis` — `"x"`, `"y"`, or `"z"` (z = scale). `dir` — sign of motion: + x −1 = leftward, y −1 = upward, z +1 = push (growing), z −1 = pull (shrinking). +- `selector` — the element that CARRIES the seam motion. Use the wrapper when the + master timeline moves the wrapper; use the in-comp hero (id or `[data-hf-id=…]`) + when the seam motion is authored inside the sub-comp. `probe` tells you which. +- `entry.scanRoot` (z seams) — subtree scanned for sign-fighting internal entrances; + defaults to the entry selector. +- `carrier` — optional on `"cut"` rows; required on match-cut/morph. `out`/`in` rects + must match at cut±1 frame (12px center / 5% size tolerance, ancestors included). + +## What each check enforces (seam verification gate rule ↔ report row) + +| Report row | Rule | +|---|---| +| `ledger` | exit/entry vectors match in the PLAN (axis + dir) | +| `exit-moving` / `entry-moving` | rule 1/3 — no settled exits, no from-rest entries | +| `exit-direction` / `entry-direction` | rule 3 — measured sign matches the ledger | +| `speed-match` (WARN) | law §3 — entry velocity ≈ exit velocity | +| `zero-overlap` | rule 6 — one side visible per frame, never both | +| `z-sign-scan` | rule 7 — incoming scene's own entrances don't fight the Z sign | +| `carrier-*` | rules 3/4 — carrier rect continuity, ancestor scale included | + +Velocities are measured on `getBoundingClientRect` (center for x/y, width-ratio for z), +so ancestor wrapper transforms are automatically included. + +## Known constraints + +- Full-frame incoming wrappers must start hidden in HTML/CSS (`opacity: 0`). + The stamper's immediate `gsap.set` cannot satisfy static lint rules that + inspect only the initial markup. +- Seek-safety: under the gate's random seeks, GSAP lazy `.to()` start-value + capture breaks on multi-stage chains — author them as + `fromTo(..., {immediateRender:false})`. +- Generated seams satisfy the generator's hard invariants, but the verifier may + still report speed-match warnings. Review warnings and the rendered frame pair. +- Tolerances: the seam gate's carrier check (12px / 5%) is deliberately + stricter than the layout skill's zoom-isolation spec (30px / 10%); the + gate's numbers govern stamped seams, the skill's govern hand-built ones. diff --git a/skills/vox-explainer/references/velocity-matched-transitions-gsap.md b/skills/vox-explainer/references/velocity-matched-transitions-gsap.md new file mode 100644 index 0000000..ee09093 --- /dev/null +++ b/skills/vox-explainer/references/velocity-matched-transitions-gsap.md @@ -0,0 +1,264 @@ +# Continuation Cut — GSAP code templates + +Canonical implementations for every variant. Parameters and rules live in +[velocity-matched-transitions.md](velocity-matched-transitions.md); +copy code from here. Worker-authored versions tween in-scene elements; registry +`gsap_template`s are injector-stamped onto the two clip wrappers (`__OLD__` / `__NEW__` +/ `__T__` / `__DUR__` tokens — see +[render-safe-seams.md](render-safe-seams.md) for the token table). + +## 1. Zoom-Through (forward) + +### Worker version (within-scene wrapper swap) + +```js +var EXIT_START = /* when readable text starts leaving */; +var CUT = EXIT_START + 0.2; + +// Phase 1: Exit — scale/blur accelerate, opacity fades linearly (separate tween) +tl.to(".text-a-wrapper", { + scale: 1.2, + filter: "blur(10px)", // text-scale: 10px + duration: 0.2, + ease: "power3.in", + overwrite: "auto" +}, EXIT_START); +tl.to(".text-a-wrapper", { opacity: 0.15, duration: 0.2, ease: "none" }, EXIT_START); + +// Phase 2: Hard cut — matched properties +tl.set(".text-a-wrapper", { opacity: 0 }, CUT); +tl.set(".text-b-wrapper", { opacity: 0.15, scale: 0.75, filter: "blur(10px)" }, CUT); + +// Phase 3: Entry — fast initial velocity, long settle +tl.to(".text-b-wrapper", { + scale: 1, filter: "blur(0px)", opacity: 1, + duration: 0.5, ease: "expo.out" +}, CUT); +``` + +### Registry gsap_template + +```js +tl.to(__OLD__, { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: __DUR__, ease: "power3.in" }, __T__); +tl.fromTo(__NEW__, { scale: 0.5, opacity: 0, filter: "blur(8px)" }, { scale: 1, opacity: 1, filter: "blur(0px)", duration: __DUR__, ease: "power3.out" }, __T__); +``` + +## 2. Inverse Zoom-Through + +### Registry gsap_template + +```js +tl.set(__NEW__, { opacity: 0 }, __T__); +tl.to(__OLD__, { scale: 0.8, filter: "blur(10px)", duration: __DUR__ * 0.3, ease: "power3.in" }, __T__); +tl.to(__OLD__, { opacity: 0.15, duration: __DUR__ * 0.3, ease: "none" }, __T__); +tl.set(__OLD__, { opacity: 0 }, __T__ + __DUR__ * 0.3); +tl.fromTo(__NEW__, { opacity: 0.15, scale: 1.25, filter: "blur(10px)" }, { opacity: 1, scale: 1, filter: "blur(0px)", duration: __DUR__ * 0.7, ease: "expo.out", immediateRender: false }, __T__ + __DUR__ * 0.3); +``` + +Worker version: same phases as zoom-through with the scale values flipped +(exit `1 → 0.8`, cut-in at `1.25`, entry `1.25 → 1`). + +## 3. Continuation Cut + +### Worker version (scene layers) + +```js +var CUT_TIME = /* scene transition point */; + +// Scene A: hero accelerates leftward (partial travel ~12% of frame) +tl.to(".scene-a-layer", { opacity: 0, duration: 0.33, ease: "power2.in" }, CUT_TIME - 0.33); +tl.to(".hero-a-wrapper", { + x: -230, filter: "blur(8px)", + duration: 0.33, ease: "power4.in", // mirrored half of power4.inOut + overwrite: "auto" +}, CUT_TIME - 0.33); + +// Hard cut +tl.set(".scene-b-layer", { opacity: 1 }, CUT_TIME); +tl.set(".hero-b-wrapper", { x: 230, filter: "blur(8px)" }, CUT_TIME); + +// Scene B: hero decelerates leftward +tl.to(".hero-b-wrapper", { + x: 0, filter: "blur(0px)", + duration: 0.33, ease: "power4.out" // matched velocity at the cut +}, CUT_TIME); +``` + +### Registry gsap_template + +`__DX__` = `-1920` (LEFT) / `1920` (RIGHT); `__DY__` = `-1080` (UP) / `1080` (DOWN). +The `* 0.12` / `* 0.21` factors yield the ~12% partial travel. + +```js +// horizontal +tl.set(__NEW__, { opacity: 0 }, __T__); +tl.to(__OLD__, { x: __DX__ * 0.12, duration: __DUR__ * 0.5, ease: "power4.in" }, __T__); +tl.to(__OLD__, { opacity: 0, duration: __DUR__ * 0.47, ease: "power2.in" }, __T__ + __DUR__ * 0.03); +tl.fromTo(__NEW__, { x: (__DX__) * -0.12, opacity: 0.35 }, { x: 0, opacity: 1, duration: __DUR__ * 0.5, ease: "power4.out", immediateRender: false }, __T__ + __DUR__ * 0.5); + +// vertical +tl.set(__NEW__, { opacity: 0 }, __T__); +tl.to(__OLD__, { y: __DY__ * 0.21, duration: __DUR__ * 0.5, ease: "power4.in" }, __T__); +tl.to(__OLD__, { opacity: 0, duration: __DUR__ * 0.47, ease: "power2.in" }, __T__ + __DUR__ * 0.03); +tl.fromTo(__NEW__, { y: (__DY__) * -0.21, opacity: 0.35 }, { y: 0, opacity: 1, duration: __DUR__ * 0.5, ease: "power4.out", immediateRender: false }, __T__ + __DUR__ * 0.5); +``` + +### Combined velocity-matched-transitions + zoom + +The scale component obeys the Z sign rule: both sides SHRINK (exit `1 → 0.92`, entry +`1.08 → 1`) — a consistent mild pull layered on the lateral cut. Never pair a shrinking +exit with a grow-from-small entry here. + +```js +// Scene A: hero slides left + mild pull + blur +tl.to(".hero-a-wrapper", { + x: -230, scale: 0.92, filter: "blur(8px)", + duration: 0.33, ease: "power4.in" +}, CUT_TIME - 0.33); + +// Cut + Scene B: continues leftward, arrives slightly oversized and retracts +tl.set(".hero-b-wrapper", { x: 230, scale: 1.08, filter: "blur(8px)" }, CUT_TIME); +tl.to(".hero-b-wrapper", { + x: 0, scale: 1, filter: "blur(0px)", + duration: 0.42, ease: "power4.out" +}, CUT_TIME); +``` + +## 4. Word-Cascade Cut + +### DOM + CSS + +```html +
And until now
+
+
+ that changes today. +
+
+``` + +```css +.beat { position: absolute; inset: 0; display: grid; place-items: center; will-change: transform, opacity; } +.w { display: inline-block; will-change: transform, opacity; } +``` + +### Timeline + +```js +// Pre-set at build time — immediateRender:false alone leaves un-started words visible. +gsap.set([...w1, ...w2], { x: 230, opacity: 0 }); + +function wordExit(words, C) { // C = cut time on the comp timeline + let s = C - 0.32; // exits START before the cut + words.forEach((el) => { + tl.to(el, { x: -230, duration: 0.34, ease: "power4.in" }, s); + tl.to(el, { opacity: 0, duration: 0.18, ease: "power1.in" }, s); // fade ends ~25-30% into travel + s += 0.022; // reading-order stagger + }); +} + +function wordEnter(words, C) { + let off = 0, gap = 0.05; + words.forEach((el) => { + tl.fromTo(el, + { x: 230, opacity: 0.35 }, // ignites MID-PATH, already moving + { x: 0, opacity: 1, duration: 0.3, ease: "power4.out", immediateRender: false }, + C + off); + off += gap; + gap *= 0.84; // shrinking gaps — the cascade accelerates + }); +} + +const C = 2.3; +wordExit(w1, C); +tl.set(b1, { opacity: 0 }, C); // hard layer swap AT the cut +tl.set(b2, { opacity: 1 }, C); +wordEnter(w2, C); +``` + +Whole-line first beat: exit as one element — `x: -230, 0.34s, power4.in` with the +opacity fade running nearly the whole ramp (ends ~0.02s before the cut). + +## 5. Rack-Focus Blur-Cut + +For a Z dolly, drop the x-offset and use scale `0.92 →` / `→ 1.08`. + +```js +var T = /* transition start */; +// Phase 1: outgoing pans + blurs, held fully opaque +tl.to("#scene-out", { x: -80, scale: 1.06, filter: "blur(12px)", duration: 0.3, ease: "power2.in" }, T); +// Phase 2: hard cut at peak blur +tl.set("#scene-out", { opacity: 0 }, T + 0.3); +tl.fromTo("#scene-in", + { opacity: 1, x: 80, scale: 1.06, filter: "blur(12px)" }, + { x: 0, scale: 1, filter: "blur(0px)", duration: 0.35, ease: "power2.out" }, + T + 0.3); +``` + +## Tuning ranges + +### Zoom-Through / Inverse Zoom + +| Parameter | Default | Range | +|-----------|---------|-------| +| Exit scale | 1.2 (fwd) / 0.8 (inv) | ±0.1 | +| Entry scale | 0.75 (fwd) / 1.25 (inv) | ±0.1 | +| Blur at cut | 10px text / 18–20px full-frame | — | +| Opacity at cut | 0.15 | 0.1–0.2 | +| Exit duration | 0.2s | 0.15–0.3s | +| Entry duration | 0.5s | 0.4–0.6s | + +### Continuation Cut + +| Parameter | Default | Range | +|-----------|---------|-------| +| Travel | 230px (~12% frame) | 150–300px | +| Blur at cut | 8px | 6–10px | +| Exit duration | 0.33s | 0.2–0.4s | +| Entry duration | 0.33–0.42s | ≥ exit | + +## 6. Cascade Entry + +Each element: `tl.set` (instant reveal + offset) then `tl.to` (whip to rest). +`nextStart = prevStart + prevDuration − (overlapFrames × F)`; +overlap = cascade, +−overlap = deliberate gap. CSS: elements start `opacity: 0; display: inline-block`. + +```js +var F = 1 / 60; +var t0 = 0.1; +// anchor (heaviest): biggest travel, longest settle +tl.set("#el-1", { opacity: 1, y: 80 }, t0); +tl.to("#el-1", { y: 0, duration: 0.18, ease: "power4.out" }, t0); +// normal word: 2 frames after the anchor finishes +var t1 = t0 + 0.18 + 2 * F; +tl.set("#el-2", { opacity: 1, y: 45 }, t1); +tl.to("#el-2", { y: 0, duration: 0.15, ease: "power4.out" }, t1); +// light word: 1 frame BEFORE the previous finishes (overlap) +var t2 = t1 + 0.15 - F; +tl.set("#el-3", { opacity: 1, y: 40 }, t2); +tl.to("#el-3", { y: 0, duration: 0.14, ease: "power4.out" }, t2); +// split final-word fragments: tightest overlap, extra travel (lighter) +var t3 = t2 + 0.14 - F; +tl.set("#frag-a", { opacity: 1, y: 70 }, t3); +tl.to("#frag-a", { y: 0, duration: 0.16, ease: "power4.out" }, t3); +var t4 = t3 + 0.14 - F; +tl.set("#frag-b", { opacity: 1, y: 70 }, t4); +tl.to("#frag-b", { y: 0, duration: 0.15, ease: "power4.out" }, t4); +// punctuation: lightest, fastest +var t5 = t4 + 0.13 - 2 * F; +tl.set("#dot", { opacity: 1, y: 48 }, t5); +tl.to("#dot", { y: 0, duration: 0.12, ease: "power4.out" }, t5); +``` + +## 7. Three-Phase Slide + +Reference values for a 270px leftward slide (0.57s total). Scale distances +proportionally for other travels; preserve the TIME ratios; tail ≥3× ramp-in. + +```js +var t = /* start after content settles */; +tl.to(".text-row", { x: -30, duration: 0.12, ease: "power3.in" }, t); // ramp-in: 11% dist / 21% time +tl.to(".text-row", { x: -210, duration: 0.10, ease: "none" }, t + 0.12); // burst: 67% dist / 18% time +tl.to(".text-row", { x: -270, duration: 0.35, ease: "power4.out" }, t + 0.22); // tail: 22% dist / 61% time +// vertical: same ratios on y. 150px variant: -15 / -115 / -150 at the same times. +``` diff --git a/skills/vox-explainer/references/velocity-matched-transitions.md b/skills/vox-explainer/references/velocity-matched-transitions.md new file mode 100644 index 0000000..b4a1faa --- /dev/null +++ b/skills/vox-explainer/references/velocity-matched-transitions.md @@ -0,0 +1,269 @@ +# Velocity-Matched Transitions — technique catalog + +Five SEAM techniques, one principle: **cut at peak velocity, match direction and speed +on both sides of the cut** — plus the two in-scene techniques (§6 arrivals, §7 slides). +The seam law — vector law, the current, the ledger, and the verification gate — lives in +[motion-continuity.md](motion-continuity.md); read it first. This reference contains the +parameters and mechanics. All GSAP code templates are in +[velocity-matched-transitions-gsap.md](velocity-matched-transitions-gsap.md). + +## Catalog + +| # | Technique | Scope | Axis | Use for | +|---|---------|-------|------|---------| +| 1 | **Zoom-Through** (forward) | Within-scene text swap | Z, toward viewer | progressing deeper into the same thought | +| 2 | **Inverse Zoom-Through** | Arrival / payoff beat | Z, away from viewer | something bigger lands | +| 3 | **Continuation Cut** | Between scenes | X / Y | the default boundary, the film's current | +| 4 | **Word-Cascade Cut** | Text-to-text seam | X, per-word | word-level handoff between big-text beats | +| 5 | **Rack-Focus Blur-Cut** | Same-surface state swap | X / Y / Z | the one cut you want SEEN — a DSLR focus-pull flourish | +| 6 | **Cascade Entry** | In-scene ARRIVAL (no seam) | Y, from below | title cards, segment openers, list intros | +| 7 | **Three-Phase Slide** | In-scene group slide (no seam) | X / Y | repositioning a composed group to make room | + +One element running two or more animations back to back is a different problem — the +velocity law applied WITHIN an object rather than across a cut. See +[animation-overlap.md](animation-overlap.md). + +## Z direction is a sign + +"Same axis" is not enough on Z — the sign of d(scale)/dt must match across the cut: + +| Z vector | Exit scale | Entry scale | Variant | +|----------|-----------|-------------|---------| +| Push (forward) | growing `1 → 1.2` | growing `0.75 → 1` | zoom-through | +| Pull (back) | shrinking `1 → 0.8` | shrinking `1.25 → 1` | inverse zoom-through | + +Banned mirrors: a receding exit answered by a grow-from-small entry (pull flips to push — +the common one, since grow-from-small is the default element entrance), and a push exit +answered by an oversized retraction. This binds the incoming scene's OWN entrances during +the seam window (cut + ~0.5s), not just the wrapper tween: hold the incoming frame +composed, or author its entrance to match the sign. Verify per seam verification gate rule 7. + +**Magnitude, not just sign.** Matching d(scale)/dt sign is necessary but not +sufficient: at the swap frame the incoming's APPARENT FIELD OF VIEW must be +equal or tighter (deeper) than the outgoing's final framing when continuing +a push, and equal or wider when continuing a pull — never the reverse. +Verify with a landmark: measure a feature common to both plates (road width, +a building) at the cut; a push whose incoming landmark is SMALLER than the +outgoing's reads as a rubber-band (zoom in, snap back out), and blur does +not hide it. When the incoming plate cannot be cropped deep enough, lower +the outgoing's exit magnification to meet it. The same law binds one subject across a +vanish: an element that exits through the lens may reappear at another +scale only through an authored arrival the eye can ride (an inverse +zoom-through, visible travel into place, or a reveal that redirects +attention first) — a same-frame re-dock at a different magnification is a +teleport and reads as random. + +## Blur logic (all Z variants) + +| Subject | Peak blur | Why | +|---------|-----------|-----| +| Text-scale (headline, word group) | **10px** | 20px smears letterforms — the cut reads as a glitch, not speed | +| Full-frame surface (window, card, screenshot) | **18–20px** | lighter blur on a big surface reads as a rendering hiccup | + +Same peak blur on both sides at the swap frame. Blur the WRAPPER, never children. + +--- + +## 1. Zoom-Through (forward) + +Z-axis velocity-matched cut; **never both texts visible.** Everything GROWS: the outgoing +text accelerates toward camera, a hard swap hides at peak blur, the incoming text keeps +growing into the focal plane. Headlines and short phrases only. Total ≈ 0.4s. + +| Phase | Scale | Blur | Opacity | Ease | Duration | +|-------|-------|------|---------|------|----------| +| Exit | 1 → 1.2 | 0 → 10px | 1 → 0.15 | power3.in (opacity: separate `none` tween) | 0.2s | +| Cut (`tl.set`) | in: 0.75 | 10px | out: 0 / in: 0.15 | — | — | +| Entry | 0.75 → 1 | 10 → 0px | 0.15 → 1 | expo.out | 0.5s | + +Exit opacity MUST be its own linear tween — `power3.in` holds opacity near 1 too long. +On entry all properties share `expo.out`. + +## 2. Inverse Zoom-Through (backward) + +The pull-back mirror: the outgoing element RECEDES; the incoming arrives OVERSIZED (as if +just behind camera) and retracts into the focal plane. Everything SHRINKS. Spend on +ARRIVAL/payoff beats — a payoff line, a giant reply, a held end-state — never ordinary +boundaries. Total ≈ 0.7s (30% exit / 70% entry). + +| Phase | Scale | Blur | Opacity | Ease | Duration | +|-------|-------|------|---------|------|----------| +| Exit | 1 → 0.8 | 0 → 10px | 1 → 0.15 | power3.in (opacity: separate `none` tween) | ~0.2s | +| Cut (`tl.set`) | in: 1.25 | 10px | out: 0 / in: 0.15 | — | — | +| Entry | 1.25 → 1 | 10 → 0px | 0.15 → 1 | expo.out | ~0.5s | + +Blur is 10px text-scale; 18–20px only when both sides are full-bleed surfaces. + +**Sign discipline:** the incoming scene arrives as a composed frame inside the retracting +wrapper — no grow-from-small intro in the seam window. Staged entrances happen after the +retraction settles, or start ≥1 and retract. + +## 3. Continuation Cut (default scene boundary) + +X/Y velocity-matched cut — the default for ALL scene-to-scene boundaries, in the film's +current, not an accent. The outgoing hero accelerates in one direction, the cut lands +mid-motion, the incoming hero continues the SAME direction and decelerates. Total ≈ 0.6s; +directions LEFT / RIGHT / UP / DOWN (default LEFT). + +**Partial travel:** ~12% of frame (≈230px at 1920) — never full off-screen moves. + +| Direction | Exit | Entry start → end | +|-----------|------|-------------------| +| Leftward | `x: 0 → −230` | `x: +230 → 0` | +| Rightward | `x: 0 → +230` | `x: −230 → 0` | +| Upward | `y: 0 → −230` | `y: +230 → 0` | +| Downward | `y: 0 → +230` | `y: −230 → 0` | + +Mechanics: +- **Mirrored eases:** exit `power4.in` + entry `power4.out`, same distance and duration — + the two halves of one `power4.inOut`, so velocity matches exactly at the cut. +- **The fade trick:** exit opacity completes at ~25–30% of its travel (fade ≈ 0.18–0.3s + vs motion 0.3–0.34s); entry ignites at ~0.35 opacity mid-path. Time the last fading + element to die right at the cut — a gap where nothing moves reads as dead air. +- Exit 0.2–0.4s; entry ≥ exit. Optional blur 8–10px. +- **Stage ground:** `#root` must be opaque + (`background: var(--canvas-deep, var(--canvas, #000))`) — the mid-window cut opens a + summed-opacity < 1 window that flashes white otherwise (see + [render-safe-seams.md](render-safe-seams.md)). + +`push-slide` exists but violates partial-travel and mid-motion phase; prefer the continuation cut. + +## 4. Word-Cascade Cut (word-by-word continuation cut) + +The continuation cut at WORD granularity — the strongest leftward cut for text-to-text seams. +Outgoing words ramp out on their own curves; incoming words cascade in mid-flight — a +wave the eye rides across the seam. + +**Scope:** worker-authored inside one multi-beat comp (stacked full-frame `.beat` layers), +NOT a registry/injector type — it tweens word spans, not clip wrappers. The boundary into +and out of the text-beat block still gets a normal registry transition. Does not count +against the 2–3 transition budget. + +| Parameter | Value | Why | +|-----------|-------|-----| +| Travel | ±230px (~12% frame) | partial travel + velocity > full-frame push | +| Exit | 0.34s `power4.in` | the acceleration IS the cut | +| Exit fade | 0.18s, starts with x | word gone by ~25–30% of travel — no smear | +| Exit stagger | +0.022s reading order | the line peels, not a block slide | +| Entry | 0.3s `power4.out` | back half of the composite — velocity match | +| Entry start opacity | 0.35 | mid-path ignition; binary 0→1 pops | +| Entry gaps | 0.05s × 0.84 decay | accelerating cascade, resolves composed | + +Rules: +- One direction per chain, riding the current. Inverse zoom is the chain's ARRIVAL beat only. +- Pre-set all words to `x: +230, opacity: 0` at build time — `immediateRender: false` + alone leaves un-started words visible at rest. +- A short first beat may exit whole-line: its fade ends ~0.02s before the cut so it is + still streaking when the next words ignite — no dead gap. +- Transform/opacity only (seek-safe); opaque stage ground applies. + +## 5. Rack-Focus Blur-Cut (the visible cut) + +The one variant where the cut is SEEN: a defocus blur SPIKE hides a single-frame hard +swap — a handheld-DSLR focus-pull. Use as an occasional flourish for a state swap of the +SAME surface within one visual theme; never the default boundary. + +Differences from the others: outgoing stays FULLY OPAQUE until the cut (the blur hides +the swap — no early fade); eases `power2.in` / `power2.out` (soft optics, not momentum). + +Rules: +- Fire only at a narrative beat, ≤ once per ~8s; never mid-caption or during a hold. +- Cut at PEAK blur (≥6px; peak 8–12px, ≤16–18px max) — swapping on the way up shows the cut. +- A subtle scale (~1.06 lens-breathing) sells it as optics. +- Same direction on both sides — the vector law still holds. Entry ≥ exit duration. +- Blur the wrapper; never blur + opacity in one tween on one element (headless + compositing bug); never blur a `