Deterministic rendering
Why two HyperFrames renders of the same composition produce byte-identical MP4 files — the seek loop, stubbed clocks, pinned fonts, and frozen encoder flags.
Determinism is the promise that sha256sum out.mp4 returns the same hash on your laptop, on CI, and on a colleague's machine three months from now. HyperFrames earns that promise by removing every source of nondeterminism from the render path.
What you'll learn
- Why the renderer drives time instead of the browser
- Which browser APIs are stubbed and what replaces them
- How fonts, encoder flags, and the frame manifest are pinned
- Why this matters for CI, diffing, and audit
The seek loop replaces the wall clock
A normal browser animation is driven by requestAnimationFrame. That ties output to whatever the OS scheduler decided was 16.7ms — which it almost never was. HyperFrames does not let the browser drive time at all.
Instead, the renderer holds the clock. For each frame in [0, duration) at the chosen frame rate, it sets the page time, dispatches a hf-seek event with the new time, waits for layout to settle, and screenshots. The browser is a pure projection of time → pixels.
Stubbed clocks and randomness
Before any user code runs, the renderer installs replacements for everything that could leak entropy into a frame:
Date.now(),performance.now(), andnew Date()return the current seek timeMath.random()is seeded from the composition hash, not from system entropyrequestAnimationFrameresolves synchronously against the seek clocksetTimeoutandsetIntervalare queued against seek time, not real timecrypto.getRandomValuesuses the same seeded PRNG
If your composition reads any of these, it reads a value that is a pure function of the frame being rendered.
Sandboxed font fallback
Fonts are the single biggest cause of cross-machine drift. The same CSS on two laptops can resolve to two different fonts because one machine has Helvetica installed and the other has Arial.
HyperFrames ships a frozen font sandbox. The browser sees exactly the fonts declared in the composition's <link> and @font-face rules, plus a single deterministic fallback (a bundled metric-compatible substitute). System fonts are unreachable. If a glyph is missing, you see the box, not whatever the host OS happened to have.
Pinned FFmpeg encoder flags
The screenshot pipeline emits PNGs. FFmpeg muxes them into MP4. The encoder is invoked with a frozen flag set — same libx264 version, same preset, same -pix_fmt yuv420p, same -movflags +faststart, same color primaries. Bumping HyperFrames is the only way those flags change, and every bump notes which hashes shift.
Frame manifest with content hashes
Every render writes a sidecar manifest.json:
{
"composition_sha": "9e3c…",
"renderer_version": "1.4.2",
"ffmpeg_args_sha": "a081…",
"frames": [
{ "n": 0, "t": 0.0, "sha": "f1c2…" },
{ "n": 1, "t": 0.0333, "sha": "27ab…" }
],
"output_sha": "b4d0…"
}Two manifests with matching composition_sha and matching renderer_version must have matching output_sha. If they do not, the renderer has a bug — and the per-frame hashes tell you exactly which frame diverged.
Verifying it yourself
# Render the same composition twice into different files.
hyperframes render intro.html --out run-a.mp4
hyperframes render intro.html --out run-b.mp4
sha256sum run-a.mp4 run-b.mp4The hashes match because nothing in the path between HTML and MP4 was allowed to vary.
What determinism does not give you
It does not give you perceptual stability across renderer upgrades. A new libx264 release can shift bytes without shifting pixels. The manifest records renderer_version for exactly this reason: a hash change without a version change is a bug; a hash change with one is expected and gated.
It also does not protect you from your own nondeterminism. If you fetch from a live API inside the composition, the render is only as deterministic as the API. Pin your data the same way HyperFrames pins everything else.