HTML is the source of truth for video. A composition is an HTML file with data-* attributes for timing, a GSAP timeline for animation, and CSS for appearance. The framework handles clip visibility, media playback, and timeline sync.
When this skill runs inside Open Design (i.e. $OD_PROJECT_DIR is set), the
output flow is fixed: only the rendered .mp4 should land in the project
root. Composition source files (hyperframes.json, meta.json,
index.html, assets) belong inside a hidden cache directory so they don't
clutter the user's FileViewer or the chat's "produced files" chips.
Render workflow inside OD — fast path:
For most OD requests ("test video", "5s product reveal", "demo clip"), do NOT write the composition HTML from scratch. Use HyperFrames' built-in scaffold and edit only what the prompt actually changes. The "author from scratch" path costs minutes of model output and silent chat-tool time; the scaffold path costs seconds.
# 1. Pick a hidden cache slot. Dotfile prefix → OD's project file
# listing skips it, so the source files never clutter the chat.
COMP_REL=".hyperframes-cache/$(date +%s)-$(openssl rand -hex 2)"
COMP="$OD_PROJECT_DIR/$COMP_REL"
# 2. Get an immediately-renderable scaffold (hyperframes.json,
# meta.json, index.html with GSAP CDN + window.__timelines.main
# already registered). This runs in your shell — pure file copy,
# no Chrome, no network beyond the npx cache.
npx hyperframes init "$COMP" --example blank --skip-skills --non-interactive
# 3. Edit ONLY $COMP/index.html — change `data-duration` on the root
# if you need a non-default length, swap the placeholder palette
# in <style>, add 1–3 clip <div>s for text/imagery, and append the
# matching GSAP tweens inside the existing
# `window.__timelines["main"] = gsap.timeline({paused:true})` block.
# Keep edits minimal; the scaffold is already valid HF.
# 4. Dispatch render through the OD daemon. Do NOT run `npx hyperframes
# render` from this shell — the daemon runs it for you in an
# unsandboxed process. (Many agent CLIs, Claude Code in particular,
# wrap Bash in macOS sandbox-exec under which puppeteer's Chrome
# subprocess hangs partway through frame capture. The daemon process
# is unsandboxed, so renders complete reliably.)
#
# The dispatcher returns within ~1s with a {taskId}; drive the
# render to completion by looping `od media wait <taskId>` calls.
# Each call long-polls up to 25s (well under your shell tool's
# default 30s cap) and exits 0/2/5 to signal done/running/failed.
out=$(node "$OD_BIN" media generate \
--project "$OD_PROJECT_ID" \
--surface video \
--model hyperframes-html \
--output "<descriptive-name>.mp4" \
--composition-dir "$COMP_REL")
ec=$?
task_id=$(printf '%s\n' "$out" | tail -1 | jq -r '.taskId // empty')
since=$(printf '%s\n' "$out" | tail -1 | jq -r '.nextSince // 0')
while [ "$ec" -eq 2 ] && [ -n "$task_id" ]; do
out=$(node "$OD_BIN" media wait "$task_id" --since "$since")
ec=$?
since=$(printf '%s\n' "$out" | tail -1 | jq -r '.nextSince // '"$since")
done
[ "$ec" -ne 0 ] && { echo "$out" >&2; exit "$ec"; }
Each generate and each wait call lasts at most ~25s, so the agent
shell tool's default ~30s cap never fires. Progress lines from HF
(Capturing frame N/M) stream to stderr live throughout the loop.
When the render finishes, the last stdout line is
{"file": { "name": "<output>", "size": …, "kind": "video", … }} —
quote file.name in your reply so the user knows what was produced.
Skip the Visual Identity Gate inside OD. The HARD-GATE section below (under "Approach") tells you to read DESIGN.md / visual-style.md or stop and ask 3 mood questions before writing any composition. That gate is for standalone HF projects. OD projects already have their own design-system layer — the user picked their visual direction at project creation time. For an OD test render, default to: dark canvas (#0b0b0f), one warm accent (#ffb76b), one cool accent (#7da4ff), restrained motion. Only ask for stylistic input if the user's prompt is too vague to even pick a subject (very rare).
When to skip the scaffold and write from scratch: only when the user explicitly asks for something the blank template clearly can't host (e.g. multi-composition timelines, audio-reactive overlays, captions synced to a TTS track they've already generated). For everything else, init + edit is the default path.
The lighter HF subcommands you CAN still run from your own shell (they don't need to spawn Chrome):
npx hyperframes lint "$COMP" — validate composition before dispatchnpx hyperframes transcribe <audio> — generate captionsnpx hyperframes tts <text> — generate narrationReserve the daemon dispatch for render/inspect/preview (anything
Chrome-bound).
Do NOT call od media generate --model hyperframes-html — that
dispatcher path returns a 400 (AGENT_RENDERED) on purpose. HyperFrames
is rendered by you directly via npx.
Do NOT drop hyperframes.json / meta.json / index.html in the
project root; OD's file listing scans recursively and the user would see
three unrelated files appear in the chat.
For CLI options beyond render (lint, preview, transcribe, tts, inspect,
benchmark) call them directly from your shell tool when the task warrants
it (e.g., generate TTS audio into the cache before referencing it from
the composition).
Before writing HTML, think at a high level:
For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
Check in this order:
style_prompt_full and structured fields. (Note: visual-style.md is a project-specific file. visual-styles.md is the style library with 8 named presets — different files.)## Style Prompt (one paragraph), ## Colors (3-5 hex values with roles), ## Typography (1-2 font families), ## What NOT to Do (3-5 anti-patterns).Every composition must trace its palette and typography back to a DESIGN.md, visual-style.md, or explicit user direction. If you're reaching for #333, #3b82f6, or Roboto — you skipped this step.
</HARD-GATE>
For motion defaults, sizing, entrance patterns, and easing — follow house-style.md. The house style handles HOW things move. The DESIGN.md handles WHAT things look like.
Position every element where it should be at its most visible moment — the frame where it's fully entered, correctly placed, and not yet exiting. Write this as static HTML+CSS first. No GSAP yet.
Why this matters: If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing the final layout. Overlaps are invisible until the video renders. By building the end state first, you can see and fix layout problems before adding any motion.
.scene-content container MUST fill the full scene using width: 100%; height: 100%; padding: Npx; with display: flex; flex-direction: column; gap: Npx; box-sizing: border-box. Use padding to push content inward — NEVER position: absolute; top: Npx on a content container. Absolute-positioned content containers overflow when content is taller than the remaining space. Reserve position: absolute for decoratives only.gsap.from() — animate FROM offscreen/invisible TO the CSS position. The CSS position is the ground truth; the tween describes the journey to get there.gsap.to() — animate TO offscreen/invisible FROM the CSS position./* scene-content fills the scene, padding positions content */
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 24px;
box-sizing: border-box;
}
.title {
font-size: 120px;
}
.subtitle {
font-size: 42px;
}
/* Container fills any scene size (1920x1080, 1080x1920, etc).
Padding positions content. Flex + gap handles spacing. */
WRONG — hardcoded dimensions and absolute positioning:
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
display: flex; /* ... */
}
// Step 3: Animate INTO those positions
tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
// Step 4: Animate OUT from those positions
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
If element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist — but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.
Layered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching unintentional overlap — two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.
| Attribute | Required | Values |
|---|---|---|
id |
Yes | Unique identifier |
data-start |
Yes | Seconds or clip ID reference ("el-1", "intro + 2") |
data-duration |
Required for img/div/compositions | Seconds. Video/audio defaults to media duration. |
data-track-index |
Yes | Integer. Same-track clips cannot overlap. |
data-media-start |
No | Trim offset into source (seconds) |
data-volume |
No | 0-1 (default 1) |
data-track-index does not affect visual layering — use CSS z-index.
| Attribute | Required | Values |
|---|---|---|
data-composition-id |
Yes | Unique composition ID |
data-start |
Yes | Start time (root composition: use "0") |
data-duration |
Yes | Takes precedence over GSAP timeline duration |
data-width / data-height |
Yes | Pixel dimensions (1920x1080 or 1080x1920) |
data-composition-src |
No | Path to external HTML file |
Sub-compositions loaded via data-composition-src use a <template> wrapper. Standalone compositions (the main index.html) do NOT use <template> — they put the data-composition-id div directly in <body>. Using <template> on a standalone file hides all content from the browser and breaks rendering.
Sub-composition structure:
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- content -->
<style>
[data-composition-id="my-comp"] {
/* scoped styles */
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// tweens...
window.__timelines["my-comp"] = tl;
</script>
</div>
</template>
Load in root: <div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>
Video must be muted playsinline. Audio is always a separate <audio> element:
<video
id="el-v"
data-start="0"
data-duration="30"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
<audio
id="el-a"
data-start="0"
data-duration="30"
data-track-index="2"
src="video.mp4"
data-volume="1"
></audio>
{ paused: true } — the player controls playbackwindow.__timelines["<composition-id>"] = tl
data-duration, not from GSAP timeline lengthDeterministic: No Math.random(), Date.now(), or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).
GSAP: Only animate visual properties (opacity, x, y, scale, rotation, color, backgroundColor, borderRadius, transforms). Do NOT animate visibility, display, or call video.play()/audio.play().
Animation conflicts: Never animate the same property on the same element from multiple timelines simultaneously.
No repeat: -1: Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration: repeat: Math.ceil(duration / cycleDuration) - 1.
Synchronous timeline construction: Never build timelines inside async/await, setTimeout, or Promises. The capture engine reads window.__timelines synchronously after page load. Fonts are embedded by the compiler, so they're available immediately — no need to wait for font loading.
Never do:
window.__timelines registration<audio>
data-layer (use data-track-index) or data-end (use data-duration)data-composition-id
repeat: -1 on any timeline or tween — always finite repeatsasync, setTimeout, Promise)gsap.set() on clip elements from later scenes — they don't exist in the DOM at page load. Use tl.set(selector, vars, timePosition) inside the timeline at or after the clip's data-start time instead.<br> in content text — forced line breaks don't account for actual rendered font width. Text that wraps naturally + a <br> produces an extra unwanted break, causing overlap. Let text wrap via max-width instead. Exception: short display titles where each word is deliberately on its own line (e.g., "THE\nIMMORTAL\nGAME" at 130px).Every multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.
gsap.from(). No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens.gsap.to() that animates opacity to 0, y offscreen, scale to 0, or any other "out" animation before a transition fires. The transition IS the exit. The outgoing scene's content MUST be fully visible at the moment the transition starts.gsap.to(..., { opacity: 0 }) is allowed.WRONG — exit animation before transition:
// BANNED — this empties the scene before the transition can use it
tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);
tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);
// transition fires on empty frame
RIGHT — entrance only, transition handles exit:
// Scene 1 entrance animations
tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
// NO exit tweens — transition at 7.2s handles the scene change
// Scene 2 entrance animations
tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);
font-variant-numeric: tabular-nums on number columnsWhen no visual-style.md or animation direction is provided, follow house-style.md for aesthetic defaults.
font-family you want in CSS — the compiler embeds supported fonts automatically. If a font isn't supported, the compiler warns.crossorigin="anonymous" to external mediawindow.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })
index.html; sub-compositions use ../
npx hyperframes lint and npx hyperframes validate both passnpx hyperframes inspect passes, or every reported overflow is intentionally markedhyperframes inspect runs the composition in headless Chrome, seeks through the timeline, and maps visual layout issues with timestamps, selectors, bounding boxes, and fix hints. Run it after lint and validate:
npx hyperframes inspect
npx hyperframes inspect --json
Failures usually mean text is spilling out of a bubble/card, a fixed-size label is clipping dynamic copy, or text has moved off the canvas. Fix by increasing container size or padding, reducing font size or letter spacing, adding a real max-width so text wraps inside the container, or using window.__hyperframes.fitTextFontSize(...) for dynamic copy.
Use --samples 15 for dense videos and --at 1.5,4,7.25 for specific hero frames. Repeated static issues are collapsed by default to avoid flooding agent context. If overflow is intentional for an entrance/exit animation, mark the element or ancestor with data-layout-allow-overflow. If a decorative element should never be audited, mark it with data-layout-ignore.
hyperframes layout is the compatibility alias for the same check.
hyperframes validate runs a WCAG contrast audit by default. It seeks to 5 timestamps, screenshots the page, samples background pixels behind every text element, and computes contrast ratios. Failures appear as warnings:
⚠ WCAG AA contrast warnings (3):
· .subtitle "secondary text" — 2.67:1 (need 4.5:1, t=5.3s)
If warnings appear:
hyperframes validate until cleanUse --no-contrast to skip if iterating rapidly and you'll check later.
After authoring animations, run the animation map to verify choreography:
node skills/hyperframes/scripts/animation-map.mjs <composition-dir> \
--out <composition-dir>/.hyperframes/anim-map
Outputs a single animation-map.json with:
"#card1 animates opacity+y over 0.50s. moves 23px up. fades in. ends at (120, 200)"
"3 elements stagger at 120ms")offscreen, collision, invisible, paced-fast (under 0.2s), paced-slow (over 2s)Read the JSON. Scan summaries for anything unexpected. Check every flag — fix or justify. Verify the timeline shows the intended choreography rhythm. Re-run after fixes.
Skip on small edits (fixing a color, adjusting one duration). Run on new compositions and significant animation changes.
references/captions.md — Captions, subtitles, lyrics, karaoke synced to audio. Tone-adaptive style detection, per-word styling, text overflow prevention, caption exit guarantees, word grouping. Read when adding any text synced to audio timing.
references/tts.md — Text-to-speech with Kokoro-82M. Voice selection, speed tuning, TTS+captions workflow. Read when generating narration or voiceover.
references/audio-reactive.md — Audio-reactive animation: map frequency bands and amplitude to GSAP properties. Read when visuals should respond to music, voice, or sound.
references/css-patterns.md — CSS+GSAP marker highlighting: highlight, circle, burst, scribble, sketchout. Deterministic, fully seekable. Read when adding visual emphasis to text.
references/typography.md — Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. Always read — every composition has text.
references/motion-principles.md — Motion design principles: easing as emotion, timing as weight, choreography as hierarchy, scene pacing, ambient motion, anti-patterns. Read when choreographing GSAP animations.
visual-styles.md — 8 named visual styles (Swiss Pulse, Velvet Standard, Deconstructed, Maximalist Type, Data Drift, Soft Signal, Folk Frequency, Shadow Cut) with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating DESIGN.md.
house-style.md — Default motion, sizing, and color palettes when no style is specified.
patterns.md — PiP, title cards, slide show patterns.
data-in-motion.md — Data, stats, and infographic patterns.
references/transcript-guide.md — Transcription commands, whisper models, external APIs, troubleshooting.
references/dynamic-techniques.md — Dynamic caption animation techniques (karaoke, clip-path, slam, scatter, elastic, 3D).
references/transitions.md — Scene transitions: crossfades, wipes, reveals, shader transitions. Energy/mood selection, CSS vs WebGL guidance. Always read for multi-scene compositions — scenes without transitions feel like jump cuts.
@hyperframes/shader-transitions (packages/shader-transitions/) — read package source, not skill files.GSAP patterns and effects are in the /gsap skill.