mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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.
|
||||
|
||||
@@ -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"');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,8 @@ Every class is namespaced `pk-`. See `compositions/panel-demo/` for a full worke
|
||||
- **`pk-pill`** — a status pill: `<span class="pk-pill pk-pill--progress">in progress</span>`. Variants: `--progress`, `--review`, `--approved`, `--completed`. To swap one pill for another (e.g. progress -> completed), stack two pills at the same spot inside a `position: relative` `pk-card__status` and give the outgoing one `pk-pill--swap-out` (fades in then out) and the incoming one `pk-pill--swap-in` (fades in and stays), each with its own `--pk-pill-enter-delay` / `--pk-pill-exit-delay` (composition-absolute seconds — the same convention `data-start` uses).
|
||||
- **`pk-toast`** — a notification card that slides in from a corner: `pk-toast__icon` + `pk-toast__body` (`pk-toast__title`, `pk-toast__text`). Position via `--pk-toast-bottom` (right-anchored).
|
||||
- **`pk-type`** / **`pk-caret`** — character-by-character typing reveal. Write the target element's final text directly in the HTML, then call `window.PanelKit.typeText(el, { delay, stagger })` (from `kit.js`) once, synchronously, before rendering starts — it splits the text into `pk-type__char` spans with staggered `animation-delay`s (works with any font, no per-character width math). `delay` is the composition-absolute second the first character appears; `stagger` defaults to 0.045s/char. Add a `<span class="pk-caret"></span>` next to it for the blinking caret.
|
||||
- **`pk-cursor`** — a small CSS-shape cursor (`pk-cursor__glyph`) that glides along a straight path and pulses (`pk-cursor__ring`) on arrival. Position via `--pk-cursor-x0/y0/x1/y1` (content-box-relative pixels) and timing via `--pk-cursor-delay` (glide start) / `--pk-click-delay` (pulse start), all composition-absolute seconds.
|
||||
- **`pk-cursor`** — a small CSS-shape cursor (`pk-cursor__glyph`). Preferred: give it `data-waypoints="t x y [click]; t x y; ..."` (composition-absolute seconds, content-box px) and call `PanelKit.choreographAllCursors()` in the composition's inline script — kit.js generates a multi-leg eased path, a fade-in/out (never pop in or blink out), an idle-hand sway between legs, and a click ring + glyph press dip at every waypoint flagged `click`. The click should land ~0.2s BEFORE the thing it visually triggers. Legacy single-glide via `--pk-cursor-x0/y0/x1/y1` + `--pk-cursor-delay`/`--pk-click-delay` still works for a one-move cameo.
|
||||
- **`pk-camera`** — a full-stage wrapper driven by `data-shots="t x y scale; ..."` + `PanelKit.choreographAllCameras()`: eased camera moves (push-ins toward the beat's focal point, pull-backs for reveals). Wrap the whole `pk-frame` in one; everything inside, cursor included, rides the move. Keep it subtle — `scale <= 1.08`, translate <= ~160px — and end settled at identity.
|
||||
|
||||
## Offline constraints (same as every composition)
|
||||
|
||||
|
||||
@@ -404,6 +404,28 @@ html, body {
|
||||
clip-path: polygon(0 0, 0 70%, 22% 55%, 38% 88%, 52% 82%, 36% 50%, 62% 50%);
|
||||
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
/* Idle-hand micro-drift between path legs (kit.js wraps the glyph in this
|
||||
when choreographing data-waypoints); a pointer frozen pixel-still while
|
||||
the scene moves reads as a rendering glitch. */
|
||||
.pk-cursor__sway {
|
||||
animation: pk-cursor-sway 3.2s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes pk-cursor-sway {
|
||||
from { transform: translate(0, 0); }
|
||||
to { transform: translate(2.5px, 1.5px); }
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- *
|
||||
* pk-camera — a full-stage wrapper kit.js drives through data-shots
|
||||
* (see choreographCamera). Wrap the scene (e.g. the whole .pk-frame) in
|
||||
* one of these; everything inside, cursor included, rides the move.
|
||||
* ---------------------------------------------------------------------- */
|
||||
.pk-camera {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
will-change: transform;
|
||||
transform-origin: 50% 42%;
|
||||
}
|
||||
.pk-cursor__ring {
|
||||
position: absolute;
|
||||
left: -14px;
|
||||
|
||||
+155
-1
@@ -32,5 +32,159 @@
|
||||
}
|
||||
}
|
||||
|
||||
window.PanelKit = { typeText: typeText };
|
||||
var pathCounter = 0;
|
||||
|
||||
// Choreographs a .pk-cursor along data-waypoints: "t x y [click]; ..." —
|
||||
// composition-absolute seconds, content-box px. Injects generated
|
||||
// @keyframes so the cursor TRAVELS through every waypoint with per-leg
|
||||
// easing, fades in at the first stop and out at the last, sways idly
|
||||
// like a resting hand, and pulses a click ring (plus a glyph press dip)
|
||||
// at each waypoint flagged `click`. The single-glide --pk-cursor-x0/x1
|
||||
// API stays untouched for cursors without data-waypoints — this engine
|
||||
// only takes over when the attribute is present.
|
||||
function choreographCursor(el) {
|
||||
if (!el || !el.getAttribute) return;
|
||||
var raw = el.getAttribute("data-waypoints");
|
||||
if (!raw) return;
|
||||
var points = [];
|
||||
raw.split(";").forEach(function (chunk) {
|
||||
var parts = chunk.trim().split(/\s+/);
|
||||
if (parts.length < 3) return;
|
||||
points.push({
|
||||
t: parseFloat(parts[0]),
|
||||
x: parseFloat(parts[1]),
|
||||
y: parseFloat(parts[2]),
|
||||
click: parts[3] === "click"
|
||||
});
|
||||
});
|
||||
if (points.length < 2) return;
|
||||
|
||||
var t0 = points[0].t;
|
||||
var span = points[points.length - 1].t - t0;
|
||||
if (!(span > 0)) return;
|
||||
var name = "pk-cursor-path-" + (++pathCounter);
|
||||
var pct = function (t) {
|
||||
return Math.min(100, Math.max(0, ((t - t0) / span) * 100)).toFixed(3);
|
||||
};
|
||||
|
||||
var css = "@keyframes " + name + " {\n";
|
||||
points.forEach(function (p, i) {
|
||||
css += " " + pct(p.t) + "% { transform: translate(" + p.x + "px, " + p.y + "px);";
|
||||
if (i < points.length - 1) {
|
||||
css += " animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);";
|
||||
}
|
||||
css += " }\n";
|
||||
});
|
||||
css += "}\n";
|
||||
// Fade in over the first 0.4s, out over the last 0.6s — a cursor that
|
||||
// pops in or blinks out of existence reads as a bug, not a hand.
|
||||
css +=
|
||||
"@keyframes " + name + "-fade {\n" +
|
||||
" 0% { opacity: 0; }\n" +
|
||||
" " + pct(t0 + 0.4) + "% { opacity: 1; }\n" +
|
||||
" " + pct(t0 + span - 0.6) + "% { opacity: 1; }\n" +
|
||||
" 100% { opacity: 0; }\n}\n";
|
||||
|
||||
var clicks = points.filter(function (p) { return p.click; });
|
||||
if (clicks.length) {
|
||||
// Press dip on the glyph, one dip per click, expressed as stops on a
|
||||
// single generated keyframes rule spanning the whole path.
|
||||
css += "@keyframes " + name + "-press {\n 0% { transform: scale(1); }\n";
|
||||
clicks.forEach(function (p) {
|
||||
css +=
|
||||
" " + pct(p.t - 0.12) + "% { transform: scale(1); }\n" +
|
||||
" " + pct(p.t) + "% { transform: scale(0.85); }\n" +
|
||||
" " + pct(p.t + 0.22) + "% { transform: scale(1); }\n";
|
||||
});
|
||||
css += " 100% { transform: scale(1); }\n}\n";
|
||||
}
|
||||
|
||||
var style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Sway wrapper: the path moves the cursor, the wrapper breathes — a
|
||||
// perfectly still pointer between legs reads as a freeze-frame.
|
||||
var glyph = el.querySelector(".pk-cursor__glyph");
|
||||
if (glyph) {
|
||||
var sway = document.createElement("div");
|
||||
sway.className = "pk-cursor__sway";
|
||||
glyph.parentNode.insertBefore(sway, glyph);
|
||||
sway.appendChild(glyph);
|
||||
if (clicks.length) {
|
||||
glyph.style.animation = name + "-press " + span + "s linear " + t0 + "s both";
|
||||
}
|
||||
}
|
||||
clicks.forEach(function (p) {
|
||||
var ring = document.createElement("div");
|
||||
ring.className = "pk-cursor__ring";
|
||||
ring.style.setProperty("--pk-click-delay", p.t + "s");
|
||||
el.appendChild(ring);
|
||||
});
|
||||
|
||||
el.style.animation =
|
||||
name + " " + span + "s linear " + t0 + "s both, " +
|
||||
name + "-fade " + span + "s linear " + t0 + "s both";
|
||||
}
|
||||
|
||||
function choreographAllCursors() {
|
||||
var els = document.querySelectorAll(".pk-cursor[data-waypoints]");
|
||||
for (var i = 0; i < els.length; i++) choreographCursor(els[i]);
|
||||
}
|
||||
|
||||
// Camera moves on a .pk-camera wrapper via data-shots: "t x y scale; ..."
|
||||
// — composition-absolute seconds, a translate in px and a zoom factor per
|
||||
// shot, eased leg by leg like the cursor path. A locked-off camera for a
|
||||
// whole clip is the single biggest "screen recording, not a video" tell;
|
||||
// push toward what matters each beat, pull wide for reveals, settle to
|
||||
// end. Keep it subtle: scale <= 1.08, translate <= ~160px.
|
||||
function choreographCamera(el) {
|
||||
if (!el || !el.getAttribute) return;
|
||||
var raw = el.getAttribute("data-shots");
|
||||
if (!raw) return;
|
||||
var shots = [];
|
||||
raw.split(";").forEach(function (chunk) {
|
||||
var parts = chunk.trim().split(/\s+/);
|
||||
if (parts.length < 4) return;
|
||||
shots.push({
|
||||
t: parseFloat(parts[0]),
|
||||
x: parseFloat(parts[1]),
|
||||
y: parseFloat(parts[2]),
|
||||
s: parseFloat(parts[3])
|
||||
});
|
||||
});
|
||||
if (shots.length < 2) return;
|
||||
|
||||
var t0 = shots[0].t;
|
||||
var span = shots[shots.length - 1].t - t0;
|
||||
if (!(span > 0)) return;
|
||||
var name = "pk-camera-shots-" + (++pathCounter);
|
||||
var css = "@keyframes " + name + " {\n";
|
||||
shots.forEach(function (p, i) {
|
||||
var pct = Math.min(100, Math.max(0, ((p.t - t0) / span) * 100)).toFixed(3);
|
||||
css += " " + pct + "% { transform: translate(" + p.x + "px, " + p.y + "px) scale(" + p.s + ");";
|
||||
if (i < shots.length - 1) {
|
||||
css += " animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);";
|
||||
}
|
||||
css += " }\n";
|
||||
});
|
||||
css += "}\n";
|
||||
var style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
el.style.animation = name + " " + span + "s linear " + t0 + "s both";
|
||||
}
|
||||
|
||||
function choreographAllCameras() {
|
||||
var els = document.querySelectorAll(".pk-camera[data-shots]");
|
||||
for (var i = 0; i < els.length; i++) choreographCamera(els[i]);
|
||||
}
|
||||
|
||||
window.PanelKit = {
|
||||
typeText: typeText,
|
||||
choreographCursor: choreographCursor,
|
||||
choreographAllCursors: choreographAllCursors,
|
||||
choreographCamera: choreographCamera,
|
||||
choreographAllCameras: choreographAllCameras
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user