From 29990a5f94bd423bcc5887265b69a6018ff5a319 Mon Sep 17 00:00:00 2001 From: jbernard077 Date: Tue, 8 Sep 2026 14:31:32 -0400 Subject: [PATCH] =?UTF-8?q?feat(skills):=20baked-sim=20rule=20=E2=80=94=20?= =?UTF-8?q?offline=20physics=20stepped=20at=20build=20time,=20replayed=20b?= =?UTF-8?q?y=20frame=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds rules/baked-sim.md to hyperframes-animation: a seeded build-time simulation (verlet rope reference) written as per-frame data, shipped as a synchronous script include, and replayed by one ease:"none" driver doing an index lookup — no integration or RNG at runtime, seek-safe by construction. Adds the rules-index row and a boundary pointer in particle-burst.md (interacting bodies are baked; ballistic ones stay closed-form). --- skills-manifest.json | 4 +- skills/hyperframes-animation/rules-index.md | 1 + .../hyperframes-animation/rules/baked-sim.md | 169 ++++++++++++++++++ .../rules/particle-burst.md | 2 +- 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 skills/hyperframes-animation/rules/baked-sim.md diff --git a/skills-manifest.json b/skills-manifest.json index 184a7712c4..791e0fdf66 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -22,8 +22,8 @@ "files": 17 }, "hyperframes-animation": { - "hash": "3947516ec450f3aa", - "files": 121 + "hash": "7e1a0bb8bc35353c", + "files": 122 }, "hyperframes-audio": { "hash": "b39bac771e873eae", diff --git a/skills/hyperframes-animation/rules-index.md b/skills/hyperframes-animation/rules-index.md index 8568d22791..ed49654a8f 100644 --- a/skills/hyperframes-animation/rules-index.md +++ b/skills/hyperframes-animation/rules-index.md @@ -96,6 +96,7 @@ A rule's own **Critical Constraints** section lists only what is SPECIFIC to tha 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 +Organic physics (rope, flag, cloth, debris, flock) never runs at render time — a build script steps the sim at `dt = 1/fps` with a seeded PRNG and writes per-frame data; playback is one `ease:"none"` driver doing `frames[round(t × fps)]`, an index lookup and nothing else. Synchronous script include (never fetched), ~60-frame pre-roll, frame 0 applied at setup. Tags: physics, simulation, baked, offline, verlet, rope, flag, seeded, replay, deterministic 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/baked-sim.md b/skills/hyperframes-animation/rules/baked-sim.md new file mode 100644 index 0000000000..5729f22f17 --- /dev/null +++ b/skills/hyperframes-animation/rules/baked-sim.md @@ -0,0 +1,169 @@ +--- +name: baked-sim +description: Organic physics (a rope, flag, cloth, debris, a flock) never runs at render time — a build script steps the simulation at dt = 1 / fps with a seeded PRNG and writes per-frame data; playback is one `ease:"none"` driver whose onUpdate does `frames[Math.round(t × fps)]`, an index lookup and nothing else. Seek-safe and deterministic by construction; shipped as a synchronous script include, never fetched. +metadata: + tags: physics, simulation, baked, offline, verlet, rope, cloth, flag, pennant, debris, seeded, prng, replay, frame-index, deterministic +--- + +# Baked Sim + +A hanging pennant catching wind, a rope settling, a cloud of debris tumbling: motion that comes from a **simulation** — integration, constraints, forces — not from an ease. A simulation is stateful by nature (frame N depends on frame N−1), which is exactly what a seeked renderer cannot run. So the simulation runs **once, offline**, at build time, and the composition replays its output by frame index. The renderer sees a lookup, never an integrator. + +Boundaries: [particle-burst.md](particle-burst.md) needs no bake — each particle is a closed-form ballistic function of time and evaluates directly. Bake when the bodies **interact** (constraints, collisions, wind on a chain, flocking) or the step is iterative. A [sine-wave-loop.md](sine-wave-loop.md) idle is neither; keep it analytic. + +## How It Works + +1. **Bake** — a build script (`bake/bake-.mjs`) steps the sim at `dt = 1 / FPS` for `PRE_ROLL + FRAMES` steps with a seeded PRNG (`mulberry32(SEED)`, never `Math.random`), fixed iteration counts, and per-frame forces computed from the frame number. It writes one entry per composition frame: an SVG path `d`, an array of transforms, a list of points. +2. **Ship synchronously** — the output is a script that assigns one global (`window.__PENNANT = {...}`), loaded with a plain ` +``` + +```js +const PEN = window.__PENNANT; // present before this script runs — no load event, no race +const rope = document.getElementById("rope"); +const LAST = PEN.frames.length - 1; +const apply = (i) => rope.setAttribute("d", PEN.frames[i]); +apply(0); // structural frame 0: the first captured frame never depends on the driver having fired +const proxy = { f: 0 }; +tl.to( + proxy, + { + f: LAST, + duration: LAST / PEN.fps, // f === k at every sampled frame k / fps — an off-by-one here shows the previous entry on half the frames + ease: "none", + onUpdate: () => apply(Math.max(0, Math.min(LAST, Math.round(proxy.f)))), // an index lookup, nothing else + }, + SCENE_AT, +); +``` + +## Variations + +- **Transform arrays** — bake `[x, y, rot]` per body per frame (debris, a flock) and write each element's `transform` in the same `onUpdate`; the pool is fixed at setup. +- **Canvas draw** — bake point lists and draw them with `ctx` inside the driver: the canvas becomes a pure function of the frame index (paint frame 0 at setup). +- **Two-rate bake** — simulate at a finer `dt` (`1 / (FPS × 2)`) for stiff constraints and write every second step; the composition still indexes at its own fps. +- **Re-bake for direction** — a physically correct sim is not yet art-directed: the reference pennant hung limp until wind ≈ 3 × gravity, and streams at that ratio. Budget one bake iteration for the look, then freeze the seed. +- **Designed motion on top** — the baked object still takes ordinary tweens (a fade-in, a settle, an exit); the bake owns only what the sim owns. + +## Values + +| token | range | notes | +| --------- | ------------------------------ | ------------------------------------------------------------------------------------ | +| FPS | = the composition's `data-fps` | one entry per composition frame; the driver's `round()` then never straddles two | +| PRE_ROLL | 30–90 frames (default 60) | enough for the system to hang / settle before frame 0 | +| ITER | 8–20 constraint passes | stiffness; more = stiffer rope, longer bake, same file size | +| SEED | any fixed integer | change it to change the gusts; commit it with the data | +| data size | ≲ 100–300 KB per baked object | a 210-frame, 16-point path is ~75 KB; past ~1 MB decimate the path or the body count | + +## Critical Constraints + +- **No integration at runtime** — the composition never steps, accumulates, or reads a previous frame; `frames[i]` is the whole runtime. A per-frame `+=` on baked state re-introduces the race the bake removed. +- **Synchronous include** — the baked script is a `