chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Compositions
|
||||
description: "The fundamental building block of a Hyperframes video."
|
||||
---
|
||||
|
||||
A composition is an HTML document that defines a video timeline. Every clip -- video, image, audio -- lives inside a composition.
|
||||
|
||||
## Structure
|
||||
|
||||
Every composition needs a root element with `data-composition-id`:
|
||||
|
||||
```html index.html
|
||||
<div id="root" data-composition-id="root"
|
||||
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.
|
||||
|
||||
## Clip Types
|
||||
|
||||
A clip is any discrete block on the timeline, represented as an HTML element with [data attributes](/concepts/data-attributes):
|
||||
|
||||
- `<video>` -- Video clips, B-roll, A-roll
|
||||
- `<img>` -- Static images, overlays
|
||||
- `<audio>` -- Music, sound effects
|
||||
- `<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.
|
||||
|
||||
## Nested Compositions
|
||||
|
||||
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.
|
||||
|
||||
<Tabs>
|
||||
<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.
|
||||
|
||||
```html index.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>
|
||||
```
|
||||
|
||||
Each external composition file wraps its content in a `<template>` tag:
|
||||
|
||||
```html compositions/intro-anim.html
|
||||
<template id="intro-anim-template">
|
||||
<div data-composition-id="intro-anim" data-width="1920" data-height="1080">
|
||||
<div class="title">Welcome!</div>
|
||||
|
||||
<style>
|
||||
[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>
|
||||
```
|
||||
</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
|
||||
data-composition-id="card-pro"
|
||||
data-composition-src="compositions/card.html"
|
||||
data-start="0"
|
||||
data-track-index="1"
|
||||
data-variable-values='{"title":"Pro","color":"#ff4d4f"}'
|
||||
></div>
|
||||
<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>
|
||||
```
|
||||
|
||||
```html compositions/card.html
|
||||
<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>
|
||||
[data-composition-id="card"] {
|
||||
--card-color: #111827;
|
||||
}
|
||||
|
||||
[data-composition-id="card"] .title {
|
||||
color: var(--card-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 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.
|
||||
|
||||
## Listing Compositions
|
||||
|
||||
Use the [CLI](/packages/cli) to see all compositions in a project:
|
||||
|
||||
```bash
|
||||
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>
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: Data Attributes
|
||||
description: "Core attributes for controlling element timing and behavior."
|
||||
---
|
||||
|
||||
Hyperframes uses HTML data attributes to control timing, media playback, and [composition](/concepts/compositions) structure. These are the declarative building blocks of every video.
|
||||
|
||||
## Timing Attributes
|
||||
|
||||
| Attribute | Example | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `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-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
|
||||
|
||||
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":
|
||||
|
||||
```html index.html
|
||||
<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>
|
||||
<video id="outro" data-start="main" data-duration="5" data-track-index="0" src="..."></video>
|
||||
```
|
||||
|
||||
`main` resolves to second 10, `outro` resolves to second 30. If `intro`'s duration changes, downstream clips shift automatically.
|
||||
|
||||
### Offsets (Gaps and Overlaps)
|
||||
|
||||
Add `+ N` or `- N` after the ID to offset from the end of the referenced clip:
|
||||
|
||||
```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>
|
||||
Overlapping clips must be on different tracks -- clips on the same track cannot overlap in time.
|
||||
</Note>
|
||||
|
||||
<Accordion title="Relative timing rules and constraints">
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**Parsing rules** -- if the value is a valid number, it is treated as absolute seconds. Otherwise it is parsed as one of:
|
||||
- `<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>
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: Deterministic Rendering
|
||||
description: "Same input, identical output. Every time."
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## How It Works
|
||||
|
||||
The rendering pipeline is frame-by-frame and seek-driven. No realtime playback is involved -- every frame is independently seeked and captured.
|
||||
|
||||
<Steps>
|
||||
<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
|
||||
graph LR
|
||||
A["Frame Clock<br/>t = frame / fps"] --> B["Seek<br/>adapter.seekFrame(frame)"]
|
||||
B --> C["Capture<br/>beginFrame API"]
|
||||
C --> D["Encode<br/>FFmpeg"]
|
||||
D --> E["MP4"]
|
||||
style A fill:#00C4FF,color:#fff
|
||||
style B fill:#00C4FF,color:#fff
|
||||
style C fill:#00C4FF,color:#fff
|
||||
style D fill:#00C4FF,color:#fff
|
||||
style E fill:#00A8E1,color:#fff
|
||||
```
|
||||
|
||||
## What Makes It Deterministic
|
||||
|
||||
- **No wall-clock dependencies** -- rendering does not use `Date.now()`, `requestAnimationFrame`, or system timers
|
||||
- **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).
|
||||
|
||||
## Docker Mode
|
||||
|
||||
For maximum reproducibility, render in Docker:
|
||||
|
||||
```bash
|
||||
npx hyperframes render --docker --output output.mp4
|
||||
```
|
||||
|
||||
Docker mode uses an exact Chrome version and font set, ensuring:
|
||||
- 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.
|
||||
|
||||
## Preview vs. Render Parity
|
||||
|
||||
The browser preview and the rendered MP4 should match. Hyperframes achieves this through:
|
||||
|
||||
- **One runtime** -- the same `hyperframe.runtime` drives both preview and render
|
||||
- **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.
|
||||
|
||||
<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}>
|
||||
<Card title="Frame Adapters" icon="plug" href="/concepts/frame-adapters">
|
||||
Build adapters that uphold the determinism contract
|
||||
</Card>
|
||||
<Card title="Rendering" icon="film" href="/guides/rendering">
|
||||
Render to MP4 locally or in Docker
|
||||
</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>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: Frame Adapters
|
||||
description: "Bring your own animation runtime to Hyperframes."
|
||||
---
|
||||
|
||||
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>
|
||||
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.
|
||||
</Info>
|
||||
|
||||
## How It Works
|
||||
|
||||
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.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Host as Host (Engine)
|
||||
participant Adapter as Frame Adapter
|
||||
participant Chrome as Chrome / Browser
|
||||
|
||||
Host->>Adapter: init(context)
|
||||
Adapter-->>Host: ready
|
||||
Host->>Adapter: getDurationFrames()
|
||||
Adapter-->>Host: 300 frames
|
||||
|
||||
loop For each frame 0..300
|
||||
Host->>Host: normalize frame (clamp, floor)
|
||||
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 = {
|
||||
id: string;
|
||||
init?: (ctx: FrameAdapterContext) => Promise<void> | void;
|
||||
getDurationFrames: () => number;
|
||||
seekFrame: (frame: number) => Promise<void> | void;
|
||||
destroy?: () => Promise<void> | void;
|
||||
};
|
||||
```
|
||||
|
||||
## Required Semantics
|
||||
|
||||
- `getDurationFrames()` must return a finite integer >= 0
|
||||
- `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
|
||||
|
||||
The host normalizes frames before calling the adapter:
|
||||
|
||||
```typescript engine/render-loop.ts
|
||||
normalizedFrame = clamp(Math.floor(frame), 0, durationFrames);
|
||||
```
|
||||
|
||||
A typical render loop:
|
||||
|
||||
```typescript engine/render-loop.ts
|
||||
await adapter.init?.({ compositionId, fps, width, height, rootElement });
|
||||
const durationFrames = adapter.getDurationFrames();
|
||||
|
||||
for (let frame = 0; frame <= durationFrames; frame += 1) {
|
||||
await adapter.seekFrame(frame);
|
||||
// capture pixel buffer for this frame
|
||||
}
|
||||
|
||||
await adapter.destroy?.();
|
||||
```
|
||||
|
||||
## Determinism Contract
|
||||
|
||||
These rules are non-negotiable for any adapter. They are the foundation of Hyperframes' [deterministic rendering](/concepts/determinism) guarantee.
|
||||
|
||||
- Canonical clock: `t = frame / fps`
|
||||
- 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>
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
title: Variables
|
||||
description: "Parameterize compositions so the same source can render different content."
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## Declaring Variables
|
||||
|
||||
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:
|
||||
|
||||
```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":"natural-lift",
|
||||
"options":[{"value":"natural-lift","label":"Natural Lift"},{"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
|
||||
id="hero-video"
|
||||
src="assets/hero.mp4"
|
||||
data-start="0"
|
||||
data-track-index="0"
|
||||
muted
|
||||
playsinline
|
||||
data-color-grading='{
|
||||
"preset":"$gradingPreset",
|
||||
"intensity":"$gradingIntensity",
|
||||
"adjust":{"exposure":"${gradingExposure}","vibrance":"$gradingVibrance"},
|
||||
"details":{"vignette":0.15,"vignetteFeather":0.72,"grain":0.08,"grainSize":0.25},
|
||||
"effects":{"blur":0.1},
|
||||
"colorSpace":"rec709"
|
||||
}'
|
||||
></video>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
When the same composition is embedded multiple times, each host's
|
||||
`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?
|
||||
|
||||
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:
|
||||
|
||||
```html compositions/hero.html
|
||||
<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 data-composition-variables='[
|
||||
{"id":"title","type":"string","label":"Title","default":"Untitled"},
|
||||
{"id":"color","type":"color","label":"Color","default":"#111827"}
|
||||
]'>
|
||||
<body>
|
||||
<div data-composition-id="card" data-width="1920" data-height="1080">
|
||||
<h1 class="card-title"></h1>
|
||||
|
||||
<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.
|
||||
|
||||
## Declarative Bindings (No Script Required)
|
||||
|
||||
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:
|
||||
|
||||
- **`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:
|
||||
|
||||
```html
|
||||
<img class="clip" data-start="0" data-duration="5"
|
||||
data-var-src="heroImage" src="fallback.jpg" />
|
||||
```
|
||||
|
||||
<Note>
|
||||
`data-var-src` is only honored on media elements (`img`, `video`, `audio`,
|
||||
`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:
|
||||
|
||||
```html
|
||||
<h1 class="clip" data-start="0" data-duration="5" data-var-text="title">Fallback title</h1>
|
||||
```
|
||||
|
||||
- **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:
|
||||
|
||||
```css
|
||||
.card-title { color: var(--accent); font-family: var(--brandFont), sans-serif; }
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
<Note>
|
||||
**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)
|
||||
|
||||
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
|
||||
<div
|
||||
data-composition-id="card-pro"
|
||||
data-composition-src="compositions/card.html"
|
||||
data-start="0"
|
||||
data-track-index="1"
|
||||
data-variable-values='{"title":"Pro","color":"#ff4d4f"}'
|
||||
></div>
|
||||
<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>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## CLI Overrides (Top-level Renders)
|
||||
|
||||
Pass variable values at render time with `--variables` or `--variables-file`. These override the declared defaults for the top-level composition:
|
||||
|
||||
```bash Terminal
|
||||
# Inline JSON
|
||||
npx hyperframes render --variables '{"title":"Q4 Report","color":"#1d4ed8"}' --output q4.mp4
|
||||
|
||||
# JSON file
|
||||
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.
|
||||
|
||||
<Note>
|
||||
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
|
||||
|
||||
Use `--batch` when the same composition should render once per data row:
|
||||
|
||||
```json rows.json
|
||||
[
|
||||
{ "name": "Alice", "title": "Q4 Report" },
|
||||
{ "name": "Bob", "title": "Renewal Plan" }
|
||||
]
|
||||
```
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes render --batch rows.json --output "renders/{name}.mp4" --strict-variables
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
For small compositions, `--batch-concurrency 2` can run rows in parallel. The default is `1` because each individual render already parallelizes across render workers.
|
||||
|
||||
## Layering and Precedence
|
||||
|
||||
Variable values are resolved by merging three sources, lowest to highest precedence:
|
||||
|
||||
| Source | Precedence | Where declared |
|
||||
|--------|-----------|---------------|
|
||||
| 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.
|
||||
|
||||
## Validation
|
||||
|
||||
The linter checks variable declarations statically:
|
||||
|
||||
```bash Terminal
|
||||
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.
|
||||
|
||||
At render time, the CLI validates `--variables` against the schema and reports issues as warnings (or errors with `--strict-variables`):
|
||||
|
||||
- **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>
|
||||
Reference in New Issue
Block a user