From 3e801697f7d4552907bf22b80e7f80ae5f7cc3ef Mon Sep 17 00:00:00 2001 From: Renzo F <45401804+rennf93@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:58:08 +0200 Subject: [PATCH] feat(motion): choreography engines + two renderer root causes fixed (#543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- motion/README.md | 10 + .../release-0.25.0/release-0.25.0.test.js | 15 +- .../compositions/release-0.25.0/square.html | 68 +- .../compositions/release-0.25.0/vertical.html | 75 +- motion/kit/README.md | 3 +- motion/kit/kit.css | 22 + motion/kit/kit.js | 156 +- roboco/runtime/orchestrator.py | 7 + video-renderer/package-lock.json | 3622 +++++++++++++++++ video-renderer/package.json | 2 +- 10 files changed, 3935 insertions(+), 45 deletions(-) create mode 100644 video-renderer/package-lock.json diff --git a/motion/README.md b/motion/README.md index 89515ac1..472e564c 100644 --- a/motion/README.md +++ b/motion/README.md @@ -36,6 +36,16 @@ This composition is the library's reference point — match its restraint, don't - **Layout** — anchor content asymmetrically (this clip sits in the lower two-thirds, left-aligned); avoid a perfectly centered card, which reads as a generic template rather than a designed frame. - **AI tells to avoid** — no default AI-purple gradient wash, no centered-everything, no emoji as design elements, no one-font-one-size, no em dash in on-screen copy (voiceover script, highlight bullets, kicker text) or filler verbs ("Elevate", "Seamless", "Unleash", "Next-Gen"). +## Cinematography & rhythm (demo register) + +A panel-demo clip is a FILM of software being used, not a screenshot with captions. Before authoring, write a shot list: for every beat, where is the camera, where is the cursor, what changes on screen, and what caused it. Then build to it. The specific tells that get a cut bounced: + +- **A locked-off camera.** Wrap the scene in `pk-camera` and drive it with `data-shots` (see `kit/README.md`): open slightly tight, push toward each beat's focal point (`scale <= 1.08`, translate <= ~160px — the audience should feel it, not see it), pull wide for a reveal, settle to end. A static frame for more than ~8s is dead air. +- **A cursor that doesn't behave like a hand.** Drive it with `data-waypoints`: it fades in, travels with eased legs, rests with an idle sway (kit adds this), clicks with a visible cause→effect (the click precedes the thing it triggers), and leaves the frame — it never pops in, freezes pixel-still, or blinks out mid-scene. +- **A metronome.** Identical beat lengths with identical flat entrances read as a slideshow. Vary entrance energy (the kit cards take a springy overshoot well), let a beat breathe after a click, and give the climax (stats/receipt) a different rhythm than the build. +- **Verify motion, not stills.** After `request_render`, sample PAIRS of frames ~0.5s apart around each cursor/camera beat and compare positions — a single frame proves presence, only a pair proves movement. +- **Clip windows are for structural layers only.** The renderer's per-clip visibility scheduler drifts badly behind the encoded timeline on long compositions (measured live: a 40s cut whose per-beat clips only reached the ~19s mark by the final frame — the entire tail silently missing from the MP4). Give `class="clip"` + `data-start`/`data-duration` only to full-length structural layers (the cold-open, the panel frame), and drive every BEAT inside them with the kit's pattern instead: base-hidden styles (`opacity: 0`) plus a delayed CSS animation (`animation-delay` + `forwards`/`both` fill) — those run on the correct clock. If a beat must also disappear, give it an exit animation, not a clip window. + ## Panel-demo kit (`kit/`) `kit/` is a second register alongside the release-announcement's text-card style: reusable `pk-`-namespaced CSS/HTML that recreates the control panel's look (dark chrome, task cards, status pills, toasts, a typing reveal, a cursor) so a composition can simulate the product actually being used, instead of announcing it over a headline. Use the **text-card register** (release-announcement's pattern) for version/feature announcements with no product visuals; use the **demo register** (`kit/`) whenever the story is "watch this happen in the app" — a task moving through the panel, a feature being triggered, an agent doing something visible. diff --git a/motion/compositions/release-0.25.0/release-0.25.0.test.js b/motion/compositions/release-0.25.0/release-0.25.0.test.js index c9bb1661..3a103246 100644 --- a/motion/compositions/release-0.25.0/release-0.25.0.test.js +++ b/motion/compositions/release-0.25.0/release-0.25.0.test.js @@ -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"'); diff --git a/motion/compositions/release-0.25.0/square.html b/motion/compositions/release-0.25.0/square.html index b9bb0a0c..ed008000 100644 --- a/motion/compositions/release-0.25.0/square.html +++ b/motion/compositions/release-0.25.0/square.html @@ -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 @@
governance gets a better UI
+ +