Files
roboco/motion/compositions/release-0.25.0/square.html
3e801697f7 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>
2026-07-17 03:58:08 +02:00

351 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="40" data-fps="30">
<head>
<meta charset="utf-8" />
<title>RoboCo v0.25.0 Release - square (1080×1080)</title>
<link rel="stylesheet" href="../../kit/kit.css" />
<script src="props.js"></script>
<script src="../../kit/kit.js"></script>
<style>
/* Square cut of the v0.25.0 release clip. Tighter spacing, smaller hero,
and the stats overlay covers the full frame for readability. Total 40s. */
.r25-hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
background: var(--pk-bg);
z-index: 30;
animation: r25-hero-out 0.6s var(--pk-ease) 2.6s forwards;
}
@keyframes r25-hero-out { to { opacity: 0; visibility: hidden; } }
.r25-hero__markwrap { position: relative; width: 400px; height: 400px; }
.r25-hero__mark {
width: 400px;
height: 400px;
transform: scale(0.82);
opacity: 0;
filter: drop-shadow(0 12px 60px rgba(255, 90, 31, 0.18));
animation: r25-mark-in 1s var(--pk-ease) 0.2s forwards;
}
@keyframes r25-mark-in { to { transform: scale(1); opacity: 1; } }
.r25-hero__ring {
position: absolute;
inset: 6px;
border-radius: 50%;
padding: 3px;
background: conic-gradient(from 0deg, transparent 0 78%, var(--pk-accent) 92%, transparent 100%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
animation: r25-ring-in 0.3s linear 0.9s forwards, r25-ring-spin 1.4s cubic-bezier(0.5, 0, 0.3, 1) 0.9s forwards;
}
@keyframes r25-ring-in { to { opacity: 1; } }
@keyframes r25-ring-spin { to { transform: rotate(360deg); opacity: 0; } }
.r25-hero__version {
font-family: var(--pk-font-display);
font-size: 96px;
color: var(--pk-text);
opacity: 0;
transform: translateY(18px);
animation: pk-enter-rise 0.7s var(--pk-ease) 1.2s forwards;
}
.r25-hero__tag {
font-size: 26px;
color: var(--pk-text-muted);
opacity: 0;
transform: translateY(14px);
animation: pk-enter-rise 0.7s var(--pk-ease) 1.5s forwards;
}
.pk-frame { animation-delay: 2.6s; }
.pk-frame__sidebar {
width: 220px;
align-items: stretch;
gap: 6px;
padding: 24px 16px 0 16px;
transform: translateX(-40px);
opacity: 0;
animation: r25-side-in 0.7s var(--pk-ease) 2.8s forwards;
}
@keyframes r25-side-in { to { transform: translateX(0); opacity: 1; } }
.r25-side__logo {
width: 40px;
height: 40px;
border-radius: 12px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 18px 4px;
}
.r25-side__logo img { width: 30px; height: 30px; }
.r25-nav {
display: flex;
align-items: center;
height: 44px;
padding: 0 14px;
border-radius: 10px;
font-size: 18px;
color: var(--pk-text-muted);
}
.r25-nav--active { background: var(--pk-active-bg); color: var(--pk-active-fg); font-weight: 600; }
.pk-frame__topbar { left: 220px; height: 84px; padding: 0 32px; }
.pk-frame__content { left: 220px; top: 84px; }
.r25-user { font-size: 18px; }
.r25-user__chip { padding: 6px 14px; }
.pk-frame__search { width: 280px; height: 42px; font-size: 18px; }
.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;
/* 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; }
.r25-stats {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
text-align: center;
background: rgba(10, 10, 10, 0.88);
z-index: 20;
font-family: var(--pk-font-display);
opacity: 0;
animation: r25-stats-in 0.4s var(--pk-ease) 24.0s forwards, r25-stats-out 0.5s var(--pk-ease) 32.0s forwards;
}
@keyframes r25-stats-in { to { opacity: 1; } }
.r25-stats__line {
font-size: 80px;
line-height: 1.25;
color: var(--pk-text);
opacity: 0;
transform: translateY(24px) scale(0.96);
animation: pk-enter-rise-scale 0.55s var(--pk-ease) forwards;
}
.r25-stats__line--accent { color: var(--pk-accent); }
@keyframes r25-stats-out { to { opacity: 0; } }
.pk-toast { --pk-toast-bottom: 84px; width: 480px; }
.pk-outro { --pk-outro-bottom: 28px; }
.r25-outro__logo {
width: 48px;
height: 48px;
border-radius: 14px;
background: #f4f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.r25-outro__logo img { width: 36px; height: 36px; }
#typeIntro { display: inline-block; overflow: hidden; white-space: pre; vertical-align: bottom; width: 0; }
@keyframes type-reveal { to { width: var(--type-ch, 20ch); } }
</style>
</head>
<body>
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1080" data-start="0" data-duration="40" data-fps="30">
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
<div class="r25-hero__markwrap">
<img class="r25-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
<div class="r25-hero__ring"></div>
</div>
<div class="r25-hero__version">v0.25.0</div>
<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>
<div class="r25-nav">Overview</div>
<div class="r25-nav">Business</div>
<div class="r25-nav">Social</div>
<div class="r25-nav r25-nav--active">Tasks</div>
<div class="r25-nav">Kanban</div>
<div class="r25-nav">Git</div>
<div class="r25-nav">Agents</div>
<div class="r25-nav">Metrics</div>
</aside>
<header class="pk-frame__topbar">
<div class="pk-frame__search">Search tasks, agents...</div>
<div class="r25-user">
<div class="pk-frame__status clip" data-start="2.6" data-duration="37.4" data-track-index="0">
<span class="pk-frame__statusdot"></span>
<span>Live</span>
</div>
<span class="r25-user__chip">CEO · Renzo</span>
</div>
</header>
<div class="pk-frame__content">
<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>
<div class="pk-column">
<div class="pk-column__header">Shipped in v0.25.0</div>
<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" 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" 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" 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" 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" 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" 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" 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>
<!-- 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>
<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" 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>
<div class="pk-toast__text" id="toastBody">I approved once. 25 agents did the rest.</div>
</div>
</div>
<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>
window.__timelines = window.__timelines || {};
window.__timelines["release-0.25.0"] = {
id: "release-0.25.0",
duration: 40,
fps: 30,
animations: []
};
(function () {
var props = (window.__PROPS__ && typeof window.__PROPS__ === "object") ? window.__PROPS__ : {};
function setText(id, value) {
if (typeof value !== "string" || !value) return;
var el = document.getElementById(id);
if (el) el.textContent = value;
}
setText("toastTitle", props.toastTitle);
setText("toastBody", props.toastBody);
function typeReveal(el) {
var n = (el.textContent || "").length || 1;
el.style.setProperty("--type-ch", (n + 1) + "ch");
el.style.animation = "type-reveal " + (n * 0.07).toFixed(2) + "s steps(" + n + ", end) 4.0s forwards";
}
var typeEl = document.getElementById("typeIntro");
if (typeEl) {
if (typeof props.introText === "string" && props.introText) {
typeEl.textContent = props.introText;
}
typeReveal(typeEl);
}
window.PanelKit.choreographAllCursors();
window.PanelKit.choreographAllCameras();
})();
</script>
</body>
</html>