* perf(core): probe media volume with one timeline walk per pass, not one per element Each bind, rebind and scene-swap pass probes every media element for volume automation, and each probe flattened the whole captured timeline (getChildren(true, true, false)) to ask whether any tween could move that one element's volume. A pass cost elements x tweens. Elements with no automation are never cached, so the cost came back on every rebind pass. On a two-hour podcast episode (2,354 media clips, ~100k caption tweens) that was several seconds of preview main thread per pass (CPU profile of the project's open in Desktop: IE/PE -> getChildren, ~6 s). volumeMoversOf() now walks the tweens once and lists the elements a tween names volume on, plus whether any non-DOM setter is tweened. Each pass builds it lazily and shares it with every probe; a probe without one walks as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(core): find the master bus from the first composition root The top-level root is the first [data-composition-id] in document order, so one match replaces a scan of every composition node, read once per clip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(core): lay out a clip again only when its layout inputs change applyClipLayout ran on every timeline post and, for every root clip, lifted display:none, read computed style and restored it. A clip whose inline layout, class and data-width/height are as last laid out gets the same answer, so it is skipped. On a 2,354-clip episode that was 4 of every 10 s of playback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(core): a clip is laid out again only when its layout inputs change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(core): play a long film without re-walking it every frame - The visibility pass skips root-level media far from the playhead during steady playback: hidden last pass, not loading or armed, and its window (widened by every look-ahead) clear of the span played and the loop start. Seeks, forced syncs and renders visit every node. - The GSAP re-render saves and restores only the landing marks near the playhead, placed once per timeline structure; the zero-duration-callback check is cached the same way. A caption track of 38k sets was walked twice a frame. - Registered timeline hosts, the root composition and the per-frame whole-film queries ([data-start], audio groups, timed audio) are kept for one timing revision; a render reads fresh. - The runtime's own media release (src reset) and load events of clips with an authored length no longer count as timing changes, which re-posted the timeline about twice a second. - The timeline payload resolves a stacking context once per parent. 2,354-clip episode in Desktop: playback 15 -> 67-89 fps (unminified runtime). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(core): check each element once per batch of raster-hint records, and cache the root from init - A script building a large tree (a caption track) made each attribute record re-walk its subtree with getComputedStyle; a batch now checks each element once, an element whose own attributes changed still reading its own hints. - The root composition cache installs the timing observer itself: a root with a declared length never reads the duration floors that installed it, so init looked the root up once per media clip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core): type the playback visibility skip for any element, and cache clip layout only once loaded The skip's predicate took an HTMLElement where the pass hands it a StylableElement (tsconfig.runtime.json). The layout cache records a clip only once the document has loaded, so a pass before stylesheets apply is redone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core): realm-safe element checks in the timing caches; cut added comments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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