hyperframes: add HTML/CSS/GSAP as a parallel composition runtime

Separates creative grammar (renderer_family) from technical engine
(render_runtime) so HyperFrames can stand alongside Remotion as a
first-class runtime instead of masquerading as a Remotion sub-case.
Locks runtime choice at proposal stage and enforces it end-to-end: the
schemas require it, video_compose routes by it, the reviewer fails
closed on silent swaps, and a parametrized contract test walks every
pipeline manifest to ensure each planning-stage skill explains the
conversation to the user. Adds hyperframes_compose (scaffold/lint/
validate/render/doctor/add_block), a playbook -> CSS style bridge, and
vendored HyperFrames Layer 3 skills from commit d291358, pinned via
PROVENANCE.md for future re-sync. Final_review now records
render_runtime_used and runtime_swap_detected so compose lies are
catchable after the fact.
This commit is contained in:
calesthio
2026-04-18 18:37:00 -07:00
parent 9e17263aa5
commit b4f7ec4eee
118 changed files with 10128 additions and 126 deletions
+136
View File
@@ -0,0 +1,136 @@
---
name: hyperframes-cli
description: HyperFrames CLI tool — hyperframes init, lint, validate, preview, render, transcribe, tts, doctor, browser, info, upgrade, compositions, docs, benchmark. Use when scaffolding a project, linting or validating compositions, previewing in the studio, rendering to video, transcribing audio, generating TTS, or troubleshooting the HyperFrames environment.
---
<!--
Vendored into OpenMontage from C:\Users\ishan\Documents\hyperframes\skills\hyperframes-cli\SKILL.md
Source commit: d291358 (2026-04-17)
See .agents/skills/hyperframes/PROVENANCE.md for re-sync instructions.
OpenMontage-local edit: added `validate` to the top-line command list and a
dedicated "Validation" section — upstream omits it even though the CLI ships it
and `hyperframes validate` is the real browser-based contract check (contrast,
timelines, assets) that runs before render.
-->
# HyperFrames CLI
Everything runs through `npx hyperframes`. Requires Node.js >= 22 and FFmpeg.
## Workflow
1. **Scaffold**`npx hyperframes init my-video`
2. **Write** — author HTML composition (see the `hyperframes` skill)
3. **Lint**`npx hyperframes lint` (static checks)
4. **Validate**`npx hyperframes validate` (browser-based runtime checks + contrast audit)
5. **Preview**`npx hyperframes preview`
6. **Render**`npx hyperframes render`
Lint catches static issues (missing `data-composition-id`, overlapping tracks, unregistered timelines). Validate catches runtime/visual issues by seeking into the paused composition in a real browser (contrast, broken overlays, missing assets). Run both before render.
## Validation
```bash
npx hyperframes validate # full validation (static + contrast + contract)
npx hyperframes validate --no-contrast # skip WCAG contrast audit when iterating fast
npx hyperframes validate --json # machine-readable output
```
Validation seeks to several timestamps in the paused composition, screenshots the page, samples pixels behind every text element to compute WCAG contrast ratios, and verifies runtime contracts (`window.__timelines` registration, `class="clip"` presence, valid `data-*` attributes). Run it before every render — it's cheap and catches issues lint cannot see.
## Scaffolding
```bash
npx hyperframes init my-video # interactive wizard
npx hyperframes init my-video --example warm-grain # pick an example
npx hyperframes init my-video --video clip.mp4 # with video file
npx hyperframes init my-video --audio track.mp3 # with audio file
npx hyperframes init my-video --non-interactive # skip prompts (CI/agents)
```
Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decision-tree`, `kinetic-type`, `product-promo`, `nyt-graph`.
`init` creates the right file structure, copies media, transcribes audio with Whisper, and installs AI coding skills. Use it instead of creating files by hand.
## Linting
```bash
npx hyperframes lint # current directory
npx hyperframes lint ./my-project # specific project
npx hyperframes lint --verbose # info-level findings
npx hyperframes lint --json # machine-readable
```
Lints `index.html` and all files in `compositions/`. Reports errors (must fix), warnings (should fix), and info (with `--verbose`).
## Previewing
```bash
npx hyperframes preview # serve current directory
npx hyperframes preview --port 4567 # custom port (default 3002)
```
Hot-reloads on file changes. Opens the studio in your browser automatically.
## Rendering
```bash
npx hyperframes render # standard MP4
npx hyperframes render --output final.mp4 # named output
npx hyperframes render --quality draft # fast iteration
npx hyperframes render --fps 60 --quality high # final delivery
npx hyperframes render --format webm # transparent WebM
npx hyperframes render --docker # byte-identical
```
| Flag | Options | Default | Notes |
| -------------- | --------------------- | -------------------------- | --------------------------- |
| `--output` | path | renders/name_timestamp.mp4 | Output path |
| `--fps` | 24, 30, 60 | 30 | 60fps doubles render time |
| `--quality` | draft, standard, high | standard | draft for iterating |
| `--format` | mp4, webm | mp4 | WebM supports transparency |
| `--workers` | 1-8 or auto | auto | Each spawns Chrome |
| `--docker` | flag | off | Reproducible output |
| `--gpu` | flag | off | GPU-accelerated encoding |
| `--strict` | flag | off | Fail on lint errors |
| `--strict-all` | flag | off | Fail on errors AND warnings |
**Quality guidance:** `draft` while iterating, `standard` for review, `high` for final delivery.
## Transcription
```bash
npx hyperframes transcribe audio.mp3
npx hyperframes transcribe video.mp4 --model medium.en --language en
npx hyperframes transcribe subtitles.srt # import existing
npx hyperframes transcribe subtitles.vtt
npx hyperframes transcribe openai-response.json
```
## Text-to-Speech
```bash
npx hyperframes tts "Text here" --voice af_nova --output narration.wav
npx hyperframes tts script.txt --voice bf_emma
npx hyperframes tts --list # show all voices
```
## Troubleshooting
```bash
npx hyperframes doctor # check environment (Chrome, FFmpeg, Node, memory)
npx hyperframes browser # manage bundled Chrome
npx hyperframes info # version and environment details
npx hyperframes upgrade # check for updates
```
Run `doctor` first if rendering fails. Common issues: missing FFmpeg, missing Chrome, low memory.
## Other
```bash
npx hyperframes compositions # list compositions in project
npx hyperframes docs # open documentation
npx hyperframes benchmark . # benchmark render performance
```
@@ -0,0 +1,104 @@
---
name: hyperframes-registry
description: Install and wire registry blocks and components into HyperFrames compositions. Use when running hyperframes add, installing a block or component, wiring an installed item into index.html, or working with hyperframes.json. Covers the add command, install locations, block sub-composition wiring, component snippet merging, and registry discovery.
---
# HyperFrames Registry
The registry provides reusable blocks and components installable via `hyperframes add <name>`.
- **Blocks** — standalone sub-compositions (own dimensions, duration, timeline). Included via `data-composition-src` in a host composition.
- **Components** — effect snippets (no own dimensions). Pasted directly into a host composition's HTML.
## When to use this skill
- User mentions `hyperframes add`, "block", "component", or `hyperframes.json`
- Output from `hyperframes add` appears in the session (file paths, clipboard snippet)
- You need to wire an installed item into an existing composition
- You want to discover what's available in the registry
## Quick reference
```bash
hyperframes add data-chart # install a block
hyperframes add grain-overlay # install a component
hyperframes add shimmer-sweep --dir . # target a specific project
hyperframes add data-chart --json # machine-readable output
hyperframes add data-chart --no-clipboard # skip clipboard (CI/headless)
```
After install, the CLI prints which files were written and a snippet to paste into your host composition. The snippet is a starting point — you'll need to add `data-composition-id` (must match the block's internal composition ID), `data-start`, and `data-track-index` attributes when wiring blocks.
Note: `hyperframes add` only works for blocks and components. For examples, use `hyperframes init <dir> --example <name>` instead.
## Install locations
Blocks install to `compositions/<name>.html` by default.
Components install to `compositions/components/<name>.html` by default.
These paths are configurable in `hyperframes.json`:
```json
{
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
"paths": {
"blocks": "compositions",
"components": "compositions/components",
"assets": "assets"
}
}
```
See [install-locations.md](./references/install-locations.md) for full details.
## Wiring blocks
Blocks are standalone compositions — include them via `data-composition-src` in your host `index.html`:
```html
<div
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-start="2"
data-duration="15"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Key attributes:
- `data-composition-src` — path to the block HTML file
- `data-composition-id` — must match the block's internal ID
- `data-start` — when the block appears in the host timeline (seconds)
- `data-duration` — how long the block plays
- `data-width` / `data-height` — block canvas dimensions
- `data-track-index` — layer ordering (higher = in front)
See [wiring-blocks.md](./references/wiring-blocks.md) for full details.
## Wiring components
Components are snippets — paste their HTML into your composition's markup, their CSS into your style block, and their JS into your script (if any):
1. Read the installed file (e.g., `compositions/components/grain-overlay.html`)
2. Copy the HTML elements into your composition's `<div data-composition-id="...">`
3. Copy the `<style>` block into your composition's styles
4. Copy any `<script>` content into your composition's script (before your timeline code)
5. If the component exposes GSAP timeline integration (see the comment block in the snippet), add those calls to your timeline
See [wiring-components.md](./references/wiring-components.md) for full details.
## Discovery
Browse available items:
```bash
# Read the registry manifest
curl -s https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry/registry.json
```
Each item's `registry-item.json` contains: name, type, title, description, tags, dimensions (blocks only), duration (blocks only), and file list.
See [discovery.md](./references/discovery.md) for details on filtering by type and tags.
@@ -0,0 +1,51 @@
# Worked Example: Adding a Block
## Scenario
User has an existing HyperFrames project and wants to add an animated chart alongside their video content.
## Steps
### 1. Install the block
```bash
hyperframes add data-chart
```
### 2. Wire into index.html
```html
<div id="stage" data-composition-id="main" data-width="1920" data-height="1080" data-duration="30">
<video
id="speaker"
src="speaker.mp4"
data-start="0"
data-duration="30"
data-track-index="0"
style="position: absolute; width: 60%; height: 100%; left: 0; top: 0; object-fit: cover;"
></video>
<!-- Data chart appears at 5s in the right 40% of the screen -->
<div
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-start="5"
data-duration="15"
data-track-index="1"
data-width="1920"
data-height="1080"
style="position: absolute; right: 0; top: 0; width: 40%; height: 100%;"
></div>
</div>
```
### 3. Lint and preview
```bash
hyperframes lint
hyperframes preview
```
### 4. Customize (optional)
Edit `compositions/data-chart.html` — data arrays are at the top of the script, colors are in the CSS rules scoped under `[data-composition-id="data-chart"]`.
@@ -0,0 +1,73 @@
# Worked Example: Adding a Component
## Scenario
User wants to add a shimmer light sweep effect to their title text.
## Steps
### 1. Install the component
```bash
hyperframes add shimmer-sweep
```
### 2. Read the snippet
Open `compositions/components/shimmer-sweep.html` and read the comment header.
### 3. Wire into your composition
**HTML** — wrap target elements:
```html
<div class="shimmer-sweep-target" style="--shimmer-color: rgba(255, 255, 255, 0.5)">
<h1 class="title">AI-Powered Video</h1>
</div>
```
**CSS** — paste the `.shimmer-sweep-target` and `.shimmer-mask` rules from the snippet.
**JS** — paste the auto-injection script (before timeline code):
```js
document.querySelectorAll(".shimmer-sweep-target").forEach((el) => {
if (!el.querySelector(".shimmer-mask")) {
const mask = document.createElement("div");
mask.className = "shimmer-mask";
el.appendChild(mask);
}
});
```
**Timeline** — add the sweep:
```js
tl.fromTo(
".shimmer-sweep-target",
{
"--shimmer-pos": "-20%",
},
{
"--shimmer-pos": "120%",
duration: 1.2,
ease: "power2.inOut",
stagger: 0.15,
},
1.5,
);
```
### 4. Lint and preview
```bash
hyperframes lint
hyperframes preview
```
### 5. Customize
- `--shimmer-color`: highlight color per element
- `--shimmer-width`: light band width (default 20%)
- `--shimmer-angle`: sweep direction (default 120deg)
- Timeline `duration`, `ease`, `stagger`: control speed and feel
@@ -0,0 +1,54 @@
# The demo.html Convention
## Why components ship demo.html
Every component in the registry ships a companion `demo.html` file alongside its snippet. The demo serves two purposes:
1. **Preview fixture** — the CI preview pipeline renders the demo to generate thumbnail images and preview videos for the catalog docs page.
2. **Usage example** — the demo shows the component effect applied to representative content, serving as a working reference.
## Demo structure
A demo is a complete, standalone HTML composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Component Name — Demo</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
/* reset + canvas size */
</style>
</head>
<body>
<div data-composition-id="<name>-demo" data-width="1920" data-height="1080" data-duration="N">
<!-- Demo content showing the effect -->
<!-- Component snippet inlined here -->
</div>
<script>
// GSAP timeline demonstrating the effect
window.__timelines = window.__timelines || {};
window.__timelines["<name>-demo"] = tl;
</script>
</body>
</html>
```
Key conventions:
- `data-composition-id` is `<component-name>-demo` to avoid collisions
- The demo is self-contained — all CSS and JS from the snippet is inlined
- The GSAP timeline is registered on `window.__timelines`
- Duration should be long enough to showcase the effect (typically 5-8 seconds)
## Blocks don't need demo.html
Blocks are already standalone compositions that can be rendered directly. Only components need the demo wrapper.
## Demos are not installed
The `demo.html` is NOT installed by `hyperframes add` — it exists only in the registry for preview generation and as a reference.
@@ -0,0 +1,53 @@
# Registry Discovery
## Reading the registry manifest
The top-level `registry.json` lists all available items:
```bash
curl -s https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry/registry.json
```
Each entry has `name` and `type` (`hyperframes:example`, `hyperframes:block`, or `hyperframes:component`).
## Reading an item's manifest
Each item has a `registry-item.json` with full metadata:
```
<base>/<type-dir>/<name>/registry-item.json
```
Where `<type-dir>` is `examples`, `blocks`, or `components`.
## Item manifest fields
| Field | Type | Required | Description |
| ---------------------- | -------- | -------- | ---------------------------------------------- |
| `name` | string | yes | Kebab-case identifier |
| `type` | string | yes | `hyperframes:block` or `hyperframes:component` |
| `title` | string | yes | Human-readable title |
| `description` | string | yes | One-line description |
| `tags` | string[] | no | Filter tags (e.g., `["data", "chart"]`) |
| `dimensions` | object | blocks | `{ width, height }` — blocks only |
| `duration` | number | blocks | Duration in seconds — blocks only |
| `files` | array | yes | Files to install (`path`, `target`, `type`) |
| `registryDependencies` | string[] | no | Other registry items this depends on |
## Available items
### Blocks
| Name | Description | Tags |
| ------------ | ----------------------------------------------- | ------------------------------- |
| `data-chart` | Animated bar + line chart with staggered reveal | data, chart, statistics |
| `flowchart` | Decision tree with SVG connectors and cursor | diagram, flowchart, interactive |
| `logo-outro` | Cinematic logo reveal with tagline | branding, outro, logo |
### Components
| Name | Description | Tags |
| -------------------- | --------------------------------------- | -------------------------------- |
| `grain-overlay` | Animated film grain texture overlay | texture, grain, overlay, film |
| `shimmer-sweep` | CSS gradient light sweep for AI accents | text, shimmer, highlight, effect |
| `grid-pixelate-wipe` | Grid dissolve transition between scenes | transition, wipe, grid, pixelate |
@@ -0,0 +1,45 @@
# Install Locations
## Default paths
| Item type | Default install path | Configured by |
| --------- | ------------------------------------- | ----------------------------------- |
| Block | `compositions/<name>.html` | `hyperframes.json#paths.blocks` |
| Component | `compositions/components/<name>.html` | `hyperframes.json#paths.components` |
## How path remapping works
The `target` field in each item's `registry-item.json` specifies a default install path. The `add` command remaps the prefix based on `hyperframes.json#paths`:
- Block targets starting with `compositions/` get remapped to `<paths.blocks>/`
- Component targets starting with `compositions/components/` get remapped to `<paths.components>/`
## hyperframes.json
Created automatically by `hyperframes init`. If it doesn't exist when you run `add`, the CLI creates it with defaults:
```json
{
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
"paths": {
"blocks": "compositions",
"components": "compositions/components",
"assets": "assets"
}
}
```
## Custom layouts
To install blocks into a `scenes/` directory instead of `compositions/`:
```json
{
"paths": {
"blocks": "scenes"
}
}
```
Then `hyperframes add data-chart` writes to `scenes/data-chart.html` instead of `compositions/data-chart.html`. The snippet output reflects the remapped path.
@@ -0,0 +1,91 @@
# Wiring Blocks
Blocks are standalone compositions with their own `data-composition-id`, dimensions, duration, and GSAP timeline. Include them in a host composition using `data-composition-src` on a `<div>`.
## Basic wiring
After `hyperframes add data-chart`, wire it into your `index.html`:
```html
<div id="stage" data-composition-id="main" data-width="1920" data-height="1080" data-duration="20">
<video id="a-roll" src="video.mp4" data-start="0" data-duration="20" data-track-index="0"></video>
<!-- Block: appears at 2s, plays for 15s, on layer 1 -->
<div
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-start="2"
data-duration="15"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
</div>
```
## Required attributes
| Attribute | Description |
| ---------------------- | -------------------------------------------------------------------- |
| `data-composition-src` | Path to the block HTML file (relative to index.html) |
| `data-composition-id` | Unique ID matching the block's internal composition ID |
| `data-start` | When the block appears in the host timeline (seconds) |
| `data-duration` | How long the block plays (seconds, at most the block's own duration) |
| `data-track-index` | Layer ordering — higher numbers render in front |
| `data-width` | Block canvas width (match the block's dimensions) |
| `data-height` | Block canvas height (match the block's dimensions) |
## Timeline coordination
The block's internal GSAP timeline runs independently from the host timeline. The HyperFrames runtime loads the sub-composition, finds its `window.__timelines` registration, and seeks the block in sync with the host, offset by `data-start`. You do NOT need to reference the block's timeline in your host's GSAP code.
## Positioning blocks
To position a block in a specific area of the screen, add CSS:
```html
<div
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-start="2"
data-duration="15"
data-track-index="1"
data-width="1920"
data-height="1080"
style="position: absolute; right: 0; top: 0; width: 40%; height: 100%;"
></div>
```
## Multiple blocks
Include multiple blocks sequentially or overlapping:
```html
<div
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-start="0"
data-duration="15"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
<div
data-composition-id="flowchart"
data-composition-src="compositions/flowchart.html"
data-start="15"
data-duration="12"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
<div
data-composition-id="logo-outro"
data-composition-src="compositions/logo-outro.html"
data-start="27"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
@@ -0,0 +1,77 @@
# Wiring Components
Components are effect snippets — HTML, CSS, and optionally JS that you merge directly into an existing composition. Unlike blocks, components have no standalone timeline; they participate in the host composition's timeline.
## General process
1. Run `hyperframes add <component-name>`
2. Open the installed file (e.g., `compositions/components/grain-overlay.html`)
3. Read the comment header for usage instructions
4. Copy the parts into your host composition:
- **HTML elements** — inside your `<div data-composition-id="...">`
- **CSS styles** — into your composition's `<style>` block
- **JS setup** — into your composition's `<script>`, before your timeline code
- **Timeline calls** — into your GSAP timeline (if the component exposes them)
## Example: grain-overlay (CSS-only, no timeline integration)
```html
<!-- Paste the overlay div into your composition -->
<div
id="grain-overlay"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 100;"
>
<div class="grain-texture"></div>
</div>
```
Then paste the CSS keyframes and `.grain-texture` rule into your styles. No GSAP timeline calls needed — the grain animates via CSS `@keyframes`.
## Example: shimmer-sweep (needs timeline integration)
Wrap target elements, paste CSS and JS, then drive the sweep from your timeline:
```js
tl.fromTo(
".shimmer-sweep-target",
{
"--shimmer-pos": "-20%",
},
{
"--shimmer-pos": "120%",
duration: 1.2,
ease: "power2.inOut",
stagger: 0.15,
},
2.0,
);
```
## Example: grid-pixelate-wipe (scene transition)
Paste the overlay HTML and CSS, then drive `.grid-cell` scale in your timeline:
```js
// Cover screen
tl.to(
".grid-cell",
{ scale: 1, duration: 0.6, stagger: { amount: 0.6, from: "center" }, ease: "power2.inOut" },
5.0,
);
// Swap scenes
tl.set("#scene-a", { opacity: 0 }, 5.6);
tl.set("#scene-b", { opacity: 1 }, 5.6);
// Reveal
tl.to(
".grid-cell",
{ scale: 0, duration: 0.6, stagger: { amount: 0.6, from: "edges" }, ease: "power2.inOut" },
5.6,
);
```
## Key principles
- Components inherit the host composition's dimensions and duration
- Place component HTML at the appropriate z-index relative to your content
- Read the comment header in each snippet for customizable values
- Run `hyperframes lint` after wiring to catch structural issues
+59
View File
@@ -0,0 +1,59 @@
# Provenance — HyperFrames Layer 3 Skills
These skills are **vendored** from the upstream HyperFrames monorepo. Do not
edit them expecting the changes to survive a re-sync — changes need to go
upstream, or be recorded in the local-edit log below.
## Source
- Repo: https://github.com/heygen-com/hyperframes
- Local clone: `C:\Users\ishan\Documents\hyperframes`
- Vendored commit: `d291358`
- Vendored date: `2026-04-17`
## Mirrored directories
| OpenMontage path | Upstream path |
| ---------------------------------------------- | ----------------------------------------- |
| `.agents/skills/hyperframes/` | `skills/hyperframes/` |
| `.agents/skills/hyperframes-cli/` | `skills/hyperframes-cli/` |
| `.agents/skills/hyperframes-registry/` | `skills/hyperframes-registry/` |
| `.agents/skills/website-to-hyperframes/` | `skills/website-to-hyperframes/` |
The `gsap` upstream skill is NOT re-vendored — OpenMontage already ships its
own GSAP skill family under `.agents/skills/gsap*/`.
## Local edits
Any divergence from upstream is noted at the top of the edited file as an
HTML comment starting with `OpenMontage-local`. Current edits:
- `hyperframes-cli/SKILL.md` — added `validate` to the command list and a
dedicated Validation section. Upstream omits it, but the CLI ships it and
OpenMontage's HyperFrames runtime path relies on `hyperframes validate` as
a real browser-based contract check before render.
## Re-sync procedure
```bash
# From the hyperframes clone
cd C:/Users/ishan/Documents/hyperframes
git pull
# From OpenMontage
cd C:/Users/ishan/Documents/OpenMontage
rm -rf .agents/skills/hyperframes .agents/skills/hyperframes-cli \
.agents/skills/hyperframes-registry .agents/skills/website-to-hyperframes
cp -r C:/Users/ishan/Documents/hyperframes/skills/hyperframes .agents/skills/
cp -r C:/Users/ishan/Documents/hyperframes/skills/hyperframes-cli .agents/skills/
cp -r C:/Users/ishan/Documents/hyperframes/skills/hyperframes-registry .agents/skills/
cp -r C:/Users/ishan/Documents/hyperframes/skills/website-to-hyperframes .agents/skills/
# Then re-apply the local edits listed above and bump the vendored commit SHA.
```
## Why we vendor instead of referencing the upstream clone directly
1. OpenMontage contributors may not have the HyperFrames monorepo on disk.
2. Skills must be readable from the OpenMontage tree for agent discovery.
3. We want deterministic knowledge — upstream moves; we control when we pick
up changes.
+347
View File
@@ -0,0 +1,347 @@
---
name: hyperframes
description: Create video compositions, animations, title cards, overlays, captions, voiceovers, audio-reactive visuals, and scene transitions in HyperFrames HTML. Use when asked to build any HTML-based video content, add captions or subtitles synced to audio, generate text-to-speech narration, create audio-reactive animation (beat sync, glow, pulse driven by music), add animated text highlighting (marker sweeps, hand-drawn circles, burst lines, scribble, sketchout), or add transitions between scenes (crossfades, wipes, reveals, shader transitions). Covers composition authoring, timing, media, and the full video production workflow. For CLI commands (init, lint, preview, render, transcribe, tts) see the hyperframes-cli skill.
---
# 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
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. **Timing** — which clips drive the duration, where do transitions land, what's the pacing.
4. **Layout** — build the end-state first. See "Layout Before Animation" below.
5. **Animate** — then add motion using the rules below.
For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
### Visual Identity Gate
<HARD-GATE>
Before writing ANY composition HTML, you MUST have a visual identity defined. Do NOT write compositions with default or generic colors.
Check in this order:
1. **DESIGN.md exists in the project?** → Read it. Use its exact colors, fonts, motion rules, and "What NOT to Do" constraints.
2. **visual-style.md exists?** → Read it. Apply its `style_prompt_full` and structured fields. (Note: `visual-style.md` is a project-specific file. `visual-styles.md` is the style library with 8 named presets — different files.)
3. **User named a style** (e.g., "Swiss Pulse", "dark and techy", "luxury brand")? → Read [visual-styles.md](./visual-styles.md) for the 8 named presets. Generate a minimal DESIGN.md with: `## Style Prompt` (one paragraph), `## Colors` (3-5 hex values with roles), `## Typography` (1-2 font families), `## What NOT to Do` (3-5 anti-patterns).
4. **None of the above?** → Ask 3 questions before writing any HTML:
- What's the mood? (explosive / cinematic / fluid / technical / chaotic / warm)
- Light or dark canvas?
- Any specific brand colors, fonts, or visual references?
Then generate a minimal DESIGN.md from the answers.
Every composition must trace its palette and typography back to a DESIGN.md, visual-style.md, or explicit user direction. If you're reaching for `#333`, `#3b82f6`, or `Roboto` — you skipped this step.
</HARD-GATE>
For motion defaults, sizing, entrance patterns, and easing — follow [house-style.md](./house-style.md). The house style handles HOW things move. The DESIGN.md handles WHAT things look like.
## 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.
4. **Add exits with `gsap.to()`** — animate TO offscreen/invisible FROM the CSS position.
### Example
```css
/* 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:**
```css
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
display: flex; /* ... */
}
```
```js
// 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 |
## 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:
```html
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- content -->
<style>
[data-composition-id="my-comp"] {
/* scoped styles */
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// tweens...
window.__timelines["my-comp"] = tl;
</script>
</div>
</template>
```
Load in root: `<div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>`
## Video and Audio
Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
```html
<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:**
```js
// BANNED — this empties the scene before the transition can use it
tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);
tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);
// transition fires on empty frame
```
**RIGHT — entrance only, transition handles exit:**
```js
// 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-nums` on number columns
When no `visual-style.md` or animation direction is provided, follow [house-style.md](./house-style.md) for aesthetic defaults.
## Typography and Assets
- **Fonts:** Just write the `font-family` you want in CSS — the compiler embeds supported fonts automatically. If a font isn't supported, the compiler warns.
- 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 the full composition first — match existing fonts, colors, animation patterns
- Only change what was requested
- Preserve timing of unrelated clips
## Output Checklist
- [ ] `npx hyperframes lint` and `npx hyperframes validate` both pass
- [ ] Contrast warnings addressed (see Quality Checks below)
- [ ] Animation choreography verified (see Quality Checks below)
## Quality Checks
### 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 validate` until clean
Use `--no-contrast` to skip if iterating rapidly and you'll check later.
### Animation Map
After authoring animations, run the animation map to verify choreography:
```bash
node skills/hyperframes/scripts/animation-map.mjs <composition-dir> \
--out <composition-dir>/.hyperframes/anim-map
```
Outputs 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/tts.md](references/tts.md)** — Text-to-speech with Kokoro-82M. Voice selection, speed tuning, TTS+captions workflow. Read when generating narration or voiceover.
- **[references/audio-reactive.md](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/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: easing as emotion, timing as weight, choreography as hierarchy, scene pacing, ambient motion, anti-patterns. Read when choreographing GSAP animations.
- **[visual-styles.md](visual-styles.md)** — 8 named visual styles (Swiss Pulse, Velvet Standard, Deconstructed, Maximalist Type, Data Drift, Soft Signal, Folk Frequency, Shadow Cut) with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating DESIGN.md.
- **[house-style.md](house-style.md)** — Default motion, sizing, and color palettes when no style is 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)** — Transcription commands, whisper models, external APIs, troubleshooting.
- **[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](references/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.
@@ -0,0 +1,19 @@
# Data in Motion
Light guidance for data and stats in video compositions. The [house style](./house-style.md) 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
+71
View File
@@ -0,0 +1,71 @@
# House Style
Creative direction for compositions when no `visual-style.md` is provided. These are starting points — override anything that doesn't serve the content.
## 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](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 `#000` or `#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](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.
## Motion
See [references/motion-principles.md](references/motion-principles.md) for full rules. Quick: 0.30.6s, vary eases, combine transforms on entrances, overlap entries.
## Typography
See [references/typography.md](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](palettes/bold-energetic.md) |
| Warm / Editorial | Storytelling, documentaries, case studies | [palettes/warm-editorial.md](palettes/warm-editorial.md) |
| Dark / Premium | Tech, finance, luxury, cinematic | [palettes/dark-premium.md](palettes/dark-premium.md) |
| Clean / Corporate | Explainers, tutorials, presentations | [palettes/clean-corporate.md](palettes/clean-corporate.md) |
| Nature / Earth | Sustainability, outdoor, organic | [palettes/nature-earth.md](palettes/nature-earth.md) |
| Neon / Electric | Gaming, tech, nightlife | [palettes/neon-electric.md](palettes/neon-electric.md) |
| Pastel / Soft | Fashion, beauty, lifestyle, wellness | [palettes/pastel-soft.md](palettes/pastel-soft.md) |
| Jewel / Rich | Luxury, events, sophisticated | [palettes/jewel-rich.md](palettes/jewel-rich.md) |
| Monochrome | Dramatic, typography-focused | [palettes/monochrome.md](palettes/monochrome.md) |
Or derive from OKLCH — pick a hue, build bg/fg/accent at different lightnesses, tint everything toward that hue.
@@ -0,0 +1,14 @@
# Bold / Energetic
Product launches, social media, announcements, high-energy content.
```
#FFBE0B #FB5607 #FF006E #8338EC #3A86FF
#F72585 #7209B7 #3A0CA3 #4361EE #4CC9F0
#EF476F #FFD166 #06D6A0 #118AB2 #073B4C
#FF595E #FFCA3A #8AC926 #1982C4 #6A4C93
#9B5DE5 #F15BB5 #FEE440 #00BBF9 #00F5D4
#390099 #9E0059 #FF0054 #FF5400 #FFBD00
#3D348B #7678ED #F7B801 #F18701 #F35B04
#FFBC42 #D81159 #8F2D56 #218380 #73D2DE
```
@@ -0,0 +1,14 @@
# Clean / Corporate
Explainers, tutorials, presentations, professional content.
```
#FFFCF2 #CCC5B9 #403D39 #252422 #EB5E28
#22223B #4A4E69 #9A8C98 #C9ADA7 #F2E9E4
#3D5A80 #98C1D9 #E0FBFC #EE6C4D #293241
#2B2D42 #8D99AE #EDF2F4 #EF233C #D90429
#353535 #3C6E71 #FFFFFF #D9D9D9 #284B63
#E7ECEF #274C77 #6096BA #A3CEF1 #8B8C89
#CFDBD5 #E8EDDF #F5CB5C #242423 #333533
#2F6690 #3A7CA5 #D9DCD6 #16425B #81C3D7
```
@@ -0,0 +1,14 @@
# Dark / Premium
Tech, finance, luxury, cinematic content.
```
#000000 #14213D #FCA311 #E5E5E5 #FFFFFF
#000814 #001D3D #003566 #FFC300 #FFD60A
#0D1B2A #1B263B #415A77 #778DA9 #E0E1DD
#0D1321 #1D2D44 #3E5C76 #748CAB #F0EBD8
#011627 #FDFFFC #2EC4B6 #E71D36 #FF9F1C
#0B090A #161A1D #660708 #A4161A #E5383B
#001427 #708D81 #F4D58D #BF0603 #8D0801
#001524 #15616D #FFECD1 #FF7D00 #78290F
```
@@ -0,0 +1,14 @@
# Jewel / Rich
Luxury, events, sophisticated, high-end content.
```
#5F0F40 #9A031E #FB8B24 #E36414 #0F4C5C
#780000 #C1121F #FDF0D5 #003049 #669BBC
#10002B #240046 #3C096C #5A189A #7B2CBF
#355070 #6D597A #B56576 #E56B6F #EAAC8B
#6F1D1B #BB9457 #432818 #99582A #FFE6A7
#231942 #5E548E #9F86C0 #BE95C4 #E0B1CB
#461220 #8C2F39 #B23A48 #FCB9B2 #FED0BB
#780116 #F7B538 #DB7C26 #D8572A #C32F27
```
@@ -0,0 +1,14 @@
# Monochrome
Dramatic, typography-focused, serious content.
```
#F8F9FA #E9ECEF #DEE2E6 #CED4DA #ADB5BD #6C757D #495057 #343A40 #212529
#0466C8 #0353A4 #023E7D #002855 #001233
#012A4A #013A63 #01497C #2A6F97 #468FAF #89C2D9
#582F0E #7F4F24 #936639 #A68A64 #C2C5AA
#463F3A #8A817C #BCB8B1 #F4F3EE #E0AFA0
#03071E #370617 #6A040F #9D0208 #DC2F02 #F48C06 #FFBA08
#590D22 #800F2F #A4133C #FF4D6D #FF8FA3 #FFCCD5
#220901 #621708 #941B0C #BC3908 #F6AA1C
```
@@ -0,0 +1,14 @@
# Nature / Earth
Sustainability, outdoor, organic, wellness content.
```
#606C38 #283618 #FEFAE0 #DDA15E #BC6C25
#DAD7CD #A3B18A #588157 #3A5A40 #344E41
#386641 #6A994E #A7C957 #F2E8CF #BC4749
#CAD2C5 #84A98C #52796F #354F52 #2F3E46
#F0EAD2 #DDE5B6 #ADC178 #A98467 #6C584C
#132A13 #31572C #4F772D #90A955 #ECF39E
#6B9080 #A4C3B2 #CCE3DE #EAF4F4 #F6FFF8
#233D4D #FE7F2D #FCCA46 #A1C181 #619B8A
```
@@ -0,0 +1,14 @@
# Neon / Electric
Gaming, tech, nightlife, Gen Z content.
```
#F72585 #B5179E #7209B7 #560BAD #3A0CA3
#70D6FF #FF70A6 #FF9770 #FFD670 #E9FF70
#7400B8 #6930C3 #5E60CE #5390D9 #48BFE3
#0B132B #1C2541 #3A506B #5BC0BE #6FFFE9
#540D6E #EE4266 #FFD23F #3BCEAC #0EAD69
#2D00F7 #6A00F4 #8900F2 #A100F2 #F20089
#FF6D00 #FF7900 #FF8500 #FF9100 #240046
#BBFBFF #8DD8FF #4E71FF #5409DA
```
@@ -0,0 +1,14 @@
# Pastel / Soft
Fashion, beauty, lifestyle, wellness content.
```
#CDB4DB #FFC8DD #FFAFCC #BDE0FE #A2D2FF
#CCD5AE #E9EDC9 #FEFAE0 #FAEDCD #D4A373
#FFD6FF #E7C6FF #C8B6FF #B8C0FF #BBD0FF
#FFA69E #FAF3DD #B8F2E6 #AED9E0 #5E6472
#EDAFB8 #F7E1D7 #DEDBD2 #B0C4B1 #4A5759
#555B6E #89B0AE #BEE3DB #FAF9F9 #FFD6BA
#006D77 #83C5BE #EDF6F9 #FFDDD2 #E29578
#0081A7 #00AFB9 #FDFCDC #FED9B7 #F07167
```
@@ -0,0 +1,14 @@
# Warm / Editorial
Storytelling, documentaries, case studies, narrative content.
```
#264653 #2A9D8F #E9C46A #F4A261 #E76F51
#335C67 #FFF3B0 #E09F3E #9E2A2B #540B0E
#F4F1DE #E07A5F #3D405B #81B29A #F2CC8F
#F6BD60 #F7EDE2 #F5CAC3 #84A59D #F28482
#003049 #D62828 #F77F00 #FCBF49 #EAE2B7
#588B8B #FFFFFF #FFD5C2 #F28F3B #C8553D
#283D3B #197278 #EDDDD4 #C44536 #772E25
#0D3B66 #FAF0CA #F4D35E #EE964B #F95738
```
+118
View File
@@ -0,0 +1,118 @@
# 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.
```html
<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>
```
```js
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);
```
## Title Card with Fade
```html
<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>
```
```js
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`.
```html
<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
```html
<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>
```
@@ -0,0 +1,76 @@
# Audio-Reactive Animation
Drive visuals from music, voice, or sound. Any GSAP-animatable property can respond to pre-extracted audio data.
## Audio Data Format
```js
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:
```js
// ✅ 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.py` from the gsap skill's scripts/)
- No `Math.random()` or `Date.now()`
- Audio reactivity runs on the same GSAP timeline as everything else
@@ -0,0 +1,132 @@
# 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
```json
[
{ "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](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](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()`:
```js
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:
```js
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 kill
```
Self-lint after building timeline — place **before** `window.__timelines[id] = tl` so it runs at composition init:
```js
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);
```
## Further References
- [dynamic-techniques.md](dynamic-techniques.md) — karaoke, clip-path reveals, slam words, scatter exits, elastic, 3D rotation
- [transcript-guide.md](transcript-guide.md) — transcription commands, whisper models, external APIs
- [css-patterns.md](css-patterns.md) — CSS+GSAP marker highlighting (deterministic, fully seekable)
## Constraints
- Deterministic. No `Math.random()`, no `Date.now()`.
- Sync to transcript timestamps.
- One group visible at a time.
- Every group must have a hard `tl.set` kill at `group.end`.
- The compiler embeds supported fonts automatically — just declare `font-family` in CSS.
@@ -0,0 +1,373 @@
# 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](#1-highlight-mode) — Yellow marker sweep behind text
- [2. Circle Mode](#2-circle-mode) — Hand-drawn ellipse around text
- [3. Burst Mode](#3-burst-mode) — Radiating lines from text
- [4. Scribble Mode](#4-scribble-mode) — Chaotic scribble over text
- [5. Sketchout Mode](#5-sketchout-mode) — Rough rectangle outline
## 1. Highlight Mode
Yellow marker sweep behind text. The most common mode.
```html
<span class="mh-highlight-wrap">
<span class="mh-highlight-bar" id="hl-1"></span>
<span class="mh-highlight-text">highlighted text</span>
</span>
```
```css
.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;
}
```
```js
// 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:
```js
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.
```html
<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>
```
```css
.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;
}
```
```js
// 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
```css
/* 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.
```html
<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>
```
```css
.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;
}
```
```js
// 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`.
```html
<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>
```
```css
.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;
}
```
```js
// 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.
```html
<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>
```
```css
.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);
}
```
```js
// 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:
```js
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.
@@ -0,0 +1,90 @@
# 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
| Energy level | Highlight | Exit | Cycle pattern |
| ------------ | ------------------------------------- | ------------------- | ----------------------------------------- |
| High | Karaoke with accent glow + scale pop | Scatter or drop | Alternate highlight styles every 2 groups |
| Medium-high | Karaoke with color pop | Scatter or collapse | Alternate every 3 groups |
| Medium | Karaoke (subtle, white only) | Fade + slide | Alternate every 3 groups |
| Medium-low | Karaoke (minimal scale change) | Fade | Single style, vary ease per group |
| Low | Karaoke (warm tones, slow transition) | Collapse | Alternate every 4 groups |
**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.
```js
// 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:
```bash
python3 skills/gsap-effects/scripts/extract-audio-data.py audio.mp3 --fps 30 --bands 8 -o audio-data.json
```
## Combining 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 |
@@ -0,0 +1,69 @@
# Motion Principles
## Guardrails
You know these rules but you violate them. Stop.
- **Don't use the same ease on every tween.** You default to `power2.out` on everything. Vary eases like you vary font weights — no more than 2 independent tweens with the same ease in a scene.
- **Don't use the same speed on everything.** You default to 0.4-0.5s for everything. The slowest scene should be 3× slower than the fastest. Vary duration deliberately.
- **Don't enter everything from the same direction.** You default to `y: 30, opacity: 0` on every element. Vary: from left, from right, from scale, opacity-only, letter-spacing.
- **Don't use the same stagger on every scene.** Each scene needs its own rhythm.
- **Don't use ambient zoom on every scene.** Pick different ambient motion per scene: slow pan, subtle rotation, scale push, color shift, or nothing. Stillness after motion is powerful.
- **Don't start at t=0.** Offset the first animation 0.1-0.3s. Zero-delay feels like a jump cut.
## What You Don't Do Without Being Told
### Easing is emotion, not technique
The transition is the verb. The easing is the adverb. A slide-in with `expo.out` = confident. With `sine.inOut` = dreamy. With `elastic.out` = playful. Same motion, different meaning. Choose the adverb deliberately.
**Direction rules — these are not optional:**
- `.out` for elements entering. Starts fast, decelerates. Feels responsive. This is your default.
- `.in` for elements leaving. Starts slow, accelerates away. Throws them off.
- `.inOut` for elements moving between positions.
You get this backwards constantly. Ease-in for entrances feels sluggish. Ease-out for exits feels reluctant.
### Speed communicates weight
- Fast (0.15-0.3s) — energy, urgency, confidence
- Medium (0.3-0.5s) — professional, most content
- Slow (0.5-0.8s) — gravity, luxury, contemplation
- Very slow (0.8-2.0s) — cinematic, emotional, atmospheric
### Scene structure: build / breathe / resolve
Every scene has three phases. You dump everything in the build and leave nothing for breathe or resolve.
- **Build (0-30%)** — elements enter, staggered. Don't dump everything at once.
- **Breathe (30-70%)** — content visible, alive with ONE ambient motion.
- **Resolve (70-100%)** — exit or decisive end. Exits are faster than entrances.
### Transitions are meaning
- **Crossfade** = "this continues"
- **Hard cut** = "wake up" / disruption
- **Slow dissolve** = "drift with me"
You crossfade everything. Use hard cuts for disruption and register shifts.
### 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 completion — overlap entries. Total stagger sequence under 500ms regardless of item count.
### Asymmetry
Entrances need longer than exits. A card takes 0.4s to appear but 0.25s to disappear.
## Visual Composition
You build for the web. Video frames are not pages.
- **Two focal points minimum per scene.** The eye needs somewhere to travel. Never a single text block floating in empty space.
- **Fill the frame.** Hero text: 60-80% of width. You will try to use web-sized elements. Don't.
- **Three layers minimum per scene.** Background treatment (glow, oversized faded type, color panel). Foreground content. Accent elements (dividers, labels, data bars).
- **Background is not empty.** Radial glows, oversized faded type bleeding off-frame, subtle border panels, hairline rules. Pure solid #000 reads as "nothing loaded."
- **Anchor to edges.** Pin content to left/top or right/bottom. Centered-and-floating is a web pattern.
- **Split frames.** Data panel on the left, content on the right. Top bar with metadata, full-width below. Zone-based layouts, not centered stacks.
- **Use structural elements.** Rules, dividers, border panels. They create paths for the eye and animate well (scaleX from 0).
@@ -0,0 +1,151 @@
# Transcript Guide
## How Transcripts Are Generated
`hyperframes transcribe` handles both transcription and format conversion:
```bash
# Transcribe audio/video (uses whisper.cpp locally, no API key needed)
npx hyperframes transcribe audio.mp3
# Use a larger model for better accuracy
npx hyperframes transcribe audio.mp3 --model medium.en
# Filter to English only (skips non-English speech)
npx hyperframes transcribe audio.mp3 --language en
# Import an existing transcript from another tool
npx hyperframes transcribe captions.srt
npx hyperframes transcribe captions.vtt
npx hyperframes transcribe openai-response.json
```
## Supported Input Formats
The CLI auto-detects and normalizes these formats:
| Format | Extension | Source | Word-level? |
| --------------------- | --------- | --------------------------------------------------------------------------- | ----------------- |
| whisper.cpp JSON | `.json` | `hyperframes init --video`, `hyperframes transcribe` | Yes |
| OpenAI Whisper API | `.json` | `openai.audio.transcriptions.create({ timestamp_granularities: ["word"] })` | Yes |
| SRT subtitles | `.srt` | Video editors, subtitle tools, YouTube | No (phrase-level) |
| VTT subtitles | `.vtt` | Web players, YouTube, transcription services | No (phrase-level) |
| Normalized word array | `.json` | Pre-processed by any tool | Yes |
**Word-level timestamps produce better captions.** SRT/VTT give phrase-level timing, which works but can't do per-word animation effects.
## Whisper Model Guide
The default model (`small.en`) balances accuracy and speed. For better results, use a larger model:
| Model | Size | Speed | Accuracy | When to use |
| ---------- | ------ | -------- | --------- | ------------------------------------- |
| `tiny` | 75 MB | Fastest | Low | Quick previews, testing pipeline |
| `base` | 142 MB | Fast | Fair | Short clips, clear audio |
| `small` | 466 MB | Moderate | Good | **Default** — good for most content |
| `medium` | 1.5 GB | Slow | Very good | Important content, noisy audio, music |
| `large-v3` | 3.1 GB | Slowest | Best | Production quality |
**Only add `.en` suffix when the user explicitly says the audio is English.** `.en` models are slightly more accurate for English but will TRANSLATE non-English audio instead of transcribing it.
**Critical: `.en` models translate non-English audio into English** — they don't transcribe it. If the audio might not be English, always use a model without the `.en` suffix and pass `--language` to specify the source language. If you're unsure of the language, use `small` (not `small.en`) without `--language` — whisper will auto-detect.
```bash
# Spanish audio
npx hyperframes transcribe audio.mp3 --model small --language es
# Unknown language — let whisper auto-detect
npx hyperframes transcribe audio.mp3 --model small
```
**Music and vocals over instrumentation**: `small.en` will misidentify lyrics — use `medium.en` as the minimum, or import lyrics manually. Even `medium.en` struggles with heavily produced tracks; for music videos, providing known lyrics as an SRT/VTT and importing with `hyperframes transcribe lyrics.srt` will always beat automated transcription.
## Transcript Quality Check (Mandatory)
After every transcription, **read the transcript and check for quality issues before proceeding.** Bad transcripts produce nonsensical captions. Never skip this step.
### What to look for
| Signal | Example | Cause |
| ---------------------------- | -------------------------------------- | ---------------------------------------------------------------------------- |
| Music note tokens (`♪`, ``) | `{ "text": "♪" }` or `{ "text": "" }` | Whisper detected music, not speech |
| Garbled / nonsense words | "Do a chin", "Get so gay", "huh" | Model misheard lyrics or background noise |
| Long gaps with no words | 20+ seconds of only `♪` tokens | Instrumental section — expected, but high ratio means speech is being missed |
| Repeated filler | Many "huh", "uh", "oh" entries | Model is hallucinating on music |
| Very short word spans | Words with `end - start < 0.05` | Unreliable timestamp alignment |
### Automatic retry rules
**If more than 20% of entries are `♪`/`` tokens, or the transcript contains obvious nonsense words, the transcription failed.** Do not proceed with the bad transcript. Instead:
1. **Retry with `medium.en`** if the original used `small.en` or smaller:
```bash
npx hyperframes transcribe audio.mp3 --model medium.en
```
2. **If `medium.en` also fails** (still >20% music tokens or garbled), tell the user the audio is too noisy for local transcription and suggest:
- Providing lyrics manually as an SRT/VTT file
- Using an external API (OpenAI or Groq Whisper — see below)
3. **Always clean the transcript** before building captions — filter out ``/`` tokens and entries where `text` is a single non-word character. Only real words should reach the caption composition.
### Cleaning a transcript
After transcription (even with a good model), strip non-word entries:
```js
var raw = JSON.parse(transcriptJson);
var words = raw.filter(function (w) {
if (!w.text || w.text.trim().length === 0) return false;
if (/^[♪\u266a\u266b\u266c\u266d\u266e\u266f]+$/.test(w.text)) return false;
if (/^(huh|uh|um|ah|oh)$/i.test(w.text) && w.end - w.start < 0.1) return false;
return true;
});
```
### When to use which model (decision tree)
1. **Is this speech over silence/light background?** → `small.en` is fine
2. **Is this speech over music, or music with vocals?** → Start with `medium.en`
3. **Is this a produced music track (vocals + full instrumentation)?** → Start with `medium.en`, expect to need manual lyrics or an external API
4. **Is this multilingual?** → Use `medium` or `large-v3` (no `.en` suffix)
## Using External Transcription APIs
For the best accuracy, use an external API and import the result:
**OpenAI Whisper API** (recommended for quality):
```bash
# Generate with word timestamps, then import
curl https://api.openai.com/v1/audio/transcriptions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F file=@audio.mp3 -F model=whisper-1 \
-F response_format=verbose_json \
-F "timestamp_granularities[]=word" \
-o transcript-openai.json
npx hyperframes transcribe transcript-openai.json
```
**Groq Whisper API** (fast, free tier available):
```bash
curl https://api.groq.com/openai/v1/audio/transcriptions \
-H "Authorization: Bearer $GROQ_API_KEY" \
-F file=@audio.mp3 -F model=whisper-large-v3 \
-F response_format=verbose_json \
-F "timestamp_granularities[]=word" \
-o transcript-groq.json
npx hyperframes transcribe transcript-groq.json
```
## If No Transcript Exists
1. Check the project root for `transcript.json`, `.srt`, or `.vtt` files
2. If none found, run transcription — pick the starting model based on the content type:
- Speech/voiceover → `small.en`
- Music with vocals → `medium.en`
```bash
npx hyperframes transcribe <audio-or-video-file> --model medium.en
```
3. **Read the transcript and run the quality check** (see above). If it fails, retry with a larger model or suggest manual lyrics.
@@ -0,0 +1,112 @@
# Scene Transitions
A transition tells the viewer how two scenes relate. A crossfade says "this continues." A push slide says "next point." A blur crossfade says "drift with me." Choose transitions that match what the content is doing emotionally, not just technically.
## Animation Rules for Multi-Scene Compositions
These are non-negotiable for every multi-scene composition:
1. **Every composition uses transitions.** No exceptions. Scenes without transitions feel like jump cuts.
2. **Every scene uses entrance animations.** Elements animate IN via `gsap.from()` — opacity, position, scale, etc. No scene should pop fully-formed onto screen.
3. **Exit animations are BANNED** except on the final scene. Do NOT use `gsap.to()` to animate elements out before a transition fires. The transition IS the exit. Outgoing scene content must be fully visible when the transition starts — the transition handles the visual handoff.
4. **Final scene exception:** The last scene MAY fade elements out (e.g., fade to black at the end of the composition). This is the only scene where exit animations are allowed.
## Energy → Primary Transition
| Energy | CSS Primary | Shader Primary | Accent | Duration | Easing |
| ---------------------------------------- | ---------------------------- | ------------------------------------ | ------------------------------ | --------- | ---------------------- |
| **Calm** (wellness, brand story, luxury) | Blur crossfade, focus pull | Cross-warp morph, thermal distortion | Light leak, circle iris | 0.5-0.8s | `sine.inOut`, `power1` |
| **Medium** (corporate, SaaS, explainer) | Push slide, staggered blocks | Whip pan, cinematic zoom | Squeeze, vertical push | 0.3-0.5s | `power2`, `power3` |
| **High** (promos, sports, music, launch) | Zoom through, overexposure | Ridged burn, glitch, chromatic split | Staggered blocks, gravity drop | 0.15-0.3s | `power4`, `expo` |
Pick ONE primary (60-70% of scene changes) + 1-2 accents. Never use a different transition for every scene.
## Mood → Transition Type
Think about what the transition _communicates_, not just what it looks like.
| Mood | Transitions | Why it works |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| **Warm / inviting** | Light leak, blur crossfade, focus pull, film burn · **Shader:** thermal distortion, light leak, cross-warp morph | Soft edges, warm color washes. Nothing sharp or mechanical. |
| **Cold / clinical** | Squeeze, zoom out, blinds, shutter, grid dissolve · **Shader:** gravitational lens | Content transforms mechanically — compressed, shrunk, sliced, gridded. |
| **Editorial / magazine** | Push slide, vertical push, diagonal split, shutter · **Shader:** whip pan | Like turning a page or slicing a layout. Clean directional movement. |
| **Tech / futuristic** | Grid dissolve, staggered blocks, blinds, chromatic aberration · **Shader:** glitch, chromatic split | Grid dissolve is the core "data" transition. Shader glitch adds posterization + scan lines. |
| **Tense / edgy** | Glitch, VHS, chromatic aberration, ripple · **Shader:** ridged burn, glitch, domain warp | Instability, distortion, digital breakdown. Ridged burn adds sharp lightning-crack edges. |
| **Playful / fun** | Elastic push, 3D flip, circle iris, morph circle, clock wipe · **Shader:** ripple waves, swirl vortex | Overshoot, bounce, rotation, expansion. Swirl vortex adds organic spiral distortion. |
| **Dramatic / cinematic** | Zoom through, zoom out, gravity drop, overexposure, color dip to black · **Shader:** cinematic zoom, gravitational lens, domain warp | Scale, weight, light extremes. Shader transitions add per-pixel depth. |
| **Premium / luxury** | Focus pull, blur crossfade, color dip to black · **Shader:** cross-warp morph, thermal distortion | Restraint. Cross-warp morph flows both scenes into each other organically. |
| **Retro / analog** | Film burn, light leak, VHS, clock wipe · **Shader:** light leak | Organic imperfection. Warm color bleeds, scan line displacement. |
## Narrative Position
| Position | Use | Why |
| -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |
| **Opening** | Your most distinctive transition. Match the mood. 0.4-0.6s | Sets the visual language for the entire piece. |
| **Between related points** | Your primary transition. Consistent. 0.3s | Don't distract — the content is continuing. |
| **Topic change** | Something different from your primary. Staggered blocks, shutter, squeeze. | Signals "new section" — the viewer's brain resets. |
| **Climax / hero reveal** | Your boldest accent. Fastest or most dramatic. | This is the payoff — spend your best transition here. |
| **Wind-down** | Return to gentle. Blur crossfade, crossfade. 0.5-0.7s | Let the viewer exhale after the climax. |
| **Outro** | Slowest, simplest. Crossfade, color dip to black. 0.6-1.0s | Closure. Don't introduce new energy at the end. |
## Blur Intensity by Energy
| Energy | Blur | Duration | Hold at peak |
| ---------- | ------- | -------- | ------------ |
| **Calm** | 20-30px | 0.8-1.2s | 0.3-0.5s |
| **Medium** | 8-15px | 0.4-0.6s | 0.1-0.2s |
| **High** | 3-6px | 0.2-0.3s | 0s |
## Presets
| Preset | Duration | Easing |
| ---------- | -------- | ----------------- |
| `snappy` | 0.2s | `power4.inOut` |
| `smooth` | 0.4s | `power2.inOut` |
| `gentle` | 0.6s | `sine.inOut` |
| `dramatic` | 0.5s | `power3.in` → out |
| `instant` | 0.15s | `expo.inOut` |
| `luxe` | 0.7s | `power1.inOut` |
## Implementation
Read [transitions/catalog.md](transitions/catalog.md) for GSAP code and hard rules for every transition type.
| Category | CSS | Shader (WebGL) |
| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Push/slide | Push slide, vertical push, elastic push, squeeze | Whip pan |
| Scale/zoom | Zoom through, zoom out, gravity drop, 3D flip | Cinematic zoom, gravitational lens |
| Reveal/mask | Circle iris, diamond iris, diagonal split, clock wipe, shutter | SDF iris |
| Dissolve | Crossfade, blur crossfade, focus pull, color dip | Cross-warp morph, domain warp |
| Cover | Staggered blocks, horizontal blinds, vertical blinds | — |
| Light | Light leak, overexposure burn, film burn | Light leak (shader), thermal distortion |
| Distortion | Glitch, chromatic aberration, ripple, VHS tape | Glitch (shader), chromatic split, ridged burn, ripple waves, swirl vortex |
| Pattern | Grid dissolve, morph circle | — |
## Transitions That Don't Work in CSS
Avoid: star iris, tilt-shift, lens flare, hinge/door. See catalog.md for why.
## CSS vs Shader
CSS transitions animate scene containers with opacity, transforms, clip-path, and filters. Shader transitions composite both scene textures per-pixel on a WebGL canvas — they can warp, dissolve, and morph in ways CSS cannot.
**Both are first-class options.** Shaders are provided by the `@hyperframes/shader-transitions` package — import from the package instead of writing raw GLSL. CSS transitions are simpler to set up. Choose based on the effect you want, not based on which is easier.
When a composition uses shader transitions, ALL transitions in that composition should be shader-based (the WebGL canvas replaces DOM-based scene switching). Don't mix CSS and shader transitions in the same composition.
## Shader-Compatible CSS Rules
Shader transitions capture DOM scenes to WebGL textures via html2canvas. The canvas 2D rendering pipeline doesn't match CSS exactly. Follow these rules to avoid visible artifacts at transition boundaries:
1. **No `transparent` keyword in gradients.** Canvas interpolates `transparent` as `rgba(0,0,0,0)` (black at zero alpha), creating dark fringes. Always use the target color at zero alpha: `rgba(200,117,51,0)` not `transparent`.
2. **No gradient backgrounds on elements thinner than 4px.** Canvas can't match CSS gradient rendering on 1-2px elements. Use solid `background-color` on thin accent lines.
3. **No CSS variables (`var()`) on elements visible during capture.** html2canvas doesn't reliably resolve custom properties. Use literal color values in inline styles.
4. **Mark uncapturable decorative elements with `data-no-capture`.** The capture function skips these. They're present on the live DOM but absent from the shader texture. Use for elements that can't follow the rules above.
5. **No gradient opacity below 0.15.** Gradient elements below 10% opacity render differently in canvas vs CSS. Increase to 0.15+ or use a solid color at equivalent brightness.
6. **Every `.scene` div must have explicit `background-color`, AND pass the same color as `bgColor` in the `init()` config.** The package captures scene elements via html2canvas. Both the CSS `background-color` on `.scene` and the `bgColor` config must match. Without either, the texture renders as black.
These rules only apply to shader transition compositions. CSS-only compositions have no restrictions.
## Visual Pattern Warning
Avoid transitions that create visible repeating geometric patterns — grids of tiles, hexagonal cells, uniform dot arrays, evenly-spaced blob circles. These look cheap and artificial regardless of the math behind them. Organic noise (FBM, domain warping) is good because it's irregular. Geometric repetition is bad because the eye instantly sees the grid.
@@ -0,0 +1,117 @@
# Transition Catalog
Hard rules, scene template, and routing to implementation code. Read the reference file for the transition type you need — don't load all of them.
## Hard Rules (CSS)
These cause real bugs if violated.
**Scene visibility:** Scene 1 visible by default (no `opacity: 0`). Scenes 2+ have `opacity: 0` on the CONTAINER div. GSAP reveals them. No visibility shim (`timedEls`).
**Fonts:** Just write the `font-family` you want — the compiler embeds supported fonts automatically via `@font-face` with inline data URIs. No need for `<link>` tags or `@import`. Works in all contexts including sandboxed iframes.
**Element structure:** No `class="clip"` on scene divs in standalone compositions. Only the root div gets `data-composition-id`/`data-start`/`data-duration`.
**Overlay elements:** Staggered blocks = full-screen 1920x1080, NOT thin strips. Glitch RGB overlays = normal blending at 35% opacity, NOT `mix-blend-mode: multiply` (invisible on dark backgrounds). Light leak overlays = larger than the frame (2400px+), never a visible shape. Overexposure = use `filter: brightness()` on the scene, not just a white overlay.
**VHS tape:** Clone actual scene content with `cloneNode(true)`, NOT colored bars. Each strip: wider than frame (2020px at left:-50px). Red+blue chromatic copies at z-index above main strip. Seeded PRNG for deterministic random offsets.
**Z-index:** Gravity drop, zoom out, diagonal split need outgoing scene ON TOP (`zIndex: 10`) so it exits while revealing the new scene behind (`zIndex: 1`).
**Page burn:** Content burns with the page — no falling debris. Hide scene1 via `tl.set` at burn end, NEVER `onComplete` (not reversible). `onUpdate` must restore `clipPath: "none"` when `wp <= 0` for rewind support. Incoming scene fades from black at 90% through burn.
**Clock wipe:** 9-point polygon with intermediate edge positions. Step through 4 quadrants with separate tweens.
**Grid dissolve:** Cycle 5 palette colors per cell, not monochrome.
**Blinds count by energy:** Calm: 4h/6v. Medium: 6-8h/8v. High: 12-16h/16v.
**Don't use:** Star iris (polygon interpolation broken), tilt-shift (no selective CSS blur), lens flare (visible shape, not optical), hinge/door (distorts too fast).
## Shader Transitions
Shader setup, WebGL init, capture, and fragment shaders are handled by `@hyperframes/shader-transitions` (`packages/shader-transitions/`). Read the package source for API details. Compositions using shaders must follow the CSS rules in [transitions.md](../transitions.md) § "Shader-Compatible CSS Rules".
## Scene Template
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
font-family: "YOUR FONT", sans-serif; /* compiler embeds supported fonts automatically */
}
.scene {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#scene1 {
z-index: 1;
background: #color;
}
#scene2 {
z-index: 2;
background: #color;
opacity: 0;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="TOTAL"
>
<div id="scene1" class="scene"><!-- visible --></div>
<div id="scene2" class="scene"><!-- hidden --></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Transition code here
window.__timelines["main"] = tl;
</script>
</body>
</html>
```
Every transition follows: position new scene → animate outgoing → swap → animate incoming → clean up overlays.
## CSS Transitions
All code examples use `old` for the outgoing scene-inner selector and `new` for the incoming, with `T` as the transition start time. Read the reference file for the type you need.
| Type | Transitions | Reference |
| -------------- | ---------------------------------------------------- | ------------------------------------------ |
| Push | Push slide, vertical push, elastic push, squeeze | [css-push.md](./css-push.md) |
| Radial / Shape | Circle iris, diamond iris, diagonal split | [css-radial.md](./css-radial.md) |
| 3D | 3D card flip | [css-3d.md](./css-3d.md) |
| Scale / Zoom | Zoom through, zoom out | [css-scale.md](./css-scale.md) |
| Dissolve | Crossfade, blur crossfade, focus pull, color dip | [css-dissolve.md](./css-dissolve.md) |
| Cover | Staggered blocks, horizontal blinds, vertical blinds | [css-cover.md](./css-cover.md) |
| Light | Light leak, overexposure burn, film burn | [css-light.md](./css-light.md) |
| Distortion | Glitch, chromatic aberration, ripple, VHS tape | [css-distortion.md](./css-distortion.md) |
| Mechanical | Shutter, clock wipe | [css-mechanical.md](./css-mechanical.md) |
| Grid | Grid dissolve | [css-grid.md](./css-grid.md) |
| Other | Gravity drop, morph circle | [css-other.md](./css-other.md) |
| Blur | Blur through, directional blur | [css-blur.md](./css-blur.md) |
| Destruction | Page burn | [css-destruction.md](./css-destruction.md) |
## Shader Transitions
WebGL shader transitions are provided by `@hyperframes/shader-transitions` (`packages/shader-transitions/`). The package handles setup, capture, WebGL init, render loop, and GSAP integration. Read the package source for available shaders and API — do not copy raw GLSL manually.
@@ -0,0 +1,12 @@
## 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`.
```js
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);
```
@@ -0,0 +1,51 @@
## Blur
All blur transitions scale with energy. See SKILL.md "Blur Intensity by Energy" for the full table.
### Blur Through
Content becomes fully abstract before resolving. The heaviest blur transition.
**Calm (default for this type — it's inherently heavy):**
```js
tl.to(old, { filter: "blur(30px)", scale: 1.08, duration: 0.5, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.3, ease: "power1.in" }, T + 0.3);
// Hold: both scenes in abstract blur state
tl.fromTo(new,
{ filter: "blur(30px)", scale: 0.92, opacity: 0 },
{ filter: "blur(30px)", scale: 0.92, opacity: 1, duration: 0.2, ease: "none" }, T + 0.5);
// Slow resolve
tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.7, ease: "power1.out" }, T + 0.7);
```
**Medium:**
```js
tl.to(old, { filter: "blur(15px)", scale: 1.05, opacity: 0, duration: 0.4, ease: "power2.in" }, T);
tl.fromTo(new,
{ filter: "blur(15px)", scale: 0.95, opacity: 0 },
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.4, ease: "power2.out" }, T + 0.2);
```
### Directional Blur
Blur + skew simulating motion in one direction. Scale blur and skew with energy.
**Medium (default):**
```js
tl.to(old, { filter: "blur(12px)", skewX: -8, x: -200, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
tl.fromTo(new,
{ filter: "blur(12px)", skewX: 8, x: 200, opacity: 0 },
{ filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.4, ease: "power3.out" }, T + 0.15);
```
**Calm (heavier blur, gentler motion):**
```js
tl.to(old, { filter: "blur(20px)", skewX: -4, x: -100, opacity: 0, duration: 0.6, ease: "power1.in" }, T);
tl.fromTo(new,
{ filter: "blur(20px)", skewX: 4, x: 100, opacity: 0 },
{ filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.6, ease: "power1.out" }, T + 0.3);
```
@@ -0,0 +1,43 @@
## Cover
### Staggered Color Blocks
Full-screen (1920x1080) colored divs slide across staggered. Scene swaps while covered.
**2-block** (standard):
```js
tl.set("#wipe-a", { x: -1920 }, T - 0.01);
tl.set("#wipe-b", { x: -1920 }, T - 0.01);
tl.to("#wipe-a", { x: 0, duration: 0.25, ease: "power3.inOut" }, T);
tl.to("#wipe-b", { x: 0, duration: 0.25, ease: "power3.inOut" }, T + 0.06);
tl.set(old, { opacity: 0 }, T + 0.2);
tl.set(new, { opacity: 1 }, T + 0.2);
tl.to("#wipe-a", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.28);
tl.to("#wipe-b", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.34);
```
**5-block** (dense variant): same pattern with 5 blocks at 0.04s stagger. Use composition palette colors.
### Horizontal Blinds
Full-width strips slide across staggered. Each strip: `width: 1920px; height: Xpx`.
**6 strips** (180px each): `0.03s` stagger
**12 strips** (90px each): `0.018s` stagger
```js
for (var i = 0; i < N; i++) {
tl.set("#blind-h-" + i, { x: -1920 }, T - 0.01);
tl.fromTo("#blind-h-" + i, { x: -1920 }, { x: 0, duration: 0.2, ease: "power3.inOut" }, T + i * stagger);
}
tl.set(old, { opacity: 0 }, T + coverTime);
tl.set(new, { opacity: 1 }, T + coverTime);
for (var i = 0; i < N; i++) {
tl.to("#blind-h-" + i, { x: 1920, duration: 0.2, ease: "power3.inOut" }, T + exitStart + i * stagger);
}
```
### Vertical Blinds
Same as horizontal but strips are tall and narrow, moving on Y axis.
@@ -0,0 +1,95 @@
## Destruction
### Page Burn
The outgoing scene literally burns away from a corner. A fire front expands with noise-based irregular edges, a canvas draws the scorched char line at the burn boundary, and individual text characters/elements chip off and fall with gravity as the fire reaches them. The incoming scene reveals behind the burn.
This transition has three systems working together:
1. **Fire geometry** — a radial front expanding from a corner (e.g., bottom-right) with noise-based irregularity for organic edges
2. **Scene clipping** — the outgoing scene uses an SVG clip-path (with `fill-rule: evenodd`) that cuts a hole matching the fire front. As the fire expands, more of the scene is clipped away. All content (text, images, lines) burns with the page — no separate debris.
3. **Scorched edge** — a `<canvas>` overlay draws a radial gradient fringe at the fire boundary to simulate charring
**When to use:** Dramatic reveals, edgy/destructive mood, gaming, cyberpunk. This is the most dramatic transition in the catalog — reserve it for hero moments.
**Requirements:**
- A `<canvas>` element for the burn edge overlay
- A noise function for organic fire edge geometry
- SVG clip-path with evenodd fill-rule for the inverted clip
**Fire geometry (deterministic noise):**
```js
function noise(x) {
var ix = Math.floor(x),
fx = x - ix;
var a = Math.sin(ix * 127.1 + 311.7) * 43758.5453;
var b = Math.sin((ix + 1) * 127.1 + 311.7) * 43758.5453;
var t = fx * fx * (3 - 2 * fx);
return a - Math.floor(a) + (b - Math.floor(b) - (a - Math.floor(a))) * t;
}
function fireRadiusAtAngle(angle, progress) {
var base = progress * maxRadius;
return (
base +
noise(angle * 3 + progress * 4) * 50 +
noise(angle * 8 + progress * 9) * 20 +
noise(angle * 15 + progress * 15) * 8
);
}
```
**Incoming scene timing:** The incoming scene should NOT be visible during the burn. As the fire consumes the outgoing scene, **black shows through the holes** — this is the dramatic part. The viewer watches content being destroyed against blackness.
At ~90% through the burn, the incoming scene fades in SLOWLY from black — the background first, then content staggered. Use long, gentle fades (`power1.out`, 0.8-1.2s durations) so it feels like the new scene materializes from darkness, not a hard swap.
```js
// Scene 2 stays at opacity: 0 during the burn — black behind the fire
tl.set("#s2-title", { opacity: 0 }, T);
tl.set("#s2-subtitle", { opacity: 0 }, T);
// At 90% through, scene bg fades in slowly from black
var contentReveal = T + BURN_DURATION * 0.9;
tl.to("#scene2", { opacity: 1, duration: 1.2, ease: "power1.out" }, contentReveal);
// Content fades in staggered on top, even slower
tl.to("#s2-title", { opacity: 1, duration: 1.0, ease: "power1.out" }, contentReveal + 0.5);
tl.to("#s2-subtitle", { opacity: 1, duration: 0.8, ease: "power1.out" }, contentReveal + 0.7);
```
**Content burns with the page — no falling debris.** The clip-path on scene1 IS the effect — as the fire shape expands, everything behind the fire edge (text, images, lines) disappears naturally. Don't clone elements, don't create falling debris. The content is part of the page being consumed. The scorched canvas edge provides the visual char line at the burn boundary.
**Hide scene1 via `tl.set` at burn end — NEVER in `onComplete`.** Using `onComplete` to hide scene1 is not reversible when scrubbing. Instead, use a `tl.set` at the exact burn end time:
```js
tl.to(
burnState,
{
progress: 1,
duration: BURN_DURATION,
ease: "none",
onUpdate: function () {
var wp = burnState.progress;
var scene1 = document.getElementById("scene1");
if (wp <= 0) {
scene1.style.clipPath = "none"; // fully visible when rewound
} else if (wp < 1) {
scene1.style.clipPath = buildClipPath(wp);
}
drawEdge(wp);
},
// NO onComplete — use tl.set instead
},
T,
);
// Hide scene1 at exact burn end — reversible via timeline
tl.set("#scene1", { opacity: 0 }, T + BURN_DURATION);
tl.set("#scene1", { clipPath: "none" }, T + BURN_DURATION);
```
The `onUpdate` handles clip-path and canvas edge per-frame. The `tl.set` handles the final hide — and GSAP automatically reverses it when scrubbing backward, restoring scene1 to `opacity: 1`.
The `onUpdate` callback is the key — it runs every frame to advance the clip-path and canvas edge in sync with the timeline.
@@ -0,0 +1,66 @@
## Dissolve
### Crossfade
Simple opacity swap. The baseline.
```js
tl.to(old, { opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, T);
```
### Blur Crossfade
Dissolve with blur + scale shift. **Scale blur amount by energy** — see SKILL.md "Blur Intensity by Energy" section. The examples below show the medium (default) version. For calm compositions, increase to 20-30px with a 0.3-0.5s hold at peak blur. For high-energy, decrease to 3-6px with no hold.
**Medium (default):**
```js
tl.to(old, { filter: "blur(10px)", scale: 1.03, opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
tl.fromTo(new,
{ filter: "blur(10px)", scale: 0.97, opacity: 0 },
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.5, ease: "power2.inOut" }, T + 0.1);
```
**Calm (wellness, luxury) — heavy blur, holds at abstract color:**
```js
tl.to(old, { filter: "blur(25px)", scale: 1.05, duration: 0.6, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.4, ease: "power1.in" }, T + 0.4);
tl.fromTo(new,
{ filter: "blur(25px)", scale: 0.95, opacity: 0 },
{ filter: "blur(25px)", scale: 0.95, opacity: 1, duration: 0.3, ease: "power1.inOut" }, T + 0.5);
tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.6, ease: "power1.out" }, T + 0.8);
```
### Focus Pull
Outgoing slowly blurs while incoming fades in sharp. Depth-of-field feel. **Scale blur amount and hold duration by energy.**
**Medium:**
```js
tl.to(old, { filter: "blur(15px)", duration: 0.5, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.3, ease: "power2.in" }, T + 0.25);
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: "power2.out" }, T + 0.25);
```
**Calm — slow rack focus with long hold at peak defocus:**
```js
tl.to(old, { filter: "blur(30px)", duration: 0.8, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.5, ease: "power1.in" }, T + 0.6);
tl.fromTo(new, { opacity: 0, filter: "blur(20px)" },
{ opacity: 1, filter: "blur(20px)", duration: 0.3, ease: "power1.inOut" }, T + 0.7);
tl.to(new, { filter: "blur(0px)", duration: 0.6, ease: "power1.out" }, T + 1.0);
```
### Color Dip
Fade to solid color, hold, fade up new scene.
```js
tl.to(old, { opacity: 0, duration: 0.2, ease: "power2.in" }, T);
// Background color shows through
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.2, ease: "power2.out" }, T + 0.25);
```
@@ -0,0 +1,45 @@
## Distortion
### Glitch
RGB-tinted overlays (NOT multiply blend — use normal blending at 35% opacity) jitter with large offsets. Scene itself also jitters.
```js
tl.set("#glitch-r", { opacity: 1, x: 40, y: -8 }, T);
tl.set("#glitch-g", { opacity: 1, x: -30, y: 12 }, T);
tl.set("#glitch-b", { opacity: 1, x: 15, y: -20 }, T);
tl.set(old, { x: -15 }, T);
// 6 jitter frames at 0.03s intervals with big offsets (±30-60px)
// ... swap and clear at T + 0.2
```
### Chromatic Aberration
RGB overlays start aligned then spread apart (±80px), scene fades, converge on new scene.
```js
tl.set("#glitch-r", { opacity: 0.6, x: 0 }, T);
tl.set("#glitch-g", { opacity: 0.6, x: 0 }, T);
tl.set("#glitch-b", { opacity: 0.6, x: 0 }, T);
tl.to("#glitch-r", { x: -80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
tl.to("#glitch-b", { x: 80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
tl.to("#glitch-g", { y: 30, duration: 0.3, ease: "power2.in" }, T);
// Swap at T + 0.3, converge back at T + 0.3
```
### Ripple
Rapid oscillation (±30px) + scale distortion (0.97-1.03) + increasing blur. Swap at peak distortion.
```js
tl.to(old, { x: 30, scale: 1.02, duration: 0.04, ease: "none" }, T);
tl.to(old, { x: -25, scale: 0.98, filter: "blur(4px)", duration: 0.04, ease: "none" }, T + 0.04);
// ... more oscillations with increasing blur
// Swap at peak, incoming stabilizes with decreasing wobble
```
### VHS Tape
Clone scene into 20 horizontal strips (each 54px, clip-path'd). Each strip shifts x independently with seeded pseudo-random offsets at per-bar random intervals. Add red+blue chromatic offset copies on each strip (z-index above main, 35% opacity). Make strips wider than frame (2020px at left:-50px) so edges never show.
See SKILL.md for clone-based implementation pattern.
@@ -0,0 +1,10 @@
## 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.
@@ -0,0 +1,49 @@
## Light
### Light Leak
Multiple warm-colored overlays wash across frame. Needs: a flat warm tint layer + 2-3 bright radial gradient divs, all larger than the frame so edges are never visible.
```js
// Warm tint washes over entire frame
tl.to("#leak-warm", { opacity: 0.4, duration: 0.3, ease: "power1.in" }, T);
// Bright leak elements drift in
tl.to("#leak-1", { opacity: 0.9, x: 300, duration: 0.5, ease: "sine.inOut" }, T + 0.05);
tl.to("#leak-2", { opacity: 0.8, x: 200, duration: 0.6, ease: "sine.inOut" }, T + 0.1);
// Peak warmth then swap
tl.to("#leak-warm", { opacity: 0.6, duration: 0.15, ease: "power2.in" }, T + 0.35);
tl.set(old, { opacity: 0 }, T + 0.45);
tl.set(new, { opacity: 1 }, T + 0.45);
// Leak fades
tl.to("#leak-warm", { opacity: 0, duration: 0.4, ease: "power2.out" }, T + 0.5);
tl.to("#leak-1", { opacity: 0, x: 600, duration: 0.35, ease: "power1.out" }, T + 0.5);
```
### Overexposure Burn
Scene progressively blows out to white using CSS `filter: brightness()`, then white overlay fades in. Swap at peak white. White recedes to reveal new scene.
```js
tl.to(old, { filter: "brightness(1.5)", scale: 1.03, duration: 0.2, ease: "power1.in" }, T);
tl.to(old, { filter: "brightness(3)", scale: 1.06, duration: 0.2, ease: "power2.in" }, T + 0.2);
tl.to("#flash-overlay", { opacity: 0.5, duration: 0.25, ease: "power1.in" }, T + 0.15);
tl.to("#flash-overlay", { opacity: 1, duration: 0.15, ease: "power2.in" }, T + 0.4);
tl.set(old, { opacity: 0, filter: "brightness(1)", scale: 1 }, T + 0.55);
tl.set(new, { opacity: 1 }, T + 0.55);
tl.to("#flash-overlay", { opacity: 0, duration: 0.35, ease: "power2.out" }, T + 0.55);
```
### Film Burn
Staggered warm overlays (amber, orange, red) bleed from one edge. Each overlay is a large radial gradient div at high z-index.
```js
tl.to("#burn-a", { opacity: 1, x: -300, duration: 0.4, ease: "power1.in" }, T);
tl.to("#burn-b", { opacity: 1, x: -500, duration: 0.5, ease: "power1.in" }, T + 0.05);
tl.to("#burn-c", { opacity: 1, x: -200, duration: 0.45, ease: "power1.in" }, T + 0.1);
tl.set(old, { opacity: 0 }, T + 0.35);
tl.set(new, { opacity: 1 }, T + 0.35);
tl.to("#burn-a", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.45);
tl.to("#burn-b", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.5);
tl.to("#burn-c", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.55);
```
@@ -0,0 +1,30 @@
## Mechanical
### Shutter
Two full-screen halves close from top and bottom, meet in the middle. Swap while closed. Open again.
```js
tl.to("#shutter-top", { y: 0, duration: 0.25, ease: "power3.in" }, T);
tl.to("#shutter-bot", { y: 0, duration: 0.25, ease: "power3.in" }, T);
tl.set(old, { opacity: 0 }, T + 0.25);
tl.set(new, { opacity: 1 }, T + 0.25);
tl.to("#shutter-top", { y: -540, duration: 0.25, ease: "power3.out" }, T + 0.3);
tl.to("#shutter-bot", { y: 540, duration: 0.25, ease: "power3.out" }, T + 0.3);
```
### Clock Wipe
Radial polygon sweep stepping through quadrants. Use 9-point polygon with intermediate edge positions for smooth sweep.
```js
tl.set(new, { opacity: 1, zIndex: 10 }, T);
var d = 0.1; // duration per quadrant
tl.set(new, { clipPath: "polygon(50% 50%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%)" }, T);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%)", duration: d, ease: "none" }, T);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%)", duration: d, ease: "none" }, T + d);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 50%)", duration: d, ease: "none" }, T + d*2);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 0%)", duration: d, ease: "none" }, T + d*3);
tl.set(new, { clipPath: "none", zIndex: "auto" }, T + d*4 + 0.02);
tl.set(old, { opacity: 0, zIndex: "auto" }, T + d*4 + 0.02);
```
@@ -0,0 +1,25 @@
## Other
### Gravity Drop
Old scene falls down with slight rotation. New scene was behind it. Needs z-index.
```js
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.
```js
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);
```
@@ -0,0 +1,41 @@
## Linear / Push
### Push Slide
Both scenes move together — new pushes old out.
```js
tl.to(old, { x: -1920, duration: 0.5, ease: "power3.inOut" }, T);
tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 0, duration: 0.5, ease: "power3.inOut" }, T);
```
### Vertical Push
Same as push slide but vertical.
```js
tl.to(old, { y: -1080, duration: 0.5, ease: "power3.inOut" }, T);
tl.fromTo(new, { y: 1080, opacity: 1 }, { y: 0, duration: 0.5, ease: "power3.inOut" }, T);
```
### Elastic Push
Push with overshoot bounce on the incoming scene.
```js
tl.to(old, { x: -1920, duration: 0.5, ease: "power3.in" }, T);
tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 30, duration: 0.4, ease: "power4.out" }, T + 0.1);
tl.to(new, { x: -15, duration: 0.15, ease: "sine.inOut" }, T + 0.5);
tl.to(new, { x: 0, duration: 0.1, ease: "sine.out" }, T + 0.65);
```
### Squeeze
Old compresses, new expands from opposite side.
```js
tl.to(old, { scaleX: 0, transformOrigin: "left center", duration: 0.4, ease: "power3.inOut" }, T);
tl.fromTo(new, { scaleX: 0, transformOrigin: "right center", opacity: 1 },
{ scaleX: 1, duration: 0.4, ease: "power3.inOut" }, T + 0.1);
tl.set(old, { opacity: 0 }, T + 0.5);
```
@@ -0,0 +1,37 @@
## Radial / Shape
### Circle Iris
Expanding circle from center reveals new scene.
```js
tl.set(new, { opacity: 1 }, T);
tl.fromTo(new,
{ clipPath: "circle(0% at 50% 50%)" },
{ clipPath: "circle(75% at 50% 50%)", duration: 0.5, ease: "power2.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.5);
```
### Diamond Iris
Expanding diamond shape from center.
```js
tl.set(new, { opacity: 1 }, T);
tl.fromTo(new,
{ clipPath: "polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)" },
{ clipPath: "polygon(50% -20%, 120% 50%, 50% 120%, -20% 50%)", duration: 0.5, ease: "power2.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.5);
```
### Diagonal Split
Old scene shrinks to a triangle in one corner.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10, clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" }, T);
tl.to(old, { clipPath: "polygon(60% 0%, 100% 0%, 100% 40%, 60% 0%)", duration: 0.5, ease: "power3.inOut" }, T);
tl.set(old, { opacity: 0, zIndex: "auto", clipPath: "none" }, T + 0.5);
tl.set(new, { zIndex: "auto" }, T + 0.5);
```
@@ -0,0 +1,24 @@
## Scale / Zoom
### Zoom Through
Old zooms past camera + blurs, new zooms in from behind.
```js
tl.to(old, { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, T);
tl.fromTo(new,
{ scale: 0.5, opacity: 0, filter: "blur(8px)" },
{ scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, T + 0.15);
```
### Zoom Out
Old shrinks away, new was behind it. Needs z-index management.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10, transformOrigin: "50% 50%" }, T);
tl.to(old, { scale: 0.3, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
tl.set(old, { zIndex: "auto" }, T + 0.4);
tl.set(new, { zIndex: "auto" }, T + 0.4);
```
@@ -0,0 +1,56 @@
# Text-to-Speech
Generate speech audio locally using Kokoro-82M (no API key, runs on CPU).
## Voice Selection
Match voice to content. Default is `af_heart`.
| Content type | Voice | Why |
| ------------- | --------------------- | -------------------------- |
| Product demo | `af_heart`/`af_nova` | Warm, professional |
| Tutorial | `am_adam`/`bf_emma` | Neutral, easy to follow |
| Marketing | `af_sky`/`am_michael` | Energetic or authoritative |
| Documentation | `bf_emma`/`bm_george` | Clear British English |
| Casual | `af_heart`/`af_sky` | Approachable, natural |
Run `npx hyperframes tts --list` for all 54 voices (8 languages).
## Speed Tuning
- **0.7-0.8** — Tutorial, complex content
- **1.0** — Natural pace (default)
- **1.1-1.2** — Intros, upbeat content
- **1.5+** — Rarely appropriate
## Usage
```bash
npx hyperframes tts "Your script here" --voice af_nova --output narration.wav
npx hyperframes tts script.txt --voice bf_emma --output narration.wav
```
In compositions:
```html
<audio
id="narration"
data-start="0"
data-duration="auto"
data-track-index="2"
src="narration.wav"
data-volume="1"
></audio>
```
## TTS + Captions Workflow
```bash
npx hyperframes tts script.txt --voice af_heart --output narration.wav
npx hyperframes transcribe narration.wav # → transcript.json with word-level timestamps
```
## Requirements
- Python 3.8+ with `kokoro-onnx` and `soundfile`
- Model downloads on first use (~311 MB + ~27 MB voices, cached in `~/.cache/hyperframes/tts/`)
@@ -0,0 +1,175 @@
# Typography
The compiler embeds supported fonts — just write `font-family` in CSS.
## Banned
Training-data defaults that every LLM reaches for. These produce monoculture across compositions.
Inter, Roboto, Open Sans, Noto Sans, Arimo, Lato, Source Sans, PT Sans, Nunito, Poppins, Outfit, Sora, Playfair Display, Cormorant Garamond, Bodoni Moda, EB Garamond, Cinzel, Prata, Syne
**Syne in particular** is the most overused "distinctive" display font. It is an instant AI design tell.
## Guardrails
You know these rules but you violate them. Stop.
- **Don't pair two sans-serifs.** You do this constantly — one for headlines, one for body. Cross the boundary: serif + sans, or sans + mono.
- **One expressive font per scene.** You pick two interesting fonts trying to make it "better." One performs, one recedes.
- **Weight contrast must be extreme.** You default to 400 vs 700. Video needs 300 vs 900. The difference must be visible in motion at a glance.
- **Video sizes, not web sizes.** Body: 20px minimum. Headlines: 60px+. Data labels: 16px. You will try to use 14px. Don't.
## What You Don't Do Without Being Told
- **Tension should mean something.** Don't pattern-match pairings. Ask WHY these two fonts disagree. The pairing should embody the content's contradiction — mechanical vs human, public vs private, institutional vs personal. If you can't articulate the tension, it's arbitrary.
- **Register switching.** Assign different fonts to different communicative modes — one voice for statements, another for data, another for attribution. Not hierarchy on a page. Voices in a conversation.
- **Tension can live inside a single font.** A font that looks familiar but is secretly strange creates tension with the viewer's expectations, not with another font.
- **One variable changed = dramatic contrast.** Same letterforms, monospaced vs proportional. Same family at different optical sizes. Changing only rhythm while everything else stays constant.
- **Double personality works.** Two expressive fonts can coexist if they share an attitude (both irreverent, both precise) even when their forms are completely different.
- **Time is hierarchy.** The first element to appear is the most important. In video, sequence replaces position.
- **Motion is typography.** How a word enters carries as much meaning as the font. A 0.1s slam vs a 2s fade — same font, completely different message.
- **Fixed reading time.** 3 seconds on screen = must be readable in 2. Fewer words, larger type.
- **Tracking tighter than web.** -0.03em to -0.05em on display sizes. Video encoding compresses letter detail.
## Finding Fonts
Don't default to what you know. If the content is luxury, a grotesque sans might create more tension than the expected Didone serif. Decide the register first, then search.
Save this script to `/tmp/fontquery.py` and run with `curl -s 'https://fonts.google.com/metadata/fonts' > /tmp/gfonts.json && python3 /tmp/fontquery.py /tmp/gfonts.json`:
```python
import json, sys, random
from collections import OrderedDict
random.seed() # true random each run
with open(sys.argv[1]) as f:
data = json.load(f)
fonts = data.get("familyMetadataList", [])
ban = {"Inter","Roboto","Open Sans","Noto Sans","Lato","Poppins","Source Sans 3",
"PT Sans","Nunito","Outfit","Sora","Playfair Display","Cormorant Garamond",
"Bodoni Moda","EB Garamond","Cinzel","Prata","Arimo","Source Sans Pro","Syne"}
skip_pfx = ("Roboto","Noto ","Google Sans","Bpmf","Playwrite","Anek","BIZ ",
"Nanum","Shippori","Sawarabi","Zen ","Kaisei","Kiwi ","Yuji ","Radio ")
def ok(f):
if f["family"] in ban: return False
if any(f["family"].startswith(b) for b in skip_pfx): return False
if "latin" not in (f.get("subsets") or []): return False
return True
seen = set()
R = OrderedDict()
# Trending Sans — recent (2022+), popular (<300)
R["Trending Sans"] = []
for f in fonts:
if not ok(f) or f["family"] in seen: continue
if f.get("category") in ("Sans Serif","Display") and f.get("dateAdded","") >= "2022-01-01" and f.get("popularity",9999) < 300:
R["Trending Sans"].append(f); seen.add(f["family"])
# Trending Serif — recent (2018+), popular (<600)
R["Trending Serif"] = []
for f in fonts:
if not ok(f) or f["family"] in seen: continue
if f.get("category") == "Serif" and f.get("dateAdded","") >= "2018-01-01" and f.get("popularity",9999) < 600:
R["Trending Serif"].append(f); seen.add(f["family"])
# Monospace — recent (2018+), popular (<600)
R["Monospace"] = []
for f in fonts:
if not ok(f) or f["family"] in seen: continue
if f.get("category") == "Monospace" and f.get("dateAdded","") >= "2018-01-01" and f.get("popularity",9999) < 600:
R["Monospace"].append(f); seen.add(f["family"])
# Impact & Condensed — heavy display fonts with 800+ weight
R["Impact & Condensed"] = []
for f in fonts:
if not ok(f) or f["family"] in seen: continue
has_heavy = any(k in list(f.get("fonts",{}).keys()) for k in ("800","900"))
is_display = f.get("category") in ("Sans Serif","Display")
if has_heavy and is_display and f.get("popularity",9999) < 400:
R["Impact & Condensed"].append(f); seen.add(f["family"])
# Script & Handwriting — popular (<300)
R["Script & Handwriting"] = []
for f in fonts:
if not ok(f) or f["family"] in seen: continue
if f.get("category") == "Handwriting" and f.get("popularity",9999) < 300:
R["Script & Handwriting"].append(f); seen.add(f["family"])
# Randomize the top 5 in each category so the LLM doesn't always pick the same first result
for cat in R:
R[cat].sort(key=lambda x: x.get("popularity",9999))
top5 = R[cat][:5]
rest = R[cat][5:]
random.shuffle(top5)
R[cat] = top5 + rest
limits = {"Trending Sans":15,"Trending Serif":12,"Monospace":8,
"Impact & Condensed":12,"Script & Handwriting":10}
for cat in R:
items = R[cat][:limits.get(cat,10)]
if not items: continue
print(f"--- {cat} ({len(items)}) ---")
for ff in items:
var = "VAR" if ff.get("axes") else " "
print(f' {ff.get("popularity"):4d} | {var} | {ff["family"]}')
print()
```
Five categories: trending sans, trending serif, monospace, impact/condensed, script/handwriting. All dynamically filtered from Google Fonts metadata — no hardcoded font names. Cross classification boundaries when pairing.
## Selection Thinking
Don't pick fonts by category reflex (editorial → serif, tech → mono, modern → geometric sans). That's pattern matching, not design.
1. **Name the register.** What voice is the content speaking in? Institutional authority? Personal confession? Technical precision? Casual irreverence? The register narrows the field more than the category.
2. **Think physically.** Imagine the font as a physical object the brand could ship — a museum exhibit caption, a hand-painted shop sign, a 1970s mainframe terminal manual, a fabric label inside a coat, a children's book printed on cheap newsprint, a tax form. Whichever physical object fits the register is pointing at the right _kind_ of typeface.
3. **Reject your first instinct.** The first font that feels right is usually your training-data default for that register. If you picked it last time too, find something else.
4. **Cross-check the assumption.** An editorial brief does NOT need a serif. A technical brief does NOT need a sans. A children's product does NOT need a rounded display font. The most distinctive choice often contradicts the category expectation.
## Similar-Font Pairing
Never pair two fonts that are similar but not identical — two geometric sans-serifs, two transitional serifs, two humanist sans. They create visual friction without clear hierarchy. The viewer senses something is "off" but can't articulate it. Either use one font at two weights, or pair fonts that contrast on multiple axes: serif + sans, condensed + wide, geometric + humanist.
## Dark Backgrounds
Light text on dark backgrounds creates two optical illusions you need to compensate for:
- **Increased apparent weight.** Light-on-dark reads heavier than dark-on-light at the same `font-weight`. Use 350 instead of 400 for body text. Headlines are less affected because size compensates.
- **Tighter apparent spacing.** Light halos around letterforms reduce perceived gaps. Increase `line-height` by 0.05-0.1 beyond your light-background value. For display sizes, add 0.01em `letter-spacing` to counteract.
## OpenType Features for Data
Most fonts ship with OpenType features that are off by default. Turn them on for data compositions:
```css
/* Tabular numbers — digits align vertically in columns */
.stat-value,
.timer,
.data-column {
font-variant-numeric: tabular-nums;
}
/* Diagonal fractions — renders 1/2 as ½ */
.recipe-amount,
.ratio {
font-variant-numeric: diagonal-fractions;
}
/* Small caps for abbreviations — less visual shouting */
.abbreviation,
.unit {
font-variant-caps: all-small-caps;
}
/* Disable ligatures in code — fi, fl, ffi should stay separate */
code,
.code {
font-variant-ligatures: none;
}
```
`tabular-nums` is essential any time numbers are stacked vertically — stat callouts, timers, scoreboards, data tables. Without it, digits have proportional widths and columns don't align.
@@ -0,0 +1,596 @@
#!/usr/bin/env node
// animation-map.mjs — HyperFrames animation map for agents
//
// Reads every GSAP timeline registered in window.__timelines, enumerates
// tweens, samples bboxes at N points per tween, computes flags and
// human-readable summaries. Outputs a single animation-map.json.
//
// Usage:
// node skills/hyperframes/scripts/animation-map.mjs <composition-dir> \
// [--frames N] [--out <dir>] [--min-duration S] [--width W] [--height H] [--fps N]
import { mkdir, writeFile } from "node:fs/promises";
import { resolve, join } from "node:path";
import {
createFileServer,
createCaptureSession,
initializeSession,
closeCaptureSession,
getCompositionDuration,
} from "@hyperframes/producer";
// ─── CLI ─────────────────────────────────────────────────────────────────────
const args = parseArgs(process.argv.slice(2));
if (!args.composition) die("missing <composition-dir>");
const FRAMES = Number(args.frames ?? 6);
const OUT_DIR = resolve(args.out ?? ".hyperframes/anim-map");
const MIN_DUR = Number(args["min-duration"] ?? 0.15);
const WIDTH = Number(args.width ?? 1920);
const HEIGHT = Number(args.height ?? 1080);
const FPS = Number(args.fps ?? 30);
const COMP_DIR = resolve(args.composition);
await mkdir(OUT_DIR, { recursive: true });
// ─── Main ────────────────────────────────────────────────────────────────────
const server = await createFileServer({ projectDir: COMP_DIR, port: 0 });
const session = await createCaptureSession(
server.url,
OUT_DIR,
{ width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
null,
);
await initializeSession(session);
try {
const duration = await getCompositionDuration(session);
const tweens = await enumerateTweens(session);
const kept = tweens.filter((tw) => tw.end - tw.start >= MIN_DUR);
const report = {
composition: COMP_DIR,
duration,
totalTweens: tweens.length,
mappedTweens: kept.length,
skippedMicroTweens: tweens.length - kept.length,
tweens: [],
};
for (let i = 0; i < kept.length; i++) {
const tw = kept[i];
const times = Array.from(
{ length: FRAMES },
(_, k) => +(tw.start + ((k + 0.5) / FRAMES) * (tw.end - tw.start)).toFixed(3),
);
const bboxes = [];
for (const t of times) {
await seekTo(session, t);
const bbox = await measureTarget(session, tw.selectorHint);
bboxes.push({ t, ...bbox });
}
const animProps = tw.props.filter(
(p) => !["parent", "overwrite", "immediateRender", "startAt", "runBackwards"].includes(p),
);
const flags = computeFlags(tw, bboxes, { width: WIDTH, height: HEIGHT });
const summary = describeTween(tw, animProps, bboxes, flags);
report.tweens.push({
index: i + 1,
selector: tw.selectorHint,
targets: tw.targetCount,
props: animProps,
start: +tw.start.toFixed(3),
end: +tw.end.toFixed(3),
duration: +(tw.end - tw.start).toFixed(3),
ease: tw.ease,
bboxes,
flags,
summary,
});
}
markCollisions(report.tweens);
for (const tw of report.tweens) {
if (tw.flags.includes("collision") && !tw.summary.includes("collision")) {
tw.summary += " Overlaps another animated element.";
}
}
// ── Composition-level analysis ──
report.choreography = buildTimeline(report.tweens, duration);
report.density = computeDensity(report.tweens, duration);
report.staggers = detectStaggers(report.tweens);
report.elements = buildElementLifecycles(report.tweens);
report.deadZones = findDeadZones(report.density, duration);
report.snapshots = await captureSnapshots(session, report.tweens, duration);
await writeFile(join(OUT_DIR, "animation-map.json"), JSON.stringify(report, null, 2));
printSummary(report);
} finally {
await closeCaptureSession(session).catch(() => {});
server.close();
}
// ─── Seek helper ────────────────────────────────────────────────────────────
async function seekTo(session, t) {
await session.page.evaluate((time) => {
if (window.__hf && typeof window.__hf.seek === "function") {
window.__hf.seek(time);
return;
}
const tls = window.__timelines;
if (tls) {
for (const tl of Object.values(tls)) {
if (typeof tl.seek === "function") tl.seek(time);
}
}
}, t);
await new Promise((r) => setTimeout(r, 100));
}
// ─── Timeline introspection ──────────────────────────────────────────────────
async function enumerateTweens(session) {
return await session.page.evaluate(() => {
const results = [];
const registry = window.__timelines || {};
const selectorOf = (el) => {
if (!el || !(el instanceof Element)) return null;
if (el.id) return `#${el.id}`;
const cls = [...el.classList].slice(0, 2).join(".");
return cls ? `${el.tagName.toLowerCase()}.${cls}` : el.tagName.toLowerCase();
};
const walk = (node, parentOffset = 0) => {
if (!node) return;
if (typeof node.getChildren === "function") {
const offset = parentOffset + (node.startTime?.() ?? 0);
for (const child of node.getChildren(true, true, true)) {
walk(child, offset);
}
return;
}
const targets = (node.targets?.() ?? []).filter((t) => t instanceof Element);
if (!targets.length) return;
const vars = node.vars ?? {};
const props = Object.keys(vars).filter(
(k) =>
![
"duration",
"ease",
"delay",
"repeat",
"yoyo",
"onStart",
"onUpdate",
"onComplete",
"stagger",
].includes(k),
);
const start = parentOffset + (node.startTime?.() ?? 0);
const end = start + (node.duration?.() ?? 0);
results.push({
selectorHint: selectorOf(targets[0]) ?? "(unknown)",
targetCount: targets.length,
props,
start,
end,
ease: typeof vars.ease === "string" ? vars.ease : (vars.ease?.toString?.() ?? "none"),
});
};
for (const tl of Object.values(registry)) walk(tl, 0);
results.sort((a, b) => a.start - b.start);
return results;
});
}
async function measureTarget(session, selector) {
return await session.page.evaluate((sel) => {
const el = document.querySelector(sel);
if (!el) return { x: 0, y: 0, w: 0, h: 0, missing: true };
const r = el.getBoundingClientRect();
const cs = getComputedStyle(el);
return {
x: Math.round(r.x),
y: Math.round(r.y),
w: Math.round(r.width),
h: Math.round(r.height),
opacity: parseFloat(cs.opacity),
visible: cs.visibility !== "hidden" && cs.display !== "none",
};
}, selector);
}
// ─── Tween description (the key output for agents) ──────────────────────────
function describeTween(tw, props, bboxes, flags) {
const dur = (tw.end - tw.start).toFixed(2);
const parts = [];
parts.push(`${tw.selectorHint} animates ${props.join("+")} over ${dur}s (${tw.ease})`);
// Movement
const first = bboxes[0];
const last = bboxes[bboxes.length - 1];
if (first && last) {
const dx = last.x - first.x;
const dy = last.y - first.y;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
const dirs = [];
if (Math.abs(dy) > 3) dirs.push(dy < 0 ? `${Math.abs(dy)}px up` : `${Math.abs(dy)}px down`);
if (Math.abs(dx) > 3)
dirs.push(dx < 0 ? `${Math.abs(dx)}px left` : `${Math.abs(dx)}px right`);
parts.push(`moves ${dirs.join(" and ")}`);
}
}
// Opacity
if (first && last && first.opacity !== undefined && last.opacity !== undefined) {
const o1 = first.opacity;
const o2 = last.opacity;
if (Math.abs(o2 - o1) > 0.1) {
if (o1 < 0.1 && o2 > 0.5) parts.push("fades in");
else if (o1 > 0.5 && o2 < 0.1) parts.push("fades out");
else parts.push(`opacity ${o1.toFixed(1)}${o2.toFixed(1)}`);
}
}
// Scale (from props)
if (props.includes("scale") || props.includes("scaleX") || props.includes("scaleY")) {
parts.push("scales");
}
// Size changes
if (first && last) {
const dw = last.w - first.w;
const dh = last.h - first.h;
if (Math.abs(dw) > 5) parts.push(`width ${first.w}${last.w}px`);
if (Math.abs(dh) > 5) parts.push(`height ${first.h}${last.h}px`);
}
// Visibility
if (first && last && first.visible !== last.visible) {
parts.push(last.visible ? "becomes visible" : "becomes hidden");
}
// Final position
if (last && !last.missing) {
parts.push(`ends at (${last.x}, ${last.y}) ${last.w}×${last.h}px`);
}
// Flags
if (flags.length > 0) {
parts.push(`FLAGS: ${flags.join(", ")}`);
}
return parts.join(". ") + ".";
}
// ─── Flag computation ───────────────────────────────────────────────────────
function computeFlags(tw, bboxes, { width, height }) {
const flags = [];
const dur = tw.end - tw.start;
if (bboxes.every((b) => b.w === 0 || b.h === 0)) flags.push("degenerate");
const anyOffscreen = bboxes.some(
(b) =>
b.x + b.w <= 0 ||
b.y + b.h <= 0 ||
b.x >= width ||
b.y >= height ||
b.x < -b.w * 0.5 ||
b.y < -b.h * 0.5 ||
b.x + b.w > width + b.w * 0.5 ||
b.y + b.h > height + b.h * 0.5,
);
if (anyOffscreen) flags.push("offscreen");
if (bboxes.every((b) => b.opacity !== undefined && b.opacity < 0.01 && b.visible)) {
flags.push("invisible");
}
if (dur < 0.2 && tw.props.some((p) => ["y", "x", "opacity", "scale"].includes(p))) {
flags.push("paced-fast");
}
if (dur > 2.0) flags.push("paced-slow");
return flags;
}
function markCollisions(tweens) {
for (let i = 0; i < tweens.length; i++) {
for (let j = i + 1; j < tweens.length; j++) {
const a = tweens[i];
const b = tweens[j];
if (a.end <= b.start || b.end <= a.start) continue;
for (const ba of a.bboxes) {
const bb = b.bboxes.find((x) => Math.abs(x.t - ba.t) < 0.05);
if (!bb) continue;
const overlap = rectOverlapArea(ba, bb);
const aArea = ba.w * ba.h;
if (aArea > 0 && overlap / aArea > 0.3) {
if (!a.flags.includes("collision")) a.flags.push("collision");
if (!b.flags.includes("collision")) b.flags.push("collision");
break;
}
}
}
}
}
function rectOverlapArea(a, b) {
const x1 = Math.max(a.x, b.x);
const y1 = Math.max(a.y, b.y);
const x2 = Math.min(a.x + a.w, b.x + b.w);
const y2 = Math.min(a.y + a.h, b.y + b.h);
return Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
}
// ─── Composition-level analysis ─────────────────────────────────────────────
function buildTimeline(tweens, duration) {
const cols = 60;
const lines = [];
const secPerCol = duration / cols;
lines.push("Timeline (" + duration.toFixed(1) + "s, each char ≈ " + secPerCol.toFixed(2) + "s):");
lines.push(" " + "0s" + " ".repeat(cols - 8) + duration.toFixed(0) + "s");
lines.push(" " + "┼" + "─".repeat(cols - 1) + "┤");
for (const tw of tweens) {
const startCol = Math.floor(tw.start / secPerCol);
const endCol = Math.min(cols, Math.ceil(tw.end / secPerCol));
const bar =
" ".repeat(startCol) +
"█".repeat(Math.max(1, endCol - startCol)) +
" ".repeat(Math.max(0, cols - endCol));
const label = tw.selector + " " + tw.props.join("+");
lines.push(" " + bar + " " + label);
}
return lines.join("\n");
}
function computeDensity(tweens, duration) {
const buckets = [];
for (let t = 0; t < duration; t += 0.5) {
const active = tweens.filter((tw) => tw.start <= t + 0.5 && tw.end >= t);
buckets.push({ t: +t.toFixed(1), activeTweens: active.length });
}
return buckets;
}
function findDeadZones(density, duration) {
const zones = [];
let zoneStart = null;
for (const d of density) {
if (d.activeTweens === 0) {
if (zoneStart === null) zoneStart = d.t;
} else {
if (zoneStart !== null) {
const zoneEnd = d.t;
if (zoneEnd - zoneStart >= 1.0) {
zones.push({
start: zoneStart,
end: zoneEnd,
duration: +(zoneEnd - zoneStart).toFixed(1),
note:
"No animation for " +
(zoneEnd - zoneStart).toFixed(1) +
"s. Intentional hold or missing entrance?",
});
}
zoneStart = null;
}
}
}
if (zoneStart !== null && duration - zoneStart >= 1.0) {
zones.push({
start: zoneStart,
end: +duration.toFixed(1),
duration: +(duration - zoneStart).toFixed(1),
note:
"No animation for " +
(duration - zoneStart).toFixed(1) +
"s at end. Final hold or missing outro?",
});
}
return zones;
}
function detectStaggers(tweens) {
const groups = [];
const used = new Set();
for (let i = 0; i < tweens.length; i++) {
if (used.has(i)) continue;
const tw = tweens[i];
const group = [tw];
used.add(i);
for (let j = i + 1; j < tweens.length; j++) {
if (used.has(j)) continue;
const other = tweens[j];
const sameProps = tw.props.join(",") === other.props.join(",");
const sameDuration = Math.abs(tw.duration - other.duration) < 0.05;
const closeInTime = other.start - tw.start < tw.duration * 4;
if (sameProps && sameDuration && closeInTime) {
group.push(other);
used.add(j);
}
}
if (group.length >= 3) {
const intervals = [];
for (let k = 1; k < group.length; k++) {
intervals.push(+(group[k].start - group[k - 1].start).toFixed(3));
}
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const maxDrift = Math.max(...intervals.map((iv) => Math.abs(iv - avgInterval)));
const consistent = maxDrift < avgInterval * 0.3;
groups.push({
elements: group.map((g) => g.selector),
props: tw.props,
count: group.length,
intervals,
avgInterval: +avgInterval.toFixed(3),
consistent,
note: consistent
? group.length +
" elements stagger at " +
(avgInterval * 1000).toFixed(0) +
"ms intervals"
: group.length +
" elements stagger with uneven intervals (" +
intervals.map((iv) => (iv * 1000).toFixed(0) + "ms").join(", ") +
")",
});
}
}
return groups;
}
function buildElementLifecycles(tweens) {
const elements = {};
for (const tw of tweens) {
const sel = tw.selector;
if (!elements[sel]) {
elements[sel] = { firstTween: tw.start, lastTween: tw.end, tweenCount: 0, props: new Set() };
}
elements[sel].firstTween = Math.min(elements[sel].firstTween, tw.start);
elements[sel].lastTween = Math.max(elements[sel].lastTween, tw.end);
elements[sel].tweenCount++;
tw.props.forEach((p) => elements[sel].props.add(p));
}
const result = {};
for (const [sel, data] of Object.entries(elements)) {
const lastBbox = findLastBbox(tweens, sel);
result[sel] = {
firstAppears: +data.firstTween.toFixed(3),
lastAnimates: +data.lastTween.toFixed(3),
tweenCount: data.tweenCount,
props: [...data.props],
endsVisible: lastBbox ? lastBbox.opacity > 0.1 && lastBbox.visible : null,
finalPosition: lastBbox
? { x: lastBbox.x, y: lastBbox.y, w: lastBbox.w, h: lastBbox.h }
: null,
};
}
return result;
}
function findLastBbox(tweens, selector) {
for (let i = tweens.length - 1; i >= 0; i--) {
if (tweens[i].selector === selector && tweens[i].bboxes?.length > 0) {
return tweens[i].bboxes[tweens[i].bboxes.length - 1];
}
}
return null;
}
async function captureSnapshots(session, tweens, duration) {
const times = [0, duration * 0.25, duration * 0.5, duration * 0.75, duration - 0.1];
const snapshots = [];
for (const t of times) {
await seekTo(session, t);
const visible = await session.page.evaluate(() => {
const out = [];
const els = document.querySelectorAll("[id]");
for (const el of els) {
const cs = getComputedStyle(el);
if (cs.display === "none") continue;
const opacity = parseFloat(cs.opacity);
if (opacity < 0.01) continue;
const rect = el.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) continue;
out.push({
id: el.id,
x: Math.round(rect.x),
y: Math.round(rect.y),
w: Math.round(rect.width),
h: Math.round(rect.height),
opacity: +opacity.toFixed(2),
});
}
return out;
});
const activeTweens = tweens
.filter((tw) => tw.start <= t && tw.end >= t)
.map((tw) => tw.selector);
snapshots.push({
t: +t.toFixed(2),
visibleElements: visible.length,
animatingNow: activeTweens,
elements: visible,
});
}
return snapshots;
}
// ─── Output ─────────────────────────────────────────────────────────────────
function printSummary(report) {
console.log(
`\nAnimation map: ${report.mappedTweens}/${report.totalTweens} tweens (skipped ${report.skippedMicroTweens} micro-tweens)`,
);
const flagCounts = {};
for (const tw of report.tweens) {
for (const f of tw.flags) flagCounts[f] = (flagCounts[f] ?? 0) + 1;
}
if (Object.keys(flagCounts).length > 0) {
for (const [f, n] of Object.entries(flagCounts)) console.log(` ${f}: ${n}`);
}
if (report.staggers?.length > 0) {
console.log(` staggers: ${report.staggers.map((s) => s.note).join("; ")}`);
}
if (report.deadZones?.length > 0) {
console.log(
` dead zones: ${report.deadZones.map((z) => z.start + "-" + z.end + "s").join(", ")}`,
);
}
console.log(report.choreography);
}
function parseArgs(argv) {
const out = {};
let positional = 0;
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith("--")) {
const k = a.slice(2);
const v = argv[i + 1]?.startsWith("--") ? true : argv[++i];
out[k] = v;
} else if (positional === 0) {
out.composition = a;
positional++;
}
}
return out;
}
function die(msg) {
console.error(`animation-map: ${msg}`);
process.exit(2);
}
@@ -0,0 +1,335 @@
#!/usr/bin/env node
// contrast-report.mjs — HyperFrames contrast audit
//
// Reads a composition, seeks to N sample timestamps, walks the DOM for text
// elements, measures the WCAG 2.1 contrast ratio between each element's
// declared foreground color and the pixels behind it, and emits:
//
// - contrast-report.json (machine-readable, one entry per text element × sample)
// - contrast-overlay.png (sprite grid; magenta=fail AA, yellow=pass AA only, green=AAA)
//
// Usage:
// node skills/hyperframes/scripts/contrast-report.mjs <composition-dir> \
// [--samples N] [--out <dir>] [--width W] [--height H] [--fps N]
//
// The composition directory must contain an index.html. Raw authoring HTML
// works — the producer's file server auto-injects the runtime at serve time.
// Exits 1 if any text element fails WCAG AA.
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import sharp from "sharp";
// Use the producer's file server — it auto-injects the HyperFrames runtime
// and render-seek bridge, so raw authoring HTML works without a build step.
import {
createFileServer,
createCaptureSession,
initializeSession,
closeCaptureSession,
captureFrameToBuffer,
getCompositionDuration,
} from "@hyperframes/producer";
// ─── CLI ─────────────────────────────────────────────────────────────────────
const args = parseArgs(process.argv.slice(2));
if (!args.composition) die("missing <composition-dir>");
const SAMPLES = Number(args.samples ?? 10);
const OUT_DIR = resolve(args.out ?? ".hyperframes/contrast");
const WIDTH = Number(args.width ?? 1920);
const HEIGHT = Number(args.height ?? 1080);
const FPS = Number(args.fps ?? 30);
const COMP_DIR = resolve(args.composition);
// ─── Main ────────────────────────────────────────────────────────────────────
await mkdir(OUT_DIR, { recursive: true });
const server = await createFileServer({ projectDir: COMP_DIR, port: 0 });
const session = await createCaptureSession(
server.url,
OUT_DIR,
{ width: WIDTH, height: HEIGHT, fps: FPS, format: "png" },
null,
);
await initializeSession(session);
try {
const duration = await getCompositionDuration(session);
const times = Array.from(
{ length: SAMPLES },
(_, i) => +(((i + 0.5) / SAMPLES) * duration).toFixed(3),
);
const allEntries = [];
const overlayFrames = [];
for (let i = 0; i < times.length; i++) {
const t = times[i];
const { buffer: pngBuf } = await captureFrameToBuffer(session, i, t);
const elements = await probeTextElements(session, t);
const annotated = await annotateFrame(pngBuf, elements);
overlayFrames.push({ t, png: annotated });
for (const el of elements) allEntries.push({ time: t, ...el });
}
const report = {
composition: COMP_DIR,
width: WIDTH,
height: HEIGHT,
duration,
samples: times,
entries: allEntries,
summary: summarize(allEntries),
};
await writeFile(resolve(OUT_DIR, "contrast-report.json"), JSON.stringify(report, null, 2));
await writeOverlaySprite(overlayFrames, resolve(OUT_DIR, "contrast-overlay.png"));
printSummary(report);
process.exitCode = report.summary.failAA > 0 ? 1 : 0;
} finally {
await closeCaptureSession(session).catch(() => {});
server.close();
}
// ─── DOM probe (runs in the page) ────────────────────────────────────────────
async function probeTextElements(session, _t) {
// `session.page` is the Puppeteer Page owned by the capture session.
// We pass a pure function to `evaluate`: it walks the DOM and returns
// enough info for us to compute a ratio in Node using the frame buffer.
return await session.page.evaluate(() => {
/** @type {Array<{selector: string, text: string, fg: [number,number,number,number], fontSize: number, fontWeight: number, bbox: {x:number,y:number,w:number,h:number}}>} */
const out = [];
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
const parseColor = (c) => {
const m = c.match(/rgba?\(([^)]+)\)/);
if (!m) return [0, 0, 0, 1];
const parts = m[1].split(",").map((s) => parseFloat(s.trim()));
return [parts[0], parts[1], parts[2], parts[3] ?? 1];
};
const selectorOf = (el) => {
if (el.id) return `#${el.id}`;
const cls = [...el.classList].slice(0, 2).join(".");
return cls ? `${el.tagName.toLowerCase()}.${cls}` : el.tagName.toLowerCase();
};
let el;
while ((el = walker.nextNode())) {
// must have direct text
const direct = [...el.childNodes].some(
(n) => n.nodeType === 3 && n.textContent.trim().length,
);
if (!direct) continue;
const cs = getComputedStyle(el);
if (cs.visibility === "hidden" || cs.display === "none") continue;
if (parseFloat(cs.opacity) <= 0.01) continue;
const rect = el.getBoundingClientRect();
if (rect.width < 8 || rect.height < 8) continue;
out.push({
selector: selectorOf(el),
text: el.textContent.trim().slice(0, 60),
fg: parseColor(cs.color),
fontSize: parseFloat(cs.fontSize),
fontWeight: Number(cs.fontWeight) || 400,
bbox: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
});
}
return out;
});
}
// ─── Pixel sampling + WCAG math ──────────────────────────────────────────────
async function annotateFrame(pngBuf, elements) {
const img = sharp(pngBuf);
const meta = await img.metadata();
const { width, height } = meta;
const raw = await img.ensureAlpha().raw().toBuffer();
const channels = 4;
const measured = [];
for (const el of elements) {
const bg = sampleRingMedian(raw, width, height, channels, el.bbox);
const fg = compositeOver(el.fg, bg); // flatten any alpha against measured bg
const ratio = wcagRatio(fg, bg);
const large = isLargeText(el.fontSize, el.fontWeight);
el.bg = bg;
el.ratio = +ratio.toFixed(2);
el.wcagAA = large ? ratio >= 3 : ratio >= 4.5;
el.wcagAALarge = ratio >= 3;
el.wcagAAA = large ? ratio >= 4.5 : ratio >= 7;
measured.push(el);
}
// Draw boxes + ratio labels as an SVG overlay (sharp composite).
const svg = buildOverlaySVG(measured, width, height);
return await sharp(pngBuf)
.composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
.png()
.toBuffer();
}
function sampleRingMedian(raw, width, height, channels, bbox) {
// 4-px ring immediately outside the element bbox. Median of each channel.
const r = [],
g = [],
b = [];
const x0 = Math.max(0, Math.floor(bbox.x) - 4);
const x1 = Math.min(width - 1, Math.ceil(bbox.x + bbox.w) + 4);
const y0 = Math.max(0, Math.floor(bbox.y) - 4);
const y1 = Math.min(height - 1, Math.ceil(bbox.y + bbox.h) + 4);
const pushPixel = (x, y) => {
const i = (y * width + x) * channels;
r.push(raw[i]);
g.push(raw[i + 1]);
b.push(raw[i + 2]);
};
for (let x = x0; x <= x1; x++) {
pushPixel(x, y0);
pushPixel(x, y1);
}
for (let y = y0; y <= y1; y++) {
pushPixel(x0, y);
pushPixel(x1, y);
}
return [median(r), median(g), median(b), 1];
}
function median(arr) {
const s = [...arr].sort((a, b) => a - b);
return s[Math.floor(s.length / 2)];
}
function compositeOver([fr, fg, fb, fa], [br, bg, bb]) {
return [
Math.round(fr * fa + br * (1 - fa)),
Math.round(fg * fa + bg * (1 - fa)),
Math.round(fb * fa + bb * (1 - fa)),
1,
];
}
function relLum([r, g, b]) {
const ch = (v) => {
const s = v / 255;
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b);
}
function wcagRatio(a, b) {
const la = relLum(a);
const lb = relLum(b);
const [L1, L2] = la > lb ? [la, lb] : [lb, la];
return (L1 + 0.05) / (L2 + 0.05);
}
function isLargeText(fontSize, fontWeight) {
return fontSize >= 24 || (fontSize >= 19 && fontWeight >= 700);
}
// ─── Overlay rendering ───────────────────────────────────────────────────────
function buildOverlaySVG(elements, w, h) {
const rects = elements
.map((el) => {
const color = !el.wcagAA ? "#ff00aa" : !el.wcagAAA ? "#ffcc00" : "#00e08a";
const { x, y, w: bw, h: bh } = el.bbox;
return `
<rect x="${x}" y="${y}" width="${bw}" height="${bh}"
fill="none" stroke="${color}" stroke-width="3"/>
<rect x="${x}" y="${y - 18}" width="${48}" height="16" fill="${color}"/>
<text x="${x + 4}" y="${y - 5}" font-family="monospace" font-size="12" fill="#000">
${el.ratio.toFixed(1)}:1
</text>`;
})
.join("");
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">${rects}</svg>`;
}
async function writeOverlaySprite(frames, outPath) {
if (!frames.length) return;
const cols = Math.min(frames.length, 5);
const rows = Math.ceil(frames.length / cols);
const { width, height } = await sharp(frames[0].png).metadata();
const scale = 0.25;
const cellW = Math.round(width * scale);
const cellH = Math.round(height * scale);
const cells = await Promise.all(
frames.map(async (f) => ({
input: await sharp(f.png).resize(cellW, cellH).png().toBuffer(),
time: f.t,
})),
);
const composites = cells.map((c, i) => ({
input: c.input,
top: Math.floor(i / cols) * cellH,
left: (i % cols) * cellW,
}));
await sharp({
create: {
width: cols * cellW,
height: rows * cellH,
channels: 3,
background: { r: 16, g: 16, b: 20 },
},
})
.composite(composites)
.png()
.toFile(outPath);
}
// ─── Summary ────────────────────────────────────────────────────────────────
function summarize(entries) {
const total = entries.length;
const failAA = entries.filter((e) => !e.wcagAA).length;
const passAAonly = entries.filter((e) => e.wcagAA && !e.wcagAAA).length;
const passAAA = entries.filter((e) => e.wcagAAA).length;
return { total, failAA, passAAonly, passAAA };
}
function printSummary({ summary, entries }) {
const { total, failAA, passAAonly, passAAA } = summary;
console.log(`\nContrast report: ${total} text-element samples`);
console.log(` fail WCAG AA: ${failAA}`);
console.log(` pass AA, not AAA: ${passAAonly}`);
console.log(` pass AAA: ${passAAA}`);
if (failAA) {
console.log("\nFailures:");
for (const e of entries.filter((x) => !x.wcagAA)) {
console.log(` t=${e.time}s ${e.selector.padEnd(24)} ${e.ratio.toFixed(2)}:1 "${e.text}"`);
}
}
}
// ─── Utilities ──────────────────────────────────────────────────────────────
function parseArgs(argv) {
const out = {};
let positional = 0;
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith("--")) {
const k = a.slice(2);
const v = argv[i + 1]?.startsWith("--") ? true : argv[++i];
out[k] = v;
} else if (positional === 0) {
out.composition = a;
positional++;
}
}
return out;
}
function die(msg) {
console.error(`contrast-report: ${msg}`);
process.exit(2);
}
+211
View File
@@ -0,0 +1,211 @@
# Visual Style Library
Named visual identities for HyperFrames videos. Each style is grounded in a real graphic design tradition. Use them to give your video a specific visual personality, not just generic "clean" or "bold."
**How to pick:** Match mood first, content second. Ask: _"What should the viewer FEEL?"_
**How to use:** Reference the style in your scene plan. Translate the style's principles into concrete composition decisions — palette choice, font selection, entrance patterns, transition type, ambient motion feel.
## Quick Reference
| Style | Mood | Best for | Primary shader |
| --------------- | --------------------- | ---------------------------------- | --------------------------------- |
| Swiss Pulse | Clinical, precise | SaaS, data, dev tools, metrics | Cinematic Zoom or SDF Iris |
| Velvet Standard | Premium, timeless | Luxury, enterprise, keynotes | Cross-Warp Morph |
| Deconstructed | Industrial, raw | Tech launches, security, punk | Glitch or Whip Pan |
| Maximalist Type | Loud, kinetic | Big announcements, launches | Ridged Burn |
| Data Drift | Futuristic, immersive | AI, ML, cutting-edge tech | Gravitational Lens or Domain Warp |
| Soft Signal | Intimate, warm | Wellness, personal stories, brand | Thermal Distortion |
| Folk Frequency | Cultural, vivid | Consumer apps, food, communities | Swirl Vortex or Ripple Waves |
| Shadow Cut | Dark, cinematic | Dramatic reveals, security, exposé | Domain Warp |
---
## 1. Swiss Pulse — Josef Müller-Brockmann
**Mood:** Clinical, precise | **Best for:** SaaS dashboards, developer tools, APIs, metrics
- Black (`#1a1a1a`), white, ONE accent — electric blue (`#0066FF`) or amber (`#FFB300`)
- Helvetica or Inter Bold for headlines, Regular for labels. Numbers large (80120px)
- Grid-locked compositions. Every element snaps to an invisible 12-column grid
- Animated counters count up from 0. Hard cuts, no decorative transitions
- Transitions: Cinematic Zoom or SDF Iris (precise, geometric)
**GSAP signature:** `expo.out`, `power4.out`. Entries are fast and snap into place. Nothing floats.
```
Swiss Pulse: Black/white + one electric accent. Grid-locked compositions.
Numbers dominate the frame at 80-120px. Counter animations from 0.
Hard cuts or geometric transitions. Nothing decorative.
```
---
## 2. Velvet Standard — Massimo Vignelli
**Mood:** Premium, timeless | **Best for:** Luxury products, enterprise software, keynotes, investor decks
- Black, white, ONE rich accent — deep navy (`#1a237e`) or gold (`#c9a84c`)
- Thin sans-serif, ALL CAPS, wide letter-spacing (`0.15em+`)
- Generous negative space. Symmetrical, centered, architectural precision
- Slow, deliberate. Sequential reveals with long holds. No frantic motion
- Transitions: Cross-Warp Morph (elegant, organic flow between scenes)
**GSAP signature:** `sine.inOut`, `power1`. Nothing snaps — everything glides with intention.
```
Velvet Standard: Black, white, one rich accent. Thin ALL CAPS type with wide tracking.
Generous negative space. Sequential reveals, long holds.
Cross-Warp Morph transitions. Slow and deliberate — luxury takes its time.
```
---
## 3. Deconstructed — Neville Brody
**Mood:** Industrial, raw | **Best for:** Tech news, developer launches, security products, punk-energy reveals
- Dark grey (`#1a1a1a`), rust orange (`#D4501E`), raw white (`#f0f0f0`)
- Type at angles, overlapping edges, escaping frames. Bold industrial weight
- Gritty textures: scan-line effects, glitch artifacts baked into the design
- Text SLAMS and SHATTERS. Letters scramble then snap to final position
- Transitions: Glitch shader or Whip Pan (breaks the rules, feels aggressive)
**GSAP signature:** `back.out(2.5)`, `steps(8)`, `elastic.out(1.2, 0.4)`. Intentional irregularity.
```
Deconstructed: Dark grey #1a1a1a + rust orange #D4501E. Type at angles, escaping frames.
Scan-line glitch overlays. Text SLAMS and scrambles into place.
Glitch shader transitions. Industrial and raw — nothing should feel polished.
```
---
## 4. Maximalist Type — Paula Scher
**Mood:** Loud, kinetic | **Best for:** Big product launches, milestone announcements, high-energy hype videos
- Bold saturated: red (`#E63946`), yellow (`#FFD60A`), black, white — maximum contrast
- Text IS the visual. Overlapping type layers at different scales and angles, filling 5080% of frame
- Everything is kinetic: slamming, sliding, scaling. 23 second rapid-fire scenes
- Text layered OVER footage — never empty backgrounds
- Transitions: Ridged Burn (explosive, dramatic, impossible to ignore)
**GSAP signature:** `expo.out`, `back.out(1.8)`. Fast arrivals, hard stops.
```
Maximalist Type: Red, yellow, black, white — max contrast. Text IS the visual.
Overlapping at different scales, 50-80% of frame. Everything in motion.
Ridged Burn transitions. No static moments — kinetic energy throughout.
```
---
## 5. Data Drift — Refik Anadol
**Mood:** Futuristic, immersive | **Best for:** AI products, ML platforms, data companies, speculative tech
- Iridescent: deep black (`#0a0a0a`), electric purple (`#7c3aed`), cyan (`#06b6d4`)
- Thin futuristic sans-serif — floating, weightless, minimal
- Fluid morphing compositions. Extreme scale shifts (micro → macro)
- Particles coalesce into numbers. Light traces data paths through the frame
- Transitions: Gravitational Lens or Domain Warp (otherworldly distortion)
**GSAP signature:** `sine.inOut`, `power2.out`. Smooth, continuous, organic. Nothing hard.
```
Data Drift: Deep black #0a0a0a with electric purple #7c3aed and cyan #06b6d4.
Thin futuristic type, minimal text. Particles coalesce into numbers.
Gravitational Lens or Domain Warp transitions. Fluid, immersive, otherworldly.
```
---
## 6. Soft Signal — Stefan Sagmeister
**Mood:** Intimate, warm | **Best for:** Wellness brands, personal stories, lifestyle products, human-centered apps
- Warm amber (`#F5A623`), cream (`#FFF8EC`), dusty rose (`#C4A3A3`), sage green (`#8FAF8C`)
- Handwritten-style or humanist serif fonts. Personal, lowercase, delicate
- Close-up framing feel: single element fills the frame. Nothing feels corporate
- Slow drifts and floats, never snaps. Soft organic motion throughout
- Transitions: Thermal Distortion (warm, flowing, like heat shimmer)
**GSAP signature:** `sine.inOut`, `power1.inOut`. Everything breathes.
```
Soft Signal: Warm amber, cream, dusty rose, sage green. Humanist or handwritten type.
Single elements fill the frame — intimate, never corporate.
Slow drifts and floats throughout. Thermal Distortion transitions.
Nothing should feel hurried or polished.
```
---
## 7. Folk Frequency — Eduardo Terrazas
**Mood:** Cultural, vivid | **Best for:** Consumer apps, food platforms, community products, festive launches
- Vivid folk: hot pink (`#FF1493`), cobalt blue (`#0047AB`), sun yellow (`#FFE000`), emerald (`#009B77`)
- Bold warm rounded type. Pattern and repetition — folk art rhythm and density
- Layered compositions with rich visual texture. Every frame feels handcrafted
- Colorful motion: elements bounce, pop, and spin into place with joy
- Transitions: Swirl Vortex or Ripple Waves (hypnotic, celebratory)
**GSAP signature:** `back.out(1.6)`, `elastic.out(1, 0.5)`. Overshoots feel intentional.
```
Folk Frequency: Hot pink #FF1493, cobalt blue, sun yellow, emerald. Bold rounded type.
Pattern and repetition throughout. Layered, dense, handcrafted feeling.
Swirl Vortex or Ripple Waves transitions. Joyful, celebratory energy.
```
---
## 8. Shadow Cut — Hans Hillmann
**Mood:** Dark, cinematic | **Best for:** Security products, dramatic reveals, investigative content, intense launches
- Near-monochrome: deep blacks (`#0a0a0a`), cold greys (`#3a3a3a`), stark white + blood red (`#C1121F`) or toxic green (`#39FF14`)
- Sharp angular text like film noir title cards. Heavy contrast, no softness
- Heavy shadow — elements emerge from darkness. Reveal is the narrative
- Slow creeping push-ins, dramatic scale reveals, silence before the hit
- Transitions: Domain Warp (dissolves reality itself before revealing the next scene)
**GSAP signature:** `power4.in` for exits, `power3.out` for dramatic reveals. The pause before the hit matters.
```
Shadow Cut: Deep blacks #0a0a0a, cold greys, stark white + one accent (blood red or toxic green).
Sharp angular type, film noir aesthetic. Elements emerge from darkness.
Slow creeping push-ins. Domain Warp transitions. The reveal IS the story.
```
---
## Mood → Style Guide
| If the content feels... | Use... |
| ---------------------------------- | --------------- |
| Data-driven, analytical, technical | Swiss Pulse |
| Premium, enterprise, luxury | Velvet Standard |
| Raw, punk, aggressive, rebellious | Deconstructed |
| Hype, loud, high-energy launch | Maximalist Type |
| AI, ML, speculative, futuristic | Data Drift |
| Human, warm, personal, wellness | Soft Signal |
| Cultural, fun, consumer, festive | Folk Frequency |
| Dark, dramatic, intense, cinematic | Shadow Cut |
---
## Creating Custom Styles
These 8 styles are examples — not constraints. Create your own by:
1. **Name it** after a designer, art movement, or cultural reference
2. **Palette**: 2-3 colors max. Declare explicit hex values
3. **Typography**: One family, two weights. State the role of each
4. **Motion rules**: How fast? Snappy or fluid? Overshoot or precision?
5. **Transition**: Which shader matches the energy?
6. **What NOT to do**: 2-3 explicit anti-patterns for this style
The pattern: **named style → palette → typography → motion rules → transition → avoids.**
@@ -0,0 +1,121 @@
---
name: website-to-hyperframes
description: |
Capture a website and create a HyperFrames video from it. Use when: (1) a user provides a URL and wants a video, (2) someone says "capture this site", "turn this into a video", "make a promo from my site", (3) the user wants a social ad, product tour, or any video based on an existing website, (4) the user shares a link and asks for any kind of video content. Even if the user just pastes a URL — this is the skill to use.
---
# Website to HyperFrames
Capture a website, then produce a professional video from it.
Users say things like:
- "Capture https://... and make me a 25-second product launch video"
- "Turn this website into a 15-second social ad for Instagram"
- "Create a 30-second product tour from https://..."
The workflow has 7 steps. Each produces an artifact that gates the next.
---
## Step 1: Capture & Understand
**Read:** [references/step-1-capture.md](references/step-1-capture.md)
Run the capture, read the extracted data, and build a working summary using the write-down-and-forget method.
**Gate:** Print your site summary (name, top colors, fonts, key assets, one-sentence vibe).
---
## Step 2: Write DESIGN.md
**Read:** [references/step-2-design.md](references/step-2-design.md)
Write a simple brand reference for the captured website. 6 sections, ~90 lines. This is a cheat sheet, not the creative plan — that comes in Step 4.
**Gate:** `DESIGN.md` exists in the project directory.
---
## Step 3: Write SCRIPT
**Read:** [references/step-3-script.md](references/step-3-script.md)
Write the narration script. The story backbone. Scene durations come from the narration, not from guessing.
**Gate:** `SCRIPT.md` exists in the project directory.
---
## Step 4: Write STORYBOARD
**Read:** [references/step-4-storyboard.md](references/step-4-storyboard.md)
Write per-beat creative direction: mood, camera, animations, transitions, assets, depth layers, SFX. This is the creative north star — the document the engineer follows to build each composition.
**Gate:** `STORYBOARD.md` exists with beat-by-beat direction and an asset audit table.
---
## Step 5: Generate VO + Map Timing
**Read:** [references/step-5-vo.md](references/step-5-vo.md)
Generate TTS audio, transcribe for word-level timestamps, and map timestamps to beats. Update STORYBOARD.md with real durations.
**Gate:** `narration.wav` (or .mp3) + `transcript.json` exist. Beat timings in STORYBOARD.md updated.
---
## Step 6: Build Compositions
**Read:** The `/hyperframes` skill (invoke it — every rule matters)
**Read:** [references/step-6-build.md](references/step-6-build.md)
Build each composition following the storyboard. After each one: self-review for layout, asset placement, and animation quality.
**Gate:** Every composition has been self-reviewed. No overlapping elements, no misplaced assets, no static images without motion.
---
## Step 7: Validate & Deliver
**Read:** [references/step-7-validate.md](references/step-7-validate.md)
Lint, validate, preview. Create a HANDOFF.md for multi-session continuity.
**Gate:** `npx hyperframes lint` and `npx hyperframes validate` pass with zero errors.
---
## Quick Reference
### Video Types
| Type | Duration | Beats | Narration |
| --------------------- | -------- | ----- | ---------------------- |
| Social ad (IG/TikTok) | 10-15s | 3-4 | Optional hook sentence |
| Product demo | 30-60s | 5-8 | Full narration |
| Feature announcement | 15-30s | 3-5 | Full narration |
| Brand reel | 20-45s | 4-6 | Optional, music focus |
| Launch teaser | 10-20s | 2-4 | Minimal, high energy |
### Format
- **Landscape**: 1920x1080 (default)
- **Portrait**: 1080x1920 (Instagram Stories, TikTok)
- **Square**: 1080x1080 (Instagram feed)
### Reference Files
| File | When to read |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [step-1-capture.md](references/step-1-capture.md) | Step 1 — reading captured data |
| [step-2-design.md](references/step-2-design.md) | Step 2 — writing DESIGN.md |
| [step-3-script.md](references/step-3-script.md) | Step 3 — writing the narration script |
| [step-4-storyboard.md](references/step-4-storyboard.md) | Step 4 — per-beat creative direction |
| [step-5-vo.md](references/step-5-vo.md) | Step 5 — TTS, transcription, timing |
| [step-6-build.md](references/step-6-build.md) | Step 6 — building compositions with self-review |
| [step-7-validate.md](references/step-7-validate.md) | Step 7 — lint, validate, preview, handoff |
| [techniques.md](references/techniques.md) | Steps 4 & 6 — 10 visual techniques with code patterns (SVG drawing, Canvas 2D, 3D, typography, Lottie, video, typing, variable fonts, MotionPath, transitions) |
@@ -0,0 +1,66 @@
# Step 1: Capture & Understand
## Run the capture
```bash
npx hyperframes capture <URL> -o captures/<project-name>
```
No API keys required. The capture extracts design tokens, screenshots, fonts, and assets with DOM-context descriptions automatically.
**Optional:** Set `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) in a `.env` file at the repo root for richer AI-powered image descriptions via Gemini 2.5 Flash vision. Free tier: 5 RPM; paid tier removes the bottleneck.
Wait for it to complete. Print how many screenshots, assets, sections, and fonts were extracted.
## Read and summarize
Read each file below. After reading each one, **write a 1-2 sentence summary** of what you learned. These summaries are your working memory — the raw file content may be cleared from context later.
### Must read (do not skip)
1. **View the scroll screenshots** — viewport-sized captures covering the full page height (the number depends on the page length). Start with:
- `screenshots/scroll-000.png` — the hero section at full 1920x1080 resolution. This is the most important image. Describe: is the background light or dark? What's the dominant visual element? What colors jump out?
- Then scan through the rest to see the full page. Each screenshot overlaps the previous by ~30%.
After viewing them, write 3-4 sentences describing the site's visual mood, layout patterns, color strategy, and overall feel.
2. **`extracted/tokens.json`** — Note the top 5-7 colors (HEX), all font families, number of sections, and number of headings/CTAs.
3. **`extracted/visible-text.txt`** — Note the site's headline, tagline, key selling points, and any notable statistics or social proof.
4. **`extracted/asset-descriptions.md`** — One-line-per-file summary of all downloaded assets. Note which assets are most visually striking or useful for video (hero images, logos, product screenshots).
### Read if they exist
5. **`extracted/animations.json`** — Note if the site uses scroll-triggered animations, marquees, canvas/WebGL, or named CSS animations.
6. **`extracted/lottie-manifest.json`** — View each preview image at `assets/lottie/previews/` to see what the animations look like.
7. **`extracted/video-manifest.json`** — View each preview at `assets/videos/previews/` to see what each video shows.
8. **`extracted/shaders.json`** — If present, this contains the actual GLSL shader code that powers the site's WebGL visual effects (gradient waves, particle systems, noise fields). Read the fragment shaders to extract: color values used in gradients, noise algorithms, blend functions. You can recreate similar effects in your compositions using Canvas 2D or by embedding the shader patterns with a `<canvas>` + WebGL context. See the Canvas 2D and procedural art patterns in `techniques.md`.
### On-demand (read when building scenes)
9. **Individual images in `assets/`** — Use `asset-descriptions.md` as your index. View specific images when you need them for a beat.
10. **`extracted/assets-catalog.json`** — Use to find remote URLs when you need an asset that wasn't downloaded.
### For rich captures (30+ images)
Launch a sub-agent to view all images and SVGs:
> "Read every image in assets/ and every SVG in assets/svgs/. For each, write one line: filename — what it shows, dominant colors, approximate size. Return the complete catalog."
Use the sub-agent's catalog as your asset reference for the rest of the workflow.
## Gate
Print your site summary before proceeding to Step 2:
- **Site:** [name]
- **Colors:** [top 3-5 HEX values with roles]
- **Fonts:** [font families]
- **Sections:** [count] sections, [count] headings, [count] CTAs
- **Key assets:** [3-5 most useful assets for video]
- **Vibe:** [one sentence describing the visual identity]
@@ -0,0 +1,128 @@
# Step 2: Write DESIGN.md
DESIGN.md is a **brand cheat sheet** for the captured website. It encodes the visual identity so you can reference exact colors, fonts, and patterns while writing the storyboard and compositions.
DESIGN.md is NOT the creative plan. The STORYBOARD (Step 4) drives creative direction. DESIGN.md is a reference you consult, not a document you follow slavishly.
## The 6 Sections
### `## Overview`
3-4 sentences. Describe the visual identity factually: layout patterns (bento grid, logo wall, hero section), color strategy, typography tone, overall feel. Be precise, not poetic.
### `## Colors`
5-10 key colors with HEX values from tokens.json and their roles:
```
- **Primary Surface**: `#020204` — deep black background
- **Primary Content**: `#FFFFFF` — high-purity white for text and borders
- **Accent Warm**: `#FB923C` — orange for CTAs and highlights
```
Include semantic colors if the site uses color to differentiate product areas.
### `## Typography`
Font families with weights, roles, and any distinctive usage:
```
- **Serif**: Cormorant Garamond (Italic). Major headings, brand identity.
- **Monospace**: Geist Mono. Subheaders, labels, terminal readouts. High tracking (0.1-0.3em), all-caps.
- **Sans-Serif**: Inter. Body copy, interface elements. Small sizes (9-14px).
```
Include sizing hierarchy if notable (hero: 64px, section: 32px, body: 16px).
### `## Elevation`
One paragraph on depth strategy: Does the site use borders, shadows, glassmorphism, or flat color shifts? Reference specific patterns (e.g., "1px borders at white/10 opacity" or "layered backdrop-blur with thin borders").
### `## Components`
Name every notable UI component you see in the screenshot. Be specific:
- "Cinematic Accordion" not "Cards"
- "Logo Marquee" not "Scrolling section"
- "Glass Cards with grain overlay" not "Content containers"
For each, note the distinctive visual treatment (border-radius, spacing, hover behavior).
### `## Do's and Don'ts`
3-5 rules each, derived from what the site actually does and doesn't do:
```
### Do's
- Use thin subtle borders (white/10) to separate sections
- Keep imagery desaturated with dark gradients for text readability
### Don'ts
- Do not use bright solid background colors — stay in "The Void"
- Do not use standard drop shadows — use radial glow or bloom effects
- Do not use sharp high-speed animations — all motion should be fluid
```
## Rules
- Use **exact HEX values** from tokens.json. Do not approximate.
- Name components by what you see in the screenshot, not generic terms.
- Keep it under 100 lines. This is a cheat sheet, not a design system document.
- No "Style Prompt" section — the storyboard handles creative direction.
- No "Assets" section — `asset-descriptions.md` already covers this.
- No "Motion" section — the storyboard specifies motion per-beat.
## Example
This is a real DESIGN.md from a production capture (Soulscape 2026):
```markdown
# Design System
## Overview
Soulscape 2026 is a cinematic, "high-signal" digital experience that positions itself as the vanguard of AI filmmaking. The visual personality is dark, technical, and premium, characterized by high-contrast "Flare" on "Void" (white on black) aesthetics. The layout is dense but organized, utilizing heavy horizontal layering and border-defined sections to evoke a wide-screen cinematic feel. Motion is a core tenet, with atmospheric grain overlays, shifting light leaks, and slow-moving marquees creating constant, breathing texture.
## Colors
- **Primary Surface**: `#020204` (Void) - Deep black for the entire background.
- **Primary Content**: `#FFFFFF` (Flare) - High-purity white for typography and primary borders.
- **Accent 1 (Warm)**: `#FB923C` - Orange for industry/executive tiers and primary CTAs.
- **Accent 2 (Cool)**: `#60A5FA` - Blue for creative voices and summit-focused components.
- **Subtle Overlays**: `rgba(255, 255, 255, 0.02)` to `0.08` for glass backgrounds.
## Typography
- **Serif**: Cormorant Garamond (Italic). Major headings and "Soul" brand identity. Classical cinematic contrast.
- **Monospace**: Geist Mono. Subheaders, labels, terminal readouts. High tracking (0.1-0.3em), all-caps.
- **Sans-Serif**: Inter. Body copy and interface elements. Small sizes (9-14px).
## Elevation
- **Glassmorphism**: Components use backdrop-filter blur(10px) with thin borders (1px solid rgba(255, 255, 255, 0.08)).
- **Layering**: Depth via fixed global grain-overlay and localized light-leak gradients rather than box-shadows.
- **Interaction**: Hover triggers subtle translateY(-5px) and increased border opacity.
## Components
- **Cinematic Accordion**: Expanding horizontal/vertical card system where panels expand from compressed state to reveal full-bleed imagery and large serif typography.
- **HUD Explorer**: Floating mobile navigation trigger styled as a "Lens" with pulsing glow and terminal readouts.
- **Slow Marquees**: Continuous horizontal tickers for partner logos and veteran listings.
- **Glass Cards**: Content containers with subtle gradients, rounded corners (2.5rem), and high-contrast iconography.
- **Grain & Flicker**: Global CSS noise filters and holographic flicker animations on UI labels.
## Do's and Don'ts
### Do's
- Use thin subtle borders (white/10) to separate sections rather than solid color changes.
- Maintain high letter-spacing on all Geist Mono labels.
- Use serif italics for emotional or visionary statements.
- Keep imagery desaturated or stylized with dark gradients for readability.
### Don'ts
- Do not use bright solid background colors — the page must remain in "The Void."
- Do not use standard drop shadows — use radial glow or bloom effects instead.
- Do not use sharp high-speed animations — all motion should be fluid and breathing.
```
@@ -0,0 +1,96 @@
# Step 3: Write the Narration Script
**Before writing, re-read DESIGN.md** — specifically the Overview and Components sections. The script should reference real product features, real stats, and real components that the website highlights. Use exact numbers from `extracted/visible-text.txt`.
The script is the backbone. Everything downstream — scene durations, animation timing, beat pacing — comes from the narration. Write it before the storyboard.
Save as `SCRIPT.md` in the project directory.
## 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.
| On the website | 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 from a website capture:
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 website.
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.
@@ -0,0 +1,222 @@
# Step 4: Write the Storyboard
**Before writing anything, fully re-read these files:**
- **DESIGN.md** — your color palette, font rules, components, Do's/Don'ts. Every creative decision must be grounded in this brand identity. If it says "white backgrounds with purple accent" — plan light scenes, not dark moody ones.
- **`extracted/asset-descriptions.md`** — read EVERY line. This is your menu of available visuals. Each line describes what the image actually shows (e.g., "translucent ribbons in orange, pink, and purple on white background" or "a high-speed train under a dark starry sky"). Use these descriptions to decide which assets belong in which beat. Assets you don't understand from the description — view them directly before assigning.
- **[techniques.md](techniques.md)** — 10 visual techniques (SVG path drawing, Canvas 2D art, CSS 3D, per-word typography, Lottie, video compositing, typing effect, variable fonts, MotionPath, velocity transitions). Pick 2-3 per beat and specify them in the storyboard.
The storyboard is the creative north star. It tells the engineer exactly what to build for each beat — mood, camera, animations, transitions, assets, sound. Write it as if you're briefing a motion designer who's never seen the website.
Save as `STORYBOARD.md` in the project directory.
---
## Global Direction
Every STORYBOARD.md starts with global settings:
```markdown
**Format:** 1920×1080
**Audio:** [TTS provider] voiceover + underscore + SFX
**VO direction:** [voice character — e.g., "mid-age male, calm confident delivery,
Apple keynote register — economy of words, silence between sentences is a feature"]
**Style basis:** DESIGN.md (brand colors, fonts, components from the captured site)
```
**Global guardrails** (adapt to the brand):
- Push color presence. Muted is fine, flat is not. Every beat should have at least one color that pulls your eye.
- Motion should be visible and intentional. Err toward more movement than feels safe — subtle reads as static at 30fps.
- Use as many captured assets as the creative vision allows. Scatter framework icons around a dashboard. Layer enterprise photos behind stats. Use product screenshots as floating cards. The assets exist — use them generously.
- Aim for 8-10 visual elements per beat, not 2-3. A great beat has: background texture, midground content, foreground accents, floating decorative elements, animated icons, SVG path drawings, particle effects, typographic details. It should feel DENSE and alive.
- Use at least 2-3 different techniques from techniques.md per beat — not across the whole video, per beat. Don't default to basic fade/scale/opacity — mix in SVG path drawing, CSS 3D transforms, typing effects, counter animations, canvas procedural art. Each beat should feel like its own visual world.
**Underscore/music direction** (if applicable):
- Describe the mood, reference artists, when it swells or drops
- Example: "Minimal electronic. Warm sustained pad already playing when the video starts. Sits underneath everything, never competing with VO. Swells gently during the flex section, drops to near-nothing for the comparison, resolves on a final chord."
---
## Asset Audit
Before writing any beats, audit every captured asset. Print this table:
| Asset | Type | Assign to Beat | Role |
| ------------------------------ | ---------- | -------------- | ------------------------------------- |
| wave-fallback-desktop.png | Hero image | Beat 1 | Full-bleed animated background |
| enterprise-accordion-hertz.png | Photo | Beat 3 | Enterprise credibility, Ken Burns pan |
| stripe-logo.svg | SVG | Beat 1, Beat 5 | Brand mark opener + closer |
| datavizstatic3x.png | Data viz | Beat 3 | Supporting visual behind stats |
| icon-3.svg | Icon | SKIP | Decorative, too small |
**Minimum utilization:**
- At least 50% of product screenshots and hero images must appear
- Brand logo appears in the first AND last beat
- The site's signature visual (gradient wave, hero illustration, key product UI) must appear — it's the most recognizable brand element
- Maximum 2 consecutive text-only beats. The 3rd must contain a visual asset
- Opening beat must contain a visual asset, not text-only
---
## 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:
### 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.
### VO cue
Which narration line plays over this beat.
### Visual description
What the viewer sees — described cinematically, not as CSS specs. Use camera language (pan, zoom, drift, settle). Describe at least 5 visual elements, not just text + background. Think in layers — what's moving in the foreground, midground, background simultaneously?
### 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."
### Assets
Which captured files to use, referenced by filename:
- "Background: `assets/wave-fallback-desktop.png` — full-bleed, slow zoom 1→1.04 over beat duration"
- "Logo: `assets/svgs/stripe-logo.svg` — centered, fades in at 0.5s"
- "Enterprise photo: `assets/enterprise-accordion-hertz.png` — Ken Burns pan, 70% opacity overlay"
### Animation choreography
Specific motion verbs per element — not "it animates in" but HOW:
| Energy | Verbs | Example |
| ------------- | --------------------------------------------- | ------------------------------------- |
| High impact | SLAMS, CRASHES, PUNCHES, STAMPS, SHATTERS | "$1.9T" SLAMS in from left at -5° |
| Medium energy | CASCADE, SLIDES, DROPS, FILLS, DRAWS | Three cards CASCADE in staggered 0.3s |
| Low energy | types on, FLOATS, morphs, COUNTS UP, fades in | Counter COUNTS UP from 0 to 135K |
Every element gets a verb. If you can't name the verb, the element is not yet designed.
### Transition
How this beat hands off to the next. Specify the type and parameters.
**CSS transitions** (choose from `skills/hyperframes/references/transitions/catalog.md`):
- Velocity-matched upward: exit `y:-150, blur:30px, 0.33s power2.in` → entry `y:150→0, blur:30px→0, 1.0s power2.out`
- Whip pan: exit `x:-400, blur:24px, 0.3s power3.in` → entry `x:400→0, blur:24px→0, 0.3s power3.out`
- Blur through: exit `blur:20px, 0.3s` → entry `blur:20px→0, 0.25s power3.out`
- Zoom through: exit `scale:1→1.2, blur:20px, 0.2s power3.in` → entry `scale:0.75→1, blur:20px→0, 0.5s expo.out`
- Hard cut / smash cut (for rapid-fire sequences)
**Shader transitions** (choose from `skills/hyperframes/references/transitions/shader-transitions.md`):
- Cross-Warp Morph (organic, versatile) — 0.5-0.8s, power2.inOut
- Cinematic Zoom (professional momentum) — 0.4-0.6s, power2.inOut
- Gravitational Lens (otherworldly) — 0.6-1.0s, power2.inOut
- Glitch (aggressive, high energy) — 0.3-0.5s
- See `skills/hyperframes/references/transitions/shader-setup.md` for the full WebGL boilerplate
**How velocity-matched CSS transitions work:**
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.
### 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."
---
## Production Architecture
Include this file tree at the bottom of the storyboard:
```
project/
├── index.html root — VO + underscore + beat orchestration
├── DESIGN.md brand reference (from Step 2)
├── SCRIPT.md narration text (from Step 3)
├── STORYBOARD.md THIS FILE — creative north star
├── transcript.json word-level timestamps (from Step 5)
├── narration.wav TTS audio (from Step 5)
├── captures/<name>/ captured website data
└── compositions/
├── beat-1-hook.html
├── beat-2-features.html
├── ...
└── captions.html
```
---
## Example: Beat-by-Beat Format
Here are three beats from a production storyboard showing the level of detail expected.
### BEAT 1 — COLD OPEN (0:000:05)
**VO:** "Your AI agent already knows how to make videos."
**Concept:** We're already in motion when the video starts. No title card, no fade from black. We're mid-flight over an infinite creative workspace — dozens of living compositions scattered below us like a city seen from a drone. Each one is alive, running a different animation. The message is clear before any words: this tool makes videos. Lots of them.
**Visual:** Slow smooth diagonal drift over a vast canvas (3600×2200px plane). Scattered across it: 25 composition cards at organic angles (±5-15° rotation), soft shadows, thin borders. Each card contains a DIFFERENT running animation — kinetic type, gradient morph, data viz, particle system, logo assembly, SVG drawing, shader noise, 3D rotating object. Depth-of-field: close cards slightly blurred, focal sweet-spot in mid-distance, far cards smaller and desaturated.
**Camera:** Diagonal drift top-left to bottom-right, slight 2-3° rotation over 5s. power1.inOut ease. Zoom accelerates in final second as we approach one specific card.
**Assets:** Product screenshots and logo on cards. Each card is a mini-composition with its own animation.
**SFX:** Ambient warmth pad already playing. Faint textured hum — overhearing creative activity from a distance.
---
### BEAT 5 — THE THESIS (0:200:24)
**VO:** "Anything a browser can render can be a frame in your video."
**Mood:** Big statement. This sentence gets its own canvas. Clean, spacious, typographic.
**Visual:** Words appear as staggered kinetic typography. "Anything a browser can render" — distinctive serif, gentle fade + rise (y: 24px → 0, opacity 0 → 1, 0.4s, power2.out). Held beat — one second of stillness. "can be a frame in your video." appears below. As the final word lands, the entire text pulses once — a brief warm flash, subtle scale bump to 101%.
**Transition OUT:** Whip pan left — x:-400, blur:24px, opacity:0.4, 0.3s power3.in
**SFX:** Silence under the first line. On the capture pulse — a soft analog shutter click.
---
### BEAT 7 — THE CONTRAST (0:380:44)
**VO:** "No new framework for the agent to learn. Just HTML."
**Mood:** Clean comparison. Light base. Two worlds side by side.
**Visual:** Left half: dense code, small, compressed, overwhelming. Scrolls slowly upward. Slightly desaturated. Right half: spacious HTML, syntax-highlighted, generous line spacing, inviting. On "Just HTML." — the left side folds inward along its center line, like a book closing. The right side expands to fill the frame. Warm glow rises behind it.
**Transition IN:** Zoom through — scale 0.75→1, blur 20px→0, 0.5s expo.out
**Transition OUT:** Velocity-matched upward — y:-150, blur:30px, 0.33s power2.in
**Assets:** Real framework code on the left (actual content, not lorem ipsum). Real HyperFrames HTML on the right.
**SFX:** Left side carries a faint low drone. On fold: drone cuts. Silence. Then a single clean chime as the right side expands.
@@ -0,0 +1,40 @@
# Step 5: Generate VO + Map Timing
## Audition voices
Never use the first voice you find. Audition 2-3 voices with the first sentence of SCRIPT.md:
- **Kokoro** (try first — free, no API key) — `npx hyperframes tts SCRIPT.md --voice af_nova --output narration.wav`. Runs locally on CPU. Requires Python 3.10+ (macOS system Python 3.9 won't work — if it fails with an onnxruntime error, move to the next option).
- **ElevenLabs** (best voice quality, widest selection) — `mcp__elevenlabs__search_voices` to browse, `mcp__elevenlabs__text_to_speech` to generate. Does not return timestamps — transcribe separately after.
- **HeyGen TTS** (returns word timestamps automatically — saves a transcribe step) — `mcp__claude_ai_HeyGen__text_to_speech`. Use when you want timestamps without a separate transcription pass.
Pick the voice that sounds most natural and conversational. Listen for pacing — does it breathe between sentences? Does it sound like a person or a robot?
## Generate full narration
Generate the full script as `narration.wav` (or `.mp3`) in the project directory.
## Transcribe for word-level timestamps
```bash
npx hyperframes transcribe narration.wav
```
Produces `transcript.json` with `[{ text, start, end }]` for every word. These timestamps are the source of truth for all beat durations.
## Map timestamps to beats
Go through STORYBOARD.md beat by beat. For each beat:
1. Find the first word of that beat's VO cue in `transcript.json`
2. Find the last word of that beat's VO cue
3. Set `beat.start = firstWord.start`, `beat.end = lastWord.end`
4. Add 0.3-0.5s padding at the end for visual breathing room
Update STORYBOARD.md with real durations. Replace estimated times (e.g., "0:00-0:05") with actual timestamps (e.g., "0.00-3.21s").
Beat boundaries land on word onsets — hard cuts to the VO.
## Update index.html
Update each scene slot's `data-start` and `data-duration` to match the real beat timings from the transcript. Also update the total composition duration and audio element duration.
@@ -0,0 +1,162 @@
# Step 6: Build Compositions
**Before building, fully re-read these files:**
- **DESIGN.md** — your color palette, fonts, components, and Do's/Don'ts. Every composition must use EXACT hex colors and font families from this file. If it says "white backgrounds" — use white, not dark.
- **STORYBOARD.md** — the beat-by-beat plan you're executing. Each beat specifies assets, animations, transitions, and which techniques to use.
- **`extracted/asset-descriptions.md`** — when the storyboard assigns an asset to a beat, re-read the description to understand what it shows and how to position/style it correctly.
- **[techniques.md](techniques.md)** — code patterns for the 10 visual techniques. When the storyboard says "SVG path drawing" or "per-word kinetic typography" — read the code pattern from this file and adapt it.
- **transcript.json** — word-level timestamps that drive scene durations.
**Split the work: spawn a sub-agent for each beat.** By this step your context is full of captured data, DESIGN.md, SCRIPT, STORYBOARD, and transcript. Building compositions on top of all that means the detailed rules below compete with thousands of tokens of prior work. Each sub-agent gets a fresh context focused on one beat — dramatically better output.
**How to dispatch each sub-agent:**
Pass file PATHS, not file contents. The #1 failure mode is reading an asset file and pasting its SVG/image data into the sub-agent prompt. The sub-agent then uses inline content instead of referencing the file on disk. Same with fonts — pass the local woff2 path, don't substitute Google Fonts.
```
Build the composition for beat 1. Save to compositions/beat-1-hook.html.
STORYBOARD for this beat:
[paste the beat section from STORYBOARD.md]
ASSETS — reference by path, do NOT read/inline the file contents:
- Logo: <img src="../assets/favicon.svg"> (top-left, 40x40px)
- Hero image: <img src="../assets/hero-bg.png"> (full-bleed background)
- Noise texture: ../assets/noise.png (full-frame overlay, 3% opacity)
FONTS — use @font-face with the captured font files, NOT Google Fonts:
@font-face { font-family: 'BrandFont'; src: url('../assets/fonts/BrandFont-Regular.woff2'); }
Read DESIGN.md for exact colors and Do's/Don'ts.
Read techniques.md for animation code patterns.
Invoke /hyperframes for composition structure rules.
```
After each sub-agent finishes, verify the composition references `../assets/` — if it used inline SVGs or Google Fonts instead of the captured files, fix it before moving on.
Invoke the `/hyperframes` skill first — it has the rules for data attributes, timeline contracts, deterministic rendering, and layout. Everything below supplements those rules, not replaces them.
---
## Per-Composition Process
For each beat in the storyboard:
### 1. Read the beat's storyboard section
Know the mood, visual description, assets, animation choreography, transition, and SFX before writing any HTML.
### 2. Build the static end-state first
Position every element where it should be at its **most visible moment** — the frame where everything is fully entered and correctly placed. Write this as static HTML+CSS. No GSAP yet.
This is the "Layout Before Animation" principle from the compose skill. The CSS position is the ground truth. Animations describe the journey to and from it.
### 3. Verify the static layout
Look at it. Check:
- Are elements where the storyboard says they should be?
- Are depth layers present (foreground / midground / background)?
- Do any elements overlap unintentionally?
- Are assets sized correctly? (hero images should fill 50-70% of frame, not sit at 100x100px)
### 4. Add entrance animations
Use `gsap.from()` — animate FROM offscreen/invisible TO the CSS position. The CSS position is where the element ends up.
### 5. Add mid-scene activity
Every visible element must have continuous motion. A still image on a still background is a JPEG with a progress bar.
| Element type | Mid-scene activity |
| ---------------------- | ------------------------------------------------ |
| Image / screenshot | Slow zoom (scale 1→1.03), slow pan, or Ken Burns |
| Stat / number | Counter animates from 0 to target |
| Logo grid | Subtle shimmer sweep, or gentle scale pulse |
| Any persistent element | Subtle float (y ±4-6px, sine.inOut, yoyo) |
### 6. Add exit / transition
Check the storyboard's transition specification for this beat:
- **CSS transition**: implement the exit animation (e.g., `y:-150, blur:30px, 0.33s power2.in`). The next composition handles its own entry.
- **Shader transition**: no exit animation needed — the shader handles the blend. Read `skills/hyperframes/references/transitions/shader-setup.md` for the full WebGL boilerplate and `skills/hyperframes/references/transitions/shader-transitions.md` for the fragment shader. Copy the FULL boilerplate — a simplified version produces black screens.
- **Hard cut**: no exit animation. The scene simply ends.
For all CSS transition types and their GSAP implementations, read `skills/hyperframes/references/transitions/catalog.md`.
### 7. Asset cross-reference
Before self-review, verify you actually used the assets you planned to:
1. Open STORYBOARD.md and find this beat's asset assignments
2. List every asset that was assigned to this beat
3. Search the composition HTML for each filename (e.g., grep for "wave-fallback-desktop")
4. If any assigned asset is missing from the HTML, add it now
5. Check for the inline anti-pattern: if the HTML contains `<svg xmlns=` or `data:image/` but no `../assets/` references, the assets were inlined instead of referenced. Replace inline content with `<img src="../assets/filename.svg">`
6. Check fonts: if the HTML uses `fonts.googleapis.com` but there are captured fonts in `assets/fonts/`, replace with `@font-face` pointing to the local files
This step catches the two most common failures: compositions ending up text-only, and assets being inlined instead of file-referenced.
### 8. Self-review
After building the composition, check WITH ACTUAL CODE:
- [ ] Asset cross-reference passed (step 7 above — every assigned asset is in the HTML)
- [ ] Elements are where the storyboard says they should be (no misplacement)
- [ ] No overlapping text (text covering text is always ugly)
- [ ] Depth layers present (2+ layers minimum)
- [ ] Every visible element has mid-scene activity (not just entrance + exit)
- [ ] Font sizes above minimum (20px body text, 16px labels — sub-14px is unreadable after encoding)
- [ ] No full-screen dark linear gradients (H.264 creates visible banding — use solid + localized radial glows)
- [ ] Timeline registered: `window.__timelines["comp-id"] = tl`
- [ ] Colors match DESIGN.md exactly (paste the HEX value, don't approximate)
**If `skills/hyperframes-animation-map/` is installed**, run it:
```bash
node skills/hyperframes-animation-map/scripts/animation-map.mjs <composition-dir>
```
Read the summaries. Fix every flag: offscreen, collision, invisible, pacing issues.
### 9. Move to the next composition
---
## Asset Presentation
Never embed a raw flat image. Every image must have motion treatment:
- **Perspective tilt**: use `gsap.set(el, { transformPerspective: 1200, rotationY: -8 })` + `box-shadow` — creates depth. Do NOT use CSS `transform: perspective(...)` as GSAP will overwrite it.
- **Slow zoom (Ken Burns)**: GSAP `scale: 1``1.04` over beat duration — makes photos cinematic
- **Device frame**: Wrap in a laptop/phone shape using CSS `border-radius` and `box-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 `y` position
---
## Audio Wiring
In the root `index.html`:
- **Narration**: `<audio id="narration" src="narration.wav" data-start="0" data-duration="..." data-track-index="0" data-volume="1">`
- **Underscore/music** (if storyboard specifies): `<audio id="underscore" src="underscore.mp3" data-start="0" data-duration="..." data-track-index="3" data-volume="0.15">`
- **SFX** (if storyboard specifies): individual `<audio>` elements at specific `data-start` timestamps
- **Captions** (optional — only if user requests): sub-composition on a parallel track. Skip unless explicitly asked for.
---
## Critical Rules
These exist because the capture engine is deterministic. Violations produce broken output.
- **No `repeat: -1`** — calculate exact repeats from beat duration
- **No `Math.random()`** — use a seeded PRNG (mulberry32)
- **Register every timeline**: `window.__timelines["comp-id"] = tl`
- **Synchronous timeline construction** — no async/await wrapping timeline code
- **Never use ANY CSS `transform` for centering** — not `translate(-50%, -50%)`, not `translateX(-50%)`, not `translateY(-50%)`. GSAP animates the `transform` property, which overwrites ALL CSS transforms including centering. The element flies offscreen. Use flexbox centering instead: `display:flex; align-items:center; justify-content:center` on a wrapper div. The linter catches this (`gsap_css_transform_conflict`) but only if you run it.
- **Minimum font sizes**: 20px body, 16px labels
- **No full-screen dark linear gradients** — H.264 banding
@@ -0,0 +1,109 @@
# Step 7: Validate & Deliver
## Lint + Validate
Run in sequence. Fix all errors before proceeding to the next command.
```bash
npx hyperframes lint
npx hyperframes validate
```
`lint` checks HTML structure statically — missing attributes, timeline registration, tween conflicts, CSS transform + GSAP conflicts (including inline styles).
`validate` loads the composition in headless Chrome and catches runtime JS errors, missing assets, and failed network requests.
## Visual Verification (snapshot)
After lint and validate pass, capture snapshot frames to SEE your own output:
```bash
npx hyperframes snapshot <project-dir> --at <beat-midpoints>
```
If the snapshot command isn't available, fall back to:
```bash
npx tsx packages/cli/src/cli.ts snapshot <project-dir> --at <beat-midpoints>
```
Calculate the midpoint of each beat from your STORYBOARD.md timings. For a 4-beat video with beats at 0-5.8s, 5.8-15.0s, 15.0-22.5s, 22.5-25.3s:
```bash
npx hyperframes snapshot <project-dir> --at 2.9,10.4,18.7,23.9
```
This renders one frame per beat at the moment when content is most visible. Use timestamps where the most content is on screen — usually 60-70% into each beat, after entrances finish but before exits start.
**View every snapshot image carefully.** Don't glance and move on. For each frame, check:
**Visibility:**
- Is there visible content? All-white or all-black frames mean compositions aren't rendering.
- Can you read ALL text? White text on white/light background is invisible. Dark text on dark background is invisible. Every text element needs contrast against what's directly behind it.
- Are images and assets showing? Empty space where an image should be means a path issue or missing file.
**Positioning and layout:**
- Do background images fill the entire frame? If an image only covers half the screen, the `object-fit`, `width`, `height`, or position values are wrong.
- Are elements where the storyboard says they should be? Compare the snapshot to the beat description.
- Is there too much empty/dead space? If more than 40% of the frame is a flat solid color with nothing on it, the composition is sparse.
- Are elements overlapping incorrectly? Text over text, or content bleeding off the edges?
**Visual quality:**
- Are overlays too heavy? If a background image is barely visible through a dark overlay, reduce the overlay opacity.
- Is the visual hierarchy clear? One dominant element per frame, supporting elements secondary.
- Do the colors match DESIGN.md? Check actual rendered colors against what was planned.
**Code vs. rendered verification:**
- For each beat, check: does the snapshot show the assets you referenced in the HTML? If a composition has `<img src="...wave.png">` but the snapshot shows no wave — the image isn't loading, the path is wrong, or it's hidden behind another element.
- If a snapshot shows nothing at a timestamp, try a slightly different time (1-2 seconds later). Compositions may still be in entrance animations.
- The snapshot command is fast — run it multiple times at different timestamps if needed.
If any frame has issues, go back to Step 6 and fix that composition before proceeding.
## Preview
```bash
npx hyperframes preview
```
Open the studio in a browser. Scrub through every beat.
## Create HANDOFF.md
Write a `HANDOFF.md` for multi-session continuity:
```markdown
# Handoff — [Project Name]
**Date:** [today]
**Preview:** `npx hyperframes preview`
## What's Built
| Beat | File | Dur | Status | Notes |
| ---- | -------------------- | ---- | ------ | ----- |
| 1 | beat-1-hook.html | 5.2s | Built | ... |
| 2 | beat-2-features.html | 6.8s | Built | ... |
## Audio
| Asset | Status | Notes |
| --------------- | ------ | ------------------------------------ |
| narration.wav | Done | [provider], [voice name], [duration] |
| transcript.json | Done | [word count] words, [duration] |
## What Needs Work
- [any known issues, polish requests, missing SFX]
## Commands
npx hyperframes preview
npx hyperframes lint
npx hyperframes validate
npx hyperframes snapshot <project-dir> --at <beat-midpoints>
npx hyperframes render --output renders/final.mp4
```
@@ -0,0 +1,341 @@
# Visual Techniques Reference
10 proven techniques from production HyperFrames videos. Use these in your storyboard and compositions to create visually rich, professional output. Each technique includes a minimal code pattern you can adapt.
These are NOT advanced — they're standard motion design patterns that every composition should use at least 2-3 of.
---
## 1. SVG Path Drawing
A path draws itself in real-time, like someone tracing with a pen. Use for revealing diagrams, arrows, connector lines, or brand marks.
```html
<svg viewBox="0 0 400 200">
<path
class="draw-path"
d="M 50 100 L 200 50 L 350 100"
stroke="#c84f1c"
stroke-width="4"
fill="none"
stroke-linecap="round"
/>
</svg>
<style>
.draw-path {
stroke-dasharray: 280;
stroke-dashoffset: 280;
}
</style>
<script>
tl.to(".draw-path", { strokeDashoffset: 0, duration: 0.7, ease: "power2.out" }, 0.5);
</script>
```
Use `path.getTotalLength()` to calculate the dasharray value dynamically.
---
## 2. Canvas 2D Procedural Art
Animated noise, particle fields, data visualizations — anything that evolves frame-by-frame. Drive it with a GSAP proxy.
```html
<canvas id="proc-canvas" width="1920" height="1080"></canvas>
<script>
var canvas = document.getElementById("proc-canvas");
var ctx = canvas.getContext("2d");
function hash(x, y) {
var n = x * 374761393 + y * 668265263;
n = (n ^ (n >> 13)) * 1274126177;
return ((n ^ (n >> 16)) & 0x7fffffff) / 0x7fffffff;
}
function drawFrame(t) {
ctx.fillStyle = "#0a0a0a";
ctx.fillRect(0, 0, 1920, 1080);
for (var i = 0; i < 200; i++) {
var x = hash(i, 0) * 1920;
var y = hash(i, 1) * 1080;
var brightness = hash(i, Math.floor(t * 10)) * 255;
ctx.fillStyle = "rgba(255, 255, 255, " + brightness / 255 + ")";
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fill();
}
}
var proxy = { time: 0 };
tl.to(
proxy,
{
time: 5,
duration: 5,
ease: "none",
onUpdate: function () {
drawFrame(proxy.time);
},
},
0,
);
</script>
```
The `hash()` function is deterministic — same frame renders identically every time.
---
## 3. CSS 3D Transforms
Perspective rotations create depth. Use for product showcases, card flips, architectural reveals.
```html
<div class="stage" style="perspective: 900px;">
<div class="card-3d" style="transform-style: preserve-3d;">
<div class="face front">Product</div>
<div class="face back" style="transform: rotateY(180deg);">Details</div>
</div>
</div>
<script>
tl.to(".card-3d", { rotationY: 360, rotationX: 15, duration: 1.2, ease: "sine.inOut" }, 0);
</script>
```
Always set `perspective` on the parent, `transform-style: preserve-3d` on the animated element.
---
## 4. Per-Word Kinetic Typography
Words appear one-by-one, synced to transcript.json timestamps. The core technique for narration-driven videos.
```html
<div class="headline">
<span class="word w-0">Anything</span>
<span class="word w-1">a</span>
<span class="word w-2">browser</span>
<span class="word w-3">can</span>
<span class="word w-4">render</span>
</div>
<style>
.word {
display: inline-block;
opacity: 0;
margin: 0 0.12em;
}
</style>
<script>
// Word onset times from transcript.json (seconds relative to beat start)
var timings = [0.0, 0.23, 0.28, 0.63, 0.78];
var slides = [80, 60, 50, 25, 12]; // horizontal slide decay (px)
document.querySelectorAll(".word").forEach(function (word, i) {
tl.from(
word,
{
x: slides[i],
y: 14,
opacity: 0,
duration: 0.35,
ease: "power2.out",
},
timings[i],
);
});
</script>
```
The slide distance DECAYS per word (80→12px) — mimics a camera settling.
---
## 5. Lottie Animation
Vector animations that play inside a composition. Use for logos, character animations, icons.
```html
<script src="https://cdn.jsdelivr.net/npm/@dotlottie/player-component@2.7.12/dist/dotlottie-player.js"></script>
<dotlottie-player
class="lottie"
src="../assets/lottie/animation-0.json"
autoplay
loop
speed="1.5"
style="width:500px;height:500px;"
>
</dotlottie-player>
<script>
gsap.set(".lottie", { scale: 0.3, opacity: 0 });
tl.to(".lottie", { scale: 1, opacity: 1, duration: 0.35, ease: "back.out(1.6)" }, 0.2);
</script>
```
Or use lottie-web for more control:
```javascript
var anim = lottie.loadAnimation({
container: document.getElementById("anim"),
renderer: "svg",
loop: false,
autoplay: false,
path: "../assets/lottie/animation-0.json",
});
```
---
## 6. Video Compositing
Embed real video footage inside compositions. Videos must be `muted` with `playsinline`.
```html
<div class="video-frame" style="width:680px;height:840px;border-radius:16px;overflow:hidden;">
<video
id="footage"
src="../assets/videos/clip.mp4"
muted
playsinline
style="width:100%;height:100%;object-fit:cover;"
></video>
</div>
<script>
// Video playback is controlled by the framework — don't call play() manually
tl.from(".video-frame", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.out" }, 0);
</script>
```
The HyperFrames runtime handles video seeking and playback.
---
## 7. Character-by-Character Typing
Terminal typing effect using `tl.call()` to update text content character by character.
```html
<div class="terminal-line">
<span class="prompt"></span>
<span class="typed" id="typed-text"></span>
<span class="cursor" style="width:11px;height:22px;background:#333;display:inline-block;"></span>
</div>
<script>
var CMD = "npx hyperframes init";
var typed = document.getElementById("typed-text");
// Cursor blinks
tl.to(".cursor", { opacity: 0, duration: 0.12, yoyo: true, repeat: 20, ease: "steps(1)" }, 0);
// Type each character
for (var i = 0; i < CMD.length; i++) {
(function (idx) {
tl.call(
function () {
typed.textContent = CMD.substring(0, idx + 1);
},
null,
(idx / CMD.length) * 0.9,
);
})(i);
}
</script>
```
Use `ease: "steps(1)"` for cursor blink — creates discrete on/off.
---
## 8. Variable Font Axis Animation
Animate font-variation-settings to reshape glyphs in real-time. Works with variable fonts that have axes like optical size (opsz), weight (wght), softness (SOFT).
```html
<style>
@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,100..900&display=block");
.wordmark {
--opsz: 144;
--wght: 440;
font-family: "Fraunces", serif;
font-variation-settings:
"opsz" var(--opsz),
"wght" var(--wght);
font-size: 200px;
}
</style>
<script>
tl.to(".wordmark", { "--opsz": 72, "--wght": 300, duration: 0.45, ease: "power2.out" }, 0);
</script>
```
The glyph subtly reshapes as axes animate — optical size adjusts detail, weight changes thickness.
---
## 9. GSAP MotionPathPlugin
Animate an element along an arbitrary SVG path. Use for sliders following curves, particles along trajectories, guided reveals.
```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js"></script>
<div class="dot" style="width:20px;height:20px;background:#2a8a7c;border-radius:50%;"></div>
<script>
gsap.registerPlugin(MotionPathPlugin);
tl.to(
".dot",
{
motionPath: { path: "M 12 300 C 280 280 520 80 820 50 S 1200 48 1308 38" },
duration: 1.5,
ease: "power2.out",
},
0,
);
</script>
```
---
## 10. Velocity-Matched Transitions
Exit one beat and enter the next with matched velocities — creates perceived continuous motion.
```javascript
// EXIT (in outgoing composition): accelerating with blur
tl.to(
".content",
{
y: -150,
filter: "blur(30px)",
opacity: 0,
duration: 0.33,
ease: "power2.in", // accelerates
},
beatDuration - 0.33,
);
// ENTRY (in incoming composition): decelerating from blur
gsap.set(".content", { y: 150, filter: "blur(30px)" });
tl.to(
".content",
{
y: 0,
filter: "blur(0px)",
duration: 1.0,
ease: "power2.out", // decelerates
},
0,
);
```
The fastest point of both curves meets at the cut — the viewer perceives smooth camera motion. Match ease families: `.in` for exits, `.out` for entries.
---
## When to Use What
| Video energy | Techniques to combine |
| ------------------------------ | --------------------------------------------------------------- |
| High impact (launches, promos) | Per-word typography + velocity transitions + counter animations |
| Cinematic (tours, stories) | SVG path drawing + video compositing + 3D transforms |
| Technical (dev tools, APIs) | Character typing + Canvas 2D procedural + MotionPath |
| Premium (luxury, enterprise) | Variable font animation + Lottie + slow velocity transitions |
| Data-driven (stats, metrics) | Canvas 2D procedural + counter animations + SVG path drawing |
+56 -22
View File
@@ -114,6 +114,22 @@ The agent must ask the user before changing any major production choice, includi
Minor prompt refinements inside an already approved provider/model path do not require separate approval unless they materially change the creative direction.
### Present Both Composition Runtimes (HARD RULE)
When both Remotion and HyperFrames are available on the machine (check `video_compose.get_info()["render_engines"]`), the agent **MUST present both options to the user** before locking `render_runtime` at the proposal stage. The agent MAY recommend one with rationale — but silently picking a "default" is forbidden even when the pipeline manifest or a director skill suggests one.
The presentation MUST include, for each runtime:
1. A one-sentence plain-language description of what it is best at for **this specific brief**.
2. A one-sentence honest tradeoff (why it might not be the right pick here).
3. The agent's recommendation and the reason, tied to the brief's delivery_promise and visual approach.
Then wait for explicit user approval before advancing. Record the full shortlist — BOTH runtimes plus any "ffmpeg" option that applies — as `options_considered` in the `render_runtime_selection` decision logged in `decision_log`. A decision log entry with only one runtime considered when both were available is a CRITICAL reviewer finding.
Exception: if only one runtime is available on the machine, the agent proceeds with it but MUST say so explicitly ("HyperFrames isn't installed on this machine; I'm proceeding with Remotion. Install HyperFrames if you want the alternative."). The `render_runtime_selection` decision still records the unavailable option as `rejected_because: "runtime not available on this machine"`.
This rule applies to every pipeline that invokes `video_compose` — not just Wave 1. A pipeline's director skill may recommend a runtime, but that recommendation is input to the conversation with the user, not a decision.
### Escalate Blockers Explicitly
When a blocker occurs, the agent must surface it immediately using this structure:
@@ -223,9 +239,31 @@ If the folder has tracks, the proposal and asset stages should present them as o
## Mandatory Preflight
Do this before any creative work:
Do this before any creative work. **Use `provider_menu_summary()` first — it's the human-ready rollup.** The raw `support_envelope()` dump is a firehose (megabytes of JSON on a well-configured machine); pasting it into chat will bury the user.
```bash
python -c "
from tools.tool_registry import registry
import json
registry.discover()
print(json.dumps(registry.provider_menu_summary(), indent=2))
"
```
The summary returns four fields the agent should translate into plain language:
- `composition_runtimes` — booleans for `ffmpeg`, `remotion`, `hyperframes`. This is the source of truth for the "Present Both Composition Runtimes (HARD RULE)" check.
- `capabilities[]` — one entry per capability family with `configured / total` counts and provider lists. Ready-made for the "N of M configured" menu.
- `setup_offers[]` — unavailable tools whose install is a 1-minute env-var fix. Lead with these when offering upgrades.
- `runtime_warnings[]` — specific signals like "hyperframes: npm package not resolvable". Surface these to the user verbatim — they're the kind of silent-failure bugs that break the governance contract.
Then, for deeper inspection (only when the summary isn't enough):
```bash
# Full menu — grouped available/unavailable per capability.
python -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.provider_menu(), indent=2))"
# Raw envelope — every tool's full contract. Slow/firehose; use for debugging only.
python -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.support_envelope(), indent=2))"
```
@@ -239,18 +277,7 @@ Then:
### Provider Menu (Mandatory at Preflight)
After running `support_envelope()`, run the **provider menu** to see the full picture:
```bash
python -c "
from tools.tool_registry import registry
import json
registry.discover()
print(json.dumps(registry.provider_menu(), indent=2))
"
```
This returns every capability grouped by status — how many providers the user has configured vs. how many exist. **Present this to the user as a capability menu**, not as a flat tool list.
Already fetched via `provider_menu_summary()` above. Read that output and **present it to the user as a capability menu**, not as a flat tool list. Use `provider_menu()` directly only when you need the per-tool detail the summary collapses.
**How to present:**
@@ -315,9 +342,9 @@ When tools are `UNAVAILABLE` but can be fixed with simple configuration, **offer
- If the user declines setup, proceed with the best available path — no nagging
- Group related fixes (tools sharing the same env var dependency)
### Remotion Rendering (Inside video_compose)
### Composition Runtimes (Inside video_compose)
`video_compose` has two render engines. Check which are available:
`video_compose` has **three** render engines / runtimes. They are parallel, not ranked — the choice is made at proposal and locked in `edit_decisions.render_runtime`. Check which are available:
```bash
python -c "
@@ -326,13 +353,17 @@ registry.discover()
info = registry._tools['video_compose'].get_info()
print('Render engines:', info.get('render_engines'))
print('Remotion note:', info.get('remotion_note'))
print('HyperFrames note:', info.get('hyperframes_note'))
"
```
| Engine | Used For | Requires |
|--------|----------|----------|
| **FFmpeg** | Video-only cuts, concat, trim, subtitle burn | `ffmpeg` binary (always available) |
| **Remotion** | Still images -> animated video, text cards, stat cards, charts, callouts, comparisons, transitions with spring physics | Node.js (`npx`) + `remotion-composer/` project |
| **Remotion** | React-based composition: still images animated video, text cards, stat cards, charts, callouts, comparisons, transitions with spring physics, word-level caption burn, TalkingHead avatar | Node.js (`npx`) + `remotion-composer/` + `node_modules` |
| **HyperFrames** | HTML/CSS/GSAP composition: kinetic typography, product promos, launch reels, website-to-video, registry-block-driven scenes | Node.js ≥ 22 + FFmpeg + `npx` (consumed via `npx @hyperframes/cli`) |
`render_runtime` is **locked at proposal** (`proposal_packet.production_plan.render_runtime`) and **carried through edit_decisions unchanged**. `video_compose` routes based on this field; silent runtime swaps are forbidden. If the chosen runtime becomes unavailable at compose time, surface a structured blocker per "Escalate Blockers Explicitly" above. See `skills/core/hyperframes.md` for the Remotion-vs-HyperFrames decision matrix.
### Critical Rule: Motion-Required Requests
@@ -346,10 +377,11 @@ For any request where the deliverable inherently depends on motion rather than s
For these requests:
- `Remotion` availability must be confirmed up front if the planned visual treatment depends on Remotion rendering.
- The `render_runtime` chosen at proposal (Remotion, HyperFrames, or FFmpeg) must be confirmed available up front if the planned visual treatment depends on it.
- Still-image fallback is forbidden. Do not quietly convert the job into a Ken Burns teaser, animatic, or slide-based video.
- FFmpeg-only fallback is forbidden when it changes the approved deliverable from motion-led video to still-led video.
- Bubble critical issues immediately. If Remotion is unavailable, fails to render, or provider clip generation fails in a way that blocks the approved treatment, stop and tell the user before proceeding.
- **Silent runtime swap is forbidden.** If `render_runtime="hyperframes"` was locked and HyperFrames is unavailable, do NOT route to Remotion instead. Surface the blocker, propose options, get user approval, log a `render_runtime_selection` decision — then proceed.
- Bubble critical issues immediately. If the chosen runtime is unavailable, fails to render, or provider clip generation fails in a way that blocks the approved treatment, stop and tell the user before proceeding.
- Do not spend more tokens or time on downgraded output unless the user explicitly approves the downgrade as an animatic or proof-of-concept.
**When Remotion is available**, the agent should design production plans around it:
@@ -363,11 +395,11 @@ For these requests:
See `remotion-composer/SCENE_TYPES.md` for the authoritative list and their cut schemas. Current scene types usable via `cut.type`:
`text_card`, `stat_card`, `callout`, `comparison`, `hero_title`, `terminal_scene`, `anime_scene`, `bar_chart`, `line_chart`, `pie_chart`, `kpi_grid`, `progress_bar`. Overlay types include `section_title`, `stat_reveal`, `hero_title`, `provider_chip`.
**When Remotion is NOT available**, `video_compose` falls back to FFmpeg Ken Burns motion on still images. This still works but produces less engaging visuals. Mention this tradeoff in the proposal.
**When Remotion is NOT available** and `render_runtime="remotion"` was NOT locked, `video_compose` may use FFmpeg Ken Burns motion on still images. This still works but produces less engaging visuals. Mention this tradeoff in the proposal. When `render_runtime="remotion"` IS locked and Remotion is unavailable, that's a blocker — escalate, don't silently swap.
That fallback is only acceptable when it does not violate the approved delivery shape. If the user asked for a motion-led trailer or comparable clip-driven piece, the project is blocked until Remotion is working or the user explicitly approves a lower-fidelity alternative.
When `render_runtime="hyperframes"` is locked and HyperFrames is unavailable (Node < 22, missing `ffmpeg`/`npx`, or `hyperframes doctor` reports issues), that's also a blocker. Do not substitute Remotion or FFmpeg without user approval + a logged `render_runtime_selection` decision.
The routing is automatic — the `render` operation in `video_compose` calls `_needs_remotion()` and routes accordingly. But the **agent must know Remotion exists at proposal time** so it can design the visual approach to take advantage of it (animated text cards, component scenes, spring transitions) rather than designing around static images.
Routing is automatic — `video_compose` reads `edit_decisions.render_runtime` and dispatches to the matching engine (`_render_via_hyperframes`, `_remotion_render`, or `_render_via_ffmpeg`). But the **agent must know both Remotion and HyperFrames exist at proposal time** so it can design the visual approach intentionally. Don't default to Remotion for motion-graphics-heavy concepts that HTML/GSAP would express more naturally, and don't default to HyperFrames for briefs that reuse the existing React scene stack.
## Capability Discovery
@@ -589,7 +621,9 @@ Reading order:
2. relevant pipeline or creative skill (Layer 2) — know HOW to use it in this context
3. underlying vendor skill (Layer 3) — **mandatory before calling any generation tool**
**NEVER read tool source code (`tools/*.py`) to understand how to use a tool.** Skills exist precisely so you don't need implementation details. Layer 2 tells you *what* and *when*. Layer 3 tells you *how*. If you're reading `.py` files to figure out input schemas or provider options, you're doing it wrong — that information belongs in the skill layer.
**Prefer skills over source code for tool usage.** Skills exist precisely so you don't need implementation details in the common case. Layer 2 tells you *what* and *when*. Layer 3 tells you *how*. For authoring prompts, choosing parameters, or understanding usage patterns, you should be reading skills — not `.py` files.
**Exception: debugging, audits, and verifying the governance contract.** When a skill and a tool disagree, or when something behaves differently than the skill claims, reading the tool source is fair game — that's often the only way to catch a silent-availability bug or a stale doc string. An audit that refuses to look at the implementation will miss exactly the bugs that matter most. If you do read source to debug, consider whether the finding belongs in a skill update afterward so the next agent doesn't need to repeat the dive.
**Layer 3 is not optional.** Every generation tool (video, image, TTS, music) has an `agent_skills` field listing its Layer 3 skills. These skills contain provider-specific prompt engineering, parameter tuning, and quality techniques. Read them before writing prompts. The difference between a generic prompt and a skill-informed prompt is the difference between "usable" and "cinematic."
+19 -1
View File
@@ -1,4 +1,4 @@
.PHONY: setup install install-dev install-gpu test test-contracts lint clean preflight demo demo-list
.PHONY: setup install install-dev install-gpu test test-contracts lint clean preflight demo demo-list hyperframes-doctor hyperframes-warm
# ---- One-command setup ----
@@ -12,11 +12,19 @@ setup:
@echo "==> Installing free offline TTS (Piper)..."
pip install piper-tts || echo " [skip] piper-tts install failed — TTS will use cloud providers instead"
@echo ""
@echo "==> Installing HyperFrames runtime (cache-warm via npx)..."
@echo " Pulls the 'hyperframes' npm package into the local npx cache so the"
@echo " first render doesn't pay a 30-60s cold-fetch penalty. ~20MB of disk."
@npx --yes hyperframes --version >/dev/null 2>&1 && echo " HyperFrames CLI cached (npx)" || echo " [skip] HyperFrames cache-warm failed — offline or npm unavailable; first render will fetch on demand"
@python -c "from tools.video.hyperframes_compose import HyperFramesCompose; HyperFramesCompose._npm_resolve_cache=None; c=HyperFramesCompose()._runtime_check(); print(f' HyperFrames runtime_available={c[\"runtime_available\"]}, npm={c.get(\"npm_package_version\") or c.get(\"npm_resolve_error\")}'); [print(f' note: {r}') for r in c['reasons']]" || echo " [skip] HyperFrames check failed — runtime can be set up later"
@echo ""
python -c "import shutil, os; e=os.path.exists('.env'); shutil.copy('.env.example','.env') if not e else None; print('==> Created .env from .env.example — add your API keys there.' if not e else '==> .env already exists — skipping.')"
@echo ""
@echo "Done! Open this project in your AI coding assistant and start creating."
@echo " Optional: add API keys to .env to unlock cloud providers."
@echo " Optional: run 'make install-gpu' if you have an NVIDIA GPU."
@echo " Optional: run 'make hyperframes-doctor' to fully validate the HyperFrames runtime."
@echo " Optional: run 'make hyperframes-warm' anytime to refresh the npx cache to the latest hyperframes version."
# ---- Individual installs ----
@@ -43,6 +51,16 @@ test-contracts:
preflight:
python -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.provider_menu(), indent=2))"
hyperframes-doctor:
@echo "==> Probing HyperFrames runtime (node/ffmpeg/npx + hyperframes doctor)..."
python -c "from tools.video.hyperframes_compose import HyperFramesCompose; r=HyperFramesCompose().execute({'operation':'doctor'}); import json; print(json.dumps(r.data, indent=2)); print('OK' if r.success else f'FAIL: {r.error}')"
hyperframes-warm:
@echo "==> Refreshing the HyperFrames npx cache to latest..."
@echo " Uses --prefer-online so npx picks up new releases since your last run."
npx --yes --prefer-online hyperframes --version
@echo "==> Cache warm complete."
demo:
@echo "==> Rendering zero-key demo videos (no API keys needed)..."
@echo " These use only Remotion components — animated charts, text, data viz."
+5
View File
@@ -70,7 +70,12 @@ Each tool's `agent_skills[]` field bridges Layer 1 → Layer 3. See `skills/INDE
| `tools/tool_registry.py` | Tool discovery and reporting |
| `tools/cost_tracker.py` | Budget governance |
| `tools/video/video_stitch.py` | Multi-clip assembly (stitch, spatial, validate, preview) |
| `tools/video/video_compose.py` | Runtime-aware composition orchestrator — routes to Remotion / HyperFrames / FFmpeg based on `edit_decisions.render_runtime` |
| `tools/video/hyperframes_compose.py` | HyperFrames runtime — workspace materialization, `hyperframes lint`/`validate`/`render`, FFmpeg floor check |
| `lib/hyperframes_style_bridge.py` | Playbook → CSS custom properties + `DESIGN.md` bridge for HyperFrames workspaces |
| `remotion-composer/src/components/` | 8 Remotion components (TextCard, StatCard, ProgressBar, CalloutBox, ComparisonCard + charts/) |
| `.agents/skills/hyperframes*/` | Vendored HyperFrames Layer 3 skills (authoring contract, CLI, registry, website-to-video) |
| `skills/core/hyperframes.md` | Layer 2 — when OpenMontage should pick HyperFrames vs Remotion, artifact → workspace mapping |
| `schemas/styles/playbook.schema.json` | Playbook schema v2 with design tokens (chart_palette, scale_system, weight_matrix, color_rules) |
| `tests/qa/` | Quality validation test scripts for tool-by-tool output inspection |
+32
View File
@@ -126,6 +126,38 @@ These use the **Animation pipeline** with `image_animation` approach — FLUX-ge
---
## HyperFrames — HTML/GSAP Motion Graphics (zero-key, ~$0)
These use the HyperFrames composition runtime — HTML + CSS + GSAP rendered deterministically to video via headless Chrome + FFmpeg. Perfect for kinetic typography, product promos, launch reels, and website-to-video treatments where the visual grammar is typographic and motion-first.
**Requirements:** Node.js ≥ 22, FFmpeg, `npx` — no monorepo checkout, the CLI is fetched via `npx @hyperframes/cli` on first run.
### Kinetic Product Launch
> "Make a 20-second product launch video for a new AI coding assistant called 'Cortex'. Big kinetic typography, three feature callouts, a bold accent color, and a final CTA card. Use the HyperFrames runtime."
**What you get:** HTML/GSAP composition with SplitText-style word reveals, staggered feature callouts, accent-driven color accents from a custom playbook, and `hyperframes lint`/`validate` gates passed before render.
**Estimated time:** 3-5 minutes | **Cost:** $0
### Website → Video Teaser
> "Here's my landing page URL: https://example.com. Make me a 15-second social ad for Instagram. Use HyperFrames and pick up the site's real colors and typography."
**What you get:** `website-to-hyperframes` workflow — capture the site, extract colors/typography into a `DESIGN.md`, storyboard 3-4 beats, generate narration, build compositions with GSAP timelines, lint + validate + render.
**Estimated time:** 8-12 minutes | **Cost:** $0 (or ~$0.05 with premium TTS)
### Launch Reel with Registry Blocks
> "Create a 25-second launch reel for a developer tools startup. Include a data chart block (showing user growth from HyperFrames registry), kinetic title cards, and a shader transition between scenes."
**What you get:** `hyperframes add data-chart` + `hyperframes add shader-transition` installed as sub-compositions, wired into index.html, animated with GSAP timelines. Registry blocks are HyperFrames-only; Remotion can't install them.
**Estimated time:** 5-10 minutes | **Cost:** $0
---
## Full Setup Prompts (~$1-$3)
With video generation (Veo, Kling, Runway) + premium TTS (ElevenLabs) + music (Suno). These produce broadcast-quality content.
+8 -2
View File
@@ -204,10 +204,13 @@ You don't need paid API keys to make real videos. Out of the box, `make setup` g
| **Narration** | Piper TTS | Free offline text-to-speech — real human-sounding narration |
| **Open footage** | Archive.org + NASA + Wikimedia Commons | Free/open archival footage, educational media, and documentary texture |
| **Extra stock** | Pexels + Unsplash + Pixabay | Free stock footage/images (developer keys are free to get) |
| **Composition** | Remotion | Turns still images into animated video with spring physics, transitions, typography, and TikTok-style captions |
| **Composition (React)** | Remotion | React-based rendering — spring-animated image scenes, text cards, stat cards, charts, TikTok-style word-level captions, TalkingHead |
| **Composition (HTML/GSAP)** | HyperFrames | HTML/CSS/GSAP rendering — kinetic typography, product promos, launch reels, registry blocks, website-to-video |
| **Post-production** | FFmpeg | Encoding, subtitle burn-in, audio mixing, color grading |
| **Subtitles** | Built-in | Auto-generated captions with word-level timing |
OpenMontage picks between Remotion and HyperFrames at proposal time (locked as `render_runtime`). Remotion is the default for data-driven explainers and anything using the existing React scene stack; HyperFrames is the default for motion-graphics-heavy briefs that express naturally as HTML + GSAP. See `skills/core/hyperframes.md` for the full decision matrix.
**Two free-ish paths:**
- **Image-based video:** Piper narrates your script, images provide the visuals, and Remotion animates them into a polished edit.
@@ -505,9 +508,12 @@ Each tool declares which Layer 3 skills it relies on. The agent reads Layer 1 to
| Engine | Type | What It Does |
|--------|------|-------------|
| **Remotion** | Local (Node.js) | React-based programmatic video — spring-animated image scenes, stat reveals, section titles, hero cards, TikTok-style word-by-word captions, scene transitions (fade/slide/wipe/flip), Google Fonts, and audio with fade curves. **When no video generation providers are configured, the agent generates still images and Remotion turns them into fully animated video.** |
| **Remotion** | Local (Node.js) | React-based programmatic video — spring-animated image scenes, stat reveals, section titles, hero cards, TikTok-style word-by-word captions, scene transitions (fade/slide/wipe/flip), Google Fonts, audio with fade curves, and the TalkingHead avatar composition. **When no video generation providers are configured, the agent generates still images and Remotion turns them into fully animated video.** |
| **HyperFrames** | Local (Node.js ≥ 22) | HTML/CSS/GSAP programmatic video — kinetic typography, product promos, launch reels, custom motion graphics, registry blocks (data charts, grain overlays, shader transitions), website-to-video workflows. Consumed via `npx @hyperframes/cli`; no monorepo checkout needed. |
| **FFmpeg** | Local | Core video assembly, encoding, subtitle burn, audio muxing, color grading |
Runtime is chosen at proposal (`render_runtime`) and locked through `edit_decisions`. Silent swaps between runtimes are a governance violation — see `skills/core/hyperframes.md`.
</details>
---
+22 -3
View File
@@ -423,14 +423,33 @@ Each profile specifies codec, audio codec, CRF, pixel format, max file size, max
---
## Remotion Composer
## Composition Runtimes
A standalone Node.js/React subproject in `remotion-composer/` for programmatic video composition using [Remotion](https://www.remotion.dev/).
OpenMontage has a multi-runtime composition layer. Three engines live behind `video_compose`, chosen at proposal and locked in `edit_decisions.render_runtime`:
### Remotion (React-based)
A standalone Node.js/React subproject in `remotion-composer/` using [Remotion](https://www.remotion.dev/).
- **React 18** + **Remotion 4.0** + **TypeScript 5.3**
- Used by `video_compose` tool for complex compositions
- Handles the existing scene-component stack (`text_card`, `stat_card`, charts, captions, `TalkingHead`, `CinematicRenderer`)
- Scripts: `start` (studio), `build` (render), `upgrade`
### HyperFrames (HTML/CSS/GSAP)
Consumed via `npx @hyperframes/cli` (no monorepo checkout needed). Runtime floor: Node.js ≥ 22, FFmpeg, `npx`.
- Handles kinetic typography, product promos, launch reels, website-to-video, registry blocks
- Driver: `tools/video/hyperframes_compose.py` materializes a workspace under `projects/<name>/hyperframes/`, then runs `lint → validate → render`
- Layer 3 skills vendored at `.agents/skills/hyperframes*/`; Layer 2 guide at `skills/core/hyperframes.md`
### FFmpeg (fallback / simple cuts)
- Handles pure concat/trim when no composition is needed
- Also handles subtitle burn-in as a post-hoc operation
`video_compose` reads `edit_decisions.render_runtime` and dispatches via `_render_via_hyperframes`, `_remotion_render`, or `_render_via_ffmpeg`. Silent runtime swaps are forbidden — the tool returns a structured blocker when the chosen runtime is unavailable. See `AGENT_GUIDE.md` → "Composition Runtimes (Inside video_compose)" and `skills/core/hyperframes.md` for the full decision matrix.
---
## Test Architecture
+194
View File
@@ -0,0 +1,194 @@
"""Bridge OpenMontage playbooks → HyperFrames-friendly style artifacts.
OpenMontage playbooks are YAML files describing visual_language, typography,
motion, and asset generation guidance. When `render_runtime = "remotion"`
they're translated into a `themeConfig` prop on the React composition
(see `tools/video/video_compose._build_theme_from_playbook`). When
`render_runtime = "hyperframes"` the equivalent translation produces:
- a dict of CSS custom properties to emit on `:root`
- a short DESIGN.md the workspace can ship so humans (and agents) can
inspect the visual system in plain language
The playbook schema is NOT forked. We read the same fields; we just render
them differently.
"""
from __future__ import annotations
from typing import Any
_FALLBACK_CSS_VARS = {
"--color-bg": "#0B0F1A",
"--color-fg": "#F5F5F5",
"--color-accent": "#F59E0B",
"--color-primary": "#2563EB",
"--color-secondary": "#10B981",
"--color-surface": "#111827",
"--color-muted": "#6B7280",
"--font-heading": "Inter",
"--font-body": "Inter",
"--font-mono": "JetBrains Mono",
"--ease-primary": "cubic-bezier(0.65, 0, 0.35, 1)",
"--ease-entrance": "cubic-bezier(0.33, 1, 0.68, 1)",
"--ease-exit": "cubic-bezier(0.32, 0, 0.67, 0)",
"--duration-entrance": "0.6s",
"--duration-transition": "0.5s",
}
def _first(raw: Any, default: str) -> str:
"""Return the first string value from a palette entry (list or scalar)."""
if isinstance(raw, list) and raw:
return str(raw[0])
if isinstance(raw, str) and raw:
return raw
return default
def _font(typo: dict[str, Any], key: str, default: str) -> str:
"""Extract a font family string from a typography block."""
node = typo.get(key) or {}
if isinstance(node, dict):
return str(node.get("font") or node.get("family") or default)
if isinstance(node, str):
return node
return default
def _motion_easing(motion: dict[str, Any]) -> tuple[str, str]:
"""Derive (duration, ease) from the playbook motion block."""
pace = (motion.get("pace") or "moderate").lower()
if pace == "fast":
return "0.4s", "cubic-bezier(0.33, 1, 0.68, 1)"
if pace == "slow":
return "0.9s", "cubic-bezier(0.65, 0, 0.35, 1)"
return "0.6s", "cubic-bezier(0.5, 0, 0.5, 1)"
def style_bridge(
playbook: dict[str, Any] | None,
edit_decisions: dict[str, Any] | None = None,
) -> tuple[dict[str, str], str]:
"""Translate a playbook into (css_vars, design_md).
Both outputs are safe to write into a HyperFrames workspace:
- `css_vars` `:root { }` block in index.html
- `design_md` `DESIGN.md` at the workspace root
When `playbook` is empty or partial, missing fields fall back to
`_FALLBACK_CSS_VARS` so the emitted HTML is always renderable.
"""
css: dict[str, str] = dict(_FALLBACK_CSS_VARS)
source_note = "built-in fallback palette"
playbook_name = ""
if playbook:
playbook_name = str(
playbook.get("name")
or playbook.get("id")
or playbook.get("display_name")
or ""
)
vl = playbook.get("visual_language", {}) or {}
palette = vl.get("color_palette", {}) or {}
typo = playbook.get("typography", {}) or {}
motion = playbook.get("motion", {}) or {}
bg = _first(palette.get("background"), css["--color-bg"])
fg = _first(palette.get("text"), css["--color-fg"])
accent = _first(palette.get("accent"), css["--color-accent"])
primary = _first(palette.get("primary"), css["--color-primary"])
secondary = _first(palette.get("secondary"), css["--color-secondary"])
surface = _first(palette.get("surface"), css["--color-surface"])
muted = _first(palette.get("muted_text"), css["--color-muted"])
duration, ease = _motion_easing(motion)
css.update(
{
"--color-bg": bg,
"--color-fg": fg,
"--color-accent": accent,
"--color-primary": primary,
"--color-secondary": secondary,
"--color-surface": surface,
"--color-muted": muted,
"--font-heading": _font(typo, "heading", css["--font-heading"]),
"--font-body": _font(typo, "body", css["--font-body"]),
"--font-mono": _font(typo, "code", css["--font-mono"]),
"--ease-primary": ease,
"--duration-entrance": duration,
}
)
source_note = f"playbook `{playbook_name}`" if playbook_name else "loaded playbook"
# Edit-decisions may override colors per-production (rare but legal).
if edit_decisions:
meta = edit_decisions.get("metadata", {}) or {}
if meta.get("primary_color"):
css["--color-primary"] = str(meta["primary_color"])
if meta.get("accent_color"):
css["--color-accent"] = str(meta["accent_color"])
if meta.get("background_color"):
css["--color-bg"] = str(meta["background_color"])
if meta.get("text_color"):
css["--color-fg"] = str(meta["text_color"])
design_md = _render_design_md(css, source_note, playbook_name)
return css, design_md
def _render_design_md(
css: dict[str, str], source_note: str, playbook_name: str
) -> str:
title = f"# DESIGN — {playbook_name}" if playbook_name else "# DESIGN"
lines = [
title,
"",
f"> Generated by OpenMontage HyperFrames style bridge (source: {source_note}).",
"",
"## Colors",
"",
f"- Background: `{css['--color-bg']}`",
f"- Foreground: `{css['--color-fg']}`",
f"- Primary: `{css['--color-primary']}`",
f"- Accent: `{css['--color-accent']}`",
f"- Secondary: `{css['--color-secondary']}`",
f"- Surface: `{css['--color-surface']}`",
f"- Muted text: `{css['--color-muted']}`",
"",
"## Typography",
"",
f"- Heading: `{css['--font-heading']}`",
f"- Body: `{css['--font-body']}`",
f"- Mono: `{css['--font-mono']}`",
"",
"## Motion",
"",
f"- Primary ease: `{css['--ease-primary']}`",
f"- Entrance duration: `{css['--duration-entrance']}`",
"",
"## How to use in compositions",
"",
"Reference these tokens via `var(...)` instead of hard-coding hex",
"values or font names:",
"",
"```css",
".scene-content h1 {",
" font-family: var(--font-heading);",
" color: var(--color-fg);",
"}",
".scene-content .accent {",
" color: var(--color-accent);",
"}",
"```",
"",
"When adjusting a composition's look, edit the `:root` block in",
"`index.html` (or this bridge for cross-project changes), not the",
"individual scene styles.",
"",
]
return "\n".join(lines)
+10
View File
@@ -27,6 +27,7 @@ def score_slideshow_risk(
scenes: list[dict[str, Any]],
edit_decisions: dict[str, Any] | None = None,
renderer_family: str | None = None,
render_runtime: str | None = None,
) -> dict[str, Any]:
"""Score slideshow risk across 6 dimensions.
@@ -34,12 +35,19 @@ def score_slideshow_risk(
scenes: Scene list from scene_plan artifact.
edit_decisions: Optional edit_decisions artifact for transition analysis.
renderer_family: Optional renderer family for cinematic claim verification.
render_runtime: Optional render runtime (remotion/hyperframes/ffmpeg).
Passed through so dimension scoring can reason about runtime-specific
indicators when it's useful. Scoring logic is currently
runtime-neutral a text-heavy HyperFrames composition is just as
slideshow-y as a text-heavy Remotion one but the parameter is
part of the contract so callers record runtime alongside family.
Returns:
{
"average": float,
"verdict": str,
"dimensions": {dimension_name: {"score": float, "reason": str}},
"render_runtime": str | None,
}
"""
if not scenes:
@@ -47,6 +55,7 @@ def score_slideshow_risk(
"average": 5.0,
"verdict": "fail",
"dimensions": {},
"render_runtime": render_runtime,
}
dimensions = {
@@ -74,6 +83,7 @@ def score_slideshow_risk(
"average": round(average, 2),
"verdict": verdict,
"dimensions": dimensions,
"render_runtime": render_runtime,
}
+3
View File
@@ -230,6 +230,7 @@ stages:
- video_stitch
tools_available:
- video_compose
- hyperframes_compose
- video_stitch
- audio_mixer
checkpoint_required: true
@@ -238,6 +239,8 @@ stages:
- Output video exists and is playable
- "Duration within +/-5% of target"
- Audio is clear with proper narration/music balance
- "render_runtime in edit_decisions matches proposal_packet — silent swap to another runtime is a CRITICAL governance violation"
- "If render_runtime='hyperframes': `hyperframes lint` and `hyperframes validate` must pass before render"
success_criteria:
- Schema-valid render_report with output paths
- Output file exists and passes ffprobe validation
+4
View File
@@ -98,6 +98,7 @@ stages:
- Free/local tool options are highlighted where applicable
- "Audio architecture decided: single narrator / character dialogue / narrator+characters"
- "Provider choice presented as comparison table with costs — agent recommends but user decides"
- "render_runtime chosen explicitly — remotion for React scene stack, hyperframes for HTML/GSAP motion graphics, ffmpeg for simple concat. See skills/core/hyperframes.md and skills/meta/animation-runtime-selector.md for the decision matrix. Record in decision_log with category render_runtime_selection."
success_criteria:
- Schema-valid proposal_packet with at least 3 concept_options
- selected_concept includes animation_mode and reuse_strategy
@@ -243,6 +244,7 @@ stages:
- video_stitch
tools_available:
- video_compose
- hyperframes_compose
- audio_mixer
- video_stitch
checkpoint_required: true
@@ -252,6 +254,8 @@ stages:
- Motion timing survives the final render
- Audio sync matches the animatic intent
- Duration within +/-5% of target
- "render_runtime in edit_decisions matches render_runtime locked in proposal_packet — any swap requires a render_runtime_selection decision in decision_log"
- "HyperFrames renders MUST pass `hyperframes lint` and `hyperframes validate` before the render step — never skip validation"
- "Post-render self-review MANDATORY: extract keyframes, visually inspect each scene, flag weak frames before presenting to user"
success_criteria:
- Schema-valid render_report artifact
+3
View File
@@ -233,6 +233,7 @@ stages:
- audio_enhance
tools_available:
- video_compose
- hyperframes_compose
- audio_mixer
- video_stitch
- video_trimmer
@@ -245,6 +246,8 @@ stages:
- Motion-required delivery was preserved without silent fallback
- Audio dynamics are controlled and intelligible
- Letterbox or frame treatment improves rather than harms the output
- "render_runtime in edit_decisions matches proposal_packet — silent swap is a CRITICAL governance violation"
- "Cinematic work typically picks render_runtime='remotion' (CinematicRenderer) for video-led pieces; render_runtime='hyperframes' for kinetic title cards and HTML/GSAP-driven trailers"
success_criteria:
- Schema-valid render_report artifact
- Output file exists and passes ffprobe validation
+1
View File
@@ -166,6 +166,7 @@ stages:
- Resolution matches target_platform canvas
- Uniform LUT applied across the timeline
- Music is mixed in (MANDATORY — silent output only with explicit user opt-out recorded in brief)
- "render_runtime locked at proposal MUST stay 'remotion' on this pipeline until HyperFrames end-tag parity lands — the overlay/concat end-tag stack depends on Remotion CinematicRenderer components. Silent swap to hyperframes or ffmpeg is a CRITICAL governance violation."
- "End-tag rendered via Remotion (MANDATORY — absence only with explicit opt-out). Default mode: overlay (ProRes 4444 with alpha composited on final scenes). Fallback: concat (opaque card appended after body)."
- "Overlay mode: extract a frame from the overlay region and verify text is visible over footage, not over black. Concat mode: last frame must be the end-tag card."
- No silent fallback from a motion-led promise
+3
View File
@@ -187,6 +187,7 @@ stages:
- audio_enhance
tools_available:
- video_compose
- hyperframes_compose
- audio_mixer
- video_stitch
- video_trimmer
@@ -198,6 +199,8 @@ stages:
- Source and support layers remain balanced
- Aspect-ratio variants preserve readability
- Audio stays coherent across footage and generated elements
- "render_runtime in edit_decisions matches proposal_packet — silent swap is a CRITICAL governance violation"
- "Hybrid typically uses render_runtime='remotion' so source footage and React support overlays compose in one pass; pick 'hyperframes' only when support layers are HTML/GSAP-native"
success_criteria:
- Schema-valid render_report artifact
- Output file exists and passes ffprobe validation
+3
View File
@@ -209,6 +209,7 @@ stages:
- audio_enhance
tools_available:
- video_compose
- hyperframes_compose
- audio_mixer
- video_trimmer
- audio_enhance
@@ -218,6 +219,8 @@ stages:
- Output video is playable and crisp at target resolution
- UI text remains readable after crops and scaling
- Audio is clear with no keyboard/click noise
- "render_runtime in edit_decisions matches proposal_packet — silent swap is a CRITICAL governance violation"
- "For synthetic UI/terminal demos, render_runtime='remotion' (TerminalScene) is preferred; render_runtime='hyperframes' is valid for custom HTML UIs — in that case, lint+validate must pass before render"
success_criteria:
- Schema-valid render_report artifact
- Output file exists and passes ffprobe validation
+12
View File
@@ -1,3 +1,15 @@
"""Render the curated zero-key Remotion demos.
This script is Remotion-specific by design the demos live in
`remotion-composer/public/demo-props/` as JSON props for existing React
scene components. It is NOT a cross-runtime demo harness.
For a HyperFrames demo, run `make hyperframes-doctor` to verify the runtime
floor, then either scaffold a real composition via `npx hyperframes init`
or drive `hyperframes_compose` from the Agent SDK. HyperFrames demos are
authored as HTML + GSAP in a project workspace, not as JSON props here.
"""
from __future__ import annotations
import argparse
@@ -28,6 +28,7 @@
"pipeline_selection",
"provider_selection",
"renderer_family_selection",
"render_runtime_selection",
"playbook_selection",
"fallback_decision",
"budget_tradeoff",
+7 -2
View File
@@ -4,7 +4,7 @@
"title": "Edit Decisions",
"description": "Editorial decisions produced by the Edit Agent.",
"type": "object",
"required": ["version", "cuts"],
"required": ["version", "cuts", "render_runtime"],
"properties": {
"version": { "type": "string", "const": "1.0" },
"cuts": {
@@ -192,7 +192,12 @@
"renderer_family": {
"type": "string",
"enum": ["explainer-data", "explainer-teacher", "cinematic-trailer", "documentary-montage", "product-reveal", "screen-demo", "presenter", "animation-first"],
"description": "Locked at proposal stage — determines which Remotion composition renders this video"
"description": "Locked at proposal stage — creative grammar for this deliverable. Preserved from proposal_packet; edit MUST NOT change without a logged decision."
},
"render_runtime": {
"type": "string",
"enum": ["remotion", "hyperframes", "ffmpeg"],
"description": "Locked at proposal stage — technical runtime that realizes renderer_family. Edit MUST carry this forward unchanged unless a logged render_runtime_selection decision overrides it."
},
"slideshow_risk_score": {
"type": "object",
+14 -1
View File
@@ -82,7 +82,20 @@
"description": "Delivery promise and renderer governance checks",
"properties": {
"delivery_promise_honored": { "type": "boolean" },
"renderer_family_used": { "type": "string" },
"renderer_family_used": { "type": "string", "description": "Creative grammar that actually ran" },
"render_runtime_used": {
"type": "string",
"enum": ["remotion", "hyperframes", "ffmpeg"],
"description": "Technical runtime that actually executed the render. Must match the runtime locked at proposal unless a logged decision overrode it."
},
"runtime_swap_detected": {
"type": "boolean",
"description": "True if the runtime that ran differs from the runtime locked in proposal_packet. An unapproved swap is a contract violation."
},
"runtime_swap_check": {
"type": "string",
"description": "Human-readable status of the runtime-swap check: 'ok — …', 'detected — …', or 'skipped — …'. When the check was skipped, the reviewer skill is responsible for cross-artifact comparison."
},
"motion_ratio_actual": { "type": "number" },
"silent_downgrade_detected": { "type": "boolean" },
"issues": {
@@ -66,7 +66,7 @@
},
"production_plan": {
"type": "object",
"required": ["pipeline", "stages"],
"required": ["pipeline", "stages", "render_runtime"],
"properties": {
"pipeline": { "type": "string", "description": "Pipeline manifest name, e.g., 'animated-explainer'" },
"playbook": { "type": "string", "description": "Selected style playbook" },
@@ -148,7 +148,12 @@
"renderer_family": {
"type": "string",
"enum": ["explainer-data", "explainer-teacher", "cinematic-trailer", "documentary-montage", "product-reveal", "screen-demo", "presenter", "animation-first"],
"description": "Locked at proposal stage — compose cannot change without logging a decision"
"description": "Locked at proposal stage — compose cannot change without logging a decision. This is the creative grammar, NOT the technical engine."
},
"render_runtime": {
"type": "string",
"enum": ["remotion", "hyperframes", "ffmpeg"],
"description": "Locked at proposal stage — the technical runtime that realizes renderer_family. remotion=React scene components, hyperframes=HTML/CSS/GSAP, ffmpeg=simple concat/trim. Must be explicit and auditable; silent swaps are forbidden."
},
"music_source": {
"type": "object",
+2 -1
View File
@@ -79,6 +79,7 @@ Key capability families to look for in the output:
|-------|------|---------|----------------------|
| FFmpeg | `core/ffmpeg.md` | Video encoding, filtering, composition | `ffmpeg`, `video_toolkit` |
| Remotion | `core/remotion.md` | React-based composition, Phase 3+ | `remotion-best-practices`, `remotion` |
| HyperFrames | `core/hyperframes.md` | HTML/CSS/GSAP composition runtime — kinetic typography, product promos, website-to-video, registry blocks | `hyperframes`, `hyperframes-cli`, `hyperframes-registry`, `website-to-hyperframes`, `gsap-core`, `gsap-timeline` |
| WhisperX | `core/whisperx.md` | Transcription with word-level timestamps | `speech-to-text` |
| Subtitle Sync | `core/subtitle-sync.md` | Subtitle timing and alignment | `remotion-best-practices` |
| Color Grading | `core/color-grading.md` | FFmpeg color profiles, LUT workflow, accessibility | `ffmpeg` |
@@ -297,7 +298,7 @@ Claude Code accesses them via symlinks in `.claude/skills/`.
| Category | Installed Skills | Source |
|----------|-----------------|--------|
| **Video Composition** | `remotion-best-practices`, `remotion` | `remotion-dev/skills`, `digitalsamba/claude-code-video-toolkit` |
| **Video Composition** | `remotion-best-practices`, `remotion`, `hyperframes`, `hyperframes-cli`, `hyperframes-registry`, `website-to-hyperframes` | `remotion-dev/skills`, `digitalsamba/claude-code-video-toolkit`, `heygen-com/hyperframes` |
| **Video Processing** | `ffmpeg`, `video_toolkit` | `digitalsamba/claude-code-video-toolkit` |
| **TTS & Audio** | `text-to-speech`, `speech-to-text`, `music`, `sound-effects`, `elevenlabs`, `agents`, `setup-api-key` | `elevenlabs/skills`, `digitalsamba/claude-code-video-toolkit` |
| **Image Generation** | `flux-best-practices`, `bfl-api`, `grok-media` | `black-forest-labs/skills`, local OpenMontage skill |
+324
View File
@@ -0,0 +1,324 @@
# HyperFrames Skill (Layer 2)
This is the **OpenMontage-specific** guide to HyperFrames. It explains when
OpenMontage pipelines should choose HyperFrames over Remotion, how OpenMontage
artifacts map to HyperFrames project files, and how the compose stage drives
the HyperFrames CLI.
For raw HyperFrames knowledge (authoring contract, `data-*` attributes, GSAP
timeline rules, CLI flags, registry blocks, website-to-video), read the Layer 3
skills:
- `.agents/skills/hyperframes/` — composition authoring contract + GSAP rules
- `.agents/skills/hyperframes-cli/` — init, lint, validate, preview, render
- `.agents/skills/hyperframes-registry/``hyperframes add` + block wiring
- `.agents/skills/website-to-hyperframes/` — capture-to-video workflow
This file teaches the bridge between the two.
---
## When OpenMontage should pick HyperFrames (vs Remotion vs FFmpeg)
OpenMontage separates two concepts:
- **`renderer_family`** — the creative grammar (`explainer-data`,
`cinematic-trailer`, `product-reveal`, etc.). Chosen at proposal.
- **`render_runtime`** — the technical engine that realizes that grammar
(`remotion`, `hyperframes`, `ffmpeg`). Also chosen at proposal.
Both are locked in `proposal_packet.schema.json` and carried through
`edit_decisions` unchanged unless a `render_runtime_selection` decision is
logged in `decision_log`. Silent runtime swaps are a contract violation.
### Decision matrix
| Scenario | Prefer | Why |
|----------|--------|-----|
| Existing explainer, React scene component stack (text_card, stat_card, chart scenes, caption overlay, TalkingHead, CinematicRenderer) | **Remotion** | These compositions already exist in `remotion-composer/`. Reusing them is free; replicating them in HTML is not. |
| Word-level caption burn / karaoke captions | **Remotion** | `remotion_caption_burn` is Remotion-specific and is NOT at parity on HyperFrames day 1. |
| Avatar / lip-sync presenter | **Remotion** | `TalkingHead` composition lives in Remotion. No HyperFrames equivalent yet. |
| Kinetic typography, heavy text motion, GSAP-native animation | **HyperFrames** | HTML/GSAP is the natural medium. Expressing this as Remotion `interpolate()` calls is slow and fragile. |
| Product promo / launch reel / marketing title card | **HyperFrames** | CSS/GSAP composition grammar matches how designers already think about these. Templates (`kinetic-type`, `product-promo`, `swiss-grid`) give a strong starting point. |
| Website-to-video / UI-driven composition | **HyperFrames** | The `website-to-hyperframes` workflow exists for exactly this. |
| Registry block needed (data chart, grain overlay, shimmer sweep, shader transition) | **HyperFrames** | The registry is HyperFrames-only. Remotion does not have `hyperframes add`. |
| Synthetic UI / fake terminal / fake browser demo | Either — depends on existing coverage | OpenMontage already ships Remotion `TerminalScene` (see `synthetic-screen-recording` Layer 3). For UI chrome beyond terminal, HyperFrames HTML is easier. |
| Pure concat / trim of source clips, no composition | **FFmpeg** | Neither Remotion nor HyperFrames add value here. |
| Remotion is not installed on this machine | **HyperFrames** (if available) or **FFmpeg** | Do not silently fall back. Tell the user before downgrading. |
### Hard rule: present both runtimes when both are available
The decision matrix above is input for the conversation with the user, NOT
a license to silently pick a "default." When both Remotion and HyperFrames
are available on the machine, the proposal stage MUST:
1. Present both to the user with brief-specific pros/cons.
2. Recommend one with rationale tied to `delivery_promise` and
`visual_approach`.
3. Wait for approval.
4. Log both in `options_considered` of a `render_runtime_selection`
decision.
See `AGENT_GUIDE.md` → "Present Both Composition Runtimes (HARD RULE)" for
the full contract and `skills/meta/reviewer.md` for the CRITICAL-finding
enforcement.
### Hard rule: motion-required deliverables
If the brief's `delivery_promise.motion_required` is `true` (sci-fi trailer,
cinematic teaser, hype edit, any brief whose promise depends on real motion),
then the runtime chosen at proposal is a **commitment**, not a hint. Compose
MUST NOT downgrade to FFmpeg Ken Burns. If the chosen runtime fails (Remotion
not installed, `npx hyperframes doctor` reports a blocker), surface the blocker
per `AGENT_GUIDE.md` > "Escalate Blockers Explicitly" and wait for user
approval before switching runtime.
---
## What stays Remotion-only (Phase 1)
Do **not** attempt to port these to HyperFrames on day 1. They require
dedicated parity work:
- `remotion_caption_burn` (word-by-word burned captions)
- `TalkingHead` composition (avatar/lip-sync presenter)
- Existing documentary-montage end-tag overlay stack (relies on specific
Remotion components)
- Anything that assumes assets are staged under `remotion-composer/public/`
and consumed by existing React scene components
For these, keep `render_runtime = "remotion"` and proceed as today.
---
## Project workspace layout
HyperFrames needs its own project workspace. Do **not** reuse
`remotion-composer/public/` — that's Remotion's shared staging directory and
mixing runtimes there causes cross-project collisions.
```
projects/<project-name>/
├── artifacts/
├── assets/
│ ├── images/
│ ├── video/
│ ├── audio/
│ └── music/
├── hyperframes/ ← HyperFrames runtime workspace (when selected)
│ ├── index.html ← root composition
│ ├── compositions/ ← sub-compositions and registry blocks
│ │ └── components/ ← registry components (grain overlays, etc.)
│ ├── assets/ ← symlink or copy of project assets
│ ├── hyperframes.json ← CLI config (registry URL, install paths)
│ ├── DESIGN.md ← playbook-derived visual brief (optional)
│ └── narration.wav ← TTS output, when applicable
└── renders/
└── final.mp4
```
The workspace is generated at compose time by `hyperframes_compose` from
`edit_decisions` + `asset_manifest` + the active playbook. It's regenerable
and gitignored along with the rest of `projects/`.
### Why a dedicated workspace per project
- HyperFrames resolves `data-composition-src`, `src=`, and registry blocks
relative to the project root. A shared workspace breaks this.
- `npx hyperframes lint | validate | render` all operate on a project
directory. They don't take an abstract composition ID the way Remotion does.
- Assets live next to the HTML that references them, matching the
`website-to-hyperframes` reference workflow.
---
## Artifact → HyperFrames mapping
When `render_runtime = "hyperframes"`, the compose stage translates
OpenMontage artifacts into HyperFrames project files:
| OpenMontage artifact field | HyperFrames target |
|---|---|
| `edit_decisions.cuts[]` (sequence of scenes) | `index.html` timeline, one `<div data-composition-id data-composition-src>` per cut |
| `edit_decisions.cuts[i].in_seconds / out_seconds` | `data-start` / `data-duration` on the clip element |
| `edit_decisions.cuts[i].type` (scene kind) | Registry block installed via `hyperframes add`, OR a hand-authored sub-composition template |
| `asset_manifest.assets[]` paths | Copied or symlinked into `projects/<p>/hyperframes/assets/` and referenced with relative `src=` |
| `audio.narration.segments[]` | `<audio>` element with matching `data-start` / `data-duration` |
| `audio.music` | Second `<audio>` element, lower `data-volume` |
| `subtitles` (enabled + source) | Either a registry `captions` block or hand-authored per-word spans — NOT `remotion_caption_burn` |
| Selected playbook (`flat-motion-graphics`, `clean-professional`, etc.) | `:root` CSS custom properties + `DESIGN.md`. See `lib/hyperframes_style_bridge.py`. |
| `renderer_family` | Controls which top-level HTML template is used and which registry blocks are pre-installed |
The concrete rendering is: `hyperframes_compose` writes files into the
workspace, runs `lint → validate → render`, and returns a `render_report`
with the path to the generated MP4. See `tools/video/hyperframes_compose.py`.
### Workspace-local authoring artifacts
Upstream's `website-to-hyperframes` skill uses `DESIGN.md`, `SCRIPT.md`, and
`STORYBOARD.md` as step-by-step workspace files. OpenMontage does **not**
replace its canonical artifact contracts with these — `brief`, `script`,
`scene_plan`, `edit_decisions`, etc. remain the source of truth under
`projects/<p>/artifacts/`. Treat the upstream files as **convenience copies**
written into the HyperFrames workspace so the runtime workflow feels natural:
- `DESIGN.md` — derived from the selected playbook, written by
`hyperframes_compose` or `lib/hyperframes_style_bridge.py`. Safe to use as
a working brief in the workspace.
- `SCRIPT.md` — optional narration copy for human review. Canonical script
stays in `artifacts/script.json`.
- `STORYBOARD.md` — optional per-beat creative direction. Canonical scene
plan stays in `artifacts/scene_plan.json`.
If a workspace-local file and a canonical artifact disagree, the canonical
artifact wins.
---
## Runtime selection rules
1. **Proposal stage** chooses `render_runtime` and logs the decision in
`decision_log` with category `render_runtime_selection`. It must consider
the decision matrix above and the actual availability of each runtime.
2. **Preflight** reports which runtimes are available (see below). A
runtime that is not available is not a valid proposal choice unless the
user explicitly approves installing it.
3. **Edit stage** carries `render_runtime` forward unchanged.
4. **Compose stage** reads `edit_decisions.render_runtime` and routes via
`video_compose``hyperframes_compose` (for HyperFrames) or the existing
Remotion path (for Remotion). Compose may not swap runtime without a new
`render_runtime_selection` decision.
5. **Final review** records `render_runtime_used` and sets
`runtime_swap_detected = true` if it differs from proposal.
---
## Preflight — HyperFrames availability
At preflight, the provider menu reports HyperFrames availability. The
`hyperframes_compose` tool's `get_info()` returns:
```json
{
"runtime_available": true | false,
"node_major": 22,
"ffmpeg_available": true,
"doctor_ok": true,
"install_instructions": "…"
}
```
Floor requirements (all must hold for `runtime_available: true`):
- Node.js major version ≥ 22
- `ffmpeg` binary on PATH
- `npx` on PATH (bundled with Node.js)
- `npx hyperframes doctor` exits 0, OR a lightweight equivalent check passes
`bun` is NOT required — HyperFrames is consumable via `npx hyperframes` (published npm package name is `hyperframes`; the monorepo-internal `@hyperframes/cli` name is NOT on the public npm registry and returns 404).
When `runtime_available: false`, preflight must surface the reason and the
install instructions. Per `AGENT_GUIDE.md` Setup Offer Protocol, group the
fix by effort:
- Missing Node 22 → 5-minute install, explain what it unlocks
- Missing FFmpeg → 1-minute install on macOS/Linux, longer on Windows
- `doctor` reports issues → show the doctor output verbatim
---
## Validation protocol
HyperFrames ships a real validation stack. Run **all** of these before
declaring a render complete:
1. **`npx hyperframes lint`** — static contract checks (duplicate ids,
overlapping tracks, missing `data-composition-id`, unregistered timelines).
MUST pass before render.
2. **`npx hyperframes validate`** — browser-based runtime checks: seeks into
the paused composition, screenshots, samples pixels, computes WCAG
contrast ratios, verifies `window.__timelines` registration and
`class="clip"` on timed elements. MUST pass before render (contrast can
be deferred with `--no-contrast` during iteration, but not for final).
3. **`npx hyperframes render --quality standard`** — produces the MP4.
4. **Post-render final review** — probe with ffprobe, sample frames,
transcribe audio, compare to script. Same contract as the Remotion path.
See `final_review.schema.json`.
If lint or validate fails, do **not** render. Fix the composition and re-run.
Silent render from a failing composition is a contract violation — the whole
point of HyperFrames is that validate catches issues that FFmpeg or Remotion
cannot.
---
## Style bridge (playbook → CSS)
OpenMontage playbooks currently translate into Remotion `themeConfig`
objects. For HyperFrames, the equivalent translation produces:
- A block of CSS custom properties on `:root` (`--color-bg`, `--color-fg`,
`--color-accent`, `--font-heading`, `--font-body`, `--ease-primary`,
`--duration-primary`, etc.).
- A short `DESIGN.md` that explains the visual system in plain language.
- Optional typography `@import` statements (only for fonts the HyperFrames
font compiler supports).
See `lib/hyperframes_style_bridge.py`. Playbooks do NOT need to fork — the
existing playbook schema carries enough information to drive both Remotion
and HyperFrames output.
---
## Cost model
HyperFrames renders are local: $0 API cost, but CPU-intensive (headless
Chrome + FFmpeg). Track via `cost_tracker`:
- `estimate` — based on composition duration × resolution × `--workers`
- `reserve` — 0 (no API spend)
- `reconcile` — wall-clock render time
Same pattern as Remotion.
---
## Anti-patterns
- ❌ Forking playbook data for HyperFrames when the current schema already
carries colors, typography, and motion.
- ❌ Writing HyperFrames compositions that reference `remotion-composer/public/`
— the HyperFrames workspace is separate and self-contained.
- ❌ Running `hyperframes init` from the OpenMontage orchestrator. `init`
creates its own project semantics and installs agent skills — it's meant
for humans bootstrapping a project, not for the pipeline. `hyperframes_compose`
generates the project files directly.
- ❌ Using HyperFrames as "React without Remotion." HyperFrames is
HTML-first with GSAP. If your scene is authored as React JSX, it belongs
in Remotion.
- ❌ Using `repeat: -1` in GSAP inside HyperFrames. Infinite tweens break the
deterministic seek-and-capture render. Always bounded repeats.
- ❌ Animating from `async` context, `setTimeout`, or Promises during
timeline construction. `window.__timelines` must be fully populated
synchronously after page load.
---
## Pipelines adopting HyperFrames
| Pipeline | Status |
|----------|--------|
| `animation` | Wave 1 — HyperFrames is a first-class option for motion-graphics-heavy briefs |
| `animated-explainer` | Wave 1 — HyperFrames viable when the concept is HTML/GSAP-native; Remotion remains default for data-chart-heavy explainers |
| `screen-demo` | Wave 1 — HyperFrames viable for synthetic product UI; `TerminalScene` (Remotion) remains preferred for terminal-specific demos |
| `cinematic` | Wave 2 |
| `hybrid` | Wave 2 |
| `documentary-montage` | Wave 2 |
| `talking-head` | Deferred — depends on TalkingHead parity |
| `avatar-spokesperson` | Deferred — depends on TalkingHead parity |
| `clip-factory`, `podcast-repurpose`, `localization-dub` | Deferred — current compose paths rely on Remotion caption burn |
| `framework-smoke` | N/A (test pipeline) |
Proposal and compose directors for adopted pipelines describe runtime choice
explicitly — see each pipeline's `proposal-director.md` and
`compose-director.md`.
+21 -2
View File
@@ -1,8 +1,27 @@
# Animation & Motion Graphics Pipeline
> Sources: School of Motion curriculum, After Effects documentation, Remotion documentation,
> Disney's 12 Principles of Animation (Frank Thomas & Ollie Johnston), Motion Design School,
> The Animator's Survival Kit (Richard Williams)
> HyperFrames documentation, Disney's 12 Principles of Animation (Frank Thomas & Ollie
> Johnston), Motion Design School, The Animator's Survival Kit (Richard Williams)
## Runtime Choice — Remotion vs HyperFrames
Animation work in OpenMontage runs on one of two composition runtimes. Both
are first-class; the choice is creative, not a fallback:
- **Remotion (React-based)** — when the scene is a React component, uses the
existing chart/text-card/comparison/kpi stack, or needs pixel-accurate
frame-level interpolation through `useCurrentFrame()` + `interpolate()`.
Default for data-heavy explainers.
- **HyperFrames (HTML/GSAP)** — when the motion is expressed naturally as
CSS + GSAP timelines: kinetic typography, product promos, launch reels,
website/UI-driven compositions, registry-block-driven scenes. Default
when the brief is motion-graphics-led and the scene library in
`remotion-composer/` doesn't already cover the look.
See `skills/core/hyperframes.md` and `skills/meta/animation-runtime-selector.md`
for the full decision matrix. Whichever runtime is chosen at proposal must
be locked in `edit_decisions.render_runtime` and preserved through compose.
## Quick Reference Card
+55 -4
View File
@@ -1,18 +1,67 @@
# Animation Runtime Selector
Meta-skill that answers: *"for the animated scene I'm about to build, which runtime + Layer 3 skills should I reach for?"*
Meta-skill that answers two questions:
Read this before authoring any animated Remotion component or HyperFrames composition. It routes you to the right Layer 3 skill so you don't waste time hand-rolling what a plugin already solves.
1. **Which composition runtime should this video use?** — Remotion, HyperFrames, or FFmpeg.
2. **Which animation library / Layer 3 skills should this scene reach for?** — Remotion primitives, GSAP plugins, framer-motion, Lottie, Manim, D3.
Read this before authoring any animated component or composition, and whenever you're choosing `render_runtime` at proposal time. It routes you to the right Layer 3 skill so you don't waste time hand-rolling what a plugin already solves.
## When to use this skill
Apply when:
- **Proposal stage** needs to lock `render_runtime` (remotion / hyperframes / ffmpeg)
- A stage director (asset, edit, compose) needs to author an animated component
- An agent is about to write Remotion JSX for a scene that involves text reveals, SVG motion, curved camera paths, shape morphs, or multi-stage choreography
- An agent is asked to build a HyperFrames composition
- An agent is uncertain whether to reach for a GSAP plugin vs inline `interpolate()`/`spring()`
## Decision matrix
## Runtime choice (Remotion vs HyperFrames vs FFmpeg)
OpenMontage separates creative grammar (`renderer_family`) from technical
engine (`render_runtime`). Both are locked at proposal and carried through
`edit_decisions` unchanged. Silent runtime swaps at compose time are a
contract violation.
### HARD RULE — present both runtimes, don't silently default
When both Remotion AND HyperFrames are available on the machine (check
`video_compose.get_info()["render_engines"]`), the agent MUST present both
options to the user before locking `render_runtime`. The decision matrix
below is the agent's input for the conversation, NOT a license to silently
pick the "default" entry. See `AGENT_GUIDE.md` → "Present Both Composition
Runtimes" for the full contract.
Concretely, at the proposal stage:
1. Query `video_compose.get_info()["render_engines"]` to find which
runtimes are available on this machine.
2. If both Remotion and HyperFrames are available, present both to the
user with: one-line description tailored to the brief, one-line
honest tradeoff, agent's recommendation with reason.
3. Wait for explicit user approval.
4. Log the decision in `decision_log` with category
`render_runtime_selection` and both runtimes in `options_considered`.
5. Only then write `render_runtime` into `proposal_packet.production_plan`.
A `render_runtime_selection` decision with only one option considered
when both were available is a CRITICAL reviewer finding.
| Brief characteristic | `render_runtime` | Read |
|---|---|---|
| Existing React scene stack (text_card, stat_card, chart, caption overlay, TalkingHead, CinematicRenderer) | **remotion** | `skills/core/remotion.md` |
| Word-level caption burn / karaoke captions | **remotion** | `skills/core/remotion.md` |
| Avatar / lip-sync / presenter | **remotion** | `skills/core/remotion.md` |
| Kinetic typography, HTML/GSAP-native motion, product promo, launch reel | **hyperframes** | `skills/core/hyperframes.md` + `.agents/skills/hyperframes/SKILL.md` |
| Website → video, UI-driven composition | **hyperframes** | `.agents/skills/website-to-hyperframes/SKILL.md` |
| Registry block needed (data-chart, grain-overlay, shader transitions, etc.) | **hyperframes** | `.agents/skills/hyperframes-registry/SKILL.md` |
| Pure concat / trim of source clips, no composition needed | **ffmpeg** | `skills/core/ffmpeg.md` |
| Selected runtime is unavailable | **escalate** — do not substitute silently | `AGENT_GUIDE.md` → Escalate Blockers |
Read `skills/core/hyperframes.md` for the full Remotion-vs-HyperFrames
decision matrix and the list of features that stay Remotion-only in Phase 1.
## Animation library decision matrix
| Animation need | Recommended runtime | Read first |
|---|---|---|
@@ -32,7 +81,9 @@ Apply when:
| Mathematical / scientific visualization | Manim | `.agents/skills/manim-composer`, `.agents/skills/manimce-best-practices` |
| D3 data-driven visualization | D3 | `.agents/skills/d3-viz` |
| Data chart (bar/line/pie/KPI) | Remotion built-in chart components | `remotion-composer/SCENE_TYPES.md` |
| HyperFrames composition (any motion) | HyperFrames + GSAP (mandatory) | `.agents/skills/gsap-core`, `.agents/skills/gsap-timeline` |
| HyperFrames composition (any motion) | HyperFrames + GSAP (mandatory) | `.agents/skills/hyperframes` + `.agents/skills/gsap-core`, `.agents/skills/gsap-timeline` |
| HyperFrames composition CLI work (lint/validate/render) | HyperFrames CLI | `.agents/skills/hyperframes-cli` |
| HyperFrames registry block install (`hyperframes add ...`) | HyperFrames registry | `.agents/skills/hyperframes-registry` |
## The "keep it simple" bias
+28 -2
View File
@@ -40,12 +40,38 @@ Based on discovery, classify the setup:
| Tier | What's Available | Best Pipelines |
|------|-----------------|----------------|
| **Zero-key** | Piper TTS + Pexels/Pixabay stock (if keys added) + Remotion + FFmpeg | Animated Explainer (stock visuals + free narration) |
| **Starter** | One configured image generation provider + free TTS + Remotion | Animated Explainer, Animation (AI-generated visuals) |
| **Zero-key** | Piper TTS + Pexels/Pixabay stock (if keys added) + Remotion and/or HyperFrames + FFmpeg | Animated Explainer (stock visuals + free narration) |
| **Starter** | One configured image generation provider + free TTS + Remotion and/or HyperFrames | Animated Explainer, Animation (AI-generated visuals) |
| **Standard** | Image gen + TTS + music gen | Animated Explainer, Animation, Screen Demo, Hybrid |
| **Full** | Video gen + image gen + premium TTS + music | All pipelines including Cinematic, Avatar, Talking Head |
| **Full + GPU** | Cloud APIs + local video gen models | All pipelines with free local fallbacks |
**Composition runtimes** — both are first-class and surface as distinct
entries in the provider menu. Report each one's availability separately:
- **Remotion** requires Node.js + `npx` + `remotion-composer/` + `node_modules`.
Best for React-based scene components (text cards, stat cards, charts),
word-level captions, and the `TalkingHead` avatar composition.
- **HyperFrames** requires Node.js ≥ 22 + `npx` + FFmpeg. Consumed via
`npx @hyperframes/cli` (no monorepo checkout required). Best for
HTML/CSS/GSAP motion graphics — kinetic typography, product promos,
launch reels, website-to-video workflows, registry blocks.
Name BOTH runtimes explicitly in the "Ready to go" summary when both are
available — not "Remotion" alone. A fresh-session agent that doesn't
mention HyperFrames by name will fail to present it at proposal time;
naming it here sets the expectation that the agent is runtime-agnostic.
If only one is available, note it in the summary and mention what the
other would unlock. If neither is available, tell the user their options
are FFmpeg-only (simple concat/trim) and what's needed to unlock HTML/React
composition.
**Do NOT pick a runtime during onboarding.** Runtime selection happens at
the proposal stage, after the agent understands the brief. During
onboarding you're reporting capabilities, not making production decisions.
See `AGENT_GUIDE.md` → "Present Both Composition Runtimes (HARD RULE)".
### Step 3: Greet and Orient
Present a **short, friendly capability summary**. Do NOT dump the raw provider menu. Instead, translate it into plain language.
+13 -1
View File
@@ -108,7 +108,7 @@ Structure your review as:
| Stage | What matters most |
|-------|-----------------|
| research | Source diversity, claim verifiability, visual reference quality |
| proposal | Delivery promise clarity, renderer family selection, music/voice plan, decision log started |
| proposal | Delivery promise clarity, renderer family AND render runtime selection, music/voice plan, decision log started |
| idea | Hook uniqueness, research depth, angle diversity |
| script | Timing accuracy, narrative arc, enhancement cue density |
| scene_plan | Full coverage, visual variety, asset feasibility, slideshow risk score |
@@ -220,6 +220,18 @@ Run at **scene_plan** and **edit** stages. Prevents the "every video looks the s
- Does `renderer_family` in edit_decisions match what was set at proposal?
- If changed without documented reason in decision log → **CRITICAL**
5. **Render runtime match** (edit and compose stages):
- `render_runtime` in edit_decisions must match proposal_packet.production_plan.render_runtime
- If changed without a `render_runtime_selection` decision logged in decision_log → **CRITICAL**
- At compose stage, `final_review.checks.promise_preservation.runtime_swap_detected` must be `false`. If `true` without an approved `render_runtime_selection` decision → **CRITICAL**
- Runtime unavailable at compose time is not an excuse for silent swap — the correct behavior is to escalate, get approval, log a decision, then run.
6. **Runtime selection presented both options** (proposal stage, MANDATORY):
- Query `video_compose.get_info()["render_engines"]`. If both `remotion` and `hyperframes` show `True`, the `render_runtime_selection` decision in `decision_log` MUST have BOTH runtimes in `options_considered`.
- A `render_runtime_selection` with only one runtime in `options_considered` when both were available on the machine → **CRITICAL**. The agent silently defaulted; the user was not presented the alternative. Re-open the proposal stage and present both.
- If only one runtime was available, `options_considered` must still list the unavailable one with `rejected_because: "runtime not available on this machine"` — otherwise the audit trail loses the fact that the choice was constrained, not discretionary.
- Per AGENT_GUIDE.md > "Present Both Composition Runtimes (HARD RULE)": the pipeline's suggested "default" runtime is NOT a license to skip the conversation with the user.
## Delivery Promise Review
Run at **edit** and **compose** stages. Uses `lib/delivery_promise.py`.
@@ -4,6 +4,22 @@
Render the animation with an emphasis on text sharpness, timing integrity, and consistent output cadence. For `image_animation` approach, this stage also includes building the composition JSON, sourcing music, running pre-render validation, and performing post-render self-review.
## Runtime Routing (MANDATORY first step)
Before any other work, read `edit_decisions.render_runtime`. It was locked at proposal and MUST NOT be changed silently. The rest of this skill assumes `render_runtime="remotion"` (the default for this pipeline). If the proposal locked a different runtime:
- **`render_runtime="hyperframes"`** — HTML/CSS/GSAP render. Do NOT follow the Remotion-specific sections below (public/ staging, Remotion composition JSON). Instead:
1. Read `skills/core/hyperframes.md` for the full routing model.
2. Read `.agents/skills/hyperframes/SKILL.md` and `.agents/skills/hyperframes-cli/SKILL.md` for authoring contract and CLI usage.
3. Call `video_compose` with `edit_decisions.render_runtime="hyperframes"` — it delegates to `hyperframes_compose`, which owns workspace materialization under `projects/<name>/hyperframes/`, runs `hyperframes lint → validate → render`, and returns the MP4 path.
4. `hyperframes lint` and `hyperframes validate` MUST both pass before render. Never skip validate; contrast can be deferred with `skip_contrast=true` during iteration but not for final delivery.
- **`render_runtime="ffmpeg"`** — simple concat/trim with no composition. Call `video_compose` directly; it will not auto-upgrade to Remotion.
- **Runtime unavailable** — do NOT silently swap to a different engine. Surface the blocker to the user per AGENT_GUIDE.md > "Escalate Blockers Explicitly" and wait for approval (recorded as a `render_runtime_selection` decision in decision_log) before switching.
The post-render self-review (final_review) is identical across runtimes — same ffprobe probe, frame sampling, audio spotcheck, and promise preservation checks. `final_review.checks.promise_preservation.render_runtime_used` must equal the runtime that actually ran.
**Pass `proposal_packet` to `video_compose.execute()`** when you invoke it. That lets the tool directly compare the proposal-locked runtime against the runtime recorded in `edit_decisions` and flip `runtime_swap_detected=true` if they diverge. Without it, the check is `skipped` and the reviewer skill has to catch swaps via cross-artifact comparison instead.
## Prerequisites
| Layer | Resource | Purpose |
+78 -42
View File
@@ -8,6 +8,34 @@ You are the **Proposal Director** for a generated animation video. You sit betwe
Animation proposals have a unique dimension: **animation mode selection**. Unlike explainer videos where the visual approach is secondary to the narrative, animation videos ARE their visual approach. The mode choice (Manim vs Remotion vs AI video vs motion graphics) fundamentally shapes the entire production.
## Runtime Selection (required field — `render_runtime`)
Animation proposals must lock **both** a `renderer_family` (creative grammar) and a `render_runtime` (technical engine). These are separate concepts now that HyperFrames is a first-class runtime. Read `skills/meta/animation-runtime-selector.md` and `skills/core/hyperframes.md` for the decision matrix, and `AGENT_GUIDE.md` → "Present Both Composition Runtimes (HARD RULE)" for the governance contract.
**MANDATORY workflow — present both runtimes, don't silently default:**
1. Query `video_compose.get_info()["render_engines"]`. If both `remotion` and `hyperframes` are `True`, proceed to step 2. If only one is available, go to step 4 with just that one.
2. Present both runtimes to the user with brief-specific analysis:
- **Remotion** — one line on fit (e.g. "your brief uses data-chart and stat_card heavily, both already exist as React components"), one line on tradeoff (e.g. "React component authoring is more rigid than HTML/CSS for custom typographic motion").
- **HyperFrames** — one line on fit (e.g. "the kinetic-typography opener fits HTML + GSAP better than Remotion interpolation"), one line on tradeoff (e.g. "no word-level caption burn parity yet; no access to existing Remotion chart library").
3. Recommend one with rationale tied to the brief's `delivery_promise`, the selected animation mode, and the reuse strategy from research.
4. Wait for explicit user approval. Do NOT write `render_runtime` into `proposal_packet.production_plan` before approval.
5. Log a `render_runtime_selection` decision in `decision_log` with BOTH runtimes in `options_considered`, the user's pick as `selected`, and the rationale as `reason`. If a runtime was unavailable, record it as rejected with `rejected_because: "runtime not available on this machine"`.
Fit cheat-sheet for the recommendation (NOT an auto-decision):
| Brief characteristic | Lean toward |
|----------------------|-------------|
| Data-chart-heavy, text_card/stat_card/kpi_grid dominant | Remotion |
| MathAnimate / Manim scene in the animatic | Remotion (Manim renders to a video, composed in Remotion) |
| Kinetic typography, product promo, launch reel, HTML/GSAP-native motion | HyperFrames |
| Website-to-video or UI-driven composition | HyperFrames |
| Registry blocks needed (data-chart, grain-overlay, shader transitions) | HyperFrames |
| Word-level/karaoke caption burn required | Remotion (HyperFrames caption parity deferred) |
| Simple source-footage concat, no composition | ffmpeg |
A `render_runtime_selection` decision with only one option considered when both were available is a CRITICAL reviewer finding. That's how the moat collapses into "everything looks like our chart stack."
## Prerequisites
| Layer | Resource | Purpose |
@@ -95,50 +123,51 @@ This is the key differentiator from the explainer proposal. **Present the user w
#### Step 3a: Tool Availability Scan
Before designing concepts, scan what's available and present it honestly:
Before designing concepts, scan what's available and present it honestly. **Do NOT hardcode provider names, costs, or key names in this output** — they drift. Read them live from the registry:
```python
from tools.tool_registry import registry
registry.discover()
summary = registry.provider_menu_summary() # see AGENT_GUIDE.md > Mandatory Preflight
```
Then render the scan from `summary`, grouping by capability. Example shape you should **generate from the registry**, not copy:
```
TOOL AVAILABILITY SCAN
──────────────────────
Image generation:
FLUX (fal.ai) — FAL_KEY detected — $0.03-0.05/image
gpt-image-1 — OPENAI_API_KEY missing — $0.13/image
❌ Stable Diffusion — Not installed locally — Free
❌ FLUX (local) — Not installed locally — Free
Video generation:
❌ Runway Gen-3 — No API key — $0.50/clip
❌ Kling — No API key — $0.10-0.30/clip
❌ CogVideoX (local) — Not installed — Free
Composition:
✅ Remotion — Installed — Free (local CPU)
✅ FFmpeg — Installed — Free
Audio:
✅ Pixabay Music — No key needed — Free
❌ OpenAI TTS — OPENAI_API_KEY missing — $0.015/min
✅ Local TTS (piper) — Not checked — Free
Math/Diagram:
❌ ManimCE — Not installed — Free
✅ diagram_gen — Available — Free
Image generation: {configured}/{total}
{tool_name} ({provider}) — available
{tool_name} ({provider}) — {install_instructions trimmed to one line}
Video generation: {configured}/{total}
...
Composition runtimes: {ffmpeg} / {remotion} / {hyperframes}
See AGENT_GUIDE.md > "Present Both Composition Runtimes (HARD RULE)".
Audio: {configured}/{total}
Math/Diagram: {configured}/{total}
```
**Rules for this output:**
- Every name, provider, cost, and install instruction comes from `provider_menu_summary()` or `provider_menu()`. Don't type them from memory — provider surfaces change between releases.
- Never cite a cost that isn't live in the tool's `estimate_cost` or install metadata.
- Composition runtimes are a separate section because the "Present Both" HARD RULE needs all three engines visible.
**Present this scan to the user.** Say: "Here's what I can see right now. Based on this, here are your animation approach options."
#### Step 3b: Animation Approach Decision Matrix
Present the approaches as clear options:
| Approach | What It Looks Like | Tools Required | Cost Range | Proven? |
|----------|-------------------|----------------|------------|---------|
| **A: Image-Based Animation (Remotion)** | AI-generated keyframes with crossfade, camera motion, particles. Looks like moving anime/illustration. | `image_selector` (any provider) + Remotion | $0.03-0.13/image × 2-3/scene | ✅ Proven (mori-no-seishin) |
| **B: Clip-Based Video** | AI-generated video clips assembled as a story. Most cinematic but least consistent. | `video_selector` (Runway/Kling/etc.) | $0.10-0.50/clip × scenes | ❌ Not yet proven |
| Approach | What It Looks Like | Tools Required | Cost | Proven? |
|----------|-------------------|----------------|------|---------|
| **A: Image-Based Animation (Remotion)** | AI-generated keyframes with crossfade, camera motion, particles. Looks like moving anime/illustration. | `image_selector` (any provider) + Remotion | Pull per-image cost from the chosen provider's `estimate_cost`; 2-3 images per scene is typical | ✅ Proven (mori-no-seishin) |
| **B: Clip-Based Video** | AI-generated video clips assembled as a story. Most cinematic but least consistent. | `video_selector` routing to whichever provider is available | Pull per-clip cost from the chosen provider's `estimate_cost`; varies widely between providers | ❌ Not yet proven |
| **C: Programmatic Animation (Manim)** | Code-driven math/geometry animation. Precise, clean, 3Blue1Brown style. | `math_animate` (ManimCE) | Free (local) | ❌ Not yet proven |
| **D: Data Visualization (Remotion)** | Animated charts, KPIs, kinetic typography. Data-driven storytelling. | Remotion (built-in components) | Free (local) | ✅ Proven (zero-key formula) |
| **E: Diagram + Image Stills** | Process flows and architecture diagrams with Ken Burns. | `diagram_gen` + `image_selector` | $0-0.05/image | ✅ Proven |
| **F: Mixed Mode** | Combine any of the above per-scene. Most flexible. | Multiple tools | Varies | Partial |
| **E: Diagram + Image Stills** | Process flows and architecture diagrams with Ken Burns. | `diagram_gen` + `image_selector` | `diagram_gen` is free; per-image cost from `image_selector`'s routed provider | ✅ Proven |
| **F: Mixed Mode** | Combine any of the above per-scene. Most flexible. | Multiple tools | Sum per-scene from each tool's `estimate_cost` | Partial |
**Rule:** do NOT fill in a dollar figure in the Cost column from memory. Read every cost live via `estimate_cost()` or `provider_menu_summary()` at proposal time. Provider pricing changes between releases.
**For each viable approach, present to the user:**
@@ -150,15 +179,15 @@ camera motion (zoom, pan, ken-burns) and particle overlays (fireflies, mist,
sparkles). Creates the illusion of movement from still frames.
You need: An image generation API key.
→ You already have: FAL_KEY (FLUX at $0.05/image)
→ Alternative: Install Stable Diffusion locally (free, slower)
→ Alternative: Add OPENAI_API_KEY for gpt-image-1 ($0.13/image)
→ You already have: {from provider_menu_summary: available image_generation providers}
→ Alternative: {from setup_offers: 1-env-var image_generation tools}
→ Alternative: local Stable Diffusion (see local_diffusion tool install_instructions)
Estimated cost for 30s video: ~$0.65 (13 images)
Estimated cost for 5min video: ~$6.00 (120 images)
Estimated cost for 30s video: pull per-image costs from each provider's
`estimate_cost` (do NOT hardcode — they drift between releases).
Style options: anime-ghibli, painterly, photorealistic, watercolor
Reference: remotion-composer/public/demo-props/mori-no-seishin.json
Style options: depend on the picked provider; read from playbook and
provider-specific Layer 3 skill (e.g. `.agents/skills/flux-best-practices`).
APPROACH B: Clip-Based Video
─────────────────────────────
@@ -166,11 +195,13 @@ What it looks like: AI-generated 3-5 second video clips assembled as a story.
Most cinematic output but hardest to maintain visual consistency across clips.
You need: A video generation API key.
→ Currently available: None detected
→ To enable: Add RUNWAY_API_KEY, KLING_API_KEY, or install CogVideoX locally
→ Currently available: {from provider_menu_summary: available video_generation providers}
→ To enable: {from setup_offers: 1-env-var video_generation tools}, or
install a local video model (see video_selector fallback_tools).
Estimated cost for 30s video: $3-15 depending on provider
Estimated cost for 5min video: $30-150
Estimated cost for 30s video: pull from each provider's `estimate_cost` on the
actual clip plan — per-clip costs range widely between providers and change
often. Do NOT hardcode.
Note: This approach is not yet proven in the OpenMontage pipeline.
Consistency across clips is the #1 challenge.
@@ -178,6 +209,11 @@ Note: This approach is not yet proven in the OpenMontage pipeline.
**Critical principle: Surface capabilities, don't hide limitations.** The user should know exactly what's possible right now vs. what needs setup.
**Rules for this section — same as Step 3a:**
- Every provider name, env var, and cost comes from `provider_menu_summary()` or a tool's live `install_instructions` / `estimate_cost`.
- The `{placeholder}` tokens above are for the agent to fill from the registry, not paste literally.
- If you find yourself typing a specific API-key env-var name or a per-unit dollar cost into this section, stop. Those drift between releases; hardcoding them in a director skill is a governance regression (see AGENT_GUIDE.md on hardcoded provider names). Pull the same data from the registry instead.
#### Step 3c: Mode Selection Rules
- If the topic is visual/artistic (anime, illustration, fantasy) → **Approach A** (image-based)
@@ -403,7 +439,7 @@ Validate the `proposal_packet` artifact against `schemas/artifacts/proposal_pack
## Common Pitfalls
- **Not showing the Tool Availability Scan**: The user must know what's available BEFORE seeing concepts. Don't hide missing keys or tools.
- **Ignoring animation approach feasibility**: If FLUX isn't available, don't propose image_animation without saying "you need to add FAL_KEY first." Design around constraints OR explicitly state what's needed.
- **Ignoring animation approach feasibility**: If the routed image/video provider isn't available, don't propose that approach without explicitly telling the user what's needed. Read each missing tool's `install_instructions` from the registry (do NOT hardcode specific env var names here — they drift). Design around constraints OR explicitly state what's needed.
- **Three versions of the same concept with different titles**: Structural diversity means different animation approaches, different narrative structures, different hooks.
- **Not leveraging free tools**: Animation has a huge cost advantage — Manim, Remotion data-viz, and diagram_gen are free. If proposing expensive AI video, justify why free alternatives won't work.
- **Over-promising visual complexity**: 20 unique hand-crafted scenes is not realistic. Design reuse strategies that look varied but share underlying templates.
@@ -4,6 +4,14 @@
Render the final spokesperson outputs. The bar is simple: the presenter must look stable, speech must be clear, and subtitles or support cards must not crowd the frame.
## Runtime Routing (HARD CONSTRAINT — Remotion only)
Phase 1 deferred from HyperFrames. `edit_decisions.render_runtime` must be `"remotion"`. This pipeline depends on the Remotion `TalkingHead` composition and `remotion_caption_burn` — both have no HyperFrames parity in Phase 1.
- If `edit_decisions.render_runtime == "hyperframes"`, stop. Re-open the idea stage and surface the constraint. Silent rewrite is a governance violation.
- Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": the lock to remotion is NOT an excuse to skip the conversation. The user deserves to know that HyperFrames exists as a runtime and why it isn't viable for avatar-spokesperson. Log a `render_runtime_selection` decision with hyperframes `rejected_because: "TalkingHead + caption parity deferred on avatar-spokesperson"`.
- Pass `proposal_packet`/`brief` to `video_compose.execute()` for in-tool runtime-swap detection.
## Prerequisites
| Layer | Resource | Purpose |
@@ -6,6 +6,12 @@ Use this pipeline when the deliverable is a presenter-led avatar video: a spokes
Your first job is to classify the avatar path honestly before anyone writes polished copy for an impossible production setup.
## Runtime Selection (MANDATORY — present the constraint, don't silently pick)
Lock `render_runtime = "remotion"`. **HyperFrames is NOT a valid runtime on this pipeline in Phase 1** — avatar-spokesperson depends on the Remotion `TalkingHead` composition and `remotion_caption_burn`, and neither has HyperFrames parity yet.
Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": do NOT silently default. Tell the user: "HyperFrames is available on your machine, but avatar-spokesperson depends on the Remotion TalkingHead composition and caption burn, so remotion is the only viable runtime here — OK to proceed?" Record a `render_runtime_selection` decision with hyperframes `rejected_because: "TalkingHead + caption parity deferred on avatar-spokesperson"`.
## Reference Inputs
- `docs/avatar-spokesperson-best-practices.md`
@@ -4,6 +4,18 @@
Render the cinematic piece with careful attention to grade, audio dynamics, and frame treatment. This is not a generic export step.
## Runtime Routing (MANDATORY first step)
Read `edit_decisions.render_runtime`. Cinematic work routes to:
- **`render_runtime="remotion"`** — default for video-led trailers using `CinematicRenderer`. Keeps video clips, transitions, and ambient overlays in one React-based pass.
- **`render_runtime="hyperframes"`** — for kinetic title cards, HTML/GSAP-driven trailers, or launch-reel-style compositions where the visual grammar is HTML/CSS. See `skills/core/hyperframes.md`. `hyperframes lint` and `hyperframes validate` must both pass before render.
- **`render_runtime="ffmpeg"`** — simple source-footage concat with no composition.
`delivery_promise.motion_required=true` means the locked runtime is a commitment. Silent swap to another runtime (including FFmpeg Ken Burns) is a CRITICAL governance violation. If the locked runtime fails, escalate per AGENT_GUIDE.md > "Escalate Blockers Explicitly."
**Pass `proposal_packet` to `video_compose.execute()`** so the tool's `runtime_swap_detected` check compares directly against `proposal_packet.production_plan.render_runtime`. Without it the swap check is skipped in-tool and only the reviewer skill catches the drift.
## Prerequisites
| Layer | Resource | Purpose |
@@ -6,6 +6,31 @@ You are the **Proposal Director** for a cinematic video (trailers, brand films,
**This is the approval gate.** Nothing downstream runs until the user says "go."
## Runtime Selection (required field — `render_runtime`)
Cinematic proposals must lock **both** a `renderer_family` (creative grammar: `cinematic-trailer`, `documentary-montage`, etc.) and a `render_runtime` (technical engine). Read `skills/meta/animation-runtime-selector.md` and `skills/core/hyperframes.md` for the decision matrix, and `AGENT_GUIDE.md` → "Present Both Composition Runtimes (HARD RULE)" for the governance contract.
**MANDATORY workflow — present both runtimes, don't silently default:**
1. Query `video_compose.get_info()["render_engines"]`. If both `remotion` and `hyperframes` are `True`, proceed to step 2.
2. Present both runtimes to the user with brief-specific analysis:
- **Remotion** — one line on fit (mention `CinematicRenderer`, `<OffthreadVideo>`, existing transition stack if applicable), one line on tradeoff.
- **HyperFrames** — one line on fit (mention kinetic title sequences, registry shader transitions, or HTML-native typographic motion if applicable), one line on tradeoff.
3. Recommend one with rationale tied to the brief's `delivery_promise` (especially `motion_required`), `renderer_family`, and approved tone.
4. Wait for explicit user approval. Do NOT write `render_runtime` into `proposal_packet.production_plan` before approval.
5. Log a `render_runtime_selection` decision in `decision_log` with BOTH runtimes in `options_considered` plus `ffmpeg` if it was a realistic option.
Fit cheat-sheet for the recommendation (NOT an auto-decision):
- Video-led trailer with motion clips via `<OffthreadVideo>` + color-graded overlays → lean **Remotion**.
- HTML/GSAP-driven trailer: kinetic title sequence, launch reel, brand film where the visual grammar is typographic → lean **HyperFrames**.
- Shader transitions or registry grain overlays → lean **HyperFrames**.
- Simplest source-footage concat with no composition → **ffmpeg**.
**Motion-required deliverables**: if `delivery_promise.motion_required=true`, the chosen runtime is a commitment. Silent downgrade to FFmpeg Ken Burns or still-led animatic is forbidden. If the chosen runtime becomes unavailable at render time, compose must escalate, not substitute.
A `render_runtime_selection` decision with only one option considered when both were available is a CRITICAL reviewer finding.
## Prerequisites
| Layer | Resource | Purpose |
@@ -4,6 +4,14 @@
Render each clip and platform variant independently. The important behaviors here are consistency, batch resilience, and clear reporting of partial failures.
## Runtime Routing (HARD CONSTRAINT — Remotion or FFmpeg only)
This pipeline is Phase 1 deferred from the HyperFrames adoption schedule. `edit_decisions.render_runtime` must be `"remotion"` (default) or `"ffmpeg"` (pure-concat clip jobs with no composition). HyperFrames is NOT a valid runtime here — clip-factory depends on Remotion word-level caption burn, and HyperFrames caption parity is deferred work.
- If `edit_decisions.render_runtime == "hyperframes"`, stop. Re-open the idea stage so the user can be presented the real constraint and lock `remotion` with a `render_runtime_selection` decision that records `hyperframes` as `rejected_because: "caption-burn parity deferred on clip-factory"`.
- Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": the constraint is NOT an excuse to skip the conversation. The user still gets to see that HyperFrames exists and why it isn't viable here.
- Pass `proposal_packet`/`brief` to `video_compose.execute()` so the in-tool runtime-swap check runs end-to-end.
## Prerequisites
| Layer | Resource | Purpose |
@@ -6,6 +6,12 @@ Use this pipeline when the source is long-form footage and the goal is multiple
You are not planning one video. You are planning a ranked portfolio of clips.
## Runtime Selection (MANDATORY — present the constraint, don't silently pick)
Lock `render_runtime = "remotion"` (for composed clips with word-level captions) or `"ffmpeg"` (for pure concat/trim with no composition). **HyperFrames is NOT a valid runtime on this pipeline in Phase 1** — clip-factory depends on Remotion's word-level caption burn, which has no HyperFrames parity yet.
Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": do NOT silently lock remotion. Surface the constraint to the user: "HyperFrames is an available runtime on your machine, but clip-factory depends on Remotion caption burn that doesn't have HyperFrames parity yet, so remotion is the only viable choice here — OK to proceed?" Record the decision in `decision_log` with category `render_runtime_selection`, including hyperframes as a rejected option (`rejected_because: "caption-burn parity deferred on clip-factory"`).
## Reference Inputs
- `docs/clip-factory-best-practices.md`
@@ -9,6 +9,14 @@ mix) that makes a mixed-era corpus feel like one film.
The output is a single mp4 plus a `render_report` artifact.
## Runtime Routing (HARD CONSTRAINT)
This pipeline currently REQUIRES `render_runtime="remotion"`. The end-tag stack (ProRes 4444 overlay composited on final scenes, or concat fallback) depends on Remotion's `CinematicRenderer` composition and its alpha-preserving render path. HyperFrames end-tag parity is explicitly Wave 3 / deferred work (see `skills/core/hyperframes.md` → "What stays Remotion-only in Phase 1").
- If `edit_decisions.render_runtime` is anything other than `remotion`, stop. This is a CRITICAL governance violation. Surface the conflict to the user, route the decision back to proposal to re-lock `render_runtime="remotion"`, log a `render_runtime_selection` correction in decision_log, and resume.
- Never silently proceed by rewriting render_runtime in edit_decisions. The documentary promise (motion-led, mood-driven, uniform grade) is preserved by the Remotion stack, and that promise is what the user approved.
- Pass `proposal_packet` to `video_compose.execute()` so the in-tool `runtime_swap_detected` check actively confirms the runtime stayed `remotion` end-to-end. A `skipped` check on this pipeline means you forgot to pass the proposal artifact.
## Prerequisites
| Layer | Resource | Purpose |
@@ -7,6 +7,12 @@ downstream stage will read. For this pipeline, the brief is the
thematic core: what the montage is ABOUT, what it should feel like,
and how long it should run.
## Runtime Selection (MANDATORY — present the constraint, don't silently pick)
Lock `render_runtime = "remotion"`. **HyperFrames is NOT a valid runtime on this pipeline in Phase 1** — documentary-montage depends on the Remotion `CinematicRenderer` composition and its ProRes-4444 alpha end-tag overlay stack, neither of which has HyperFrames parity.
Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": do NOT silently default. Tell the user: "HyperFrames is available on your machine as an alternative runtime, but documentary-montage depends on the Remotion CinematicRenderer + end-tag overlay stack, so remotion is the only viable choice here — OK to proceed?" Record a `render_runtime_selection` decision in `decision_log` listing both runtimes in `options_considered`, with hyperframes `rejected_because: "CinematicRenderer + end-tag overlay parity deferred on documentary-montage"`.
## Prerequisites
| Layer | Resource | Purpose |
@@ -6,6 +6,18 @@ You are the Compositor for a generated explainer video. You have `edit_decisions
This is the last technical stage before the video exists as a playable file. Everything converges here.
## Runtime Routing (MANDATORY first step)
Read `edit_decisions.render_runtime` before anything else. It was locked at proposal and must not be changed silently. The rest of this skill's process steps (Remotion public/ staging, word-level caption burn, etc.) assume `render_runtime="remotion"` — the default for data-driven explainers.
- **`render_runtime="hyperframes"`** — HTML/CSS/GSAP render. Do NOT follow the Remotion-specific steps below. Instead: read `skills/core/hyperframes.md`, `.agents/skills/hyperframes/SKILL.md`, and `.agents/skills/hyperframes-cli/SKILL.md`. Call `video_compose` with the edit_decisions unchanged — it will delegate to `hyperframes_compose`, which materializes a workspace under `projects/<name>/hyperframes/`, runs `lint → validate → render`, and returns the MP4. Both lint AND validate must pass before render; contrast can be deferred during iteration but not for final delivery.
- **`render_runtime="ffmpeg"`** — simple concat/trim. Call `video_compose` directly; it will NOT auto-upgrade to Remotion when this runtime is explicitly locked.
- **Runtime unavailable** — surface the blocker per AGENT_GUIDE.md > "Escalate Blockers Explicitly" and get user approval (recorded as a `render_runtime_selection` decision in decision_log) before switching.
`final_review.checks.promise_preservation.render_runtime_used` must equal the runtime that actually ran; `runtime_swap_detected` must be `false` unless an approved decision authorizes the swap.
**Pass `proposal_packet` to `video_compose.execute()`** so in-tool swap detection can actually fire. Without it the `runtime_swap_check` is reported as `skipped` and you have to rely on the reviewer skill's cross-artifact comparison instead.
## Prerequisites
| Layer | Resource | Purpose |
@@ -8,6 +8,28 @@ You are the **Proposal Director** for a generated explainer video. You sit betwe
Think of yourself as a creative agency pitching to a client: you present concepts backed by research, show what it'll cost, explain the tradeoffs, and let the client choose.
## Runtime Selection (required field — `render_runtime`)
Explainer proposals must lock **both** a `renderer_family` (creative grammar) and a `render_runtime` (technical engine). Read `skills/meta/animation-runtime-selector.md` for the decision matrix and `AGENT_GUIDE.md` → "Present Both Composition Runtimes (HARD RULE)" for the governance contract.
**MANDATORY workflow — present both runtimes, don't silently default:**
1. Query `video_compose.get_info()["render_engines"]`. If both `remotion` and `hyperframes` are `True`, proceed to step 2. If only one is available, go to step 4 with just that one.
2. Present both runtimes to the user with brief-specific analysis. For THIS concept:
- **Remotion** — one line on fit (mention the React scene stack components that apply), one line on tradeoff.
- **HyperFrames** — one line on fit (mention HTML/GSAP motion, registry blocks, kinetic typography if applicable), one line on tradeoff.
3. Recommend one with rationale tied to the brief's `delivery_promise`, `visual_approach`, and whether word-level caption burn is required (that one forces Remotion).
4. Wait for explicit user approval. Do NOT write `render_runtime` into `proposal_packet.production_plan` before approval.
5. Log a `render_runtime_selection` decision in `decision_log` with BOTH runtimes (plus `ffmpeg` if it was a realistic option) in `options_considered`, the user's pick as `selected`, and the rationale as `reason`. If a runtime was unavailable, record it as rejected with `rejected_because: "runtime not available on this machine"`.
Fit cheat-sheet for recommendation (input for the conversation, not an auto-decision):
- Existing React scene stack (text_card, stat_card, bar_chart, line_chart, pie_chart, kpi_grid, callout, comparison, hero_title, caption overlay, anime_scene) fits → recommend **Remotion**.
- Kinetic typography, custom HTML motion graphics, registry-block-driven scenes, or website-to-video → recommend **HyperFrames**.
- Word-level/karaoke captions required → **Remotion only** in Phase 1 (caption parity is deferred).
A `render_runtime_selection` decision with only one option considered when both were available is a CRITICAL reviewer finding.
## Prerequisites
| Layer | Resource | Purpose |
@@ -4,6 +4,18 @@
Render the hybrid project so source media, support graphics, and audio all remain coherent across outputs.
## Runtime Routing (MANDATORY first step)
Read `edit_decisions.render_runtime`. Hybrid work typically sticks with Remotion because source footage + React support overlays compose cleanly in one pass:
- **`render_runtime="remotion"`** — default. Source footage via `<OffthreadVideo>`, support graphics as React components, one render.
- **`render_runtime="hyperframes"`** — pick only when the support layer is HTML/GSAP-native (e.g., animated text callouts, registry blocks). Source footage is still possible via `<video class="clip">` but lose some of the Remotion component stack. See `skills/core/hyperframes.md`.
- **`render_runtime="ffmpeg"`** — rare on this pipeline; implies no generated support layer.
Silent runtime swap is a CRITICAL governance violation. Escalate blockers per AGENT_GUIDE.md before substituting.
**Pass `proposal_packet` to `video_compose.execute()`** so the tool's in-tool swap-detection check runs against the proposal directly instead of being `skipped`.
## Prerequisites
| Layer | Resource | Purpose |
+13
View File
@@ -6,6 +6,19 @@ Use this pipeline when the project combines real source media with support visua
Hybrid is not a catch-all. Your first job is to define what stays primary.
## Runtime Selection (MANDATORY — present both runtimes)
Before locking the production plan, decide `render_runtime` with the user. Hybrid supports BOTH Remotion and HyperFrames; neither is an auto-default. Follow the contract in AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)":
1. Query `video_compose.get_info()["render_engines"]`. If both `remotion` and `hyperframes` are `True`, present both to the user with brief-specific analysis:
- **Remotion** — fits when source footage dominates and support layers are React scene components (chart, callout, text card). Remotion composes video clips + React overlays in one pass via `<OffthreadVideo>`.
- **HyperFrames** — fits when support layers are HTML/GSAP-native (kinetic callouts, registry blocks, typographic overlays) and source footage is embedded as `<video class="clip">`.
2. Recommend one with rationale tied to the anchor medium and the shape of the support layer.
3. Wait for explicit user approval.
4. Log the choice in `decision_log` as a `render_runtime_selection` decision with BOTH runtimes in `options_considered`.
A `render_runtime_selection` decision with only one runtime in `options_considered` when both were available is a CRITICAL reviewer finding.
## Reference Inputs
- `docs/hybrid-video-best-practices.md`
@@ -4,6 +4,14 @@
Render the localized outputs. The quality bar is intelligibility, timing coherence, and clear version labeling across every language package.
## Runtime Routing (HARD CONSTRAINT — Remotion or FFmpeg only)
Phase 1 deferred from HyperFrames. `edit_decisions.render_runtime` must be `"remotion"` or `"ffmpeg"`. Localization depends on Remotion's caption stack (per-locale subtitle burn) and, when dubbing with lip-sync, on the Remotion TalkingHead pipeline. HyperFrames has no parity for either in Phase 1.
- If `edit_decisions.render_runtime == "hyperframes"`, stop. Re-open the idea stage and surface the constraint — don't silently rewrite the runtime.
- Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": the pipeline's constraint does NOT skip the conversation. Present the constraint to the user so they know HyperFrames exists but isn't viable here. Log a `render_runtime_selection` decision with hyperframes `rejected_because: "caption + lip-sync parity deferred on localization-dub"`.
- Pass `proposal_packet`/`brief` to `video_compose.execute()` for end-to-end runtime-swap detection.
## Prerequisites
| Layer | Resource | Purpose |
@@ -6,6 +6,12 @@ Use this pipeline when the user has a source video and wants translated delivera
Your first responsibility is to define what kind of localization is actually required, because subtitle-only, dubbed-audio, and lip-synced translation are different jobs.
## Runtime Selection (MANDATORY — present the constraint, don't silently pick)
Lock `render_runtime = "remotion"` (composed deliverables with per-locale caption burn / lip-sync) or `"ffmpeg"` (pure subtitle-burn over source with no composition). **HyperFrames is NOT a valid runtime on this pipeline in Phase 1** — localization depends on Remotion's caption stack and, for dubbed-with-lip-sync, on the Remotion TalkingHead pipeline.
Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": do NOT silently default to remotion. Tell the user: "HyperFrames is available, but localization-dub depends on Remotion caption + TalkingHead parity that isn't there yet in Phase 1 — remotion is the only viable choice". Record a `render_runtime_selection` decision with hyperframes `rejected_because: "caption + lip-sync parity deferred on localization-dub"`.
## Reference Inputs
- `docs/localization-dubbing-best-practices.md`
@@ -4,6 +4,14 @@
Render the podcast-derived outputs with audio fidelity as the top priority. The visuals need to support the speech, not compete with it.
## Runtime Routing (HARD CONSTRAINT — Remotion or FFmpeg only)
Phase 1 deferred from HyperFrames. `edit_decisions.render_runtime` must be `"remotion"` (audiograms, composed outputs) or `"ffmpeg"` (pure-audio-led clip exports). HyperFrames caption-burn parity is deferred, and podcast outputs lean on Remotion's word-level caption stack.
- If `edit_decisions.render_runtime == "hyperframes"`, stop. Re-open the idea stage and surface the constraint to the user. Never silently rewrite the runtime.
- Per AGENT_GUIDE.md → "Present Both Composition Runtimes (HARD RULE)": tell the user HyperFrames exists and why it isn't viable on this pipeline, rather than silently locking remotion. Record a `render_runtime_selection` decision with hyperframes `rejected_because: "caption-burn parity deferred on podcast-repurpose"`.
- Pass `proposal_packet`/`brief` to `video_compose.execute()` for end-to-end runtime-swap detection.
## Prerequisites
| Layer | Resource | Purpose |

Some files were not shown because too many files have changed in this diff Show More