diff --git a/CLAUDE.md b/CLAUDE.md
index 6243bab06..96dd07605 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,40 +4,22 @@
This repo ships skills that are installed globally via `npx hyperframes skills` (runs automatically during `hyperframes init`). **Always use the appropriate skill instead of writing code from scratch or fetching external docs.**
-### HyperFrames Skills (from this repo)
+### Skills
-| Skill | Invoke with | When to use |
-| ------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **hyperframes-compose** | `/hyperframes-compose` | Creating ANY HTML composition — videos, animations, title cards, overlays. Contains required HTML structure, `class="clip"` rules, GSAP timeline patterns, and rendering constraints. |
-| **hyperframes-captions** | `/hyperframes-captions` | Any task involving text synced to audio: captions, subtitles, lyrics, lyric videos, karaoke. Also covers transcription strategy (whisper model selection, transcript format). |
-| **hyperframes-tts** | `/hyperframes-tts` | Generating speech from text: narration, voiceovers, text-to-speech. Voice selection, speed control, and combining TTS output with compositions. |
-| **marker-highlight** | `/marker-highlight` | Animated text highlighting — marker sweeps, hand-drawn circles, burst lines, scribble, sketchout. Use with captions for dynamic emphasis. |
-
-### GSAP Skills (from [greensock/gsap-skills](https://github.com/greensock/gsap-skills))
-
-| Skill | Invoke with | When to use |
-| ---------------------- | --------------------- | -------------------------------------------------------------------------------- |
-| **gsap-core** | `/gsap-core` | `gsap.to()`, `from()`, `fromTo()`, easing, duration, stagger, defaults |
-| **gsap-timeline** | `/gsap-timeline` | Timeline sequencing, position parameter, labels, nesting, playback |
-| **gsap-performance** | `/gsap-performance` | Performance best practices — transforms over layout props, will-change, batching |
-| **gsap-plugins** | `/gsap-plugins` | ScrollTrigger, Flip, Draggable, SplitText, and other GSAP plugins |
-| **gsap-scrolltrigger** | `/gsap-scrolltrigger` | Scroll-linked animations, pinning, scrub, triggers |
-| **gsap-utils** | `/gsap-utils` | `gsap.utils` helpers — clamp, mapRange, snap, toArray, wrap, pipe |
+| Skill | Invoke with | When to use |
+| ------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **hyperframes** | `/hyperframes` | Creating or editing HTML compositions, captions/subtitles, TTS narration, audio-reactive animation, marker highlights. Composition authoring rules. |
+| **hyperframes-cli** | `/hyperframes-cli` | CLI commands: init, lint, preview, render, transcribe, tts, doctor. Use when scaffolding, validating, previewing, or rendering. |
+| **gsap** | `/gsap` | GSAP animations — tweens, timelines, easing, ScrollTrigger, plugins (Flip, Draggable, SplitText, etc.), React/Vue/Svelte, performance optimization. |
### Why this matters
-The skills encode HyperFrames-specific patterns (e.g., required `class="clip"` on all timed elements, GSAP timeline registration via `window.__GSAP_TIMELINE`, `data-*` attribute semantics) that are NOT in generic web docs. Skipping the skills and writing from scratch will produce broken compositions.
+The skills encode HyperFrames-specific patterns (e.g., required `class="clip"` on all timed elements, GSAP timeline registration via `window.__timelines`, `data-*` attribute semantics) that are NOT in generic web docs. Skipping the skills and writing from scratch will produce broken compositions.
### Rules
-- When creating or modifying HTML compositions → invoke `/hyperframes-compose` BEFORE writing any code
-- When adding captions, subtitles, lyrics, or any text synced to audio → invoke `/hyperframes-captions` BEFORE writing any code
-- When transcribing audio or choosing a whisper model → invoke `/hyperframes-captions` BEFORE running any transcription tool
-- When generating speech from text (narration, voiceover, TTS) → invoke `/hyperframes-tts` BEFORE running any TTS command
-- When creating a video from audio (music video, lyric video, audio visualizer with text) → invoke BOTH `/hyperframes-compose` AND `/hyperframes-captions`
-- When writing GSAP animations → invoke `/gsap-core` and `/gsap-timeline` BEFORE writing any code
-- When optimizing animation performance → invoke `/gsap-performance` BEFORE making changes
-- When adding animated text emphasis (highlight sweeps, circles, bursts, scribbles) → invoke `/marker-highlight` BEFORE writing any code
+- When creating or modifying HTML compositions, captions, TTS, audio-reactive, or marker highlights → invoke `/hyperframes` BEFORE writing any code
+- When writing GSAP animations (tweens, timelines, ScrollTrigger, plugins) → invoke `/gsap` BEFORE writing any code
- After creating or editing any `.html` composition → run `npx hyperframes lint` and `npx hyperframes validate` in parallel, fix all errors before opening the studio or considering the task complete. `lint` checks the HTML structure statically; `validate` loads the composition in headless Chrome and catches runtime JS errors, missing assets, and failed network requests. Always validate before `npx hyperframes preview`.
### Installing skills
@@ -143,7 +125,7 @@ If captions are inaccurate (wrong words, bad timing):
2. **Set language**: `--language en` to filter non-target speech
3. **Use an external API**: Transcribe via OpenAI or Groq Whisper API, then import the JSON with `hyperframes transcribe response.json`
-See the `/hyperframes-captions` skill for full details on model selection and API usage.
+See the `/hyperframes` skill (references/captions.md and references/transcript-guide.md) for full details on model selection and API usage.
## Text-to-Speech
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 3ee9a0c07..4f2146f63 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -21,7 +21,7 @@
"build:fonts": "cd ../producer && tsx scripts/generate-font-data.ts",
"build:studio": "cd ../studio && bun run build",
"build:runtime": "tsx scripts/build-runtime.ts",
- "build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/blank src/templates/_shared dist/templates/ && cp -r ../../skills/hyperframes-compose ../../skills/hyperframes-captions ../../skills/hyperframes-tts dist/skills/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
+ "build:copy": "mkdir -p dist/studio dist/docs dist/templates dist/skills && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/blank src/templates/_shared dist/templates/ && cp -r ../../skills/hyperframes ../../skills/hyperframes-cli ../../skills/gsap dist/skills/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
"typecheck": "tsc --noEmit"
},
"dependencies": {
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index ccf5b0677..89002311d 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -7,6 +7,7 @@ export const examples: Example[] = [
["Start from an existing video file", "hyperframes init my-video --video clip.mp4"],
["Start from an audio file", "hyperframes init my-video --audio track.mp3"],
["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"],
+ ["Skip AI coding skills installation", "hyperframes init my-video --skip-skills"],
];
import {
existsSync,
@@ -390,12 +391,17 @@ export default defineCommand({
type: "boolean",
description: "Disable interactive prompts (for CI/agents)",
},
+ "skip-skills": {
+ type: "boolean",
+ description: "Skip AI coding skills installation",
+ },
},
async run({ args }) {
const templateFlag = args.template;
const videoFlag = args.video;
const audioFlag = args.audio;
const skipTranscribe = args["skip-transcribe"] === true;
+ const skipSkills = args["skip-skills"] === true;
const nonInteractive = args["non-interactive"] === true;
const modelFlag = args.model;
const languageFlag = args.language;
@@ -693,17 +699,19 @@ export default defineCommand({
clack.note(files.map((f) => c.accent(f)).join("\n"), c.success(`Created ${name}/`));
// Offer to install AI coding skills
- const installSkills = await clack.confirm({
- message: "Install AI coding skills? (for Claude Code, Cursor, Codex, etc.)",
- initialValue: true,
- });
- if (clack.isCancel(installSkills)) {
- clack.cancel("Setup cancelled.");
- process.exit(0);
- }
- if (installSkills) {
- const skillsCmd = await import("./skills.js").then((m) => m.default);
- await runCommand(skillsCmd, { rawArgs: [] });
+ if (!skipSkills) {
+ const installSkills = await clack.confirm({
+ message: "Install AI coding skills? (for Claude Code, Cursor, Codex, etc.)",
+ initialValue: true,
+ });
+ if (clack.isCancel(installSkills)) {
+ clack.cancel("Setup cancelled.");
+ process.exit(0);
+ }
+ if (installSkills) {
+ const skillsCmd = await import("./skills.js").then((m) => m.default);
+ await runCommand(skillsCmd, { rawArgs: [] });
+ }
}
// Auto-launch studio preview
diff --git a/packages/cli/src/templates/_shared/CLAUDE.md b/packages/cli/src/templates/_shared/CLAUDE.md
index 0e74629f2..7a65b2955 100644
--- a/packages/cli/src/templates/_shared/CLAUDE.md
+++ b/packages/cli/src/templates/_shared/CLAUDE.md
@@ -4,13 +4,11 @@
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `class="clip"`, `window.__timelines`, `data-*` attributes) that are NOT in generic web docs. Skipping them produces broken compositions.
-| Skill | Command | When to use |
-| ------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------ |
-| **hyperframes-compose** | `/hyperframes-compose` | Creating or editing ANY HTML composition — videos, animations, title cards, overlays, sub-compositions |
-| **hyperframes-captions** | `/hyperframes-captions` | Any text synced to audio: captions, subtitles, lyrics, karaoke. Also covers transcription strategy. |
-| **gsap-core** | `/gsap-core` | GSAP tweens: `gsap.to()`, `from()`, `fromTo()`, easing, stagger, defaults |
-| **gsap-timeline** | `/gsap-timeline` | Timeline sequencing, position parameter, labels, nesting |
-| **gsap-performance** | `/gsap-performance` | Animation performance — transforms over layout props, will-change, batching |
+| Skill | Command | When to use |
+| ------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
+| **hyperframes** | `/hyperframes` | Creating or editing HTML compositions, captions, TTS, audio-reactive animation, marker highlights |
+| **hyperframes-cli** | `/hyperframes-cli` | CLI commands: init, lint, preview, render, transcribe, tts |
+| **gsap** | `/gsap` | GSAP animations — tweens, timelines, easing, ScrollTrigger, plugins, React/Vue/Svelte, performance optimization |
> **Skills not available?** Ask the user to run `npx hyperframes skills` and restart their
> agent session, or install manually: `npx skills add heygen-com/hyperframes` and
diff --git a/skills/audio-reactive/SKILL.md b/skills/audio-reactive/SKILL.md
deleted file mode 100644
index 8eaee925c..000000000
--- a/skills/audio-reactive/SKILL.md
+++ /dev/null
@@ -1,98 +0,0 @@
----
-name: audio-reactive
-description: Audio-reactive animation — drive visuals from music, voice, or sound in HyperFrames. Maps frequency bands and amplitude to any GSAP-animatable property.
-trigger: Use when a composition involves music, beat-synced animation, audio visualization, or any visual reacting to sound.
----
-
-# Audio-Reactive Animation
-
-When audio data is available (extracted via `extract-audio-data.py` or loaded from `audio-data.json`), any visual element in the composition can be driven by the music — captions, backgrounds, shapes, overlays, anything GSAP can animate.
-
-## Audio Data Format
-
-```js
-var AUDIO_DATA = {
- fps: 30, // frame rate of the analysis
- totalFrames: 900, // total analyzed frames
- frames: [
- { bands: [0.82, 0.45, 0.31, ...] }, // per-frame frequency band amplitudes
- // ...
- ]
-};
-```
-
-- `frames[i].bands[]` — frequency band amplitudes, normalized 0–1. Index 0 = bass, higher indices = mids and treble.
-- `fps` — frame rate of the analysis (matches composition frame rate)
-- `totalFrames` — total number of analyzed frames
-
-## Mapping Audio to Visuals
-
-Map frequency bands and amplitude to any GSAP-animatable property. The creative choice is yours — these are common mappings:
-
-| 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 |
-| Beat onset | `scale`, `color`, `rotation` | Flash or pop on hits |
-| Mid-range (bands[4–8]) | `borderRadius`, `width`, `height` | Shape morphing |
-
-These are starting points. Any property GSAP can tween is fair game — `clipPath`, `filter`, `backgroundPosition`, SVG attributes, custom CSS properties.
-
-## Content, Not Medium
-
-Audio data provides **timing and intensity** for visuals grounded in the content. It tells the animation _when_ and _how much_ — not _what to show_. The visual vocabulary comes from the narrative, theme, and emotion of the piece. A funeral dirge and a party anthem should produce completely different visuals even though their audio data has the same shape.
-
-**Never add these — they represent the medium, not the content:**
-
-- Frequency equalizer bars, spectrum analyzers, radial spectrum rings — technical readouts that just say "audio exists"
-- Waveform displays, oscilloscope lines, VU meters — diagnostic tools, not creative choices
-- Musical notes, vinyl records, turntable imagery — clip art signaling "music is playing"
-- Generic particle systems driven by amplitude — interchangeable across any song or mood
-- Background color cycling through rainbow hues on frequency — rave aesthetic regardless of content
-- Strobing/flashing white on beat hits — lazy beat sync (also an accessibility problem)
-- Abstract pulsing orbs, breathing geometric wireframes, concentric rings — would look identical on a lullaby or a metal track
-
-**Instead, let the content guide the visual and the audio drive its behavior:**
-
-- If the scene is warm, bass makes the warmth _swell_ (slight scale, deeper color saturation)
-- If the mood is tense, treble sharpens _contrast_ or tightens _letterSpacing_
-- If text is the focus, bass gives it subtle _weight_ (shadow depth, y offset) while treble adds _shimmer_ (glow, lightness)
-- The visual choice comes from asking "what does this piece feel like?" — audio data just animates the answer
-
-## Guidelines
-
-- **Subtlety for text.** Captions and readable text should stay in the 3–6% scale variation range with soft glow. Heavy pulsing makes text unreadable.
-- **Go bigger on non-text elements.** Backgrounds, shapes, and decorative elements can handle 10–30% scale swings, full color shifts, and dramatic transforms.
-- **Match the energy.** A corporate explainer needs subtle reactivity. A music video can go hard.
-- **Deterministic.** Audio data is pre-extracted — no Web Audio API, no `AnalyserNode`, no runtime mic input. The data is static JSON, the animation is repeatable.
-
-## Sampling Frequency
-
-**One tween per element is not audio reactivity.** Reading peak bass/treble for a time range and creating a single tween at the start sets one static value — the viewer cannot perceive it as reactive. Audio reactivity requires multiple tweens over time so the visual _changes_ with the music.
-
-Sample at **100-200ms intervals** throughout the element's visible lifetime:
-
-```js
-// ✓ Perceptible: sample every 150ms, create a tween at each point
-for (var t = group.start; t < group.end; t += 0.15) {
- var bass = getBass(t);
- tl.to(el, { scale: 1 + bass * 0.06, duration: 0.075, ease: "sine.inOut" }, t);
-}
-
-// ✗ Imperceptible: one tween from peak values
-var peakBass = getPeakBass(group.start, group.end);
-tl.to(el, { scale: 1 + peakBass * 0.06, duration: 0.3 }, group.start);
-```
-
-## textShadow on Containers
-
-**Never apply `textShadow` to a container that has semi-transparent children.** When a caption group has inactive words at `rgba(255,255,255,0.3)`, a `textShadow` on the parent div renders a visible glow rectangle behind all children — it looks like a gray background, not a glow effect.
-
-Apply `scale` to the group container for bass-reactive pulsing. Apply `textShadow` to individual active word elements only.
-
-## Constraints
-
-- All audio data must be pre-extracted — no runtime audio analysis
-- No `Math.random()` or `Date.now()` — deterministic rendering applies
-- Audio reactivity runs on the same GSAP timeline as everything else
diff --git a/skills/gsap-effects/SKILL.md b/skills/gsap-effects/SKILL.md
deleted file mode 100644
index 17b112eae..000000000
--- a/skills/gsap-effects/SKILL.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: gsap-effects
-description: Typewriter text, audio visualizer, and drop-in animation effects for HyperFrames compositions. Use for character-by-character reveals, spectrum bars, waveforms, or audio-reactive visuals.
----
-
-# GSAP Effects
-
-Drop-in animation patterns for HyperFrames compositions. Each effect is a self-contained reference with the HTML, CSS, and code needed to add it to a composition.
-
-These effects follow all HyperFrames composition rules — deterministic, no randomness, timelines registered via `window.__timelines`.
-
-## Available Effects
-
-| Effect | File | Use when |
-| ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
-| Typewriter | [typewriter.md](./typewriter.md) | Text should appear character by character, with or without a blinking cursor |
-| Audio Visualizer | [audio-visualizer.md](./audio-visualizer.md) | Reactive bars, waveforms, circles, or glow that respond to audio. Includes extraction script and Canvas 2D patterns |
diff --git a/skills/gsap-effects/audio-visualizer.md b/skills/gsap-effects/audio-visualizer.md
deleted file mode 100644
index c9edb2592..000000000
--- a/skills/gsap-effects/audio-visualizer.md
+++ /dev/null
@@ -1,238 +0,0 @@
-# Audio Visualizer
-
-Reactive audio visualizations for HyperFrames compositions. Pre-extracts amplitude and frequency data from an audio file, then drives rendering from the GSAP timeline.
-
-## Why Pre-Extraction
-
-HyperFrames renders frame-by-frame in headless Chrome — there's no audio playing during rendering, so the Web Audio API's real-time `AnalyserNode` won't work. Instead, extract all audio data before the composition runs and bake it as a static JSON array. The composition reads the array by frame index. This is fully deterministic and seekable.
-
-## Step 1: Extract Audio Data
-
-```bash
-python skills/gsap-effects/scripts/extract-audio-data.py audio.mp3 -o audio-data.json
-python skills/gsap-effects/scripts/extract-audio-data.py video.mp4 --fps 30 --bands 16 -o audio-data.json
-```
-
-Requires ffmpeg and numpy (`pip install numpy`).
-
-| Flag | Default | Description |
-| --------- | --------------- | -------------------------------------------------------- |
-| `--fps` | 30 | Must match the composition/render FPS |
-| `--bands` | 16 | Number of frequency bands (more = finer spectrum detail) |
-| `-o` | audio-data.json | Output path |
-
-The script uses a 4096-sample FFT window (not the per-frame sample count) to ensure each frequency band maps to distinct FFT bins. Bands are logarithmically spaced from 30Hz to 16kHz — the useful range for music. Each band is normalized independently across the full track so treble activity is visible even when bass is louder in absolute terms.
-
-## Step 2: Understanding the Data
-
-```json
-{
- "duration": 180.5,
- "fps": 30,
- "bands": 16,
- "totalFrames": 5415,
- "frames": [
- { "time": 0.0, "rms": 0.0, "bands": [0.0, 0.0, 0.0, ...] },
- { "time": 0.0333, "rms": 0.42, "bands": [0.8, 0.6, 0.3, ...] }
- ]
-}
-```
-
-**`rms`** (0-1) — overall loudness of this frame, normalized across the full track. 0 is silence, 1 is the loudest moment in the entire audio. Use this for anything that should respond to overall energy: scaling, pulsing, glow intensity, opacity, movement speed.
-
-**`bands`** (array of 0-1 values) — frequency magnitudes. Each value is normalized independently for that band across the full track, so a 0.8 in treble means "this is 80% of the loudest this treble band gets anywhere in the audio" — not that treble is as loud as bass in absolute terms. This is what makes all frequency ranges visually active.
-
-- Index 0 = lowest bass (~30Hz). Index `n-1` = highest treble (~16kHz).
-- Low indices (0-3) react to kick drums, bass lines, sub-bass rumble.
-- Mid indices (4-9) react to vocals, guitars, synths, most melodic content.
-- High indices (10-15) react to hi-hats, cymbals, sibilance, brightness.
-
-## Loading the Data
-
-Embed the data in the composition so it's available when the timeline runs.
-
-```js
-// Option A: inline (small files, under ~500KB)
-const AUDIO_DATA = {
- /* paste audio-data.json contents */
-};
-setupTimeline(AUDIO_DATA);
-
-// Option B: fetch (large files)
-fetch("audio-data.json")
- .then((r) => r.json())
- .then((data) => {
- setupTimeline(data);
- });
-
-function setupTimeline(AUDIO_DATA) {
- // Register tl.call() draws here — AUDIO_DATA is guaranteed to be loaded
- for (let f = 0; f < AUDIO_DATA.totalFrames; f++) {
- tl.call(
- () => {
- draw(AUDIO_DATA.frames[f]);
- },
- [],
- f / AUDIO_DATA.fps,
- );
- }
-}
-```
-
-With fetch, wrap all timeline setup inside the callback so `AUDIO_DATA` is available when the `for` loop reads `totalFrames`. The fetch completes before the renderer's first seek because it waits for `window.__hf` readiness.
-
-## Step 3: Drive Rendering from the Timeline
-
-Register a `tl.call()` at every frame interval. Each call reads the pre-computed data and renders. This is deterministic and seekable — scrubbing in the studio works because each frame's draw is tied to a specific timeline position.
-
-## Rendering Approaches
-
-The data is framework-agnostic. Here's how to wire it up in each approach.
-
-### Canvas 2D
-
-Best for: bars, waveforms, circles, gradients, particles. Most common choice.
-
-```js
-const canvas = document.querySelector("#viz-canvas");
-const ctx = canvas.getContext("2d");
-
-for (let f = 0; f < AUDIO_DATA.totalFrames; f++) {
- tl.call(
- () => {
- const frame = AUDIO_DATA.frames[f];
- if (!frame) return;
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- // read frame.rms and frame.bands, draw whatever you want
- },
- [],
- f / AUDIO_DATA.fps,
- );
-}
-```
-
-### WebGL / Three.js
-
-HyperFrames has a Three.js adapter that patches `THREE.Clock` for deterministic time. Create your scene normally, then update uniforms or object properties from the audio data each frame.
-
-```js
-// In your Three.js setup:
-const uniforms = { uBass: { value: 0 }, uMid: { value: 0 }, uRms: { value: 0 } };
-
-for (let f = 0; f < AUDIO_DATA.totalFrames; f++) {
- tl.call(
- () => {
- const frame = AUDIO_DATA.frames[f];
- if (!frame) return;
- uniforms.uBass.value = Math.max(frame.bands[0], frame.bands[1], frame.bands[2]);
- uniforms.uMid.value = Math.max(frame.bands[6], frame.bands[7], frame.bands[8]);
- uniforms.uRms.value = frame.rms;
- },
- [],
- f / AUDIO_DATA.fps,
- );
-}
-```
-
-### DOM Elements
-
-For simpler visualizations (a few bars, a pulsing element), you can animate DOM elements directly. Less performant than Canvas for many elements, but fine for under ~20.
-
-```js
-const bars = document.querySelectorAll(".bar");
-for (let f = 0; f < AUDIO_DATA.totalFrames; f++) {
- tl.call(
- () => {
- const frame = AUDIO_DATA.frames[f];
- if (!frame) return;
- bars.forEach((bar, i) => {
- bar.style.height = frame.bands[i] * 100 + "%";
- });
- },
- [],
- f / AUDIO_DATA.fps,
- );
-}
-```
-
-## Spatial Mapping
-
-When laying out frequency data spatially, follow these conventions so visualizations read naturally:
-
-- **Horizontal layouts**: low frequencies (bass) on the left, high frequencies (treble) on the right. Iterate the bands array left-to-right.
-- **Vertical layouts**: low frequencies at the bottom, high frequencies at the top.
-- **Circular layouts**: bass starts at the top (12 o'clock) and wraps clockwise. Mirror the bands array for a full circle.
-
-## Motion Principles
-
-### Smoothing
-
-Raw per-frame data changes abruptly. Blend with the previous frame for fluid motion:
-
-```js
-let prev = null;
-const smoothing = 0.25; // 0 = no smoothing, higher = more lag
-
-function smooth(f) {
- const raw = AUDIO_DATA.frames[f];
- if (!raw) return prev;
- if (!prev) {
- prev = { rms: raw.rms, bands: [...raw.bands] };
- return prev;
- }
- prev = {
- rms: prev.rms * smoothing + raw.rms * (1 - smoothing),
- bands: raw.bands.map((b, i) => prev.bands[i] * smoothing + b * (1 - smoothing)),
- };
- return prev;
-}
-```
-
-Lower smoothing (0.1-0.2) feels snappy and responsive — good for percussive music. Higher smoothing (0.3-0.5) feels languid and flowing — good for ambient or orchestral.
-
-### Value Mapping
-
-Audio data is 0-1 but visual properties need different ranges. Map with intention:
-
-- **Scale/size**: multiply by a max value. A bar's height = `bands[i] * maxHeight`. Don't let elements disappear at 0 — add a minimum: `minHeight + bands[i] * (maxHeight - minHeight)`.
-- **Opacity**: low values should still be slightly visible. `0.15 + bands[i] * 0.85` keeps elements present during quiet moments.
-- **Color intensity**: shift between a muted base and a vivid peak. Interpolate HSL lightness or RGB channels based on the value.
-- **Position/offset**: use rms to drive drift or wobble. Small movements (5-20px) feel organic; large movements look chaotic.
-
-### What Makes It Feel Good
-
-- **Bass drives the big moves.** Scale, position shifts, and glow should react to low bands. Bass is what makes a visualization feel like it's "hitting."
-- **Treble drives the detail.** Small particle movements, edge shimmer, opacity flicker. Treble adds texture without dominating.
-- **RMS drives global properties.** Background brightness, overall scale, color warmth. It's the "energy level" of the whole frame.
-- **Don't animate everything at once.** Pick 2-3 visual properties to tie to the audio. More than that looks noisy.
-- **Quiet sections should still have life.** A completely static frame during a soft passage looks broken. Keep minimum values above zero.
-
-## Band Count Guide
-
-| Bands | Detail level | Good for |
-| ----- | ------------ | ------------------------------------------- |
-| 4 | Low | Simple pulsing, background glow |
-| 8 | Medium | Bar visualizations, basic spectrum |
-| 16 | High | Detailed EQ, circular visualizers (default) |
-| 32 | Very high | Smooth curves, dense radial layouts |
-
-More bands = larger JSON file. 16 is a good default.
-
-## Layering
-
-Layer multiple canvases with CSS z-index for depth:
-
-```html
-
-
-```
-
-A background layer driven by bass/rms and a foreground layer driven by individual bands creates depth without complexity.
-
-## HyperFrames Integration Notes
-
-- The `