
Hyperframes
- 8 installs
- 33 repo stars
- Updated July 27, 2026
- dirnbauer/webconsulting-skills
This is a copy of hyperframes by heygen-com - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
hyperframes is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- hyperframes
- AI & Agent Building
- AI-coding skill
Hyperframes by the numbers
- 8 all-time installs (skills.sh)
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dirnbauer/webconsulting-skills --skill hyperframesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | July 27, 2026 |
| Repository | dirnbauer/webconsulting-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
HyperFrames
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.
Approach
Discovery (exploratory requests only)
For open-ended requests ("make me a product launch video", "create something for our brand") where the user hasn't committed to a direction, understand intent before picking colors:
- Audience — who watches this? Developers? Executives? General consumers?
- Platform — where does it play? Social (15s), website hero, product demo, internal?
- Priority — what matters most? Motion quality? Content accuracy? Brand fidelity? Speed?
- Variations — does the user want options, or a single best shot?
For specific requests ("add a title card", "fix the timing on scene 3"), skip discovery.
For exploratory requests, consider offering 2-3 variations that differ meaningfully — not just color swaps, but different pacing, energy levels, or structural approaches. One safe/expected, one ambitious. Don't mandate this — it's a tool available when appropriate.
Step 1: Design system
If a design spec exists in the project, read it first. Look in precedence order: frame.md → design.md → DESIGN.md (design.md and DESIGN.md are different files on Linux — check both casings; frame.md is always lowercase, no FRAME.md variant). frame.md is the preferred spec for video/hyperframes projects and wins if more than one exists; it uses the same format as design.md. It's the source of truth for brand colors, fonts, and constraints. Use its exact values — don't invent colors or substitute fonts. Any format works (YAML frontmatter, prose, tables — just extract the values).
If it names fonts you can't find locally (no fonts/ directory with .woff2 files, not a built-in font), warn the user before writing HTML: "the spec specifies [font name] but no font files found. Please add .woff2 files to fonts/ or I'll fall back to [closest built-in alternative]."
If no frame.md or design.md exists, offer the user a choice:
1. User named a style or mood? → Read visual-styles.md for the 8 named presets. Pick the closest match. 2. Want to browse options visually? → Run the design picker: read references/design-picker.md for the full workflow. This serves a visual picker page. The user configures mood, palette, typography, and motion in the browser, then copies the generated design.md and pastes it back into the conversation. 3. Want to skip and go fast? → Ask: mood, light or dark, any brand colors/fonts? Then pick a palette from house-style.md.
The design spec defines the brand. It does not define video composition rules. Those come from references/video-composition.md and house-style.md. Use brand colors at video-appropriate scale — not at web-UI opacity.
Step 2: Prompt expansion
Always run on every composition (except single-scene pieces and trivial edits). This step grounds the user's intent against the design spec (frame.md or design.md) and house-style.md and produces a consistent intermediate that every downstream agent reads the same way.
Read references/prompt-expansion.md for the full process and output format.
Step 3: Plan
Before writing HTML, think at a high level:
1. What — what should the viewer experience? Identify the narrative arc, key moments, and emotional beats. 2. Structure — how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions). 3. Rhythm — declare your scene rhythm before implementing. Which scenes are quick hits, which are holds, where do shaders land, where does energy peak. Name the pattern: fast-fast-SLOW-fast-SHADER-hold. Read references/beat-direction.md for rhythm templates. 4. Timing — which clips drive the duration, where do transitions land, what's the pacing. 5. Layout — build the end-state first. See "Layout Before Animation" below. 6. Animate — then add motion using the rules below.
Build what was asked. A request for "a title card" is not a request for "a title card + 3 supporting scenes + ambient music + captions." Every scene, every element, every tween should earn its place. If additional scenes or elements would genuinely improve the piece, propose them — don't add them.
For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
<HARD-GATE> Before writing ANY composition HTML — verify you have a visual identity from Step 1. If you're reaching for #333, #3b82f6, or Roboto, you skipped it. </HARD-GATE>
Layout Before Animation
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.
The process
1. Identify the hero frame for each scene — the moment when the most elements are simultaneously visible. This is the layout you build. 2. Write static CSS for that frame. The .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. 3. Add entrances with `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. (In sub-compositions loaded via data-composition-src, prefer gsap.fromTo() — see load-bearing GSAP rules in references/motion-principles.md.) 4. Add exits with `gsap.to()` — animate TO offscreen/invisible FROM the CSS position.
Example
/* 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);When elements share space across time
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.
What counts as intentional overlap
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.
Data Attributes
All Clips
| 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.
Composition Clips
| 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 |
data-variable-values | No | JSON object of per-instance variable overrides on a sub-comp host |
On the root <html> element:
| Attribute | Required | Values |
|---|---|---|
data-composition-variables | No | JSON array of declared variables (id/type/label/default) — drives Studio editing UI and provides defaults for getVariables() |
Composition Structure
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>
Variables (Parametrized Compositions)
Render the same composition with different content — title, theme color, prices, captions — without editing the source HTML.
Three-step pattern:
1. Declare variables on the composition's <html> root with data-composition-variables. Each entry needs id, type (one of string, number, color, boolean, enum), label, and default. Enum entries also need options: [{value, label}, ...]. 2. Read the resolved values inside the composition's script with window.__hyperframes.getVariables(). Returns the merged result of declared defaults + per-instance overrides + CLI overrides. 3. Override at render time with npx hyperframes render --variables '{...}' (top-level) or with data-variable-values='{...}' on the host element (per-instance for sub-comps).
<!doctype html>
<html
data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Hello"},
{"id":"theme","type":"enum","label":"Theme","default":"light","options":[
{"value":"light","label":"Light"},
{"value":"dark","label":"Dark"}
]}
]'
>
<body>
<div data-composition-id="root" data-width="1920" data-height="1080">
<h1 id="hero" class="clip" data-start="0" data-duration="3"></h1>
<script>
const { title, theme } = window.__hyperframes.getVariables();
document.getElementById("hero").textContent = title;
document.body.dataset.theme = theme;
</script>
</div>
</body>
</html># Dev preview uses declared defaults
npx hyperframes preview
# Render with overrides
npx hyperframes render --variables '{"title":"Q4 Report","theme":"dark"}' --output q4.mp4
# Or from a JSON file
npx hyperframes render --variables-file ./vars.jsonSub-composition per-instance values: the same getVariables() works inside sub-comps loaded via data-composition-src. Each host element passes its own values:
<div
data-composition-id="card-pro"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"Pro","price":"$29"}'
></div>
<div
data-composition-id="card-enterprise"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"Enterprise","price":"Custom"}'
></div>The runtime layers each host's data-variable-values over the sub-comp's declared defaults on a per-instance basis, so the same source can be embedded multiple times with different content.
Rules of thumb:
- Always provide a sensible
defaultfor every declared variable. Dev preview uses defaults — without them, the composition won't render correctly until--variablesis provided. - Read variables once at the top of the script (
const { title } = ...), not inside frame loops or event handlers —getVariables()allocates a fresh object per call. - Use
--strict-variablesin CI to fail fast on undeclared keys or type mismatches. - Variable types are validated at render time.
string,number,boolean, andcolor(hex string) checktypeof;enumchecks the value is in the declaredoptions.
Video and Audio
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>Timeline Contract
- All timelines start
{ paused: true }— the player controls playback - Register every timeline:
window.__timelines["<composition-id>"] = tl - Framework auto-nests sub-timelines — do NOT manually add them
- Duration comes from
data-duration, not from GSAP timeline length - Never create empty tweens to set duration
Rules (Non-Negotiable)
Deterministic: 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:
1. Forget window.__timelines registration 2. Use video for audio — always muted video + separate <audio> 3. Nest video inside a timed div — use a non-timed wrapper 4. Use data-layer (use data-track-index) or data-end (use data-duration) 5. Animate video element dimensions — animate a wrapper div 6. Call play/pause/seek on media — framework owns playback 7. Create a top-level container without data-composition-id 8. Use repeat: -1 on any timeline or tween — always finite repeats 9. Build timelines asynchronously (inside async, setTimeout, Promise) 10. Use 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. 11. Use <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).
Scene Transitions (Non-Negotiable)
Every multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.
1. ALWAYS use transitions between scenes. No jump cuts. No exceptions. 2. ALWAYS use entrance animations on every scene. Every element animates IN via gsap.from(). No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens. 3. NEVER use exit animations except on the final scene. This means: NO 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. 4. Final scene only: The last scene may fade elements out (e.g., fade to black). This is the ONLY scene where 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 frameRIGHT — 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);Animation Guardrails
- Offset first animation 0.1-0.3s (not t=0)
- Vary eases across entrance tweens — use at least 3 different eases per scene
- Don't repeat an entrance pattern within a scene
- Avoid full-screen linear gradients on dark backgrounds (H.264 banding — use radial or solid + localized glow)
- 60px+ headlines, 20px+ body, 16px+ data labels for rendered video
font-variant-numeric: tabular-numson number columns
If no frame.md or design.md exists, follow house-style.md for aesthetic defaults.
Typography and Assets
- Built-in fonts: Write the
font-familyyou want in CSS — the compiler embeds supported fonts automatically. - Custom fonts: If the spec (
frame.mdordesign.md) names a font that isn't built-in, the user must provide.woff2files in afonts/directory. If missing, warn before writing HTML. When files exist, add@font-facedeclarations pointing to the local files. - Add
crossorigin="anonymous"to external media - For dynamic text overflow, use
window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight }) - All files live at the project root alongside
index.html; sub-compositions use../
Editing Existing Compositions
- Read actual files, don't guess. When editing, extending, or creating companion compositions, read the existing source. Don't reconstruct hex codes from memory. Don't guess GSAP easing patterns. The composition IS the spec — extract exact values from it.
- Match existing fonts, colors, animation patterns from what you read
- Only change what was requested
- Preserve timing of unrelated clips
Output Checklist
Fast (run immediately, block on results):
- [ ]
npx hyperframes lintandnpx hyperframes validateboth pass - [ ] Design adherence verified if a design spec (
frame.mdordesign.md) exists
Slow (run in parallel while presenting the preview to the user):
- [ ]
npx hyperframes inspectpasses, or every reported overflow is intentionally marked - [ ] Contrast warnings addressed (see Quality Checks below)
- [ ] Animation choreography verified (see Quality Checks below)
Quality Checks
Visual Inspect
hyperframes 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 --jsonFailures 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.
Contrast
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:
- On dark backgrounds: brighten the failing color until it clears 4.5:1 (normal text) or 3:1 (large text, 24px+ or 19px+ bold)
- On light backgrounds: darken it
- Stay within the palette family — don't invent a new color, adjust the existing one
- Re-run
hyperframes validateuntil clean
Use --no-contrast to skip if iterating rapidly and you'll check later.
Design Adherence
If a design spec (frame.md or design.md) exists, verify the composition follows it after authoring. Read the HTML and check:
1. Colors — every hex value in the composition appears in the spec's palette section (however the user labeled it: Colors, Palette, Theme, etc.). Flag any invented colors. 2. Typography — font families and weights match the spec's type spec. No substitutions. 3. Corners — border-radius values match the declared corner style, if specified. 4. Spacing — padding and gap values fall within the declared density range, if specified. 5. Depth — shadow usage matches the declared depth level, if specified (flat = none, subtle = light, layered = glows). 6. Avoidance rules — if the spec has a section listing things to avoid (commonly "What NOT to Do", "Don'ts", "Anti-patterns", or "Do's and Don'ts"), verify none are present.
Report violations as a checklist. Fix each one before serving.
If no design spec exists (house-style-only path), verify:
1. Palette consistency — the same bg, fg, and accent colors are used across all scenes. No per-scene color invention. 2. No lazy defaults — check the composition against house-style.md's "Lazy Defaults to Question" list. If any appear, they must be a deliberate choice for the content, not a default.
Animation Map
After authoring animations, run the animation map to verify choreography:
node skills/hyperframes/scripts/animation-map.mjs <composition-dir> \
--out <composition-dir>/.hyperframes/anim-mapOutputs a single animation-map.json with:
- Per-tween summaries:
"#card1 animates opacity+y over 0.50s. moves 23px up. fades in. ends at (120, 200)" - ASCII timeline: Gantt chart of all tweens across the composition duration
- Stagger detection: reports actual intervals (
"3 elements stagger at 120ms") - Dead zones: periods over 1s with no animation — intentional hold or missing entrance?
- Element lifecycles: first/last animation time, final visibility
- Scene snapshots: visible element state at 5 key timestamps
- Flags:
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 (loaded on demand)
- [references/captions.md](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/audio-reactive.md](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](references/css-patterns.md) — CSS+GSAP marker highlighting: highlight, circle, burst, scribble, sketchout. Deterministic, fully seekable. Read when adding visual emphasis to text.
- [references/video-composition.md](references/video-composition.md) — Video-medium rules: density, color presence, scale, frame composition, the design spec as brand not layout. Always read — these override web instincts.
- [references/beat-direction.md](references/beat-direction.md) — Beat planning: concept, mood, choreography verbs, rhythm templates, transition decisions, depth layers. Always read for multi-scene compositions.
- [references/typography.md](references/typography.md) — Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. Always read — every composition has text.
- [references/motion-principles.md](references/motion-principles.md) — Motion design principles, image motion treatment, load-bearing GSAP rules. Always read — every composition has motion.
- [references/techniques.md](references/techniques.md) — 13 primitive animation techniques with code patterns: SVG drawing, Canvas 2D, CSS 3D, kinetic type, Lottie, video compositing, typing, variable fonts, MotionPath, velocity transitions, audio-reactive, clip-path reveals, WebGL shaders. Adapt the patterns — don't copy-paste. (For pre-built UI templates — terminal chrome, device mockups, moodboard layouts — see
registry/blocks/.) - [references/html-in-canvas-patterns.md](references/html-in-canvas-patterns.md) — HTML-in-Canvas patterns: live DOM as GPU texture via
drawElementImage+layoutsubtree. Shared boilerplate + ~6 effect recipes (iPhone/MacBook mockups, liquid glass, magnetic, portal, shatter, text cursor). Use for 1–3 hero beats per video. - [references/narration.md](references/narration.md) — Pacing, tone, script structure, number pronunciation, opening line patterns. Read when the composition includes voiceover or TTS.
- [references/design-picker.md](references/design-picker.md) — Create a design.md via visual picker. Read when no
frame.mdordesign.mdexists and the user wants to create one. - [visual-styles.md](visual-styles.md) — 8 named visual styles with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating a design spec.
- [house-style.md](house-style.md) — Default motion, sizing, and color palettes when no
frame.mdordesign.mdis specified. - [patterns.md](patterns.md) — PiP, title cards, slide show patterns.
- [data-in-motion.md](data-in-motion.md) — Data, stats, and infographic patterns.
- [references/transcript-guide.md](references/transcript-guide.md) — Caption-side transcript handling: input formats, mandatory quality check, cleaning JS, OpenAI/Groq API fallback, "if no transcript exists" flow. (For the
transcribeCLI invocation, model selection rules, and the.engotcha, see thehyperframes-mediaskill.) - [references/dynamic-techniques.md](references/dynamic-techniques.md) — Dynamic caption animation techniques (karaoke, clip-path, slam, scatter, elastic, 3D).
- [references/transitions.md](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.
- transitions/catalog.md — Hard rules, scene template, and routing to per-type implementation code.
- Shader transitions are in
@hyperframes/shader-transitions(packages/shader-transitions/) — read package source, not skill files.
GSAP patterns and effects are in the /gsap skill.
---
Credits & Attribution
This skill is based on the excellent work by [HeyGen](https://www.heygen.com/).
Original repository: https://github.com/heygen-com/hyperframes
Copyright (c) HeyGen - HyperFrames HTML-to-video composition and workflow guidance (Apache 2.0)
Special thanks to HeyGen for their generous open-source contributions, which helped shape this skill collection. Adapted by webconsulting.at for this skill collection
Data in Motion
Light guidance for data and stats in video compositions. The house style handles aesthetics — this just addresses data-specific pitfalls.
Visual Continuity
When successive stats belong to the same concept (Q1 → Q2 → Q3 → Q4, or three metrics for the same product), keep them in the same visual space with the same aesthetic. Only the VALUE changes. An aesthetic change should signal a new concept, not just a new number.
Numbers Need Visual Weight
A number on its own floats in empty space. Pair every metric with a visual element that gives it presence — a proportional fill bar, a background color shift, a shape that represents the value, a progress ring. The visual doesn't need to be a chart — it just needs to fill the frame and make the data feel tangible rather than just text on a background.
Avoid Web Patterns
- No pie charts — hard to compare, looks like PowerPoint
- No multi-axis charts — viewer can't study intersections in a 3-second window
- No 6-panel dashboards — 2-3 related metrics side-by-side is fine, 6+ is a web pattern
- No gridlines, tick marks, or legends — visual noise that adds nothing in motion
- No chart library output — build with GSAP + SVG/CSS, not D3 or Chart.js
House Style
Creative direction for compositions when no design spec (frame.md or design.md) is provided. These are starting points — override anything that doesn't serve the content. When a spec exists, its brand values take precedence; house-style fills gaps.
Before Writing HTML
1. Interpret the prompt. Generate real content. A recipe lists real ingredients. A HUD has real readouts. 2. Pick a palette. Light or dark? Declare bg, fg, accent before writing code. 3. Pick typefaces. Run the font discovery script in references/typography.md — or pick a font you already know that fits the theme. The script broadens your options; it's not the only source.
Lazy Defaults to Question
These patterns are AI design tells — the first thing every LLM reaches for. If you're about to use one, pause and ask: is this a deliberate choice for THIS content, or am I defaulting?
- Gradient text (
background-clip: text+ gradient) - Left-edge accent stripes on cards/callouts
- Cyan-on-dark / purple-to-blue gradients / neon accents
- Pure
#000or#fff(tint toward your accent hue instead) - Identical card grids (same-size cards repeated)
- Everything centered with equal weight (lead the eye somewhere)
- Banned fonts (see references/typography.md for full list)
If the content genuinely calls for one of these — centered layout for a solemn closing, cards for a real product UI mockup, a banned font because it's the perfect thematic match — use it. The goal is intentionality, not avoidance.
Color
- Match light/dark to content: food, wellness, kids → light. Tech, cinema, finance → dark.
- One accent hue. Same background across all scenes.
- Tint neutrals toward your accent (even subtle warmth/coolness beats dead gray).
- Contrast: enforced by
hyperframes validate(WCAG AA). Text must be readable with decoratives removed. - Declare palette up front. Don't invent colors per-element.
Background Layer
Every scene needs visual depth — persistent decorative elements that stay visible while content animates in. Without these, scenes feel empty during entrance staggering.
Ideas (mix and match, 2-5 per scene):
- Radial glows (accent-tinted, low opacity, breathing scale)
- Ghost text (theme words at 3-8% opacity, very large, slow drift)
- Accent lines (hairline rules, subtle pulse)
- Grain/noise overlay, geometric shapes, grid patterns
- Thematic decoratives (orbit rings for space, vinyl grooves for music, grid lines for data)
All decoratives should have slow ambient GSAP animation — breathing, drift, pulse. Static decoratives feel dead.
Decorative count vs motion count. The "2-5 per scene" count refers to decorative _elements_. If a project's spec (frame.md or design.md) says "single ambient motion per scene", it means one looping motion applied to these decoratives (a shared breath/drift/pulse) — not one element total. A scene with 4 decoratives sharing one breathing motion is correct; a scene with 1 decorative is under-dressed.
Motion
See references/motion-principles.md for full rules. Quick: 0.3–0.6s, vary eases, combine transforms on entrances, overlap entries.
Typography
See references/typography.md for full rules. Quick: 700-900 headlines / 300-400 body, serif + sans (not two sans), 60px+ headlines / 20px+ body.
Palettes
Declare one background, one foreground, one accent before writing HTML.
| Category | Use for | File |
|---|---|---|
| Bold / Energetic | Product launches, social media, announcements | palettes/bold-energetic.md |
| Warm / Editorial | Storytelling, documentaries, case studies | palettes/warm-editorial.md |
| Dark / Premium | Tech, finance, luxury, cinematic | palettes/dark-premium.md |
| Clean / Corporate | Explainers, tutorials, presentations | palettes/clean-corporate.md |
| Nature / Earth | Sustainability, outdoor, organic | palettes/nature-earth.md |
| Neon / Electric | Gaming, tech, nightlife | palettes/neon-electric.md |
| Pastel / Soft | Fashion, beauty, lifestyle, wellness | palettes/pastel-soft.md |
| Jewel / Rich | Luxury, events, sophisticated | palettes/jewel-rich.md |
| Monochrome | Dramatic, typography-focused | palettes/monochrome.md |
Or derive from OKLCH — pick a hue, build bg/fg/accent at different lightnesses, tint everything toward that hue.
Composition Patterns
Picture-in-Picture (Video in a Frame)
Animate a wrapper div for position/size. The video fills the wrapper. The wrapper has NO data attributes.
<div
id="pip-frame"
style="position:absolute;top:0;left:0;width:1920px;height:1080px;z-index:50;overflow:hidden;"
>
<video
id="el-video"
data-start="0"
data-duration="60"
data-track-index="0"
src="talking-head.mp4"
muted
playsinline
></video>
</div>tl.to(
"#pip-frame",
{ top: 700, left: 1360, width: 500, height: 280, borderRadius: 16, duration: 1 },
10,
);
tl.to("#pip-frame", { left: 40, duration: 0.6 }, 30);Text Behind Subject (transparent webm overlay)
Put a headline _behind_ a presenter so their silhouette occludes the text. Requires a transparent cutout produced by npx hyperframes remove-background presenter.mp4 -o presenter.webm.
Three layers, plus one critical rule:
<!-- z=1 base — full opaque mp4 (lobby + presenter), always visible -->
<video
id="cf-base"
data-start="0"
data-duration="6"
data-media-start="0"
data-track-index="0"
src="presenter.mp4"
muted
playsinline
></video>
<!-- z=2 headline — visible the whole time -->
<h1
id="cf-headline"
style="position:absolute;top:50%;left:50%;
transform:translate(-50%,-50%); z-index:2; font-size:220px; font-weight:900;
color:#fff; text-shadow:0 6px 32px rgba(0,0,0,.55); clip-path:inset(0 0 100% 0);"
>
MAKE IT IN HYPERFRAMES
</h1>
<!-- z=3 cutout — same source, alpha around presenter, hidden until the cut -->
<!-- WRAPPER has the opacity, NOT the video itself (see rule below). -->
<div class="cutout-wrap" style="position:absolute;inset:0;z-index:3;opacity:0">
<video
id="cf-cutout"
data-start="0"
data-duration="6"
data-media-start="0"
data-track-index="1"
src="presenter.webm"
muted
playsinline
></video>
</div>const tl = gsap.timeline({ paused: true });
const CUT = 3.3;
// Reveal headline early
tl.to("#cf-headline", { clipPath: "inset(0 0 0% 0)", duration: 0.6, ease: "expo.out" }, 0.25);
// At the cut, flip the cutout wrapper visible — the presenter's silhouette
// punches through the headline.
tl.set(".cutout-wrap", { opacity: 1 }, CUT);
// Sentinel: extend timeline to the composition's full duration so the
// renderer doesn't bail past the last meaningful tween.
tl.set({}, {}, 6);
window.__timelines["cover-flip"] = tl;Why a wrapper div, not opacity on the video itself?
The framework forces opacity: 1 on any element with data-start/data-duration while it's "active" — that's how it manages clip lifecycles. A CSS opacity: 0 on the video element is silently overwritten. Wrap the video in a div with no data-* attributes; the wrapper is owned by your CSS/GSAP.
Why both videos at `data-start="0"`?
So both decode in sync from t=0. Late-mounting the cutout (data-start=3.3) makes Chrome do a seek + decoder warm-up at mount, which can land a frame off the base mp4 — visible as a one-frame jitter at the cut.
Color match: remove-background defaults to --quality balanced (crf 18) which keeps the cutout's RGB nearly identical to the source mp4 — minimal edge halo or color shift when overlaid. Use --quality best (crf 12) for hero shots; only drop to --quality fast (crf 30) when the cutout sits over a _different_ background and the size matters.
Title Card with Fade
<div
id="title-card"
data-start="0"
data-duration="5"
data-track-index="5"
style="display:flex;align-items:center;justify-content:center;background:#111;z-index:60;"
>
<h1 style="font-size:64px;color:#fff;opacity:0;">My Video Title</h1>
</div>tl.to("#title-card h1", { opacity: 1, duration: 0.6 }, 0.3);
tl.to("#title-card", { opacity: 0, duration: 0.5 }, 4);Slide Show with Section Headers
Use separate elements on the same track, each with its own time range. Slides auto-mount/unmount based on data-start/data-duration.
<div class="slide" data-start="0" data-duration="30" data-track-index="3">...</div>
<div class="slide" data-start="30" data-duration="25" data-track-index="3">...</div>
<div class="slide" data-start="55" data-duration="20" data-track-index="3">...</div>Top-Level Composition Example
<div
id="comp-1"
data-composition-id="my-video"
data-start="0"
data-duration="60"
data-width="1920"
data-height="1080"
>
<!-- Primitive clips -->
<video
id="el-1"
data-start="0"
data-duration="10"
data-track-index="0"
src="..."
muted
playsinline
></video>
<video
id="el-2"
data-start="el-1"
data-duration="8"
data-track-index="0"
src="..."
muted
playsinline
></video>
<img id="el-3" data-start="5" data-duration="4" data-track-index="1" src="..." />
<audio id="el-4" data-start="0" data-duration="30" data-track-index="2" src="..." />
<!-- Sub-compositions loaded from files -->
<div
id="el-5"
data-composition-id="intro-anim"
data-composition-src="compositions/intro-anim.html"
data-start="0"
data-track-index="3"
></div>
<div
id="el-6"
data-composition-id="captions"
data-composition-src="compositions/caption-overlay.html"
data-start="0"
data-track-index="4"
></div>
<script>
// Just register the timeline — framework auto-nests sub-compositions
const tl = gsap.timeline({ paused: true });
window.__timelines["my-video"] = tl;
</script>
</div>Audio-Reactive Animation
Drive visuals from music, voice, or sound. Any GSAP-animatable property can respond to pre-extracted audio data.
Audio Data Format
var AUDIO_DATA = {
fps: 30,
totalFrames: 900,
frames: [{ bands: [0.82, 0.45, 0.31, ...] }, ...]
};frames[i].bands[]— frequency band amplitudes, 0-1. Index 0 = bass, higher = treble.- Each band normalized independently across the full track.
Mapping Audio to Visuals
| Audio signal | Visual property | Effect |
|---|---|---|
| Bass (bands[0]) | scale | Pulse on beat |
| Treble (bands[12-14]) | textShadow, boxShadow | Glow intensity |
| Overall amplitude | opacity, y, backgroundColor | Breathe, lift, color shift |
| Mid-range (bands[4-8]) | borderRadius, width | Shape morphing |
Any GSAP-tweenable property works — clipPath, filter, SVG attributes, CSS custom properties.
Content, Not Medium
Audio provides timing and intensity. The visual vocabulary comes from the narrative.
Never add: equalizer bars, spectrum analyzers, waveform displays, musical notes clip art, generic particle systems, rainbow color cycling, strobing white on beats, abstract pulsing orbs.
Instead: Let content guide the visual and audio drive its behavior. Bass makes warmth _swell_. Treble sharpens _contrast_. The visual choice comes from "what does this piece feel like?"
Sampling Pattern
Audio reactivity requires per-frame sampling via a for loop with tl.call(), not a single tween:
// ✅ Correct — sample every frame
for (var f = 0; f < AUDIO_DATA.totalFrames; f++) {
tl.call(
(function (frame) {
return function () {
draw(frame);
};
})(AUDIO_DATA.frames[f]),
[],
f / AUDIO_DATA.fps,
);
}
// ❌ Wrong — single tween, doesn't react to audio
gsap.to(".el", { scale: 1.2, duration: totalDuration });Without per-frame sampling, the composition doesn't actually react to audio.
textShadow Gotcha
textShadow on a parent container with semi-transparent children (e.g., inactive caption words at rgba(255,255,255,0.3)) renders a visible glow rectangle behind all children. Fix: apply scale to the container for beat pulse, but apply textShadow to individual active words only.
Guidelines
- Subtlety for text — 3-6% scale variation, soft glow. Heavy pulsing makes text unreadable.
- Go bigger on non-text — backgrounds and shapes can handle 10-30% swings.
- Match the energy — corporate = subtle; music video = dramatic.
- Deterministic — pre-extracted data, no Web Audio API, no runtime analysis.
Constraints
- All audio data must be pre-extracted (use
extract-audio-data.pyfrom the gsap skill's scripts/) - No
Math.random()orDate.now() - Audio reactivity runs on the same GSAP timeline as everything else
Beat Direction
How to plan and direct individual scenes (beats) in a multi-scene composition. Read before writing any multi-scene video.
---
Per-Beat Direction
Each beat is a WORLD, not a layout. Before writing CSS specs and GSAP instructions, describe what the viewer EXPERIENCES. The difference between a great storyboard and a mediocre one:
Mediocre: "Dark navy background. '$1.9T' in white, 280px. Logo top-left. Wave image bottom-right." Great: "Camera is already mid-flight over a vast dark canvas. The gradient wave sweeps across the frame like aurora borealis — alive, shifting. '$1.9T' SLAMS into existence with such force the wave ripples in response. This isn't a slide — it's a moment."
The first describes pixels. The second describes an experience. Write the second, then figure out the pixels.
Each beat should have:
For text animations: pick a named effect from the `text-effects.md` reference and name it by ID in the storyboard. Don't describe "text fades in" — write soft-blur-in or kinetic-center-build. The catalog is maintained as a separate skill (pixel-point/animate-text); see text-effects.md for how sub-agents load it and find the implementation specs.
---
Concept
The big idea for this beat in 2-3 sentences. What visual WORLD are we in? What metaphor drives it? What should the viewer FEEL? This is the most important part — everything else flows from it.
Mood direction
Cultural and design references, not hex codes:
- "Geometric, rhythmic, precise. Think Josef Albers or Bauhaus color studies."
- "Warm workspace. Nice notebook energy, not technical blueprint."
- "Cinematic title sequence. The kind of opening where you lean forward."
Animation choreography
Specific motion verbs per element — not "it animates in" but HOW. Verbs come from the beat's concept and content, not from an energy bucket. A wellness brand's "slow" beat might still have something that DROPS if the content is about letting go. A stats beat might FLOAT if the brand's identity is weightless.
The vocabulary of motion verbs (organized by physical character, not by energy level):
Impact / weight: SLAMS, CRASHES, PUNCHES, STAMPS, SHATTERS, DROPS (with force) Directional / deliberate: SLIDES, PUSHES, PULLS, WIPES, CUTS Reveals / builds: DRAWS, FILLS, GROWS, EXPANDS, ASSEMBLES, COUNTS UP Organic / ambient: FLOATS, DRIFTS, BREATHES, PULSES, ORBITS, MORPHS Mechanical / precise: TYPES ON, CLICKS, LOCKS IN, SNAPS, STEPS
Every element gets a verb. If you can't name the verb, the element is not yet designed. The verb should follow from the beat's concept — not from a lookup of what "high energy" or "low energy" beats use.
Transition
How this beat hands off to the next. Specify the type and parameters.
When to pick which:
| Choose shader transition for | Choose CSS transition for | Choose hard cut for |
|---|---|---|
| Reveals, big reaction shots, product/logo unveils, energy shifts, "wow" moments | Continuous camera-motion beats where the scene feels like one move broken into cuts | Rapid-fire lists, percussive edits on the beat, comedic timing |
| Any moment the music/VO punctuates with a downbeat or SFX hit | Beats that ease from one composition into the next with shared motion vocabulary | Sequences of 3+ quick tempo-matched switches |
| Brand moments where the transition itself _is_ the visual | Minimal/editorial pacing | Anytime a 0.3-0.8s transition would feel too slow |
Rule of thumb: if the beat is the _centerpiece_ of the video, shader-transition into it. If the beat is connective tissue, a CSS crossfade is fine. A brand reel of 5-7 beats usually wants 1-2 shader transitions (the hero reveal + the CTA) — too many flatten their impact.
Mixing shader and CSS crossfade transitions in one composition is supported. Omit shader on any transition entry to get a smooth opacity crossfade. HyperShader manages all scene visibility regardless:
var tl = HyperShader.init({
bgColor: "#0a0a0f",
scenes: ["s1", "s2", "s3", "s4"],
transitions: [
{ time: 4.0, shader: "sdf-iris", duration: 0.7 }, // WebGL shader
{ time: 8.5, duration: 0.8 }, // no shader → CSS crossfade
{ time: 13.0, shader: "domain-warp", duration: 0.6 },
],
});
// Add beat animations to the returned tl AFTER init()
tl.fromTo("#hero", { opacity: 0 }, { opacity: 1, duration: 0.6 }, 0.2);
window.__timelines["main"] = tl;Let HyperShader create the timeline — don't pass a pre-built timeline: option. Add all composition tweens to the returned tl after the call.
CSS transitions — 30+ patterns across 13 categories. Full code in skills/hyperframes/references/transitions/. Pick based on the energy and feel:
| Category | Patterns | Motion character |
|---|---|---|
| Push / slide | Push slide, vertical push, elastic push, squeeze | Content moves through the frame as if on a continuous surface |
| Scale / zoom | Zoom through, zoom out | Perspective shifts — moving toward or away from content |
| Radial / clip | Circle iris, diamond iris, diagonal split | Geometric reveal — content emerges or is covered by a shape |
| 3D | 3D card flip | Physical — content flips like a tangible object |
| Dissolve | Crossfade, blur crossfade, focus pull, color dip | Overlap and blend — both scenes exist simultaneously during the transition |
| Cover / blinds | Staggered color blocks, horizontal blinds (6/12 strips), vertical blinds | Structural — content is sliced, layered, or covered |
| Light | Light leak overlays, overexposure burn, film burn | Organic film — light bleeds across the frame |
| Distortion | Glitch (CSS RGB jitter), chromatic aberration, ripple, VHS tape | Instability — the image itself appears to malfunction |
| Blur | Blur through, directional blur | Soft defocus — content blurs in or out |
| Mechanical | Shutter (two-half), clock wipe (9-point wedge) | Precision — transitions with visible mechanical logic |
| Grid | Grid dissolve (12/120 cells) | Fragmentation — the frame breaks into pieces |
| Destruction | Page burn (SVG clip-path + canvas rim) | Dramatic decay — the previous scene is destroyed |
| Other | Gravity drop, morph circle | Physical or shape-based motion that doesn't fit other categories |
Common quick-picks:
- Velocity-matched upward: exit
y:-150, blur:30px, 0.33s power2.in→ entryy:150→0, blur:30px→0, 1.0s power2.out - Whip pan: exit
x:-400, blur:24px, 0.3s power3.in→ entryx:400→0, blur:24px→0, 0.3s power3.out - Blur through: exit
blur:20px, 0.3s→ entryblur:20px→0, 0.25s power3.out - Zoom through: exit
scale:1→1.2, blur:20px, 0.2s power3.in→ entryscale:0.75→1, blur:20px→0, 0.5s expo.out - Hard cut / smash cut: instant, for rapid-fire sequences
Timing presets: snappy (0.2s), smooth (0.4s), gentle (0.6s), dramatic (0.5s), instant (0.15s), luxe (0.7s).
Shader transitions — 14 built-in WebGL GPU effects. Install with npx hyperframes add <name>. Full API in shader-transitions docs.
| Shader | Visual description | Duration range |
|---|---|---|
| domain-warp | Organic FBM dissolve — both scenes warp toward each other with an accent flash at the midpoint | 0.5–0.8s |
| ridged-burn | Multifractal mask reveals the incoming scene through a burn ramp with sparks at the edge | 0.5–0.8s |
| whip-pan | 10-sample horizontal motion blur + lateral crossfade — reads like a camera pan between shots | 0.3–0.5s |
| sdf-iris | Circle SDF expands from center, with accent-tinted glow rings at the expanding edge | 0.5–0.7s |
| ripple-waves | Radial standing-wave UV displacement — content ripples outward as scenes cross | 0.6–1.0s |
| gravitational-lens | Pinch pull toward center + R/B chromatic separation — content bends inward then releases | 0.6–1.0s |
| cinematic-zoom | 12 RGB-offset radial zoom blur samples — motion streak radiating from center | 0.4–0.6s |
| chromatic-split | R/B radial channel shift outward, G fixed — channels separate then rejoin | 0.3–0.5s |
| swirl-vortex | CCW swirl with FBM noise — content spirals away and the new scene spirals in | 0.5–0.8s |
| thermal-distortion | Vertical sine + FBM horizontal displacement — heat-haze shimmer across the frame | 0.5–0.8s |
| flash-through-white | Fade through white midpoint — almost invisible at 0.01s, noticeable at 0.3s | 0.01s–0.3s |
| cross-warp-morph | FBM vector field displaces both scenes; a third FBM biases the wipe direction | 0.5–0.8s |
| light-leak | Fixed off-frame light source with exponential falloff, warmth, and a ridge flare | 0.5–0.8s |
| glitch | Line displacement + RGB lateral split + scan modulation + posterization + flicker | 0.3–0.5s |
You are not limited to what's listed here. These are the built-in options, but you can and should:
- Write custom GLSL shaders from scratch for unique transition effects
- Search online for shader code (ShaderToy, GLSL Sandbox, GitHub) and adapt it
- Build custom CSS transitions that aren't in any category — combine clip-path, transforms, filters in new ways
- Ask the user to provide or find specific effects if you need something specialized
If the storyboard calls for an effect that doesn't exist yet — build it. The framework renders anything a browser can run.
Depth layers
What's in foreground, midground, and background. Every beat should have at least 2 layers:
- "BG: dark navy fill + subtle radial glow. MG: stat cards with drop shadow. FG: brand logo bottom-right."
SFX cues
What sounds at what moment:
- "On the capture pulse — a soft, warm analog shutter click."
- "Left side carries a faint low drone. On fold: drone cuts. Silence. Then a single clean chime."
---
Rhythm Planning
Before writing HTML, declare your scene rhythm: which scenes are quick hits, which are holds, where do shaders land, where does energy peak. Name the pattern — fast-fast-SLOW-fast-SHADER-hold — before implementing.
Derive the rhythm from the storyboard and the brand, not from a lookup. A 15-second social ad for an architectural firm and a 15-second social ad for a gaming brand have different rhythms — both are 15 seconds, but one is slow-reveal-hold-CTA and the other is rapid-fire-SLAM-hook. Video type sets constraints (duration, approximate beat count); the brand and content determine whether those beats are slow or fast, sparse or dense, dramatic or controlled.
Questions that drive rhythm decisions:
- What emotional journey should the viewer take? Where is the peak moment?
- Where does the narration land its heaviest emphasis? That's usually where energy should peak.
- What does the brand's own visual pacing suggest — unhurried or urgent?
- How many beats can the duration actually support without feeling rushed or padded?
A social ad that tries to hook in 2s, showcase 3 features, and end with a CTA in 15s will feel like noise. Sometimes "hook-hold-CTA" with one strong feature is the right rhythm for 15 seconds. Name the rhythm you've planned before implementing.
---
Velocity-Matched Transitions
Exit the outgoing beat with an accelerating ease (power2.in or power3.in) plus a blur ramp. Enter the incoming beat with a decelerating ease (power2.out or power3.out) plus blur clear. The fastest point of both easing curves meets at the cut — the viewer perceives continuous camera motion, not two discrete animations. Match exit velocity to entry velocity within ~5% tolerance.
Captions
Language Rule (Non-Negotiable)
Never use `.en` models unless the user explicitly states the audio is English. .en models TRANSLATE non-English audio into English instead of transcribing it.
1. User says the language → --model small --language <code> (no .en) 2. User says English → --model small.en 3. Language unknown → --model small (no .en, no --language) — auto-detects
---
Analyze spoken content to determine caption style. If user specifies a style, use that. Otherwise, detect tone from the transcript.
Transcript Source
[
{ "text": "Hello", "start": 0.0, "end": 0.5 },
{ "text": "world.", "start": 0.6, "end": 1.2 }
]For transcription commands, whisper models, external APIs, see transcript-guide.md.
Style Detection (When No Style Specified)
Read the full transcript before choosing. Four dimensions:
1. Visual feel — corporate→clean; energetic→bold; storytelling→elegant; technical→precise; social→playful.
2. Color palette — dark+bright for energy; muted for professional; high contrast for clarity; one accent color.
3. Font mood — heavy/condensed for impact; clean sans for modern; rounded for friendly; serif for elegance.
4. Animation character — scale-pop for punchy; gentle fade for calm; word-by-word for emphasis; typewriter for technical.
Per-Word Styling
Scan for words deserving distinct treatment:
- Brand/product names — larger size, unique color
- ALL CAPS — scale boost, flash, accent color
- Numbers/statistics — bold weight, accent color
- Emotional keywords — exaggerated animation (overshoot, bounce)
- Call-to-action — highlight, underline, color pop
- Marker highlight — for beyond-color emphasis, see css-patterns.md
Script-to-Style Mapping
| Tone | Font mood | Animation | Color | Size |
|---|---|---|---|---|
| Hype/launch | Heavy condensed, 800-900 | Scale-pop, back.out(1.7), 0.1-0.2s | Bright on dark | 72-96px |
| Corporate | Clean sans, 600-700 | Fade+slide, power3.out, 0.3s | White/neutral, muted accent | 56-72px |
| Tutorial | Mono/clean sans, 500-600 | Typewriter/fade, 0.4-0.5s | High contrast, minimal | 48-64px |
| Storytelling | Serif/elegant, 400-500 | Slow fade, power2.out, 0.5-0.6s | Warm muted tones | 44-56px |
| Social | Rounded sans, 700-800 | Bounce, elastic.out, word-by-word | Playful, colored pills | 56-80px |
Word Grouping
- High energy: 2-3 words. Quick turnover.
- Conversational: 3-5 words. Natural phrases.
- Measured/calm: 4-6 words. Longer groups.
Break on sentence boundaries, 150ms+ pauses, or max word count.
Positioning
- Landscape (1920x1080): Bottom 80-120px, centered
- Portrait (1080x1920): Lower middle ~600-700px from bottom, centered
- Never cover the subject's face
position: absolute— never relative- One caption group visible at a time
Text Overflow Prevention
Use window.__hyperframes.fitTextFontSize():
var result = window.__hyperframes.fitTextFontSize(group.text.toUpperCase(), {
fontFamily: "Outfit",
fontWeight: 900,
maxWidth: 1600,
});
el.style.fontSize = result.fontSize + "px";Options: maxWidth (1600 landscape, 900 portrait), baseFontSize (78), minFontSize (42), fontWeight, fontFamily, step (2).
CSS safety nets: max-width on container, overflow: visible (not hidden — hidden clips scaled emphasis words and glow effects), position: absolute, explicit height. When per-word styling uses scale > 1.0, compute maxWidth = safeWidth / maxScale to leave headroom.
Container pattern: Full-width absolute container, centered. Do not use left: 50%; transform: translateX(-50%) — causes clipping at composition edges.
Caption Exit Guarantee
Every group must have a hard kill after exit animation:
tl.to(groupEl, { opacity: 0, scale: 0.95, duration: 0.12, ease: "power2.in" }, group.end - 0.12);
tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end); // deterministic killSelf-lint after building timeline — place before window.__timelines[id] = tl so it runs at composition init:
GROUPS.forEach(function (group, gi) {
var el = document.getElementById("cg-" + gi);
if (!el) return;
tl.seek(group.end + 0.01);
var computed = window.getComputedStyle(el);
if (computed.opacity !== "0" && computed.visibility !== "hidden") {
console.warn(
"[caption-lint] group " + gi + " still visible at t=" + (group.end + 0.01).toFixed(2) + "s",
);
}
});
tl.seek(0);Pre-Built Caption Components
Before building caption styles from scratch, check the registry — 15 ready-to-use caption components cover the most common styles. Install with npx hyperframes add <name> and use as a sub-composition via data-composition-src.
npx hyperframes catalog --tag caption-style # list all caption components
npx hyperframes add caption-highlight # install a specific one| Style | Component | Best for |
|---|---|---|
| TikTok-style highlight | caption-highlight | Social, high-energy |
| Karaoke pill | caption-pill-karaoke | Music, lyric videos |
| Cinematic editorial | caption-editorial-emphasis | Documentary, storytelling |
| Glitch / cyber | caption-glitch-rgb | Tech, gaming |
| Full-screen slam | caption-kinetic-slam | Hype, announcements |
| Neon glow | caption-neon-glow | Night, club, neon aesthetics |
| Neon accent (multi-color) | caption-neon-accent | Colorful, playful |
| Wipe reveal | caption-clip-wipe | Clean, modern |
| Gradient fill | caption-gradient-fill | Vibrant, eye-catching |
| Matrix decode | caption-matrix-decode | Sci-fi, tech reveals |
| Emoji pop | caption-emoji-pop | Social, casual |
| Parallax layers | caption-parallax-layers | Depth, cinematic |
| Particle burst | caption-particle-burst | Celebration, impact keywords |
| Lava texture | caption-texture | Bold, dramatic |
| Weight shift | caption-weight-shift | Elegant, typographic |
Browse all with previews: hyperframes.heygen.com/catalog
Caption components ship with transparent backgrounds — they're pure overlays. If the underlying video is bright or busy, add a contrast layer (e.g. a semi-transparent dark div) in the host composition beneath the caption sub-composition, not inside the component itself.
Further References
- dynamic-techniques.md — karaoke, clip-path reveals, slam words, scatter exits, elastic, 3D rotation
- transcript-guide.md — transcription commands, whisper models, external APIs
- css-patterns.md — CSS+GSAP marker highlighting (deterministic, fully seekable)
Constraints
- Deterministic. No
Math.random(), noDate.now(). - Sync to transcript timestamps.
- One group visible at a time.
- Every group must have a hard
tl.setkill atgroup.end. - The compiler embeds supported fonts automatically — just declare
font-familyin CSS.
CSS Patterns for Marker Highlighting
Pure CSS + GSAP implementations of all five MarkerHighlight.js drawing modes. Use these for deterministic rendering in HyperFrames compositions — no external library dependency, full GSAP timeline control.
Table of Contents
- 1. Highlight Mode — Yellow marker sweep behind text
- 2. Circle Mode — Hand-drawn ellipse around text
- 3. Burst Mode — Radiating lines from text
- 4. Scribble Mode — Chaotic scribble over text
- 5. Sketchout Mode — Rough rectangle outline
1. Highlight Mode
Yellow marker sweep behind text. The most common mode.
<span class="mh-highlight-wrap">
<span class="mh-highlight-bar" id="hl-1"></span>
<span class="mh-highlight-text">highlighted text</span>
</span>.mh-highlight-wrap {
position: relative;
display: inline;
}
.mh-highlight-bar {
position: absolute;
top: 0;
left: -6px;
right: -6px;
bottom: 0;
background: #fdd835;
opacity: 0.35;
transform: scaleX(0);
transform-origin: left center;
border-radius: 3px;
z-index: 0;
}
.mh-highlight-text {
position: relative;
z-index: 1;
}// Sweep in from left
tl.to("#hl-1", { scaleX: 1, duration: 0.5, ease: "power2.out" }, 0.6);
// Optional: skew for hand-drawn feel
// gsap.set("#hl-1", { skewX: -2 });Multi-line Highlight
Stagger bars across multiple lines:
tl.to(
".mh-highlight-bar",
{
scaleX: 1,
duration: 0.5,
ease: "power2.out",
stagger: 0.3,
},
0.6,
);2. Circle Mode
Hand-drawn circle around text. Use border-radius: 50% with a slight rotation for organic feel.
<span class="mh-circle-wrap">
<span class="mh-circle-text" id="circle-word">IMPORTANT</span>
<span class="mh-circle-ring" id="circle-1"></span>
</span>.mh-circle-wrap {
position: relative;
display: inline;
}
.mh-circle-text {
position: relative;
z-index: 1;
}
.mh-circle-ring {
position: absolute;
top: 50%;
left: 50%;
width: 130%;
height: 160%;
transform: translate(-50%, -50%) rotate(-3deg) scale(0);
border: 3px solid #e53935;
border-radius: 50%;
pointer-events: none;
z-index: 0;
}// Circle scales in with a wobble
tl.to(
"#circle-1",
{
scale: 1,
rotation: -3,
duration: 0.6,
ease: "back.out(1.7)",
transformOrigin: "center center",
},
0.7,
);Variations
/* Tighter circle (for short words) */
.mh-circle-ring.tight {
width: 150%;
height: 180%;
}
/* Squared circle (rounded rectangle) */
.mh-circle-ring.rounded {
border-radius: 30%;
width: 120%;
height: 140%;
}
/* Ellipse (wider than tall) */
.mh-circle-ring.ellipse {
width: 150%;
height: 130%;
border-radius: 50%;
}3. Burst Mode
Radiating lines from text center. Each line is a positioned div rotated to its angle.
<span class="mh-burst-wrap">
<span class="mh-burst-text">WOW</span>
<span class="mh-burst-container" id="burst-1">
<span class="mh-burst-line" style="--angle: 0deg; --len: 70px;"></span>
<span class="mh-burst-line" style="--angle: 30deg; --len: 55px;"></span>
<span class="mh-burst-line" style="--angle: 60deg; --len: 80px;"></span>
<span class="mh-burst-line" style="--angle: 90deg; --len: 45px;"></span>
<span class="mh-burst-line" style="--angle: 120deg; --len: 65px;"></span>
<span class="mh-burst-line" style="--angle: 150deg; --len: 75px;"></span>
<span class="mh-burst-line" style="--angle: 180deg; --len: 50px;"></span>
<span class="mh-burst-line" style="--angle: 210deg; --len: 60px;"></span>
<span class="mh-burst-line" style="--angle: 240deg; --len: 80px;"></span>
<span class="mh-burst-line" style="--angle: 270deg; --len: 40px;"></span>
<span class="mh-burst-line" style="--angle: 300deg; --len: 70px;"></span>
<span class="mh-burst-line" style="--angle: 330deg; --len: 55px;"></span>
</span>
</span>.mh-burst-wrap {
position: relative;
display: inline;
}
.mh-burst-text {
position: relative;
z-index: 2;
}
.mh-burst-container {
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
z-index: 1;
}
.mh-burst-line {
position: absolute;
display: block;
width: 3px;
height: var(--len);
background: #1e88e5;
left: -1.5px;
top: calc(-1 * var(--len));
transform: rotate(var(--angle));
transform-origin: bottom center;
opacity: 0;
}// All lines burst outward simultaneously with slight stagger
tl.fromTo(
"#burst-1 .mh-burst-line",
{ scaleY: 0, opacity: 0 },
{ scaleY: 1, opacity: 1, duration: 0.4, ease: "power2.out", stagger: 0.03 },
0.7,
);Vary line lengths (40-80px range) for an organic, hand-drawn feel. Equal lengths look mechanical.
4. Scribble Mode
Wavy SVG underlines and strikethroughs that draw themselves via stroke-dashoffset.
<span class="mh-scribble-wrap">
<span class="mh-scribble-text">underlined text</span>
<svg class="mh-scribble-svg" viewBox="0 0 500 24" preserveAspectRatio="none">
<path
id="scribble-1"
d="M0,12 Q31,0 62,12 Q93,24 125,12 Q156,0 187,12 Q218,24 250,12 Q281,0 312,12 Q343,24 375,12 Q406,0 437,12 Q468,24 500,12"
fill="none"
stroke="#FDD835"
stroke-width="3"
stroke-linecap="round"
/>
</svg>
</div>.mh-scribble-wrap {
position: relative;
display: inline;
}
.mh-scribble-text {
position: relative;
z-index: 1;
}
.mh-scribble-svg {
position: absolute;
left: 0;
bottom: -6px;
width: 100%;
height: 24px;
z-index: 0;
}// Measure path length and set initial dash state
var path = document.querySelector("#scribble-1");
var len = path.getTotalLength();
gsap.set(path, { strokeDasharray: len, strokeDashoffset: len });
// Draw the line
tl.to(
"#scribble-1",
{
strokeDashoffset: 0,
duration: 0.8,
ease: "power1.inOut",
},
0.7,
);Strikethrough Variant
Position the SVG at top: 50%; transform: translateY(-50%) instead of bottom: -6px.
Wavy Path Generator
Scale the path's viewBox width to match text width. The wave pattern Q x1,y1 x2,y2 alternates between y=0 and y=24 for a natural wobble. Adjust the control points for tighter or looser waves:
- Tight waves: smaller x-increments (25px per half-wave)
- Loose waves: larger x-increments (50px per half-wave)
- Amplitude: change the y range (0-24 for standard, 0-16 for subtle)
5. Sketchout Mode
Cross-hatch lines over de-emphasized text. Multiple angled lines create a "crossed out" effect.
<span class="mh-sketchout-wrap">
<span class="mh-sketchout-text">old price</span>
<span class="mh-sketchout-lines" id="sketchout-1">
<span class="mh-sketchout-line mh-sketchout-fwd"></span>
<span class="mh-sketchout-line mh-sketchout-bwd"></span>
</span>
</span>.mh-sketchout-wrap {
position: relative;
display: inline;
}
.mh-sketchout-text {
position: relative;
z-index: 0;
}
.mh-sketchout-lines {
position: absolute;
top: 0;
left: -4px;
right: -4px;
bottom: 0;
overflow: hidden;
z-index: 1;
}
.mh-sketchout-line {
position: absolute;
display: block;
top: 50%;
left: 0;
width: 100%;
height: 2px;
background: #e53935;
transform-origin: left center;
transform: scaleX(0);
}
.mh-sketchout-fwd {
transform: scaleX(0) rotate(-12deg);
}
.mh-sketchout-bwd {
transform: scaleX(0) rotate(12deg);
}// Forward slash draws first
tl.to(
"#sketchout-1 .mh-sketchout-fwd",
{
scaleX: 1,
duration: 0.3,
ease: "power2.out",
},
1.0,
);
// Backward slash follows
tl.to(
"#sketchout-1 .mh-sketchout-bwd",
{
scaleX: 1,
duration: 0.3,
ease: "power2.out",
},
1.15,
);Combining Modes in Captions
Use mode cycling for visual variety across caption groups:
var MODES = ["highlight", "circle", "burst", "scribble"];
GROUPS.forEach(function (group, gi) {
var mode = MODES[gi % MODES.length];
// Apply the mode's CSS pattern to emphasis words in this group
group.emphasisWords.forEach(function (word) {
applyMode(word.el, mode, tl, word.start);
});
});Cycle every 2-3 groups for high energy, every 3-4 for medium, every 4-5 for low.
Design Picker
Two-phase visual picker: mood boards first (pick a complete direction), then fine-tune individual categories.
Prerequisites
Read these before generating options — they define the rules your options must follow:
- typography.md
- ../house-style.md
- video-composition.md
- ../visual-styles.md
- beat-direction.md
Building the picker
1. Generate options deeply contextual to the user's prompt. Every category — not just architectures — must reflect the specific product, brand, audience, and mood. Generic options that could appear on any picker are a failure.
Mood boards — as many as the creative space warrants (4-8). Every board must tell a different STORY about the brand, not just reshuffle the same elements. Ask: "what are the genuinely different ways to position this product?" A cat food brand might be: playful chaos, premium positioning, comfort/cozy, social-native, flavor showcase, humor-led, sensory/appetizing. Each is a different narrative, not a different font on the same layout.
Architectures — one per mood board minimum, each visually distinct. Use {{prompt_headline}} and {{prompt_sub}} tokens. If the user provided media assets, use them as background images (use url(path) without quotes — single quotes inside style='...' break the attribute).
Palettes (5-6) — named after the brand's world, not generic moods. The palette names and colors should feel like they belong to THIS specific product. Always mix dark + light + tinted. Every palette must be visually distinct at swatch size. If two palettes share the same background lightness AND a similar accent hue, cut one. Test: would a user see the difference in a 14px swatch chip? If not, they're duplicates.
Type pairings (5-6) — RUN the font discovery script from typography.md BEFORE generating pairings. This is not optional. Download Google Fonts metadata, run the script, and pick from its output. You will otherwise reach for the same 8 fonts every time (Bricolage Grotesque, Instrument Serif, Fraunces, Archivo Black, DM Serif Display, Space Grotesk, Fredoka) — that's your training data default, not a contextual choice. Match the brand's energy and audience. Cross-category per typography.md (never two sans-serifs).
2. mkdir -p .hyperframes then copy ../templates/design-picker.html to .hyperframes/pick-design.html. 3. Replace these placeholders using Python (don't hand-escape quotes in sed):
__ARCHITECTURES_JSON__— array of architecture objects__PALETTES_JSON__— array of palette objects__TYPEPAIRS_JSON__— array of type pairing objects__MOODBOARDS_JSON__— array of mood board objects (see format below)__PROMPT_JSON__— object with prompt context (see format below)
Architecture data format
Each architecture object must include a preview_html field — the HTML that renders in the preview panel. Use token placeholders that the template replaces at runtime: {{bg}}, {{fg}}, {{ac}}, {{mt}}, {{hf}}, {{hw}}, {{bf}}, {{bw}}, {{cr}} (corner radius), {{pad}}, {{gap}}, {{shadow}}, {{g}} (grid line color), {{fg3}}/{{fg6}}/{{fg8}}/{{fg15}} (fg at opacity), {{ac3}}/{{ac5}}/{{ac25}} (accent at opacity).
Every token must be used. Apply {{cr}} to all cards, buttons, and containers. Apply {{shadow}} to elevated elements (cards, buttons, code blocks). Apply {{pad}} and {{gap}} to control spacing. If a token isn't used in the preview_html, that option will have no visible effect.
Density matters. Each architecture preview must include 15+ distinct elements to give the user a real sense of the layout. Include: headline, subhead, body paragraph, label/overline, stat with number, secondary stat, quote/testimonial, attribution, card with title+body, second card (different treatment), code/command block, primary button, secondary button, list or tags, accent divider/rule, and a data element (table row, progress bar, or chart).
Optionally include components (component styling rules) and dos (do's and don'ts) as strings — these appear in the generated design.md.
Layout constraint: All preview HTML must use percentage widths or max-width: 100%. Use flex-wrap: wrap on all flex rows. Absolute-positioned decoratives must stay within a parent with overflow: hidden.
Security: Architecture preview_html must not contain <script> tags, event handlers (onclick, onerror, etc.), or javascript: URLs. It is injected via innerHTML.
Image URLs: When using background images in preview_html, use url(path/to/image.jpg) WITHOUT quotes around the path. Single quotes like url('path.jpg') break because preview_html is inside a style='...' attribute — the inner single quotes terminate the outer attribute.
Palette variety: Always include a mix of light, dark, and tinted backgrounds across the 6 palettes — even for calm/wellness prompts.
Example architecture object
{
"name": "Editorial Stack",
"description": "Vertical rhythm with large type, pull quotes, and data callouts",
"tag": "editorial / longform / narrative",
"mood": "Confident, unhurried, typographically driven",
"preview_html": "<div style='background:{{bg}};color:{{fg}};padding:{{pad}};min-height:100vh;font-family:\"{{bf}}\",sans-serif;font-weight:{{bw}};'><div style='max-width:100%;display:flex;flex-direction:column;gap:{{gap}};'><div style='font-size:10px;text-transform:uppercase;letter-spacing:0.12em;color:{{mt}};'>Overline Label</div><div style='font-family:\"{{hf}}\",serif;font-weight:{{hw}};font-size:48px;line-height:1.1;letter-spacing:-0.02em;'>The Headline Goes Here</div><div style='font-size:20px;color:{{mt}};max-width:70%;line-height:1.5;'>Subheading text that introduces the narrative arc of this composition with enough words to fill two lines.</div><div style='font-size:15px;line-height:1.7;color:{{fg}};max-width:65%;'>Body paragraph with real sentences. The quick brown fox jumps over the lazy dog. This gives a sense of text density and reading rhythm at the chosen type size.</div><div style='display:flex;gap:{{gap}};flex-wrap:wrap;'><div style='background:{{fg6}};border-radius:{{cr}};padding:{{pad}};flex:1;min-width:200px;box-shadow:{{shadow}};'><div style='font-size:36px;font-family:\"{{hf}}\",serif;font-weight:{{hw}};color:{{ac}};'>2.4M</div><div style='font-size:12px;color:{{mt}};margin-top:4px;'>Primary Stat</div></div><div style='background:{{fg6}};border-radius:{{cr}};padding:{{pad}};flex:1;min-width:200px;box-shadow:{{shadow}};'><div style='font-size:36px;font-family:\"{{hf}}\",serif;font-weight:{{hw}};color:{{fg}};'>87%</div><div style='font-size:12px;color:{{mt}};margin-top:4px;'>Secondary Stat</div></div></div><div style='border-left:3px solid {{ac}};padding:12px {{pad}};background:{{ac3}};border-radius:0 {{cr}} {{cr}} 0;'><div style='font-size:18px;font-style:italic;color:{{fg}};line-height:1.5;'>\"A pull quote that captures the key insight of the piece.\"</div><div style='font-size:12px;color:{{mt}};margin-top:8px;'>— Attribution Name</div></div><div style='background:{{fg3}};border-radius:{{cr}};padding:{{pad}};box-shadow:{{shadow}};'><div style='font-size:14px;font-weight:{{hw}};margin-bottom:8px;'>Card Title</div><div style='font-size:13px;color:{{mt}};line-height:1.5;'>Card body text with a different treatment than the main content area.</div></div><div style='background:{{ac5}};border:1px solid {{ac25}};border-radius:{{cr}};padding:{{pad}};box-shadow:{{shadow}};'><div style='font-size:14px;font-weight:{{hw}};color:{{ac}};margin-bottom:8px;'>Accent Card</div><div style='font-size:13px;color:{{fg}};line-height:1.5;'>Second card with a tinted accent treatment for variety.</div></div><div style='font-family:monospace;font-size:13px;background:{{fg8}};border-radius:{{cr}};padding:{{pad}};color:{{fg15}};box-shadow:{{shadow}};'>$ hyperframes render --output video.mp4</div><div style='display:flex;gap:12px;flex-wrap:wrap;'><button style='background:{{ac}};color:{{bg}};border:none;padding:10px 24px;border-radius:{{cr}};font-size:14px;font-weight:600;box-shadow:{{shadow}};cursor:pointer;'>Primary Action</button><button style='background:transparent;color:{{fg}};border:1px solid {{fg15}};padding:10px 24px;border-radius:{{cr}};font-size:14px;cursor:pointer;'>Secondary</button></div><div style='display:flex;gap:8px;flex-wrap:wrap;'><span style='background:{{fg6}};border-radius:100px;padding:4px 12px;font-size:11px;color:{{mt}};'>Tag One</span><span style='background:{{fg6}};border-radius:100px;padding:4px 12px;font-size:11px;color:{{mt}};'>Tag Two</span><span style='background:{{ac5}};border-radius:100px;padding:4px 12px;font-size:11px;color:{{ac}};'>Accent Tag</span></div><div style='height:1px;background:linear-gradient(to right,{{ac25}},{{fg6}},{{ac25}});'></div><div style='display:flex;justify-content:space-between;font-size:12px;color:{{mt}};border-bottom:1px solid {{g}};padding:8px 0;'><span>Data row label</span><span style='color:{{fg}};font-weight:600;'>1,234</span></div></div></div>"
}Mood board data format
Each mood board pre-selects one option from each category. The user picks a mood board in Phase 1, then fine-tunes in Phase 2 with those selections pre-filled.
{
"name": "Terminal Precision",
"description": "Code-forward, data-dense, CLI energy. Dark canvas, monospace body, sharp corners.",
"theme": "dark",
"arch_index": 0,
"palette_index": 0,
"type_index": 0,
"corners_index": 0,
"density_index": 0,
"depth_index": 1,
"easing_index": 0,
"corners": "0px",
"padding": "12px",
"gap": "8px",
"shadow": "0 2px 16px rgba(0,230,255,0.15)"
}Indices reference into the ARCHITECTURES, PALETTES, and TYPEPAIRS arrays. The template renders a mini preview of each mood board using its architecture's preview_html with the mood board's palette/type applied.
Prompt context data format
{
"title": "AI Coding Assistant",
"headline": "Your Code, Understood.",
"subline": "An AI coding assistant that reads your entire codebase.",
"section_desc": "Layout options for your product launch"
}title appears in the Phase 1 header. headline and subline replace {{prompt_headline}} and {{prompt_sub}} in architecture preview_html so previews show real content.
Content tokens in preview_html
In addition to the standard design tokens ({{bg}}, {{fg}}, {{ac}}, etc.), architecture preview_html can use:
{{prompt_headline}}— the user's actual headline text{{prompt_sub}}— the user's actual subline text
This makes previews contextual — the user sees their own content styled, not generic placeholders.
Serving and user selection
4. Serve the file: cd <project-dir> && python3 -m http.server 8723 & (use port 8723 or any unused port above 8000; if the curl check fails, try the next port). Verify: curl -s -o /dev/null -w "%{http_code}" http://localhost:8723/.hyperframes/pick-design.html — only share the link if it returns 200. Do NOT use npx hyperframes preview for the picker — it blocks. Only start the HTTP server from the main conversation thread. If you are running as a dispatched task or subagent, return the file path and let the caller serve it. 5. Once the user picks, tell them: "Copy the design.md from the picker and paste it here." The user pastes the markdown back into the conversation. Save it verbatim to design.md in the project root — it's already in spec format (YAML frontmatter + prose sections). After the user pastes, kill the background server: kill %1 or kill $(lsof -ti:8723). Then proceed with construction.
The picker outputs a google-labs-code/design.md spec-compliant file: YAML frontmatter with colors, typography, rounded, and spacing tokens, followed by ## Overview, ## Colors, ## Typography, ## Layout, ## Elevation, ## Components, and ## Do's and Don'ts prose sections.
Dynamic Caption Techniques
You are here because SKILL.md told you to read this file before writing animation code. Pick your technique combination from the table below based on the energy level you detected from the transcript, then implement using standard GSAP patterns.
Technique Selection by Energy
Captions are a constrained surface — the highlight and exit technique is closely tied to how much intensity the spoken content carries. The table below is a calibration reference. If the design spec (frame.md or design.md) or the storyboard specifies a caption style, that overrides anything here.
The core principle: all energy levels use karaoke highlight as the baseline. The difference is intensity — not the technique type.
What changes with energy:
- Highlight intensity: high energy gets accent color + glow + 15% scale pop on active words. Low energy gets a gentle white shift with 3% scale. The karaoke behavior is the same; the amplitude is different.
- Exit style: high energy exits scatter or drop (the word leaves with motion). Low energy exits collapse (the word simply fades or shrinks). The exit should express the same energy as the content.
- Cycle variation: high energy alternates highlight styles every 2 groups for variety. Low energy uses a single consistent style, varying only the ease. Variation itself creates energy; consistency creates calm.
Calibration reference (starting points, not rules):
| Energy level | Highlight amplitude | Exit | Cycle variation |
|---|---|---|---|
| High | Accent color + glow + 15% scale pop | Scatter or drop | Every 2 groups |
| Medium-high | Color pop, no glow | Scatter or collapse | Every 3 groups |
| Medium | White shift only | Fade + slide | Every 3 groups |
| Medium-low | Minimal scale change | Fade | Single style |
| Low | Warm tones, slow transition | Collapse | Single style |
All energy levels use karaoke highlight as the baseline. The difference is intensity — high energy gets accent color + glow + 15% scale pop on active words, low energy gets a gentle white shift with 3% scale.
Emphasis words always break the pattern. When a word is flagged as emphasis (emotional keyword, ALL CAPS, brand name), give it a stronger animation than surrounding words (larger scale, accent color, overshoot ease). This creates contrast.
Marker highlight modes add a visual layer on top of karaoke. For emphasis words that need more than color/scale, add a marker-style effect — highlight sweep, circle, burst, or scribble — using the /marker-highlight skill. Match mode to energy: burst for hype, circle for key terms, highlight for standard, scribble for subtle.
Audio-Reactive Captions (Mandatory for Music)
If the source audio is music (vocals over instrumentation, beats, any musical content), you MUST extract audio data and add audio-reactive animations. This is not optional — music without audio reactivity looks disconnected. Even low-energy ballads get subtle bass pulse and treble glow.
No special wiring is needed. The group loop already iterates over every caption group to build entrance, karaoke, and exit tweens. At that point, read the audio data for each group's time range and use it to modulate the group's animation intensity with regular GSAP tweens.
// Load audio data inline (same pattern as TRANSCRIPT)
var AUDIO = JSON.parse(audioDataJson); // { fps, totalFrames, frames: [{ bands: [...] }] }
GROUPS.forEach(function (group, gi) {
var groupEl = document.getElementById("cg-" + gi);
if (!groupEl) return;
// Read peak energy for this group's time range
var startFrame = Math.floor(group.start * AUDIO.fps);
var endFrame = Math.min(Math.floor(group.end * AUDIO.fps), AUDIO.totalFrames - 1);
var peakBass = 0;
var peakTreble = 0;
for (var f = startFrame; f <= endFrame; f++) {
var frame = AUDIO.frames[f];
if (!frame) continue;
peakBass = Math.max(peakBass, frame.bands[0] || 0, frame.bands[1] || 0);
peakTreble = Math.max(peakTreble, frame.bands[6] || 0, frame.bands[7] || 0);
}
// Modulate entrance — louder groups enter bigger and glowier
tl.to(
groupEl,
{
scale: 1 + peakBass * 0.06,
textShadow:
"0 0 " + Math.round(peakTreble * 12) + "px rgba(255,255,255," + peakTreble * 0.4 + ")",
duration: 0.3,
ease: "power2.out",
},
group.start,
);
// Reset at exit so audio-driven values don't persist
tl.set(groupEl, { scale: 1, textShadow: "none" }, group.end - 0.15);
});This shapes the animation at build time, not playback time — no per-frame callbacks, no tl.call() loops, no async fetch timing issues. Loud groups come in with more weight and glow; quiet groups come in soft. The audio data modulates _how much_, the content determines _what_.
Keep audio reactivity subtle — 3-6% scale variation and soft glow. Heavy pulsing makes text unreadable.
To generate the audio data file:
python3 skills/gsap-effects/scripts/extract-audio-data.py audio.mp3 --fps 30 --bands 8 -o audio-data.jsonCombining Techniques
Don't use the same highlight animation on every group — cycle through styles using the group index. Don't combine multiple competing animations on the same word at the same timestamp. Vary techniques across groups to match the content's pace changes.
Marker highlight effects (from the /marker-highlight skill) layer well with karaoke — use karaoke for the word-by-word reveal, then add a marker effect on emphasis words only. For example: karaoke highlights each word in white, but brand names get a yellow highlight sweep and stats get a red circle. Cycle marker modes across groups for visual variety (see the mode-to-energy mapping in the marker-highlight skill).
Available Tools
These tools are available in the HyperFrames runtime. Use them when they solve a real problem — not every composition needs all of them.
| Tool | What it does | Access | When it's useful |
|---|---|---|---|
| pretext | Pure-arithmetic text measurement without DOM reflow. 0.0002ms per call. | window.__hyperframes.pretext.prepare(text, font) / .layout(prepared, maxWidth, lineHeight) | Per-frame text reflow, shrinkwrap containers, computing layout before render |
| fitTextFontSize | Finds the largest font size that fits text on one line. Built on pretext. | window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight }) | Overflow prevention for long phrases, portrait mode, large base sizes |
| audio data | Pre-extracted per-frame RMS energy and frequency bands. | Extract with extract-audio-data.py, load inline or via fetch("audio-data.json") | Audio-reactive visuals — modulate intensity based on the music |
| GSAP | Animation timeline with tweens and callbacks. | gsap.to(), gsap.set(), tl.to(), tl.set() | All caption animation |
HTML-in-Canvas Patterns
HyperFrames' most powerful visual capability. Capture ANY live HTML/CSS as a GPU texture, then render it through WebGL shaders, Three.js 3D scenes, or post-processing effects — at 60fps, pixel-perfect, with every CSS feature supported.
Read this file when a beat deserves cinematic treatment beyond flat GSAP animations. Use for 1-3 hero beats per video, not every beat. The rest can use standard GSAP — the contrast between flat beats and HTML-in-Canvas beats IS part of the visual storytelling.
---
Core Boilerplate (same in every HTML-in-Canvas composition)
Every HTML-in-Canvas effect shares this structure. Learn this once, adapt it for any effect.
<!-- 1. Source HTML — your content goes inside a layoutsubtree canvas -->
<canvas
id="hic-source"
layoutsubtree
width="1920"
height="1080"
style="position:absolute;inset:0;opacity:0;"
>
<div id="hic-content" style="width:1920px;height:1080px;">
<!-- YOUR HTML CONTENT HERE — text, images, cards, dashboards, anything -->
</div>
</canvas>
<!-- 2. Render target — the visible canvas that shows the effect -->
<canvas id="hic-output" width="1920" height="1080" style="position:absolute;inset:0;"></canvas>// 3. Feature detection — always check, always provide fallback
function isHiCSupported() {
var tc = document.createElement("canvas");
if (!("layoutSubtree" in tc)) return false;
tc.setAttribute("layoutsubtree", "");
var ctx = tc.getContext("2d");
return ctx && typeof ctx.drawElementImage === "function";
}
var apiOk = isHiCSupported();
// 4. Capture function — call this every frame in onUpdate
var capCanvas = document.getElementById("hic-source");
var capCtx = capCanvas.getContext("2d");
function captureContent() {
if (apiOk) {
capCtx.drawElementImage(document.getElementById("hic-content"), 0, 0, 1920, 1080);
}
}
// 5. Drive from GSAP timeline — capture + render every frame
tl.to(
proxy,
{
/* your animation properties */
duration: BEAT_DURATION,
ease: "sine.inOut",
onUpdate: function () {
captureContent();
// render your effect here (Three.js or WebGL2)
},
},
0,
);Fallback: When drawElementImage is not available (preview without Chrome flag), draw a solid-color placeholder or use Canvas 2D text. The HyperFrames renderer auto-enables the flag — the effect WILL work in the final video. See the liquid-glass block for a complete fallback example.
---
Effect Catalog
1. 3D Rotation with Bloom (Three.js)
What it looks like: Content floats in 3D space, slowly rotating with cinematic glow around bright edges. Like a product screenshot displayed in a dark theater.
When to use: Hero product showcase, feature reveal, CTA with premium feel.
Key Three.js components: PlaneGeometry + CanvasTexture + EffectComposer + UnrealBloomPass
// After the boilerplate above, add:
var scene3d = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, 1920 / 1080, 0.1, 100);
camera.position.set(0, 0, 4);
var renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("hic-output"),
antialias: true,
alpha: true,
});
renderer.setSize(1920, 1080);
var texture = new THREE.CanvasTexture(capCanvas);
var mesh = new THREE.Mesh(
new THREE.PlaneGeometry(3.6, 2.2),
new THREE.MeshBasicMaterial({ map: texture }),
);
scene3d.add(mesh);
// Post-processing: bloom for cinematic glow.
// EffectComposer / RenderPass / UnrealBloomPass are ES-module named imports
// (see the import block below) — they're NOT properties of THREE in modern
// versions. Three.js r150+ removed the UMD `examples/js/` globals.
var composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene3d, camera));
composer.addPass(new UnrealBloomPass(new THREE.Vector2(1920, 1080), 0.3, 0.4, 0.85));
var proxy = { rotY: -0.12, zoom: 4.2 };
tl.to(
proxy,
{
rotY: 0.12,
zoom: 3.6,
duration: BEAT_DURATION,
ease: "sine.inOut",
onUpdate: function () {
captureContent();
texture.needsUpdate = true;
mesh.rotation.y = proxy.rotY;
camera.position.z = proxy.zoom;
composer.render();
},
},
0,
);Load Three.js and post-processing via ESM (use a `type="module"` script):
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.181.2/+esm";
import { EffectComposer } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/RenderPass.js";
import { ShaderPass } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/ShaderPass.js";
import { UnrealBloomPass } from "https://cdn.jsdelivr.net/npm/three@0.181.2/examples/jsm/postprocessing/UnrealBloomPass.js";
// ... rest of composition code using these imports
</script>The examples/js/ path was removed in Three.js r152. Use examples/jsm/ (ES modules) with three@0.181.2 — the version used by the HyperFrames Three.js adapter.
---
2. Magnetic Cursor Distortion (Raw WebGL2)
What it looks like: Content warps and bends toward a moving point, like a magnet pulling on pixels. Chromatic aberration splits RGB channels at the distortion site.
When to use: Interactive feel, product demo with cursor, "look at THIS feature" moment.
Key technique: Custom fragment shader with Gaussian warp + chromatic split. No Three.js needed — just raw WebGL2.
// WebGL2 setup
var gl = document.getElementById("hic-output").getContext("webgl2", {
alpha: false,
preserveDrawingBuffer: true,
});
// Vertex shader — full-screen quad
var VS = `#version 300 es
in vec2 a_pos;
out vec2 v_uv;
void main() {
v_uv = a_pos * 0.5 + 0.5;
gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
// Fragment shader — magnetic warp + chromatic aberration
var FS = `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 fragColor;
uniform sampler2D u_tex;
uniform vec2 u_cursor; // cursor position (0-1)
uniform float u_strength; // warp strength (0-1)
void main() {
vec2 uv = v_uv;
vec2 delta = uv - u_cursor;
float dist = length(delta);
float warp = u_strength * exp(-dist * dist * 8.0);
vec2 warped = uv - delta * warp * 0.3;
// Chromatic aberration at distortion site
float aberration = warp * 0.008;
float r = texture(u_tex, warped + vec2(aberration, 0.0)).r;
float g = texture(u_tex, warped).g;
float b = texture(u_tex, warped - vec2(aberration, 0.0)).b;
fragColor = vec4(r, g, b, 1.0);
}`;
// Compile, link, setup quad geometry, upload texture...
// (See registry/blocks/vfx-magnetic/vfx-magnetic.html for complete implementation)
// Drive cursor position from GSAP
var proxy = { cx: 0.2, cy: 0.5, strength: 0.0 };
tl.to(
proxy,
{
cx: 0.8,
cy: 0.4,
strength: 1.0,
duration: BEAT_DURATION,
ease: "power2.inOut",
onUpdate: function () {
captureContent();
// Upload texture, set uniforms, draw
gl.uniform2f(cursorLoc, proxy.cx, proxy.cy);
gl.uniform1f(strengthLoc, proxy.strength);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
},
},
0,
);---
3. Shatter / Fragment Explosion (Three.js)
What it looks like: Content breaks into geometric fragments that fly apart, revealing what's behind.
When to use: Dramatic transition, "breaking free" moment, tension release.
Key technique: Subdivide the source texture into triangle mesh fragments using BufferGeometry, then animate each fragment's position/rotation with GSAP.
Study registry/blocks/vfx-shatter/vfx-shatter.html for the complete 1156-line implementation. The core idea:
// 1. Capture content to texture (same boilerplate)
// Seeded PRNG for determinism — Math.random() is banned
function mulberry32(seed) {
return function () {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
var rng = mulberry32(42);
// 2. Create N triangle fragments from the texture
var fragments = [];
for (var i = 0; i < NUM_FRAGMENTS; i++) {
var geom = new THREE.BufferGeometry();
var mesh = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ map: texture }));
scene3d.add(mesh);
fragments.push({ mesh: mesh, targetPos: randomExplosionVector(rng), delay: rng() * 0.5 });
}
// 3. Animate: first hold still, then EXPLODE
tl.to({}, { duration: holdTime }, 0);
fragments.forEach(function (frag) {
tl.to(
frag.mesh.position,
{
x: frag.targetPos.x,
y: frag.targetPos.y,
z: frag.targetPos.z,
duration: 0.8,
ease: "power3.in",
},
holdTime + frag.delay,
);
tl.to(
frag.mesh.rotation,
{ x: rng() * 4, y: rng() * 4, duration: 0.8, ease: "power2.in" },
holdTime + frag.delay,
);
});---
4. Liquid / Fluid Surface (Three.js)
What it looks like: Content floats above a rippling liquid surface with real-time wave dynamics. Or content IS the surface, undulating like water.
When to use: Organic/premium feel, ambient background, "living" product showcase.
Key technique: Subdivided PlaneGeometry with vertex displacement driven by noise functions in a vertex shader.
Study registry/blocks/vfx-liquid-background/vfx-liquid-background.html for the 1244-line implementation. Core idea:
// Custom vertex shader with wave displacement
var vertexShader = `
varying vec2 vUv;
uniform float u_time;
void main() {
vUv = uv;
vec3 pos = position;
// Sine wave displacement
pos.z += sin(pos.x * 3.0 + u_time * 2.0) * 0.15;
pos.z += cos(pos.y * 2.5 + u_time * 1.5) * 0.1;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
var mesh = new THREE.Mesh(
new THREE.PlaneGeometry(4, 3, 64, 64), // heavily subdivided for smooth waves
new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: `varying vec2 vUv; uniform sampler2D u_tex;
void main() { gl_FragColor = texture2D(u_tex, vUv); }`,
uniforms: {
u_tex: { value: texture },
u_time: { value: 0 },
},
}),
);---
5. Portal / Dimensional Reveal (Three.js)
What it looks like: A glowing circular portal opens and content emerges through it from another dimension.
When to use: Product reveal, "entering the app" moment, hero feature introduction.
Study registry/blocks/vfx-portal/vfx-portal.html for the complete 863-line implementation.
---
When to Use HTML-in-Canvas vs Standard GSAP
| Scenario | Use | Why |
|---|---|---|
| Hero product screenshot showcase | HTML-in-Canvas (3D rotation + bloom) | Makes flat UI feel cinematic |
| Feature list / stats | Standard GSAP | Content-focused, doesn't need 3D |
| CTA / brand reveal | HTML-in-Canvas (portal or magnetic) | Makes the moment memorable |
| Social proof / logos | Standard GSAP | Orderly cascade, trust is steady |
| Transition between acts | HTML-in-Canvas (shatter) | Dramatic act break |
| Background atmosphere | HTML-in-Canvas (liquid surface) | Premium ambient feel |
| Quick feature cards | Standard GSAP | Speed matters, 3D would slow it down |
---
More Effects You Can Build
These aren't in the VFX blocks — build them yourself from the core boilerplate + a custom fragment shader. Each effect is a single GLSL function applied to the captured texture.
6. Noise Dissolve
Content dissolves into noise particles, revealing what's behind. Great for transitions.
// Fragment shader — noise-based dissolve
uniform float u_progress; // 0.0 = fully visible, 1.0 = fully dissolved
uniform sampler2D u_tex;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
void main() {
vec2 uv = v_uv;
float noise = hash(uv * 50.0);
float threshold = u_progress;
if (noise < threshold) {
// Edge glow at the dissolve boundary
float edge = smoothstep(threshold - 0.05, threshold, noise);
fragColor = vec4(1.0, 0.6, 0.2, 1.0) * (1.0 - edge); // orange edge glow
} else {
fragColor = texture(u_tex, uv);
}
}7. Holographic / Iridescent
Content gets a rainbow-shifting holographic sheen that moves with time. Premium, futuristic feel.
uniform float u_time;
uniform sampler2D u_tex;
void main() {
vec4 color = texture(u_tex, v_uv);
// Iridescent color shift based on position + time
float angle = v_uv.x * 6.28 + v_uv.y * 3.14 + u_time * 0.5;
vec3 holo = vec3(
sin(angle) * 0.5 + 0.5,
sin(angle + 2.094) * 0.5 + 0.5,
sin(angle + 4.189) * 0.5 + 0.5
);
// Blend holographic over content (subtle overlay)
fragColor = vec4(mix(color.rgb, holo, 0.15 + 0.1 * sin(u_time)), color.a);
}8. Scan Lines + CRT
Retro CRT monitor look — scan lines, slight curvature, phosphor glow. Great for "code" or "terminal" beats.
uniform sampler2D u_tex;
uniform float u_time;
void main() {
vec2 uv = v_uv;
// Barrel distortion (CRT curvature)
vec2 centered = uv - 0.5;
float dist = dot(centered, centered);
uv = uv + centered * dist * 0.15;
vec4 color = texture(u_tex, uv);
// Scan lines
float scanline = sin(uv.y * 800.0) * 0.04;
color.rgb -= scanline;
// Slight RGB offset (phosphor)
color.r = texture(u_tex, uv + vec2(0.001, 0.0)).r;
color.b = texture(u_tex, uv - vec2(0.001, 0.0)).b;
// Vignette
float vignette = 1.0 - dist * 2.0;
fragColor = vec4(color.rgb * vignette, 1.0);
}9. Frosted Glass Blur
Content behind frosted glass — visible but softened, with subtle light refraction. Good for "behind the scenes" or "coming soon" moments.
uniform sampler2D u_tex;
uniform float u_blur; // 0.0 = clear, 1.0 = full frost
void main() {
vec2 uv = v_uv;
vec4 color = vec4(0.0);
// Box blur with offset
float radius = u_blur * 0.015;
for (float x = -2.0; x <= 2.0; x += 1.0) {
for (float y = -2.0; y <= 2.0; y += 1.0) {
color += texture(u_tex, uv + vec2(x, y) * radius);
}
}
color /= 25.0;
// Add frost noise texture
float frost = fract(sin(dot(uv * 200.0, vec2(12.9898, 78.233))) * 43758.5453);
color.rgb += frost * 0.03 * u_blur;
fragColor = color;
}10. Pixel Sort / Glitch Art
Pixels rearrange themselves in vertical or horizontal strips — digital art aesthetic. Great for tech/creative brands.
uniform sampler2D u_tex;
uniform float u_intensity; // 0-1
void main() {
vec2 uv = v_uv;
// Random horizontal displacement per row
float row = floor(uv.y * 80.0);
float noise = fract(sin(row * 127.1) * 43758.5);
float displace = step(0.7, noise) * u_intensity * 0.1;
// Shift UV with RGB split
float r = texture(u_tex, uv + vec2(displace, 0.0)).r;
float g = texture(u_tex, uv).g;
float b = texture(u_tex, uv - vec2(displace * 0.5, 0.0)).b;
fragColor = vec4(r, g, b, 1.0);
}---
Creating ANY Custom Effect
The fragment shaders above are templates. The pattern is always:
1. Capture your HTML content with drawElementImage (the boilerplate at the top) 2. Upload the captured canvas as a WebGL texture 3. Write a fragment shader that reads from the texture and outputs modified colors 4. Drive shader uniforms from GSAP via onUpdate
Any GLSL effect from ShaderToy, The Book of Shaders, CodePen, or anywhere else can be adapted:
1. Find an effect you like (search "GLSL [effect name]" or browse shadertoy.com) 2. Copy the fragment shader 3. Replace iResolution with vec2(1920.0, 1080.0), iTime with your u_time uniform 4. Add uniform sampler2D u_tex; for the captured content texture 5. Wire the uniforms to GSAP proxy values
Geometry ideas beyond flat planes:
SphereGeometry— content mapped onto a globe (world map, global reach)CylinderGeometry— content on a rotating cylinder (carousel/scroll feel)TorusGeometry— content wrapped around a ring (infinity, cycle)BoxGeometry— content on a 3D box (product packaging, dice)- GLTF models — content mapped as screen texture on phone, laptop, monitor (see
vfx-iphone-device)
Post-processing stacking (Three.js EffectComposer):
- Bloom + film grain = cinematic
- Bloom + chromatic aberration = lens effect
- Depth of field + vignette = focused attention
- Film grain + scan lines = retro
- Multiple passes stack — add as many as you want
You are not limited to the effects listed here. If you can imagine a visual treatment, you can build it. The HTML-in-Canvas API gives you the source material (any HTML rendered as a texture), and WebGL/Three.js gives you unlimited creative control over how that material is presented.
Motion Principles
Common defaults that produce monoculture
These are the patterns LLMs reach for without thinking. None of them are wrong in isolation — they're wrong as defaults. If every scene of every video lands on the same easing, the same speed, and the same entrance direction, the compositions blur into one another no matter what the brand is.
- Same ease on every tween.
power2.outis the most common default. Aim for variety: no more than two independent tweens sharing an ease within a scene. Eases are like font weights — vary them deliberately. - Same speed on every tween. 0.4–0.5s is a common default that flattens rhythm. The slowest motion in a scene should be roughly 3× slower than the fastest. Vary duration so the eye can tell what's important.
- Same entrance direction.
y: 30, opacity: 0is the universal LLM entrance. The same scene can use entrances from left, from right, from scale, from blur, opacity-only, letter-spacing — each one says something different about the element. - Same stagger across scenes. Each scene should have its own rhythm. A 0.08s stagger in beat 1 and a 0.15s stagger in beat 2 makes the two beats feel like different moments.
- Ambient zoom on every scene. Slow-scale-up is the default ambient motion and it telegraphs "LLM-generated video." Vary the ambient motion per scene: slow pan, subtle rotation, color temperature shift, gentle drift — and sometimes nothing. Stillness after motion has real weight.
- First animation at t=0. Zero-delay feels like a jump cut. Offset the opening 0.1–0.3s so the scene reads as composed rather than thrown together.
Easing is emotion, not technique
The motion is the verb. The easing is the adverb. A slide-in with expo.out feels confident. With sine.inOut, dreamy. With elastic.out, playful. Same motion, three different meanings. Choose the adverb deliberately.
Direction rules:
.outfor elements entering. Starts fast, decelerates. Feels responsive. This is the default for entrances..infor elements leaving. Starts slow, accelerates away. Sends them off with momentum..inOutfor elements moving between positions, neither entering nor leaving the scene.
Ease-in on an entrance feels sluggish. Ease-out on an exit feels reluctant. These are the most common reversals and they're worth checking your work against.
Speed expresses weight
Duration is one of the most direct ways a composition communicates what it values. Faster motion reads as confident, urgent, kinetic — it gives the viewer less time to study what's happening, which means the work has to land in fewer frames. Slower motion reads as deliberate, considered, weighty — the viewer has time to take in the element, which means each element has to earn that attention.
Useful calibration ranges (not prescriptions — what a duration _expresses_ depends on what surrounds it):
- 0.15–0.3s — quick, percussive, kinetic. The motion reads as something happening _to_ the frame.
- 0.3–0.5s — comfortable, professional. The motion reads as composed and reliable.
- 0.5–0.8s — deliberate. The motion has visible weight and asks for attention.
- 0.8s+ — atmospheric. The motion becomes part of what the scene _is_, not something happening within it.
A composition that uses only one of these ranges feels one-note. Mix them — a scene where the headline takes 0.7s to settle and the supporting details land in 0.25s creates contrast that reinforces hierarchy without needing different colors or sizes.
Scene structure: build, breathe, resolve
Every scene has three phases. The most common failure is dumping everything into the build and leaving nothing for the other two.
- Build (0–30%) — elements enter, staggered. Not all at once.
- Breathe (30–70%) — content visible, alive with one ambient motion. The viewer reads, registers, settles.
- Resolve (70–100%) — exit or decisive end. Exits are faster than entrances (see Asymmetry below).
A scene that's all build feels like a slideshow. A scene with no breathe phase doesn't let the content land.
Transitions carry meaning
The transition type tells the viewer how two scenes relate:
- Crossfade — "this continues." Connective tissue between related ideas.
- Hard cut — "wake up" or a register shift. Disruption, surprise, percussive emphasis.
- Slow dissolve — "drift with me." Atmospheric, meditative, between-thoughts.
Crossfade is the default and it's defensible most of the time. The thing to watch for is using it for everything — when every transition is a crossfade, the viewer stops registering scene changes as meaningful. Hard cuts and slow dissolves are tools for the moments where the change in scene _is_ the message.
Choreography is hierarchy
The element that moves first is perceived as most important. Stagger in order of importance, not DOM order. Don't wait for one entrance to complete before starting the next — overlap entries. Total stagger sequence under 500ms regardless of item count keeps the scene from feeling like a slow drip.
Asymmetry between entrances and exits
Entrances need longer than exits. A card might take 0.4s to appear but 0.25s to disappear — entrances build presence, exits remove it, and remove takes less time than build.
Visual composition
Video frames are not web pages. Web layout patterns that work on a scrollable page often look broken in a fixed-frame composition.
- Two focal points minimum per scene. The eye needs somewhere to travel. A single text block floating in empty space reads as unfinished.
- Fill the frame. Hero text typically wants 60–80% of frame width. Web type sizes — 16px body, 32px headlines — disappear at video distance.
- Three layers minimum. Background treatment (glow, oversized faded type, color panel), foreground content, accent elements (dividers, labels, data bars). A scene with only one layer reads flat.
- Background is not empty. Radial glows, oversized faded type bleeding off-frame, subtle border panels, hairline rules. Pure solid
#000reads as "nothing loaded." - Anchor to edges. Pin content to left/top or right/bottom. Centered-and-floating is a web pattern that looks lost on a 16:9 canvas.
- Split frames. Data panel on the left, content on the right. Top bar with metadata, full-width below. Zone-based layouts beat centered stacks.
- Use structural elements. Rules, dividers, border panels. They create paths for the eye and animate well (
scaleXfrom 0).
Image motion treatment
Embedded images shouldn't sit flat — every image earns some motion treatment:
- Perspective tilt —
gsap.set(el, { transformPerspective: 1200, rotationY: -8 })plus abox-shadowcreates depth. Do NOT use CSStransform: perspective(...); GSAP will overwrite it. - Slow zoom (Ken Burns) — GSAP
scale: 1→1.04over beat duration. Makes photos feel cinematic rather than pasted in. - Device frame — wrap in a laptop or phone shape using
border-radiusandbox-shadow. - Floating UI — extract a key element and animate it at a different z-depth for parallax.
- Scroll reveal — clip the image to a viewport window and animate
yposition.
Load-Bearing GSAP Rules
Rules below came out of two independent website-to-hyperframes builds (2026-04-20) where compositions lint-clean and still ship broken — elements that never appear, ambient motion that doesn't scrub, entrance tweens that silently kill their target. The linter cannot catch these; the rules must be followed by the author.
- No iframes for captured content. Iframes do not seek deterministically with the timeline — the capture engine cannot scrub inside them, so they appear frozen (or blank) in the rendered output. If the source you're stylizing is a live web app, use the screenshots from
capture/as stacked panels or layered images, not live embeds.
- Never stack two transform tweens on the same element. A common failure: a
yentrance plus ascaleKen Burns on the same<img>. The second tween'simmediateRender: truewrites the element's initial state at construction time, overwriting whatever the first tween set — leaving the element invisible or offscreen with no lint warning. A secondary mechanism:tl.from()resets to its declared "from" state when the playhead is seeked past the timeline's end, so an element that looked correct in linear playback vanishes in the capture engine's non-linear seek. Fix one of two ways:
<!-- BAD: two transforms on one element -->
<img class="hero" src="..." />
<script>
tl.from(".hero", { y: 50, opacity: 0, duration: 0.6 }, 0);
tl.to(".hero", { scale: 1.04, duration: beat }, 0); // kills the entrance
</script>
<!-- GOOD option A: combine into one tween -->
<script>
tl.fromTo(
".hero",
{ y: 50, opacity: 0, scale: 1.0 },
{ y: 0, opacity: 1, scale: 1.04, duration: beat, ease: "none" },
0,
);
</script>
<!-- GOOD option B: split across parent + child -->
<div class="hero-wrap"><img class="hero" src="..." /></div>
<script>
tl.from(".hero-wrap", { y: 50, opacity: 0, duration: 0.6 }, 0); // entrance on parent
tl.to(".hero", { scale: 1.04, duration: beat }, 0); // Ken Burns on child
</script>- Prefer `tl.fromTo()` over `tl.from()` inside `.clip` scenes.
gsap.from()setsimmediateRender: trueby default, which writes the "from" state at timeline construction — before the.clipscene'sdata-startis active. Elements can flash visible, start from the wrong position, or skip their entrance entirely when the scene is seeked non-linearly (which the capture engine does). ExplicitfromTomakes the state at every timeline position deterministic:
// BRITTLE: immediateRender interacts badly with scene boundaries
tl.from(el, { opacity: 0, y: 50, duration: 0.6 }, t);
// DETERMINISTIC: state is defined at both ends, no immediateRender surprise
tl.fromTo(el, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 0.6 }, t);- Ambient pulses must attach to the seekable `tl`, never bare `gsap.to()`. Auras, shimmers, gentle float loops, logo breathing — all of these must be added to the scene's timeline, not fired standalone. Standalone tweens run on wallclock time and do not scrub with the capture engine, so the effect is absent in the rendered video even though it looks correct in the studio preview:
// BAD: lives outside the timeline, never renders in capture
gsap.to(".aura", { scale: 1.08, yoyo: true, repeat: 5, duration: 1.2 });
// GOOD: seekable, deterministic, renders
tl.to(".aura", { scale: 1.08, yoyo: true, repeat: 5, duration: 1.2 }, 0);- Hard-kill every scene boundary, not just captions. The same hard-kill pattern from
captions.mdgeneralizes to all elements with exit animations: any element whose visibility changes at a beat boundary needs a deterministictl.set()kill after its fade, because later tweens on the same element (orimmediateRenderfrom a sibling tween) can resurrect it. Apply to every element with an exit animation:
tl.to(el, { opacity: 0, duration: 0.3 }, beatEnd);
tl.set(el, { opacity: 0, visibility: "hidden" }, beatEnd + 0.3); // deterministic killThese are the exact rules with the exact code examples — don't summarize or shorten them. They exist because compositions that lint clean still ship broken without them.
Narration & Script
How to write narration scripts for video compositions. Read when the composition includes voiceover or TTS.
Pacing
- 2.5 words per second is natural speaking pace
- 15s = ~37 words. 30s = ~75 words. 60s = ~150 words
- Leave room for pauses. Silence between sentences is a feature, not dead air
- The script should feel SHORTER than the video — visual breathing room matters
Tone
Write like a person, not a brochure:
- Use contractions: "it's", "you'll", "that's", "we've"
- Vary sentence length — short punchy phrases mixed with longer flowing ones
- Read it out loud. If it sounds robotic, rewrite it
- Avoid jargon unless the audience expects it
Number Pronunciation
Write what you want the voice to say. TTS reads literally.
| In the product | Write in script as |
|---|---|
| 135+ | more than one hundred thirty five |
| $1.9T | nearly two trillion dollars |
| 99.999% | ninety nine point nine percent |
| 200M+ | over two hundred million |
| 10x | ten times |
| API | A P I |
| stripe.com | stripe dot com |
The visual can show the exact figure while the voice rounds it.
Structure
For product videos:
1. Hook — what's surprising or impressive about this product? A bold claim, a provocative question, a contrast, or a striking number. This is the opening line. Vary the hook type — don't default to a stat every time. 2. Story — what does the product do? Who uses it? Keep it concrete. 3. Proof — stats, customer names, social proof. Real numbers from the product. 4. CTA — what should the viewer do? "Start building at stripe dot com."
Not every video needs all four. A 15-second social ad might be Hook + Proof + CTA. A 60-second product tour uses all four with more Story.
The Opening Line
The most important sentence in the video. It must create tension, curiosity, or surprise in the first 3 seconds.
Patterns that work:
- A bold claim: "The financial infrastructure that powers the internet economy."
- A question that provokes: "What if your database could think?"
- A contrast: "Your AI agent already knows how to make videos. It just needs the right format."
- A number that shocks: "Nearly two trillion dollars." (Use sparingly — not every video should open with a stat.)
If the opening is generic ("Welcome to Stripe" / "Introducing our product"), start over.
Example
From a 62-second product launch video (team reference):
Your AI agent already knows how to make videos.
It just needs the right format.
This is Hyperframes. An open source framework. HTML in, video out.
A div is a keyframe. Data attributes are your timeline.
CSS is your look. G-Sap is your animation engine.
Anything a browser can render can be a frame in your video.
CSS animations. G-Sap. Lottie. Shaders. Three.js.
Drop in music, sound effects, footage — it all composes together.
No new framework for the agent to learn.
Just HTML.
The agent writes it. The renderer captures every frame as MP4.
It's deterministic. Identical outputs, every time.
Give your agent the CLI. Tell it what to make.
Watch it build.
Hyperframes. Go make something.Note: ~140 words for 62 seconds — that's 2.3 words/sec, leaving room for pauses and visual breathing.
3D
3D Card Flip
180° Y-axis rotation. Requires CSS: backface-visibility: hidden; transform-style: preserve-3d; on both scene-inners. Parent needs perspective: 1200px.
tl.set(new, { rotationY: -180, opacity: 1 }, T);
tl.to(old, { rotationY: 180, duration: 0.6, ease: "power2.inOut" }, T);
tl.to(new, { rotationY: 0, duration: 0.6, ease: "power2.inOut" }, T);
tl.set(old, { opacity: 0 }, T + 0.6);Grid
Grid Dissolve
Grid of colored cells covers the frame in a ripple from center. Scene swaps at 50% coverage. Cells fade out in ripple.
12-cell (4x3, each 480x270): standard 120-cell (12x10, each 160x108): dense variant — lower opacity (0.75), tighter ripple
Cells are created dynamically in JS, sorted by distance from center for ripple stagger.
Other
Gravity Drop
Old scene falls down with slight rotation. New scene was behind it. Needs z-index.
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10 }, T);
tl.to(old, { y: 1200, rotation: 4, duration: 0.5, ease: "power3.in" }, T);
tl.set(old, { opacity: 0, zIndex: "auto" }, T + 0.5);
tl.set(new, { zIndex: "auto" }, T + 0.5);Morph Circle
A circle scales up from center to fill frame (becoming the new scene's background color). New scene content fades in on top.
tl.set("#morph-circle", { background: newBgColor, opacity: 1, scale: 0 }, T);
tl.to("#morph-circle", { scale: 30, duration: 0.5, ease: "power3.in" }, T);
tl.set(old, { opacity: 0 }, T + 0.4);
tl.set(new, { opacity: 1 }, T + 0.4);
tl.to("#morph-circle", { opacity: 0, duration: 0.15, ease: "power2.out" }, T + 0.5);