Vawe

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:

APIWhat the page sees
Date, performance.nowfilm time in milliseconds
requestAnimationFramecallbacks run once per seek, with film time
setTimeout, setIntervalfire when the seek passes their due time, in due order
Math.randoma 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() or window.seek(t).
  • Compute each frame from t alone. Do not write x += dx.
  • Do not use a live clock, such as a requestAnimationFrame loop that keeps its own time.
  • For a value that must look random, seed it per element. core/motion/springs.js exports rng(seed) and noise1(x, seed).
  • Bundle every font in films/<name>/assets/ and load it with @font-face. The renderer waits for document.fonts, not for the network.

Check it

bin/vawe check anim-traps films/<name>/page.html

The 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.

On this page