Rendering through HyperFrames
A Kinetic page can also render through HyperFrames, a command-line renderer by HeyGen, which Kinetic is not affiliated with. This page covers when that route is worth taking, the runtime script it needs, what changes on the page while the runtime is present, and the frame-rate rule its player imposes.
The runtime's script tag goes after Kinetic's and before the page's own scripts, and the composition root gains data-no-timeline.
With the runtime on the page it owns the clock and the media, and the mode is video, so leave its tag off a lesson page.
A render() on the page at a frame rate that does not divide the root's data-fps throws (KN-206).
When to use it
Kinetic renders in the browser, with the sound of the page's audio clips, and the batch script renders one page once for each row of a file.
HyperFrames adds a render split across parallel workers, for a long piece on a slow machine, and a render on a server with no browser window, through Node 22 and FFmpeg.
Its npx hyperframes transcribe writes a word-timed transcript from a voice track, in the shape captions() reads, and its beat detection writes the beat file beat() reads.
srt() reads a transcript from any other tool.
The composition's attributes (data-composition-id, data-start, data-duration, data-composition-variables, and the audio and footage clips) are the ones HyperFrames reads, so the page needs no change beyond the script tag below.
The exception is a data-start in Kinetic's own .start form, such as "voice.start", which HyperFrames does not read, so a page that uses it renders through Kinetic only; a group keeps two clips together in a form both read (Starting with another clip).
Kinetic is tested against HyperFrames runtime 0.8.133, which wildflower.kinetic.compat.hyperframes reports.
The runtime script
A HyperFrames render needs the HyperFrames runtime, 0.8.0 or later, which Kinetic does not bundle. Its script tag goes after Kinetic's and before the page's own scripts.
<script defer src="https://cdn.jsdelivr.net/npm/@hyperframes/core@0.8.133/dist/hyperframe.runtime.iife.js"></script>
The runtime starts as soon as its script runs and waits for Kinetic, so Kinetic comes first.
As it initializes, it replaces window.__hf.buildReady, the promises it waits on before capturing frame 0, with a fresh object.
Kinetic keeps its readiness entry through the replacement, and the runtime then shows the page with the scenes bound.
With the runtime on the page, the composition root also needs data-no-timeline, which tells the runtime the page registers no animation timeline, so the renderer does not wait for one.
Only the runtime reads the attribute, so a page that Kinetic plays and renders by itself leaves it out.
Without it, and without config({ channel: 'timeline' }), development builds warn (KN-111).
<div data-composition-id="main" data-no-timeline data-start="0" data-duration="6"
data-width="1280" data-height="720" data-fps="30">
<!-- the scenes -->
</div>
Then, in the project folder, one of these.
npx hyperframes render
npx hyperframes render --batch rows.json
--batch renders the page once for each row of the file, with the row's values as the composition's variables, as the batch script does.
What changes with the runtime on the page
Kinetic treats the page as having the runtime when it finds window.__player, window.__renderReady or window.__hyperframes.
The runtime then drives the seeks and the clock, waits on readiness, hides clips outside their windows, and plays audio, footage, and Lottie animations itself.
Each item below gives what Kinetic does instead.
Seeks, readiness, the clock and hiding
- Seeks. The runtime dispatches
hf-seekon every seek, in preview and in a render, with{ time, waitUntil }. Kinetic applies the time and callswaitUntilwith a promise that settles after the framework's flush, every promise a scene'sseek()returned, the CSS-animation catch-up and any Trillium draws, so the frame is captured only once the DOM holds it.config({ channel: 'timeline' })registers an object atwindow.__timelines[rootId]instead, which the runtime drives through its timeline registry; the root then needs adata-composition-idand has to be in the document (KN-100). - Readiness. The runtime waits on every promise in
window.__hf.buildReadybefore it publishes__renderReady, and Kinetic's entry iswindow.__hf.buildReady.kinetic. The runtime ignores a rejected promise there and renders anyway, so on a failure the entry stays unsettled and the render stops at the runtime's readiness timeout, with the KN error in its console output.data-kinetic-ready="true"waits for__renderReady. - The clock. The preview bar,
seek(),play()andpause()drive the runtime's player.config({ loop })has no effect, andplay({ from, to })holds the last frame with the player too. The mode is video whateverconfig({ mode })says, sotick()never runs, and a runtime that loads after Kinetic puts the page in video mode before the next seek. Leave the runtime's tag off a lesson page. - Hiding. The runtime hides each clip outside its window with
visibilityalone, where Kinetic on its own also clips it to nothing. An SVG filter withfeFlood,feImageorfeTurbulencepaints throughvisibility: hidden, so a scene turns such a filter on only inside its clip's window.
Media, Lottie, variables, layout and render()
- Media. The runtime plays audio and footage itself.
Kinetic plays no sound in the preview, does not seek, play, mute or wait for footage (KN-138 does not occur), and does not wait for the runtime's media before drawing an effect, so a
<video>source of an effect can be drawn from the frame before the seek's. A runtime that arrives after Kinetic started playing the clips gets them paused, each with themutedits markup gives it. A footage clip needsmutedordata-has-audio="true"for the runtime, which development builds remind you of (KN-139).data-playback-ratekeeps the pitch through HyperFrames and moves it in Kinetic, so at a rate other than 1 the two routes sound different. HyperFrames reads none ofdata-tempo,data-downbeat,data-meteranddata-markers. - Lottie. The runtime finds lottie-web animations and seeks them itself, so Kinetic does not, and
lottie()'sstart,rateandloopdo not apply. Readiness still waits for each animation to load. - Variables.
varsandinputsread the values the runtime resolved, fromwindow.__hyperframes.getVariables()or the per-composition table of an inlined sub-composition. Without the runtime they read the declared defaults withwindow.__hfVariablesover them, which is what the batch script sets. For values that change from clip to clip in one video, each clip becomes a HyperFrames sub-composition with variables of its own. - Layout. The runtime sets a size, margin, padding and overflow on
htmlandbody, and outside a render Kinetic removes them, so the page keeps its own layout. The renderer captures the window's top left corner at the composition's size, so whilewindow.__HF_RENDER_CAPTURE_MODEis set Kinetic hides every element beside the composition root and beside each of its ancestors up to the children ofbody, leavingscript,style,linkandtemplateelements alone, and zeroes the margin and padding of each ancestor and the root's own margin, once the page is parsed and again when readiness settles. The preview bar never mounts while a render is capturing. render()on the page. It seeks through the runtime's player, so the shutter is not applied (KN-205, once per page, andsmear()keeps its copies), and the render's frame rate has to divide the root'sdata-fps(The frame rate), orrender()throws KN-206 with where the seek landed and puts the page and the clock back. HyperFrames' own command line does not callrender(), so the shutter never applies there either.
The frame rate
The runtime's player puts every seek on a grid of frames, at the rate in window.__HF_EXPORT_RENDER_SEEK_CONFIG when it starts, or 30 without one.
When the page sets none, Kinetic defines one that reads as the root's data-fps at the moment it is read, and as no config when the root has none or is not in the page yet.
- HyperFrames' command line sets that config before the page loads.
config({ fps })does not reach it.- A config the page sets, before Kinetic loads or after, is kept as the page set it.
- With no config set by the time the runtime starts, the player stays at 30, and
render()at a rate that grid misses throws (KN-206).
When the player reads Kinetic's config, seek(), the preview bar's frame steps and every frame of render() land on the composition's own frames.
Whether it reads it in time depends on the two tags.
| Tags | Kinetic's config |
|---|---|
| Kinetic's tag, plain or deferred, before the runtime's, as above | Read. |
| A plain runtime tag, then a plain or deferred Kinetic tag | Read, since a plain runtime tag waits for parsing to finish before it reads the config. |
| A deferred runtime tag, then a deferred Kinetic tag | Too late, since a deferred runtime tag reads the config when its script runs. |
| A deferred runtime tag, then a Kinetic script added after the page has loaded | Too late. |
The runtime takes any config as an export render, so previewing a root with data-fps with the runtime on the page runs its export branch.
That branch skips the codec-proxy swap and the cross-origin audio diagnostic, snaps clip windows to frames, and does not wait for a video still loading.
Also
Coming from Remotion, HyperFrames or After Effects maps a HyperFrames GSAP timeline to a Kinetic scene and lists where each part comes from. HyperFrames is by HeyGen, and Kinetic is not affiliated with HeyGen.