Deliverable one of issue #4010: average K sub-frame captures into each output frame so rotation, scale and opacity blur the way After Effects does, which the translation-only catalog component (#4008) cannot do. Opt in per render with CaptureOptions.motionBlur; absent means the capture path is unchanged. The blocker was quantizeTimeToFrame flooring every seek onto the output frame grid, which collapsed all K samples onto one instant and made the averaged frame byte-identical to the unblurred one. quantizeSeekTime now owns that decision at all three quantize sites (runtime player, its no-timeline fallback, and the init transport) and refines the grid by an integer subdivision count, which can only contain the output grid, never move a frame-time instant off it. The finer grid rounds rather than floors: a sample time round-trips through the page bridge as a float, and past a few million ticks that error exceeds the floor epsilon. Accumulation sits inside captureFrameCore AFTER the static-dedup early return, so a static frame still costs one reused buffer and only moving frames pay K. It is not a before-capture hook, which would have disabled dedup outright. Callback invariant: one eventful seek per output frame at the frame time, then K suppressed sample seeks, then a suppressed restore to the frame time. The restore matters because a suppressed seek still moves the playhead, so without it the next frame's eventful seek would swallow the callbacks inside the shutter window. Scope of this deliverable: screenshot capture mode and PNG output. beginframe, drawelement and JPEG encoding are rejected with a named error rather than silently rendering unblurred. Video content is rejected for the same reason it is a documented limitation: injected frames are extracted per output frame and cannot follow a sub-frame seek. Blend defaults to sRGB, matching AE's non-linearised 8-bpc working space and the catalog component. Linear light is an explicit opt-in. Samples are folded into a running average as they are captured rather than held until the end, so peak memory is one accumulator plus one decoded sample instead of all K: at 1080p that is a constant 41 MB rather than 141 MB at 16 samples and 540 MB at the 64 maximum, on every frame of the render. PNG chunk CRC32 now has one owner in utils/crc32.ts, shared by the new encoder and ffprobe's metadata reader. The encoder previously imported zlib's crc32 by name, which throws at module evaluation on Node 22.0 and 22.1; ffprobe already carried the guarded version and the comment explaining why. The sRGB transfer function is likewise exported once and shared. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
@hyperframes/core
Types, parsers, generators, compiler, linter, runtime, and frame adapters for the Hyperframes video framework.
Install
npm install @hyperframes/core
Most users don't need to install core directly — the CLI, producer, and studio packages depend on it internally.
What's inside
| Module | Description |
|---|---|
| Types | TimelineElement, CompositionSpec, Asset, canvas dimensions, defaults |
| Parsers | parseHtml — extract timeline elements from HTML; parseGsapScript — parse GSAP animations |
| Generators | generateHyperframesHtml — produce valid Hyperframes HTML from a composition spec |
| Compiler | compileTimingAttrs — resolve data-start / data-duration into absolute times |
| Linter | lintHyperframeHtml — validate Hyperframes HTML (missing attributes, overlapping tracks, etc.) |
| Runtime | IIFE script injected into the browser — manages seek, media playback, and the window.__hf protocol |
| Frame Adapters | Pluggable animation drivers (GSAP, Lottie, CSS, or custom) |
Generated composition trust
Composition generators require trusted authors for code-bearing inputs. styles and
generateHyperframesStyles preserve authored CSS, which can load external resources.
animations may contain __raw: values that are emitted as JavaScript;
includeScripts: true includes executable timeline code. serializeGsapAnimations
also accepts raw preamble, postamble, and a code-bearing timelineVar. Never fill
these inputs with untrusted data. Attribute encoding and closing-tag containment
are not a sandbox; render untrusted compositions in an appropriately isolated
execution environment and never serve them on a privileged origin.
Text content retains the supported inline-formatting sanitizer contract. The clip parser intentionally flattens inner formatting to text, so parse/generate is not a lossless replacement for editing the source HTML.
Frame Adapters
A frame adapter tells the engine how to seek your animation to a specific frame:
import { createGSAPFrameAdapter } from "@hyperframes/core";
const adapter = createGSAPFrameAdapter({
getTimeline: () => gsap.timeline(),
compositionId: "my-video",
});
Implement FrameAdapter for custom animation runtimes:
import type { FrameAdapter } from "@hyperframes/core";
const myAdapter: FrameAdapter = {
id: "my-adapter",
getDurationFrames: () => 300,
seekFrame: (frame) => {
/* seek your animation */
},
};
Parsing and generating HTML
import { parseHtml, generateHyperframesHtml } from "@hyperframes/core";
const { elements, metadata } = parseHtml(htmlString);
const html = generateHyperframesHtml(spec);
Linting
import { lintHyperframeHtml } from "@hyperframes/core/lint";
const result = lintHyperframeHtml(htmlString);
// result.findings: { severity, message, elementId }[]
Documentation
Full documentation: hyperframes.heygen.com/packages/core
Related packages
@hyperframes/engine— rendering engine that drives the browser@hyperframes/producer— full render pipeline (capture + encode)hyperframes— CLI