Project layout (created by tools/new-project.mjs)
<project>/
script.json lines (id, scene, text, de), voice, bpm, scene plan, music
comp/index.html the stage (1920×1080) + the review bar
comp/style.css tokens, layout
comp/engine.js time helpers, captions, camera, spotlight, cursor, player
comp/main.js THE composition: frame(t)
tools/ vo.py, words.py, timeline.mjs, music.mjs, serve.mjs, render.mjs, mix.mjs, srt.mjs
audio/vo/*.wav one clip per line (+ durations.json, words.json)
audio/music.wav, audio/mix.wav
build/timeline.json lines at beat-grid times with absolute word times
assets/ screenshots, logos, documents
footage/ recorded frames + frames.json [{file, t}]
shots/ preview stills and contact sheets
out/ final MP4 and SRT
The contract
frame(t)sets every style fromtalone. No CSS transitions, no timers, no state between frames.window.__seek(t)awaits image decodes and drawst(renderer).window.__timingexposes{fps, duration, sfx, lines}for the mixer.?render=1hides the review bar;?subs=deturns on subtitles;?t=12.5opens at a time.- Kinetic type runs on one paused GSAP timeline (
master) with a label per voice line;frame(t)callsmaster.time(t, true)first. Procedural parts (camera, spotlight, cursor, footage) are computed right after.
Timing helpers
S('scene', o) // scene start + o E('scene', o) // scene end + o
L('line', o) // line start + o LE('line', o) // line end + o
W('line', i) // start of spoken word i; throws if the word does not exist
W throwing is deliberate: when the voice is re-recorded, a missing word fails loudly instead of silently mistiming the edit. Without word times (draft before the voice exists), the engine estimates them evenly across the line.
The live review loop (do this instead of rendering MP4s)
node tools/serve.mjs(default port 5199) serves only the project folders the page needs, with HTTP range support for audio.- Open
http://127.0.0.1:5199/comp/index.html. Play / Space, scrub bar, time readout, subtitle toggle. The bar drivesframe()fromaudio.currentTimeofaudio/mix.wav(falls back tomusic.wav, then a silent clock). - The user reviews, asks for changes; you edit and they reload. Only the approved cut is rendered.
- For your own QA use stills:
node tools/render.mjs preview 0,4.2,13.1,...writesshots/preview_*.pngandshots/preview_sheet.png(3 columns). Pick each settled state plus mid-transition moments, and look at every sheet.
The stage is 1920×1080 and is scaled to fit the window with translate(-50%,-50%) scale(k) on an absolutely centred stage (grid centring overflows to the right for oversized children).
Rendering
node tools/render.mjs video out/work/v.mp4 blur=4 fps=60 [from-to] [subs=de]
- Headless Edge/Chromium via playwright-core, screenshot per subframe piped as JPEG into ffmpeg (
image2pipe,libx264 -crf 15 -preset slow -pix_fmt yuv420p). - 4 subframes at 60 fps for a 68 s film: about 12 minutes on a laptop. Run it in the background.
- Fast moves (floods, wipes, drags) want 8 subframes: render that section with
blur=8 <from>-<to>and the rest atblur=4, then join the parts, so render time grows only where needed (a shutter of about 0.7 of a frame is the target; per-frame shutter control is not in the template yet). Look at a fast-move frame pair zoomed in for ghost steps (gotchas.md). - WebGL in headless Edge: add
--use-angle=d3d11 --ignore-gpu-blocklistandpreserveDrawingBuffer: true. - The renderer writes
build/comp_timing.json(sfx cues, line times) for the mixer.
Then: node tools/mix.mjs (music + VO + SFX, ducked, -14 LUFS) and mux: ffmpeg -i v.mp4 -i audio/mix.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 256k -shortest -movflags +faststart out/name.mp4
Footage inside the composition
- Store screen recordings as numbered JPEG frames +
frames.json([{file, t}], source seconds). - Time-remap per segment
[outStart, outEnd, srcStart, srcEnd]so the typing, the send click and the result land on the voice; binary-search the frame for the source time. - Warm the cache on load (create
Image()objects for every frame the segments use) so live playback does not stutter. - Swap stills for states the recording doesn't hold cleanly (a modal, a final result).
Windows notes
- PowerShell 5.1 mangles UTF-8; write files from Node or Python, not
Set-Content. - Smart App Control may block bundled ffmpeg; use the system ffmpeg.
- Long bash heredocs with backticks and quotes can break; write edit scripts to a file and run them.