docs: rebuild developer and rendering reference

This commit is contained in:
ukimsanov
2026-08-04 02:45:21 -07:00
parent bb416a1413
commit bebaf679d9
36 changed files with 2608 additions and 4129 deletions
+22 -9
View File
@@ -3,7 +3,7 @@ title: Adopters
description: Organizations using HyperFrames in production or actively evaluating it. description: Organizations using HyperFrames in production or actively evaluating it.
--- ---
The teams below are shipping with HyperFrames. If your organization uses HyperFrames — in production, in evaluation, or in a side project — we'd love to add you. The teams below use HyperFrames in production or are actively evaluating it. If your organization uses HyperFrames — in production, in evaluation, or in a side project — we'd love to add you.
## How to add your organization ## How to add your organization
@@ -16,46 +16,59 @@ Open a pull request that adds your team to [`ADOPTERS.md`](https://github.com/he
If you'd rather not be listed publicly, we'd still love to hear about your usage — drop a note in [our Discord](https://discord.gg/EbK98HBPdk). If you'd rather not be listed publicly, we'd still love to hear about your usage — drop a note in [our Discord](https://discord.gg/EbK98HBPdk).
## Production ## Teams using or evaluating HyperFrames
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="HeyGen" href="https://www.heygen.com"> <Card title="HeyGen" href="https://www.heygen.com">
<img src="https://app.heygen.com/apple-touch-icon.png" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} /> <img alt="HeyGen logo" src="https://app.heygen.com/apple-touch-icon.png" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} />
Powers AI-generated video composition and rendering across HeyGen's video product surface. Powers AI-generated video composition and rendering across HeyGen's video product surface.
[@jrusso1020](https://github.com/jrusso1020) [@jrusso1020](https://github.com/jrusso1020)
</Card> </Card>
<Card title="tldraw" href="https://tldraw.com"> <Card title="tldraw" href="https://tldraw.com">
<img src="https://www.google.com/s2/favicons?domain=tldraw.com&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} /> <img alt="tldraw logo" src="https://www.google.com/s2/favicons?domain=tldraw.com&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} />
Generates automated pull-request walkthrough videos with GSAP-animated code diffs, narration, and captions. Generates automated pull-request walkthrough videos with GSAP-animated code diffs, narration, and captions.
[@steveruizok](https://github.com/steveruizok) [@steveruizok](https://github.com/steveruizok)
</Card> </Card>
<Card title="TanStack" href="https://tanstack.com"> <Card title="TanStack" href="https://tanstack.com">
<img src="https://www.google.com/s2/favicons?domain=tanstack.com&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} /> <img alt="TanStack logo" src="https://www.google.com/s2/favicons?domain=tanstack.com&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} />
Exploring HyperFrames for short-form code demo videos and documentation. Exploring HyperFrames for short-form code demo videos and documentation.
[@AlemTuzlak](https://github.com/AlemTuzlak) [@AlemTuzlak](https://github.com/AlemTuzlak)
</Card> </Card>
<Card title="OptinMonster" href="https://optinmonster.com"> <Card title="OptinMonster" href="https://optinmonster.com">
<img src="https://www.google.com/s2/favicons?domain=optinmonster.com&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} /> <img alt="OptinMonster logo" src="https://www.google.com/s2/favicons?domain=optinmonster.com&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} />
Exploring HyperFrames for marketing and product video content. Exploring HyperFrames for marketing and product video content.
Angie Meeker Angie Meeker
</Card> </Card>
<Card title="reap" href="https://reap.video"> <Card title="reap" href="https://reap.video">
<img src="https://www.google.com/s2/favicons?domain=reap.video&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} /> <img alt="reap logo" src="https://www.google.com/s2/favicons?domain=reap.video&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} />
Powers agent-first AI video clipping, editing, and rendering across reap.video's creator and agent workflows. Powers agent-first AI video clipping, editing, and rendering across reap.video's creator and agent workflows.
[@usamaabid](https://github.com/usamaabid) [@usamaabid](https://github.com/usamaabid)
</Card>
<Card title="Typeframe" href="https://typeframe.app">
<img alt="Typeframe logo" src="https://www.google.com/s2/favicons?domain=typeframe.app&sz=128" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} />
Renders speech videos into shareable MP4 exports with word-timed typing captions and styled caption layouts.
[@kiyeonjeon21](https://github.com/kiyeonjeon21)
</Card> </Card>
<Card title="THU-MAIC" href="https://github.com/THU-MAIC"> <Card title="THU-MAIC" href="https://github.com/THU-MAIC">
<img src="https://avatars.githubusercontent.com/u/163809488?v=4" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} /> <img alt="THU-MAIC logo" src="https://avatars.githubusercontent.com/u/163809488?v=4" width="48" height="48" style={{borderRadius: "10px", marginBottom: "12px"}} />
Powers OpenMAIC's one-click MP4 export for AI-generated interactive classrooms using self-contained HyperFrames compositions. Powers OpenMAIC's one-click MP4 export for AI-generated interactive classrooms using self-contained HyperFrames compositions.
@@ -63,4 +76,4 @@ If you'd rather not be listed publicly, we'd still love to hear about your usage
</Card> </Card>
</CardGroup> </CardGroup>
The HeyGen team's actual launch-video sources (the ones featured in product announcements) live at [hyperframes-launches](https://github.com/heygen-com/hyperframes-launches) — see [Launch Videos](/launch-videos) for the writeup. The HeyGen team's actual launch-video sources (the ones featured in product announcements) live at [hyperframes-launches](https://github.com/heygen-com/hyperframes-launches) — see [Examples](/examples) for the writeup.
+46 -188
View File
@@ -1,218 +1,76 @@
--- ---
title: Compositions title: "Understand compositions"
description: "The fundamental building block of a Hyperframes video." sidebarTitle: "Compositions"
description: "See how a HyperFrames project divides a video into timed, reusable parts."
--- ---
A composition is an HTML document that defines a video timeline. Every clip -- video, image, audio -- lives inside a composition. A composition is a piece of a HyperFrames project with its own size and timeline. The main composition is the finished sequence. Other compositions can be scenes, captions, title systems, or reusable visual parts inside it.
## Structure ```text
Main video
Every composition needs a root element with `data-composition-id`: ├── Opening
├── Product demo
```html index.html ├── Customer quote
<div id="root" data-composition-id="root" └── Closing
data-start="0" data-width="1920" data-height="1080">
<!-- Elements go here -->
</div>
``` ```
The `index.html` file is the top-level composition. It can contain nested compositions within it. Any composition can be imported into another -- there is no special "root" type. Each part is still HTML. Studio, the agent, and normal code tools all work on the same files.
## Clip Types ## What a composition controls
A clip is any discrete block on the timeline, represented as an HTML element with [data attributes](/concepts/data-attributes): A composition brings three things together:
- `<video>` -- Video clips, B-roll, A-roll - **content** — text, shapes, images, video, audio, or another composition;
- `<img>` -- Static images, overlays - **time** — when each part starts and how long it remains;
- `<audio>` -- Music, sound effects - **motion** — seekable animation tied to the same playhead.
- `<div data-composition-id="...">` -- Nested compositions (animations, grouped sequences)
See the [HTML Schema Reference](/reference/html-schema) for the full list of attributes on each clip type. The main composition also defines the output frame and total duration.
## Nested Compositions ## When to create another composition
You can embed one composition inside another in two ways: loading from an external file or defining it inline. External files are the recommended approach for reusable compositions. Separate a part when it has a clear job and can be understood on its own.
<Tabs> Good boundaries include:
<Tab title="External file">
Reference another HTML file with `data-composition-src`. The framework automatically fetches the file, extracts the `<template>` content, mounts it, executes scripts, and registers the timeline.
`data-composition-src` paths resolve relative to the **project root**, not the referencing file — a nested composition one level deep still writes `compositions/foo.html`, never `../compositions/foo.html`. - a complete scene;
- a repeated title or caption treatment;
- a product-demo sequence;
- a visual reused in several places;
- a complex section that is easier to review separately.
```html index.html Do not split every small element into its own file. Extra nesting makes a simple project harder to follow.
<div
id="el-5"
data-composition-id="intro-anim"
data-composition-src="compositions/intro-anim.html"
data-start="0"
data-duration="4"
data-playback-start="0"
data-track-index="3"
></div>
```
Each external composition file wraps its content in a `<template>` tag: ## How nesting works
```html compositions/intro-anim.html The main composition gives each nested composition a place on its timeline:
<template id="intro-anim-template">
<div data-composition-id="intro-anim" data-width="1920" data-height="1080">
<div class="title">Welcome!</div>
<style> ```html
[data-composition-id="intro-anim"] .title {
font-size: 72px; color: white; text-align: center;
}
</style>
<script>
const tl = gsap.timeline({ paused: true });
tl.from(".title", { opacity: 0, y: -50, duration: 1 });
window.__timelines["intro-anim"] = tl;
</script>
</div>
</template>
```
`data-playback-start` selects the child timeline time shown when the host begins. It defaults to `0`. A left trim or split advances this source-time offset by the elapsed host time multiplied by `data-playback-rate`, so the nested animation remains continuous instead of restarting.
</Tab>
<Tab title="Inline">
Define a nested composition directly inside the parent. This is simpler for one-off compositions that do not need to be reused.
```html index.html
<div id="root" data-composition-id="root"
data-start="0" data-width="1920" data-height="1080">
<!-- Inline nested composition -->
<div id="el-5" data-composition-id="intro-anim"
data-start="0" data-track-index="3"
data-width="1920" data-height="1080">
<div class="title">Welcome!</div>
</div>
<script>
// Timeline for the inline composition
const introTl = gsap.timeline({ paused: true });
introTl.from(".title", { opacity: 0, y: -50, duration: 1 });
window.__timelines["intro-anim"] = introTl;
</script>
</div>
```
Inline compositions do not use `<template>` tags or `data-composition-src`.
</Tab>
</Tabs>
### Project Structure
<Tree>
<Tree.Folder name="project" defaultOpen>
<Tree.File name="index.html" />
<Tree.Folder name="compositions" defaultOpen>
<Tree.File name="intro-anim.html" />
<Tree.File name="caption-overlay.html" />
<Tree.File name="outro-title.html" />
</Tree.Folder>
<Tree.Folder name="assets">
<Tree.File name="video.mp4" />
<Tree.File name="music.mp3" />
<Tree.File name="logo.png" />
</Tree.Folder>
</Tree.Folder>
</Tree>
## Two Layers: Primitives and Scripts
Every composition has two layers:
- **HTML** -- primitive clips (`video`, `img`, `audio`, nested compositions). The declarative structure: what plays, when, and on which track. Controlled by [data attributes](/concepts/data-attributes).
- **Script** -- effects, transitions, dynamic DOM, canvas, SVG -- creative animation via [GSAP](/guides/gsap-animation). Scripts do **not** control media playback or clip visibility.
<Warning>
Never use scripts to play/pause/seek media elements or to show/hide clips based on timing. The framework handles this automatically from data attributes. Scripts that duplicate this behavior will conflict with the framework. See [Common Mistakes](/guides/common-mistakes) for examples.
</Warning>
## Variables
HyperFrames does not automatically bind `data-var-*` attributes into your composition DOM or CSS.
The supported pattern is:
1. Declare the variables once on the sub-comp's composition root with `data-composition-variables` (id + type + default) — the `<html>` element for a full-document composition, or the `[data-composition-id]` root element for a template / fragment sub-composition.
2. Pass per-instance values on each composition host with `data-variable-values`.
3. Read the resolved values inside the composition with `window.__hyperframes.getVariables()`. The runtime layers the host's `data-variable-values` over the declared defaults on a per-instance basis, so the same source can be embedded multiple times with different values.
```html index.html
<div <div
data-composition-id="card-pro" id="product-demo-host"
data-composition-src="compositions/card.html" data-composition-id="product-demo"
data-start="0" data-composition-src="compositions/product-demo.html"
data-start="4"
data-duration="6"
data-track-index="1" data-track-index="1"
data-variable-values='{"title":"Pro","color":"#ff4d4f"}' data-width="1920"
></div> data-height="1080"
<div
data-composition-id="card-enterprise"
data-composition-src="compositions/card.html"
data-start="card-pro"
data-track-index="1"
data-variable-values='{"title":"Enterprise","color":"#22c55e"}'
></div> ></div>
``` ```
```html compositions/card.html `data-composition-src` paths resolve from the project root, even when the referencing composition is inside another folder.
<html data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Fallback"},
{"id":"color","type":"color","label":"Color","default":"#111827"}
]'>
<body>
<div data-composition-id="card" data-width="1920" data-height="1080">
<h1 class="title"></h1>
<style> This tells HyperFrames to mount `product-demo.html` at four seconds and show a six-second window of it. The nested source keeps its own content and animation.
[data-composition-id="card"] {
--card-color: #111827;
}
[data-composition-id="card"] .title { If the same source is mounted more than once, each instance can start at a different time or receive different [variables](/concepts/variables).
color: var(--card-color);
}
</style>
<script> ## Decide where to make a change
// Inside a sub-comp script, getVariables() returns the per-instance
// values: declared defaults < host data-variable-values overrides.
const { title, color } = __hyperframes.getVariables();
const root = document.querySelector('[data-composition-id="card"]');
root.querySelector(".title").textContent = title;
root.style.setProperty("--card-color", color);
</script>
</div>
</body>
</html>
```
If you are building tooling on top of `@hyperframes/core`, the same `data-composition-variables` array is readable via `extractCompositionMetadata()` for Studio editing UI and analysis pipelines. - Edit the **nested composition** when the scene itself should change everywhere it is used.
- Edit the **main composition** when only its placement, duration, or relationship to other scenes should change.
- Ask the **agent** when a revision changes the story or crosses several compositions.
## Listing Compositions In Studio, open or expand a nested scene when you need to work inside it, then use the breadcrumb to return to the parent sequence.
Use the [CLI](/packages/cli) to see all compositions in a project: ## Continue
```bash Use [Variables](/concepts/variables) when the design should stay fixed while approved content changes. Use the [HTML schema](/reference/html-schema) when you need exact element and attribute rules.
npx hyperframes compositions
```
## Next Steps
<CardGroup cols={2}>
<Card title="Data Attributes" icon="code" href="/concepts/data-attributes">
Full reference for timing, media, and composition attributes
</Card>
<Card title="GSAP Animation" icon="wand-magic-sparkles" href="/guides/gsap-animation">
Add animations to your compositions with GSAP timelines
</Card>
<Card title="Examples" icon="grid-2" href="/examples">
Start from built-in examples for common video patterns
</Card>
<Card title="HTML Schema Reference" icon="book" href="/reference/html-schema">
Complete schema for authoring compositions
</Card>
</CardGroup>
+52 -94
View File
@@ -1,111 +1,69 @@
--- ---
title: Data Attributes title: "Time elements with data attributes"
description: "Core attributes for controlling element timing and behavior." sidebarTitle: "Data attributes"
description: "Place clips on the HyperFrames timeline without putting timing logic in JavaScript."
--- ---
Hyperframes uses HTML data attributes to control timing, media playback, and [composition](/concepts/compositions) structure. These are the declarative building blocks of every video. HyperFrames keeps timing in HTML. A timed element normally needs a stable ID,
a start, a duration, and a track:
## Timing Attributes ```html
<section id="headline" class="clip" data-start="0" data-duration="3" data-track-index="1">
| Attribute | Example | Description | Launch day
|-----------|---------|-------------| </section>
| `data-start` | `"0"` or `"intro"` | Start time in seconds, or a clip ID reference for [relative timing](#relative-timing) |
| `data-duration` | `"5"` | Duration in seconds. Required for images and sub-compositions. Optional for video/audio (defaults to source duration). On the **root** composition it sets the total render length (see [Composition Attributes](#composition-attributes)). |
| `data-track-index` | `"0"` | Timeline track number. Temporal ordering — groups clips into rows on the timeline. Clips on the same track cannot overlap. Does **not** control z-ordering (use CSS `z-index` for that). |
## Media Attributes
| Attribute | Example | Description |
|-----------|---------|-------------|
| `data-media-start` | `"2"` | Media playback offset / trim point in seconds. Default: `0` |
| `data-playback-start` | `"2"` | Source-time offset in seconds for media wrappers and nested composition hosts. Missing values default to `0`; Studio writes this attribute when a composition is trimmed or split. |
| `data-playback-rate` | `"1.5"` | Source playback multiplier, clamped to `0.1``5`. Source time advances by timeline elapsed time multiplied by the canonical rate; invalid values default to `1`. |
| `data-volume` | `"0.8"` | Audio/video volume, 0 to 1 |
| `data-has-audio` | `"true"` | Indicates video has an audio track |
## Composition Attributes
| Attribute | Example | Description |
|-----------|---------|-------------|
| `data-composition-id` | `"root"` | Unique ID for [composition](/concepts/compositions) wrapper (required on every composition) |
| `data-width` | `"1920"` | Composition width in pixels |
| `data-height` | `"1080"` | Composition height in pixels |
| `data-duration` (root) | `"30"` | On the **root** composition, the total render length / frame count in seconds. Read once from the source HTML at compile time, like `data-width` / `data-height`, so a script or `hyperframes render --variables` cannot change it (author it directly, one value per output). If the root omits `data-duration`, and only then, the renderer derives the total length from the live DOM / timeline after scripts run. |
| `data-composition-src` | `"./intro.html"` | Path to external [composition](/concepts/compositions) HTML file |
| `data-variable-values` | `'{"title":"Hello"}'` | JSON object of values passed to a nested composition. Inside the sub-composition, read them via `window.__hyperframes.getVariables()` — the runtime layers these over the sub-comp's own `data-composition-variables` defaults and exposes the merged result on a per-instance basis (the same source can be embedded multiple times with different values). |
| `data-composition-variables` | `'[{"id":"title","type":"string","label":"Title","default":"Hello"}]'` | JSON array of declared variables (`id`, `type`, `label`, `default`). Drives Studio editing UI and provides defaults read by `window.__hyperframes.getVariables()`. The CLI flag `hyperframes render --variables '<json>'` overrides these defaults at top-level render time; host elements override them per-instance via `data-variable-values`. |
| `data-var-src` | `"heroImage"` | Binds the element's `src` to a declared variable — the runtime substitutes the value (URL string or image `{url}`) in preview and render; the authored `src` stays as the fallback. |
| `data-var-text` | `"title"` | Binds the element's own text to a scalar variable. Element children (nested clips, animated spans) are preserved. Scalar variables are also applied as `--{id}` CSS custom properties on the composition root, so `color: var(--accent)` responds to overrides. |
## Element Visibility
Add `class="clip"` to all timed elements so the runtime can manage their visibility lifecycle:
```html index.html
<h1 id="title" class="clip"
data-start="0" data-duration="5" data-track-index="0">
Hello World
</h1>
``` ```
## Relative Timing | Attribute | What it controls |
| ------------------ | ------------------------------------------------ |
| `data-start` | When the element enters the composition timeline |
| `data-duration` | How long its timeline slot lasts |
| `data-track-index` | Which timeline lane owns that slot |
Instead of calculating absolute start times, a clip can reference another clip's `id` in its `data-start` attribute. This means "start when that clip ends": Add `class="clip"` to timed DOM and image elements so the runtime can control
their visibility. Video visibility is managed by the media runtime; audio has
no visual lifecycle.
```html index.html ## Tracks are not layers
<video id="intro" data-start="0" data-duration="10" data-track-index="0" src="..."></video>
<video id="main" data-start="intro" data-duration="20" data-track-index="0" src="..."></video> Tracks prevent time ranges from colliding. They do not decide which element is
<video id="outro" data-start="main" data-duration="5" data-track-index="0" src="..."></video> in front. Use CSS `z-index` for paint order.
Two clips on one track cannot overlap. Put an intentional overlap, such as a
crossfade, on separate tracks:
```html
<video
id="intro"
data-start="0"
data-duration="4"
data-track-index="0"
src="./intro.mp4"
muted
playsinline
></video>
<section id="result" class="clip" data-start="intro - 0.5" data-duration="3" data-track-index="1">
The result
</section>
``` ```
`main` resolves to second 10, `outro` resolves to second 30. If `intro`'s duration changes, downstream clips shift automatically. ## Start relative to another clip
### Offsets (Gaps and Overlaps) A numeric `data-start` is an absolute time in seconds. A clip ID means “start
when that clip ends.” Add or subtract seconds for a gap or overlap:
Add `+ N` or `- N` after the ID to offset from the end of the referenced clip: ```html
data-start="intro" data-start="intro + 0.5" data-start="intro - 0.5"
```html index.html
<!-- 2-second gap after intro -->
<video id="scene-a" data-start="intro + 2" data-duration="20"
data-track-index="0" src="..."></video>
<!-- 0.5-second overlap with intro (crossfade) -->
<video id="scene-b" data-start="intro - 0.5" data-duration="20"
data-track-index="1" src="..."></video>
``` ```
<Note> References resolve only inside the same composition. The referenced clip must
Overlapping clips must be on different tracks -- clips on the same track cannot overlap in time. have a known duration, and reference chains cannot contain a cycle.
</Note>
<Accordion title="Relative timing rules and constraints"> ## Media and nested compositions
**Same composition only** -- references resolve within the clip's parent [composition](/concepts/compositions). You cannot reference a clip in a sibling or parent composition.
**No circular references** -- A cannot start after B if B starts after A. The resolver detects cycles and throws an error. Media can also declare a source offset, playback rate, and volume. A nested
composition adds a source path and its own fixed timeline window. These are
exact contracts rather than new timing models.
**Referenced clip must have a known duration** -- either an explicit `data-duration` or a duration inferred from source media. If the referenced clip has no known duration, the reference cannot resolve. Use the [HTML schema reference](/reference/html-schema) for every supported
attribute and element-specific rule. Use [Compositions](/concepts/compositions)
**Parsing rules** -- if the value is a valid number, it is treated as absolute seconds. Otherwise it is parsed as one of: to decide when a scene should become a separate composition.
- `<id>` -- start when that clip ends
- `<id> + <number>` -- start N seconds after that clip ends
- `<id> - <number>` -- start N seconds before that clip ends
**Chain length** -- references can chain (`A` -> `B` -> `C`), but deeply nested chains make the timeline harder to reason about. Keep chains under 3-4 levels for readability.
</Accordion>
## Next Steps
<CardGroup cols={2}>
<Card title="Compositions" icon="layer-group" href="/concepts/compositions">
How compositions use data attributes to define video structure
</Card>
<Card title="HTML Schema Reference" icon="book" href="/reference/html-schema">
Complete attribute reference with per-element details
</Card>
<Card title="GSAP Animation" icon="wand-magic-sparkles" href="/guides/gsap-animation">
Animate elements alongside data-attribute-driven timing
</Card>
<Card title="Common Mistakes" icon="triangle-exclamation" href="/guides/common-mistakes">
Pitfalls to avoid when setting up timing and attributes
</Card>
</CardGroup>
+45 -76
View File
@@ -1,103 +1,72 @@
--- ---
title: Deterministic Rendering title: Deterministic Rendering
description: "Same input, identical output. Every time." description: "Make every frame depend on the playhead, not the wall clock."
--- ---
Hyperframes is built around a core guarantee: **the same [composition](/concepts/compositions) always produces the same video**. This is what makes automated pipelines, CI testing, and AI-driven workflows reliable. HyperFrames renders by seeking to one frame at a time. A well-authored composition can therefore reproduce the same state whenever the renderer seeks to the same frame.
## How It Works This is what makes a slow animation safe to render: the renderer does not need to play it in real time, and it does not drop frames when a frame takes longer to produce.
The rendering pipeline is frame-by-frame and seek-driven. No realtime playback is involved -- every frame is independently seeked and captured. ## The rendering model
<Steps> For each output frame, HyperFrames:
<Step title="Frame clock">
The [engine](/packages/engine) computes the time for each frame using integer math: `time = floor(frame) / fps`. There is no wall-clock dependency -- rendering is entirely decoupled from real time.
</Step>
<Step title="Seek">
The [frame adapter](/concepts/frame-adapters) receives a `seekFrame(frame)` call and deterministically positions all animations, DOM state, and canvas content to the exact frame. The adapter pauses all [GSAP](/guides/gsap-animation) timelines and seeks them to the computed time.
</Step>
<Step title="Capture">
Chrome's `HeadlessExperimental.beginFrame` API captures the pixel buffer for the current frame. This is a single, atomic operation -- no partial paints or race conditions.
</Step>
<Step title="Encode">
FFmpeg encodes the captured frames into the final MP4 video. Audio tracks from `<audio>` and `<video>` elements are mixed in during this stage.
</Step>
</Steps>
```mermaid 1. converts the frame number to a time;
graph LR 2. seeks the registered animation adapter to that state;
A["Frame Clock<br/>t = frame / fps"] --> B["Seek<br/>adapter.seekFrame(frame)"] 3. updates time-based media and composition state;
B --> C["Capture<br/>beginFrame API"] 4. captures the frame;
C --> D["Encode<br/>FFmpeg"] 5. sends the result to the encoder.
D --> E["MP4"]
style A fill:#00C4FF,color:#fff The preview and renderer use the same composition runtime. Preview performance can still depend on your computer, while the final render advances frame by frame.
style B fill:#00C4FF,color:#fff
style C fill:#00C4FF,color:#fff ## What your composition must control
style D fill:#00C4FF,color:#fff
style E fill:#00A8E1,color:#fff The frame must be reproducible from its inputs and current time.
- Use a paused, registered timeline instead of animation driven by `requestAnimationFrame`.
- Do not use `Date.now()` or the current system time.
- Seed random values instead of calling unseeded `Math.random()`.
- Keep assets local or make sure they are fully available before rendering.
- Give the composition a finite duration, dimensions, and frame rate.
- Make custom frame adapters safe to seek in any order.
Run both checks before a final render:
```bash
npx hyperframes lint
npx hyperframes check
``` ```
## What Makes It Deterministic ## What “repeatable” does not mean
- **No wall-clock dependencies** -- rendering does not use `Date.now()`, `requestAnimationFrame`, or system timers Seek-driven rendering controls time. It does not make different computers identical.
- **No unseeded randomness** -- `Math.random()` without a seed breaks determinism
- **No render-time network fetches** -- all assets must be loaded before rendering starts
- **Fixed output parameters** -- `fps`, `width`, and `height` are locked before the first frame
- **Finite duration** -- every [composition](/concepts/compositions) has a known, finite length
These same rules apply to every [frame adapter](/concepts/frame-adapters). If you are building a custom adapter, you must follow the [determinism contract](/concepts/frame-adapters#determinism-contract). Chrome versions, installed fonts, GPU behavior, operating systems, and encoder versions can produce small differences even when the composition is unchanged. If exact reproduction across machines matters, render in the same controlled environment:
## Docker Mode
For maximum reproducibility, render in Docker:
```bash ```bash
npx hyperframes render --docker --output output.mp4 npx hyperframes render --docker --output output.mp4
``` ```
Docker mode uses an exact Chrome version and font set, ensuring: Docker reduces environment differences by using a controlled browser, font, and encoder setup. Keep the same assets and render settings as well.
- Same Chromium rendering engine across all platforms
- Same system fonts (no platform-specific font substitution)
- Same FFmpeg encoder version
See the [Rendering guide](/guides/rendering) for all rendering options. ## For frame-adapter authors
## Preview vs. Render Parity A frame adapter must follow four rules:
The browser preview and the rendered MP4 should match. Hyperframes achieves this through: - seeking the same frame twice returns the same state;
- frames can be requested out of order;
- no unfinished asynchronous work changes the committed frame later;
- `destroy()` leaves no state behind for the next render.
- **One runtime** -- the same `hyperframe.runtime` drives both preview and render See [Frame adapters](/concepts/frame-adapters) for the interface and a complete example.
- **Producer-canonical behavior** -- the [producer's](/packages/producer) seek semantics are the source of truth
- **Readiness gates** -- `__playerReady` and `__renderReady` ensure the [composition](/concepts/compositions) is fully loaded before any frame is captured
Parity here means **visual fidelity** — every frame looks the same. It does *not* mean performance parity. Preview plays in real time in a browser, so frame-rate limits are bound by your hardware. Render is seek-driven and frame-at-a-time, so it never drops frames regardless of per-frame cost. A composition can stutter in preview and render perfectly. See [Performance](/guides/performance) for why. ## Continue
<Note>
Local rendering (without Docker) may show slight differences due to platform-specific font rendering and Chrome version. Use Docker mode when exact reproducibility matters.
</Note>
## For Adapter Authors
If you are building a [frame adapter](/concepts/frame-adapters), your adapter must follow the determinism contract:
- `seekFrame(frame)` must be idempotent -- same frame, same result
- No side effects that depend on call order (must handle random access)
- No async operations that resolve after the frame is "committed"
- Clean lifecycle: `init` -> `seekFrame` (N times) -> `destroy`
## Next Steps
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Frame Adapters" icon="plug" href="/concepts/frame-adapters"> <Card title="Render a composition" icon="film" href="/guides/rendering">
Build adapters that uphold the determinism contract Choose local, Docker, batch, or cloud rendering.
</Card> </Card>
<Card title="Rendering" icon="film" href="/guides/rendering"> <Card title="Improve render performance" icon="gauge-high" href="/guides/performance">
Render to MP4 locally or in Docker Understand preview speed, capture cost, and render time.
</Card>
<Card title="@hyperframes/producer" icon="clapperboard" href="/packages/producer">
The full rendering pipeline that orchestrates deterministic output
</Card>
<Card title="Common Mistakes" icon="triangle-exclamation" href="/guides/common-mistakes">
Pitfalls that break determinism and how to avoid them
</Card> </Card>
</CardGroup> </CardGroup>
+42 -117
View File
@@ -1,148 +1,73 @@
--- ---
title: Frame Adapters title: "Frame adapters"
description: "Bring your own animation runtime to Hyperframes." description: "Connect a seekable animation timeline to a custom HyperFrames host."
--- ---
The Frame Adapter pattern is how Hyperframes supports multiple animation runtimes. The core question every adapter answers:
> What should the screen look like at frame N?
If a runtime can answer that, it can plug into Hyperframes.
<Info> <Info>
The Adapter API is currently at **v0** (experimental). Breaking changes are possible until v1. The core contract (seek-by-frame, deterministic output) is stable, but method signatures may evolve. The exported `FrameAdapter` interface is experimental v0 API. Its signatures
may change before v1.
</Info> </Info>
## How It Works A frame adapter answers one question: what state should an animation have at frame N?
The host application (the [engine](/packages/engine) or [producer](/packages/producer)) drives rendering by calling adapter methods in a strict sequence. The adapter never controls its own clock -- it only responds to seek commands. Most composition authors do not implement this interface. HyperFrames already seeks registered GSAP, CSS, Anime.js, Lottie, Three.js, Web Animations, and TypeGPU animation through its browser runtime. Use the [GSAP guide](/guides/gsap-animation) for the normal authoring path.
```mermaid Use `FrameAdapter` when you are building a custom host around a seekable animation object.
sequenceDiagram
participant Host as Host (Engine)
participant Adapter as Frame Adapter
participant Chrome as Chrome / Browser
Host->>Adapter: init(context) ## Interface
Adapter-->>Host: ready
Host->>Adapter: getDurationFrames()
Adapter-->>Host: 300 frames
loop For each frame 0..300 ```ts
Host->>Host: normalize frame (clamp, floor) import type { FrameAdapter, FrameAdapterContext } from "@hyperframes/core";
Host->>Adapter: seekFrame(frame)
Adapter->>Chrome: Update DOM / canvas state
Adapter-->>Host: done
Host->>Chrome: Capture pixel buffer
end
Host->>Adapter: destroy()
Adapter-->>Host: cleaned up
```
## Adapter API (v0)
```typescript adapters/types.ts
type FrameAdapterContext = {
compositionId: string;
fps: number;
width: number;
height: number;
rootElement?: HTMLElement;
};
type FrameAdapter = { type FrameAdapter = {
id: string; id: string;
init?: (ctx: FrameAdapterContext) => Promise<void> | void; init?: (context: FrameAdapterContext) => Promise<void> | void;
getDurationFrames: () => number; getDurationFrames: () => number;
seekFrame: (frame: number) => Promise<void> | void; seekFrame: (frame: number) => Promise<void> | void;
destroy?: () => Promise<void> | void; destroy?: () => Promise<void> | void;
}; };
``` ```
## Required Semantics The context contains the composition ID, frame rate, dimensions, and optional root element.
- `getDurationFrames()` must return a finite integer >= 0 ## Adapt a GSAP timeline
- `seekFrame(frame)` must support arbitrary seek order (forward, backward, random)
- `seekFrame(frame)` must be idempotent for the same input frame
- `seekFrame(frame)` must clamp internal time to the adapter's range
- Adapters should be paused/seek-driven, not clock-driven
## Host Orchestration `@hyperframes/core` includes a helper for a GSAP-like timeline:
The host normalizes frames before calling the adapter: ```ts
import { createGSAPFrameAdapter } from "@hyperframes/core";
```typescript engine/render-loop.ts const adapter = createGSAPFrameAdapter({
normalizedFrame = clamp(Math.floor(frame), 0, durationFrames); id: "intro",
fps: 30,
timeline,
});
await adapter.init?.({
compositionId: "intro",
fps: 30,
width: 1920,
height: 1080,
});
await adapter.seekFrame(90); // three seconds
``` ```
A typical render loop: The helper pauses the timeline, derives its frame length, and converts each frame request to seconds.
```typescript engine/render-loop.ts ## Contract
await adapter.init?.({ compositionId, fps, width, height, rootElement });
const durationFrames = adapter.getDurationFrames();
for (let frame = 0; frame <= durationFrames; frame += 1) { A custom adapter must:
await adapter.seekFrame(frame);
// capture pixel buffer for this frame
}
await adapter.destroy?.(); - return a finite, non-negative frame count;
``` - support forward, backward, and random seeks;
- return the same state when the same frame is requested again;
- avoid wall-clock timers and unseeded randomness;
- finish asynchronous work before the frame is captured;
- release listeners and other resources in `destroy()`.
## Determinism Contract The host still owns the capture and encoding pipeline. The adapter owns only the animation state.
These rules are non-negotiable for any adapter. They are the foundation of Hyperframes' [deterministic rendering](/concepts/determinism) guarantee. ## Continue
- Canonical clock: `t = frame / fps` Read [Deterministic rendering](/concepts/determinism) for the timing rules or [`@hyperframes/core`](/packages/core) for the package exports.
- No wall-clock dependencies (`Date.now`, drift-dependent logic)
- No unseeded randomness
- No render-time network fetches
- Fixed output params (`fps`, `width`, `height`)
- Finite duration only
- Deterministic frame quantization before seek
## Supported Runtimes
First-party runtime adapters:
All runtime adapters live in the `/hyperframes-animation` skill — invoke it for the runtime-specific seek API as well as motion rules, scene blueprints, and transitions.
| Runtime | Seek Method | Skill |
|---------|-------------|-------|
| [GSAP](/guides/gsap-animation) | `timeline.totalTime(timeSeconds)` or `timeline.seek(timeSeconds)` | `/hyperframes-animation` |
| Anime.js | `instance.seek(timeMs)` for animations registered on `window.__hfAnime` | `/hyperframes-animation` |
| CSS keyframes | Browser `Animation.currentTime`, with paused negative-delay fallback | `/hyperframes-animation` |
| Lottie / dotLottie | `goToAndStop(timeMs, false)`, raw-frame setters, or player seek APIs | `/hyperframes-animation` |
| Three.js / WebGL | `hf-seek` events plus `window.__hfThreeTime` for deterministic scene rendering | `/hyperframes-animation` |
| Web Animations API | `document.getAnimations()` and `animation.currentTime` | `/hyperframes-animation` |
| TypeGPU / WebGPU | GPU compute shaders with deterministic seek via `hf-seek` events | `/hyperframes-animation` |
Community adapters are welcome -- if it can seek by frame, it belongs in Hyperframes.
## Conformance Tests
Every adapter should pass these minimum tests:
1. **Repeatability** -- seek same frame twice, get identical output
2. **Random seek** -- seek order `[90, 10, 50, 10]` produces deterministic results
3. **Bounds** -- negative and overflow frame values do not break
4. **Duration** -- returned duration is a finite integer
5. **Cleanup** -- no leaked timers/listeners after `destroy`
## Next Steps
<CardGroup cols={2}>
<Card title="Deterministic Rendering" icon="lock" href="/concepts/determinism">
Understand the determinism guarantees adapters must uphold
</Card>
<Card title="GSAP Animation" icon="wand-magic-sparkles" href="/guides/gsap-animation">
See the first-party GSAP adapter in action
</Card>
<Card title="@hyperframes/engine" icon="gear" href="/packages/engine">
The capture engine that drives adapters during rendering
</Card>
<Card title="Contributing" icon="code-branch" href="/contributing">
Build and contribute your own adapter
</Card>
</CardGroup>
+125 -315
View File
@@ -1,383 +1,193 @@
--- ---
title: Variables title: "Reuse a design with variables"
description: "Parameterize compositions so the same source can render different content." sidebarTitle: "Variables"
description: "Change approved text, colors, media, and choices without rebuilding the composition."
--- ---
Variables let you declare named, typed slots in a composition and fill them at render time — from a parent composition, from the CLI, or from an API call. A card composition that takes `title` and `color` can be embedded a hundred times with a hundred different values without duplicating any HTML. Variables expose the parts of a composition that are meant to change. One
customer card can accept a different name, logo, color, and plan while keeping
the same layout and motion.
## Declaring Variables Use a variable when the design should remain stable across versions. Make a
normal source edit when the structure itself needs to change.
Add `data-composition-variables` to a composition's declaration root. For a full-document composition that's the `<html>` element; for a template / fragment sub-composition (which has no `<html>` shell of its own) it's the composition root element — the `[data-composition-id]` div. Its value is a JSON array of variable declarations — one object per variable: <div className="not-prose my-7 grid grid-cols-2 gap-3">
<div className="overflow-hidden rounded-xl border border-zinc-200 bg-zinc-950 dark:border-zinc-800">
```html compositions/card.html
<html data-composition-variables='[
{"id":"title", "type":"string", "label":"Title", "default":"Hello"},
{"id":"color", "type":"color", "label":"Color", "default":"#111827"},
{"id":"price", "type":"number", "label":"Price", "default":0, "unit":"$"},
{"id":"featured","type":"boolean","label":"Featured","default":false},
{"id":"plan", "type":"enum", "label":"Plan", "default":"pro",
"options":[{"value":"pro","label":"Pro"},{"value":"enterprise","label":"Enterprise"}]}
]'>
```
Every declaration requires four fields: `id`, `type`, `label`, and `default`. `id` must be unique within the composition.
## Variable Types
| Type | `default` value | Extra options |
|------|----------------|---------------|
| `string` | `"some text"` | `placeholder?: string`, `maxLength?: number` |
| `number` | `0` | `min?: number`, `max?: number`, `step?: number`, `unit?: string` |
| `color` | `"#rrggbb"` | — |
| `boolean` | `true` / `false` | — |
| `enum` | one of the option values | `options: [{value: string, label: string}]` |
The Studio editing UI uses `label`, `type`, and the type-specific options to render the right input widget for each variable.
## What can be a variable
Variables come in two layers. The five [declared types](#variable-types) above cover typed primitive data — strings, numbers, colors, booleans, enums. For everything else, a `string` variable holding a URL is the escape hatch: your composition reads the URL and assigns it to whatever DOM element needs it.
### Parameterizing media assets
The same composition can render different images, video clips, or audio tracks just by swapping URLs through a string variable:
```html compositions/product-card.html
<html data-composition-variables='[
{"id":"productImage","type":"string","label":"Product image URL","default":"https://cdn.example.com/products/default.png"},
{"id":"productName","type":"string","label":"Product name","default":"Untitled"}
]'>
<body>
<div data-composition-id="product-card" data-width="1920" data-height="1080" data-duration="5">
<img class="product-img" alt="" />
<h1 class="product-name"></h1>
<script>
const {
productImage = "https://cdn.example.com/products/default.png",
productName = "Untitled",
} = __hyperframes.getVariables();
const root = document.querySelector('[data-composition-id="product-card"]');
root.querySelector(".product-img").src = productImage;
root.querySelector(".product-name").textContent = productName;
</script>
</div>
</body>
</html>
```
<Note>
The runtime probes the DOM after your composition script runs, so a `<video>` or `<audio>` `src` assigned at runtime from a variable is discovered and pre-extracted for the render. No extra wiring required — just set the `src` from your variable.
</Note>
The same pattern covers the three media element types:
- **`<img src>`** — assign from a string variable. Chrome fetches it during capture like any other image; no extra config.
- **`<video src>`** — assign from a string variable, but keep the timing attributes (`data-start`, `data-duration`, `data-track-index`, `data-has-audio`) on the element itself. The probe phase scans `video[data-start]` elements after your script runs and reads the resolved `src` for pre-extraction.
- **`<audio src>`** — same as video. The audio is decoded during capture and mixed into the final output.
Pass assets as URL references your composition resolves at render time; don't inline base64. URL-shaped assets travel cleanly through both the local renderer and the Lambda surface — see [Templates on Lambda](/deploy/templates-on-lambda#working-with-large-variables) for the 256 KiB execution-input cap on distributed renders.
### Parameterizing media color grading
Media color grading can also read exact variable references inside
`data-color-grading`. Use `$name` or `${name}` as the entire value for a field;
the runtime resolves it from the current composition's variables before applying
the shader grading, finishing details, blur/pixelate effects, and optional LUT:
```html compositions/hero.html
<html data-composition-variables='[
{"id":"gradingPreset","type":"enum","label":"Preset","default":"clean-studio",
"options":[{"value":"clean-studio","label":"Clean Studio"},{"value":"warm-daylight","label":"Warm Daylight"}]},
{"id":"gradingIntensity","type":"number","label":"Preset strength","default":0.75,"min":0,"max":1,"step":0.05},
{"id":"gradingExposure","type":"number","label":"Exposure","default":0,"min":-2,"max":2,"step":0.05},
{"id":"gradingVibrance","type":"number","label":"Vibrance","default":0.08,"min":-1,"max":1,"step":0.01}
]'>
<body>
<div data-composition-id="hero" data-width="1920" data-height="1080">
<video <video
id="hero-video" className="aspect-video w-full object-cover"
src="assets/hero.mp4" src="https://static.heygen.ai/hyperframes-oss/docs/images/prompting/validate-variables-default.mp4#t=0.1"
data-start="0" autoPlay
data-track-index="0"
muted muted
playsinline loop
data-color-grading='{ playsInline
"preset":"$gradingPreset", preload="metadata"
"intensity":"$gradingIntensity", />
"adjust":{"exposure":"${gradingExposure}","vibrance":"$gradingVibrance"}, <div className="px-3 py-2 text-sm text-zinc-600 dark:text-zinc-400">Default values</div>
"details":{"vignette":0.15,"vignetteFeather":0.72,"grain":0.08,"grainSize":0.25},
"effects":{"blur":0.1},
"colorSpace":"rec709"
}'
></video>
</div> </div>
</body> <div className="overflow-hidden rounded-xl border border-zinc-200 bg-zinc-950 dark:border-zinc-800">
</html> <video
``` className="aspect-video w-full object-cover"
src="https://static.heygen.ai/hyperframes-oss/docs/images/prompting/validate-variables-variant.mp4#t=0.1"
autoPlay
muted
loop
playsInline
preload="metadata"
/>
<div className="px-3 py-2 text-sm text-zinc-600 dark:text-zinc-400">
The same design with different values
</div>
</div>
</div>
When the same composition is embedded multiple times, each host's ## Use variables in Studio
`data-variable-values` can produce different grading without copying or rewriting
the media element's `data-color-grading` JSON.
### Swapping media: do you need to vary duration too? Studio can create and bind variables, preview overrides, and copy the reviewed
values into a render command. Follow [Use variables and templates](/studio/variables)
for that complete workflow.
A common follow-up: if a variable swaps a `<video>` to a different clip, does `data-duration` need to change too? Usually no. `data-duration` is optional on `<video>` and `<audio>` — leave it off and the renderer ffprobes the source and uses its natural length: ## Advanced: declare the approved inputs
```html compositions/hero.html Variables live on the composition declaration:
<video id="hero" data-start="0" data-track-index="0"></video>
<script>
document.getElementById("hero").src = __hyperframes.getVariables().heroVideo;
</script>
```
If you need to clamp or pin the clip to a specific length per render — for example, to keep downstream timing stable across clips of different source lengths — expose duration as its own `number` variable and apply it via the same script:
```html compositions/hero.html
<video id="hero" data-start="0" data-track-index="0"></video>
<script>
const { heroVideo, heroDuration } = __hyperframes.getVariables();
const el = document.getElementById("hero");
el.src = heroVideo;
if (heroDuration !== undefined) {
el.setAttribute("data-duration", String(heroDuration));
}
</script>
```
The probe phase reads a **clip's** `data-duration` from the live DOM after your script runs, so an attribute written programmatically onto a clip or media element behaves identically to one baked into the source HTML.
<Warning>
This live-DOM re-read applies to clip and media elements, **not** to the **root composition's own `data-duration`** (its total render length / frame count). The renderer reads the root `data-duration` once at compile time, before your scripts run, exactly like `data-width` / `data-height`. If the root element carries a static `data-duration`, a script (or a variable) that rewrites it afterward is ignored, and the render uses the compile-time value. To make total render length vary per render, author the root `data-duration` directly (one value per output) rather than trying to drive it from a script. See [What can't be a variable](#what-cant-be-a-variable).
</Warning>
## What can't be a variable
A small set of inputs are read once from the source HTML or from the CLI / SDK, with no live-DOM re-read — no script (and therefore no variable) can change them:
| What | Mechanism (not a variable) |
|------|----------------------------|
| Composition dimensions | `data-width` / `data-height` on the composition element, parsed from the source HTML at compile time, not from the live DOM |
| Root composition total duration | `data-duration` on the **root** composition element (the total render length / frame count), parsed from the source HTML at compile time. A static root `data-duration` is locked before scripts run, so neither a script nor a variable can change the render length. (A clip's own `data-duration` is different: it is re-read from the live DOM, as shown above.) |
| Frame rate | `--fps` flag on `hyperframes render`, or `config.fps` in the SDK |
| Output format / codec / quality | `--format` / `--codec` / `--quality` flags, or the SDK equivalents |
| A sibling or parent composition's variables | Variables are per-composition; use [`data-variable-values`](#per-instance-overrides-sub-compositions) on each sub-comp host element to pass overrides |
The deeper rule: variables are runtime values your script applies to the DOM. They can drive anything the renderer reads from the live DOM after that script runs: text, colors, media `src`, even clip `data-duration` as shown above. They can't change inputs the renderer reads once at compile time (composition dimensions, and the root composition's total duration / render length) or that live entirely outside the composition (CLI flags, encoder settings).
## Reading Variables at Runtime
Inside any composition script, call `window.__hyperframes.getVariables()` to get the resolved variable values. The return type is `Partial<Record<string, unknown>>` — use destructuring with defaults matching the declared `default` values:
```html compositions/card.html ```html compositions/card.html
<html data-composition-variables='[ <html
{"id":"title","type":"string","label":"Title","default":"Untitled"}, data-composition-variables='[
{"id":"color","type":"color","label":"Color","default":"#111827"} {"id":"title","type":"string","label":"Title","default":"Pro"},
]'> {"id":"accent","type":"color","label":"Accent color","default":"#6c5ce7"},
<body> {"id":"logo","type":"string","label":"Logo","default":"assets/logo.svg"}
<div data-composition-id="card" data-width="1920" data-height="1080"> ]'
<h1 class="card-title"></h1> ></html>
<style>
[data-composition-id="card"] { --card-color: #111827; }
[data-composition-id="card"] .card-title { color: var(--card-color); }
</style>
<script>
const { title = "Untitled", color = "#111827" } = __hyperframes.getVariables();
const root = document.querySelector('[data-composition-id="card"]');
root.querySelector(".card-title").textContent = title;
root.style.setProperty("--card-color", color);
</script>
</div>
</body>
</html>
``` ```
`__hyperframes.getVariables()` is a shorthand for `window.__hyperframes.getVariables()` and works in both top-level and sub-composition scripts. The runtime automatically scopes sub-compositions so each instance sees its own resolved values. Supported declared types are:
## Declarative Bindings (No Script Required) | Type | Good for |
| --------- | -------------------------------------- |
| `string` | Text or a media path |
| `number` | Counts, positions, sizes, or strengths |
| `color` | Approved color choices |
| `boolean` | On or off |
| `enum` | One value from an approved list |
| `font` | A font-family choice |
| `image` | An image path or image value |
For the common cases — replaceable media, dynamic text, and CSS-driven styling — you don't need a script at all. The runtime resolves these bindings once at load, identically in preview and render: The type lets Studio show the right control and lets rendering catch invalid
values.
- **`data-var-src="id"`** — sets the element's `src` from the variable value (a URL string, or an image value's `{url}`). The authored `src` stays as the fallback when the variable resolves to nothing: ## Bind common values without a script
```html Use direct bindings for the normal cases:
<img class="clip" data-start="0" data-duration="5"
data-var-src="heroImage" src="fallback.jpg" />
```
<Note> ```html
`data-var-src` is only honored on media elements (`img`, `video`, `audio`, <h1 data-var-text="title">Pro</h1>
`source`) and only for safe URL protocols (`http(s):`, `blob:`, relative
paths, and `data:image/…`). A binding on a script-executing tag such as
`<iframe>`/`<script>`, or a value using `javascript:`/`data:text/html`, is
ignored — variable values may be attacker-influenced, so this prevents them
from becoming a script-injection sink. Scalar values applied as CSS custom
properties are likewise stripped of declaration-smuggling characters
(`; { } < >`).
</Note>
- **`data-var-text="id"`** — sets the element's text content from a scalar variable: <img data-var-src="logo" src="assets/logo.svg" alt="" />
```html <style>
<h1 class="clip" data-start="0" data-duration="5" data-var-text="title">Fallback title</h1> .card-title {
``` color: var(--accent);
}
</style>
```
- **CSS custom properties** — every scalar variable is applied as `--{id}` on its composition root (font values apply their family name), so plain CSS bindings respond to render/preview overrides: - `data-var-text` replaces the elements own text.
- `data-var-src` replaces an image, video, audio, or source URL.
- Scalar variables are available as CSS custom properties such as
`var(--accent)`.
```css Use `window.__hyperframes.getVariables()` only when the result needs conditions,
.card-title { color: var(--accent); font-family: var(--brandFont), sans-serif; } loops, or derived values:
```
Bindings resolve against the element's owning composition, so sub-composition instances see their own per-instance values. The Studio Variables panel counts these bindings as usage. Use `getVariables()` in a script only when you need logic beyond direct substitution (loops, conditionals, derived values). ```js
const { featured = false } = window.__hyperframes.getVariables();
document.querySelector(".badge").hidden = !featured;
```
<Note> ## Give each nested composition different values
**Content Security Policy.** The preview server injects override values via an
inline `<script>window.__hfVariables=…</script>` tag. If you embed the preview
behind a strict CSP (`script-src 'self'` with no `'unsafe-inline'`), that tag is
blocked and the preview silently falls back to declared defaults — allow it with
a nonce or hash. The declarative-binding runtime itself emits no inline scripts,
and the final rendered output is unaffected.
</Note>
## Per-instance Overrides (Sub-compositions) A parent can reuse the same composition several times:
When embedding a composition inside another, use `data-variable-values` on the host element to pass a JSON object of override values for that particular instance:
```html index.html ```html index.html
<div <div
data-composition-id="card-pro" data-composition-id="card-pro"
data-composition-src="compositions/card.html" data-composition-src="compositions/card.html"
data-start="0" data-start="0"
data-track-index="1" data-variable-values='{"title":"Pro","accent":"#ff4d4f"}'
data-variable-values='{"title":"Pro","color":"#ff4d4f"}'
></div> ></div>
<div <div
data-composition-id="card-enterprise" data-composition-id="card-enterprise"
data-composition-src="compositions/card.html" data-composition-src="compositions/card.html"
data-start="card-pro" data-start="card-pro"
data-track-index="1" data-variable-values='{"title":"Enterprise","accent":"#22c55e"}'
data-variable-values='{"title":"Enterprise","color":"#22c55e"}'
></div> ></div>
``` ```
Both host elements point to the same `card.html` source, but each instance receives different values. The runtime merges the host's `data-variable-values` over the sub-comp's declared defaults on a per-instance basis — the same sub-composition can run with completely different content simultaneously. Both instances keep the same source and receive different content.
## CLI Overrides (Top-level Renders) ## Advanced: render a version from data
Pass variable values at render time with `--variables` or `--variables-file`. These override the declared defaults for the top-level composition: Override top-level values from the CLI:
```bash Terminal ```bash
# Inline JSON npx hyperframes render \
npx hyperframes render --variables '{"title":"Q4 Report","color":"#1d4ed8"}' --output q4.mp4 --variables '{"title":"Enterprise","accent":"#22c55e"}' \
--strict-variables \
# JSON file --output enterprise.mp4
npx hyperframes render --variables-file ./vars.json --output out.mp4
# Fail on undeclared or mistyped variables
npx hyperframes render --variables '{"title":"Q4 Report"}' --strict-variables --output out.mp4
``` ```
`--strict-variables` turns variable warnings into errors. Any variable in `--variables` that is not declared in `data-composition-variables`, or whose value does not match the declared type, causes the render to exit non-zero. Useful in CI pipelines where an undeclared variable key likely indicates a typo or a schema mismatch. Use `--variables-file` for a JSON file and `--batch` when the same composition
must render once per data row. The [CLI reference](/packages/cli) covers batch
output, validation, and automation.
<Note> ### Batch renders
CLI overrides apply only to the top-level composition. Sub-composition variables are controlled by `data-variable-values` on each host element.
</Note>
## Batch Renders Put one variable object per row in a JSON array, then use placeholders from the
row to name each output:
Use `--batch` when the same composition should render once per data row:
```json rows.json ```json rows.json
[ [
{ "name": "Alice", "title": "Q4 Report" }, { "name": "acme", "title": "Acme Pro" },
{ "name": "Bob", "title": "Renewal Plan" } { "name": "northstar", "title": "Northstar Pro" }
] ]
``` ```
```bash Terminal ```bash
npx hyperframes render --batch rows.json --output "renders/{name}.mp4" --strict-variables npx hyperframes render \
--batch rows.json \
--strict-variables \
--output "renders/{name}.mp4"
``` ```
Each row is treated like a `--variables` object and merged over the composition defaults. Output paths support `{key}` placeholders from the row plus `{index}`. Hyperframes validates missing placeholders, output collisions, and `--strict-variables` issues before the first row starts rendering, then writes `manifest.json` next to the outputs with one status row per render. Start with the default single-row concurrency. Increase `--batch-concurrency`
only after one real render is stable and the machine has enough memory for
several renders at once.
For small compositions, `--batch-concurrency 2` can run rows in parallel. The default is `1` because each individual render already parallelizes across render workers. ## What can't be a variable
## Layering and Precedence Variables change content inside a composition. They do not change:
Variable values are resolved by merging three sources, lowest to highest precedence: - the composition viewport;
- the root compositions total render duration;
- frame rate;
- output format, codec, or quality;
- a parent or sibling composition unless values are passed to it explicitly.
| Source | Precedence | Where declared | Those choices are read from source or render settings before composition logic
|--------|-----------|---------------| runs.
| Declared defaults | Lowest | `data-composition-variables` on the declaration root (`<html>`, or the composition root div for template/fragment comps) |
| Per-instance host overrides | Middle | `data-variable-values` on the sub-comp host element |
| CLI `--variables` flag | Highest | `hyperframes render --variables '{...}'` |
A missing key at any layer falls through to the next lower layer. If no layer provides a value, the declared `default` is used. ## Check the contract
## Validation Run:
The linter checks variable declarations statically: ```bash
```bash Terminal
npx hyperframes lint npx hyperframes lint
``` ```
It catches malformed JSON, missing required fields (`id`, `type`, `label`, `default`), and type mismatches between `type` and the `default` value. Fix lint errors before rendering — they indicate the runtime will be unable to resolve variables correctly. The linter catches malformed declarations, missing fields, wrong default types,
and invalid enum choices. `--strict-variables` turns undeclared or mistyped
render values into errors.
At render time, the CLI validates `--variables` against the schema and reports issues as warnings (or errors with `--strict-variables`): Continue to [Compositions](/concepts/compositions) for nesting or the
[HTML schema](/reference/html-schema) for the complete attribute contract.
- **undeclared** — a key in `--variables` has no matching `id` in `data-composition-variables`
- **type-mismatch** — the value's JavaScript type does not match the declared `type` (e.g. a string where a number is expected)
- **enum-out-of-range** — an enum value is not in the declared `options` list
## Inspecting Variables Programmatically
If you are building tooling on top of `@hyperframes/core`, the variable declarations are readable without rendering:
```typescript
import { extractCompositionMetadata } from "@hyperframes/core";
import { readFileSync } from "node:fs";
const html = readFileSync("compositions/card.html", "utf8");
const { variables } = extractCompositionMetadata(html);
// variables is CompositionVariable[]
```
This is the same API the Studio Variables panel uses to build its editor for each composition.
## Variables in Studio
The Studio's **Variables** tab (right inspector panel) is a full UI over this
system:
- **Declare and edit** — add, edit, and remove declarations without touching the
HTML by hand; edits persist into `data-composition-variables` with undo support.
- **Preview with values** — type-appropriate inputs write ephemeral overrides that
are injected into the preview as `window.__hfVariables`, exactly like render-time
injection, so what you preview is what `--variables` renders. A header pill shows
whether you're previewing defaults or custom values.
- **Render with values** — renders started from the Renders tab carry the active
preview overrides.
- **Handoff** — copy the effective values as JSON or as a ready-to-run
`hyperframes render --variables` command.
- **Usage** — declarations no script reads are badged `unused`; ids read by scripts
but missing from the schema get a one-click Declare action.
## Next Steps
<CardGroup cols={2}>
<Card title="Data Attributes" icon="code" href="/concepts/data-attributes">
Full reference for data-composition-variables and data-variable-values attributes
</Card>
<Card title="Compositions" icon="layer-group" href="/concepts/compositions">
How nested compositions use variables for reuse
</Card>
<Card title="Rendering" icon="play" href="/guides/rendering">
CLI flags for passing variables at render time
</Card>
<Card title="CLI Reference" icon="terminal" href="/packages/cli">
All CLI commands and flags
</Card>
</CardGroup>
+22 -8
View File
@@ -47,6 +47,7 @@ The Lambda handler is a thin dispatch: parse the Step Functions event, download
| AWS credentials | The CLI and the deploy step both call AWS APIs. | Env vars, `~/.aws/credentials`, SSO, or IMDS — any chain the AWS SDK for JavaScript v3 would resolve. | | AWS credentials | The CLI and the deploy step both call AWS APIs. | Env vars, `~/.aws/credentials`, SSO, or IMDS — any chain the AWS SDK for JavaScript v3 would resolve. |
| AWS SAM CLI | `hyperframes lambda deploy/destroy` shells out to `sam deploy`/`sam delete`. | [Install guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) | | AWS SAM CLI | `hyperframes lambda deploy/destroy` shells out to `sam deploy`/`sam delete`. | [Install guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) |
| `bun` | Used to build `packages/aws-lambda/dist/handler.zip` at deploy time. | `npm install -g bun` or [bun.sh](https://bun.sh) | | `bun` | Used to build `packages/aws-lambda/dist/handler.zip` at deploy time. | `npm install -g bun` or [bun.sh](https://bun.sh) |
| Lambda adapter | The published CLI loads the AWS adapter only for Lambda commands. | `npm install -g @hyperframes/aws-lambda` alongside the CLI |
| HyperFrames repo checkout | `lambda deploy` builds the Lambda handler ZIP from source. Adopters who deploy outside a checkout can set `HYPERFRAMES_REPO_ROOT` to point at one. | `git clone https://github.com/heygen-com/hyperframes` | | HyperFrames repo checkout | `lambda deploy` builds the Lambda handler ZIP from source. Adopters who deploy outside a checkout can set `HYPERFRAMES_REPO_ROOT` to point at one. | `git clone https://github.com/heygen-com/hyperframes` |
## Three deployment paths ## Three deployment paths
@@ -63,7 +64,10 @@ hyperframes lambda deploy \
--memory=10240 --memory=10240
``` ```
The default `--concurrency=8` is deliberately conservative for first-time users. The Lambda Map state's default would let an unbounded number of chunks fan out in parallel; 8 caps your worst-case spend on a runaway render at roughly `8 × (15 min × 10 GB × $0.0000167/GB-s) ≈ $1.20`. Raise it after you've sized your typical render's chunk count. The default `--concurrency=8` is deliberately conservative for first-time
users. It limits how many Lambda workers the deployed stack can run at once.
Raise it only after you have measured a typical render and checked the account's
limits and budget.
After `deploy`, render anything with: After `deploy`, render anything with:
@@ -150,7 +154,7 @@ The CLI ships a built-in IAM bootstrap to avoid the "User is not authorized to p
hyperframes lambda policies user hyperframes lambda policies user
# Print { TrustRelationship, InlinePolicy } for a CloudFormation service role. # Print { TrustRelationship, InlinePolicy } for a CloudFormation service role.
hyperframes lambda policies role --principal=cloudformation hyperframes lambda policies role
# Validate a checked-in policy still covers the CLI's needs (exit non-zero on missing). # Validate a checked-in policy still covers the CLI's needs (exit non-zero on missing).
hyperframes lambda policies validate ./infra/iam/hyperframes-deploy.json hyperframes lambda policies validate ./infra/iam/hyperframes-deploy.json
@@ -172,7 +176,8 @@ hyperframes lambda progress my-render-id
# Output: s3://hyperframes-renders/.../output.mp4 # Output: s3://hyperframes-renders/.../output.mp4
``` ```
The cost number is best-effort: Lambda billed duration comes from the handler's own `DurationMs` return value (which SFN history surfaces in the success payload) and S3 transfer is not included. The math is in `packages/aws-lambda/src/sdk/costAccounting.ts` if you want to verify; CLI-shown values match what AWS Billing reports within rounding noise. The cost number is an estimate: Lambda duration comes from the handler result,
and S3 transfer is not included. Use AWS billing data for actual spend.
## Troubleshooting ## Troubleshooting
@@ -209,9 +214,18 @@ If progress doesn't advance for >10 minutes, check the Step Functions execution
The render bucket is created with CloudFormation `Retain` on delete — `hyperframes lambda destroy` (or `sam delete`) tears the function + state machine down but the bucket survives. This is intentional: it protects final-rendered MP4s from being lost when you re-deploy. To fully reclaim storage, empty + delete the bucket via the AWS console / `aws s3 rb`. The render bucket is created with CloudFormation `Retain` on delete — `hyperframes lambda destroy` (or `sam delete`) tears the function + state machine down but the bucket survives. This is intentional: it protects final-rendered MP4s from being lost when you re-deploy. To fully reclaim storage, empty + delete the bucket via the AWS console / `aws s3 rb`.
## What's NOT in the v1 surface ## Current limits
- **Webhooks on completion.** Not in v1 — poll with `hyperframes lambda progress` or watch the Step Functions execution. A `--webhook` flag with an SNS topic is on the Phase 6c backlog. - There is no completion webhook. Poll with `hyperframes lambda progress` or
- **`compositions` discovery verb.** Coming separately (PR 6.10 on the plan); for now, point `lambda render` at the project directory containing your `index.html`. watch the Step Functions execution.
- **Multi-region.** Each `--region` is an independent stack. There is no built-in cross-region failover. - There is no Lambda-specific composition-discovery verb. Point
- **HDR.** Distributed mode is SDR-only. HDR mp4 with bsf signaling is on the v1.5 backlog. `lambda render` at the project directory containing `index.html`.
- Each `--region` is an independent stack. Cross-region failover is not built
in.
- The distributed AWS path is SDR-only.
## Related topics
- [Render variable-driven templates on Lambda](/deploy/templates-on-lambda)
- [Use the AWS Lambda package](/packages/aws-lambda)
- [Choose another rendering path](/deploy/overview)
+79 -187
View File
@@ -1,232 +1,124 @@
--- ---
title: Cloud Rendering title: Cloud rendering
description: "Render a composition on HeyGen's hosted cloud — no local Chrome, no FFmpeg, no AWS to manage." description: Render on HeyGen's managed cloud without deploying your own infrastructure.
--- ---
Render any HyperFrames composition on HeyGen's managed cloud: the CLI zips your project, uploads it, runs the render on HeyGen's infrastructure, and downloads the finished video. There's nothing to deploy and no Chrome or FFmpeg to install — you pay per credit. Use managed cloud rendering when you want a finished file without installing Chrome or FFmpeg or maintaining AWS or Google Cloud infrastructure.
## Render a project
Sign in once, then render the current project:
```bash ```bash
hyperframes auth login # one-time sign-in hyperframes auth login
hyperframes cloud render # zip → upload → render → download hyperframes cloud render
``` ```
The command packages the project, uploads it, waits for the render, and downloads the finished file to `renders/`.
Choose a composition or output path when the defaults are not right:
```bash ```bash
# ◆ Zipping my-video
# 42 files · 3.1 MB
# ◆ Uploading to /v3/assets
# asset_id: asst_abc123 · 1.2s
# Polling hfr_def456 every 10s …
# completed 47s
# ◆ Downloading to renders/hfr_def456.mp4
# 8.4 MB written
```
This is the zero-infra alternative to running your own renderer. If you'd rather own the compute, see [AWS Lambda](/deploy/aws-lambda), [GCP Cloud Run](/deploy/gcp-cloud-run), or the [Vercel, Cloudflare, or Modal templates](/guides/deploy). For local iteration during authoring, use [`hyperframes render`](/guides/rendering).
## Authenticate
Cloud rendering needs a HeyGen credential. Sign in once — the CLI stores it in `~/.heygen/credentials` (mode `0600`), and the same credential drives every `cloud` subcommand.
<Steps>
<Step title="Sign in">
The default flow opens your browser for OAuth 2.0 + PKCE and captures the token on a loopback port:
```bash
hyperframes auth login
# ✓ Signed in as you@example.com.
```
For CI or headless machines, use a long-lived API key instead:
```bash
# Interactive hidden-input prompt
hyperframes auth login --api-key
# Or pipe a key from stdin
echo "$HEYGEN_API_KEY" | hyperframes auth login --api-key
```
</Step>
<Step title="Confirm you're signed in">
```bash
hyperframes auth status
# Shows the active credential's source, identity, and billing snapshot.
```
</Step>
</Steps>
The credential is **shared with the [`heygen` CLI](https://github.com/heygen-com/heygen-cli)** — sign in with one and the other picks up the session. Credentials resolve in this order (first match wins):
1. `HEYGEN_API_KEY` environment variable
2. `HYPERFRAMES_API_KEY` environment variable (hyperframes alias)
3. `~/.heygen/credentials`
<Note>
Point the CLI at a different backend with `HEYGEN_API_URL` (default `https://api.heygen.com`). Use `hyperframes auth refresh` to force-refresh an OAuth token before a long job; `hyperframes auth logout` clears the stored credential. For the keys voice, music, and capture use across the skills — and the fully local fallback — see [Authentication & API keys](/guides/authentication).
</Note>
## How a cloud render flows
`hyperframes cloud render` runs the whole pipeline end-to-end:
```
Your machine HeyGen cloud
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ zip project │ ──PUT───▶│ direct-to-S3 asset upload │
│ (.hyperframesignore + │ upload │ → asset_id │
│ generated outputs) │ │ │
│ │ ──POST──▶│ /v3/hyperframes/renders │
│ │ submit │ → render_id (queued) │
│ │ │ Chromium + FFmpeg render │
│ poll GET /renders/{id} │ ◀────────│ queued → rendering → completed │
│ stream video to disk │ ◀────────│ signed video_url │
└─────────────────────────┘ └─────────────────────────────────┘
```
1. **Resolve the project** — a local directory (default `.`), or skip the upload with `--asset-id` / `--url`.
2. **Auto-detect the aspect ratio** from the entry HTML's `data-width`/`data-height` so you rarely set it by hand.
3. **Zip** the project (same ignore set as `hyperframes publish`, including `.hyperframesignore`).
4. **Upload** the zip through the direct-to-S3 asset flow, yielding an `asset_id`.
5. **Submit** the render to `POST /v3/hyperframes/renders`.
6. **Poll** `GET /v3/hyperframes/renders/{id}` until it completes or fails (skip with `--no-wait`).
7. **Download** the signed video URL to disk.
## Control archive size
Hosted cloud project uploads are limited to 200 MB. HyperFrames automatically excludes root-level `renders/` and `snapshots/` plus development-only paths such as `.git`, `node_modules`, `dist`, `.next`, `coverage`, and dotfiles.
Use a gitignore-style `.hyperframesignore` at the project root for additional generated or intermediate files that are not needed at render time:
```gitignore
/snapshots2/
/exports/
/assets/source-master.mp4
```
Inspect the exact archive without authenticating, uploading, or starting a render:
```bash
hyperframes cloud render . --dry-run
hyperframes cloud render . --dry-run --json
```
The dry run reports compressed size, file count, and the ten largest included files. Rules also apply to `hyperframes publish`. Avoid broad patterns such as `assets/`: dynamically selected media may not appear as an obvious static HTML reference.
## Render options
The most-used flags — see the [CLI reference](/packages/cli#hyperframes-cloud) for the full list.
| Flag | Default | Meaning |
| --- | --- | --- |
| `--fps` | `30` | Frames per second, 1240. |
| `--quality` | `standard` | `draft`, `standard`, or `high`. |
| `--format` | `mp4` | `mp4`, `webm`, or `mov` (webm/mov carry alpha). |
| `--resolution` | `1080p` | `1080p` or `4k`. 4k is billed at 1.5×. |
| `--aspect-ratio` | auto | `16:9`, `9:16`, or `1:1`. Auto-detected from a local project's `data-width`/`data-height`; for `--asset-id`/`--url` it defaults to `16:9` unless set. |
| `--composition` / `-c` | `index.html` | Entry HTML file inside the zip. |
| `--output` / `-o` | `renders/<render_id>.<ext>` | Local destination for the download. |
| `--dry-run` | off | Build and inspect a local project zip without authenticating, uploading, or rendering. |
```bash
# Pick a composition and an output path.
hyperframes cloud render . \ hyperframes cloud render . \
--composition compositions/intro.html \ --composition compositions/intro.html \
--output ./renders/intro.mp4 --output renders/intro.mp4
```
# Higher quality at 60fps. For CI or another headless environment, save a long-lived API key instead:
```bash
echo "$HEYGEN_API_KEY" | hyperframes auth login --api-key
```
See [Authentication and API keys](/guides/authentication) for credential precedence and local alternatives.
## Choose the output
| Option | Values | Default |
| --- | --- | --- |
| `--fps` | 1240 | `30` |
| `--quality` | `draft`, `standard`, `high` | `standard` |
| `--format` | `mp4`, `webm`, `mov` | `mp4` |
| `--resolution` | `1080p`, `4k` | `1080p` |
| `--aspect-ratio` | `16:9`, `9:16`, `1:1` | detected from a local composition when possible |
```bash
hyperframes cloud render --quality high --fps 60 hyperframes cloud render --quality high --fps 60
hyperframes cloud render --resolution 4k
``` ```
<Warning> <Warning>
`--resolution 4k` can't be combined with `--format webm` or `--format mov`. The 4k supersampling path runs through the screenshot capture pipeline, which has no alpha channel. Render 4k as `mp4`, or render alpha at the composition's native resolution. 4K is billed at 1.5× and supports MP4 only. WebM and MOV use the alpha-capable path, which does not support 4K supersampling.
</Warning> </Warning>
## Templates and variables ## Check what will upload
Cloud rendering supports [variables](/concepts/variables) — the same mechanism that powers templates everywhere else in HyperFrames. Declare `data-composition-variables` on your composition, then fill them at render time: Cloud project archives have a 200 MB limit. Inspect the archive before starting a render:
```bash ```bash
# Inline JSON hyperframes cloud render --dry-run
hyperframes cloud render --variables '{"title":"Q4 Recap","theme":"dark"}'
# From a file
hyperframes cloud render --variables-file ./vars.json
# Fail fast on undeclared keys or wrong types
hyperframes cloud render --variables '{"title":"Q4 Recap"}' --strict-variables
``` ```
For a **local project**, the CLI validates your `--variables` against the composition's declared schema *before* uploading. For `--asset-id` / `--url` the schema lives server-side, so mismatches surface as a `hyperframes_project_invalid` API error. Add generated or unnecessary files to `.hyperframesignore` when needed:
The idiomatic template workflow is **upload once, re-render many**: render a local project to get its `asset_id`, then submit new renders against that same asset with different variables — no re-zip, no re-upload. ```gitignore
/exports/
/source-masters/
```
Do not exclude media that the composition needs at render time. HyperFrames already omits common development and generated paths, including `.git`, `node_modules`, root-level `renders/`, and root-level `snapshots/`.
## Fill template variables
Use the variables declared by the composition:
```bash ```bash
# 1. Upload + render once; note the asset_id printed during upload. hyperframes cloud render \
hyperframes cloud render ./card-template --variables '{"title":"Q4 recap","theme":"dark"}' \
--strict-variables
# 2. Re-render the same asset with new values (skips zip + upload).
hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Ada"}'
hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Linus"}'
``` ```
For high-volume personalized batches, the bring-your-own-AWS path adds a JSONL fan-out — see [Templates on Lambda](/deploy/templates-on-lambda). For larger payloads, pass a JSON file with `--variables-file`.
## Fire-and-forget and webhooks The first `hyperframes cloud render` that uploads a local project prints an `asset_id`. Reuse it to
render the same uploaded project with different values:
By default the CLI blocks, polls, and downloads. Pass `--no-wait` to submit and exit with just the `render_id`, and `--callback-url` to get an HTTPS webhook when the render terminates. The webhook fires whether or not the CLI is still polling, so combine them for true fire-and-forget:
```bash ```bash
hyperframes cloud render --callback-url https://example.com/hf-hook --no-wait hyperframes cloud render \
# ✓ Submitted hfr_def456 --asset-id asst_abc123 \
# Poll with: hyperframes cloud get hfr_def456 --variables '{"title":"Customer update"}'
``` ```
| Flag | Meaning | ## Run asynchronously
| --- | --- |
| `--no-wait` | Submit and exit immediately; print the `render_id`. |
| `--callback-url` | HTTPS webhook fired when the render terminates. |
| `--callback-id` | Opaque tracking ID echoed in webhook payloads. |
| `--poll-interval` | Poll cadence in seconds (default `10`). |
| `--max-wait` | Max poll duration in minutes (default `60`). |
## Managing renders Submit without waiting and receive a webhook when the render finishes:
```bash ```bash
hyperframes cloud list # recent renders (--limit, --token, --all) hyperframes cloud render \
hyperframes cloud get hfr_def456 # full detail + short-lived signed video_url --callback-url https://example.com/hyperframes-hook \
hyperframes cloud delete hfr_def456 # soft-delete (--no-confirm to skip the prompt) --no-wait
``` ```
`video_url` and `thumbnail_url` are short-lived presigned URLs — re-fetch with `cloud get` rather than caching them. Then inspect or manage renders by ID:
## Safe retries
The CLI transparently retries on a `401 Unauthorized` by force-refreshing the OAuth token and replaying the request. That's harmless for reads, but the zip upload (`POST /v3/assets`) is **not** idempotent on its own — a blind retry would create a duplicate asset and bill the workspace twice. Pass `--idempotency-key` so retries are safe:
```bash ```bash
hyperframes cloud render . --idempotency-key "$(uuidgen)" hyperframes cloud list
hyperframes cloud get hfr_abc123
hyperframes cloud delete hfr_abc123
``` ```
The key is forwarded to both the upload and submit calls; the server scopes idempotency per-endpoint, so reusing one value across both steps is safe. Use any opaque string in `[A-Za-z0-9_:.-]` (1255 chars). For automated retries, pass a stable `--idempotency-key` so an interrupted request can be replayed safely.
## Cloud vs. Lambda vs. local ## Choose another renderer
- **`hyperframes render`** (local) — fastest iteration loop; use while authoring. See [Rendering](/guides/rendering). - Use [`hyperframes render`](/guides/rendering) while authoring or when the machine running the command should do the work.
- **`hyperframes cloud render`** — zero-infra; HeyGen runs the render and you pay per credit. Use when you don't want to manage Chrome/FFmpeg/AWS. - Use [AWS Lambda](/deploy/aws-lambda) or [Google Cloud Run](/deploy/gcp-cloud-run) when you need to own the rendering infrastructure.
- **`hyperframes lambda render`** — bring-your-own-AWS distributed rendering with chunked parallelism. Use when you've already invested in AWS. See [AWS Lambda](/deploy/aws-lambda). - Use a [deployment template](/guides/deploy) when you need a working hosted starting point rather than the full distributed-rendering stack.
## Next steps See the [CLI reference](/packages/cli#hyperframes-cloud) for every flag and JSON output shape.
<CardGroup cols={2}> ## Related topics
<Card title="Variables" icon="sliders" href="/concepts/variables">
Declare and fill template slots in a composition - [Compare every rendering path](/deploy/overview)
</Card> - [Render locally from the CLI](/guides/rendering)
<Card title="Templates on Lambda" icon="layer-group" href="/deploy/templates-on-lambda"> - [Operate rendering in your own cloud account](/deploy/aws-lambda)
High-volume personalized renders on your own AWS
</Card>
<Card title="Local rendering" icon="film" href="/guides/rendering">
Render locally or in Docker during authoring
</Card>
<Card title="CLI reference" icon="terminal" href="/packages/cli#hyperframes-cloud">
Every `cloud` and `auth` flag in detail
</Card>
</CardGroup>
+100 -76
View File
@@ -1,99 +1,123 @@
--- ---
title: Google Cloud Run title: "Google Cloud Run"
description: "Deploy distributed HyperFrames rendering to Google Cloud Run + Cloud Workflows, and drive renders from a laptop or CI." description: "Deploy distributed HyperFrames rendering to Cloud Run, Cloud Workflows, and Google Cloud Storage."
--- ---
HyperFrames ships a Google Cloud deployment that mirrors the [AWS Lambda](/deploy/aws-lambda) one: a single Cloud Run service fronts a Cloud Workflows definition that fans renders out across many parallel chunk workers, with intermediate artifacts in Google Cloud Storage. The render primitives are identical — only the storage, compute, and orchestration adapters differ. Use this path when renders must run in your Google Cloud account. A Cloud
Workflow plans the render, sends chunks to Cloud Run in parallel, and assembles
the result in Google Cloud Storage.
It's the right choice for teams already running their backend and storage on Google Cloud who want distributed HyperFrames rendering without adding AWS infrastructure. ```text
Cloud Workflow: Plan → Render chunks in parallel → Assemble
## Architecture
Cloud Run service
```
┌──────────────────────────────────────────────────────────────────┐ GCS
│ Cloud Workflows definition │
│ Plan → parallel(for chunk) RenderChunk → Assemble │
└──────────────────────────────────────────────────────────────────┘
│ OIDC-authenticated http.post per step
┌──────────────────────────────────────────────────────────────────┐
│ One Cloud Run service (packages/gcp-cloud-run/Dockerfile) │
│ dist/server.js │
│ ├─ Action="plan" → @hyperframes/producer/distributed │
│ ├─ Action="renderChunk" → @hyperframes/producer/distributed │
│ └─ Action="assemble" → @hyperframes/producer/distributed │
└──────────────────────────────────────────────────────────────────┘
│ GCS download / upload
Google Cloud Storage bucket
``` ```
Each workflow step `POST`s to the same Cloud Run URL with a different `Action`. The handler downloads its inputs from GCS into the container's filesystem, runs the matching OSS primitive, uploads the output back to GCS, and returns a small JSON result. The workflow accumulates every step's result and returns `{ Plan, Chunks, Assemble }`. For a hosted render without infrastructure ownership, use
[HyperFrames Cloud](/deploy/cloud). For a small preview application and render
endpoint, use a [hosted template](/guides/deploy).
## Why Cloud Run is simpler than Lambda here ## Prerequisites
Cloud Run runs a container image, so the Chrome story collapses to a `Dockerfile` line. There's no 250 MB ZIP ceiling, no `@sparticuz/chromium` runtime decompression, and no packaging probe — the image installs the same pinned `chrome-headless-shell` build the production renderer uses. Cloud Run gen2 also gives more headroom than Lambda: up to a 60-minute request timeout and 32 GiB of memory. - A Google Cloud project with billing enabled
- `gcloud` authenticated for that project
- Terraform 1.5 or newer
- Cloud Build access, or an existing compatible container image
- `@hyperframes/gcp-cloud-run` installed alongside the CLI
The CLI can build the renderer automatically when it runs from a HyperFrames
repository checkout. Outside the repository, pass an image built from
`packages/gcp-cloud-run/Dockerfile` with `--image`.
## Deploy ## Deploy
The Terraform module at `packages/gcp-cloud-run/terraform` provisions the GCS bucket, the Cloud Run service, the Cloud Workflows definition, two least-privilege service accounts, and a runaway-request alert.
```bash ```bash
# 1. Build + push the render image. hyperframes cloudrun deploy --project my-gcp-project
gcloud builds submit . \
--tag us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
# 2. Apply the module.
cd packages/gcp-cloud-run/terraform
terraform init
terraform apply \
-var project_id=PROJECT \
-var region=us-central1 \
-var image=us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
``` ```
Terraform outputs `render_bucket_name`, `service_url`, `workflow_name`, and `region`. Pass those into the SDK. The command enables the required Google Cloud APIs, builds and pushes the
renderer when needed, applies the bundled Terraform module, and stores the
resulting bucket, service URL, and workflow name in `~/.hyperframes/`.
<Note> The default region is `us-central1`. Machine and scaling controls include
The target GCP project must have **billing enabled** — Cloud Run, Cloud Workflows, Artifact Registry, and Cloud Build are all billed services. `--region`, `--cpu`, `--memory`, `--max-instances`, and `--timeout`.
</Note>
## Render ## Render
```ts Width and height describe the authored canvas. Use `--output-resolution` when
import { the encoded result should be supersampled without changing the layout.
renderToCloudRun,
getRenderProgress,
} from "@hyperframes/gcp-cloud-run/sdk";
const handle = await renderToCloudRun({
projectDir: "./my-composition",
config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
bucketName: "hyperframes-render-my-project",
projectId: "my-project",
location: "us-central1",
workflowId: "hyperframes-render",
serviceUrl: "https://hyperframes-render-abc.us-central1.run.app",
});
let progress = await getRenderProgress({ executionName: handle.executionName });
while (progress.status === "running") {
await new Promise((r) => setTimeout(r, 5000));
progress = await getRenderProgress({ executionName: handle.executionName });
}
console.log(progress.status, progress.outputFile, progress.costs.displayCost);
```
Templates with [variables](/concepts/variables) work the same way — declare `data-composition-variables` on the composition and pass `config.variables`. The Cloud Workflows execution argument is capped at 512 KiB, so pass media as URL references the composition resolves at render time rather than inlining base64.
## End-to-end smoke
`examples/gcp-cloud-run/scripts/smoke.sh` builds the image, applies the Terraform module, renders a fixture composition through the workflow at one or more chunk sizes, PSNR-compares each output against the in-process baseline, and tears the stack down.
```bash ```bash
examples/gcp-cloud-run/scripts/smoke.sh --project my-project --region us-central1 hyperframes cloudrun render ./my-project \
--width 1920 \
--height 1080 \
--wait
``` ```
## Supported formats The command accepts the same template variables used by local and AWS renders:
Same as the distributed pipeline everywhere: `mp4` (H.264 / H.265), `webm` (VP9), `mov` (ProRes), and `png-sequence`. HDR mp4 is not supported in distributed mode. ```bash
hyperframes cloudrun render ./card-template \
--width 1920 \
--height 1080 \
--variables '{"name":"Ada"}' \
--wait
```
Supported distributed outputs are MP4, MOV, WebM, and PNG sequences. MP4 can
use H.264 or H.265. Distributed rendering is currently SDR-only.
## Reuse an upload
Projects are content-addressed. Upload an unchanged project once, then reuse
its site ID for later renders:
```bash
hyperframes cloudrun sites create ./my-project
```
Use `render-batch` with a JSONL file when one template needs many sets of
variables:
```bash
hyperframes cloudrun render-batch ./card-template \
--batch ./recipients.jsonl \
--width 1920 \
--height 1080 \
--max-concurrent 10
```
Each JSONL row contains an output key and optional variables:
```json
{"outputKey":"renders/ada.mp4","variables":{"name":"Ada"}}
```
## Progress and teardown
Without `--wait`, a render returns its workflow execution name immediately.
```bash
hyperframes cloudrun progress <execution-name>
hyperframes cloudrun destroy --project my-gcp-project
```
`destroy` removes the Terraform-managed stack and its render bucket. Download
anything that must be retained before running it.
## Programmatic use
`@hyperframes/gcp-cloud-run/sdk` exposes `deploySite`, `renderToCloudRun`, and
`getRenderProgress` for Node backends. The package also exports the Terraform
module and HTTP handler used by the deployed service.
See the [GCP package reference](/packages/gcp-cloud-run) for the SDK contract
and the complete infrastructure shape.
## Related topics
- [Use the Google Cloud Run package](/packages/gcp-cloud-run)
- [Choose another rendering path](/deploy/overview)
- [Compare with AWS Lambda](/deploy/aws-lambda)
+36 -11
View File
@@ -8,7 +8,7 @@ If you're already running a different framework that deploys a serverless video
## Concept mapping ## Concept mapping
| In your current framework you call... | In HyperFrames you call... | Notes | | In your current framework you call... | In HyperFrames you call... | Notes |
|--------------------------------------|----------------------------|-------| | ------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| One-shot deploy command | `hyperframes lambda deploy` | Builds `packages/aws-lambda/dist/handler.zip` and runs `sam deploy`. Idempotent. | | One-shot deploy command | `hyperframes lambda deploy` | Builds `packages/aws-lambda/dist/handler.zip` and runs `sam deploy`. Idempotent. |
| One-shot site upload | `hyperframes lambda sites create ./project` | Content-addressed S3 key — re-uploads of an unchanged tree are skipped via a HeadObject 200. | | One-shot site upload | `hyperframes lambda sites create ./project` | Content-addressed S3 key — re-uploads of an unchanged tree are skipped via a HeadObject 200. |
| Trigger a render | `hyperframes lambda render ./project --width 1920 --height 1080` | Returns immediately with a `renderId`; add `--wait` to stream per-chunk progress. | | Trigger a render | `hyperframes lambda render ./project --width 1920 --height 1080` | Returns immediately with a `renderId`; add `--wait` to stream per-chunk progress. |
@@ -20,13 +20,26 @@ If you're already running a different framework that deploys a serverless video
If your current framework is **React-based**, you write JSX components, register them in a `Composition`, and the renderer compiles them at render time. If your current framework is **React-based**, you write JSX components, register them in a `Composition`, and the renderer compiles them at render time.
In HyperFrames, **compositions are plain HTML files**. The `data-duration`, `data-width`, `data-height`, and `data-fps` attributes on the root element drive every render parameter. There is no JSX compilation step — what you write is what the browser renders. In HyperFrames, **compositions are plain HTML files**. A composition element
declares its ID and canvas, while clips declare their own timing. There is no
JSX compilation step.
```html ```html
<!doctype html> <!doctype html>
<html data-duration="10" data-width="1920" data-height="1080" data-fps="30"> <html lang="en">
<body> <body>
<h1 style="animation: fade-in 1s">Hello</h1> <div
id="stage"
data-composition-id="intro"
data-start="0"
data-width="1920"
data-height="1080"
data-duration="10"
data-fps="30"
data-no-timeline
>
<h1 id="title" class="clip" data-start="0" data-duration="10" data-track-index="0">Hello</h1>
</div>
</body> </body>
</html> </html>
``` ```
@@ -38,7 +51,7 @@ For framework-agnostic animation, HyperFrames supports first-party adapters for
Most adopters' render config maps directly: Most adopters' render config maps directly:
| Concept | HyperFrames equivalent | Where it lives | | Concept | HyperFrames equivalent | Where it lives |
|---------|------------------------|----------------| | ------------------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fps` | `--fps=30` (CLI) or `config.fps` (SDK) | 24, 30, 60 only — non-integer NTSC rationals are an in-process-only feature. | | `fps` | `--fps=30` (CLI) or `config.fps` (SDK) | 24, 30, 60 only — non-integer NTSC rationals are an in-process-only feature. |
| `width` / `height` | `--width` / `--height` flags, or `config.width` / `config.height` | Even integers ≤ 7680 (yuv420p parity). | | `width` / `height` | `--width` / `--height` flags, or `config.width` / `config.height` | Even integers ≤ 7680 (yuv420p parity). |
| `codec: 'h264' / 'h265'` | `--codec=h264` or `--codec=h265` (mp4 only) | h265 uses libx265 with closed-GOP keyint params so chunked concat-copy round-trips losslessly. | | `codec: 'h264' / 'h265'` | `--codec=h264` or `--codec=h265` (mp4 only) | h265 uses libx265 with closed-GOP keyint params so chunked concat-copy round-trips losslessly. |
@@ -47,11 +60,11 @@ Most adopters' render config maps directly:
| Chunk size in frames | `--chunk-size=240` (default 240) | ~8s at 30 fps; sized to fit Lambda's 15-min cap with headroom. | | Chunk size in frames | `--chunk-size=240` (default 240) | ~8s at 30 fps; sized to fit Lambda's 15-min cap with headroom. |
| Max parallel chunks | `--max-parallel-chunks=16` (default 16) | Caps the Map state's fan-out. | | Max parallel chunks | `--max-parallel-chunks=16` (default 16) | Caps the Map state's fan-out. |
| Per-chunk frame ceiling | `--target-chunk-frames=N` (optional) | Caps frames per chunk so one chunk can't run past Lambda's 15-min cap on a long video: the planner adds chunks (up to `--max-parallel-chunks`) to keep each at or below `N`, and short videos still collapse to fewer chunks. A ceiling, not a fixed size; ignored when `--chunk-size` is set. | | Per-chunk frame ceiling | `--target-chunk-frames=N` (optional) | Caps frames per chunk so one chunk can't run past Lambda's 15-min cap on a long video: the planner adds chunks (up to `--max-parallel-chunks`) to keep each at or below `N`, and short videos still collapse to fewer chunks. A ceiling, not a fixed size; ignored when `--chunk-size` is set. |
| Bitrate / CRF | `--bitrate=10M` or `--crf=18` | Mutually exclusive. | | Bitrate / CRF | `config.bitrate` or `config.crf` in the SDK | Mutually exclusive; the current Lambda CLI does not expose these two fields. |
## Variables (inputProps) ## Variables (inputProps)
Render-time payloads — `inputProps` in some frameworks, `variables` in HyperFrames — are isomorphic. Declare the composition's variable shape on the root `<html>` element via `data-composition-variables`, then pass per-render values with `hyperframes render --variables '{...}'` locally or `hyperframes lambda render --variables` on the Lambda surface. The same 256 KiB execution-input cap and "URL your assets, don't inline base64" convention apply. Render-time payloads — `inputProps` in some frameworks, `variables` in HyperFrames — are isomorphic. Declare the composition's variable shape on its root `[data-composition-id]` element via `data-composition-variables`, then pass per-render values with `hyperframes render --variables '{...}'` locally or `hyperframes lambda render --variables` on the Lambda surface. The Lambda execution input is capped at 256 KiB, so reference large assets by URL instead of embedding base64 data.
The full mapping — `defaultProps` → declarations, `useCurrentFrame()` + `props.<x>` → `__hyperframes.getVariables().<x>`, `renderMediaOnLambda({ inputProps })` → `renderToLambda({ config: { variables } })` — lives in [Templates on Lambda](/deploy/templates-on-lambda#migrating-from-remotion-lambda-inputprops). The full mapping — `defaultProps` → declarations, `useCurrentFrame()` + `props.<x>` → `__hyperframes.getVariables().<x>`, `renderMediaOnLambda({ inputProps })` → `renderToLambda({ config: { variables } })` — lives in [Templates on Lambda](/deploy/templates-on-lambda#migrating-from-remotion-lambda-inputprops).
@@ -67,15 +80,21 @@ HyperFrames refuses `data-gpu-mode="hardware"` in distributed mode — hardware
`failClosedFontFetch` is default-on in distributed mode. A composition that references a `font-family` HyperFrames can't fetch will fail at plan time (`FONT_FETCH_FAILED`) rather than silently falling back to the OS default. If you currently lean on system-font fallbacks, list the fonts you need explicitly via `<link rel="stylesheet">` or `@fontsource/*` imports. `failClosedFontFetch` is default-on in distributed mode. A composition that references a `font-family` HyperFrames can't fetch will fail at plan time (`FONT_FETCH_FAILED`) rather than silently falling back to the OS default. If you currently lean on system-font fallbacks, list the fonts you need explicitly via `<link rel="stylesheet">` or `@fontsource/*` imports.
### No HDR (yet) ### HDR is not supported
`hdrMode: 'force-hdr'` is rejected at plan time. The v1.5 backlog covers HDR mp4 via `-bsf:v hevc_metadata` re-application; for now, HDR renders use the in-process renderer outside Lambda. `hdrMode: 'force-hdr'` is rejected at plan time. Use the in-process renderer
outside Lambda for HDR output.
### webm uses closed-GOP VP9 ### webm uses closed-GOP VP9
webm distributed renders go through libvpx-vp9 with `-g <chunkSize>`, `-keyint_min <chunkSize>`, `-auto-alt-ref 0`, and `-cpu-used 4` by default. The alt-ref disable is the load-bearing bit: libvpx-vp9's default non-displayable alt-ref frames can land anywhere in a GOP, which breaks concat-copy at chunk seams. Closed-GOP forces a keyframe at every chunk boundary so `ffmpeg -f concat -c copy` round-trips losslessly. Output is `yuva420p` to preserve alpha. Audio is muxed as Opus. webm distributed renders go through libvpx-vp9 with `-g <chunkSize>`, `-keyint_min <chunkSize>`, `-auto-alt-ref 0`, and `-cpu-used 4` by default. The alt-ref disable is the load-bearing bit: libvpx-vp9's default non-displayable alt-ref frames can land anywhere in a GOP, which breaks concat-copy at chunk seams. Closed-GOP forces a keyframe at every chunk boundary so `ffmpeg -f concat -c copy` round-trips losslessly. Output is `yuva420p` to preserve alpha. Audio is muxed as Opus.
Distributed webm files are typically ~10-25% larger than the same composition rendered in-process at the same CRF, because closed-GOP forces more keyframes than the in-process single-pass would emit. VP9 encode speed is controlled by `PRODUCER_VP9_CPU_USED` (`-8` to `8`); use lower values for quality-sensitive or long-form WebM, and higher values when wall-clock encode time matters more than compression efficiency. The single-machine in-process renderer remains the right choice for short webm renders; distributed pays for itself once a render's wall-clock exceeds what one machine delivers. Distributed WebM can be larger than the same composition rendered in one pass
because closed-GOP encoding forces more keyframes. VP9 encode speed is
controlled by `PRODUCER_VP9_CPU_USED` (`-8` to `8`); use lower values for
quality-sensitive or long-form WebM, and higher values when wall-clock encode
time matters more than compression efficiency. Benchmark local and distributed
rendering with the actual composition before choosing a path.
### State files are local by default ### State files are local by default
@@ -88,7 +107,7 @@ The default policy doc emitted by `hyperframes lambda policies user/role` uses `
## Migration checklist ## Migration checklist
1. **Inventory** the compositions you want to migrate. Filter out anything that needs HDR — that stays on your current framework for now. webm renders distributed via closed-GOP VP9 + concat-copy (see the webm section above). 1. **Inventory** the compositions you want to migrate. Filter out anything that needs HDR — that stays on your current framework for now. webm renders distributed via closed-GOP VP9 + concat-copy (see the webm section above).
2. **Translate** each composition to plain HTML. The `[Concepts](/concepts)` page covers the data-attribute conventions; installing the skills (`npx skills add heygen-com/hyperframes`) makes Claude / Cursor / Codex aware of them too — start at `/hyperframes`, which routes to `/hyperframes-core` for the composition contract. 2. **Translate** each composition to plain HTML. The `[Concepts](/concepts)` page covers the data-attribute conventions; installing the skills (`npx hyperframes skills update`) makes Claude / Cursor / Codex aware of them too — start at `/hyperframes`, which routes to `/hyperframes-core` for the composition contract.
3. **Wire** the new composition into your build pipeline alongside the old one. HyperFrames doesn't need an external bundler — you can `npx hyperframes preview` against the HTML directly. 3. **Wire** the new composition into your build pipeline alongside the old one. HyperFrames doesn't need an external bundler — you can `npx hyperframes preview` against the HTML directly.
4. **Deploy** in a separate AWS account or with a `--stack-name=hyperframes-staging` first. Run a real render with `--wait`; verify the output bytes. 4. **Deploy** in a separate AWS account or with a `--stack-name=hyperframes-staging` first. Run a real render with `--wait`; verify the output bytes.
5. **Add the policy** to your CI. `hyperframes lambda policies user > infra/iam/hyperframes.json` then `hyperframes lambda policies validate infra/iam/hyperframes.json` on every PR. 5. **Add the policy** to your CI. `hyperframes lambda policies user > infra/iam/hyperframes.json` then `hyperframes lambda policies validate infra/iam/hyperframes.json` on every PR.
@@ -105,3 +124,9 @@ If you don't want Lambda specifically, the same `@hyperframes/producer/distribut
- Plain Docker on a beefy VM - Plain Docker on a beefy VM
Build it yourself — we don't publish a Docker image to a registry. The Dockerfile is documented inline and bakes Node 22 + chrome-headless-shell + ffmpeg + the producer at the version your checkout is on. Build it yourself — we don't publish a Docker image to a registry. The Dockerfile is documented inline and bakes Node 22 + chrome-headless-shell + ffmpeg + the producer at the version your checkout is on.
## Related topics
- [Deploy HyperFrames on AWS Lambda](/deploy/aws-lambda)
- [Render templates on Lambda](/deploy/templates-on-lambda)
- [Use the lower-level Producer pipeline](/packages/producer)
+120 -257
View File
@@ -1,320 +1,183 @@
--- ---
title: Templates on Lambda title: "Render templates on Lambda"
description: "Render personalised template videos at scale on AWS Lambda using --variables and the lambda render-batch verb." description: "Render one HyperFrames composition with different variable values, individually or from a JSONL batch."
--- ---
HyperFrames templates are compositions that take typed variables — a name, a colour, a chart payload, a CTA URL — and produce a finished render parameterised by those values. Pair a template with the deployed Lambda stack and `lambda render-batch`, and you get personalised-video-at-scale in one CLI call: A HyperFrames template is a composition with declared variables. The same
project can produce many videos without rewriting its HTML.
```bash Use this guide after deploying the [AWS Lambda render stack](/deploy/aws-lambda).
hyperframes lambda render-batch ./my-template \
--batch ./users.jsonl \
--width 1920 --height 1080
```
This guide walks the full loop: declare variables on a composition, iterate locally with `hyperframes render`, deploy to Lambda once, then fan out N renders from a batch file. The same flow also drives single personalised renders via `lambda render --variables` and programmatic batches via `renderToLambda({ variables })`. ## Declare the inputs
```mermaid Declare variables on the document, then bind them in the composition. This
flowchart LR example exposes a headline and accent color:
A["Local iteration<br/>hyperframes render --variables"] --> B["Deploy stack<br/>hyperframes lambda deploy"]
B --> C["Upload site once<br/>hyperframes lambda sites create"]
C --> D["Fan out renders<br/>hyperframes lambda render-batch"]
D --> E["N personalised videos<br/>in S3"]
```
## What's a template
A template is just a HyperFrames composition whose top-level HTML element declares a `data-composition-variables` attribute listing the variables it accepts. The composition reads the runtime values via `window.__hyperframes.getVariables()`.
```html ```html
<!doctype html> <!doctype html>
<html <html
data-composition-variables='[ data-composition-variables='[
{"id":"title","type":"string","label":"Headline","default":"Welcome"}, {"id":"title","type":"string","label":"Headline","default":"Welcome"},
{"id":"accentColor","type":"string","label":"Accent","default":"#0a0a0a"}, {"id":"accent","type":"color","label":"Accent","default":"#6d5dfc"}
{"id":"avatarUrl","type":"string","label":"Avatar image","default":"/avatars/default.png"}
]' ]'
> >
<head><meta charset="utf-8"><title>Welcome template</title></head> <body>
<body style="margin:0;background:#f6f5f1"> <div
<div data-composition-id="root" data-width="1920" data-height="1080" data-duration="5"> id="stage"
<h1 id="title" style="font:80px Inter,sans-serif">Welcome</h1> data-composition-id="welcome"
<div id="accent" style="width:100%;height:8px"></div> data-start="0"
<img id="avatar" alt="" style="width:240px;height:240px;border-radius:50%" /> data-width="1920"
data-height="1080"
data-duration="5"
data-no-timeline
style="color:var(--accent)"
>
<h1 data-var-text="title">Welcome</h1>
</div> </div>
<script> </body>
(function () {
var v = window.__hyperframes.getVariables();
document.getElementById("title").textContent = v.title;
document.getElementById("accent").style.background = v.accentColor;
document.getElementById("avatar").src = v.avatarUrl;
})();
</script>
</body>
</html> </html>
``` ```
The runtime helper is exposed as a global — `window.__hyperframes.getVariables()` — not as a fetchable module. Use a plain `<script>` (not `<script type="module">`) so the runtime is already initialized when the script executes. See [Variables](/concepts/variables) for every type and binding method.
## Declaring variables ## Test locally
Each entry in the `data-composition-variables` array describes one variable. Supported shapes: Render one realistic payload before using cloud infrastructure:
| Field | Required | Example |
|-------|----------|---------|
| `id` | yes | `"title"` |
| `type` | yes | `"string"`, `"number"`, `"color"`, `"boolean"`, `"enum"` |
| `label` | recommended | `"Headline"` |
| `default` | recommended | `"Welcome"` |
See [Variables](/concepts/variables) for the per-type editor widgets and the `"enum"`-only `options` field.
`getVariables()` returns the merged result of declared defaults and any caller overrides, so a composition with sensible defaults renders unchanged in preview mode and in production. Render-time overrides come from `--variables '{...}'` on the CLI or the `variables` field on the SDK's `renderToLambda` call.
Variables are typed primitives; for structured data (a list of bullets, a nested record), serialise it on the caller side and parse it back inside the composition:
```html
<html data-composition-variables='[
{"id":"heroJson","type":"string","label":"Hero copy (JSON)","default":"{\"title\":\"Hi\"}"}
]'>
```
The runtime won't accept declaration `type: "object"` — the parser rejects anything outside the five canonical types and silently drops the declaration, so `--strict-variables` would then flag every key as undeclared.
## Local iteration loop
Fast iteration is the whole point of templates — you should not have to deploy to Lambda to see how a value looks. Use `hyperframes render` locally with `--variables` (or `--variables-file`) to render the template against any payload:
```bash ```bash
hyperframes render --variables '{"title":"Hello Alice","accentColor":"#ff0000"}' \ hyperframes render ./my-template \
--output renders/alice-preview.mp4 --variables '{"title":"Hello Ada","accent":"#ff4d67"}' \
--strict-variables \
--output renders/ada-preview.mp4
``` ```
Pass `--strict-variables` to fail on type mismatches against the `data-composition-variables` declaration. Without the flag, mismatches print as warnings and the render continues. `--strict-variables` rejects undeclared keys and values with the wrong type.
```bash ## Render one version on Lambda
hyperframes render --variables-file ./alice.json --strict-variables \
--output renders/alice-preview.mp4
```
## Deploying to Lambda
Templates render on the standard `hyperframes lambda` stack — there's no special template-only deployment. Run:
```bash
hyperframes lambda deploy
```
once per AWS account/region. The [aws-lambda deploy guide](/deploy/aws-lambda) covers the SAM stack, IAM policies, and the CloudFormation outputs.
When the same template will produce many renders, upload the project once with `lambda sites create` and reference its content-addressed `siteId` from every subsequent render or batch:
```bash
hyperframes lambda sites create ./my-template
# → Site ID: abc1234deadbeef0
```
## Single personalised render
For one-off renders, pass `--site-id` + per-render `--variables`. The CLI synthesises the minimal site handle from the `siteId` (no re-tarring) and invokes `renderToLambda`:
```bash ```bash
hyperframes lambda render ./my-template \ hyperframes lambda render ./my-template \
--site-id abc1234deadbeef0 \ --width 1920 \
--width 1920 --height 1080 \ --height 1080 \
--variables '{"title":"Hello Alice","accentColor":"#ff0000"}' \ --variables '{"title":"Hello Ada","accent":"#ff4d67"}' \
--output-key renders/alice.mp4 \ --output-key renders/ada.mp4 \
--wait --wait
``` ```
`--wait` streams progress lines until the render finishes; without it the CLI returns immediately and you poll via `hyperframes lambda progress <renderId>`. Without `--wait`, the command returns a render ID. Check it later with:
## Batch pipeline (the headline) ```bash
hyperframes lambda progress <render-id>
`lambda render-batch` is the headline ergonomic: one CLI call dispatches N personalised renders. Author a JSONL file with one entry per recipient:
```jsonl
{"outputKey": "renders/alice.mp4", "variables": {"title": "Hi Alice", "accentColor": "#ff0000"}}
{"outputKey": "renders/bob.mp4", "variables": {"title": "Hi Bob", "accentColor": "#00aa00"}}
{"outputKey": "renders/carol.mp4", "variables": {"title": "Hi Carol", "accentColor": "#0000ff"}}
{"outputKey": "renders/dave.mp4", "variables": {"title": "Hi Dave", "accentColor": "#ff00aa"}}
{"outputKey": "renders/erin.mp4", "variables": {"title": "Hi Erin", "accentColor": "#aa00ff"}}
``` ```
Run the batch: ## Reuse the project upload
`lambda render` can upload the project for each call. For repeated renders,
create a content-addressed site once:
```bash
hyperframes lambda sites create ./my-template
```
Pass the returned ID with later renders:
```bash
hyperframes lambda render ./my-template \
--site-id <site-id> \
--width 1920 \
--height 1080 \
--variables '{"title":"Hello Lin"}'
```
An unchanged project keeps the same site ID and skips another S3 upload.
## Render a batch
Create one JSON object per line. `outputKey` is required; `variables` and
`executionName` are optional.
```jsonl
{"outputKey":"renders/ada.mp4","variables":{"title":"Hello Ada","accent":"#ff4d67"}}
{"outputKey":"renders/lin.mp4","variables":{"title":"Hello Lin","accent":"#36b37e"}}
```
Start the batch:
```bash ```bash
hyperframes lambda render-batch ./my-template \ hyperframes lambda render-batch ./my-template \
--batch ./users.jsonl \ --batch ./recipients.jsonl \
--width 1920 --height 1080 \ --width 1920 \
--max-concurrent 5 --height 1080 \
--max-concurrent 10
``` ```
The verb deploys the site once (or skips with `--site-id`), then calls `renderToLambda` per row. Variables travel inline in each JSONL entry — `render-batch` does not accept `--variables-file` because per-entry payloads are the whole point. Concurrent Step Functions starts are capped at `--max-concurrent` (default 50) so a 10 000-entry batch doesn't try to spawn 10 000 executions simultaneously and trip the AWS account's concurrent-execution limit. The command uploads the project once and returns one manifest row per input
line. A failure to start one row does not discard the other started renders.
The manifest output gives one row per input line: Validate the file without starting AWS executions:
```
Batch dispatched: 5 started, 0 failed-to-start.
✓ line 1 renders/alice.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
✓ line 2 renders/bob.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
✓ line 3 renders/carol.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
✓ line 4 renders/dave.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
✓ line 5 renders/erin.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
```
Add `--json` for the machine-readable form your batch coordinator can pipe to `jq`:
```bash ```bash
hyperframes lambda render-batch ./my-template --batch ./users.jsonl \ hyperframes lambda render-batch ./my-template \
--width 1920 --height 1080 --json \ --batch ./recipients.jsonl \
| jq -r '.[] | select(.status == "started") | .executionArn' --width 1920 \
--height 1080 \
--strict-variables \
--dry-run \
--json
``` ```
Poll each `executionArn` (or `renderId`) with `lambda progress` to track completions: ## Keep variables small
```bash The complete Step Functions Standard execution input is limited to 256 KiB.
hyperframes lambda progress arn:aws:states:us-east-1:1234:execution:hf:hf-render-abcd HyperFrames checks this before starting the execution.
```
Use `--dry-run` to lint a batch file before paying for any executions. Every entry's status becomes `would-invoke`: Use variables for JSON data. Store images, audio, and video separately and
pass their URLs instead of base64 content.
```bash
hyperframes lambda render-batch ./my-template --batch ./users.jsonl \
--width 1920 --height 1080 --dry-run --json
```
## Programmatic via SDK
The same surface is available from TypeScript via `@hyperframes/aws-lambda/sdk`. Deploy the site once and parallel-render the batch:
```typescript
import { deploySite, renderToLambda } from "@hyperframes/aws-lambda/sdk";
const users = [
{ name: "Alice", accentColor: "#ff0000" },
{ name: "Bob", accentColor: "#00aa00" },
// … 1 000 more rows …
];
const siteHandle = await deploySite({
projectDir: "./my-template",
bucketName: process.env.HYPERFRAMES_BUCKET!,
});
const handles = await Promise.all(
users.map((user) =>
renderToLambda({
siteHandle,
bucketName: process.env.HYPERFRAMES_BUCKET!,
stateMachineArn: process.env.HYPERFRAMES_SFN_ARN!,
config: {
fps: 30,
width: 1920,
height: 1080,
format: "mp4",
variables: { title: `Hello ${user.name}`, accentColor: user.accentColor },
},
outputKey: `renders/${user.name.toLowerCase()}.mp4`,
}),
),
);
console.log(`Started ${handles.length} renders`);
```
`HYPERFRAMES_BUCKET` and `HYPERFRAMES_SFN_ARN` come from the deployed stack. `hyperframes lambda deploy` prints them as `RenderBucketName` and `RenderStateMachineArn`, and they're also available via `aws cloudformation describe-stacks --query "Stacks[0].Outputs"`. See the [aws-lambda deploy guide](/deploy/aws-lambda) for the full CloudFormation outputs table.
Wrap the `Promise.all` in a semaphore (or use the CLI's `runWithConcurrencyLimit` pattern) when the batch is large enough that an unbounded burst would trip your AWS Lambda concurrent-execution quota.
## Working with large variables
Variables travel inside the Step Functions Standard execution input, which AWS caps at **256 KiB for the entire input** (not just the variables — the cap is on the full serialised payload). Express workflows cap at 32 KiB; we use Standard for execution-history visibility, so 256 KiB applies.
The SDK validates the size client-side and rejects oversize inputs with a clear error before any AWS call:
```
[validateConfig] config: Step Functions execution input is 287422 bytes,
which exceeds the 262144-byte (256 KiB) limit for Standard workflows.
Variables are for typed data (strings, numbers, structured records);
media assets (images, audio, video) should be passed as URL references
the composition resolves at render time, not inlined as base64. See
https://hyperframes.heygen.com/deploy/templates-on-lambda#working-with-large-variables
for the URL-your-assets convention.
```
**The convention: variables are for typed data; media assets are URL references the composition resolves at render time.**
Right:
```json ```json
{ {
"title": "Hello Alice", "title": "Hello Ada",
"accentColor": "#ff0000", "avatarUrl": "https://cdn.example.com/avatars/ada.png"
"avatarUrl": "https://cdn.example.com/avatars/alice.png"
} }
``` ```
Wrong (will explode for any non-trivial image): ## Migrating from Remotion Lambda inputProps
```json HyperFrames variables fill the role that `inputProps` serves in a Remotion
{ Lambda render:
"title": "Hello Alice",
"avatarBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUh..."
}
```
In the composition, the script that wires variables into the DOM uses the URL directly — the Lambda chunk worker fetches the asset over the file server during capture, the same way it would on the local renderer: | Remotion pattern | HyperFrames equivalent |
| ----------------------- | ------------------------------------------------------------------ |
| `defaultProps` | A `default` value in `data-composition-variables` |
| `inputProps` | `--variables` or the SDK render config's `variables` |
| `props.title` | `data-var-text="title"`, a CSS variable, or `getVariables().title` |
| `renderMediaOnLambda()` | `renderToLambda()` |
```html Declare the allowed values in the composition, pass only the per-render data,
<script> and keep large media outside the execution payload. Use `--strict-variables`
(function () { while migrating so an old or misspelled input fails before the render begins.
var v = window.__hyperframes.getVariables();
document.getElementById("avatar").src = v.avatarUrl;
})();
</script>
```
The same constraint applies to Remotion's `inputProps` — if you're migrating from `@remotion/lambda`, your payloads should already be structured this way. ## Control concurrency
If your typed-data payload genuinely exceeds 256 KiB (e.g. a long structured record per render with no media), [file an issue](https://github.com/heygen-com/hyperframes/issues/new) — there's a clean path via S3-hosted variable files, but we want to see real demand before designing the API. Three settings control different layers:
## Cost and scale | Setting | Controls |
| -------------------------------------- | ------------------------------------------------------------------- |
| `lambda deploy --concurrency` | Maximum concurrent invocations for the deployed Lambda function |
| `lambda render --max-parallel-chunks` | Maximum chunk workers used by one render |
| `lambda render-batch --max-concurrent` | Maximum render executions started concurrently by the batch command |
Each personalised render is one Step Functions execution + N chunk Lambda invocations. At default settings (`chunkSize: 240`, `maxParallelChunks: 16`) a 5-second 30fps composition is 1 chunk; a 60-second composition is ~8 chunks. Start conservatively and measure a real composition before increasing them.
The cost knobs: ## Use the SDK
- **`--max-parallel-chunks`**: per render, default 16. Smaller compositions don't fan out beyond `ceil(totalFrames / chunkSize)`. Higher values pay more Lambda invocations but finish faster. For a backend service, `@hyperframes/aws-lambda/sdk` exposes `deploySite`,
- **`--target-chunk-frames`**: optional per-chunk frame ceiling. With the default count-based sizing, a long composition's chunks grow with its length (`maxParallelChunks` chunks of `ceil(totalFrames / maxParallelChunks)` frames each), so a long enough render produces chunks too big to finish inside Lambda's 15-min cap. Setting this caps frames per chunk — the planner uses `clamp(ceil(totalFrames / targetChunkFrames), 1, maxParallelChunks)` chunks, adding chunks on long videos to keep each under the bound while still collapsing short videos to fewer chunks. It's a ceiling, not a fixed size, and is ignored when `--chunk-size` is set. A render long enough to need more than `maxParallelChunks` chunks stays at the cap (chunks then exceed the target — raise `--max-parallel-chunks` or shorten the render). `renderToLambda`, and `getRenderProgress`. See the
- **Lambda reserved concurrency** (`lambda deploy --concurrency=<N>`): caps how many Lambda invocations the render function can run in parallel. Other workloads in the same AWS account share the same account-level concurrency pool (~1 000 in most regions by default), so reserved concurrency keeps the render function from starving them and vice-versa. [AWS package reference](/packages/aws-lambda) for the current types and a
- **`render-batch --max-concurrent`**: orchestrator-side. Caps how many `StartExecution` calls run simultaneously — distinct from the Lambda concurrency cap, which lives one level below at the chunk-invoke layer. The CLI cannot enforce Lambda's account limit; it can only avoid creating excess Step Functions executions queued against it. working example.
- **Lambda memory** (`lambda deploy --memory`): default 10 240 MB (max). Higher memory buys faster Chrome capture + more vCPUs per chunk; lower memory saves cost but risks `15 min` timeouts on heavy compositions.
Each Step Functions execution fans out to ~`maxParallelChunks` Lambda invocations. So if the deployed reserved concurrency is 8 and `maxParallelChunks` stays at the 16 default, even a single render will get throttled — bump the deploy concurrency before running large batches. ## Related topics
For small batches (< 100 entries) the default `--max-concurrent 50` is fine. For large batches (> 1 000), a useful starting point is `--max-concurrent ≈ floor(reservedConcurrency / maxParallelChunks)` so each running render gets its full chunk fan-out budget; the batch verb does NOT enforce this, it's just guidance for picking the flag value. - [Deploy the AWS Lambda stack](/deploy/aws-lambda)
- [Use the AWS Lambda package](/packages/aws-lambda)
In-process vs distributed crossover: for a single render under ~30 seconds, the in-process renderer (`hyperframes render`) wins on latency because there's no S3 round-trip per chunk. Distributed wins for renders over ~60 seconds or when you need a personalised batch — that's the whole reason this surface exists. (The Phase 7 small-render shortcut, when it lands, will collapse the gap for short renders.) - [Understand composition variables](/concepts/variables)
## Migrating from @remotion/lambda inputProps
Remotion's `inputProps` API and HyperFrames' `variables` are isomorphic — both are JSON objects injected as render-time overrides on top of declared composition defaults. The mapping is mechanical:
| Remotion | HyperFrames |
|----------|-------------|
| `Composition.defaultProps` | `data-composition-variables` declaration on the root HTML element |
| `useCurrentFrame()` + `props.<x>` | `window.__hyperframes.getVariables().<x>` (read once on DOMContentLoaded) |
| `renderMediaOnLambda({ inputProps })` | `renderToLambda({ config: { variables } })` |
| Lambda inputProps 256 KiB cap | Step Functions execution-input 256 KiB cap |
| inputProps URL'ing pattern for large media | Same convention — URL references, not inlined bytes |
Remotion's `inputProps` has the same 256 KiB constraint and the same "URL your assets" convention, so a migration of a working `inputProps` pipeline is a straightforward CLI/SDK swap, not a payload reshape.
## What's next
- **Smaller batch primitives**: HTML-form input alongside JSONL. Open an issue if you'd find this useful.
- **TypeScript types generated from `data-composition-variables`**: `hyperframes types generate <projectDir>` is sketched and may land in v1.5; it would let SDK callers `import type { Variables } from "./template/variables"` for autocomplete + typecheck.
- **HDR template support**: HDR mp4 is currently distributed-mode-rejected (in-process only). The next v1.5 item is unblocking HDR for distributed renders so templates can produce wide-gamut output.
If your template pipeline hits a wall the docs don't cover, [file an issue on GitHub](https://github.com/heygen-com/hyperframes/issues/new) — the batch surface is new and the feedback loop on it is short.
+1 -1
View File
@@ -125,7 +125,7 @@ bun run --cwd packages/aws-lambda verify:zip-size
The build stages Chromium, Puppeteer, FFmpeg, and the handler bundle into `packages/aws-lambda/dist/handler.zip`. The size verifier keeps the unzipped artifact below Lambda's deployment limit. The build stages Chromium, Puppeteer, FFmpeg, and the handler bundle into `packages/aws-lambda/dist/handler.zip`. The size verifier keeps the unzipped artifact below Lambda's deployment limit.
## Related Guides ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="AWS Lambda Deployment" icon="cloud" href="/deploy/aws-lambda"> <Card title="AWS Lambda Deployment" icon="cloud" href="/deploy/aws-lambda">
+864 -642
View File
File diff suppressed because it is too large Load Diff
+107 -384
View File
@@ -1,453 +1,176 @@
--- ---
title: "@hyperframes/core" title: "@hyperframes/core"
description: "Types, HTML generation, runtime, and linter — the foundation every other package depends on." description: "Composition types, generation, compilation, and browser runtime."
--- ---
The core package provides the foundational types, HTML parsing/generation, runtime, and composition linter that all other Hyperframes packages build on. If you are building tooling, writing a custom integration, or extending Hyperframes itself, this is the package you need. `@hyperframes/core` contains the shared composition model and browser runtime
used across HyperFrames.
Most people should use the [CLI](/developers/cli), [Studio](/studio), or
[SDK](/sdk/quickstart). Install Core directly when you are generating composition
HTML, compiling projects, building tooling around the shared types, or embedding
the runtime.
```bash ```bash
npm install @hyperframes/core npm install @hyperframes/core
``` ```
## When to Use ## Main surfaces
<Tip> | Need | Import |
**Most users do not need to install `@hyperframes/core` directly.** The [CLI](/packages/cli), [producer](/packages/producer), and [studio](/packages/studio) packages all depend on core internally. You only need it if you are doing one of the things listed below. | --------------------------------------- | ---------------------------------------- |
</Tip> | Types, generators, and common utilities | `@hyperframes/core` |
| Timing compilation and project bundling | `@hyperframes/core/compiler` |
| Variable declarations and validation | `@hyperframes/core/variables` |
| Composition contract helpers | `@hyperframes/core/composition-contract` |
| Prebuilt browser runtime | `@hyperframes/core/runtime` |
**Use `@hyperframes/core` when you need to:** The standalone [Parsers](/packages/parsers) and [Linter](/packages/lint)
- Lint compositions programmatically (CI pipelines, editor plugins) packages own those concerns for new integrations. Core retains compatibility
- Parse HTML compositions into structured TypeScript objects re-exports for some older imports.
- Generate composition HTML from data (e.g., from an API or AI agent)
- Access the Hyperframes type system for your own tooling
- Embed the Hyperframes runtime in a custom player
**Use a different package if you want to:**
- Preview compositions in the browser — use the [CLI](/packages/cli) (`npx hyperframes preview`) or [studio](/packages/studio)
- Render compositions to MP4 — use the [CLI](/packages/cli) (`npx hyperframes render`) or [producer](/packages/producer)
- Capture frames from a headless browser — use the [engine](/packages/engine)
## Package Exports
The core package has four entry points:
| Import | Description |
|--------|-------------|
| `@hyperframes/core` | Types, parsers, generators, adapters, runtime utilities |
| `@hyperframes/core/lint` | Composition linter |
| `@hyperframes/core/compiler` | Timing compiler, HTML compiler, bundler, static guard |
| `@hyperframes/core/runtime` | Pre-built IIFE runtime for browser injection |
## Types ## Types
The core type system models compositions, timeline elements, and variables: ```ts
```typescript
import type { import type {
CanvasResolution,
CompositionSpec,
CompositionVariable,
TimelineCompositionElement,
TimelineElement, TimelineElement,
TimelineMediaElement, TimelineMediaElement,
TimelineTextElement, TimelineTextElement,
TimelineCompositionElement, } from "@hyperframes/core";
TimelineElementType, // "video" | "image" | "text" | "audio" | "composition"
CompositionSpec,
CompositionVariable,
CanvasResolution, // "landscape" | "portrait" | "landscape-4k" | "portrait-4k" | "square" | "square-4k"
Orientation, // "16:9" | "9:16"
FrameAdapter,
FrameAdapterContext,
} from '@hyperframes/core';
// Type guards
import {
isTextElement,
isMediaElement,
isCompositionElement,
} from '@hyperframes/core';
// Constants
import {
CANVAS_DIMENSIONS, // { landscape: { width, height }, portrait: { width, height } }
TIMELINE_COLORS,
DEFAULT_DURATIONS,
} from '@hyperframes/core';
``` ```
### Variable Types Composition variables support `string`, `number`, `color`, `boolean`, `enum`,
`font`, and `image` values.
Compositions can expose typed variables for dynamic content: ## Parse or generate HTML
```typescript Core re-exports the common parsing helpers:
import type {
CompositionVariableType, // "string" | "number" | "color" | "boolean" | "enum" ```ts
StringVariable, import { extractCompositionMetadata, parseHtml } from "@hyperframes/core";
NumberVariable,
ColorVariable, const parsed = parseHtml(html);
BooleanVariable, const metadata = extractCompositionMetadata(html);
EnumVariable,
} from '@hyperframes/core'; console.log(metadata.compositionId, metadata.compositionDuration, metadata.variables);
``` ```
### Keyframe Types Generate a complete composition from `TimelineElement` data:
```typescript ```ts
import type { import { generateHyperframesHtml } from "@hyperframes/core";
Keyframe,
KeyframeProperties,
ElementKeyframes,
StageZoom,
StageZoomKeyframe,
} from '@hyperframes/core';
import { getDefaultStageZoom } from '@hyperframes/core'; const html = generateHyperframesHtml(elements, 6, {
compositionId: "product-intro",
resolution: "landscape",
animations,
styles,
});
``` ```
## Parsing and Generating HTML The second argument is the requested duration in seconds. Pass a stable
`compositionId` when output must be reproducible.
Round-trip between HTML and structured data: ## Read and validate variables
```typescript Inside a composition script, `getVariables()` reads declared defaults plus the
import { parseHtml, generateHyperframesHtml } from '@hyperframes/core'; values supplied for the current preview or render:
import type { ParsedHtml, CompositionMetadata } from '@hyperframes/core';
// Parse HTML into structured data ```ts
const parsed: ParsedHtml = parseHtml(htmlString); import { getVariables } from "@hyperframes/core";
// parsed.elements, parsed.gsapScript, parsed.styles, parsed.resolution, parsed.keyframes
// Extract composition metadata
import { extractCompositionMetadata } from '@hyperframes/core';
const meta: CompositionMetadata = extractCompositionMetadata(htmlString);
// meta.id, meta.duration, meta.width, meta.height, meta.variables
//
// Variable metadata is declared on the document root, for example:
// <html
// data-composition-id="card"
// data-composition-duration="3"
// data-composition-variables='[{"id":"title","label":"Title","type":"string","default":"Hello"}]'
// >
// Read resolved variables inside a composition (declared defaults +
// CLI overrides + per-instance host data-variable-values):
import { getVariables } from '@hyperframes/core';
const { title } = getVariables<{ title: string }>(); const { title } = getVariables<{ title: string }>();
```
In Node tooling, validate a values object against declarations parsed from the
composition:
```ts
import { formatVariableValidationIssue, validateVariables } from "@hyperframes/core";
const issues = validateVariables({ title: "Launch day" }, metadata.variables);
// Validate CLI / host overrides against the declared schema:
import { validateVariables, formatVariableValidationIssue } from '@hyperframes/core';
const issues = validateVariables({ title: 'Hello', count: 'three' }, meta.variables);
for (const issue of issues) { for (const issue of issues) {
console.warn(formatVariableValidationIssue(issue)); console.warn(formatVariableValidationIssue(issue));
} }
// Generate HTML from structured data
const html = generateHyperframesHtml(elements, {
animations,
styles,
resolution: 'landscape',
compositionId: 'my-video',
});
``` ```
### Modifying HTML ## Compile a project
```typescript Use the compiler entry when your integration needs resolved media timing or a
import { single bundled document.
updateElementInHtml,
addElementToHtml,
removeElementFromHtml,
validateCompositionHtml,
} from '@hyperframes/core';
// Update an element's properties ```ts
const updatedHtml = updateElementInHtml(html, 'el-1', { start: 5 }); import { bundleToSingleHtml, compileHtml } from "@hyperframes/core/compiler";
// Add a new element const compiled = await compileHtml(rawHtml, "./project", async (mediaPath) =>
const newHtml = addElementToHtml(html, newElement); probeDuration(mediaPath),
// Remove an element
const cleanHtml = removeElementFromHtml(html, 'el-1');
// Validate HTML structure
const result = validateCompositionHtml(html);
// result.valid, result.errors
```
### GSAP Script Parsing
<Info>
The GSAP + HTML parsing layer now lives in its own standalone package, [`@hyperframes/parsers`](/packages/parsers). Core re-exports the API below for back-compat; import from `@hyperframes/parsers` directly in new code.
</Info>
```typescript
import {
serializeGsapAnimations,
getAnimationsForElementId,
validateCompositionGsap,
keyframesToGsapAnimations,
gsapAnimationsToKeyframes,
} from '@hyperframes/core';
// GSAP parsing, mutation, and constants live in @hyperframes/parsers:
import { parseGsapScript, SUPPORTED_PROPS, SUPPORTED_EASES } from '@hyperframes/parsers/gsap-parser';
import { updateAnimationInScript, addAnimationToScript, removeAnimationFromScript } from '@hyperframes/parsers/gsap-writer-acorn';
import type { GsapAnimation, GsapMethod, ParsedGsap } from '@hyperframes/core';
// Parse GSAP script into structured animations
const parsed: ParsedGsap = parseGsapScript(scriptContent);
// parsed.animations, parsed.timelineVar, parsed.preamble, parsed.postamble
// Serialize back to script
const script = serializeGsapAnimations(parsed.animations);
```
### HTML Generation
```typescript
import {
generateHyperframesHtml,
generateGsapTimelineScript,
generateHyperframesStyles,
} from '@hyperframes/core';
// Generate a complete HTML composition
const html = generateHyperframesHtml(elements, options);
// Generate just the GSAP script
const script = generateGsapTimelineScript(animations, options);
// Generate CSS styles
const { coreCss, customCss, googleFontsLink } = generateHyperframesStyles(
elements, 'landscape', customStyles
); );
const bundled = await bundleToSingleHtml("./project", {
entryFile: "index.html",
});
``` ```
### Template Utilities The compiler also exposes lower-level timing helpers such as
`compileTimingAttrs()`, `injectDurations()`, and `extractResolvedMedia()`.
```typescript ## Check the static contract
import {
generateBaseHtml,
getStageStyles,
GSAP_CDN,
BASE_STYLES,
ELEMENT_BASE_STYLES,
MEDIA_STYLES,
TEXT_STYLES,
ZOOM_CONTAINER_STYLES,
} from '@hyperframes/core';
// Generate base HTML structure for a resolution ```ts
const baseHtml = generateBaseHtml('landscape'); import { validateHyperframeHtmlContract } from "@hyperframes/core/compiler";
const styles = getStageStyles('portrait');
```
## Linter const result = await validateHyperframeHtmlContract(html);
<Info> if (!result.isValid) {
The composition linter now lives in its own package, [`@hyperframes/lint`](/packages/lint) — install it directly to lint a project or HTML string from Node without the CLI. `@hyperframes/core/lint` remains a back-compat re-export. console.error(result.missingKeys);
</Info>
The composition linter checks for structural issues that would cause rendering failures or unexpected behavior. You can run it from the CLI with `npx hyperframes lint`, or call it programmatically:
```typescript
import { lintHyperframeHtml, lintMediaUrls } from '@hyperframes/core/lint';
import type {
HyperframeLintResult,
HyperframeLintFinding,
HyperframeLintSeverity, // "error" | "warning" | "info"
HyperframeLinterOptions,
} from '@hyperframes/core/lint';
const result: HyperframeLintResult = lintHyperframeHtml(html, { filePath: 'index.html' });
// result.ok, result.errorCount, result.warningCount, result.findings
for (const finding of result.findings) {
console.log(finding.severity, finding.code, finding.message);
// finding.file, finding.selector, finding.elementId, finding.fixHint, finding.snippet
} }
// Additional media URL validation
const mediaFindings = lintMediaUrls(result.findings);
``` ```
Detected issues include: For the full composition lint result, use [`@hyperframes/lint`](/packages/lint)
or run `npx hyperframes lint`.
- Missing timeline registration (`window.__timelines`) ## Build a frame adapter
- Unmuted video elements (causes autoplay failures)
- Missing `class="clip"` on timed visible elements
- Deprecated attribute names
- Missing composition dimensions (`data-width`, `data-height`)
- Invalid `data-start` references to nonexistent clip IDs
<Info> Frame adapters make an animation runtime seekable by frame. Core includes the
For a full list of what the linter catches and how to fix each issue, see [Common Mistakes](/guides/common-mistakes) and [Troubleshooting](/guides/troubleshooting). GSAP adapter:
</Info>
## Compiler ```ts
import { createGSAPFrameAdapter } from "@hyperframes/core";
The compiler sub-package handles timing resolution, HTML compilation, and bundling: const adapter = createGSAPFrameAdapter({
id: "product-intro",
```typescript
// Timing compiler (browser-safe — no Node.js dependencies)
import {
compileTimingAttrs,
injectDurations,
extractResolvedMedia,
clampDurations,
} from '@hyperframes/core/compiler';
import type {
UnresolvedElement,
ResolvedDuration,
ResolvedMediaElement,
CompilationResult,
} from '@hyperframes/core/compiler';
// Compile timing attributes from HTML
const compiled: CompilationResult = compileTimingAttrs(html);
// Inject resolved durations back into HTML
const updatedHtml = injectDurations(html, compiled.durations);
// Extract resolved media elements
const media: ResolvedMediaElement[] = extractResolvedMedia(html);
```
```typescript
// HTML compiler (Node.js — requires media probing)
import { compileHtml } from '@hyperframes/core/compiler';
import type { MediaDurationProber } from '@hyperframes/core/compiler';
const prober: MediaDurationProber = async (src) => getDuration(src);
const compiledHtml = await compileHtml(html, prober);
```
```typescript
// HTML bundler (Node.js — bundles to single file)
import { bundleToSingleHtml } from '@hyperframes/core/compiler';
import type { BundleOptions } from '@hyperframes/core/compiler';
const bundled = await bundleToSingleHtml({ entryPath: './index.html', inline: true });
```
```typescript
// Static guard — validate HTML contract
import { validateHyperframeHtmlContract } from '@hyperframes/core/compiler';
import type {
HyperframeStaticGuardResult,
HyperframeStaticFailureReason,
} from '@hyperframes/core/compiler';
const guard: HyperframeStaticGuardResult = validateHyperframeHtmlContract(html);
// guard.ok, guard.failures[]
// Failure reasons: "missing_composition_id" | "missing_composition_dimensions"
// | "missing_timeline_registry" | "invalid_script_syntax"
// | "invalid_static_hyperframe_contract"
```
## Runtime
The Hyperframes runtime manages playback, seeking, and clip lifecycle in the browser. The core package provides utilities for building and loading the runtime:
```typescript
import {
loadHyperframeRuntimeSource,
buildHyperframesRuntimeScript,
HYPERFRAME_RUNTIME_ARTIFACTS,
HYPERFRAME_RUNTIME_CONTRACT,
HYPERFRAME_RUNTIME_GLOBALS,
HYPERFRAME_BRIDGE_SOURCES,
HYPERFRAME_CONTROL_ACTIONS,
} from '@hyperframes/core';
import type {
HyperframeControlAction,
HyperframesRuntimeBuildOptions,
} from '@hyperframes/core';
// Load the pre-built runtime IIFE
const runtimeSource = loadHyperframeRuntimeSource();
// Build a custom runtime script
const script = buildHyperframesRuntimeScript(options);
```
The pre-built runtime IIFE is available as a direct import:
```typescript
import runtime from '@hyperframes/core/runtime';
```
## Frame Adapters
The core package defines the [Frame Adapter](/concepts/frame-adapters) interface and provides the built-in GSAP adapter:
```typescript
import { createGSAPFrameAdapter } from '@hyperframes/core';
import type {
FrameAdapter,
FrameAdapterContext,
GSAPTimelineLike,
CreateGSAPFrameAdapterOptions,
} from '@hyperframes/core';
// Create a GSAP frame adapter
const adapter: FrameAdapter = createGSAPFrameAdapter({
id: 'my-composition',
fps: 30, fps: 30,
timeline: gsapTimeline, timeline,
});
await adapter.init?.({
compositionId: "product-intro",
fps: 30,
width: 1920,
height: 1080,
}); });
// Adapter lifecycle
await adapter.init?.(context);
const durationFrames = adapter.getDurationFrames();
await adapter.seekFrame(42); await adapter.seekFrame(42);
await adapter.destroy?.();
``` ```
## Media Utilities ## Related topics
```typescript
import {
MEDIA_VISUAL_STYLE_PROPERTIES,
copyMediaVisualStyles,
quantizeTimeToFrame,
} from '@hyperframes/core';
import type { MediaVisualStyleProperty } from '@hyperframes/core';
// Quantize a time value to the nearest frame boundary
const frameTime = quantizeTimeToFrame(5.033, 30); // → 5.033... snapped to frame
// Copy visual styles between media elements
copyMediaVisualStyles(fromElement, toElement);
```
## Picker API
For element selection in editor UIs:
```typescript
import type {
HyperframePickerApi,
HyperframePickerBoundingBox,
HyperframePickerElementInfo,
} from '@hyperframes/core';
```
## Related Packages
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="@hyperframes/parsers" icon="code" href="/packages/parsers"> <Card title="Composition schema" icon="code" href="/reference/html-schema">
The standalone GSAP + HTML parsing layer extracted from core. Learn the HTML contract that Core reads and writes.
</Card>
<Card title="@hyperframes/parsers" icon="brackets-curly" href="/packages/parsers">
Work directly with HTML and GSAP parsing.
</Card> </Card>
<Card title="@hyperframes/lint" icon="circle-check" href="/packages/lint"> <Card title="@hyperframes/lint" icon="circle-check" href="/packages/lint">
The composition linter as a standalone library. Validate composition HTML in your own tooling.
</Card> </Card>
<Card title="@hyperframes/studio-server" icon="server" href="/packages/studio-server"> <Card title="@hyperframes/producer" icon="video" href="/packages/producer">
The mountable studio preview/editor backend. Turn a project into a finished video from Node.js.
</Card>
<Card title="CLI" icon="terminal" href="/packages/cli">
The easiest way to create, preview, lint, and render compositions.
</Card>
<Card title="Engine" icon="gear" href="/packages/engine">
Low-level frame capture pipeline that uses core types and runtime.
</Card>
<Card title="Producer" icon="film" href="/packages/producer">
Full rendering pipeline built on top of core and engine.
</Card> </Card>
</CardGroup> </CardGroup>
+60 -383
View File
@@ -1,417 +1,94 @@
--- ---
title: "@hyperframes/engine" title: "@hyperframes/engine"
description: "Seekable page-to-video capture engine using Chrome's BeginFrame API." description: "Low-level, seekable frame capture and encoding primitives."
--- ---
The engine package provides the low-level video capture pipeline: it loads an HTML page in headless Chrome, seeks to each frame independently, and captures pixel buffers using Chrome's `HeadlessExperimental.beginFrame` API. This is the layer that makes Hyperframes rendering deterministic. `@hyperframes/engine` is the low-level layer beneath the
[Producer](/packages/producer). It opens a page that implements the HyperFrames
seek protocol, seeks to an exact time, and captures the resulting frame.
Most integrations should use the CLI or Producer. Use the Engine only when you
need to own frame capture, encoding, media extraction, or browser management.
```bash ```bash
npm install @hyperframes/engine npm install @hyperframes/engine
``` ```
## When to Use ## Why it is different from screen recording
<Warning> A screen recorder waits for the wall clock and may miss frames under load. The
**Most users should NOT use the engine directly.** Use the [CLI](/packages/cli) (`npx hyperframes render`) or the [producer](/packages/producer) package instead — they handle runtime injection, audio mixing, and encoding for you. Engine asks the page to seek to a specific time and captures that state before
</Warning> moving on.
**Use `@hyperframes/engine` when you need to:** That makes frame scheduling repeatable and prevents dropped frames caused by a
- Build a custom rendering pipeline with full control over frame capture slow machine. Exact pixels can still vary with Chrome, fonts, codecs, GPU
- Integrate Hyperframes capture into an existing video processing system behavior, and the host environment. Pin those dependencies when exact visual
- Capture individual frames (e.g., for thumbnails or sprite sheets) without encoding to video reproducibility matters.
- Implement a custom encoding backend (not FFmpeg)
**Use a different package if you want to:** ## Capture frames
- Render an HTML composition to a finished MP4 or WebM — use the [producer](/packages/producer) or [CLI](/packages/cli)
- Preview compositions in the browser — use the [CLI](/packages/cli) or [studio](/packages/studio)
- Lint or parse composition HTML — use [core](/packages/core)
## How It Works The page at `serverUrl` must expose `window.__hf` with a `duration` and a
deterministic `seek(time)` function.
The engine implements a **seek-and-capture** loop that is fundamentally different from screen recording: ```ts
<Steps>
<Step title="Launch headless Chrome">
The engine starts `chrome-headless-shell`, a minimal headless Chrome binary optimized for programmatic control via the Chrome DevTools Protocol (CDP).
</Step>
<Step title="Load the composition">
Your HTML composition is loaded into a browser page. The Hyperframes runtime is injected to manage timeline seeking.
</Step>
<Step title="Seek to each frame">
For every frame in the video (e.g., 900 frames for a 30-second video at 30fps), the engine calls `window.__hf.seek(time)` to advance the composition to the exact timestamp. No wall clock is involved — each frame is independently positioned.
</Step>
<Step title="Capture via BeginFrame">
Chrome's `HeadlessExperimental.beginFrame` API captures the compositor output as a pixel buffer. This produces pixel-perfect frames without any screen recording artifacts.
</Step>
<Step title="Hand off frames">
Captured frame buffers are passed to a consumer — typically FFmpeg (via the producer) for encoding into MP4, but you can provide your own consumer.
</Step>
</Steps>
This approach guarantees [deterministic rendering](/concepts/determinism): the same HTML always produces the identical video, regardless of system load or timing.
## Configuration
```typescript
import { resolveConfig, DEFAULT_CONFIG } from '@hyperframes/engine';
import type { EngineConfig } from '@hyperframes/engine';
// Use defaults
const config = DEFAULT_CONFIG;
// Or resolve with overrides
const config = resolveConfig({
// ... custom options
});
```
### Quality Presets
| Preset | Use Case | Speed |
|--------|----------|-------|
| `draft` | Fast iteration during development | Fastest |
| `standard` | Production renders with good quality/speed balance | Moderate |
| `high` | Final delivery, maximum quality | Slowest |
### FPS Options
| FPS | Use Case |
|-----|----------|
| `24` | Cinematic look, smaller file size |
| `30` | Standard web video, good balance |
| `60` | Smooth motion, UI animations, screen recordings |
## Programmatic Usage
The engine uses a session-based API for frame capture:
```typescript
import { import {
createCaptureSession,
initializeSession,
captureFrame, captureFrame,
captureFrameToBuffer,
getCompositionDuration,
closeCaptureSession, closeCaptureSession,
} from '@hyperframes/engine'; createCaptureSession,
getCompositionDuration,
initializeSession,
} from "@hyperframes/engine";
// 1. Create a capture session (serverUrl, outputDir, options) const fps = { num: 30, den: 1 };
const session = await createCaptureSession(serverUrl, outputDir, { const session = await createCaptureSession(
fps: { num: 30, den: 1 }, width: 1920, height: 1080, serverUrl,
}); "./frames",
{
// 2. Initialize the session
await initializeSession(session);
// 3. Get the total duration (async)
const duration = await getCompositionDuration(session);
// 4. Capture frames
const totalFrames = Math.ceil(duration * 30);
for (let i = 0; i < totalFrames; i++) {
// Capture to disk
const result = await captureFrame(session, i);
// result.path, result.captureTimeMs
// Or capture to buffer (in-memory)
const bufResult = await captureFrameToBuffer(session, i);
// bufResult.buffer, bufResult.captureTimeMs
}
// 5. Clean up
await closeCaptureSession(session);
```
### Browser Management
```typescript
import {
acquireBrowser,
releaseBrowser,
resolveHeadlessShellPath,
buildChromeArgs,
} from '@hyperframes/engine';
// Acquire a browser instance (creates or reuses from pool)
const browser = await acquireBrowser();
// Get the Chrome binary path
const chromePath = await resolveHeadlessShellPath();
// Release when done
await releaseBrowser(browser);
```
### Encoding
The engine includes FFmpeg encoding utilities with support for MP4 (h264) and WebM (VP9 with alpha):
```typescript
import {
encodeFramesFromDir,
muxVideoWithAudio,
applyFaststart,
detectGpuEncoder,
getEncoderPreset,
ENCODER_PRESETS,
} from '@hyperframes/engine';
// Get format-aware encoder settings
const mp4Preset = getEncoderPreset('standard', 'mp4');
// { codec: "h264", pixelFormat: "yuv420p", preset: "medium", quality: 23 }
const webmPreset = getEncoderPreset('standard', 'webm');
// { codec: "vp9", pixelFormat: "yuva420p", preset: "good", quality: 23 }
// Encode captured frames to video
await encodeFramesFromDir(framesDir, 'frame_%06d.png', outputPath, {
fps: { num: 30, den: 1 },
...webmPreset,
});
// Mix video with audio (uses Opus for WebM, AAC for MP4)
await muxVideoWithAudio(videoPath, audioPath, outputPath);
// Apply MP4 faststart for streaming (no-op for WebM)
await applyFaststart(inputPath, outputPath);
// Detect GPU encoding support
const gpu = await detectGpuEncoder();
// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | "amf" | null
```
#### WebM with VP9 Alpha
When encoding for transparency, use `format: "webm"` with `getEncoderPreset()`. This configures:
- **VP9 codec** (`libvpx-vp9`) with alpha-capable `yuva420p` pixel format
- **`-auto-alt-ref 0`** and **`alpha_mode=1`** metadata for proper alpha encoding
- **`-row-mt 1`** for multi-threaded VP9 encoding
- **Opus audio** in the mux step (instead of AAC for MP4)
### Streaming Encoder
For memory-efficient encoding without writing frames to disk:
```typescript
import { spawnStreamingEncoder } from '@hyperframes/engine';
const encoder = await spawnStreamingEncoder({
outputPath: './output.mp4',
fps: { num: 30, den: 1 },
width: 1920, width: 1920,
height: 1080, height: 1080,
}); fps,
format: "jpeg",
},
);
// Feed frames directly to encoder try {
encoder.writeFrame(frameBuffer); await initializeSession(session);
// ... const duration = await getCompositionDuration(session);
const result = await encoder.finalize(); const totalFrames = Math.ceil(duration * 30);
```
### Video Frame Extraction for (let frame = 0; frame < totalFrames; frame += 1) {
const time = frame / 30;
Extract frames from source video files for injection into the browser: await captureFrame(session, frame, time);
}
```typescript } finally {
import { await closeCaptureSession(session);
parseVideoElements,
extractAllVideoFrames,
getFrameAtTime,
createFrameLookupTable,
FrameLookupTable,
} from '@hyperframes/engine';
// Parse video elements from HTML
const videos = parseVideoElements(html);
// Extract all frames from a video
const frames = await extractAllVideoFrames(videoPath, { fps: 30 });
// Create a lookup table for fast frame access
const lookup = createFrameLookupTable(frames);
const frame = lookup.getFrame('video-1', 5.0);
```
### Audio Processing
```typescript
import { parseAudioElements, processCompositionAudio } from '@hyperframes/engine';
// Parse audio elements from HTML
const audioElements = parseAudioElements(html);
// Process and mix all audio tracks
const mixResult = await processCompositionAudio({ audioElements, duration, fps });
```
### Parallel Rendering
```typescript
import {
calculateOptimalWorkers,
distributeFrames,
executeParallelCapture,
getSystemResources,
} from '@hyperframes/engine';
// Check system resources
const resources = getSystemResources();
// Calculate optimal worker count
const workers = calculateOptimalWorkers(totalFrames);
// Distribute frames across workers
const tasks = distributeFrames(totalFrames, workers);
// Execute parallel capture
const results = await executeParallelCapture(tasks);
```
### File Server
Serve composition files over HTTP for the browser to load:
```typescript
import { createFileServer } from '@hyperframes/engine';
const server = await createFileServer({ root: './my-video', port: 0 });
// server.url, server.port
// ... use server.url as the composition URL
await server.close();
```
## HDR APIs
The engine exports two layers of HDR support: **color-space utilities** that classify sources and configure the FFmpeg encoder, and a **WebGPU readback runtime** for capturing CSS-animated DOM directly into HDR.
For end-to-end HDR rendering (HDR video and image sources composited into an HDR10 MP4) use the [producer](/packages/producer) or the CLI render pipeline with HDR auto-detect / `--hdr` / `--sdr` — see [HDR Rendering](/guides/hdr). The APIs below are for custom integrations.
### Color space utilities
```typescript
import {
isHdrColorSpace,
detectTransfer,
analyzeCompositionHdr,
getHdrEncoderColorParams,
DEFAULT_HDR10_MASTERING,
} from '@hyperframes/engine';
import type { HdrTransfer, HdrEncoderColorParams, HdrMasteringMetadata } from '@hyperframes/engine';
// Classify a single source from its ffprobe color space
isHdrColorSpace(colorSpace); // boolean — true for BT.2020 / PQ / HLG
detectTransfer(colorSpace); // 'pq' | 'hlg' (gate on isHdrColorSpace first)
// Pick the dominant transfer across many sources
analyzeCompositionHdr([cs1, cs2]); // { hasHdr, dominantTransfer: 'pq' | 'hlg' | null }
// Build the FFmpeg color params + HDR10 static metadata for x265
const params = getHdrEncoderColorParams('pq');
// {
// colorPrimaries: 'bt2020',
// colorTrc: 'smpte2084',
// colorspace: 'bt2020nc',
// pixelFormat: 'yuv420p10le',
// x265ColorParams: 'colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=...:max-cll=1000,400',
// mastering: { masterDisplay: '...', maxCll: '1000,400' },
// }
```
`getHdrEncoderColorParams` always includes both color tagging *and* the HDR10 static metadata (mastering display + content light level). Without that metadata, downstream players treat the file as SDR BT.2020 and tone-map incorrectly. Pass a custom `HdrMasteringMetadata` if you have measured per-content values; otherwise the conservative `DEFAULT_HDR10_MASTERING` defaults match how most HDR10 grading suites tag content.
### WebGPU HDR DOM capture
For capturing CSS-animated DOM directly into HDR (no FFmpeg source involved), the engine exposes a separate WebGPU pipeline:
```typescript
import {
launchHdrBrowser,
buildHdrChromeArgs,
initHdrReadback,
uploadAndReadbackHdrFrame,
float16ToPqRgb,
} from '@hyperframes/engine';
// Launch headed Chrome with WebGPU enabled
const { browser, page } = await launchHdrBrowser({ width: 1920, height: 1080 });
// Inject the WebGPU readback runtime
const ok = await initHdrReadback(page, 1920, 1080);
// For each frame: upload float16 pixels, read back float16 RGBA
const { rgba16, bytesPerRow } = await uploadAndReadbackHdrFrame(page, float16Base64);
// Convert linear float16 → PQ-encoded 16-bit RGB suitable for piping into ffmpeg/x265
const pqRgb = float16ToPqRgb(rgba16, width, height, bytesPerRow);
```
<Warning>
This path requires **headed Chrome with `--enable-unsafe-webgpu`** — WebGPU is unavailable in `chrome-headless-shell`. It is *not* used by the default HDR-aware render pipeline (which extracts HDR pixels from sources via FFmpeg and composites in Node). Use it only for advanced custom pipelines that need CSS animations driving HDR pixel output.
</Warning>
## The `window.__hf` Protocol
The engine communicates with the browser page via the `window.__hf` protocol. Any page that implements this protocol can be captured by the engine — you are not limited to Hyperframes compositions.
```typescript
// The page must expose this on window.__hf
interface HfProtocol {
duration: number; // Total duration in seconds
seek(time: number): void; // Seek to a specific time
media?: HfMediaElement[]; // Optional media element declarations
}
interface HfMediaElement {
elementId: string; // DOM element ID
src: string; // Media source URL
startTime: number; // Start time on timeline
endTime: number; // End time on timeline
mediaOffset?: number; // Playback offset in source
volume?: number; // Volume (0-1)
hasAudio?: boolean; // Whether element has audio
} }
``` ```
## Key Concepts Use `captureFrameToBuffer()` when the next stage needs an in-memory buffer
instead of a file.
### BeginFrame Rendering ## What else the package exposes
Traditional screen capture records at wall-clock speed — if your system is under load, frames get dropped. The engine uses Chrome's `HeadlessExperimental.beginFrame` to explicitly advance the compositor, producing each frame on demand. This means: | Surface | Use it for |
| --- | --- |
| Browser management | Launching, pooling, and releasing Chrome |
| Encoders | MP4, WebM, MOV, GIF, PNG-sequence, muxing, and faststart |
| Media extraction | Preparing source video frames and audio tracks |
| Parallel capture | Distributing frame ranges across workers |
| Diagnostics | Capture performance, media metadata, and GPU parity |
- **No dropped frames** — every frame is captured These are rendering internals. Their result and error conventions differ by
- **No timing dependency** — a 60-second video does not take 60 seconds to capture operation: browser and capture orchestration throws, FFmpeg wrappers generally
- **Pixel-perfect output** — the compositor produces the exact pixels it would display return result objects, and teardown helpers avoid masking the original failure.
For more on how this enables deterministic output, see [Deterministic Rendering](/concepts/determinism). ## Related topics
### Seek Contract
The engine relies on the Hyperframes runtime's `window.__hf.seek(time)` function. When called, `seek`:
1. Pauses all GSAP timelines
2. Seeks every timeline to the exact timestamp
3. Updates all media elements (video, audio) to match
4. Mounts/unmounts clips based on their `data-start` and `data-duration`
This contract is what makes frame-by-frame capture possible — each frame is a complete, independent snapshot of the composition at that point in time.
### Chrome Requirements
The engine requires `chrome-headless-shell`, which is included when you install the package. It uses a pinned Chrome version to ensure consistent rendering across environments. For fully deterministic output (including fonts), use Docker mode via the [producer](/packages/producer).
## Related Packages
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Producer" icon="film" href="/packages/producer"> <Card title="@hyperframes/producer" icon="video" href="/packages/producer">
Wraps the engine with runtime injection, FFmpeg encoding, and audio mixing for complete MP4 output. Get a finished video without assembling the pipeline yourself.
</Card> </Card>
<Card title="Core" icon="cube" href="/packages/core"> <Card title="Deterministic rendering" icon="repeat" href="/concepts/determinism">
Provides the types, runtime, and linter that the engine depends on. Understand what HyperFrames controls and what the environment still affects.
</Card>
<Card title="CLI" icon="terminal" href="/packages/cli">
The easiest way to render — calls the producer (and engine) under the hood.
</Card>
<Card title="Studio" icon="palette" href="/packages/studio">
Visual editor for building compositions before rendering them with the engine.
</Card> </Card>
</CardGroup> </CardGroup>
+1 -1
View File
@@ -104,7 +104,7 @@ behavior until they opt in.
Pass `projectDir` for one-shot uploads, or call `deploySite()` separately and reuse the returned site handle across many renders. Pass `projectDir` for one-shot uploads, or call `deploySite()` separately and reuse the returned site handle across many renders.
## Related Guides ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="GCP Cloud Run Deployment" icon="cloud" href="/deploy/gcp-cloud-run"> <Card title="GCP Cloud Run Deployment" icon="cloud" href="/deploy/gcp-cloud-run">
+5 -5
View File
@@ -50,7 +50,7 @@ import type {
```typescript ```typescript
import { lintHyperframeHtml, lintMediaUrls } from '@hyperframes/lint'; import { lintHyperframeHtml, lintMediaUrls } from '@hyperframes/lint';
const result = lintHyperframeHtml(html, { filePath: 'index.html' }); const result = await lintHyperframeHtml(html, { filePath: 'index.html' });
// result.ok, result.errorCount, result.warningCount, result.findings // result.ok, result.errorCount, result.warningCount, result.findings
for (const finding of result.findings) { for (const finding of result.findings) {
@@ -59,7 +59,7 @@ for (const finding of result.findings) {
} }
// Additional media URL validation // Additional media URL validation
const mediaFindings = lintMediaUrls(result.findings); const mediaFindings = await lintMediaUrls(html);
``` ```
## Linting a Project ## Linting a Project
@@ -74,7 +74,7 @@ const result: ProjectLintResult = await lintProject('./my-composition');
// result.totalErrors, result.totalWarnings, result.results[] // result.totalErrors, result.totalWarnings, result.results[]
// each result entry: { file, result: HyperframeLintResult } // each result entry: { file, result: HyperframeLintResult }
if (shouldBlockRender(false, false, result.totalErrors, result.totalWarnings)) { if (shouldBlockRender(true, false, result.totalErrors, result.totalWarnings)) {
throw new Error(`Lint found ${result.totalErrors} blocking error(s)`); throw new Error(`Lint found ${result.totalErrors} blocking error(s)`);
} }
``` ```
@@ -123,10 +123,10 @@ Detected issues include:
(`gsap_timeline_set_initial_hide`) (`gsap_timeline_set_initial_hide`)
<Info> <Info>
For a full list of what the linter catches and how to fix each issue, see [Common Mistakes](/guides/common-mistakes) and [Troubleshooting](/guides/troubleshooting). For common failures and fixes, see [Troubleshooting](/guides/troubleshooting).
</Info> </Info>
## Related Packages ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="@hyperframes/parsers" icon="code" href="/packages/parsers"> <Card title="@hyperframes/parsers" icon="code" href="/packages/parsers">
+7 -3
View File
@@ -3,7 +3,10 @@ title: "@hyperframes/parsers"
description: "The GSAP + HTML parser/writer suite — standalone, zero @hyperframes/* dependencies." description: "The GSAP + HTML parser/writer suite — standalone, zero @hyperframes/* dependencies."
--- ---
The parsers package is the standalone foundation extracted from core. It owns the GSAP animation parser/writer (recast **and** acorn implementations), the HTML composition parser, hf-id stamping, and spring-ease generation. It has **no `@hyperframes/*` dependencies**, so it's the base every other package builds on. The parsers package owns the GSAP animation parser/writer, HTML composition
parser, hf-id stamping, and spring-ease generation. It has no
`@hyperframes/*` runtime dependencies, so it can be used without the rest of
the framework.
```bash ```bash
npm install @hyperframes/parsers npm install @hyperframes/parsers
@@ -39,7 +42,8 @@ npm install @hyperframes/parsers
| `@hyperframes/parsers/sub-composition-validity` | Sub-composition validation utilities | | `@hyperframes/parsers/sub-composition-validity` | Sub-composition validation utilities |
<Info> <Info>
The package ships subpath entries so consumers tree-shake to what they use — importing `@hyperframes/parsers/hf-ids` (a couple KB) does **not** pull in the GSAP AST machinery (recast/babel/acorn). The package ships focused subpath entries. Import
`@hyperframes/parsers/hf-ids` when you do not need the GSAP AST machinery.
</Info> </Info>
## HTML Parsing ## HTML Parsing
@@ -144,7 +148,7 @@ const withIds = ensureHfIds(htmlString);
import { generateSpringEaseData, SPRING_PRESETS } from '@hyperframes/parsers/spring-ease'; import { generateSpringEaseData, SPRING_PRESETS } from '@hyperframes/parsers/spring-ease';
``` ```
## Related Packages ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="@hyperframes/core" icon="cube" href="/packages/core"> <Card title="@hyperframes/core" icon="cube" href="/packages/core">
+73 -156
View File
@@ -3,239 +3,156 @@ title: "@hyperframes/player"
description: "Embeddable web component for playing HyperFrames compositions in any web page." description: "Embeddable web component for playing HyperFrames compositions in any web page."
--- ---
The player package provides a `<hyperframes-player>` custom element that embeds a HyperFrames composition anywhere — in any framework or plain HTML. Zero dependencies, 3KB gzipped. The player package provides a `<hyperframes-player>` custom element that embeds a
HyperFrames composition in plain HTML or a framework application.
```bash ```bash
npm install @hyperframes/player npm install @hyperframes/player
``` ```
## When to Use Use Player when an application needs to play and seek an HTML composition. Use
[Studio](/packages/studio) to edit it or the [CLI](/packages/cli) and
[Producer](/packages/producer) to render a video file.
**Use `@hyperframes/player` when you need to:** ## Embed a composition
- Embed a rendered composition in a website, dashboard, or app
- Add a video-like player to a landing page or product demo
- Show compositions in documentation or blog posts
**Use a different package if you want to:**
- Edit compositions interactively — use the [studio](/packages/studio)
- Preview during development — use the [CLI](/packages/cli) (`npx hyperframes preview`)
- Render to MP4 — use the [CLI](/packages/cli) or [producer](/packages/producer)
## Quick Start
### Via CDN ### Via CDN
```html ```html title="index.html"
<script type="module" src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script> <script type="module" src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
<hyperframes-player <hyperframes-player
src="./my-composition/index.html" src="./my-composition/index.html"
controls controls
autoplay
muted
style="width: 100%; max-width: 800px; aspect-ratio: 16/9" style="width: 100%; max-width: 800px; aspect-ratio: 16/9"
></hyperframes-player> ></hyperframes-player>
``` ```
If you need a classic `<script>` tag instead of ESM, use the explicit global build: With a package manager:
```html
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player/dist/hyperframes-player.global.js"></script>
```
### Via npm
```js ```js
import '@hyperframes/player'; import "@hyperframes/player";
``` ```
```html ```html title="index.html"
<hyperframes-player src="/compositions/intro.html" controls></hyperframes-player> <hyperframes-player
src="/compositions/intro.html"
controls
></hyperframes-player>
``` ```
## HTML Attributes Set `autoplay muted` only when playback should start without a user gesture.
## Attributes
| Attribute | Type | Default | Description | | Attribute | Type | Default | Description |
|-----------|------|---------|-------------| | --------------- | ------- | ------- | --------------------------------------------------------- |
| `src` | string | required | URL or relative path to composition HTML | | `src` | string | | URL or relative path to composition HTML |
| `width` | number | 1920 | Composition width in pixels | | `srcdoc` | string | — | Composition HTML already available as a string |
| `height` | number | 1080 | Composition height in pixels | | `width` | number | 1920 | Native composition width used for aspect ratio |
| `controls` | boolean | false | Show playback controls overlay | | `height` | number | 1080 | Native composition height used for aspect ratio |
| `autoplay` | boolean | false | Start playing on load | | `controls` | boolean | false | Show playback, scrub, speed, time, and volume controls |
| `loop` | boolean | false | Loop playback | | `autoplay` | boolean | false | Start when the composition is ready |
| `muted` | boolean | false | Mute audio (set to `true` for autoplay in most browsers) | | `loop` | boolean | false | Restart at the end |
| `muted` | boolean | false | Mute audio |
| `volume` | number | 1 | Playback volume from 0 to 1 |
| `poster` | string | — | Image URL to show before first play | | `poster` | string | — | Image URL to show before first play |
| `playback-rate` | number | 1 | Playback speed multiplier | | `playback-rate` | number | 1 | Playback speed multiplier |
| `audio-src` | string | — | Optional primary audio URL to preload in the parent frame |
| `audio-locked` | boolean | false | Force muted playback and hide volume controls |
Player also accepts `shader-capture-scale` and `shader-loading` for previewing
projects that use shader transitions. These are preview controls, not composition
authoring attributes.
## JavaScript API ## JavaScript API
The player mirrors the native `<video>` element API: The main API follows familiar media-player behavior:
```js ```js
const player = document.querySelector('hyperframes-player'); const player = document.querySelector("hyperframes-player");
// Playback
player.play(); player.play();
player.pause(); player.pause();
player.seek(2.5); // seek to 2.5 seconds player.seek(2.5);
// Properties player.currentTime = 5;
player.currentTime; // number — current position in seconds player.playbackRate = 1.5;
player.currentTime = 5; // seek to 5 seconds player.muted = true;
player.duration; // number — total duration
player.paused; // boolean console.log(player.duration, player.paused, player.ready);
player.ready; // boolean — true after composition loads
player.playbackRate; // number — get/set speed
player.muted; // boolean — get/set mute
player.loop; // boolean — get/set loop
``` ```
## Events ## Events
```js ```js
const player = document.querySelector('hyperframes-player'); const player = document.querySelector("hyperframes-player");
player.addEventListener('ready', (e) => { player.addEventListener("ready", (event) => {
console.log('Duration:', e.detail.duration); console.log("Duration:", event.detail.duration);
}); });
player.addEventListener('timeupdate', (e) => { player.addEventListener("timeupdate", (event) => {
console.log('Time:', e.detail.currentTime); console.log("Time:", event.detail.currentTime);
}); });
player.addEventListener('play', () => console.log('Playing'));
player.addEventListener('pause', () => console.log('Paused'));
player.addEventListener('ended', () => console.log('Ended'));
player.addEventListener('error', (e) => console.error(e.detail.message));
``` ```
| Event | Detail | Description | | Event | Detail | Description |
|-------|--------|-------------| | -------------- | ----------------- | ------------------------------------------------------------ |
| `ready` | `{ duration }` | Composition loaded and timeline discovered | | `ready` | `{ duration }` | Composition loaded and timeline discovered |
| `timeupdate` | `{ currentTime }` | Fires during playback (~30fps) | | `timeupdate` | `{ currentTime }` | Playback position changed, approximately 10 times per second |
| `play` | — | Playback started | | `play` | — | Playback started |
| `pause` | — | Playback paused | | `pause` | — | Playback paused |
| `ended` | — | Playback reached end | | `ended` | — | Playback reached end |
| `ratechange` | — | Playback rate changed |
| `volumechange` | — | Volume or muted state changed |
| `scenes` | `{ scenes }` | The runtime reported its scene list |
| `error` | `{ message }` | Load or runtime error | | `error` | `{ message }` | Load or runtime error |
## Framework Examples
### React
```jsx
import '@hyperframes/player';
function VideoPreview({ src }) {
return (
<hyperframes-player
src={src}
controls
style={{ width: '100%', maxWidth: 800 }}
/>
);
}
```
### Vue
```vue
<template>
<hyperframes-player :src="compositionUrl" controls />
</template>
<script setup>
import '@hyperframes/player';
const compositionUrl = './compositions/intro.html';
</script>
```
### Programmatic
```js
import '@hyperframes/player';
const player = document.createElement('hyperframes-player');
player.src = './my-composition/index.html';
player.controls = true;
player.addEventListener('ready', () => player.play());
document.getElementById('player-container').appendChild(player);
```
## Advanced: iframe access ## Advanced: iframe access
The composition runs inside a sandboxed `<iframe>` in the player's Shadow DOM. For most use cases you don't need direct access — the JavaScript API and events above are sufficient. But if you're building an editor, recorder, or custom timeline on top of the player, you'll need to inspect the composition's DOM or read its `__player` / `__timelines` runtime objects. The `iframeElement` getter exposes the inner iframe for these consumers: The composition runs inside an `<iframe>` in the player's Shadow DOM. For most
uses, the JavaScript API and events above are enough. The `iframeElement` getter
exists for same-origin tools that must inspect the composition DOM or connect a
custom editing surface:
```js ```js
const player = document.querySelector('hyperframes-player'); const player = document.querySelector("hyperframes-player");
const iframe = player.iframeElement; const iframe = player.iframeElement;
// Reach into the composition's DOM // Reach into the composition's DOM
iframe.contentDocument.querySelectorAll('[data-composition-id]'); iframe.contentDocument.querySelectorAll("[data-composition-id]");
// Read the runtime (GSAP timelines, element registry, etc.) // Read the runtime (GSAP timelines, element registry, etc.)
iframe.contentWindow.__timelines; iframe.contentWindow.__timelines;
``` ```
This is the canonical way to bridge the player into editor tools like [`@hyperframes/studio`](/packages/studio). The studio exports a `resolveIframe` helper that handles both direct iframe refs and web-component refs: Direct DOM access works only when the composition and the host page are
same-origin. Cross-origin embeds must use the Player API and events.
[`@hyperframes/studio`](/packages/studio) exports `resolveIframe` for consumers
that need to pass the inner iframe to Studio's timeline hooks:
```ts ```ts
import { useTimelinePlayer, resolveIframe } from '@hyperframes/studio'; import { resolveIframe, useTimelinePlayer } from "@hyperframes/studio";
const { iframeRef } = useTimelinePlayer(); const { iframeRef } = useTimelinePlayer();
const player = document.createElement('hyperframes-player'); const player = document.createElement("hyperframes-player");
player.setAttribute('src', src); player.setAttribute("src", src);
container.appendChild(player); container.appendChild(player);
// Forward the inner iframe so useTimelinePlayer can drive play/pause/seek. // Forward the inner iframe so useTimelinePlayer can drive play/pause/seek.
iframeRef.current = resolveIframe(player); iframeRef.current = resolveIframe(player);
``` ```
### React: declarative ref pattern ## How it works
If you prefer JSX over imperative element creation, attach a ref to the web component and resolve the iframe inside an effect: The composition runs in a sandboxed iframe inside the player's Shadow DOM. This
isolates its styles, scales it to the player container, and lets the player
communicate with the HyperFrames runtime through `postMessage`.
```tsx ## Related topics
import '@hyperframes/player';
import type { HyperframesPlayer } from '@hyperframes/player';
import { useTimelinePlayer, resolveIframe } from '@hyperframes/studio';
function StudioPreview({ src }: { src: string }) { - [Edit the same composition with the SDK](/sdk/quickstart)
const { iframeRef, onIframeLoad } = useTimelinePlayer(); - [Use the complete Studio interface](/studio)
const playerRef = useRef<HyperframesPlayer>(null); - [Understand the composition contract](/reference/html-schema)
useEffect(() => {
iframeRef.current = resolveIframe(playerRef.current);
});
return (
<hyperframes-player
ref={playerRef}
src={src}
onLoad={onIframeLoad}
/>
);
}
```
<Warning>
**Common gotcha** — if you pass the `<hyperframes-player>` element itself (not `iframeElement`) into a hook or API that expects an `<iframe>`, every `.contentWindow` / `.contentDocument` access returns `null` because the iframe lives inside the player's Shadow DOM. Timeline seek, play, pause, and DOM inspection all silently no-op. **Always extract `iframeElement` first**, or use `resolveIframe` from `@hyperframes/studio` which handles both iframe and web-component hosts transparently.
</Warning>
## Architecture
The player uses an iframe inside a Shadow DOM container. This provides:
- **Isolation** — composition CSS/JS can't leak into or conflict with your page
- **Security** — iframe sandbox restricts composition capabilities
- **Scaling** — auto-scales the composition to fit the player's container via CSS transforms
The player communicates with the composition via the HyperFrames runtime bridge protocol (`postMessage`). Existing compositions work without modification.
## Controls
When the `controls` attribute is present, a minimal overlay appears at the bottom:
- **Play/Pause** button (left)
- **Scrub bar** with drag support (mouse + touch)
- **Time display** showing current / total duration (right)
- Auto-hides after 3 seconds of inactivity, reappears on hover
+76 -322
View File
@@ -1,371 +1,125 @@
--- ---
title: "@hyperframes/producer" title: "@hyperframes/producer"
description: "Full HTML-to-video rendering pipeline with encoding, audio mixing, and Docker support." description: "Render a HyperFrames project from Node.js."
--- ---
The producer package combines the [engine's](/packages/engine) frame capture with FFmpeg encoding to deliver a complete HTML-to-video rendering pipeline. It supports MP4 (h264) and WebM (VP9 with alpha transparency), and handles runtime injection, readiness gates, audio mixing, and optional Docker-based deterministic rendering. `@hyperframes/producer` owns the complete render pipeline: compile the project,
capture its frames, encode the video, and mix its audio.
Use it when rendering is part of your own Node.js service or job runner. For a
local script or terminal workflow, use [`npx hyperframes render`](/developers/cli)
instead.
```bash ```bash
npm install @hyperframes/producer npm install @hyperframes/producer
``` ```
## When to Use ## Render one project
**Use `@hyperframes/producer` when you need to:** `createRenderJob()` describes the render. `executeRenderJob()` receives that
- Render compositions to MP4 or WebM programmatically from Node.js (e.g., in a backend service or CI pipeline) job, the project directory, and the output path.
- Build a custom rendering service with fine-grained control over the pipeline
- Run visual regression tests against golden baselines
- Benchmark render performance across different configurations
**Use a different package if you want to:** ```ts
- Render from the command line without writing code — use the [CLI](/packages/cli) (`npx hyperframes render`) import {
- Preview compositions in the browser — use the [CLI](/packages/cli) or [studio](/packages/studio) createRenderJob,
- Capture frames without encoding — use the [engine](/packages/engine) executeRenderJob,
- Lint or parse composition HTML — use [core](/packages/core) } from "@hyperframes/producer";
<Tip>
If you are building a web application or script that just needs to render a video, the [CLI](/packages/cli) is the fastest path. The producer package is for when you need programmatic control inside Node.js.
</Tip>
## What It Does
The producer orchestrates the full render pipeline:
<Steps>
<Step title="Load the composition HTML">
Reads your `index.html` and any referenced sub-compositions.
</Step>
<Step title="Inject the Hyperframes runtime">
Adds the runtime script that manages timeline seeking, clip lifecycle, and media playback.
</Step>
<Step title="Wait for readiness gates">
Polls for `window.__playerReady` and `window.__renderReady` to ensure all assets (fonts, images, video) are loaded before capture begins.
</Step>
<Step title="Capture frames via the engine">
Uses the [engine's](/packages/engine) BeginFrame pipeline to capture each frame as a pixel buffer.
</Step>
<Step title="Encode to MP4 or WebM via FFmpeg">
Pipes frame buffers into FFmpeg with the selected quality preset. MP4 uses h264; WebM uses VP9 with alpha transparency support.
</Step>
<Step title="Mix audio tracks">
Extracts audio from video clips and audio elements, applies `data-volume` and `data-media-start` offsets, and mixes them into the final MP4.
</Step>
</Steps>
## Programmatic Usage
The producer uses a two-step API: create a render job configuration, then execute it.
```typescript
import { createRenderJob, executeRenderJob } from '@hyperframes/producer';
const job = createRenderJob({ const job = createRenderJob({
fps: 30, fps: 30,
quality: 'standard', quality: "standard",
format: "mp4",
}); });
await executeRenderJob(job, './my-video', './output.mp4'); await executeRenderJob(
job,
"./my-hyperframes-project",
"./renders/video.mp4",
(currentJob, message) => {
console.log(
`${Math.round(currentJob.progress)}% ${message}`,
);
},
);
``` ```
### Render Configuration The entry file defaults to `index.html`. Set `entryFile` when the project has a
different entry composition.
```typescript
import { createRenderJob } from '@hyperframes/producer';
```ts
const job = createRenderJob({ const job = createRenderJob({
fps: 30, // integer, or { num: 30000, den: 1001 } for NTSC fps: { num: 30000, den: 1001 },
quality: 'standard', // 'draft', 'standard', or 'high' quality: "high",
format: 'mp4', // 'mp4', 'webm', 'mov', 'gif', or 'png-sequence' entryFile: "compositions/launch.html",
workers: 4, // Parallel render workers (1-24, or omit for auto)
useGpu: false, // GPU-accelerated encoding
debug: false, // Debug logging
}); });
``` ```
#### WebM with Transparency ## Choose an output
Set `format: 'webm'` to render with a transparent background using VP9 alpha: | Format | Best for | Audio |
| --- | --- | --- |
| `mp4` | Default delivery and web playback | AAC |
| `webm` | Transparent video for the web | Opus |
| `mov` | Transparent ProRes 4444 for an editor | AAC |
| `gif` | Small silent previews | None |
| `png-sequence` | Lossless frames for another pipeline | AAC sidecar when needed |
```typescript HDR output is available for MP4 through `hdrMode`. Transparent formats render
in SDR because HDR and alpha are not one supported output path.
```ts
const job = createRenderJob({ const job = createRenderJob({
fps: 30, fps: 30,
quality: 'standard', quality: "high",
format: 'webm', format: "mp4",
hdrMode: "auto",
}); });
await executeRenderJob(job, './my-overlay', './overlay.webm');
``` ```
When `format: 'webm'`: See [HDR rendering](/guides/hdr) for the source and delivery constraints.
- Frames are captured as PNG (preserves alpha channel)
- Chrome's page background is set to transparent via CDP
- FFmpeg encodes with VP9 + `yuva420p` pixel format
- Audio is encoded as Opus (instead of AAC for MP4)
#### HDR Output ## Cancel a render
Set `hdrMode` to control HDR behavior. The producer probes every video and image source for BT.2020 / PQ / HLG color tagging — if any HDR source is found and the mode allows it, the output uses H.265 10-bit BT.2020 with HDR10 static metadata. SDR-only compositions are unaffected. Pass an abort signal to the final argument:
```typescript ```ts
const job = createRenderJob({ const controller = new AbortController();
fps: 30,
quality: 'standard',
format: 'mp4',
hdrMode: 'auto', // 'auto' | 'force-hdr' | 'force-sdr'
});
await executeRenderJob(job, './my-video', './output.mp4'); await executeRenderJob(
job,
"./my-hyperframes-project",
"./renders/video.mp4",
undefined,
controller.signal,
);
// Call controller.abort() from your cancellation path.
``` ```
When `hdrMode` is `'auto'` or `'force-hdr'`: An aborted render throws `RenderCancelledError`. Correctness warnings can also
- Sources are probed via `ffprobe`; PQ takes precedence over HLG when both are present block a render when `strictness: "strict"` is enabled.
- HDR videos and images are extracted as 16-bit linear-light pixels and composited natively
- SDR DOM overlays are converted from sRGB → BT.2020 before being layered on top
- Output uses `libx265` with `yuv420p10le` and HDR10 mastering / content-light-level metadata
- `format` must be `'mp4'` — `'mov'` and `'webm'` fall back to SDR
- HDR `<img>` extraction support is **still images only**; animated GIF inputs are prepared as timeline-synced video before render
For full details on source requirements, fallback rules, and verification, see [HDR Rendering](/guides/hdr). ## Build a render service
### Progress Callbacks The package exports a Hono application and server helpers:
```typescript ```ts
import type { ProgressCallback, RenderStatus } from '@hyperframes/producer'; import { startServer } from "@hyperframes/producer";
const onProgress: ProgressCallback = (status: RenderStatus) => {
console.log(`Status: ${status}`);
// Statuses: "queued" | "preprocessing" | "rendering" | "encoding"
// | "assembling" | "complete" | "failed" | "cancelled"
};
```
### Cancellation
```typescript
import { RenderCancelledError } from '@hyperframes/producer';
try {
await executeRenderJob(job);
} catch (err) {
if (err instanceof RenderCancelledError) {
console.log(`Cancelled: ${err.reason}`);
// reason: "user_cancelled" | "timeout" | "aborted"
}
}
```
## HTTP Server
The producer includes a built-in HTTP server for running as a rendering service:
```typescript
import { startServer } from '@hyperframes/producer/server';
await startServer({ port: 8080 }); await startServer({ port: 8080 });
``` ```
### Server Endpoints For larger workloads, the `@hyperframes/producer/distributed` entry exposes the
three rendering activities—`plan`, `renderChunk`, and `assemble`. Your
orchestrator remains responsible for dispatch, retries, storage, and networking.
| Method | Path | Description | ## Related topics
|--------|------|-------------|
| `POST` | `/render` | Blocking render — returns JSON result |
| `POST` | `/render/stream` | Streaming render with Server-Sent Events |
| `POST` | `/lint` | Lint a composition for issues |
| `GET` | `/health` | Health check |
| `GET` | `/outputs/:token` | Download a rendered MP4 |
For custom server integration, use the lower-level handlers:
```typescript
import { createRenderHandlers, createProducerApp } from '@hyperframes/producer/server';
// Get individual request handlers
const handlers = createRenderHandlers(options);
// Or get a full Hono app
const app = createProducerApp(options);
```
## Docker Rendering
For deterministic output, the producer can render inside a Docker container with a pinned Chrome version and font set. This guarantees identical output across machines — critical for CI pipelines and production services.
```bash
# Via the CLI (recommended)
npx hyperframes render --docker --output output.mp4
```
<Info>
Docker mode requires Docker to be installed and running. Run `npx hyperframes doctor` to verify your environment. See [Deterministic Rendering](/concepts/determinism) for details on what makes Docker mode deterministic.
</Info>
## Quality Presets
| Preset | Resolution | Encoding | Use Case |
|--------|-----------|----------|----------|
| `draft` | Original | Fast CRF | Quick iteration, previewing edits |
| `standard` | Original | Balanced CRF | Production renders, sharing |
| `high` | Original | High-quality CRF | Final delivery, archival |
## GPU Encoding
The producer supports hardware-accelerated encoding for faster renders:
| Platform | Encoder | Selection |
|----------|---------|-----------|
| NVIDIA | NVENC | Auto-detected |
| macOS | VideoToolbox | Auto-detected |
| Linux | VAAPI | Auto-detected |
| Intel | QSV | Auto-detected |
| AMD on Windows | AMF | Auto-detected |
When GPU encoding is enabled, Hyperframes detects the available FFmpeg hardware encoder automatically. To check your system's capabilities:
```bash
npx hyperframes doctor
```
The CLI enables local Chrome/WebGL GPU capture automatically and supports `--no-browser-gpu` as an opt-out. When using the producer API directly, pass an engine config override:
```typescript
import { resolveConfig } from '@hyperframes/producer';
const job = createRenderJob({
fps: 30,
quality: 'standard',
producerConfig: resolveConfig({ browserGpuMode: 'hardware' }),
});
```
## Additional Exports
The producer also re-exports key engine functionality for convenience:
| Export | Description |
|--------|-------------|
| `createCaptureSession()` | Create a frame capture session |
| `initializeSession()` | Initialize session with a composition |
| `captureFrame()` / `captureFrameToBuffer()` | Capture individual frames |
| `closeCaptureSession()` | Clean up a capture session |
| `getCompositionDuration()` | Get total composition duration |
| `getCapturePerfSummary()` | Get capture performance metrics |
| `createFileServer()` | Create an HTTP file server for serving assets |
| `createVideoFrameInjector()` | Create a video frame injector for page |
| `resolveConfig()` / `DEFAULT_CONFIG` | Producer configuration |
| `createConsoleLogger()` / `defaultLogger` | Logging utilities |
| `quantizeTimeToFrame()` | Convert time to frame boundary |
| `resolveRenderPaths()` | Resolve render directory paths |
| `prepareHyperframeLintBody()` / `runHyperframeLint()` | Linting utilities |
## Logging
The producer ships a small pluggable logger so callers can inject Pino, Winston, or any structured backend without taking a dependency on it.
```ts
export type LogLevel = "error" | "warn" | "info" | "debug";
export interface ProducerLogger {
error(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
info(message: string, meta?: Record<string, unknown>): void;
debug(message: string, meta?: Record<string, unknown>): void;
isLevelEnabled?(level: LogLevel): boolean;
}
```
`createConsoleLogger(level)` returns a console-backed implementation that filters by level and JSON-stringifies the optional `meta` object. `defaultLogger` is the singleton at `level="info"`.
### Skipping expensive metadata in hot paths
`isLevelEnabled` is **optional** so existing custom loggers keep working unchanged. When you build a non-trivial meta object in a hot loop just to attach to a debug log, gate the construction with the nullish-coalescing pattern so production runs (`level=info`) pay nothing while loggers without the method behave exactly as before:
```ts
// Inside a per-frame loop in the encode pipeline:
if (i % 30 === 0 && (log.isLevelEnabled?.("debug") ?? true)) {
const hdrEl = stackingInfo.find((e) => e.isHdr);
log.debug("[Render] HDR layer composite frame", {
frame: i,
time: time.toFixed(2),
hdrElement: hdrEl
? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width }
: null,
stackingCount: stackingInfo.length,
activeTransition: activeTransition?.shader,
});
}
```
The `?? true` fallback means callers using a custom logger that does not implement `isLevelEnabled` continue to build and pass the meta object — the optimization is opt-in for logger implementations that want it.
## Regression Testing
The producer includes a regression harness for comparing render output against golden baselines. This is useful for catching visual regressions when changing the runtime, engine, or rendering pipeline.
```bash
cd packages/producer
# Build the test Docker image
bun run docker:build:test
# Run regression tests (compares output against golden baselines)
bun run docker:test
# Regenerate golden baselines after intentional changes
bun run docker:test:update
```
## Benchmarking
Find optimal render settings for your hardware:
```bash
# Via the CLI
npx hyperframes benchmark
# Directly from the producer package
cd packages/producer
bun run benchmark
```
The benchmark runs several compositions with different quality and FPS settings and reports timing for each combination.
## External assets (files outside `projectDir`)
A composition can reference absolute paths to assets outside the project
directory — a local voiceover in `~/Downloads`, a shared-drive image, a
generated fixture at an absolute path. The producer handles these by:
1. **Detection.** During compilation, the HTML compiler walks every
`[src]` / `[href]` and every `url(...)` in `<style>`. A path that
resolves to a file outside `projectDir` is collected into an
`externalAssets` map.
2. **Sanitised keys.** Each absolute path is converted into a safe,
cross-platform relative key prefixed with `hf-ext/`. Windows
drive-letter colons are stripped (`D:\foo\x.wav` → `hf-ext/D/foo/x.wav`)
so that `path.join(compileDir, key)` stays inside the compile
directory on every OS.
3. **Copy + rewrite.** The orchestrator copies the file under
`<compileDir>/hf-ext/...` and the HTML is rewritten to point at the
sanitised key. The file server then serves both project-internal and
external assets from the same root.
The containment check uses `path.relative()` rather than a hardcoded
separator, so external assets work identically on macOS, Linux, and
Windows. See `packages/producer/src/utils/paths.ts` for the helpers.
## Related Packages
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/packages/cli"> <Card title="Choose rendering infrastructure" icon="server" href="/deploy/overview">
Command-line interface that wraps the producer for rendering, previewing, and more. Decide between local, server, Temporal, AWS, or another adapter.
</Card> </Card>
<Card title="Engine" icon="gear" href="/packages/engine"> <Card title="@hyperframes/engine" icon="gear" href="/packages/engine">
The low-level capture pipeline that the producer uses to grab frames. Use the lower-level capture and encoding primitives.
</Card>
<Card title="Core" icon="cube" href="/packages/core">
Types, runtime, and linter that the producer depends on.
</Card>
<Card title="Studio" icon="palette" href="/packages/studio">
Visual editor for building compositions before rendering with the producer.
</Card> </Card>
</CardGroup> </CardGroup>
+12 -5
View File
@@ -12,6 +12,7 @@ npm install @hyperframes/sdk
## When to Use ## When to Use
**Use `@hyperframes/sdk` when you need to:** **Use `@hyperframes/sdk` when you need to:**
- Build a custom composition editor in your own application - Build a custom composition editor in your own application
- Let an agent inspect and edit composition HTML without driving a browser UI - Let an agent inspect and edit composition HTML without driving a browser UI
- Apply batch text, style, timing, asset, variable, or animation edits from code - Apply batch text, style, timing, asset, variable, or animation edits from code
@@ -20,23 +21,29 @@ npm install @hyperframes/sdk
- Layer sparse overrides on top of a reusable base composition template - Layer sparse overrides on top of a reusable base composition template
**Use a different package if you want to:** **Use a different package if you want to:**
- Render compositions to MP4 or WebM - use the [CLI](/packages/cli) or [producer](/packages/producer) - Render compositions to MP4 or WebM - use the [CLI](/packages/cli) or [producer](/packages/producer)
- Preview and edit visually out of the box - use the [CLI](/packages/cli) (`npx hyperframes preview`) or [studio](/packages/studio) - Preview and edit visually out of the box - use the [CLI](/packages/cli) (`npx hyperframes preview`) or [studio](/packages/studio)
- Parse, lint, or generate low-level composition HTML - use [core](/packages/core) - Parse composition HTML - use [parsers](/packages/parsers)
- Lint composition HTML or projects - use [lint](/packages/lint)
- Compile or generate low-level composition HTML - use [core](/packages/core)
- Capture frames from a headless browser - use [engine](/packages/engine) - Capture frames from a headless browser - use [engine](/packages/engine)
<Tip> <Tip>
The SDK is the right layer for product integrations and agents that need structured edits. The CLI and Studio are user-facing tools built around the same composition format; the SDK is the editing engine you embed behind your own UI or automation. The SDK is the right layer for product integrations and agents that need structured edits. The CLI
and Studio are user-facing tools built around the same composition format; the SDK is the editing
engine you embed behind your own UI or automation.
</Tip> </Tip>
<Note> <Note>
This page is the package overview. For the full API — guides and a complete reference for every method, operation, type, and adapter — see the [**SDK**](/sdk/overview) tab. This page is the package overview. Start with the [SDK quickstart](/sdk/quickstart), then use the
focused guides and reference pages for exact methods, operations, types, and adapters.
</Note> </Note>
## Package Exports ## Package Exports
| Import | Description | | Import | Description |
|--------|-------------| | ------------------------------------ | ------------------------------------------------------------------------- |
| `@hyperframes/sdk` | Main editing API, types, memory/headless adapters, iframe preview adapter | | `@hyperframes/sdk` | Main editing API, types, memory/headless adapters, iframe preview adapter |
| `@hyperframes/sdk/adapters/memory` | In-memory persistence adapter for tests, demos, and ephemeral sessions | | `@hyperframes/sdk/adapters/memory` | In-memory persistence adapter for tests, demos, and ephemeral sessions |
| `@hyperframes/sdk/adapters/fs` | Node.js filesystem persistence adapter with version history | | `@hyperframes/sdk/adapters/fs` | Node.js filesystem persistence adapter with version history |
@@ -209,7 +216,7 @@ const comp = await openComposition(html, {
For browser integrations, `createIframePreviewAdapter()` bridges the SDK to a same-origin composition iframe so hit-testing, selection, and draft preview updates can stay outside the model mutation path. For browser integrations, `createIframePreviewAdapter()` bridges the SDK to a same-origin composition iframe so hit-testing, selection, and draft preview updates can stay outside the model mutation path.
## Related Packages ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="@hyperframes/core" icon="cube" href="/packages/core"> <Card title="@hyperframes/core" icon="cube" href="/packages/core">
+1 -1
View File
@@ -131,7 +131,7 @@ Browser previews pre-capture transition samples and cache matching snapshots in
During producer renders, shader transitions use a deterministic page-side compositor instead of preview-time snapshot caching. That keeps frame capture seek-driven and avoids depending on wall-clock playback. During producer renders, shader transitions use a deterministic page-side compositor instead of preview-time snapshot caching. That keeps frame capture seek-driven and avoids depending on wall-clock playback.
## Related Packages ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="@hyperframes/producer" icon="video" href="/packages/producer"> <Card title="@hyperframes/producer" icon="video" href="/packages/producer">
+53 -80
View File
@@ -1,99 +1,72 @@
--- ---
title: "@hyperframes/studio-server" title: "@hyperframes/studio-server"
description: "The studio preview/editor backend — a mountable Hono API, extracted from core." description: "Mount the Studio project and preview API in your own server."
--- ---
The studio-server package is the HTTP backend that powers studio preview and editing — project routes, file serving, preview bundling, thumbnails, and source-mutation helpers. It was extracted from `@hyperframes/core/studio-api` into a dedicated package so an embedder can mount the studio backend **without** depending on core's full surface, and so core no longer ships a web server it doesn't need at render time. `@hyperframes/studio-server` is the backend used by Studio. It serves projects
and previews, applies source mutations, creates thumbnails, and starts render
jobs.
Most users do not need it. The CLI wires the server and Studio together when
you run `npx hyperframes preview`. Use this package only when your own
application must host the Studio backend.
```bash ```bash
npm install @hyperframes/studio-server npm install @hyperframes/studio-server
``` ```
## When to Use ## Mount the API
<Tip> `createStudioApi()` returns a Hono application. The adapter is the boundary
**Most users do not need this package directly.** The [CLI](/packages/cli) (`npx hyperframes preview`) and [studio](/packages/studio) wire it up for you. Reach for it when you're **embedding** the studio backend into your own server. between Studio and your project storage, bundler, linter, and renderer.
</Tip>
**Use `@hyperframes/studio-server` when you need to:** ```ts
- Mount the studio preview/editing API into an existing Node/Hono server import type { Hono } from "hono";
- Serve project files and bundled preview HTML to a custom frontend
- Drive source mutations (manual edits, draft markers) from your own tooling
<Info>
`@hyperframes/core/studio-api` still resolves (via a back-compat re-export stub), so existing imports keep working. New code should import from `@hyperframes/studio-server` directly.
</Info>
## Package Exports
| Import | Description |
|--------|-------------|
| `@hyperframes/studio-server` | `createStudioApi`, helpers, types |
| `@hyperframes/studio-server/source-mutation` | Source mutation utilities |
| `@hyperframes/studio-server/screenshot-clip` | Element screenshot-clip geometry |
| `@hyperframes/studio-server/manual-edits-render-script` | Manual-edits render body script |
| `@hyperframes/studio-server/studio-motion-render-script` | Studio motion render body script |
| `@hyperframes/studio-server/draft-markers` | Draft gesture-marker attributes |
| `@hyperframes/studio-server/finite-mutation` | Finite-mutation safety checks |
## Mounting the API
`createStudioApi` returns a [Hono](https://hono.dev) app you can mount into any server. You supply a `StudioApiAdapter` that tells the API how to resolve projects, bundle preview HTML, and lint:
```typescript
import { createStudioApi } from '@hyperframes/studio-server';
import type { StudioApiAdapter, ResolvedProject } from '@hyperframes/studio-server';
const adapter: StudioApiAdapter = {
listProjects: () => projects,
resolveProject: (id) => projectsById.get(id) ?? null,
bundle: async (projectDir) => bundleToSingleHtml(projectDir),
lint: (html, opts) => lintHyperframeHtml(html, opts),
runtimeUrl: '/hyperframe-runtime.js',
rendersDir: (project) => join(project.dir, 'renders'),
startRender: async (opts) => startRenderJob(opts),
};
const api = createStudioApi(adapter); // → Hono app
// Mount under /api in your own Hono server
app.route('/api', api);
```
The adapter is the seam between the framework-agnostic route logic and your storage / bundling / lint implementation — the [CLI](/packages/cli) supplies a filesystem-backed adapter, but you can back it with anything.
## Helpers
```typescript
import { import {
createProjectSignature, // cache key for a project's files createStudioApi,
isSafePath, // path-traversal guard type StudioApiAdapter,
walkDir, } from "@hyperframes/studio-server";
getMimeType,
buildSubCompositionHtml, export function mountStudioApi(
getElementScreenshotClip, app: Hono,
} from '@hyperframes/studio-server'; adapter: StudioApiAdapter,
import type { ) {
ResolvedProject, app.route("/api", createStudioApi(adapter));
RenderJobState, }
LintResult,
ScreenshotClip,
} from '@hyperframes/studio-server';
``` ```
## Related Packages A `StudioApiAdapter` supplies:
- project listing and resolution;
- composition bundling and linting;
- render output storage and job startup;
- optional thumbnails, sessions, registry installation, and background removal.
Use the interface exported by your installed version as the exact contract. It
changes as Studio gains capabilities.
## Other entry points
| Import | Purpose |
| --- | --- |
| `@hyperframes/studio-server/source-mutation` | Apply guarded changes to composition source |
| `@hyperframes/studio-server/screenshot-clip` | Calculate element screenshot geometry |
| `@hyperframes/studio-server/manual-edits-render-script` | Reapply Studio manual edits during render |
| `@hyperframes/studio-server/studio-motion-render-script` | Reapply Studio motion edits during render |
| `@hyperframes/studio-server/draft-markers` | Work with draft gesture markers |
| `@hyperframes/studio-server/finite-mutation` | Guard source mutations against non-finite values |
| `@hyperframes/studio-server/media-proxy-preview` | Support proxied media in preview |
`@hyperframes/core/studio-api` remains a deprecated compatibility re-export.
New integrations should import from `@hyperframes/studio-server`.
## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Studio" icon="palette" href="/packages/studio"> <Card title="@hyperframes/studio" icon="palette" href="/packages/studio">
The browser editor UI this server backs. Embed lower-level Studio interface components.
</Card> </Card>
<Card title="@hyperframes/parsers" icon="code" href="/packages/parsers"> <Card title="Studio guide" icon="display" href="/studio">
The HTML + GSAP parsing layer it builds on. Use the complete editor that ships with HyperFrames.
</Card>
<Card title="@hyperframes/core" icon="cube" href="/packages/core">
Types and runtime; re-exports the studio API for back-compat.
</Card>
<Card title="CLI" icon="terminal" href="/packages/cli">
`npx hyperframes preview` wires this server up for you.
</Card> </Card>
</CardGroup> </CardGroup>
+87 -221
View File
@@ -1,256 +1,122 @@
--- ---
title: "@hyperframes/studio" title: "@hyperframes/studio"
description: "Visual composition editor with live preview, timeline view, and hot reload." description: "React components and hooks used to build the HyperFrames Studio editing interface."
--- ---
The studio package provides a browser-based visual editor for creating and previewing Hyperframes compositions. It gives you a real-time preview of your video, a visual timeline of all clips, and player controls for seeking and playback — all updating live as you edit your HTML. `@hyperframes/studio` contains the React application, editor components, player,
timeline, and hooks that power HyperFrames Studio.
```bash Most people should not install this package directly. To open a project in the
npm install @hyperframes/studio complete Studio application, run:
```
## When to Use
**Use `@hyperframes/studio` when you need to:**
- Build a custom composition editor UI (e.g., embedded in your own web application)
- Integrate the Hyperframes preview player into a larger product
- Extend the editor with custom panels, toolbars, or integrations
**Use a different package if you want to:**
- Preview compositions during development — use the [CLI](/packages/cli) (`npx hyperframes preview`), which launches the studio for you
- Render compositions to MP4 — use the [CLI](/packages/cli) or [producer](/packages/producer)
- Capture frames programmatically — use the [engine](/packages/engine)
<Tip>
**For most development workflows, you do not need to install the studio directly.** Running `npx hyperframes preview` starts the studio automatically with hot reload. Install `@hyperframes/studio` only if you are embedding the editor into your own application.
</Tip>
## Running the Studio
### Via the CLI (recommended)
```bash ```bash
npx hyperframes preview npx hyperframes preview
``` ```
This starts the studio development server, opens your composition in the browser, and watches for file changes. This is the easiest way to get a live preview. Install the package only when you are extending Studio or building an editor that
uses its lower-level parts:
### From the monorepo
```bash ```bash
# From the root npm install @hyperframes/studio react react-dom zustand
bun run dev
# Or target the studio package directly
bun run --filter @hyperframes/studio dev
``` ```
The studio dev server listens on `http://localhost:5190` (see `packages/studio/vite.config.ts` `server.port`). <Warning>
The exported components are building blocks, not a drop-in embedded editor. Components such as
`EditorShell`, `NLEPreview`, `Timeline`, `PropertyPanel`, and `FileTree` require project state,
callbacks, or Studio contexts. Use `StudioApp` when you need the complete application, or inspect
the component types before composing a custom host.
</Warning>
## Package Exports When you run Studio from the monorepo, its dev server listens on `http://localhost:5190`.
The studio has two entry points: ## Entry points
| Import | Description | | Import | Contains |
|--------|-------------| | ------------------------------------- | ---------------------------------------------------------- |
| `@hyperframes/studio` | React components, hooks, and types | | `@hyperframes/studio` | React components, hooks, utilities, and their public types |
| `@hyperframes/studio/tailwind-preset` | Tailwind CSS preset for studio styling | | `@hyperframes/studio/tailwind-preset` | Tailwind preset used by Studio |
Peer dependencies: `react` (19), `react-dom` (19), `zustand` (4 or 5). Peer dependencies are React 19, React DOM 19, and Zustand 4 or 5.
## Components ## Main exports
### Layout ### Complete application
| Export | Use |
| ----------- | ------------------------------- |
| `StudioApp` | The complete Studio application |
### Editor structure
| Export | Use |
| ----------------------- | ---------------------------------------- |
| `EditorShell` | Preview, side panels, and timeline shell |
| `NLEPreview` | Composition preview surface |
| `CompositionBreadcrumb` | Navigation through nested compositions |
| `SourceEditor` | Source editor |
| `PropertyPanel` | Inspector for the selected element |
| `FileTree` | Project file browser |
### Playback and timeline
| Export | Use |
| ---------------------- | -------------------------------------- |
| `Player` | Composition iframe and playback bridge |
| `PlayerControls` | Playback, seeking, and frame controls |
| `Timeline` | Timeline editing surface |
| `VideoThumbnail` | Video clip thumbnail |
| `CompositionThumbnail` | Nested-composition thumbnail |
### Hooks and utilities
| Export | Use |
| ----------------------------------------------------------- | ----------------------------------------------------- |
| `useTimelinePlayer` | Read and control the shared player state |
| `usePlayerStore` | Access the Zustand player store |
| `useElementPicker` | Select and edit an element through the preview iframe |
| `resolveSourceFile`, `applyPatch` | Locate and patch source |
| `parseStyleString`, `mergeStyleIntoTag`, `findElementBlock` | Work with inline source styles and elements |
The package also exports `CompositionLevel`, `TimelineElement`, `PickedElement`,
and `PatchOperation`.
## How Studio stays connected to a project
1. Studio loads the composition in an iframe with the HyperFrames runtime.
2. Player controls communicate with that runtime to play, pause, and seek.
3. The editor and timeline send changes through the Studio API.
4. The Studio server writes those changes back to the project files.
5. External file changes refresh the preview while keeping the current position
when possible.
Preview and render use the same seekable composition contract. Preview still
depends on the browser playing in real time, while render captures frames one at a
time. A heavy project can therefore stutter during preview and still render every
frame.
## Tailwind preset
```typescript ```typescript
import { NLELayout, NLEPreview, CompositionBreadcrumb } from '@hyperframes/studio'; import studioPreset from "@hyperframes/studio/tailwind-preset";
import type { CompositionLevel } from '@hyperframes/studio';
// Main NLE (Non-Linear Editor) layout container
<NLELayout>
{/* Preview, timeline, and editor panels */}
</NLELayout>
// Preview panel
<NLEPreview />
// Breadcrumb navigation for nested compositions
<CompositionBreadcrumb levels={levels} />
```
### Player & Timeline
```typescript
import {
Player,
PlayerControls,
Timeline,
} from '@hyperframes/studio';
import type { TimelineElement } from '@hyperframes/studio';
// Embed the preview player
<Player />
// Playback controls (play, pause, seek, frame-step)
<PlayerControls />
// Timeline editor with scrubber
<Timeline />
```
### Editor Components
```typescript
import { SourceEditor, PropertyPanel, FileTree } from '@hyperframes/studio';
// Code editor (CodeMirror-based) for HTML, CSS, and JavaScript
<SourceEditor />
// Property inspector for selected elements
<PropertyPanel />
// Project file browser
<FileTree />
```
### Full Application
```typescript
import { StudioApp } from '@hyperframes/studio';
// The complete studio application (wraps all components)
<StudioApp />
```
## Hooks
### `useTimelinePlayer`
Manages player state and playback control:
```typescript
import { useTimelinePlayer } from '@hyperframes/studio';
const player = useTimelinePlayer();
// player.play(), player.pause(), player.togglePlay(), player.seek(time)
// player.refreshPlayer(), player.saveSeekPosition(), player.resetPlayer()
```
### `usePlayerStore`
Zustand store for player state:
```typescript
import { usePlayerStore, liveTime, formatTime } from '@hyperframes/studio';
const store = usePlayerStore();
// Access current time, duration, playing state, etc.
// Format time for display
const display = formatTime(liveTime.current);
```
### `useElementPicker`
Element selection from the preview:
```typescript
import { useElementPicker } from '@hyperframes/studio';
const picker = useElementPicker(iframeRef);
// picker.isPickMode, picker.pickedElement
// picker.enablePick(), picker.disablePick(), picker.clearPick()
// picker.setStyle(prop, value), picker.setDataAttr(name, value), picker.setTextContent(text)
```
## Features
### Live Preview
The studio renders your composition in an iframe using the Hyperframes runtime. What you see in the preview is exactly what will be captured during rendering — the same runtime code, the same seek logic, the same clip lifecycle.
Changes to your HTML are picked up automatically through hot reload, so you can edit `index.html` in your editor and see the result in the browser within milliseconds.
<Note>
The *visual* output of preview matches render exactly. Real-time *playback smoothness* depends on your hardware, because preview actually plays the composition in your browser at 30/60fps. Render doesn't have that constraint — it captures each frame individually via a seek-driven pipeline, so expensive frames make the render slower but never drop. If you see stutter in preview but the rendered mp4 is clean, that's expected. See [Performance](/guides/performance) for the patterns that most often cause it.
</Note>
### Timeline View
The timeline panel provides a visual representation of your composition's structure:
- Each clip appears as a colored bar on its track
- Bar position and width reflect `data-start` and `data-duration`
- Visually higher rows render in front; lower rows render underneath
- Relative timing references (e.g., `data-start="intro"`) are resolved and displayed as absolute positions
This makes it easy to understand the temporal structure of complex compositions with many overlapping clips.
### Timeline Editing
The timeline supports move and trim actions that persist directly back into your HTML source.
For a full breakdown of:
- what timeline editing can do today
- how each action maps to `data-start`, `data-duration`, `data-track-index`, and `z-index`
- which clip types support start trim
- current limitations and mental models
see [Timeline Editing](/guides/timeline-editing).
### Player Controls
The studio includes a full set of playback controls:
- **Play / Pause** — start and stop playback
- **Seek** — click anywhere on the timeline to jump to that point
- **Scrub** — drag the playhead to scrub through the composition frame by frame
- **Frame step** — advance or rewind one frame at a time for precise positioning
### Hot Reload
File changes are detected and applied without restarting the server. The preview maintains its current playback position when possible, so you can tweak an animation at the 5-second mark without having to seek back to it after every save.
## Architecture
The studio is a React application with the following structure:
1. **Iframe preview** — your composition HTML is loaded in an isolated iframe with the Hyperframes runtime injected. This ensures the preview uses the same rendering path as production.
2. **Runtime bridge** — the studio communicates with the iframe via `postMessage` to control playback (play, pause, seek) and receive state updates (current time, duration, readiness).
3. **Timeline component** — parses the composition using `@hyperframes/core` to extract clip timing data and renders the visual timeline panel.
4. **File watcher** — a development server (Vite-based) watches your project files and triggers hot module replacement when changes are detected.
## Tailwind CSS Preset
The studio exports a Tailwind CSS preset for consistent styling:
```typescript
// tailwind.config.ts
import studioPreset from '@hyperframes/studio/tailwind-preset';
export default { export default {
presets: [studioPreset], presets: [studioPreset],
// ... your config
}; };
``` ```
## Related Packages ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/packages/cli"> <Card title="Use Studio" icon="pen-to-square" href="/studio">
Launches the studio via `npx hyperframes preview` — the easiest way to preview compositions. Learn the complete editing workflow.
</Card> </Card>
<Card title="Core" icon="cube" href="/packages/core"> <Card title="Studio server" icon="server" href="/packages/studio-server">
Types, parsing, and runtime that the studio uses for preview and timeline rendering. Connect project files, media, linting, and render jobs.
</Card> </Card>
<Card title="Producer" icon="film" href="/packages/producer"> <Card title="Player" icon="circle-play" href="/packages/player">
Renders the compositions you build in the studio to finished MP4 files. Embed playback without the editing interface.
</Card> </Card>
<Card title="Engine" icon="gear" href="/packages/engine"> <Card title="SDK" icon="code" href="/sdk/quickstart">
The capture engine that powers production rendering of your compositions. Query and edit compositions without Studio.
</Card> </Card>
</CardGroup> </CardGroup>
+262 -223
View File
@@ -1,270 +1,309 @@
--- ---
title: HTML Schema Reference title: "HTML schema reference"
description: "Complete reference for authoring Hyperframes HTML compositions." description: "The current contract for a HyperFrames composition."
--- ---
This is the full schema reference for Hyperframes compositions. For a gentler introduction, see [Compositions](/concepts/compositions) and [Data Attributes](/concepts/data-attributes). HyperFrames uses normal HTML and CSS for appearance. A small set of attributes
declares the frame size, duration, clips, media, and nested compositions.
## Overview For a gentler explanation, start with [Compositions](/concepts/compositions) and
[Data attributes](/concepts/data-attributes).
Hyperframes uses HTML as the source of truth for describing a video: ## Minimal composition
- **HTML clips** = video, image, audio, composition
- **[Data attributes](/concepts/data-attributes)** = timing, metadata, styling
- **CSS** = positioning and appearance
- **GSAP timeline** = animations and playback sync (see [GSAP Animation](/guides/gsap-animation))
## Framework-Managed Behavior
The framework reads data attributes and automatically manages:
- **Primitive clip timeline entries** — reads `data-start`, `data-duration`, and `data-track-index` from clips and adds them to the GSAP timeline
- **Media playback** (play, pause, seek) for `<video>` and `<audio>`
- **Clip lifecycle** — clips are mounted/unmounted based on `data-start` and `data-duration`
- **Timeline synchronization** — keeps media in sync with the GSAP master timeline
- **Media loading** — waits for all media to load before resolving timing
Mounting/unmounting controls **presence**, not appearance. Transitions (fade in, slide in) are animated in scripts.
<Warning>
Do not manually call `video.play()`, `video.pause()`, set `audio.currentTime`, or mount/unmount clips in scripts. The framework owns media playback and clip lifecycle. See [Common Mistakes](/guides/common-mistakes) for more details.
</Warning>
## Viewport
Every composition must include `data-width` and `data-height` on the root element:
```html ```html
<div id="main" data-composition-id="my-video" <!doctype html>
data-start="0" data-width="1920" data-height="1080"> <html lang="en">
<!-- clips --> <head>
</div> <meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body { margin: 0; }
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
.clip {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="5"
data-width="1920"
data-height="1080"
>
<section
id="title-card"
class="clip"
data-start="0"
data-duration="5"
data-track-index="1"
>
<h1 id="title">Hello HyperFrames</h1>
</section>
</div>
<script>
window.__timelines = window.__timelines || {};
const timeline = gsap.timeline({ paused: true });
timeline.fromTo(
"#title",
{ y: 48, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.6, ease: "power3.out" },
0.2,
);
window.__timelines.main = timeline;
</script>
</body>
</html>
``` ```
Common sizes: The root is a real, explicitly sized box. Its `data-composition-id` matches the
- **Landscape**: `data-width="1920" data-height="1080"` timeline registry key.
- **Portrait**: `data-width="1080" data-height="1920"`
## All Clip Attributes ## Composition root
| Attribute | Applies To | Required | Description | | Attribute | Required | Meaning |
|-----------|-----------|----------|-------------| | --- | --- | --- |
| `id` | All | Yes | Unique identifier (e.g., `"el-1"`). Used for relative timing references and CSS targeting. | | `data-composition-id` | Yes | Unique composition ID |
| `class="clip"` | Visible elements | Yes | Enables runtime visibility management. Omit for audio-only clips. | | `data-start="0"` | Yes on the top-level root | Start of the composition |
| `data-start` | All | Yes | Start time in seconds (e.g., `"0"`, `"5.5"`), or a clip ID reference for [relative timing](#relative-timing) (e.g., `"intro"`). | | `data-width` and `data-height` | Yes | Authored frame dimensions in pixels |
| `data-duration` | video, img, audio, nested composition hosts | See below | Timeline slot duration in seconds. **Required** for images and nested composition hosts. Optional for video/audio (defaults to source duration). | | `data-duration` | Usually | Total render duration in seconds |
| `data-track-index` | All | Yes | Timeline track number. Controls z-ordering (higher = in front). Clips on the same track cannot overlap. | | `data-no-timeline` | Only for a timeline-free composition | Tells the runtime not to wait for a timeline |
| `data-media-start` | video, audio | No | Playback offset / trim point in source file (seconds). Default: `0`. See [Data Attributes](/concepts/data-attributes). |
| `data-playback-start` | video, audio, composition | No | Source-time offset in seconds. On a nested composition, this is the child timeline time shown at the host's `data-start`. Default: `0`. |
| `data-playback-rate` | video, audio, composition | No | Source playback multiplier clamped to `0.1``5`. Invalid values default to `1`. |
| `data-volume` | audio, video | No | Volume level from `0` to `1`. Default: `1`. |
| `data-composition-id` | div | On compositions | Unique composition ID. Must match the key used in `window.__timelines`. |
| `data-composition-src` | div | No | Path to external composition HTML file (for [nested compositions](#composition-clips)). |
| `data-variable-values` | div | No | JSON object of values passed to a nested composition. Read via `getVariables()` in scripts, or consumed automatically by declarative bindings. |
| `data-var-src` | img, video, audio | No | Binds the element's `src` to a declared variable id — the runtime substitutes the value (URL string or image `{url}`); the authored `src` is the fallback. |
| `data-var-text` | any | No | Binds the element's own text to a scalar variable id. Element children are preserved. |
| `data-color-grading` | img, video | No | Validated JSON payload for media-level correction, grading, LUT, finishing, and shader effects. Prefer Studio or `hyperframes media-treatment` to author it. |
| `data-width` | div | On compositions | Composition width in pixels. |
| `data-height` | div | On compositions | Composition height in pixels. |
## Clip Types An explicit root `data-duration` is the render length. The compiler reads it
before composition scripts run, so a script or variable override cannot change
that value for the same render.
<AccordionGroup> The root may omit `data-duration` only when HyperFrames can infer a finite
<Accordion title="Video Clips"> duration from the registered animation runtime or timed media. Three.js,
Video clips embed `<video>` elements with timing and playback attributes. unbounded animation, and timeline-free compositions need an explicit duration.
```html ## Timed clips
<video
id="el-1"
data-start="0"
data-duration="15"
data-track-index="0"
data-media-start="0"
src="./assets/video.mp4"
></video>
```
**Key behavior:** | Attribute | Required | Meaning |
- `data-duration` is **optional** — defaults to the remaining duration of the source file from `data-media-start` | --- | --- | --- |
- If source media runs out before `data-duration`, the clip shows the last frame (freeze frame) | `id` | Yes | Stable identifier for timing, editing, and animation |
- `data-media-start` trims the beginning of the source video — `data-media-start="5"` starts playback 5 seconds into the source file | `data-start` | Yes | Start in seconds or a relative timing expression |
- `data-volume` controls the audio volume of the video — set to `"0"` for silent video | `data-duration` | Yes for DOM, image, and nested-composition clips | Visible slot length in seconds |
- Do **not** add `class="clip"` to video elements — the framework manages their visibility directly | `data-track-index` | Yes | Timeline lane used to prevent temporal overlap |
| `class="clip"` | Yes for authored timed DOM and image elements | Lets the runtime own their visibility window |
<Warning> `data-track-index` does not control paint order. Use CSS `z-index` for
Do not animate `width`, `height`, `top`, or `left` directly on `<video>` elements with GSAP. This can cause Chrome to stop rendering video frames. Wrap the video in a `<div>` and animate the wrapper instead. See [Common Mistakes](/guides/common-mistakes). front-to-back layering. Two clips on the same track must not overlap in time.
</Warning>
</Accordion>
<Accordion title="Image Clips"> Video visibility is managed as media and does not require `class="clip"`.
Image clips display static images with controlled timing. Audio has no visual lifecycle.
```html ## Media
<img
id="el-2"
class="clip"
data-start="5"
data-duration="4"
data-track-index="1"
src="./assets/overlay.png"
/>
```
**Key behavior:** ```html
- `data-duration` is **required** for images (unlike video/audio, there is no source duration to default to)
- `class="clip"` is **required** — this enables the runtime to show/hide the image based on timing
- Supported formats: PNG, JPG, WebP, SVG, GIF. Animated GIFs are prepared as timeline-synced video for preview and render; `data-loop` can override GIF loop metadata.
- Position and size with CSS — the image renders at its natural size unless styled otherwise
</Accordion>
<Accordion title="Audio Clips">
Audio clips add sound to the composition without any visual element.
```html
<audio
id="el-4"
data-start="0"
data-duration="30"
data-track-index="2"
src="./assets/music.mp3"
></audio>
```
**Key behavior:**
- `data-duration` is **optional** — defaults to the remaining duration of the source file from `data-media-start`
- Audio clips are invisible — do not add `class="clip"` (there is nothing to show/hide)
- `data-volume` controls volume — use `"0.5"` for background music at 50% volume
- `data-media-start` trims the beginning of the audio source, just like video
- Multiple audio clips can overlap on different tracks for layered sound design
</Accordion>
<Accordion title="Composition Clips (Nested)">
Composition clips embed one composition inside another, enabling modular, reusable video building blocks.
```html
<div
id="el-5"
data-composition-id="intro-anim"
data-composition-src="compositions/intro-anim.html"
data-start="0"
data-track-index="3"
></div>
```
**Key behavior:**
- Compositions do **not** use `data-duration` — duration is determined by the composition's GSAP timeline (`tl.duration()`)
- External compositions are loaded from `data-composition-src` and wrapped in `<template>` tags
- Each nested composition has its own `window.__timelines` entry, registered by its own `<script>` block
- The framework automatically nests sub-timelines — do not manually add them to the parent timeline
- Any composition can be nested inside any other — there is no special "root" type
- Per-instance values can be passed with `data-variable-values`; sub-composition scripts read them via `getVariables()`, and `data-var-*` bindings / `var(--id)` CSS resolve them automatically
For more on how compositions work, see [Compositions](/concepts/compositions).
</Accordion>
</AccordionGroup>
## Media Treatments
Real `<img>` and `<video>` elements may carry a `data-color-grading` payload:
```html index.html
<video <video
id="hero" id="demo"
src="assets/hero.mp4" src="./assets/demo.mp4"
data-start="0" data-start="0"
data-duration="8"
data-track-index="0" data-track-index="0"
muted muted
playsinline playsinline
></video>
```
| Attribute | Applies to | Meaning |
| --- | --- | --- |
| `data-media-start` | Video and audio | Offset into the source file |
| `data-playback-start` | Video, audio, nested composition | Source-time offset used by trim and split operations |
| `data-playback-rate` | Video, audio, nested composition | Playback multiplier from `0.1` to `5` |
| `data-volume` | Video and audio | Static volume from `0` to `1` |
| `data-has-audio="true"` | Video | Declares that the video contributes audio |
Video and audio may omit `data-duration` when their intrinsic duration is known
and the whole remaining source should play.
Do not imperatively call `play()`, `pause()`, or set `currentTime`.
HyperFrames owns media playback and seeking. A video should be `muted` unless it
is intentionally audible and declares `data-has-audio="true"`.
Media can live inside nested composition markup. Keep element IDs unique across
the assembled project because render-time frame injection targets those IDs.
## Color grading and media effects
Studio and the CLI store color correction, grading, finishing controls, LUTs,
and media effects on an image or video with `data-color-grading`:
```html
<video
id="demo"
src="./assets/demo.mp4"
data-color-grading='{ data-color-grading='{
"preset":"clean-studio", "preset": "clean-studio",
"intensity":0.8, "intensity": 0.8,
"adjust":{"highlights":-0.08,"shadows":0.06}, "adjust": { "highlights": -0.08, "shadows": 0.06 },
"effects":{"bloom":0.12}, "details": { "grain": 0.12, "vignette": 0.08 },
"colorSpace":"rec709" "effects": { "bloom": 0.12 },
"colorSpace": "rec709"
}' }'
></video> ></video>
``` ```
The runtime renders the complete payload on a sibling WebGL canvas. It does not The current effect families include essentials, retro and glitch, print, and
apply to text, SVG, arbitrary DOM, or CSS background images. Use Studio or art treatments. Run
`npx hyperframes media-treatment --capabilities --json` for the current
machine-readable surface instead of hard-coding an old effect list.
The grading pipeline applies to real `<img>` and `<video>` elements. It does
not grade a complete HTML scene. Use Studio or
[`media-treatment`](/packages/cli#media-treatment) for normal authoring; see [`media-treatment`](/packages/cli#media-treatment) for normal authoring; see
[Color Grading](/guides/color-grading) and [Media Effects](/guides/media-effects) [Color Grading](/guides/color-grading) and [Media Effects](/guides/media-effects)
for the supported workflow and current SDR/HDR boundary. for the workflow and current SDR/HDR boundary.
## Relative Timing ## Relative timing
Reference another clip's ID in `data-start` to mean "start when that clip ends": A non-numeric `data-start` refers to the end of another clip in the same
composition:
```html ```html
<video id="intro" data-start="0" data-duration="10" data-track-index="0" src="..."></video> <video
<video id="main" data-start="intro" data-duration="20" data-track-index="0" src="..."></video> id="intro"
data-start="0"
data-duration="4"
data-track-index="0"
src="./intro.mp4"
muted
playsinline
></video>
<section
id="result"
class="clip"
data-start="intro + 0.5"
data-duration="3"
data-track-index="0"
>
Result
</section>
``` ```
`main` starts at second 10 (when `intro` ends). Supported forms are `clip-id`, `clip-id + seconds`, and
`clip-id - seconds`. References stay within one composition, must resolve to a
known duration, and cannot form a cycle. Put intentional overlaps on different
tracks.
**Offsets** let you add gaps or overlaps: ## Nested compositions
```html The host declares a fixed timeline window:
<!-- 2-second gap after intro -->
<video id="main" data-start="intro + 2" data-duration="20" data-track-index="0" src="..."></video>
<!-- 0.5-second overlap with intro -->
<video id="main" data-start="intro - 0.5" data-duration="20" data-track-index="0" src="..."></video>
```
For a deeper explanation, see the [relative timing section](/concepts/data-attributes#relative-timing) in the Data Attributes concept page.
## Timeline Contract
The framework initializes `window.__timelines = {}` before any scripts run. Every composition must register a GSAP timeline at the key matching its `data-composition-id`:
```javascript
const tl = gsap.timeline({ paused: true });
// Add animations
tl.to("#title", { opacity: 1, duration: 0.5 }, 0);
tl.to("#title", { opacity: 0, duration: 0.5 }, 4.5);
// Register the timeline
window.__timelines["<data-composition-id>"] = tl;
```
### Rules
- Every composition needs a `<script>` block that creates and registers its timeline
- All timelines must start paused (`{ paused: true }`)
- The framework auto-nests sub-timelines into the parent — do **not** manually add them
- Duration comes from `tl.duration()` — do **not** add `data-duration` on composition elements
- Timelines must be finite (no infinite loops or repeats)
- The timeline ID must exactly match the `data-composition-id` attribute on the root element
For a complete guide to working with GSAP timelines, see [GSAP Animation](/guides/gsap-animation).
## Caption Discoverability
For caption compositions, add these attributes to the root node so the framework can identify and special-case caption rendering:
```html ```html
<div <div
data-composition-id="captions" id="pricing-scene"
data-timeline-role="captions" data-composition-id="pricing"
data-caption-root="true" data-composition-src="./compositions/pricing.html"
... data-start="4"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
The host `data-composition-id` must match the ID inside the source file and its
timeline registry key. The host `data-duration` controls how long the nested
composition remains visible. A shorter inner timeline holds its final state;
a shorter host duration cuts the slot.
A nested composition file transports its live markup through `<template>`.
Styles and scripts needed by that composition must be inside the template:
```html
<template>
<style>
#root {
position: absolute;
inset: 0;
}
</style>
<div
id="root"
data-composition-id="pricing"
data-width="1920"
data-height="1080"
>
<!-- scene content -->
</div>
<script>
window.__timelines = window.__timelines || {};
const timeline = gsap.timeline({ paused: true });
// scene animation
window.__timelines.pricing = timeline;
</script>
</template>
```
HyperFrames seeks nested timelines independently. Do not add a child timeline
manually to the parent GSAP timeline.
## Variables
Declare the schema with `data-composition-variables`, then pass values through a
render or a nested host:
```html
<html
data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Hello"}
]'
> >
``` ```
## Output Checklist ```html
<div
data-composition-id="pricing"
data-composition-src="./compositions/pricing.html"
data-variable-values='{"title":"Built for teams"}'
data-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
<Check> Use `data-var-text`, `data-var-src`, or CSS `var(--variableId)` for direct
Before rendering, verify your composition meets these requirements: bindings. Use `getVariables()` when the value affects logic.
- Every composition has `data-width` and `data-height` on the root element ## Animation contract
- Each reusable composition is in its own HTML file
- External compositions are loaded via `data-composition-src` A composition using GSAP must:
- Each external composition file uses a `<template>` wrapper
- All GSAP timelines are registered in `window.__timelines` with the correct ID - create one finite timeline with `{ paused: true }`;
- Timed visible elements (images, divs) have `class="clip"` - register it synchronously on `window.__timelines`;
- Video elements do **not** have `class="clip"` (framework manages them directly) - use the same key as `data-composition-id`;
- All `data-start` references point to existing clip IDs - avoid wall-clock state, unseeded randomness, and infinite repeats.
- Run `npx hyperframes lint` to catch structural issues automatically
</Check> HyperFrames controls seeking. Composition code describes how the visual state
looks at a given time.
## Validate
```bash
npx hyperframes lint
npx hyperframes check
```
`lint` checks the static contract. `check` opens the project in a browser and
checks runtime behavior, layout, motion, and contrast. Watch representative
snapshots and the finished render as the final visual gate.
+29 -32
View File
@@ -54,19 +54,19 @@ detach();
`preview.elementAtPoint(x, y)` performs a synchronous hit-test at coordinates in the iframe's own coordinate space and returns the nearest `[data-hf-id]` element, or `null` for a transparent hit. `preview.elementAtPoint(x, y)` performs a synchronous hit-test at coordinates in the iframe's own coordinate space and returns the nearest `[data-hf-id]` element, or `null` for a transparent hit.
```typescript ```typescript
iframe.addEventListener("click", (e) => { iframe.addEventListener("load", () => {
// e.clientX / e.clientY are in the outer frame's space. const frameDocument = iframe.contentDocument;
// If the iframe is positioned, convert to iframe-local coords. if (!frameDocument) return;
const rect = iframe.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const hit = preview.elementAtPoint(x, y); // Events inside an iframe do not bubble to the outer <iframe> element.
frameDocument.addEventListener("click", (event) => {
const hit = preview.elementAtPoint(event.clientX, event.clientY);
if (hit) { if (hit) {
// hit.id — the data-hf-id value // hit.id — the data-hf-id value
// hit.tag — the lowercased tag name (e.g. "div", "img", "video") // hit.tag — the lowercased tag name (e.g. "div", "img", "video")
preview.select([hit.id]); preview.select([hit.id]);
} }
});
}); });
``` ```
@@ -111,45 +111,43 @@ let startX = 0;
let startY = 0; let startY = 0;
let targetId: string | null = null; let targetId: string | null = null;
iframe.addEventListener("pointerdown", (e) => { iframe.addEventListener("load", () => {
const rect = iframe.getBoundingClientRect(); const frameDocument = iframe.contentDocument;
const hit = preview.elementAtPoint(e.clientX - rect.left, e.clientY - rect.top); if (!frameDocument) return;
frameDocument.addEventListener("pointerdown", (event) => {
const hit = preview.elementAtPoint(event.clientX, event.clientY);
if (!hit) return; if (!hit) return;
dragging = true; dragging = true;
targetId = hit.id; targetId = hit.id;
startX = e.clientX; startX = event.clientX;
startY = e.clientY; startY = event.clientY;
iframe.setPointerCapture(e.pointerId); if (event.target instanceof Element && "setPointerCapture" in event.target) {
}); event.target.setPointerCapture(event.pointerId);
}
});
iframe.addEventListener("pointermove", (e) => { frameDocument.addEventListener("pointermove", (event) => {
if (!dragging || !targetId) return; if (!dragging || !targetId) return;
preview.applyDraft(targetId, {
dx: event.clientX - startX,
dy: event.clientY - startY,
});
});
const dx = e.clientX - startX; frameDocument.addEventListener("pointerup", () => {
const dy = e.clientY - startY;
// applyDraft at 60fps — no model mutation, no patch event
preview.applyDraft(targetId, { dx, dy });
});
iframe.addEventListener("pointerup", () => {
if (!dragging || !targetId) return; if (!dragging || !targetId) return;
// Derives a moveElement op from the accumulated dx/dy, dispatches it
// through the callback you passed to createIframePreviewAdapter, then
// clears the CSS vars and internal draft state.
preview.commitPreview(); preview.commitPreview();
dragging = false; dragging = false;
targetId = null; targetId = null;
}); });
iframe.addEventListener("pointercancel", () => { frameDocument.addEventListener("pointercancel", () => {
// Clears the CSS vars. Model is never touched.
preview.cancelPreview(); preview.cancelPreview();
dragging = false; dragging = false;
targetId = null; targetId = null;
});
}); });
``` ```
@@ -200,4 +198,3 @@ Once hit-testing and drag are working, you can use the affordance resolver to dr
Resolve which edit controls to show for the selected element. Resolve which edit controls to show for the selected element.
</Card> </Card>
</CardGroup> </CardGroup>
+4
View File
@@ -57,6 +57,8 @@ interface EditingAffordances {
interface DomEditCapabilities { interface DomEditCapabilities {
canSelect: boolean; canSelect: boolean;
canEditStyles: boolean; canEditStyles: boolean;
/** Apply a non-destructive clip-path crop. */
canCrop: boolean;
/** Directly editable authored left/top style fields. */ /** Directly editable authored left/top style fields. */
canMove: boolean; canMove: boolean;
/** Directly editable authored width/height style fields. */ /** Directly editable authored width/height style fields. */
@@ -83,6 +85,8 @@ interface EditingSectionApplicability {
colorGrading: boolean; // true for <video> and <img> — element-level only colorGrading: boolean; // true for <video> and <img> — element-level only
timing: boolean; // true when data-start is present or animationCount > 0 timing: boolean; // true when data-start is present or animationCount > 0
animation: boolean; // true when animationCount > 0 animation: boolean; // true when animationCount > 0
layout: boolean; // position, size, rotation, and stacking controls apply
style: boolean; // fill, radius, stroke, shadow, blend, and clip controls apply
} }
``` ```
+28 -20
View File
@@ -3,7 +3,7 @@ title: "Timing & Animation"
description: "Set clip timing, elastic holds, GSAP tweens, and keyframed animations on composition elements." description: "Set clip timing, elastic holds, GSAP tweens, and keyframed animations on composition elements."
--- ---
Every clip in a HyperFrames composition has a position on the timeline (`data-start`, `data-duration`) and an optional animation attached to it. The SDK exposes typed helpers for both: the timing API controls *when* an element appears and how long it stays on screen; the animation API controls *how* it moves through that window. Every clip in a HyperFrames composition has a position on the timeline (`data-start`, `data-duration`) and an optional animation attached to it. The SDK exposes typed helpers for both: the timing API controls _when_ an element appears and how long it stays on screen; the animation API controls _how_ it moves through that window.
## Clip Timing ## Clip Timing
@@ -31,11 +31,14 @@ Each `ElementTimingSnapshot` contains:
Absolute timeline position (seconds) at which the element exits. Absolute timeline position (seconds) at which the element exits.
</ResponseField> </ResponseField>
<ResponseField name="labels" type="string[]"> <ResponseField name="labels" type="string[]">
GSAP timeline label names whose numeric position falls within `[enterAt, exitAt]`. Parsed fresh from the GSAP script on every `getElementTimings()` call — never stale. GSAP timeline label names whose numeric position falls within `[enterAt, exitAt]`. Parsed fresh
from the GSAP script on every `getElementTimings()` call — never stale.
</ResponseField> </ResponseField>
<Note> <Note>
`getElementTimings()` only includes elements that have `data-start` and either `data-duration` or `data-end` attributes. Untimed elements are omitted. The method prefers `data-duration` over `data-end data-start` when both exist, matching the behavior of `setTiming`. `getElementTimings()` only includes elements that have `data-start` and either `data-duration` or
`data-end` attributes. Untimed elements are omitted. The method prefers `data-duration` over
`data-end data-start` when both exist, matching the behavior of `setTiming`.
</Note> </Note>
### Setting timing on one element ### Setting timing on one element
@@ -92,7 +95,8 @@ comp.setHold("hf-card", {
Absolute composition time at which the hold ends. Absolute composition time at which the hold ends.
</ParamField> </ParamField>
<ParamField path="fill" type='"freeze" | "loop"'> <ParamField path="fill" type='"freeze" | "loop"'>
`"freeze"` holds the last frame until `end`; `"loop"` repeats the segment from `start` back to `start`. `"freeze"` holds the last frame until `end`; `"loop"` repeats the segment from `start` back to
`start`.
</ParamField> </ParamField>
--- ---
@@ -122,7 +126,9 @@ const animId = comp.addGsapTween("hf-title", {
The GSAP timeline method to call. The GSAP timeline method to call.
</ParamField> </ParamField>
<ParamField path="position" type="number | string"> <ParamField path="position" type="number | string">
Timeline position: a number (seconds) or a label-relative string (e.g. `"intro+=0.3"`). Number-only is required for `addWithKeyframes` / `replaceWithKeyframes` — see [Keyframes](#keyframes) below. Timeline position: a number (seconds) or a label-relative string (e.g. `"intro+=0.3"`).
Number-only is required for `addWithKeyframes` / `replaceWithKeyframes` — see
[Keyframes](#keyframes) below.
</ParamField> </ParamField>
<ParamField path="duration" type="number"> <ParamField path="duration" type="number">
Tween duration in seconds. Tween duration in seconds.
@@ -195,13 +201,17 @@ if (!check.ok) {
return; return;
} }
comp.dispatch({ type: "setGsapTween", animationId: firstAnimId, properties: { ease: "power3.inOut" } }); comp.dispatch({
type: "setGsapTween",
animationId: firstAnimId,
properties: { ease: "power3.inOut" },
});
``` ```
Stable error codes returned by `can()`: Stable error codes returned by `can()`:
| Code | Meaning | | Code | Meaning |
|------|---------| | -------------------- | --------------------------------------------------------------- |
| `E_TARGET_NOT_FOUND` | The target `HfId` does not exist in the document. | | `E_TARGET_NOT_FOUND` | The target `HfId` does not exist in the document. |
| `E_NO_ROOT` | The document has no root element. | | `E_NO_ROOT` | The document has no root element. |
| `E_NO_GSAP_TIMELINE` | Op requires the GSAP parser engine, which is not yet available. | | `E_NO_GSAP_TIMELINE` | Op requires the GSAP parser engine, which is not yet available. |
@@ -234,21 +244,18 @@ Returns the new animation ID string, or `""` if the op was rejected.
### Replacing an existing keyframed tween ### Replacing an existing keyframed tween
```typescript ```typescript
const newAnimId = comp.replaceWithKeyframes( const newAnimId = comp.replaceWithKeyframes(oldAnimId, "#hf-badge", 1.0, 1.2, [
oldAnimId,
"#hf-badge",
1.0,
1.2,
[
{ percentage: 0, properties: { x: -60, opacity: 0 } }, { percentage: 0, properties: { x: -60, opacity: 0 } },
{ percentage: 100, properties: { x: 0, opacity: 1 }, ease: "back.out(1.7)" }, { percentage: 100, properties: { x: 0, opacity: 1 }, ease: "back.out(1.7)" },
], ]);
);
// newAnimId !== oldAnimId — position-derived IDs renumber after the remove // newAnimId !== oldAnimId — position-derived IDs renumber after the remove
``` ```
<Warning> <Warning>
`replaceWithKeyframes` is equivalent to `removeGsapTween` + `addWithKeyframes` in one atomic op. Because position-derived tween IDs renumber after the removal step, the returned ID is always a **new** ID and must not be assumed equal to the input `animationId`. Re-query `element.animationIds` after a replace to get the current set. `replaceWithKeyframes` is equivalent to `removeGsapTween` + `addWithKeyframes` in one atomic op.
Because position-derived tween IDs renumber after the removal step, the returned ID is always a
**new** ID and must not be assumed equal to the input `animationId`. Re-query
`element.animationIds` after a replace to get the current set.
</Warning> </Warning>
### KeyframeSpec fields ### KeyframeSpec fields
@@ -263,7 +270,8 @@ const newAnimId = comp.replaceWithKeyframes(
Per-keyframe ease applied *from* this keyframe to the next. Per-keyframe ease applied *from* this keyframe to the next.
</ParamField> </ParamField>
<ParamField path="auto" type="boolean"> <ParamField path="auto" type="boolean">
GSAP endpoint flag — when `true`, this keyframe picks up the element's current value automatically. GSAP endpoint flag — when `true`, this keyframe picks up the element's current value
automatically.
</ParamField> </ParamField>
### Lower-level keyframe and label ops ### Lower-level keyframe and label ops
@@ -274,7 +282,8 @@ For lower-level operations — individual keyframe mutations (`addGsapKeyframe`,
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Edit Operations" icon="code" href="/sdk/reference/edit-operations"> <Card title="Edit Operations" icon="code" href="/sdk/reference/edit-operations">
Full reference for every op type in the `EditOp` union, including lower-level keyframe and arc ops. Full reference for every op type in the `EditOp` union, including lower-level keyframe and arc
ops.
</Card> </Card>
<Card title="Types" icon="brackets-curly" href="/sdk/reference/types"> <Card title="Types" icon="brackets-curly" href="/sdk/reference/types">
`GsapTweenSpec`, `KeyframeSpec`, `ElasticHold`, `ElementTimingSnapshot`, and all related types. `GsapTweenSpec`, `KeyframeSpec`, `ElasticHold`, `ElementTimingSnapshot`, and all related types.
@@ -282,8 +291,7 @@ For lower-level operations — individual keyframe mutations (`addGsapKeyframe`,
<Card title="GSAP Animation Guide" icon="wand-magic-sparkles" href="/guides/gsap-animation"> <Card title="GSAP Animation Guide" icon="wand-magic-sparkles" href="/guides/gsap-animation">
Authoring GSAP timelines in composition HTML. Authoring GSAP timelines in composition HTML.
</Card> </Card>
<Card title="Keyframes Guide" icon="film" href="/guides/keyframes"> <Card title="Animation in Studio" icon="film" href="/studio/animation">
Keyframe authoring patterns and best practices. Keyframe authoring patterns and best practices.
</Card> </Card>
</CardGroup> </CardGroup>
-79
View File
@@ -1,79 +0,0 @@
---
title: "SDK Overview"
description: "Headless, framework-neutral composition editing engine — query, mutate, patch, and persist without a browser UI."
---
`@hyperframes/sdk` is the editing engine inside HyperFrames Studio and the CLI. It opens composition HTML, exposes query and mutation APIs, emits RFC 6902 JSON patches, tracks undo/redo, and persists changes through pluggable adapters — all without requiring React, Studio, or a browser UI.
```bash
npm install @hyperframes/sdk
```
## Mental model
**Every edit targets a stable `hf-id`.** The SDK stamps all elements with `data-hf-id` identifiers before any query or mutation runs. This means edits never depend on mouse state or a UI selection — agents and backend jobs operate on the same surface as a Studio user.
**Typed methods are sugar over `dispatch()`.** `comp.setText("hf-title", "Hello")` is exactly equivalent to `comp.dispatch({ type: "setText", target: "hf-title", value: "Hello" })`. Both go through the same validation and emit the same patch event. Use typed methods for clarity; use `dispatch()` when you're building data-driven automation or want to feed programmatic op arrays.
**Patches are the source of truth for history and sync.** Every committed change emits a `PatchEvent` with forward patches and inverse patches (RFC 6902 `add`/`remove`/`replace` ops). The undo stack replays inverse patches; embedded hosts replay the same patches into their own state machine; collaboration layers forward them to other clients. You can subscribe to `patch` events and mirror SDK mutations anywhere without re-parsing the HTML.
**Adapters decouple persistence and preview.** The SDK never reaches the filesystem or an iframe directly. You supply a `PersistAdapter` (memory, filesystem, S3, HTTP — same interface) and optionally a `PreviewAdapter`. The session fires `persist:error` events on write failures instead of throwing. This makes the SDK equally at home in a Node.js agent, a browser editor, and a CI pipeline.
**Standalone vs embedded mode.** In standalone mode you own the HTML and the SDK owns history and autosave. In embedded (override) mode you supply a base template and a sparse `OverrideSet` delta; the SDK folds the delta onto the template at open time, accumulates further edits into the override set, and lets you store only the delta — the base template stays untouched.
## Guides
<CardGroup cols={2}>
<Card title="Quickstart" icon="bolt" href="/sdk/quickstart">
Open a composition, make edits, serialize, and add persistence in five minutes.
</Card>
<Card title="Querying & Editing Elements" icon="magnifying-glass" href="/sdk/guides/querying-and-editing">
getElements, find, typed methods, batch, element handles, and selection.
</Card>
<Card title="Timing & Animation" icon="clock" href="/sdk/guides/timing-and-animation">
setTiming, setHold, GSAP tween and keyframe operations.
</Card>
<Card title="Undo, Redo & Patches" icon="arrow-rotate-left" href="/sdk/guides/undo-redo-and-patches">
History module, patch events, applyPatches, and origin tagging.
</Card>
<Card title="Persistence" icon="floppy-disk" href="/sdk/guides/persistence">
Memory, filesystem, and custom adapters. Version history and flush.
</Card>
<Card title="Embedded Override Mode" icon="layers" href="/sdk/guides/embedded-override-mode">
Template-driven products with host-owned undo and delta storage.
</Card>
<Card title="Canvas Integration" icon="paintbrush" href="/sdk/guides/canvas-integration">
Connecting the SDK to an iframe preview surface.
</Card>
<Card title="Editing Affordances" icon="sliders" href="/sdk/guides/editing-affordances">
Resolve which controls to show for a live element.
</Card>
</CardGroup>
## Reference
<CardGroup cols={2}>
<Card title="openComposition" icon="door-open" href="/sdk/reference/open-composition">
The single entry point — options, modes, and examples.
</Card>
<Card title="Composition" icon="cube" href="/sdk/reference/composition">
Full method reference for the session object.
</Card>
<Card title="Edit Operations" icon="pen-to-square" href="/sdk/reference/edit-operations">
Every EditOp variant with field-level documentation.
</Card>
<Card title="Types" icon="brackets-curly" href="/sdk/reference/types">
HyperFramesElement, FindQuery, PatchEvent, OverrideSet, and more.
</Card>
<Card title="Adapters" icon="plug" href="/sdk/reference/adapters">
PersistAdapter and PreviewAdapter interfaces and built-in implementations.
</Card>
<Card title="Utilities" icon="wrench" href="/sdk/reference/utilities">
buildDocument, flatElements, UnsupportedOpError, and helper exports.
</Card>
</CardGroup>
<Tip>
If you want to render a composition to MP4 or WebM rather than edit it, see the [CLI](/packages/cli) or [producer](/packages/producer). The SDK is the editing layer — rendering is a separate pipeline.
</Tip>
+31 -15
View File
@@ -1,9 +1,17 @@
--- ---
title: "SDK Quickstart" title: "Edit a composition with the SDK"
description: "Open a composition, query and edit elements, serialize, and add autosave in minutes." sidebarTitle: "SDK quickstart"
description: "Open composition HTML, query and edit elements, serialize the result, and add persistence."
--- ---
This guide walks you through the core SDK loop from scratch. You will open a composition HTML string, find and edit elements by their stable `hf-id`, serialize the result, and then extend the example to save changes to disk automatically. Use `@hyperframes/sdk` when an application must inspect or change composition
HTML without opening Studio. If you only need playback, use the
[Player](/packages/player). If you only need a rendered file, use the
[CLI](/developers/cli) or [Producer](/packages/producer).
The core loop is open, query, edit, and serialize. The SDK adds stable
`data-hf-id` values where they are missing so later edits target the same
elements.
## Open, edit, serialize ## Open, edit, serialize
@@ -27,6 +35,7 @@ This guide walks you through the core SDK loop from scratch. You will open a com
``` ```
The call is async because it runs the ID-stamping pass over the DOM before returning. The call is async because it runs the ID-stamping pass over the DOM before returning.
</Step> </Step>
<Step title="Find elements"> <Step title="Find elements">
@@ -45,6 +54,7 @@ This guide walks you through the core SDK loop from scratch. You will open a com
``` ```
`find()` returns an array of `scopedId` strings. For top-level elements, `scopedId === id`. For elements inside inlined sub-compositions, it is `"hf-HOST/hf-LEAF"`. `find()` returns an array of `scopedId` strings. For top-level elements, `scopedId === id`. For elements inside inlined sub-compositions, it is `"hf-HOST/hf-LEAF"`.
</Step> </Step>
<Step title="Edit elements with typed methods"> <Step title="Edit elements with typed methods">
@@ -61,14 +71,8 @@ This guide walks you through the core SDK loop from scratch. You will open a com
fontWeight: "700", fontWeight: "700",
}); });
// Set or clear an attribute (null removes it)
comp.setAttribute("hf-logo", "src", "/assets/logo-v2.png");
// Adjust clip timing // Adjust clip timing
comp.setTiming("hf-title", { start: 0.5, duration: 4 }); comp.setTiming("hf-title", { start: 0.5, duration: 4 });
// Set a composition variable
comp.setVariableValue("brandColor", "#6C5CE7");
``` ```
Use `batch()` when several mutations should collapse into one undo entry, one persist write, and one `change` event: Use `batch()` when several mutations should collapse into one undo entry, one persist write, and one `change` event:
@@ -80,6 +84,7 @@ This guide walks you through the core SDK loop from scratch. You will open a com
comp.setTiming("hf-title", { start: 0.5, duration: 4 }); comp.setTiming("hf-title", { start: 0.5, duration: 4 });
}); });
``` ```
</Step> </Step>
<Step title="Serialize and dispose"> <Step title="Serialize and dispose">
@@ -91,12 +96,15 @@ This guide walks you through the core SDK loop from scratch. You will open a com
// updatedHtml is ready to write to disk, send to a renderer, or store in a database. // updatedHtml is ready to write to disk, send to a renderer, or store in a database.
``` ```
</Step> </Step>
</Steps> </Steps>
## Add a persistence adapter ## Add a persistence adapter
The headless pattern above is fine for one-shot transforms. When you want the SDK to autosave after every edit, pass a `PersistAdapter`. The headless pattern above is fine for one-shot transforms. When you want the SDK to persist edits,
pass a `PersistAdapter`. Rapid changes are coalesced and written in order, with the latest state
winning rather than one disk write per UI event.
The filesystem adapter (`@hyperframes/sdk/adapters/fs`) writes to a local directory and keeps a rolling version history. The filesystem adapter (`@hyperframes/sdk/adapters/fs`) writes to a local directory and keeps a rolling version history.
@@ -129,16 +137,25 @@ comp.dispose();
The adapter writes `./project/index.html` after every mutation and keeps up to 20 version snapshots under `./project/.hf-versions/`. The adapter writes `./project/index.html` after every mutation and keeps up to 20 version snapshots under `./project/.hf-versions/`.
<Note> <Note>
Disabling undo (`history: false`) does **not** disable autosave. The two are independent. Passing `history: false` is only necessary when you are managing the undo stack yourself. Disabling undo (`history: false`) does **not** disable autosave. The two are independent. Passing
`history: false` is only necessary when you are managing the undo stack yourself.
</Note> </Note>
## Next steps ## Related topics
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Querying & Editing Elements" icon="magnifying-glass" href="/sdk/guides/querying-and-editing"> <Card
title="Querying & Editing Elements"
icon="magnifying-glass"
href="/sdk/guides/querying-and-editing"
>
FindQuery fields, scopedId for sub-compositions, batch semantics, and element handles. FindQuery fields, scopedId for sub-compositions, batch semantics, and element handles.
</Card> </Card>
<Card title="Undo, Redo & Patches" icon="arrow-rotate-left" href="/sdk/guides/undo-redo-and-patches"> <Card
title="Undo, Redo & Patches"
icon="arrow-rotate-left"
href="/sdk/guides/undo-redo-and-patches"
>
History module, patch events for host sync, and applyPatches loop prevention. History module, patch events for host sync, and applyPatches loop prevention.
</Card> </Card>
<Card title="Persistence" icon="floppy-disk" href="/sdk/guides/persistence"> <Card title="Persistence" icon="floppy-disk" href="/sdk/guides/persistence">
@@ -148,4 +165,3 @@ The adapter writes `./project/index.html` after every mutation and keeps up to 2
Full option reference — adapters, overrides, coalesce window, and more. Full option reference — adapters, overrides, coalesce window, and more.
</Card> </Card>
</CardGroup> </CardGroup>
+9 -8
View File
@@ -271,7 +271,9 @@ const comp = await openComposition(html, {
``` ```
<Note> <Note>
`openComposition` defaults to a headless preview adapter when none is supplied, so you rarely need to pass it explicitly. The main use case is making the intent clear in code that runs in both headless and browser environments. `openComposition` does not create a preview adapter automatically. Omit `preview` when no preview
surface is needed, or pass `createHeadlessAdapter()` when an explicit no-op adapter makes shared
code clearer.
</Note> </Note>
--- ---
@@ -299,20 +301,19 @@ Returns a `PreviewAdapter` that bridges the SDK to a same-origin `<iframe>` cont
import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk"; import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk";
const iframe = document.querySelector<HTMLIFrameElement>("#preview-frame")!; const iframe = document.querySelector<HTMLIFrameElement>("#preview-frame")!;
let comp: Awaited<ReturnType<typeof openComposition>>;
const comp = await openComposition(html);
const preview = createIframePreviewAdapter(iframe, (op) => comp.dispatch(op)); const preview = createIframePreviewAdapter(iframe, (op) => comp.dispatch(op));
comp = await openComposition(html, { preview });
// Hit-test at pointer position // Hit-test at pointer position
const hit = preview.elementAtPoint(pointerX, pointerY); const hit = preview.elementAtPoint(pointerX, pointerY);
if (hit) { if (hit) {
preview.select([hit.id]); preview.select([hit.id]);
}
// Drag: call applyDraft at 60fps, commitPreview on pointer-up // Drag: call applyDraft at 60fps, commitPreview on pointer-up
preview.applyDraft(hit.id, { dx: 12, dy: -5 }); preview.applyDraft(hit.id, { dx: 12, dy: -5 });
preview.commitPreview(); preview.commitPreview();
}
``` ```
--- ---
+5 -5
View File
@@ -64,7 +64,7 @@ comp.setAttribute("hf-video", "autoplay", null); // removes autoplay
setTiming(id: HfId, timing: { start?: number; duration?: number; trackIndex?: number }): void setTiming(id: HfId, timing: { start?: number; duration?: number; trackIndex?: number }): void
``` ```
Update the `data-start`, `data-duration`, and/or `data-track` attributes of one element. All fields are optional; omitted fields are left unchanged. Update the `data-start`, `data-duration`, and/or `data-track-index` attributes of one element. All fields are optional; omitted fields are left unchanged.
```typescript ```typescript
comp.setTiming("hf-title", { start: 0.5, duration: 2.5 }); comp.setTiming("hf-title", { start: 0.5, duration: 2.5 });
@@ -189,9 +189,9 @@ getVariableUsage(): VariableUsageReport
Read-only variable APIs (no dispatch). `getVariableDeclarations` returns the typed Read-only variable APIs (no dispatch). `getVariableDeclarations` returns the typed
schema (same strict filter the render pipeline uses). `getVariableValues` resolves schema (same strict filter the render pipeline uses). `getVariableValues` resolves
values exactly like the runtime's `getVariables()` — declared defaults merged under values for this composition file — its declared defaults merged under the given overrides. The
the given overrides — so callers can predict what a composition script will read for runtime additionally walks declarations from inlined sub-compositions in an assembled document, so
a given `--variables` payload. `validateVariableValues` runs the same checks as query each SDK composition separately when you need that wider view. `validateVariableValues` runs the same checks as
`--strict-variables` (`undeclared` / `type-mismatch` / `enum-out-of-range`). `--strict-variables` (`undeclared` / `type-mismatch` / `enum-out-of-range`).
`getVariableUsage` statically scans every inline script for `getVariables()` reads `getVariableUsage` statically scans every inline script for `getVariables()` reads
and cross-references the schema: `{ usedIds, unusedDeclarations, undeclaredReads, and cross-references the schema: `{ usedIds, unusedDeclarations, undeclaredReads,
@@ -439,7 +439,7 @@ Search elements by structured query. Returns an array of `scopedId` strings for
| `tag` | `string` | Exact HTML tag name, lowercase (`"div"`, `"img"`). | | `tag` | `string` | Exact HTML tag name, lowercase (`"div"`, `"img"`). |
| `text` | `string` | Substring match against the element's `text` field. | | `text` | `string` | Substring match against the element's `text` field. |
| `name` | `string` | Exact match against `data-name` attribute. | | `name` | `string` | Exact match against `data-name` attribute. |
| `track` | `number` | Exact track index (`data-track`). | | `track` | `number` | Exact track index (`data-track-index`). |
| `composition` | `string` | Filter to elements inside a specific sub-composition host by its `hf-id`. | | `composition` | `string` | Filter to elements inside a specific sub-composition host by its `hf-id`. |
```typescript ```typescript
+1 -2
View File
@@ -92,7 +92,7 @@ These ops target one or more elements by explicit hf-id. `target` accepts a sing
| `setStyle` | `target`, `styles` | Merges CSS inline styles. `null` values remove that property. | | `setStyle` | `target`, `styles` | Merges CSS inline styles. `null` values remove that property. |
| `setText` | `target`, `value` | Replaces the element's direct text content. | | `setText` | `target`, `value` | Replaces the element's direct text content. |
| `setAttribute` | `target`, `name`, `value` | Sets or removes an HTML attribute. `null` removes it. Does not touch `style`, `class`, or `data-hf-*`. | | `setAttribute` | `target`, `name`, `value` | Sets or removes an HTML attribute. `null` removes it. Does not touch `style`, `class`, or `data-hf-*`. |
| `setTiming` | `target`, `start?`, `duration?`, `trackIndex?` | Updates one or more timing attributes (`data-start`, `data-duration`, `data-track`). Omitted fields are unchanged. | | `setTiming` | `target`, `start?`, `duration?`, `trackIndex?` | Updates one or more timing attributes (`data-start`, `data-duration`, `data-track-index`). Omitted fields are unchanged. |
| `setHold` | `target`, `hold` | Sets an elastic hold window; see `ElasticHold` shape below. | | `setHold` | `target`, `hold` | Sets an elastic hold window; see `ElasticHold` shape below. |
| `moveElement` | `target`, `x`, `y` | Repositions the element by setting `data-x` / `data-y` (not CSS `left`/`top`). | | `moveElement` | `target`, `x`, `y` | Repositions the element by setting `data-x` / `data-y` (not CSS `left`/`top`). |
| `removeElement` | `target` | Removes the element and all its children from the document. Inverse of `addElement`. | | `removeElement` | `target` | Removes the element and all its children from the document. Inverse of `addElement`. |
@@ -554,4 +554,3 @@ comp.dispatch({
update: { curviness: 2, cp1: { x: 220, y: -100 } }, update: { curviness: 2, cp1: { x: 220, y: -100 } },
}); });
``` ```
+1 -2
View File
@@ -6,7 +6,7 @@ description: "Open a composition HTML string for editing. Returns a Composition
`openComposition` is the single entry point for every SDK session. It parses the composition HTML, stamps stable `hf-id` attributes on any elements that lack them, and returns a [`Composition`](/sdk/reference/composition) ready to receive edits. `openComposition` is the single entry point for every SDK session. It parses the composition HTML, stamps stable `hf-id` attributes on any elements that lack them, and returns a [`Composition`](/sdk/reference/composition) ready to receive edits.
```typescript ```typescript
import { openComposition } from "@hyperframes/sdk"; import { openComposition, ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
const comp = await openComposition(html, opts?); const comp = await openComposition(html, opts?);
``` ```
@@ -172,4 +172,3 @@ const comp = await openComposition(html, {
Template-driven products with host-owned history. Template-driven products with host-owned history.
</Card> </Card>
</CardGroup> </CardGroup>
+2 -3
View File
@@ -81,7 +81,7 @@ interface HyperFramesElement {
</ResponseField> </ResponseField>
<ResponseField name="trackIndex" type="number | null"> <ResponseField name="trackIndex" type="number | null">
Zero-based track index (`data-track`). `null` when not set. Zero-based track index (`data-track-index`). `null` when not set.
</ResponseField> </ResponseField>
<ResponseField name="animationIds" type="readonly string[]"> <ResponseField name="animationIds" type="readonly string[]">
@@ -213,7 +213,7 @@ interface FindQuery {
</ResponseField> </ResponseField>
<ResponseField name="track" type="number"> <ResponseField name="track" type="number">
Match by zero-based track index (`data-track`). Match by zero-based track index (`data-track-index`).
</ResponseField> </ResponseField>
<ResponseField name="composition" type="string"> <ResponseField name="composition" type="string">
@@ -766,4 +766,3 @@ interface PersistErrorEvent {
<ResponseField name="error.cause" type="unknown"> <ResponseField name="error.cause" type="unknown">
Underlying error object, if available. Underlying error object, if available.
</ResponseField> </ResponseField>
+5 -3
View File
@@ -289,10 +289,13 @@ import { readVariableDefault } from "@hyperframes/sdk";
### readVariableDefault ### readVariableDefault
```typescript ```typescript
function readVariableDefault(document: Document, id: string): unknown; function readVariableDefault(declarationElement: Element | null, id: string): unknown;
``` ```
Read a declared variable's current `default` value directly from the document's `data-composition-variables` schema attribute, bypassing the session layer. This is the same function `comp.getVariableValue()` calls internally; prefer the typed `Composition` method in session code — use this only when you're working against a raw `Document` outside of an open session (matching `buildDocument`/`buildRoots`'s "same functions the SDK uses internally" pattern above). Read a declared variable's current `default` value from the element that carries
`data-composition-variables`: normally `<html>` for a full document or the composition root for a
template/fragment. Prefer the typed `Composition` method in session code; use this helper when you
already have the declaration element.
--- ---
@@ -393,4 +396,3 @@ Determines which editing operations are available for a live element given its c
Wiring a PersistAdapter, handling errors, and restoring from version history. Wiring a PersistAdapter, handling errors, and restoring from version history.
</Card> </Card>
</CardGroup> </CardGroup>