Time and seek
The renderer never plays a page. It seeks the page to a time t and paints one frame, so every frame must come from t alone.
To seekTo seek is to set the page to a time t in seconds and paint the frame for that time. is to set the page to a time t in seconds and paint the frame for that time. The renderer never plays the page. For each frame it seeks, takes a picture and moves on. Your page code must be a pure function of t.
Why it exists
A film must come out the same on every render, in any order, on any number of workers. A page that plays in real time cannot do this, because the wall clock differs on every run. A seek has no wall clock: frame 400 needs only the number 6.667, not frame 399.
This is also why vawe can render slices of a film in parallel, and render again only the seconds that you changed. See Determinism.
How it works
The function seekTo in core/engine/page-seek.js does one seek, in this order:
- It waits for the page to start (image decodes,
fetchand the fonts) and fordocument.fonts. - It sets the virtual clockThe virtual clock is the code in core/engine/page-clock.js that controls Date, requestAnimationFrame, timers and Math.random during a render. to
t. - It calls
window.seek(t)when your page defines it. It waits only for a nativePromise. - It pauses every CSS animation and Web Animation and sets its
currentTimetot * 1000milliseconds. - It seeks every SVG animation with
setCurrentTime(t). - It calls every hook that you registered with
vawe.onFrame.
After the seek, the renderer waits for fonts, for image decodes and for two real paints. Then it takes the screenshot.
Two ways to move
You can use either way, or both in one page.
| way | you write | the renderer does |
|---|---|---|
| CSS and Web Animations | @keyframes or element.animate() | It sets currentTime on each animation. |
window.seek(t) | A function that paints the frame for t | It calls the function on every seek. |
Use window.seek(t) for canvas, three.js and anything else that CSS cannot move. A CSS linear() string cannot run in window.seek or onFrame code. There, use easeFn(name) from core/motion/presets.js for a named ease.
The virtual clock
Before any page script runs, core/engine/page-clock.js replaces these APIs. They follow the seek time.
| API | What the page sees |
|---|---|
Date, performance.now | The film time in milliseconds. |
requestAnimationFrame | Callbacks run once for each seek, with the film time. |
setTimeout, setInterval | They fire when the seek passes their due time, in due order. |
Math.random | A seeded generator. It starts again from t on each seek, so the same t gives the same numbers. |
A page that reads the clock still gets the seek time. Do not rely on this. Write each value as a function of t. For a value that must look random but stay the same, use rng(seed) from core/motion/springs.js. See seeded randomA seeded random number comes from a generator that starts from a fixed seed, such as rng(seed) in core/motion/springs.js..
Frames and rates
The render rate is not part of the page. A draft renders at 30 fps and a final at 60 fps. The script harness/media/render-page.mjs takes --fps N for another rate. The number of frames is round(duration x fps), and frame i shows the time from + i / fps, where from is the --from second of a draft window. At 60 fps, 1.2 s is frame 72.
<meta name="fps"> is a different thing. It is the rate of your own frame tables, and the page reads it as window.vawe.fps. A page can seek at a time that is not a whole frame. Motion blur does this: it blends up to 32 subframes for each frame of a final. Write a table in seconds or in your own frame rate. Never assume that t is a multiple of 1 / 60.
An example
This page moves a bar with a Web Animation and a count with window.seek. Both use t only.
<meta name="duration" content="3">
<div data-world="s1"><div id="bar"></div><p id="n"></p></div>
<script type="module">
document.getElementById('bar').animate(
[{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }],
{ duration: 2000, fill: 'both' });
window.seek = (t) => {
document.getElementById('n').textContent = Math.round(Math.min(1, t / 2) * 100);
};
</script>The same t always gives the same bar and the same number. You can seek to 1.2 s, then to 3.0 s, then back to 1.2 s, and see the same frame.
Limits
- State between frames does not work.
x += dxgives a different value for each order of seeks. Frame 400 must not need frame 399. - A module-level
awaitof something other than an image decode, afetchordocument.fonts.readyis not waited for. The renderer can paint frame 0 before yourvawe.onFrameexists. If the renderer waits too long, it stops with the name of what is pending, after 10 s. - A
window.seekthat never resolves, or a font that never loads, stops the render after several tries of the slice. window.seekreturns a value that the renderer ignores, unless it is a nativePromise. A GSAP timeline has athen()that never settles, so the renderer does not wait for it.- A
@keyframesrule with no100%stop slides back after it ends. Afill: bothon an animation with a delay shows the first keyframe from frame 0.
The anim-traps gate finds these and more:
bin/vawe check anim-traps films/<name>/page.htmlDeterminism
The page
When it looks wrong
From the blogSeed Math.random and freeze Date.now in JavaScript