4.8 KiB
Lane B — Timeline: master clock, tracks, keyframes, evaluation
You own: scenegod/web/timeline.js, tlui.js (+ your marked block at the
bottom of style.css, + your dev-only stagestub.js until SYNC 1).
Milestone: M1. You develop against the Stage API in PLAN §4.2 — code to
the contract, not to Lane A's implementation.
B1. stagestub.js (first commit, deleted at SYNC 1)
Implements the full Stage API from PLAN §4.2 with plain objects: entities
are {id, kind, transform, params}, setTransform/setParam record into a
stub.applied array and console.debug. prepareClip returns a fake
{duration: 2.4} clip. This lets every timeline feature be built and tested
headless before Lane A lands.
B2. timeline.js — the model + clock (no DOM in this file)
- Owns the authoritative scene fields:
fps,duration,entities[].tracks,cameraCuts.load(sceneJson)/toJSON()round-trip losslessly — unknown fields on entities are preserved (Lane A owns them). - Master clock:
play()usesrequestAnimationFramedeltas;seek(t)clamps to[0, duration]; every advance callsevaluate(t)thenonTicklisteners.step(frame)=seek(frame/fps)with NO raf — pure and deterministic (Lane C's final render calls it in a loop). evaluate(t)per entity:- Transform track: binary-search the bracketing keys, interpolate —
pos/scale lerp, rot via Quaternion.slerp (convert stored eulers once at
load, cache quats on the key objects), easing (
linear|in|out|inout= cubic,step= hold) applied to the segment's normalized u. 0 keys → rest transform; 1 key → hold. Result →stage.setTransform(id, …). - Params track: same interpolation per
keyname (numbers lerp, colors lerp in sRGB — fine for v1) →stage.setParam. - Clip blocks (characters): a block covers
[start, start + (out-in)*loop]. If t inside: local clip time =in + ((t-start) % (out-in)); drive via the entity's mixer — oneAnimationActionper block,action.time = localTime; action.weight = w; mixer.update(0)(set-time style, NOT free-running — scrubbing must be exact). Crossfade: in the lastfadeseconds of a block that has a successor, ramp weight 1→0 while successor ramps 0→1. Outside any block: weight 0 (character holds rest / last transform). - Camera cuts: last cut with
cut.t <= t→stage.setActiveCamera.
- Transform track: binary-search the bracketing keys, interpolate —
pos/scale lerp, rot via Quaternion.slerp (convert stored eulers once at
load, cache quats on the key objects), easing (
- Keyframe ops API (tlui + Lane A's event use these):
addKey(id, track, key),moveKey,deleteKey,addClipBlock(id, block),moveClipBlock,trimClipBlock,addCut(t, cameraId)— every mutator keeps arrays sorted bytand pushes an inverse op onto a simple undo stack (M2: wire to Ctrl+Z; the stack itself costs nothing now). - Listen for
scenegod:capturekey(see Lane A dock): on it,addKey(id, 'transform', {t: this.time, ...stage.entityTransform(id), ease:'inout'}).
B3. tlui.js — the panel (mounts into #timeline)
- Left column: track names (one row per entity, auto-synced to
stage.entities via
onChange/poll; + Cameras row + a per-character clip sub-row). Right: time ruler + canvas-drawn track lanes (a<canvas>per lane or one big one — your call, log the decision). - Interactions (M1): click ruler = seek; drag playhead; space = play/pause; double-click a transform lane = add key at that t from current stage state; drag key = move (snap to frame, hold Alt = free); right-click key = delete; drag a clip block horizontally; drag block edges = trim. Home/End = 0/duration.
- Scene bar: name field, duration field, Save/Load buttons →
POST/GET /scenes/{name}(Lane C; until then, localStorage fallback — keep the fetch code, catch and fall back). - Keep ALL DOM here; timeline.js must run headless under node for tests.
B4. self-check — web/timeline_test.mjs
Runs under plain node (import timeline.js + stagestub): build a scene with
2 transform keys + 1 clip block + 2 camera cuts, step() through 90 frames,
assert: interpolated midpoint position, ease-step hold, clip local time at
3 sample frames, active camera flips at the cut, toJSON() deep-equals a
re-loaded copy. node web/timeline_test.mjs green = M1 code-done.
M1 acceptance (log it)
- timeline_test.mjs passes.
- In the browser against the stub: scrub bar moves entities in the stub log, keys draggable, play/pause works, save/load round-trips via localStorage.
- Do NOT integrate with the real Stage yourself — that's SYNC 1, the orchestrator runs it (your code should make it a one-line swap).
M2 preview (locked until orchestrator flips it)
Clip crossfades polish, params lanes UI, camera-cut lane UI, snapping config, undo wiring, box-select + multi-drag of keys.
ORCHESTRATOR UPDATES
(none yet — check back each session)