feat(motion): choreography engines + two renderer root causes fixed (#543)

The release cuts read as static screen recordings: one 6s cursor glide
that then vanished, a locked-off camera, metronomic flat card fades. And
two renderer defects were silently wrecking every cut:

- @hyperframes/producer floated (^0.7.36, no lockfile — image builds get
  whatever is latest): 0.7.60 fails EVERY render with "Cannot access 'rt'
  before initialization". Pinned 0.7.36 exact and committed
  package-lock.json so sidecar builds are reproducible.
- the producer's per-clip visibility scheduler runs on a clock that lags
  ~50% behind the encoded timeline on a 40s cut — the final frame showed
  the authored ~18s state, so the tail scenes (bell card, toast, outro)
  were silently missing from the MP4. This, not authoring, is why
  rendered cuts kept losing their late scenes. Fix: clip windows are for
  structural layers only (hero, panel frame, status); every beat rides
  base-hidden styles + delayed CSS animations, which run on the true
  clock. Verified frame-by-frame: all four cards, stats, toast, and
  outro now land exactly on schedule in both cuts.

Craft, made reusable in the kit instead of per-composition heroics:

- kit.js choreographCursor: data-waypoints="t x y [click]; ..." generates
  a multi-leg eased path with fade in/out, an idle-hand sway, and click
  rings + glyph press dips — the cursor behaves like a hand, never pops
  in, freezes, or blinks out
- kit.js choreographCamera: data-shots="t x y scale; ..." — push-ins
  toward each beat's focal point, pull-backs for reveals, settle to end
- release-0.25.0 both cuts re-choreographed: the cursor is the CEO's
  hand (settle on the intake while it types, ONE submit click, witness
  each card completing, acknowledge the toast, exit off-frame); the
  camera lives on every beat; springy card entrances replace flat fades
- motion/README.md gains 'Cinematography & rhythm' (shot-list-first, no
  locked-off camera, verify motion with frame PAIRS) + the clip-window
  rule; kit/README.md documents both engines; the dev video prompt block
  carries the craft bar

Verified end to end through the real sidecar: both cuts render green,
32-frame strips read visually — cursor travels and clicks on schedule,
camera moves, every scene present. motion pnpm test 15/15.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-17 03:58:08 +02:00
committed by GitHub
co-authored by Renn F
parent e9ca7d4036
commit 3e801697f7
10 changed files with 3935 additions and 45 deletions
@@ -55,7 +55,7 @@ describe("release-0.25.0 composition", () => {
expect(html).toContain("pk-caret");
expect(html).toContain("typeReveal");
const cardCount = (html.match(/pk-card clip/g) || []).length;
const cardCount = (html.match(/class="pk-card"/g) || []).length;
const progressCount = (html.match(/pk-pill--progress/g) || []).length;
const completedCount = (html.match(/pk-pill--completed/g) || []).length;
expect(cardCount).toBe(4);
@@ -68,8 +68,19 @@ describe("release-0.25.0 composition", () => {
expect(html).toContain("Metrics donut");
expect(html).toContain("Notification bell");
// Choreographed cursor: multi-waypoint path with at least one click
// beat (rings are generated at runtime by kit.js, not static HTML),
// and a camera that moves — a locked-off frame is an automatic bounce.
expect(html).toContain("pk-cursor");
expect(html).toContain("pk-cursor__ring");
const waypoints = html.match(/data-waypoints="([^"]+)"/);
expect(waypoints).not.toBeNull();
expect(waypoints[1].split(";").length).toBeGreaterThanOrEqual(6);
expect(waypoints[1]).toContain("click");
const shots = html.match(/data-shots="([^"]+)"/);
expect(shots).not.toBeNull();
expect(shots[1].split(";").length).toBeGreaterThanOrEqual(4);
expect(html).toContain("choreographAllCursors");
expect(html).toContain("choreographAllCameras");
expect(html).toContain("pk-toast");
expect(html).toContain('id="toastTitle"');
+49 -19
View File
@@ -111,7 +111,14 @@
.pk-intake { --pk-intake-top: 18px; }
.pk-intake__field { height: 60px; font-size: 28px; }
.pk-column { --pk-column-top: 130px; width: 560px; }
.pk-column .pk-card { position: static; margin-bottom: 12px; padding: 18px 24px; }
.pk-column .pk-card {
position: static;
margin-bottom: 12px;
padding: 18px 24px;
/* Springy entrance (see vertical.html); flat fades read as slides. */
animation-duration: 0.55s;
animation-timing-function: cubic-bezier(0.34, 1.4, 0.64, 1);
}
.pk-card__title { font-size: 28px; }
.r25-sub { font-size: 18px; }
@@ -172,6 +179,17 @@
<div class="r25-hero__tag">governance gets a better UI</div>
</div>
<!-- Camera: tight open, push per card beat, dead-wide before the
full-screen stats overlay, gentle push for the toast, settle. -->
<div class="pk-camera"
data-shots="2.6 0 10 1.03; 4.8 0 6 1.035;
6.4 30 62 1.055; 9.2 26 56 1.05;
10.4 28 8 1.06; 13.6 24 4 1.055;
15.4 30 -46 1.06; 18.6 26 -50 1.055;
20.4 28 -96 1.06; 22.4 22 -98 1.05;
23.8 0 0 1.0; 32.6 0 0 1.0;
33.8 -20 -60 1.03; 35.6 -18 -58 1.028;
37.4 0 0 1.0; 39.6 0 4 1.004">
<div class="pk-frame clip" data-start="2.6" data-duration="37.4" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r25-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
@@ -197,7 +215,7 @@
</header>
<div class="pk-frame__content">
<div class="pk-intake clip" data-start="3.6" data-duration="36.4" data-track-index="2" style="animation-delay: 3.6s;">
<div class="pk-intake" style="animation-delay: 3.6s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">Governance gets a better UI</span><span class="pk-caret"></span></div>
</div>
@@ -205,67 +223,75 @@
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.25.0</div>
<div class="pk-card clip" data-start="5.0" data-duration="35" data-track-index="3" style="animation-delay: 5.0s;">
<div class="pk-card" style="animation-delay: 5.0s;">
<div class="pk-card__title">Env ladder</div>
<div class="r25-sub">Dev to prod, one rung at a time.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">project settings</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.0" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.6" data-duration="33.4" data-track-index="5" style="--pk-pill-enter-delay: 6.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 6.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="10.0" data-duration="30" data-track-index="6" style="animation-delay: 10.0s;">
<div class="pk-card" style="animation-delay: 10.0s;">
<div class="pk-card__title">Collision map</div>
<div class="r25-sub">See who touched what before you review.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">task detail</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.0" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="11.6" data-duration="28.4" data-track-index="8" style="--pk-pill-enter-delay: 11.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 11.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="15.0" data-duration="25" data-track-index="9" style="animation-delay: 15.0s;">
<div class="pk-card" style="animation-delay: 15.0s;">
<div class="pk-card__title">Metrics donut</div>
<div class="r25-sub">90 days of real task flow.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">performance tab</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="15.0" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="16.6" data-duration="23.4" data-track-index="11" style="--pk-pill-enter-delay: 16.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 16.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="20.0" data-duration="20" data-track-index="12" style="animation-delay: 20.0s;">
<div class="pk-card" style="animation-delay: 20.0s;">
<div class="pk-card__title">Notification bell</div>
<div class="r25-sub">Real read/ack actions on every alert.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">notifications</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="20.0" data-duration="1.6" data-track-index="13" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="21.6" data-duration="18.4" data-track-index="14" style="--pk-pill-enter-delay: 21.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 21.6s;">completed</span>
</div>
</div>
</div>
<div class="pk-cursor clip" data-start="22.0" data-duration="6" data-track-index="15" style="--pk-cursor-delay: 22.0s; --pk-cursor-x0: 700px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 700px;">
<!-- Same hand as the vertical cut: submit click, witness each
card, exit right BEFORE the full-screen stats overlay lands
at 24s (the cursor sits under its backdrop). -->
<div class="pk-cursor"
data-waypoints="4.0 520 130; 4.5 452 80; 4.8 445 76 click;
5.6 250 185; 6.4 138 250; 7.6 152 262;
9.6 220 340; 10.4 148 412; 12.0 134 424;
14.6 215 505; 15.4 140 576; 17.0 155 588;
19.6 220 668; 20.4 150 740; 22.0 136 752;
23.0 480 780; 23.9 920 820">
<div class="pk-cursor__glyph"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 23.4s;"></div>
</div>
<div class="r25-stats clip" data-start="24.0" data-duration="8" data-track-index="17">
<div class="r25-stats">
<div class="r25-stats__line" style="animation-delay: 24.0s;">1 release</div>
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 24.3s;">25 agents</div>
<div class="r25-stats__line" style="animation-delay: 24.6s;">1 human</div>
</div>
<div class="pk-toast clip" data-start="30.0" data-duration="8" data-track-index="18" style="animation-delay: 30.0s;">
<div class="pk-toast" style="animation-delay: 30.0s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.25.0 shipped</div>
@@ -273,12 +299,13 @@
</div>
</div>
<div class="pk-outro clip" data-start="36.0" data-duration="4" data-track-index="19" style="animation-delay: 36.0s;">
<div class="pk-outro" style="animation-delay: 36.0s;">
<span class="r25-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
</div>
</div>
</div>
</div>
<script>
@@ -314,6 +341,9 @@
}
typeReveal(typeEl);
}
window.PanelKit.choreographAllCursors();
window.PanelKit.choreographAllCameras();
})();
</script>
</body>
@@ -124,7 +124,14 @@
/* ---- Content placement --------------------------------------------- */
.pk-intake { --pk-intake-top: 32px; }
.pk-column { --pk-column-top: 220px; width: 720px; }
.pk-column .pk-card { position: static; margin-bottom: 20px; }
.pk-column .pk-card {
position: static;
margin-bottom: 20px;
/* Springy entrance with a hint of overshoot; a flat fade on every
card is the metronome that makes the cut feel like a slideshow. */
animation-duration: 0.55s;
animation-timing-function: cubic-bezier(0.34, 1.4, 0.64, 1);
}
.r25-sub {
margin-top: 10px;
font-size: 22px;
@@ -189,7 +196,18 @@
<div class="r25-hero__tag">governance gets a better UI</div>
</div>
<!-- Beat 2: the panel assembles. -->
<!-- Beat 2: the panel assembles, and the camera lives on it: start a
touch tight, push toward each card as it completes, pull wide for
the receipt, ease onto the toast, settle for the lockup. -->
<div class="pk-camera"
data-shots="2.6 0 14 1.025; 4.8 0 8 1.03;
6.4 34 96 1.055; 9.2 30 88 1.05;
10.4 30 30 1.06; 13.6 26 24 1.055;
15.4 32 -52 1.065; 18.6 28 -58 1.06;
20.4 30 -150 1.065; 22.6 26 -156 1.06;
25.0 0 0 1.0; 29.2 0 0 1.0;
30.8 -26 -120 1.035; 33.4 -22 -116 1.03;
37.0 0 0 1.0; 39.6 0 6 1.005">
<div class="pk-frame clip" data-start="2.6" data-duration="37.4" data-track-index="1">
<aside class="pk-frame__sidebar">
<div class="r25-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
@@ -216,7 +234,7 @@
<div class="pk-frame__content">
<!-- Beat 3: the hook types into intake. -->
<div class="pk-intake clip" data-start="3.6" data-duration="36.4" data-track-index="2" style="animation-delay: 3.6s;">
<div class="pk-intake" style="animation-delay: 3.6s;">
<div class="pk-intake__label">New task</div>
<div class="pk-intake__field"><span id="typeIntro">Governance gets a better UI</span><span class="pk-caret"></span></div>
</div>
@@ -225,70 +243,81 @@
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.25.0</div>
<div class="pk-card clip" data-start="5.0" data-duration="35" data-track-index="3" style="animation-delay: 5.0s;">
<div class="pk-card" style="animation-delay: 5.0s;">
<div class="pk-card__title">Env ladder</div>
<div class="r25-sub">Dev to prod, one rung at a time.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">project settings</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.0" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.6" data-duration="33.4" data-track-index="5" style="--pk-pill-enter-delay: 6.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 6.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="10.0" data-duration="30" data-track-index="6" style="animation-delay: 10.0s;">
<div class="pk-card" style="animation-delay: 10.0s;">
<div class="pk-card__title">Collision map</div>
<div class="r25-sub">See who touched what before you review.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">task detail</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.0" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="11.6" data-duration="28.4" data-track-index="8" style="--pk-pill-enter-delay: 11.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 11.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="15.0" data-duration="25" data-track-index="9" style="animation-delay: 15.0s;">
<div class="pk-card" style="animation-delay: 15.0s;">
<div class="pk-card__title">Metrics donut</div>
<div class="r25-sub">90 days of real task flow.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">performance tab</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="15.0" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="16.6" data-duration="23.4" data-track-index="11" style="--pk-pill-enter-delay: 16.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 16.6s;">completed</span>
</div>
</div>
<div class="pk-card clip" data-start="20.0" data-duration="20" data-track-index="12" style="animation-delay: 20.0s;">
<div class="pk-card" style="animation-delay: 20.0s;">
<div class="pk-card__title">Notification bell</div>
<div class="r25-sub">Real read/ack actions on every alert.</div>
<div class="pk-card__meta">
<span class="pk-chip pk-chip--gray">notifications</span>
</div>
<div class="pk-card__status">
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="20.0" data-duration="1.6" data-track-index="13" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="21.6" data-duration="18.4" data-track-index="14" style="--pk-pill-enter-delay: 21.6s;">completed</span>
<span class="pk-pill pk-pill--progress pk-pill--swap-out" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
<span class="pk-pill pk-pill--completed pk-pill--swap-in" style="--pk-pill-enter-delay: 21.6s;">completed</span>
</div>
</div>
</div>
<!-- Cursor glide to the newest card. -->
<div class="pk-cursor clip" data-start="22.0" data-duration="6" data-track-index="15" style="--pk-cursor-delay: 22.0s; --pk-cursor-x0: 720px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 1090px;">
<!-- The cursor is the CEO's hand: settle on the intake while it
types, ONE submit click just before the org takes over, then
travel to witness each card completing (the agents do the
clicking-free work), drift to the receipt, acknowledge the
toast, and leave the frame; never freeze, never blink out. -->
<div class="pk-cursor"
data-waypoints="4.0 640 210; 4.5 552 152; 4.8 545 148 click;
5.6 300 330; 6.4 160 405; 7.6 178 420;
9.6 260 520; 10.4 190 628; 12.0 172 642;
14.6 250 760; 15.4 162 848; 17.0 180 862;
19.6 255 985; 20.4 190 1068; 22.0 174 1082;
23.6 420 1180; 24.8 560 1250; 28.6 585 1268;
29.6 615 1420; 30.8 612 1462 click;
33.2 700 1520; 35.0 940 1660">
<div class="pk-cursor__glyph"></div>
<div class="pk-cursor__ring" style="--pk-click-delay: 23.4s;"></div>
</div>
<!-- Beat 5: receipt stats. -->
<div class="r25-stats clip" data-start="24.0" data-duration="8" data-track-index="17">
<div class="r25-stats">
<div class="r25-stats__line" style="animation-delay: 24.0s;">1 release</div>
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 24.3s;">25 agents</div>
<div class="r25-stats__line" style="animation-delay: 24.6s;">1 human</div>
</div>
<!-- Beat 6: confirmation toast, then lockup. -->
<div class="pk-toast clip" data-start="30.0" data-duration="8" data-track-index="18" style="animation-delay: 30.0s;">
<div class="pk-toast" style="animation-delay: 30.0s;">
<div class="pk-toast__icon"></div>
<div class="pk-toast__body">
<div class="pk-toast__title" id="toastTitle">v0.25.0 shipped</div>
@@ -296,12 +325,13 @@
</div>
</div>
<div class="pk-outro clip" data-start="36.0" data-duration="4" data-track-index="19" style="animation-delay: 36.0s;">
<div class="pk-outro" style="animation-delay: 36.0s;">
<span class="r25-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
<span>roboco.tech</span>
</div>
</div>
</div>
</div>
</div>
<script>
@@ -337,6 +367,9 @@
}
typeReveal(typeEl);
}
window.PanelKit.choreographAllCursors();
window.PanelKit.choreographAllCameras();
})();
</script>
</body>