The Clock You Do Not See
HyperFrames' most important runtime decision is to treat a video as a sequence of requested states, not as a screen that happens to be playing. The focus here is the path from a frame number to captured pixels: how time is calculated, how animation is moved, when the page is considered ready, and which conditions can still make the result differ. Once that path is clear, “deterministic” becomes an operating rule rather than a slogan.
From frame number to captured pixels
Before frame zero, the renderer fixes the inputs that give a frame meaning: the frame rate, canvas dimensions, variables, and the composition's finite duration. Then it can ask a precise question. At 30 frames per second, frame 90 corresponds to three seconds. The renderer does not need to wait three seconds of wall-clock time, and it does not need frame 89 to have been played continuously before it can request frame 90.
The page must expose a seekable runtime. HyperFrames waits for that runtime to report a usable duration and a seek function, along with the readiness conditions required by the composition. This matters for assets and nested scenes. A page that has loaded its HTML but has not finished binding its animation or preparing the resources needed for a frame is not yet a trustworthy answer to a capture request.
The basic lifecycle is:
- The renderer chooses a frame number and derives its time from the fixed frame rate.
- The runtime moves the composition and its supported animation systems to that time.
- Any asynchronous work needed to settle the requested visual state completes.
- Chrome captures the page's pixels in one operation, using its controlled frame mechanism when available or a screenshot path when necessary.
- FFmpeg encodes the captured images and combines audio supplied by the composition.
The order is part of the guarantee. If capture happens before an image finishes decoding, the renderer may preserve a blank or incomplete state. If encoding begins before the frame sequence is complete, the output no longer represents the requested timeline. HyperFrames therefore treats readiness as a boundary between “the page has loaded” and “the page can answer a frame request.” A composition can be visible in a browser and still not be ready for deterministic capture if its timeline has not been bound or its media state has not settled.
The renderer can also reason about frames independently of their history. Suppose it requests frame 90 of a title animation, then returns to frame 20 to inspect an earlier transition. Frame 20 must be derived from the composition's time, not from whatever state was left behind at frame 90. That property makes parallel capture and targeted snapshots possible: the request names the state to produce, while the capture pipeline handles the work needed to produce it. The cost may be higher than simply watching the page once, but the result is a sequence whose timing is explicit.
The capture step is not a screen recording of whatever Chrome last painted. The Engine documentation describes a seekable page protocol in which a page exposes a duration and a deterministic seek(time) function. The frame-capture service waits for that protocol, then uses Chrome's HeadlessExperimental.beginFrame where its controlled compositor path is live, with a screenshot fallback for cases that require it. Either way, the important handoff is the same: the browser receives a requested state before the pixels are captured.
This changes the meaning of slowness. If a frame contains an expensive blur, a large image, or a demanding canvas draw, the renderer may take longer to finish that frame. It should not silently skip the frame merely because the machine could not paint it before a real-time deadline. A preview is a playback experience and can stutter; a render is a sequence of explicit capture requests and can take longer while preserving the sequence.
The final encoding stage is also part of the lifecycle rather than an unrelated export trick. Captured frames become a video stream, while audio from the composition is prepared and mixed according to the output format. A page can therefore succeed at seeking and still produce an unsuitable delivery file if the chosen format does not fit its audio, transparency, or downstream requirements. Deterministic capture answers the pixel-timing problem; it does not decide what every recipient should do with the resulting container.
Adapters make motion addressable
The renderer cannot directly understand every animation library. It needs a small boundary that answers: what state should this animation have at frame N? HyperFrames calls that boundary a FrameAdapter. An adapter has an identity, can initialize with the composition's frame rate and dimensions, reports a finite number of frames, seeks to a requested frame, and can release its resources when the render ends.
GSAP shows why the abstraction is useful. A GSAP timeline is normally capable of playing itself, but HyperFrames uses it as a state function. The built-in adapter pauses the timeline during initialization. For each request, it converts the frame number to seconds, pauses again if necessary, and seeks to that exact time. Its duration is converted to frames using the frame rate, with the full timeline duration used when available. The timeline's registry key must also match the composition ID so the runtime can bind the right animation to the right root.
That registration rule has a visible consequence. A page can contain a perfectly written GSAP timeline and still render as a still image if the root composition is registered under a different name—or not registered at all. During continuous browser playback, a person may see the script run and assume the render will follow. The renderer, however, is looking for the state associated with the composition it was asked to capture. A missing binding means there is no reliable animation state to seek.
A good adapter also supports seeks that do not resemble playback. The renderer may request frame 90, then frame 20, then frame 90 again. Each request must produce the state for that frame, independent of the path taken to reach it. An adapter that only updates correctly after every intermediate tick is a playback controller, not a render-safe adapter. This is why the contract requires forward, backward, and random seeks, the same result when a frame is requested again, and completion of asynchronous work before capture.
The adapter owns animation state, not the entire render pipeline. The runtime decides how composition timing and media visibility map onto the page. The adapter moves a particular animation system. The Engine captures the resulting browser state, and the Producer or CLI can continue with extraction, audio, encoding, and assembly. Keeping those responsibilities separate prevents a custom animation integration from quietly taking over media playback or output assembly.
The lifecycle also gives a custom adapter a clear stopping point. Initialization may discover the animation and connect it to the composition. Seeking may update the animation and wait for asynchronous work to finish. Destruction must release listeners and other resources once the final frame is done. The adapter does not need to know whether the caller will write an MP4, a transparent WebM, or a PNG sequence; it only needs to return the correct visual state for the requested frame. That narrow boundary makes extension possible without making every animation integration a new renderer.
The same idea extends beyond GSAP. HyperFrames documents deterministic adapters for systems such as CSS animations, Anime.js, Lottie, Three.js, and TypeGPU, alongside integrations for other browser-based visual runtimes. The shared promise is not that these libraries behave identically. It is that the runtime has a way to ask each supported system for a state at a specified time. If an author uses a library or effect with no suitable adapter, the fact that it animates in a browser is not evidence that it can be reconstructed during a render.
This also explains why “paused” is not the same as “static.” A paused timeline can be moved deliberately to different positions. A timeline that is allowed to play during capture depends on elapsed time and the scheduling behavior of the host machine. HyperFrames needs the first behavior. It pauses motion so a seek sets the state, then captures that state before the next request changes it.
What deterministic rendering does—and does not—control
The rules follow from the lifecycle. Anything that makes the answer depend on history, wall-clock timing, or an event that may arrive at a different moment can make the same frame ambiguous. HyperFrames therefore excludes Date.now(), requestAnimationFrame, system timers, and unseeded Math.random() from render-safe composition logic. It also expects assets to be available before capture rather than fetched unpredictably in the middle of a render. Every composition needs a finite end, and the frame rate, width, and height must be fixed before frame zero.
Consider an animation that chooses a random color each time its update callback runs. It may look lively in a live preview, but “frame 40” no longer identifies one color. Or consider a title whose position is derived from the wall clock. Two renders started at different times will ask the same frame to show different positions. These are not failures that a faster browser fixes; they are failures to define frame state as a function of the requested time.
The same reasoning applies to asynchronous work. A custom animation may load data or finish a canvas operation after the seek call returns. If capture is allowed to proceed first, the renderer has captured an intermediate state; if it waits for the adapter's completion signal, the requested frame has a defined stopping point. This is why the adapter contract includes settlement before capture and why a finite duration matters: the renderer needs to know both what state to request and when it is safe to commit that state as a frame.
The stronger test is to seek directly. If the renderer jumps to a late frame, the entrance should already be in the right state. If it jumps backward, the earlier state should return. If it repeats a frame, the pixels should not drift because a callback or timer accumulated hidden history. A composition that passes only when allowed to play from the beginning is relying on a weaker contract than HyperFrames requires.
The guarantee still has a boundary. It controls the relationship between a fixed request and the authored runtime state. It does not make Chrome versions, font files, codecs, GPU behavior, or host environments identical. A font substitution can change line wrapping while the timeline remains perfectly seekable. A different browser or graphics path can produce small pixel differences even when the same frame was requested. These are environmental inputs, not evidence that the frame clock was consulting live playback.
When exact cross-machine reproduction matters, the documented answer is to control more of the environment. Docker can pin the Chromium version, font set, and FFmpeg encoder, reducing the number of variables outside the composition. That is stronger than simply rerunning the same source on a different laptop, but it is not a cryptographic identity system. The framework's container metadata, where present, is an unsigned diagnostic hint rather than proof that a file came from a particular renderer.
The useful conclusion is precise. HyperFrames makes a frame reproducible when the source supplies finite, seekable state and the runtime waits for that state before capture. The adapter contract turns animation from a process that must be watched into a state that can be requested. The environment can still affect the pixels, so reliable rendering means both honoring the seek rules and choosing how tightly to control the machine that executes them.