How It Works
The rendering pipeline is frame-by-frame and seek-driven. No realtime playback is involved — every frame is independently seeked and captured.Frame clock
The engine computes the time for each frame using integer math:
time = floor(frame) / fps. There is no wall-clock dependency — rendering is entirely decoupled from real time.Seek
The frame adapter receives a
seekFrame(frame) call and deterministically positions all animations, DOM state, and canvas content to the exact frame. The adapter’s renderSeek pauses all GSAP timelines and seeks them to the computed time.Capture
Chrome’s
HeadlessExperimental.beginFrame API captures the pixel buffer for the current frame. This is a single, atomic operation — no partial paints or race conditions.What Makes It Deterministic
- No wall-clock dependencies — rendering does not use
Date.now(),requestAnimationFrame, or system timers - No unseeded randomness —
Math.random()without a seed breaks determinism - No render-time network fetches — all assets must be loaded before rendering starts
- Fixed output parameters —
fps,width, andheightare locked before the first frame - Finite duration — every composition has a known, finite length
Docker Mode
For maximum reproducibility, render in Docker:- Same Chromium rendering engine across all platforms
- Same system fonts (no platform-specific font substitution)
- Same FFmpeg encoder version
Preview vs. Render Parity
The browser preview and the rendered MP4 should match. Hyperframes achieves this through:- One runtime — the same
hyperframe.runtimedrives both preview and render - Producer-canonical behavior — the producer’s seek semantics are the source of truth
- Readiness gates —
__playerReadyand__renderReadyensure the composition is fully loaded before any frame is captured
Local rendering (without Docker) may show slight differences due to platform-specific font rendering and Chrome version. Use Docker mode when exact reproducibility matters.
For Adapter Authors
If you are building a frame adapter, your adapter must follow the determinism contract:seekFrame(frame)must be idempotent — same frame, same result- No side effects that depend on call order (must handle random access)
- No async operations that resolve after the frame is “committed”
- Clean lifecycle:
init->seekFrame(N times) ->destroy
Next Steps
Frame Adapters
Build adapters that uphold the determinism contract
Rendering
Render to MP4 locally or in Docker
@hyperframes/producer
The full rendering pipeline that orchestrates deterministic output
Common Mistakes
Pitfalls that break determinism and how to avoid them