Files
hyperframes/packages/core
Vance IngallsandClaude Opus 5 c3291f2912 fix(core): reverb level, phaser wiring, per-channel dynamics, one-pole rebuild
Four defects in the graph builders, all found by review rather than by ear.

**Reverb was unusable at its own defaults.** A ConvolverNode applies the
impulse's gain whole — the graph sets `normalize = false` so a room is
deterministic rather than browser-defined — but the impulse was raw decaying
noise. Measured L2 at the registry default (size 0.7 / damping 0.5): 46.4, or
+33.3 dB, putting the wet path ~24 dB over dry at the default `wet: 0.35`. It is
now normalised to unit energy, so the wet knob means what it says. Preview and
render both convolve this buffer, so they stayed identical throughout — equally
deafening before, equally correct now.

**Phaser in_gain/out_gain trim the signal entering and leaving the effect**, not
a wet/dry pair. Wired to the wet and dry legs, "Input" muted the dry path and
the two defaults summed to 1.14, so inserting a phaser raised the track level.
They are now input and output trims with the legs summed at unity. Its declared
waveform is also honoured: `lfo.type` was never assigned, so the default
"Triangular" was silently a sine.

**The dynamics worklets held one envelope across a channel-major loop.** The
followers advance per sample, so on stereo a 20 ms attack behaved as 10 ms, and
the right channel's gain came from an envelope that had already traversed the
left — the two ducked differently from the same input and the image pumped.
State is now per channel, as is the gate's smoothed gain and bitcrush's
sample-hold counter, which previously advanced only on the last channel and left
every earlier one frozen for a whole quantum. The gate also honours the knee it
declares instead of chattering on material sitting at the threshold.

**A one-pole filter's cutoff was swallowed in preview.** Its coefficients are
fixed at construction, so `update` cannot push a new frequency — but the shape
signature carried only type and pole count, so a cutoff change looked like a
values-only edit and went into a no-op updater. Preview kept filtering at the
old frequency while the render used the new one: a preview/render divergence in
exactly the two effects that do not use a BiquadFilterNode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:36:30 -07:00
..
2026-08-04 09:49:34 -07:00

@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)

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