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:
+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