Determinism
Why a frame is a function of the seek time, what the virtual clock owns, and how to check your page.
The same page must give the same frames on every run. The renderer does not wait for your page to play. It seeks to a time t, takes a picture, and moves on. Frame 400 never needs frame 399.
What the virtual clock owns
The renderer installs a virtual clock (core/engine/page-clock.js) before any page script runs. It replaces these:
| API | What the page sees |
|---|---|
Date, performance.now | film time in milliseconds |
requestAnimationFrame | callbacks run once per seek, with film time |
setTimeout, setInterval | fire when the seek passes their due time, in due order |
Math.random | a seeded generator, reseeded from t on every seek: same t, same sequence |
Then the renderer seeks in this order (core/engine/page-seek.js): set the clock, call window.seek(t) if it exists, seek every CSS and Web Animation and every SVG animation, then run every vawe.onFrame hook.
What to write
- Drive motion from the seek:
@keyframes,element.animate()orwindow.seek(t). - Compute each frame from
talone. Do not writex += dx. - Do not use a live clock, such as a
requestAnimationFrameloop that keeps its own time. - For a value that must look random, seed it per element.
core/motion/springs.jsexportsrng(seed)andnoise1(x, seed). - Bundle every font in
films/<name>/assets/and load it with@font-face. The renderer waits fordocument.fonts, not for the network.
Check it
bin/vawe check anim-traps films/<name>/page.htmlThe anim-traps gate loads the page, reads the animations the seek will drive, and searches the source for a live clock, an unseeded Math.random and WebGL capture mistakes. It also finds seven traps that never throw an error. See When it looks wrong.
bin/vawe e2e runs the page tests and a parallel half-size draft of every film.