Seek engine
The whole trick is one rule: a frame is a pure function of its time. window.seek(t) paints exactly the frame for t, every time, in any order. Frame 431 always looks the same, a fix is a one-line edit plus a re-render, and any single frame can be checked without playing the film. Everything else is that function, grown up.
Apply only the routes and checks for the selected job. Read a linked skill's named file from its installed folder when available, or from the linked source. Report an unavailable required reference instead of inventing it. [skill-authoring, DISTRIBUTION.md#composing-skills]
Start a film
From the film folder (make-video scaffolds it):
cp <this skill>/templates/index.html .
cp <this skill>/scripts/render.mjs <this skill>/scripts/LICENSE-anatomy-reveal.txt <this skill>/scripts/add-font.mjs <this skill>/scripts/gradient.py scripts/
npm install # playwright, from the scaffolded package.json
open index.html # live preview: click for sound in sync, space pauses, arrows seek 2 s, r restarts; ?t=2.5 ?format=9:16 ?style=night
The template's demo (a pill that morphs to a card and a circle, driven by a cursor, looping) exists to show the kit working. It is one idea, not a template: keep the FILM, MOTION LIBRARY, LOOK, LAYOUT and API blocks, and replace the SCENE block with your film.
The contract
- No clock. No CSS
transition, animation or @keyframes, no setTimeout, no requestAnimationFrame in render mode, no Date.now() or performance.now(). The preview loop in the template is the only clock, and it is off when window.__RENDER__ is set.
- No carried state. Nothing accumulates between frames: no counters, no physics stepped frame by frame, no "previous value". Compute everything from
t in closed form. A simulation that must integrate is precomputed once, deterministically, into an array indexed by time.
- No unseeded randomness.
rng(seed) and hash(i) only, never Math.random().
- Values with many targets are sums of springs.
track([[t, value, spring], ...]) adds one closed-form spring per change, so frame 812 renders without simulating frames 0-811. Colours use colorTrack.
- The page exposes the API:
seek(t) (it may be async, for real footage; the renderer awaits it), configure({ beat, format, style }) (rebuild every timed thing when the beat changes), and timeline() returning { duration, fps, loop, beat, bpm, format, formats, styles, size, cues, cuts }. cuts lists designed hard cuts in seconds, so the pop scan doesn't flag them. Optional: speed(t, dt) returns the largest on-screen travel, in the frame's own pixels (the units of size), of anything visible between t and t + dt; it only reads the timeline and paints nothing. When it exists, render.mjs full sizes each frame's motion blur from it.
- Comment every tween with its duration, curve and overshoot, in that order, so the code can be checked against the beat table.
- Readable text lives in the DOM, so the smoke test can find overlaps and edges. Canvas, SVG and WebGL are for everything else, all drawn from
t.
- Fonts are local.
node scripts/add-font.mjs "Inter" --weights 400,600 vendors any Google Fonts family (every subset, Hebrew included) into assets/fonts/ with its @font-face CSS; link that CSS. The renderer waits for document.fonts.ready. A network font that fails silently renders a fallback in every frame.
- GSAP is welcome when paused. Build the scene's GSAP timeline paused and call
tl.seek(t) inside window.seek; GSAP then moves every layer exactly like the springs do. A timeline left playing on its own clock breaks the contract.
- One timing source. States, copy and cues are written in beats (
beat(n)) in data arrays at the top of the scene. The shot list's times and these arrays are the same numbers.
Motion
Choose every duration, curve and spring from the number sheet, never by feel: MOTION-NUMBERS.md. Adjectives produce the model's defaults; numbers produce the film. The template implements it: SPR springs and EASE curves; track and colorTrack (sums of springs); kf, tween, swap, lmix; hitPulse and squash for hits and contact; arrive (vertical rises that don't tick), durFor (duration from distance), whole (discrete state per whole frame), exposure (drawings on twos or held while the camera stays smooth), timeWarp (hit-stop: game time freezes on contacts while effects keep running); and the sync helpers peakOf, settleOf and springReach, which compute where each sound belongs from the motion itself. Before writing motion code, list each beat with its start, duration, curve and overshoot in a table; the review checks the render against it.
Techniques live in one reference per kind of film. Read only the one the shot list calls for:
- TECHNIQUES.md: every film. Collections (galleries, carousels, rings, coverflow) as poses blended per value, the anchor camera and parallax, CSS and WAAPI pages, cursors, seams, floods, irises, smears, liquid glass, type and right-to-left text, product UI that tells the truth, loops, finish and texture, OKLab colour and banding-free gradients, and the Chromium traps table.
- TECHNIQUES-COLOUR-AND-FINISH.md: split from TECHNIQUES.md: collections as poses, loops, OKLab colour and banding-free gradients, finish and texture, and the Chromium traps table.
- KINETIC-TYPE.md: words in motion. Reading time as arithmetic, reveals with their numbers, letters with mass, motion that means what the words mean, writing that builds itself, letters that morph into shapes, exits in each device's own voice, the five composition models (fixed anchor, accretion, growing line, world-space, annotation) and the speed-read device.
- STOP-MOTION.md: a deliberately held-pose film, with a global pose grid, fractional frame boundaries, visible pose spacing, shared picture/audio events and explicit alternatives to moving holds and smooth cameras. [stopmotion-video, references/clock.md; references/poses.md; references/sound.md]
- WORD-FOLD.md: a walking compass strip reveals one word and becomes its final letter; supplied glyph/ending guides, kit, sounds and corrected helpers in
templates/word-fold/. This source uses CoAnimator's player/timeline contract, not the default renderer API. [word-fold, SKILL.md and bundled kit]
- ILLUSTRATION.md: hand-drawn, ink, watercolour, characters (rigs, walk cycles, IK, acting), game feel and hit-stop, pixel art. Copy
templates/pen.js (brush, pencil, watercolour, self-writing text, stable seeds, opt-in boil); the drawing kit is templates/drawkit.js plus templates/drawkit-nature.js and templates/drawkit-cast.js, with load order and namespace in that reference.
- DEPTH-AND-FOOTAGE.md: real footage and clips, green-screen keying, stills in 2.5D, infinite zoom, 3D, faceted low-poly and extruded type, physics and data-slot numbers.
- SHADERS.md: per-pixel looks in WebGL with twgl.js (
npm i twgl.js): moving noise gradients, glows, tunnels, kaleidoscopes, melting distance-field shapes, post passes, and feedback trails or GPU particles made deterministic by a fixed-step replay.
- ISOMETRIC.md: how something works, explained by an invented machine whose moving parts do the idea (a queue as a marble run, a cache as a bookcase): the object-is-the-explanation rule, parts lists, four-tone solids, one lit part, painter's order, exploded views, and an isometric figure driven by
seek(t). Its full toolset is owned by isometric-figures, under skills/web-ui/isometric-figures/templates/isokit/: the solids kit, round parts and pipes, the geometry audit, WebGL inside the drawing, the 3D mode, eight examples and their manuals; examples/turning-dial/film.js is a 3D figure driven by seek(t).
- REAL-OBJECTS.md: a real product as the subject (an exploded view, a teardown, an anatomy figure, a product reveal), from a photo or a name: dimensions, features and a parts list and how to research them, part counts, how each kind of object comes apart and its build order, and the reveal shot sequence (tumble in, macro details, one feature working, explode and snap back, hero). Read with ISOMETRIC.md.
- SIMULATIONS.md: swarms, agents, creatures with legs, machines to fly through, evidence walls, maps, terrain, crowds and light.
- DIRECTOR.md: a film cut automatically to a track, with captions on the beat, flashes and glitches.
- EFFECTS.md: sixteen product and type effects, each one object becoming another (button to player, search to results, chart morph, dashboard zoom, masked and elastic type, glass focus, flowing paths, a particle logo or number), on an 8 s loop clock or held under narration.
The measured corpus. measured/ holds thirteen references measured frame by frame from 53 professional motion moments (about 4,300 moves): durations by property, the 12 measured eases, twenty transition types with their numbers, type cadence, UI demos, endings, layout, world-building and a slop review. The template carries the 12 eases as MEASURED.<name> (softLand for landings, accelExit and crashOut for leaving, snapSettle for hero and CTA entries, popOver for the rare late overshoot, cruise for fades and drift) with their typical lengths in MEASURED_FRAMES. Use those numbers for short moments and effects; MOTION-NUMBERS.md for whole films.
A 3D hero and phone UI. templates/hero3d/hero3d.js is one seekable Three.js hero object (extruded text, an SVG logo with bevels, a coin, a glass slab, a phone, a glTF) with studio light, a moving softbox sweep, bloom and depth of field, every property a pure function of t through renderAt(t): import it from src/*.js, npm i three esbuild, pass the font as a data URL (opts.font; Inter Bold is in hero3d/fonts/, fontconv.py turns a woff2 into a static ttf), bundle with bundle.mjs. Tested: chrome type with a sweep and a coin, bundled and rendered, every smoke counter 0. templates/native-ui/ builds iOS UI at true proportions as HTML (NativeUI.phone, lockScreen, notification, banner, appScreen, list, tabBar, appIcon, statusBar); use its HTML builders independently. Its historical arrive(M, ...) and macro(M, ...) moves require the source Motion runtime (motion.js), which is absent here: they call M.enter/M.kf and M.camera/M.mark. The old line incorrectly named GSAP. [native-ui.js, arrive and macro] Every notification requires an icon (or app), a title and a real sentence; time defaults to now.
A finished example film that passes every smoke counter in every format is in examples/; copy it instead of starting from the demo.
Formats
16:9, 9:16, 1:1 and 4:5 (1080 × 1350) are different compositions of one story, never crops of one render. All layout derives from W, H and U (1% of the short side) inside setFormat(); give the vertical cut larger type, fewer simultaneous elements and its own camera path. Check each format with its own sheet. For YouTube, "avoid adding padding or black bars directly to your video" (YouTube Help): render the 16:9 cut at 16:9, not a letterboxed copy of another format.
Looks
The Cinematic UI look (oblique macro shots of real UI, optical depth of field and stipple reveals) has source-specific dark and light stages in CINEMATIC-UI.md; choose from the product's real theme. [1weiho/cinematic-shot, references/look.md]
All colour lives in TOKENS, chosen from the named sets in STYLES. A second look of the same film is a new entry in STYLES, never a fork: render.mjs full --style night renders it with identical timing. Gradient stages are baked PNGs from scripts/gradient.py, never CSS gradients (they band).
Render
node scripts/render.mjs still 0 2.5 7.9 # full-resolution stills -> out/stills/
node scripts/render.mjs sheet --every 0.5 # labelled contact sheet -> out/sheets/sheet.png
node scripts/render.mjs sheet --at 1,1.2,4.05 # exact moments
node scripts/render.mjs beats # one settled frame per beat
node scripts/render.mjs strip 3.9 4.3 # every frame of a stretch: pops, overlaps, ghosting
node scripts/render.mjs full # out/16x9.mp4: 60 fps, sub-frames blended (by speed when the page has speed()), audio/mix.wav muxed
node scripts/render.mjs full --format all # every format in FILM.formats
node scripts/render.mjs full --scale 0.5 --sub 1 # fast draft, and the animatic before polish
node scripts/render.mjs full --range 4:7 # re-render only the affected seconds
node scripts/render.mjs full --chunk 10 # long film: 10 s segments, a stopped render resumes where it left off
node scripts/render.mjs full --gpu # 3D, shader-heavy or WebGPU film: the real GPU (Metal on macOS), ~9x faster; WebGPU needs it
node scripts/bundle.mjs src.html index.html # a film with module imports (three.js): bundle into one page first
node scripts/render.mjs full --shutter 180 --sub 8 --grain 0.03 # a lens finish for a smooth film: shutter blur and seeded grain (grain encodes large: plan the file size)
node scripts/render.mjs full --alpha # transparent intro, outro or overlay: ProRes 4444 .mov (--alpha webm for the web)
node scripts/render.mjs full --data data/acme.json --out out/acme.mp4 # one film per record: the page reads window.__DATA__
A film made of several scene pages (one HTML file per scene, each with its own seek): render each with --page, then join them with no re-encode, scenes listed in order in out/scenes.txt as file '01.mp4' lines:
node scripts/render.mjs full --page scenes/01.html --out out/01.mp4 --audio none
node scripts/render.mjs full --page scenes/02.html --out out/02.mp4 --audio none
ffmpeg -f concat -safe 0 -i out/scenes.txt -c copy out/picture.mp4
ffmpeg -i out/picture.mp4 -i audio/mix.wav -map 0:v -map 1:a -c:v copy -af apad -c:a aac -b:a 256k -ar 48000 \
-t "$(ffprobe -v error -show_entries format=duration -of csv=p=0 out/picture.mp4)" -movflags +faststart out/film.mp4
Cut each scene on a beat: give each page's duration in whole beats so the joins land on the grid. The sound is mixed once for the whole film, never per scene.
Every mode takes --format, reads audio/beats.json when present, writes out/cues.json for the sound pass, and fails on any page error. Every mode also takes --style. full blurs adaptively when the page exposes speed(t, dt): each frame gets ceil(travel / --step) sub-frames (--step 4 output px, at most --most 40), the travel measured over the open shutter, so a hold renders 1 and a whip up to 40 (after anatomy-reveal's render.mjs blur, MIT, scripts/LICENSE-anatomy-reveal.txt). Each frame's sub-frames are averaged with ffmpeg tmix, one call per frame. --sub N overrides it with a fixed count for every frame; a page without speed gets --sub 4, and --sub 8 for fast moves (4 sub-frames leave ghost copies on a whip). --shutter 240 (degrees, centred on the frame; blur never crosses a declared cut), --scale 2 for a supersampled master, --workers and --crf. Chromium launches with --force-color-profile=srgb, so pixels are sRGB on any display. Output is H.264, yuv420p, BT.709-tagged, square pixels, AAC 48 kHz, +faststart. When audio/vo.srt exists (the voiceover skill writes it), a render from 0 carries it as a selectable subtitle track and writes the .srt beside the MP4. --chunk keys its segments by the top-level HTML, selected render options, beats and supplied data. Imported CSS, JavaScript, images and fonts are outside that key; use --fresh after changing those dependencies. The old line claimed every edit invalidated stale segments. The source records an identical single-pass comparison. [scripts/render.mjs, chunk key and --fresh] --gpu pixels can differ between machines: keep one machine per film. With --alpha the page sees window.__ALPHA__ and paints no background of its own (the template's drawFx already skips it); the file is video only and renders in one pass. Without a mix, an MP4 still gets a silent audio track (feeds mishandle files with none). A data-driven film reads every name, number and line from window.__DATA__ with its own default for the preview; smoke the longest record first (smoke.mjs --data), check its stills, then render every record. Tested: alpha renders carry real transparency in ProRes 4444 and VP9; two records render their own text; a malformed record fails with its file name.
Gotchas that cost real time
will-change on anything the camera scales makes its text rasterize blurry. On a layer whose filter: blur() animates, it causes nondeterministic ghost frames.
- Put all of an element's translation in one
transform; a fractional left/top plus a transform snaps to whole pixels and jitters.
- Clamp every value that feeds geometry: springs overshoot, and a negative width, radius or blur throws or flashes.
- A colour change between black and white through a spring or crossfade passes through grey. Flood the new colour from a point (the template's
#flood) or move an accent element between states.
- Frame 0 is the thumbnail and the first thing a muted feed shows: compose it. In a loop, keep frame 0 mid-hold, not mid-morph.
- Periodic decoration in a loop (drift, rotation) must divide the loop length, or the seam jumps. The smoke test catches it.
- Declare every variable before the first
seek() call; a temporal-dead-zone error only appears in render mode.
- WebGL (Three.js) is fine when the scene is rebuilt from
t each frame and any shader time uniform is t. Shaders on their own clock paint a different picture each run.
- WebGL on a live web page is the opposite case (refuse software rendering with
failIfMajorPerformanceCaveat, a DPR cap chosen by shader cost, context loss, a rAF loop): webgl SKILL.md. A film keeps SwiftShader, the fixed maxRatio and seek(t).
1---2name: seek-engine3description: "The render contract and renderer for code-made video: one HTML file whose seek(t) paints any frame, rendered to stills and MP4. Use when building, retiming or rendering a video's frames, or when a render flickers or differs between runs."4metadata:5 source: https://skillmd.com/skills/quegenx/seek-engine6---78# Seek engine910The whole trick is one rule: **a frame is a pure function of its time.** `window.seek(t)` paints exactly the frame for `t`, every time, in any order. Frame 431 always looks the same, a fix is a one-line edit plus a re-render, and any single frame can be checked without playing the film. Everything else is that function, grown up.1112Apply only the routes and checks for the selected job. Read a linked skill's named file from its installed folder when available, or from the linked source. Report an unavailable required reference instead of inventing it. [skill-authoring, DISTRIBUTION.md#composing-skills]1314## Start a film1516From the film folder (`make-video` scaffolds it):1718```bash19cp <this skill>/templates/index.html .20cp <this skill>/scripts/render.mjs <this skill>/scripts/LICENSE-anatomy-reveal.txt <this skill>/scripts/add-font.mjs <this skill>/scripts/gradient.py scripts/21npm install # playwright, from the scaffolded package.json22open index.html # live preview: click for sound in sync, space pauses, arrows seek 2 s, r restarts; ?t=2.5 ?format=9:16 ?style=night23```2425The template's demo (a pill that morphs to a card and a circle, driven by a cursor, looping) exists to show the kit working. It is one idea, not a template: keep the FILM, MOTION LIBRARY, LOOK, LAYOUT and API blocks, and replace the SCENE block with your film.2627## The contract2829- **No clock.** No CSS `transition`, `animation` or `@keyframes`, no `setTimeout`, no `requestAnimationFrame` in render mode, no `Date.now()` or `performance.now()`. The preview loop in the template is the only clock, and it is off when `window.__RENDER__` is set.30- **No carried state.** Nothing accumulates between frames: no counters, no physics stepped frame by frame, no "previous value". Compute everything from `t` in closed form. A simulation that must integrate is precomputed once, deterministically, into an array indexed by time.31- **No unseeded randomness.** `rng(seed)` and `hash(i)` only, never `Math.random()`.32- **Values with many targets are sums of springs.** `track([[t, value, spring], ...])` adds one closed-form spring per change, so frame 812 renders without simulating frames 0-811. Colours use `colorTrack`.33- **The page exposes the API**: `seek(t)` (it may be `async`, for real footage; the renderer awaits it), `configure({ beat, format, style })` (rebuild every timed thing when the beat changes), and `timeline()` returning `{ duration, fps, loop, beat, bpm, format, formats, styles, size, cues, cuts }`. `cuts` lists designed hard cuts in seconds, so the pop scan doesn't flag them. Optional: `speed(t, dt)` returns the largest on-screen travel, in the frame's own pixels (the units of `size`), of anything visible between `t` and `t + dt`; it only reads the timeline and paints nothing. When it exists, `render.mjs full` sizes each frame's motion blur from it.34- **Comment every tween with its duration, curve and overshoot**, in that order, so the code can be checked against the beat table.35- **Readable text lives in the DOM**, so the smoke test can find overlaps and edges. Canvas, SVG and WebGL are for everything else, all drawn from `t`.36- **Fonts are local.** `node scripts/add-font.mjs "Inter" --weights 400,600` vendors any Google Fonts family (every subset, Hebrew included) into `assets/fonts/` with its `@font-face` CSS; link that CSS. The renderer waits for `document.fonts.ready`. A network font that fails silently renders a fallback in every frame.37- **GSAP is welcome when paused.** Build the scene's GSAP timeline paused and call `tl.seek(t)` inside `window.seek`; GSAP then moves every layer exactly like the springs do. A timeline left playing on its own clock breaks the contract.38- **One timing source.** States, copy and cues are written in beats (`beat(n)`) in data arrays at the top of the scene. The shot list's times and these arrays are the same numbers.3940## Motion4142Choose every duration, curve and spring from the number sheet, never by feel: [MOTION-NUMBERS.md](MOTION-NUMBERS.md). Adjectives produce the model's defaults; numbers produce the film. The template implements it: `SPR` springs and `EASE` curves; `track` and `colorTrack` (sums of springs); `kf`, `tween`, `swap`, `lmix`; `hitPulse` and `squash` for hits and contact; `arrive` (vertical rises that don't tick), `durFor` (duration from distance), `whole` (discrete state per whole frame), `exposure` (drawings on twos or held while the camera stays smooth), `timeWarp` (hit-stop: game time freezes on contacts while effects keep running); and the sync helpers `peakOf`, `settleOf` and `springReach`, which compute where each sound belongs from the motion itself. Before writing motion code, list each beat with its start, duration, curve and overshoot in a table; the review checks the render against it.4344Techniques live in one reference per kind of film. Read only the one the shot list calls for:4546- [TECHNIQUES.md](TECHNIQUES.md): every film. Collections (galleries, carousels, rings, coverflow) as poses blended per value, the anchor camera and parallax, CSS and WAAPI pages, cursors, seams, floods, irises, smears, liquid glass, type and right-to-left text, product UI that tells the truth, loops, finish and texture, OKLab colour and banding-free gradients, and the Chromium traps table.47- [TECHNIQUES-COLOUR-AND-FINISH.md](TECHNIQUES-COLOUR-AND-FINISH.md): split from TECHNIQUES.md: collections as poses, loops, OKLab colour and banding-free gradients, finish and texture, and the Chromium traps table.48- [KINETIC-TYPE.md](KINETIC-TYPE.md): words in motion. Reading time as arithmetic, reveals with their numbers, letters with mass, motion that means what the words mean, writing that builds itself, letters that morph into shapes, exits in each device's own voice, the five composition models (fixed anchor, accretion, growing line, world-space, annotation) and the speed-read device.49- [STOP-MOTION.md](STOP-MOTION.md): a deliberately held-pose film, with a global pose grid, fractional frame boundaries, visible pose spacing, shared picture/audio events and explicit alternatives to moving holds and smooth cameras. [stopmotion-video, references/clock.md; references/poses.md; references/sound.md]50- [WORD-FOLD.md](WORD-FOLD.md): a walking compass strip reveals one word and becomes its final letter; supplied glyph/ending guides, kit, sounds and corrected helpers in `templates/word-fold/`. This source uses CoAnimator's player/timeline contract, not the default renderer API. [word-fold, SKILL.md and bundled kit]51- [ILLUSTRATION.md](ILLUSTRATION.md#hand-drawn-line-art-ink-and-watercolour): hand-drawn, ink, watercolour, characters (rigs, walk cycles, IK, acting), game feel and hit-stop, pixel art. Copy `templates/pen.js` (brush, pencil, watercolour, self-writing text, stable seeds, opt-in boil); the drawing kit is `templates/drawkit.js` plus `templates/drawkit-nature.js` and `templates/drawkit-cast.js`, with load order and namespace in that reference.52- [DEPTH-AND-FOOTAGE.md](DEPTH-AND-FOOTAGE.md): real footage and clips, green-screen keying, stills in 2.5D, infinite zoom, 3D, faceted low-poly and extruded type, physics and data-slot numbers.53- [SHADERS.md](SHADERS.md): per-pixel looks in WebGL with twgl.js (`npm i twgl.js`): moving noise gradients, glows, tunnels, kaleidoscopes, melting distance-field shapes, post passes, and feedback trails or GPU particles made deterministic by a fixed-step replay.54- [ISOMETRIC.md](ISOMETRIC.md): how something works, explained by an invented machine whose moving parts do the idea (a queue as a marble run, a cache as a bookcase): the object-is-the-explanation rule, parts lists, four-tone solids, one lit part, painter's order, exploded views, and an isometric figure driven by `seek(t)`. Its full toolset is owned by [isometric-figures](https://github.com/Quegenx/agent-skills/blob/main/skills/web-ui/isometric-figures/SKILL.md), under `skills/web-ui/isometric-figures/templates/isokit/`: the solids kit, round parts and pipes, the geometry audit, WebGL inside the drawing, the 3D mode, eight examples and their manuals; `examples/turning-dial/film.js` is a 3D figure driven by `seek(t)`.55- [REAL-OBJECTS.md](REAL-OBJECTS.md): a real product as the subject (an exploded view, a teardown, an anatomy figure, a product reveal), from a photo or a name: dimensions, features and a parts list and how to research them, part counts, how each kind of object comes apart and its build order, and the reveal shot sequence (tumble in, macro details, one feature working, explode and snap back, hero). Read with ISOMETRIC.md.56- [SIMULATIONS.md](SIMULATIONS.md): swarms, agents, creatures with legs, machines to fly through, evidence walls, maps, terrain, crowds and light.57- [DIRECTOR.md](DIRECTOR.md): a film cut automatically to a track, with captions on the beat, flashes and glitches.58- [EFFECTS.md](EFFECTS.md): sixteen product and type effects, each one object becoming another (button to player, search to results, chart morph, dashboard zoom, masked and elastic type, glass focus, flowing paths, a particle logo or number), on an 8 s loop clock or held under narration.5960**The measured corpus.** [measured/](measured/README.md) holds thirteen references measured frame by frame from 53 professional motion moments (about 4,300 moves): durations by property, the 12 measured eases, twenty transition types with their numbers, type cadence, UI demos, endings, layout, world-building and a slop review. The template carries the 12 eases as `MEASURED.<name>` (`softLand` for landings, `accelExit` and `crashOut` for leaving, `snapSettle` for hero and CTA entries, `popOver` for the rare late overshoot, `cruise` for fades and drift) with their typical lengths in `MEASURED_FRAMES`. Use those numbers for short moments and effects; `MOTION-NUMBERS.md` for whole films.6162**A 3D hero and phone UI.** `templates/hero3d/hero3d.js` is one seekable Three.js hero object (extruded text, an SVG logo with bevels, a coin, a glass slab, a phone, a glTF) with studio light, a moving softbox sweep, bloom and depth of field, every property a pure function of `t` through `renderAt(t)`: import it from `src/*.js`, `npm i three esbuild`, pass the font as a data URL (`opts.font`; Inter Bold is in `hero3d/fonts/`, `fontconv.py` turns a woff2 into a static ttf), bundle with `bundle.mjs`. Tested: chrome type with a sweep and a coin, bundled and rendered, every smoke counter 0. `templates/native-ui/` builds iOS UI at true proportions as HTML (`NativeUI.phone`, `lockScreen`, `notification`, `banner`, `appScreen`, `list`, `tabBar`, `appIcon`, `statusBar`); use its HTML builders independently. Its historical `arrive(M, ...)` and `macro(M, ...)` moves require the source Motion runtime (`motion.js`), which is absent here: they call `M.enter`/`M.kf` and `M.camera`/`M.mark`. The old line incorrectly named GSAP. [native-ui.js, arrive and macro] Every notification requires an icon (or app), a title and a real sentence; time defaults to `now`.6364A finished example film that passes every smoke counter in every format is in [examples/](examples/README.md); copy it instead of starting from the demo.6566## Formats676816:9, 9:16, 1:1 and 4:5 (1080 × 1350) are different compositions of one story, never crops of one render. All layout derives from `W`, `H` and `U` (1% of the short side) inside `setFormat()`; give the vertical cut larger type, fewer simultaneous elements and its own camera path. Check each format with its own sheet. For YouTube, "avoid adding padding or black bars directly to your video" ([YouTube Help](https://support.google.com/youtube/answer/6375112)): render the 16:9 cut at 16:9, not a letterboxed copy of another format.6970## Looks7172The *Cinematic UI* look (oblique macro shots of real UI, optical depth of field and stipple reveals) has source-specific dark and light stages in [CINEMATIC-UI.md](CINEMATIC-UI.md); choose from the product's real theme. [1weiho/cinematic-shot, references/look.md]7374All colour lives in `TOKENS`, chosen from the named sets in `STYLES`. A second look of the same film is a new entry in `STYLES`, never a fork: `render.mjs full --style night` renders it with identical timing. Gradient stages are baked PNGs from `scripts/gradient.py`, never CSS gradients (they band).7576## Render7778```bash79node scripts/render.mjs still 0 2.5 7.9 # full-resolution stills -> out/stills/80node scripts/render.mjs sheet --every 0.5 # labelled contact sheet -> out/sheets/sheet.png81node scripts/render.mjs sheet --at 1,1.2,4.05 # exact moments82node scripts/render.mjs beats # one settled frame per beat83node scripts/render.mjs strip 3.9 4.3 # every frame of a stretch: pops, overlaps, ghosting84node scripts/render.mjs full # out/16x9.mp4: 60 fps, sub-frames blended (by speed when the page has speed()), audio/mix.wav muxed85node scripts/render.mjs full --format all # every format in FILM.formats86node scripts/render.mjs full --scale 0.5 --sub 1 # fast draft, and the animatic before polish87node scripts/render.mjs full --range 4:7 # re-render only the affected seconds88node scripts/render.mjs full --chunk 10 # long film: 10 s segments, a stopped render resumes where it left off89node scripts/render.mjs full --gpu # 3D, shader-heavy or WebGPU film: the real GPU (Metal on macOS), ~9x faster; WebGPU needs it90node scripts/bundle.mjs src.html index.html # a film with module imports (three.js): bundle into one page first91node scripts/render.mjs full --shutter 180 --sub 8 --grain 0.03 # a lens finish for a smooth film: shutter blur and seeded grain (grain encodes large: plan the file size)92node scripts/render.mjs full --alpha # transparent intro, outro or overlay: ProRes 4444 .mov (--alpha webm for the web)93node scripts/render.mjs full --data data/acme.json --out out/acme.mp4 # one film per record: the page reads window.__DATA__94```9596**A film made of several scene pages** (one HTML file per scene, each with its own `seek`): render each with `--page`, then join them with no re-encode, scenes listed in order in `out/scenes.txt` as `file '01.mp4'` lines:9798```bash99node scripts/render.mjs full --page scenes/01.html --out out/01.mp4 --audio none100node scripts/render.mjs full --page scenes/02.html --out out/02.mp4 --audio none101ffmpeg -f concat -safe 0 -i out/scenes.txt -c copy out/picture.mp4102ffmpeg -i out/picture.mp4 -i audio/mix.wav -map 0:v -map 1:a -c:v copy -af apad -c:a aac -b:a 256k -ar 48000 \103 -t "$(ffprobe -v error -show_entries format=duration -of csv=p=0 out/picture.mp4)" -movflags +faststart out/film.mp4104```105106Cut each scene on a beat: give each page's duration in whole beats so the joins land on the grid. The sound is mixed once for the whole film, never per scene.107108Every mode takes `--format`, reads `audio/beats.json` when present, writes `out/cues.json` for the sound pass, and fails on any page error. Every mode also takes `--style`. `full` blurs adaptively when the page exposes `speed(t, dt)`: each frame gets `ceil(travel / --step)` sub-frames (`--step 4` output px, at most `--most 40`), the travel measured over the open shutter, so a hold renders 1 and a whip up to 40 (after anatomy-reveal's `render.mjs blur`, MIT, `scripts/LICENSE-anatomy-reveal.txt`). Each frame's sub-frames are averaged with ffmpeg `tmix`, one call per frame. `--sub N` overrides it with a fixed count for every frame; a page without `speed` gets `--sub 4`, and `--sub 8` for fast moves (4 sub-frames leave ghost copies on a whip). `--shutter 240` (degrees, centred on the frame; blur never crosses a declared cut), `--scale 2` for a supersampled master, `--workers` and `--crf`. Chromium launches with `--force-color-profile=srgb`, so pixels are sRGB on any display. Output is H.264, yuv420p, BT.709-tagged, square pixels, AAC 48 kHz, `+faststart`. When `audio/vo.srt` exists (the voiceover skill writes it), a render from 0 carries it as a selectable subtitle track and writes the `.srt` beside the MP4. `--chunk` keys its segments by the top-level HTML, selected render options, beats and supplied data. Imported CSS, JavaScript, images and fonts are outside that key; use `--fresh` after changing those dependencies. The old line claimed every edit invalidated stale segments. The source records an identical single-pass comparison. [scripts/render.mjs, chunk key and --fresh] `--gpu` pixels can differ between machines: keep one machine per film. With `--alpha` the page sees `window.__ALPHA__` and paints no background of its own (the template's `drawFx` already skips it); the file is video only and renders in one pass. Without a mix, an MP4 still gets a silent audio track (feeds mishandle files with none). A data-driven film reads every name, number and line from `window.__DATA__` with its own default for the preview; smoke the longest record first (`smoke.mjs --data`), check its stills, then render every record. Tested: alpha renders carry real transparency in ProRes 4444 and VP9; two records render their own text; a malformed record fails with its file name.109110## Gotchas that cost real time111112- `will-change` on anything the camera scales makes its text rasterize blurry. On a layer whose `filter: blur()` animates, it causes nondeterministic ghost frames.113- Put all of an element's translation in one `transform`; a fractional `left/top` plus a transform snaps to whole pixels and jitters.114- Clamp every value that feeds geometry: springs overshoot, and a negative width, radius or blur throws or flashes.115- A colour change between black and white through a spring or crossfade passes through grey. Flood the new colour from a point (the template's `#flood`) or move an accent element between states.116- Frame 0 is the thumbnail and the first thing a muted feed shows: compose it. In a loop, keep frame 0 mid-hold, not mid-morph.117- Periodic decoration in a loop (drift, rotation) must divide the loop length, or the seam jumps. The smoke test catches it.118- Declare every variable before the first `seek()` call; a temporal-dead-zone error only appears in render mode.119- WebGL (Three.js) is fine when the scene is rebuilt from `t` each frame and any shader time uniform is `t`. Shaders on their own clock paint a different picture each run.120- WebGL on a live web page is the opposite case (refuse software rendering with `failIfMajorPerformanceCaveat`, a DPR cap chosen by shader cost, context loss, a rAF loop): [webgl SKILL.md](https://github.com/Quegenx/agent-skills/blob/main/skills/web-ui/webgl/SKILL.md). A film keeps SwiftShader, the fixed `maxRatio` and `seek(t)`.