
Music To Video
- 110k installs
- 38.1k repo stars
- Updated July 28, 2026
- heygen-com/hyperframes
music-to-video is a HyperFrames agent skill that turns an audio track into a beat-synced video by analyzing the music once, planning per-frame cuts, and rendering typography or user-supplied assets on a shared beat grid.
About
music-to-video is a HeyGen HyperFrames skill that converts a music track into a beat-synced video from calm to hard-hitting intensity. One analyzer reads the track once; an orchestrator lays out frames and fills a per-frame plan; one sub-agent builds each frame.
- Beat-synced video generation from audio tracks with musical intensity analysis
- One analyzer reads track once; orchestrator plans frames; sub-agent builds each frame
- Supports music-driven promos, lyric videos, and kinetic social clips programmatically
Music To Video by the numbers
- 110,073 all-time installs (skills.sh)
- +16,289 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #12 of 1,881 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/heygen-com/hyperframes --skill music-to-videoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 110k |
|---|---|
| repo stars | ★ 38.1k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | heygen-com/hyperframes ↗ |
How do you generate a beat-synced video from audio?
Generate beat-synced music videos automatically for social clips, lyric videos, and promotional content.
Who is it for?
Developers automating music-driven promos, lyric videos, or social clips inside the HyperFrames agent composition pipeline.
Skip if: Talking-head overlay packaging, plain subtitle burns, or manual timeline editing outside HyperFrames.
When should I use this skill?
The user has a music track or audio to extract and wants a beat-synced HyperFrames video from calm to hard-hitting.
What you get
Beat-synced HyperFrames MP4 with per-frame plan, typography frames, and optional beat-cut image or video inserts.
- beat-synced MP4
- per-frame plan
By the numbers
- Uses one audio analyzer, one orchestrator, and one sub-agent per frame
- Supports zero supplied assets via typography-only templates as the floor
Files
music-to-video — one music-grounded, beat-synced video workflow
Use this skill to turn a music track into a beat-synced HyperFrames video. You analyze the track once, lay out the frames, fill in a per-frame plan, and build each frame as a composition. The input is a music track plus optional user images or videos — there is no narration and no website capture. Typography and templates are the floor (a complete video needs zero assets); any media the user supplies is cut in on the same beat grid.
You are the orchestrator. Work in videos/<project>/. Run the steps in order and pass each Gate before moving on. Two steps need the user: Step 3 (plan approval) and Step 6 (render approval). Do every step yourself except Step 4, where you dispatch one sub-agent per frame. Keep design and motion rules out of this file — they live in references/ and the frame-worker sub-agent.
SKILL_DIR = this skill directory. PROJECT_DIR = videos/<project-name>/.
Workflow: Step 0 setup → hyperframes.json + assets/bgm.mp3; Step 1 analyze → audiomap.json; Step 2 skeleton → STORYBOARD.md (frames, groups TBD); Step 3 plan → complete STORYBOARD.md + frame.md; Step 4 build → compositions/frames/NN-*.html; Step 5 assemble → index.html; Step 6 render → renders/video.mp4.
Two ideas that shape everything
- One analyzer, and you trust it.
analyze-beatgrid.pyis the only beat analyzer — never re-measure beats with another tool or by ear. Its energy / density / rolls / onsets / silences are always reliable. Itsbpmandbeats_secare reliable only when the music is genuinely rhythmic; on calm music the grid is a metronome the tracker imposed, so pace by phrases and energy instead and never hard-cut to it. Deciding which case you're in is each frame'spacing(Step 2). - One frame = one file; groups live inside. Step 2 cuts the track into frames, and each frame becomes one composition file
compositions/frames/NN-<frame_id>.html, built by one frame-worker. A frame can subdivide into groups (each a template or a motion-primitives combo). Extra density goes _inside_ a group, so frame count tracks distinct treatments, not beats — a fast track does not blow up the number of sub-agents.
---
Step 0: Setup, BGM, and inputs
Goal: Establish the music source, create the HyperFrames project, and note any user-supplied media.
The music is the spine — establish one track before anything else. This skill is tuned for fast, high-energy BGM: a strong beat grid drives the cuts (calm tracks work, but pace by phrase rather than beat). If the user gave you audio — a music file, or a video to pull the audio from — use it. If not, generate one: choose the mood from the user's description (e.g. "driving synthwave", "trap beat", "upbeat corporate") and produce a track via /hyperframes-media (references/bgm.md — HeyGen retrieval when credentialed, else local Lyria / MusicGen; ElevenLabs or another generator also works). Before generating, run npx hyperframes auth status and relay its output verbatim (don't paraphrase or rewrite it) — it shows whether BGM comes from HeyGen or local MusicGen and, if not signed in, how to sign in. If not signed in, STOP and wait for the user to choose — sign in, or continue offline with local MusicGen — before generating the track; don't write keys into a per-repo .env. (In autonomous mode, note the status and continue offline.) See /hyperframes-media → Preflight for the canonical guidance. Either way the track lands at assets/bgm.mp3. Stage any user-supplied images or videos so frames can weave them in on the beat grid; otherwise typography carries the whole video.
Initialize only if hyperframes.json is missing. Name <project> from the brief in kebab-case, such as midnight-drive-loop — never a timestamp.
npx hyperframes init "videos/<project>" --non-interactive --skip-skills --example=blank
mkdir -p "$PROJECT_DIR/assets" "$PROJECT_DIR/renders"
cp "<user-music>" "$PROJECT_DIR/assets/bgm.mp3" # extract from a video first if needed
# only if the user gave you images/videos:
node <SKILL_DIR>/scripts/stage-assets.mjs --from <dir> --hyperframes "$PROJECT_DIR" --into publicThe brand (font + palette) is chosen at Step 3, not here. Don't pick a genre or a track type up front — assets are just an optional ingredient, and the genre emerges from the per-frame choices.
Gate: hyperframes.json + assets/bgm.mp3 exist; aspect / length / fps and (if any) the asset inventory are noted.
---
Step 1: Analyze the music
Goal: Produce the one canonical timing analysis the whole video is built on.
analyze-beatgrid.py is the only beat analyzer — never re-measure beats with another tool or by ear. It reads the track once and writes audiomap.json: energy phases (level / density / feel), onsets + onset_rate, rolls, silences, hard_stops, key_moments, phrases, tempo / grid, and audio.duration_sec. It's deterministic — the same file always gives the same map. Most fields are reliable on any music; bpm and beats_sec are reliable only when the music is genuinely rhythmic, and judging that is the call you make at Step 2.
Prerequisites: Python 3 with librosa, numpy, and soundfile available. If import fails, install them into the active Python environment before running the analyzer:
python3 -m pip install librosa numpy soundfilepython3 <SKILL_DIR>/scripts/analyze-beatgrid.py "$PROJECT_DIR/assets/bgm.mp3" \
-o "$PROJECT_DIR/audiomap.json" --printGate: audiomap.json exists; audio.duration_sec is known.
---
Step 2: Frame skeleton (structure only)
Goal: Read the music and lay out the frames — the skeleton of STORYBOARD.md.
Read `references/frame-skeleton.md`. Turn audiomap.json into the skeleton of STORYBOARD.md yourself — there is no intermediate JSON. Cut the track into frames at real musical changes (hard_stops, SURGE / DROP key_moments, the edges of a roll, a stretch with no onsets, a big energy jump), snapping every boundary to an audiomap anchor. For each frame set span_sec, pacing (the verdict from Step 1's trust call — beat_cut when the grid is real, phrase_flow when it's a metronome imposed on calm music), mood, and a one-line feel (the plain music situation Step 3 matches a template against). Only classify and lay out here: leave every frame's ### Groups as TBD (Step 3) and the frontmatter style blank — no templates, copy, color, or fonts. Expect ~1–6 frames.
Gate: frames tile the track (first at 0, last at duration_s); each carries span_sec + pacing + mood + feel; every ### Groups is TBD; no content anywhere.
---
Step 3: Fill the plan (user-gated)
Goal: Turn the skeleton into an approved, complete STORYBOARD.md.
Read `references/planning.md`, `storyboard-format.md`, `template-catalog.md`, `motion-primitive-catalog.md`, and `montage.md` (only if the user supplied assets). Editing the same file in place, do two things:
1. Pick the brand. Choose one preset from ../hyperframes-creative/frame-presets/ using the table in ../hyperframes-creative/references/design-spec.md (match the track's mood; only its fonts and colors matter — templates own composition). Copy it into frame.md unmodified and fill the frontmatter style (font + a ≤4–6 swatch palette) from it. 2. Fill every frame. Decide its groups and give each a treatment: a matched template from the catalog (with bound params and real audiomap anchors), a free-compose from the primitive catalog, or an asset treatment that obeys `pacing`. Write the copy. You own WHAT (template / primitives + content + anchors); the frame-worker owns HOW — never write millisecond tweens into the storyboard.
node <SKILL_DIR>/scripts/validate-plan.mjs --storyboard "$PROJECT_DIR/STORYBOARD.md" \
--audiomap "$PROJECT_DIR/audiomap.json" --templates <SKILL_DIR>/references/templatesFix every ✗ (hard errors: duration mismatch, frames not tiling the track, a missing src); warnings are best-effort. Then show the user a frame-by-frame summary and iterate until they approve.
Gate: frame.md is a verbatim preset copy; validate-plan.mjs exits 0; the user approved the plan.
---
Step 4: Build frames from the plan
Goal: Build every frame as a self-contained composition file.
Create compositions/frames/. Read `sub-agents/frame-worker.md` and ../hyperframes-core/references/subagent-dispatch.md. Dispatch one frame-worker per frame, in parallel where possible (otherwise in waves). Each worker gets exactly one frame and this context:
PROJECT_DIR: <abs path>
frame_id: <NN-frame_id> # = the frame file stem, e.g. 02-f2; the composition id
Your block: the `## Frame N — <frame_id>` block in PROJECT_DIR/STORYBOARD.md
audiomap: PROJECT_DIR/audiomap.json
frame.md: PROJECT_DIR/frame.md
Materials: for each group, <SKILL_DIR>/references/templates/<id>/index.html (templates) and
<SKILL_DIR>/references/motion-primitives/<id>/ (free); staged assets/ (asset groups)
Contracts: ../hyperframes-core/references/sub-compositions.md + determinism-rules.md
Canvas: <w>×<h> Pacing: <beat_cut|phrase_flow>
Write to: PROJECT_DIR/compositions/frames/<frame_id>.htmlThe worker forks the cited materials, converts every anchor to frame-local seconds (local_t = track_t − span_sec[0]), gates its groups with 0ms cuts, and writes one seek-safe frame file. The worker never runs the `hyperframes` CLI — those commands operate on the assembled project, which doesn't exist yet, so they'd report on the wrong files. The worker just writes to the contract and stops; you verify after assembly (Step 6). As each worker returns, you can confirm its file landed on disk.
Gate: every frame has its compositions/frames/NN-*.html on disk.
---
Step 5: Assemble
Goal: Wire the built frames + BGM into the playable index.html.
assemble-index.mjs is deterministic — no subagent, no judgment. It references each frame file at its cumulative data-start, mounts assets/bgm.mp3 on track 11, and hard-cuts frame → frame (frames tile the track with no gaps, so there is no transition injector).
node <SKILL_DIR>/scripts/assemble-index.mjs --storyboard "$PROJECT_DIR/STORYBOARD.md" \
--hyperframes "$PROJECT_DIR" --audiomap "$PROJECT_DIR/audiomap.json"Fix any ✗ it reports — a missing or blank frame file means that worker wrote a partial file; re-dispatch it (Step 4) and re-assemble.
Gate: index.html exists; total duration == audiomap.audio.duration_sec.
---
Step 6: Verify and render
Goal: Verify the assembled video, get user approval, and render the final MP4.
Run the CLI on the assembled project — that's the correct unit (the per-frame workers couldn't run it). lint checks structure, validate runs headless Chrome (catching JS errors and missing assets), inspect snapshots frames.
( cd "$PROJECT_DIR" && npx hyperframes lint . && npx hyperframes validate . && npx hyperframes inspect . )Inspect at t=0, each frame start, the strongest DROP / SURGE, every hard_stops[].t, and the final frame. On failure, make the cheapest safe fix yourself: edit the offending compositions/frames/NN-*.html. Never change duration or audio timing to hide a sync issue. Once the gates pass, pause for user review, then render only on approval:
( cd "$PROJECT_DIR" && npx hyperframes render . --skill=music-to-video -q draft -o renders/video.mp4 --fps 30 )Gate: lint / validate / inspect passed; the user approved; renders/video.mp4 exists with audio, duration == audiomap.audio.duration_sec. The final reply states the MP4 path and duration.
---
Resume table
| You have | Continue from |
|---|---|
assets/bgm.mp3 only | Step 1 |
audiomap.json | Step 2 |
STORYBOARD.md (skeleton) | Step 3 |
STORYBOARD.md (complete) | Step 4 |
| all frame files | Step 5 |
index.html | Step 6 |
Quick Reference
Formats: landscape 1920x1080 by default; portrait 1080x1920; square 1080x1080. Set the canvas once in the storyboard frontmatter (canvas: { w, h, fps }).
Scripts under scripts/: analyze-beatgrid.py (the one analyzer), validate-plan.mjs (plan check), assemble-index.mjs (index assembly), stage-assets.mjs (stage user media), lib/storyboard.mjs (vendored parser). Everything else is the hyperframes CLI.
| Read | When |
|---|---|
| `references/frame-skeleton.md` | Step 2: read the music, lay out the frames, set pacing |
| `references/planning.md` · `storyboard-format.md` | Step 3: pick the brand, fill each frame, write the plan |
| `references/template-catalog.md` | Step 3: pick a template per group |
| `references/motion-primitive-catalog.md` | Step 3/4: L0 recipes for free-compose |
| `references/montage.md` | Step 3/4: asset treatments (beat-cut / ken-burns) |
| `sub-agents/frame-worker.md` | Step 4: dispatch + build one frame |
../hyperframes-core/references/subagent-dispatch.md | Step 4: dispatch sub-agents safely |
../hyperframes-creative/references/design-spec.md | Step 3: pick the preset (the brand) |
Directory layout
music-to-video/
SKILL.md
references/ frame-skeleton.md · planning.md · storyboard-format.md
template-catalog.md · motion-primitive-catalog.md · montage.md
templates/<id>/ { index.html (+ assets/ · program.json) } ← L1 catalog impls
motion-primitives/<id>/ { index.html } (+ ../assets/gsap.min.js shared by recipes) ← L0 catalog impls
scripts/ analyze-beatgrid.py · assemble-index.mjs · validate-plan.mjs · stage-assets.mjs · lib/storyboard.mjs
sub-agents/ frame-worker.md ← the one subagent (one per frame)Frame skeleton (Step 2) — read the music, lay out the frames
At Step 2 you (the orchestrator) read audiomap.json and write the skeleton of STORYBOARD.md directly: cut the track into frames (one frame = one composition file = one scene), and for each frame set its span, its pacing (does this stretch want hard beat-cuts, or calm phrase/energy flow?), its mood, and a one-line feel note.
You classify and lay out the spine only. You do not pick templates, write copy, choose colors/fonts, or decide a frame's groups — those are Step 3 (the plan fills each frame in place). Leave every frame's ### Groups as TBD (Step 3) and the frontmatter style blank.
There is no intermediate JSON — the skeleton _is_ the start of STORYBOARD.md. Step 3 edits the same file.
The trust boundary (read this first)
audiomap.json is one analyzer's output. Some fields are robust on any music; some are reliable only when the music is actually rhythmic. This decides each frame's pacing:
| Field | Trust |
|---|---|
energy_phases[] (level / energy / density / feel), events[] + onset_rate, rolls[] (and their absence), silences[], hard_stops[], key_moments[], phrases[], audio.duration_sec | Always — robust measurements |
tempo.bpm, grid.beats_sec / downbeats_sec precision | Only when the music is rhythmic. On calm / sparse material the beat grid is a metronome the tracker _imposes_ (often octave-doubled) — usually more grid beats than real onsets. Do not anchor cuts to it there. |
- Grid is reliable when: rolls present, and/or dense phases, and/or high
onset_ratewith a steady grid. - Grid is fictional when:
rolls≈0, mostlysparsephases, lowonset_rate→ pace byphrases[]+energy_phases[], not beats.
How to lay out frames (run in order)
1. Gestalt. From summary / tempo / audio.duration_sec + the roll count, write the frontmatter compositionId, duration_s (== audio.duration_sec), canvas, and a one-line read of the track's density arc. 2. Cut into frames. Walk energy_phases[] and split where the music genuinely changes state — at hard_stops[], SURGE / DROP key_moments, the start/end of a rolls[] run, an onset desert (a long gap in events[]), or a big energy-level jump. Collapse adjacent phases that are one gesture. Expect ~1–6 frames; a short clip may be one. Snap every boundary to an audiomap anchor, then re-snap to the nearest beats_sec (tolerance ≤ ½ beat) only when the grid is reliable; on calm material snap to phrases[] / energy_phases[] edges instead. Frames tile the track (first at 0, last at duration_s, no gaps/overlaps). 3. Per frame, set `pacing` — the trust-boundary call:
- `beat_cut` — genuinely rhythmic: a roll present, or dense, or (clearly high
onset_rateand a steady grid). Hard cuts / per-onset reveals may anchor to beats. - `phrase_flow` — calm / sparse:
rolls≈0, mostly sparse, lowonset_rate. Do not anchor hard cuts to the grid; pace byphrases[]+ the energy envelope (slow crossfades, long holds).
4. Per frame, tag `mood` (1–3 of: warm · dark · hype · elegant · glitch · cinematic · playful · tense · dreamy · aggressive) from energy_phases[].feel + energy + any genre cue in the brief/title. 5. Per frame, write a one-line `feel` — the plain-language music situation Step 3 matches a template against (e.g. "accelerating onset stream into a held downbeat", "calm held pad, one onset desert", "fast sustained-fill roll, no readable message"). This is what the planner reads against the catalog's Reach for it when — keep it concrete, drawn from the robust fields, never invented.
What the skeleton looks like
A valid STORYBOARD.md with the spine set and every frame's treatment left for Step 3 (full syntax in `storyboard-format.md`):
---
compositionId: bgm
duration_s: 30.0 # == audiomap.audio.duration_sec
canvas: { w: 1920, h: 1080, fps: 30 }
style: # blank — Step 3 fills it from the chosen frame.md preset
build_notes: ["one paused timeline per frame", "no remote assets"]
---
## Frame 1 — f1
- src: compositions/frames/01-f1.html
- duration: 7.198s # = span length; assembler sums these for cumulative data-start
- span_sec: [0.0, 7.198] # track seconds; frames tile the track
- pacing: beat_cut
- mood: [hype]
- feel: accelerating onset stream building into a held downbeat
### Groups
- TBD (Step 3)
## Frame 2 — f2
- src: compositions/frames/02-f2.html
- duration: 10.4s
- span_sec: [7.198, 17.598]
- pacing: phrase_flow
- mood: [warm, cinematic]
- feel: calm held pad, one long onset desert
### Groups
- TBD (Step 3)Self-check
duration_s == audiomap.audio.duration_sec; frames tile the track gap-free (first at 0, last atduration_s).- Every frame has
src+span_sec+duration+pacing+mood+ a one-linefeel. pacingwas set from the robust fields (energy / density / rolls / onset_rate), never frombpm/beats_secalone.- No frame boundary sits inside a
rolls[]run or leaves a sub-1-bar fragment. - Every frame's
### GroupsisTBD (Step 3);styleis blank. No template, copy, color, or font anywhere.
Asset treatments — weaving user media onto the beat spine
When the user supplies images/videos, a group can be an asset treatment instead of a typographic template/free-compose. Assets are an additive ingredient on the same beat spine — never a separate pipeline. Typography/templates stay the floor: if no asset fits a group, fall back to a template/free group (a complete video needs zero assets).
The planner (Step 3) picks the treatment and the clips + anchors (WHAT); the frame-worker realizes it inside the frame file (HOW). Obey the frame's `pacing`.
The three treatments
beat_cut — one clip per anchor (only on a beat_cut frame)
The asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor (the frame's beats/onsets from the audiomap). Each clip is a class="clip" element (<img> for a photo, muted <video> for a motion clip) placed at its anchor with data-start/data-duration/data-track-index per the core clip contract. Between clips, crossfade the outgoing content to opacity:0 ending at the next anchor, then immediately hard-kill with a tl.set(..., {opacity:0}, anchor) — this pair is required (the gsap_exit_missing_hard_kill lint rule; non-linear seeking otherwise bleeds stale frames). Cut on the strong anchors; land a hero clip on a key_moment/downbeat.
ken_burns — slow push on one clip (fits a phrase_flow frame)
For calm frames: one clip held over the span with a slow scale/translate push (e.g. scale 1.0→1.08 + a small drift) eased across the whole span_sec — paced by the frame, not by beats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment when the beat grid is unreliable (calm music).
bg_under_text — clip dimmed behind a template/free group
A full-bleed clip dimmed ~30–50% as the background of a group whose foreground is a template or free-compose typographic treatment. The text rides on the same anchors; the clip is the bed. Use when the user wants their footage present but the message must stay readable.
Rules
- `pacing` decides the treatment:
beat_cutonly on abeat_cutframe; on a
phrase_flow frame use ken_burns or a slow crossfade — never per-onset hard cuts on the (unreliable) calm grid.
- Clips are muted; the root owns audio. Mount each
<video class="clip">muted, as a
direct child of the frame root (never nested in another timed element, or the renderer freezes it). The BGM is the only audio in v1.
- Crossfades animate `opacity`/`autoAlpha`, never
visibility/displayon a.clip
(the framework owns clip visibility — that trips gsap_animates_clip_element).
- Backgrounds dim ~30–50% so any foreground text stays legible.
- Anchors are track seconds from `audiomap.json`; the worker subtracts the frame start
to get frame-local time.
- Local staged assets only (
assets/viastage-assets.mjs); never remote URLs.
Deferred hook (not v1)
A clip that should play its own sound (interview cut, lyric clip) needs a sibling <audio> mounted at the root by the assembler, with the BGM ducked under it (timeline volume automation: tl.to("#el-bgm",{volume:0.15,…},clipStart) … tl.to(…,{volume:0.9,…}, clipEnd)). The frame-worker mounts no audio. Keep clips muted in v1; wire clip-audio + ducking only when the user asks.
Motion-primitive catalog — the free-compose menu
The atomic layer: one anchor → one micro-move. When no template fits a group, free-compose by naming primitives from here. Scan anchor + best span + what it does, then pick the smallest set that carries the group.
Timing & latency (applies to every primitive)
- Hard hits are 0ms. Cuts, palette flips, content swaps, freezes are
tl.set(...)with no duration — the percussion _is_ the motion. Easing a hit kills it. - Lead the anchor. A move that must _land_ on a beat (a wipe covering the frame, a count-up locking, two blocks colliding) starts ~40–190ms early so it completes ON the anchor. Reactive entrances (something appearing _because_ of the hit) fire 0–45ms after.
- Eased entrances: 300–500ms (scale punch, slides, camera pushes). Macro builds: 800–2000ms spanning a whole roll / silence.
- Per-bar caps: one accumulating element per hit (not a burst); a camera move at most once per phrase, never per beat; a dense flip/strobe system runs ≤2–3s.
- Tension-builds lock. A count-up / sequential build / morph must _resolve on_ a downbeat or hard_stop, never trail off mid-bar.
- Best span means active motion. The catalog's span guidance is not a license to stretch one primitive over a whole frame. If a free-composed group runs longer than the listed span, add a hold / bed / next primitive, or split the frame into another group at the next musical anchor.
Catalog
| id | anchor | best span | what it does |
|---|---|---|---|
hypercut-whip | beat / hard_stop | 0.18-0.45s | fast whip-pan hard cut between frames |
kinetic-letter-in | downbeat / phrase | 0.4-1.2s | per-letter kinetic entrance |
braam-punch | drop / surge | 0.2-0.9s active | big impact: scale + weight slam |
chromatic-split | snare / glitch / surge | 0.1-0.6s | RGB channel split / glitch on a word |
mask-reveal | section_start / downbeat | 0.5-1.2s | clip-path mask wipe reveal |
screen-shake | drop / crash / kick | 0.1-0.5s | camera / screen shake jitter |
binary-decrypt | roll / build | 0.8-2.5s | scramble→decode text (binary → word) |
dolly-zoom | phrase / build | 1.2-2.5s | vertigo dolly-zoom (scale vs perspective) |
iris-open | section_start / reveal | 0.6-1.2s | circular iris-open reveal |
electric-arc | accent / glitch | 0.1-0.6s | electric arc / lightning accent |
neon-flicker | hold / texture | 0.5-2.5s | neon-sign flicker |
chrome-sweep | downbeat / reveal | 0.6-1.4s | metallic specular sweep across text |
slot-machine-reveal | roll → downbeat | 0.8-2.0s | slot-machine spin-to-land character reveal |
liquid-morph | phrase / transition | 1.0-2.5s | liquid / blob morph |
gooey-metaball | build / drop | 1.5-3.0s | gooey metaball merge field |
3d-card-flip | downbeat / swap | 0.8-1.6s | 3D card flip (rotateY) |
crash-zoom-in | drop / surge | 0.2-0.8s | violent crash zoom-in |
spotlight-sweep | reveal / hold | 0.8-2.0s | spotlight / gradient sweep over text |
outline-to-fill | downbeat / reveal | 0.8-1.8s | stroke outline → solid fill |
counting-punch | roll → downbeat | 1.0-2.5s | number count-up that punches & locks |
particle-burst | drop / crash | 0.2-1.2s | particle explosion burst |
radial-burst-lines | drop / surge | 0.2-0.8s | radial speed-lines burst |
pixel-dissolve | transition / hard_stop | 0.5-1.5s | pixelated dissolve |
datamosh-smear | glitch / transition | 0.4-1.2s | datamosh / motion smear |
text-wave-distort | hold / texture | 1.0-2.5s | wavy text distortion |
bg-flow-field | energy / whole span (bed) | 4-12s bed | generative curl-noise background bed; compose any foreground move over it |
blur-resolve | stop / final hold | 0.7-2.0s | blur-in to crisp focus, then blur-out on the cut |
chromatic-pressure | snare / glitch | 0.1-0.5s | RGB split / digital tension on a transient |
color-grid-shuffle | onset | 0ms hits; ≤2s run | grid of cells recolored by a deterministic index per onset |
content-swap | beat | 0ms hits; ≤3s run | 0ms swap of stacked nodes: the workhorse percussive move |
directional-fill | beat / reveal | 0.3-1.0s each | directional wipe-fill (scaleX) sweeping across bars |
flash-cut | drop / crash | 0-0.6s | full-frame flash masking a word / color state change |
freeze-hold | hard_stop | 0ms in; 0.5-2s | freeze the moving system and hold it |
hard-cut | beat / hard_stop | 0ms in; 0.3-2s | sample-accurate color-block + word cut |
mosaic-pack | beat / build | 1.5-3.5s | scattered tiles fly in and pack into a grid |
negative-space-hold | silence / hard_stop / final hold | 1-6s hold | kill busy layers, hold one readable mark in empty space |
overlay-pop | accent | 0.2-0.6s in | badge / lower-third overlay pops in over a base |
palette-flip | section change | 0ms flip; 0.5-4s | same layout re-skins via 0ms palette-variable flips |
staggered-exit | phrase / transition | 0.4-1.2s | ordered cascade-out clearing the frame |
staggered-reveal | build | 0.8-2.5s | ordered cascade-in of a stack / list |
system-replace | drop / regime change | 0ms cut | hard-cut the entire visual system, then boot the new one |
text-spectral-rays | phrase / sweep (hero text) | 2.5-5s | volumetric light-rays cast by a wordmark toward a sweeping light cursor |
tile-mosaic | build / reveal | 1.5-3.5s | grid of tiles revealed in a diagonal sweep, assembling a poster |
typewriter-reveal | roll / build | 1.0-3.0s | character / word type-on with caret |
value-counter | roll → downbeat | 1.0-2.5s | count-up that locks on a downbeat / hard_stop |
word-grid-burst | onsets → downbeat | 1.8-3.2s | grid of words revealed per onset, refocus one on a downbeat |
How to combine
- One dominant system per group; layer at most one texture primitive over one structural primitive.
- Structure on strong beats (cuts, camera,
system-replace→ downbeat / phrase / section_start); texture on weak / syncopated hits (content-swap, typewriter letters, chromatic accents). - A roll is an accumulation container — build during it, hard-cut to a clean layout on the downbeat that ends it.
drop≠downbeat: a downbeat is a cut within the regime; a drop is a regime change (system-replace, total clear, element-count jump).- Let silence remove density (
negative-space-hold). - Background beds are a layer, not a move: one bed at a time, under foreground primitives.
text-spectral-raysis the hero wordmark treatment; do not stack another visible copy of the same word on top.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>18 · 3d_card_flip</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.card-stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
perspective: 2400px;
}
.card {
position: relative;
width: 1000px;
height: 580px;
transform-style: preserve-3d;
will-change: transform;
}
.face {
position: absolute;
inset: 0;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
font-size: 200px;
letter-spacing: -0.04em;
border-radius: 24px;
box-shadow:
0 40px 80px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.08) inset;
}
.face.front {
background: linear-gradient(135deg, #1e1b3a 0%, #0a0a0f 100%);
color: var(--hg-fg);
}
.face.back {
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 60%, #d946ef 100%);
color: var(--hg-fg);
transform: rotateY(180deg);
}
.face .meta {
position: absolute;
top: 36px;
left: 40px;
font-family: "JetBrains Mono", monospace;
font-weight: 600;
font-size: 18px;
letter-spacing: 0.16em;
opacity: 0.6;
}
.card-shadow {
position: absolute;
left: 50%;
bottom: 22%;
width: 1000px;
height: 80px;
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.55) 0%, transparent 70%);
transform: translateX(-50%);
filter: blur(12px);
will-change: opacity, transform;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="card-shadow"></div>
<div class="card-stage">
<div class="card" id="card">
<div class="face front">
<div class="meta">v4 · CURRENT</div>
PRODUCT
</div>
<div class="face back">
<div class="meta">v5 · LAUNCH</div>
v5.0
</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo(
"#card",
{ rotateY: 0, scale: 0.92, opacity: 0 },
{ rotateY: 0, scale: 1, opacity: 1, duration: 0.45, ease: "power3.out" },
0.15,
);
// Hold then flip
tl.to("#card", { rotateY: 180, duration: 1.0, ease: "power3.inOut" }, 0.7);
// Shadow squishes mid-flip
tl.to(
".card-shadow",
{ scaleX: 0.6, opacity: 0.4, duration: 0.5, ease: "power2.inOut" },
0.7,
);
tl.to(".card-shadow", { scaleX: 1, opacity: 0.7, duration: 0.5, ease: "power2.inOut" }, 1.2);
// Tiny landing settle
tl.to("#card", { scale: 1.02, duration: 0.12, ease: "power2.out" }, 1.68);
tl.to("#card", { scale: 1, duration: 0.3, ease: "elastic.out(1, 0.5)" }, 1.8);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html
lang="en"
data-composition-variables='[
{"id":"bgColor", "type":"color", "label":"Page background", "default":"#050507"},
{"id":"colorLow", "type":"color", "label":"Flow — shadow tone", "default":"#0a1430"},
{"id":"colorMid", "type":"color", "label":"Flow — mid tone", "default":"#6a6f9e"},
{"id":"colorHigh", "type":"color", "label":"Flow — highlight tone", "default":"#e8f0ff"},
{"id":"accentColor","type":"color", "label":"Core / glow accent", "default":"#aee4ff"},
{"id":"flowSpeed", "type":"number", "label":"Flow speed", "default":1, "min":0.1, "max":3, "step":0.1}
]'
>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>bg_flow_field</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #050507;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.field {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
display: block;
}
.label {
position: absolute;
left: 48px;
bottom: 40px;
font-size: 22px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.7);
mix-blend-mode: screen;
pointer-events: none;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="4" data-track-index="1">
<canvas class="field" width="1920" height="1080"></canvas>
<div class="label">bg_flow_field</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// ── Variables (palette + speed); falls back to defaults standalone ──
const defaults = {
bgColor: "#050507",
colorLow: "#0a1430",
colorMid: "#6a6f9e",
colorHigh: "#e8f0ff",
accentColor: "#aee4ff",
flowSpeed: 1,
};
const vars = Object.assign(
{},
defaults,
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {},
);
// sRGB hex -> linear-light [r,g,b] (shader works in linear, gamma-encodes at end)
function hexLin(hex) {
let h = String(hex).replace("#", "").trim();
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
const r = parseInt(h.substr(0, 2), 16) / 255;
const g = parseInt(h.substr(2, 2), 16) / 255;
const b = parseInt(h.substr(4, 2), 16) / 255;
const lin = (c) => Math.pow(c, 2.2);
return [lin(r), lin(g), lin(b)];
}
const canvas = document.querySelector(".field");
const gl = canvas.getContext("webgl", {
antialias: false,
alpha: false,
preserveDrawingBuffer: true,
});
const VERT = [
"attribute vec2 position;",
"varying vec2 vUv;",
"void main(){",
" vUv = position * 0.5 + 0.5;",
" gl_Position = vec4(position, 0.0, 1.0);",
"}",
].join("\n");
// Curl-noise flow field — ported verbatim from the held-message-living-field
// template's "living field" background. All visible state comes from uniforms.
const FRAG = [
"precision highp float;",
"varying vec2 vUv;",
"uniform float uTime;",
"uniform float uPhase;",
"uniform float uReveal;",
"uniform float uChromAb;",
"uniform float uVignette;",
"uniform float uIntensity;",
"uniform float uSpeed;",
"uniform vec2 uResolution;",
"uniform vec3 uColLow;",
"uniform vec3 uColMid;",
"uniform vec3 uColHigh;",
"uniform vec3 uAccent;",
"float hash21(vec2 p){",
" p = fract(p * vec2(123.34, 456.21));",
" p += dot(p, p + 45.32);",
" return fract(p.x * p.y);",
"}",
"float noise2(vec2 p){",
" vec2 i = floor(p);",
" vec2 f = fract(p);",
" vec2 u = f * f * (3.0 - 2.0 * f);",
" float a = hash21(i);",
" float b = hash21(i + vec2(1.0, 0.0));",
" float c = hash21(i + vec2(0.0, 1.0));",
" float d = hash21(i + vec2(1.0, 1.0));",
" return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);",
"}",
"float fbm(vec2 p){",
" float v = 0.0;",
" float a = 0.5;",
" for(int i=0;i<5;i++){",
" v += a * noise2(p);",
" p *= 2.02;",
" a *= 0.5;",
" }",
" return v;",
"}",
"vec2 curl(vec2 p, float t){",
" float e = 0.6;",
" float n1 = fbm(p + vec2(0.0, e) + t*0.18);",
" float n2 = fbm(p + vec2(0.0, -e) + t*0.18);",
" float n3 = fbm(p + vec2( e, 0.0) + t*0.18);",
" float n4 = fbm(p + vec2(-e, 0.0) + t*0.18);",
" return vec2(n1 - n2, -(n3 - n4));",
"}",
"vec3 palette(float t){",
" t = clamp(t, 0.0, 1.0);",
" vec3 lowMid = mix(uColLow, uColMid, smoothstep(0.0, 0.5, t));",
" return mix(lowMid, uColHigh, smoothstep(0.5, 1.0, t));",
"}",
"vec3 sampleField(vec2 uv, float t, float energy){",
" vec2 p = (uv - 0.5) * vec2(uResolution.x/uResolution.y, 1.0) * 2.4;",
" vec2 c = curl(p * 0.9, t);",
" vec2 q = p + c * (0.45 + 0.55 * energy);",
" float flow = fbm(q * 1.6 + vec2(t * 0.11, -t * 0.07));",
" float bands = sin(flow * 14.0 + t * 1.1);",
" bands = smoothstep(0.0, 1.0, 0.5 + 0.5 * bands);",
" float r = length(p);",
" float core = smoothstep(1.5, 0.05, r) * (0.6 + 0.4 * energy);",
" float glow = exp(-r * 1.6) * (0.55 + 0.45 * energy);",
" float k = clamp(flow * 0.85 + 0.15 * bands + 0.25 * core + 0.6 * glow, 0.0, 1.4);",
" vec3 col = palette(k * 0.9 + 0.1 * sin(t * 0.4));",
" col *= 0.7 + 0.6 * energy;",
" col += uAccent * core * 0.55;",
" return col;",
"}",
"void main(){",
" vec2 uv = vUv;",
" float t = uTime * uSpeed;",
" float e = uIntensity;",
" vec2 dir = normalize(uv - 0.5 + 1e-6);",
" float ca = uChromAb * (0.004 + 0.012 * length(uv - 0.5));",
" vec3 cR = sampleField(uv + dir * ca, t, e);",
" vec3 cG = sampleField(uv, t, e);",
" vec3 cB = sampleField(uv - dir * ca, t, e);",
" vec3 col = vec3(cR.r, cG.g, cB.b);",
" float lum = dot(col, vec3(0.299, 0.587, 0.114));",
" col = mix(vec3(lum), col, 1.05);",
" float rev = uReveal;",
" float sweep = smoothstep(0.0, 0.6, rev) * (1.0 - smoothstep(0.6, 1.0, rev));",
" col += uColHigh * sweep * 0.35;",
" vec3 phaseTint = mix(vec3(0.85, 0.92, 1.05), vec3(1.05, 0.95, 0.85), uPhase);",
" col *= phaseTint;",
" float v = length(uv - 0.5);",
" col *= 1.0 - smoothstep(0.45, 0.95, v) * uVignette;",
" col = col / (col + 1.0);",
" col = pow(col, vec3(1.0/2.2));",
" gl_FragColor = vec4(col, 1.0);",
"}",
].join("\n");
function compile(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
gl.useProgram(prog);
// full-screen triangle
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const posLoc = gl.getAttribLocation(prog, "position");
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
gl.viewport(0, 0, 1920, 1080);
const L = (n) => gl.getUniformLocation(prog, n);
const uLoc = {
uTime: L("uTime"),
uPhase: L("uPhase"),
uReveal: L("uReveal"),
uChromAb: L("uChromAb"),
uVignette: L("uVignette"),
uIntensity: L("uIntensity"),
uSpeed: L("uSpeed"),
uResolution: L("uResolution"),
uColLow: L("uColLow"),
uColMid: L("uColMid"),
uColHigh: L("uColHigh"),
uAccent: L("uAccent"),
};
// animated uniform state (GSAP tweens these .value fields)
const u = {
uTime: { value: 0 },
uPhase: { value: 0.1 },
uReveal: { value: 0 },
uChromAb: { value: 0 },
uVignette: { value: 0 },
uIntensity: { value: 0.3 },
};
const speed = Number(vars.flowSpeed) || 1;
const cLow = hexLin(vars.colorLow);
const cMid = hexLin(vars.colorMid);
const cHigh = hexLin(vars.colorHigh);
const cAcc = hexLin(vars.accentColor);
function render() {
gl.uniform1f(uLoc.uTime, u.uTime.value);
gl.uniform1f(uLoc.uPhase, u.uPhase.value);
gl.uniform1f(uLoc.uReveal, u.uReveal.value);
gl.uniform1f(uLoc.uChromAb, u.uChromAb.value);
gl.uniform1f(uLoc.uVignette, u.uVignette.value);
gl.uniform1f(uLoc.uIntensity, u.uIntensity.value);
gl.uniform1f(uLoc.uSpeed, speed);
gl.uniform2f(uLoc.uResolution, 1920, 1080);
gl.uniform3fv(uLoc.uColLow, cLow);
gl.uniform3fv(uLoc.uColMid, cMid);
gl.uniform3fv(uLoc.uColHigh, cHigh);
gl.uniform3fv(uLoc.uAccent, cAcc);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
render(); // initial frame so a seek to 0 / first paint is correct
// ── Energy-envelope drive (the "living field" breath): all repaint is via
// onUpdate, so the field is fully determined by the timeline position — no
// requestAnimationFrame, seek-safe. ──
tl.eventCallback("onUpdate", render);
tl.to(u.uTime, { value: 4, duration: 4, ease: "none" }, 0);
// energy: settle-in → breathe → SURGE → settle dark for the hold
tl.to(u.uIntensity, { value: 0.6, duration: 1.2, ease: "sine.inOut" }, 0);
tl.to(u.uIntensity, { value: 1.0, duration: 0.7, ease: "power2.in" }, 1.3);
tl.to(u.uIntensity, { value: 0.35, duration: 2.0, ease: "power2.out" }, 2.0);
// chromatic light-leak: light at entry, spikes on the surge, then settles
tl.fromTo(u.uChromAb, { value: 0 }, { value: 0.4, duration: 1.2, ease: "sine.in" }, 0);
tl.to(u.uChromAb, { value: 1.4, duration: 0.5, ease: "power3.in" }, 1.4);
tl.to(u.uChromAb, { value: 0.2, duration: 1.6, ease: "power2.out" }, 1.9);
// a brief highlight sweep across the surge
tl.fromTo(u.uReveal, { value: 0 }, { value: 1, duration: 0.7, ease: "none" }, 1.3);
tl.set(u.uReveal, { value: 0 }, 2.0);
// colour-temperature drift
tl.to(u.uPhase, { value: 0.6, duration: 2.2, ease: "sine.inOut" }, 0.6);
// black mask settles the field to near-black + central glow for the hold
tl.to(u.uVignette, { value: 0.85, duration: 1.6, ease: "power2.out" }, 1.9);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>08 · binary_decrypt</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-cyan: #2afff0;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.crypt {
font-family: "JetBrains Mono", monospace;
font-weight: 700;
font-size: 200px;
letter-spacing: 0.02em;
color: var(--hg-cyan);
text-shadow: 0 0 30px rgba(42, 255, 240, 0.5);
}
.crypt .char {
display: inline-block;
min-width: 1ch;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="crypt">
<span class="char" data-target="D">_</span><span class="char" data-target="E">_</span
><span class="char" data-target="C">_</span><span class="char" data-target="R">_</span
><span class="char" data-target="Y">_</span><span class="char" data-target="P">_</span
><span class="char" data-target="T">_</span>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
const SCRAMBLE = "01█▓▒░▀▄■◆◇▪▫";
function scrambleChar(charIdx, phase) {
const tick = Math.floor(phase * 60);
const i = (charIdx * 7 + tick * 13 + 3) % SCRAMBLE.length;
return SCRAMBLE.charAt(i);
}
const cryptState = [{ p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }, { p: 0 }];
cryptState.forEach((state, idx) => {
tl.to(
state,
{
p: 1,
duration: 0.85,
delay: idx * 0.05,
ease: "power2.out",
onUpdate: function () {
const spans = document.querySelectorAll(".crypt .char");
const span = spans[idx];
if (!span) return;
const target = span.getAttribute("data-target");
if (state.p >= 0.72) {
span.textContent = target;
span.style.color = "var(--hg-fg)";
span.style.textShadow = "0 0 30px rgba(168, 85, 247, 0.8)";
} else {
span.textContent = scrambleChar(idx, state.p);
}
},
},
0.15,
);
});
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>blur_resolve</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hero {
font-weight: 900;
font-size: 320px;
letter-spacing: -0.04em;
line-height: 0.95;
color: var(--hg-fg);
white-space: nowrap;
will-change: filter, transform, opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="hero" id="hero">FOCUS</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Resolve: heavy blur + scaled-up + dim settles into perfect crisp focus.
tl.fromTo(
"#hero",
{ filter: "blur(36px)", scale: 1.08, opacity: 0.25 },
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.7, ease: "power2.out" },
0.05,
);
// Hold crisp through the middle (no keys needed — state persists to ~1.5).
// Blur-out near the end: brief defocus + dim, a cut-away that loops seamlessly.
tl.to(
"#hero",
{ filter: "blur(24px)", opacity: 0.25, duration: 0.25, ease: "power2.in" },
1.7,
);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>03 · braam_punch</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hero {
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
line-height: 0.95;
color: var(--hg-fg);
white-space: nowrap;
will-change: transform, text-shadow;
}
.glow-ring {
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
background: radial-gradient(circle, rgba(124, 58, 237, 0.55) 0%, rgba(124, 58, 237, 0) 65%);
filter: blur(20px);
will-change: transform, opacity;
opacity: 0;
}
.flash {
position: absolute;
inset: 0;
background: #fff;
opacity: 0;
pointer-events: none;
will-change: opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="glow-ring"></div>
<div class="hero" id="hero">BRAAM</div>
</div>
<div class="flash"></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.set("#hero", { scale: 0.92, opacity: 0 }, 0);
tl.set(".glow-ring", { scale: 0.6, opacity: 0 }, 0);
tl.to("#hero", { opacity: 1, duration: 0.18, ease: "power2.out" }, 0.05);
tl.to(".glow-ring", { scale: 1.2, opacity: 1, duration: 0.45, ease: "power3.out" }, 0.05);
tl.to(
"#hero",
{
scale: 1.18,
textShadow: "0 0 60px rgba(168,85,247,0.9)",
duration: 0.12,
ease: "power4.out",
},
0.25,
);
tl.to(
"#hero",
{
scale: 1.0,
textShadow: "0 0 0px rgba(168,85,247,0)",
duration: 0.7,
ease: "elastic.out(1, 0.5)",
},
0.37,
);
tl.fromTo(
".flash",
{ opacity: 0 },
{ opacity: 0.85, duration: 0.06, ease: "power4.out" },
0.25,
);
tl.to(".flash", { opacity: 0, duration: 0.35, ease: "power3.in" }, 0.31);
tl.to(
".glow-ring",
{ scale: 1.45, opacity: 0.4, duration: 1.0, ease: "power2.out", overwrite: "auto" },
0.55,
);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>04 · chromatic_split</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.stack {
position: relative;
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
line-height: 0.95;
white-space: nowrap;
}
.layer {
position: absolute;
top: 0;
left: 0;
mix-blend-mode: screen;
will-change: transform;
}
.layer.r {
color: #ff2a4a;
}
.layer.g {
color: #2afff0;
}
.layer.b {
color: #b56dff;
}
.spacer {
color: transparent;
}
.scanlines {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
rgba(255, 255, 255, 0) 0px,
rgba(255, 255, 255, 0) 3px,
rgba(255, 255, 255, 0.04) 3px,
rgba(255, 255, 255, 0.04) 4px
);
opacity: 0;
pointer-events: none;
will-change: opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="stack">
<span class="spacer">GLITCH</span>
<span class="layer r">GLITCH</span>
<span class="layer g">GLITCH</span>
<span class="layer b">GLITCH</span>
</div>
</div>
<div class="scanlines"></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo(
".stack",
{ opacity: 0, scale: 0.95 },
{ opacity: 1, scale: 1, duration: 0.25, ease: "power2.out" },
0.05,
);
tl.set(".layer.r", { x: -2 }, 0);
tl.set(".layer.g", { x: 2 }, 0);
tl.set(".layer.b", { x: 0 }, 0);
tl.to(".layer.r", { x: -14, duration: 0.06, ease: "steps(1)" }, 0.35);
tl.to(".layer.g", { x: 14, duration: 0.06, ease: "steps(1)" }, 0.35);
tl.to(".layer.r", { x: -4, duration: 0.06, ease: "steps(1)" }, 0.5);
tl.to(".layer.g", { x: 4, duration: 0.06, ease: "steps(1)" }, 0.5);
tl.to(".layer.r", { x: -22, duration: 0.04, ease: "steps(1)" }, 0.85);
tl.to(".layer.g", { x: 22, duration: 0.04, ease: "steps(1)" }, 0.85);
tl.to(".layer.b", { x: 6, duration: 0.04, ease: "steps(1)" }, 0.85);
tl.to(".layer.r", { x: -6, duration: 0.04, ease: "steps(1)" }, 0.97);
tl.to(".layer.g", { x: 6, duration: 0.04, ease: "steps(1)" }, 0.97);
tl.to(".layer.b", { x: -2, duration: 0.04, ease: "steps(1)" }, 0.97);
tl.to(".layer.r", { x: -3, duration: 0.4, ease: "power2.out" }, 1.2);
tl.to(".layer.g", { x: 3, duration: 0.4, ease: "power2.out" }, 1.2);
tl.to(".layer.b", { x: 0, duration: 0.4, ease: "power2.out" }, 1.2);
tl.fromTo(
".scanlines",
{ opacity: 0 },
{ opacity: 0.85, duration: 0.06, ease: "steps(1)" },
0.35,
);
tl.to(".scanlines", { opacity: 0.25, duration: 0.5, ease: "power2.out" }, 1.0);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>13 · chrome_sweep</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.chrome {
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
background: linear-gradient(
110deg,
#404048 0%,
#b4b4be 20%,
#ffffff 45%,
#ffffff 50%,
#d8d8e0 60%,
#5a5a64 85%,
#ffffff 100%
);
background-size: 250% 100%;
background-position: 200% 0;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
will-change: background-position;
filter: drop-shadow(0 6px 0 rgba(0, 0, 0, 0.4));
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage"><div class="chrome">CHROME</div></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo(
".chrome",
{ opacity: 0, scale: 0.96 },
{ opacity: 1, scale: 1, duration: 0.3, ease: "power3.out" },
0.2,
);
tl.fromTo(
".chrome",
{ backgroundPosition: "200% 0" },
{ backgroundPosition: "-100% 0", duration: 1.4, ease: "power2.inOut" },
0.3,
);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>22 · counting_punch</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-cyan: #2afff0;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 30px;
}
.counter {
font-weight: 900;
font-size: 360px;
letter-spacing: -0.04em;
font-variant-numeric: tabular-nums;
color: var(--hg-fg);
will-change: transform, text-shadow;
line-height: 1;
}
.caption {
font-family: "JetBrains Mono", monospace;
font-size: 28px;
color: var(--hg-dim);
letter-spacing: 0.3em;
text-transform: uppercase;
opacity: 0;
}
.underline {
height: 4px;
background: var(--hg-violet-2);
width: 0;
box-shadow: 0 0 20px var(--hg-violet);
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="counter" id="counter">0</div>
<div class="underline" id="underline"></div>
<div class="caption" id="caption">USERS THIS WEEK</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
const TARGET = 12847;
const counter = { v: 0 };
let lastShown = 0;
function format(n) {
return Math.floor(n).toLocaleString("en-US");
}
tl.to(
counter,
{
v: TARGET,
duration: 1.15,
ease: "expo.out",
onUpdate: function () {
const el = document.getElementById("counter");
if (!el) return;
const cur = Math.floor(counter.v);
if (cur !== lastShown) {
el.textContent = format(cur);
// tiny scale tick on each digit change
el.style.transform = "scale(1.045)";
el.style.textShadow = "0 0 40px rgba(168, 85, 247, 0.6)";
// browser will paint, then this resets via the next onUpdate
}
lastShown = cur;
},
},
0.15,
);
// Big settle punch at landing
tl.to(
"#counter",
{
scale: 1.12,
textShadow: "0 0 80px rgba(168, 85, 247, 0.9)",
duration: 0.1,
ease: "power4.out",
},
1.3,
);
tl.to(
"#counter",
{
scale: 1.0,
textShadow: "0 0 20px rgba(168, 85, 247, 0.4)",
duration: 0.6,
ease: "elastic.out(1, 0.55)",
},
1.4,
);
// Underline draws
tl.to("#underline", { width: 420, duration: 0.6, ease: "expo.out" }, 1.35);
tl.to("#caption", { opacity: 1, duration: 0.5, ease: "power2.out" }, 1.55);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>19 · crash_zoom_in</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.zoom-stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
will-change: transform;
}
.radial-bg {
position: absolute;
inset: 0;
background: radial-gradient(
circle at center,
rgba(168, 85, 247, 0.18) 0%,
transparent 35%,
rgba(0, 0, 0, 0.6) 80%
);
opacity: 0;
will-change: opacity, transform;
}
.speed-lines {
position: absolute;
inset: 0;
background: repeating-radial-gradient(
circle at 50% 50%,
transparent 0px,
transparent 6px,
rgba(255, 255, 255, 0.08) 7px,
transparent 8px
);
opacity: 0;
will-change: opacity;
mix-blend-mode: screen;
}
.hero {
font-weight: 900;
font-size: 360px;
letter-spacing: -0.05em;
color: var(--hg-fg);
white-space: nowrap;
will-change: transform, filter;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="radial-bg"></div>
<div class="speed-lines"></div>
<div class="zoom-stage"><div class="hero">CRASH</div></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Hold black for tension
tl.set(".hero", { scale: 0.03, filter: "blur(20px)", opacity: 0 }, 0);
const CRASH = 0.45;
// The crash itself
tl.to(
".hero",
{ scale: 1.0, filter: "blur(0px)", opacity: 1, duration: 0.32, ease: "expo.in" },
CRASH,
);
// Background swoosh in same window
tl.fromTo(
".radial-bg",
{ opacity: 0, scale: 0.4 },
{ opacity: 1, scale: 1, duration: 0.32, ease: "expo.in" },
CRASH,
);
tl.fromTo(
".speed-lines",
{ opacity: 0, scale: 0.2 },
{ opacity: 1, scale: 1.4, duration: 0.32, ease: "expo.in" },
CRASH,
);
// Landing settle
const LAND = CRASH + 0.32;
tl.to(".hero", { scale: 0.96, duration: 0.08, ease: "power3.out" }, LAND);
tl.to(".hero", { scale: 1.0, duration: 0.5, ease: "elastic.out(1, 0.55)" }, LAND + 0.08);
// Quick landing shake
const sh = [9, -7, 5, -4, 2, -1];
sh.forEach((amp, i) => {
tl.to(
".zoom-stage",
{ x: amp, y: amp * 0.5 * (i % 2 ? -1 : 1), duration: 0.03, ease: "steps(1)" },
LAND + i * 0.03,
);
});
tl.to(
".zoom-stage",
{ x: 0, y: 0, duration: 0.1, ease: "power2.out" },
LAND + sh.length * 0.03,
);
// Speed lines fade
tl.to(".speed-lines", { opacity: 0, duration: 0.4, ease: "power2.out" }, LAND + 0.05);
// Radial settles
tl.to(".radial-bg", { opacity: 0.5, duration: 0.5, ease: "power2.out" }, LAND);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>27 · datamosh_smear</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-cyan: #2afff0;
--hg-magenta: #ff2a4a;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.smear-stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.smear-hero {
position: relative;
font-weight: 900;
font-size: 320px;
letter-spacing: -0.05em;
line-height: 0.95;
white-space: nowrap;
}
.smear-layer {
position: absolute;
top: 0;
left: 0;
mix-blend-mode: screen;
will-change: transform, opacity, filter;
}
.smear-layer.l1 {
color: #ff2a4a;
}
.smear-layer.l2 {
color: #2afff0;
}
.smear-layer.l3 {
color: #b56dff;
}
.smear-layer.l4 {
color: #ffd166;
}
.smear-layer.l5 {
color: #f5f5fa;
}
.spacer {
color: transparent;
}
.scan-noise {
position: absolute;
inset: 0;
background:
repeating-linear-gradient(
0deg,
rgba(255, 255, 255, 0) 0px,
rgba(255, 255, 255, 0) 2px,
rgba(255, 255, 255, 0.07) 3px
),
repeating-linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0) 4px,
rgba(0, 0, 0, 0.15) 5px
);
opacity: 0;
will-change: opacity;
pointer-events: none;
mix-blend-mode: overlay;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="smear-stage">
<div class="smear-hero">
<span class="spacer">SMEAR</span>
<span class="smear-layer l1">SMEAR</span>
<span class="smear-layer l2">SMEAR</span>
<span class="smear-layer l3">SMEAR</span>
<span class="smear-layer l4">SMEAR</span>
<span class="smear-layer l5">SMEAR</span>
</div>
</div>
<div class="scan-noise"></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// All layers start far right, stagger-lag in, then settle near center
const layers = [
".smear-layer.l1",
".smear-layer.l2",
".smear-layer.l3",
".smear-layer.l4",
".smear-layer.l5",
];
layers.forEach((sel, i) => {
tl.fromTo(
sel,
{ x: 360, opacity: 0, filter: "blur(20px)" },
{ x: 0, opacity: 0.85, filter: "blur(0px)", duration: 0.7, ease: "expo.out" },
0.2 + i * 0.08,
);
});
// Final settle: pull layers into proper offset
const finalX = [-6, -2, 0, 2, 4];
layers.forEach((sel, i) => {
tl.to(sel, { x: finalX[i], duration: 0.4, ease: "power2.out" }, 1.1);
});
// Scan noise pulses
tl.fromTo(
".scan-noise",
{ opacity: 0 },
{ opacity: 1, duration: 0.06, ease: "steps(1)" },
0.2,
);
tl.to(".scan-noise", { opacity: 0.25, duration: 0.8, ease: "power2.out" }, 0.6);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>directional_fill</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 36px;
}
.bar {
position: relative;
width: 1040px;
height: 188px;
border: 3px solid var(--hg-dim);
border-radius: 12px;
overflow: hidden;
display: flex;
align-items: center;
padding: 0 56px;
}
.fill {
position: absolute;
inset: 0;
transform: scaleX(0);
will-change: transform;
}
.fill.a {
background: var(--hg-violet);
transform-origin: left center;
}
.fill.b {
background: var(--hg-violet-2);
transform-origin: right center;
}
.fill.c {
background: #2afff0;
transform-origin: left center;
}
.label {
position: relative;
z-index: 1;
font-weight: 900;
font-size: 96px;
letter-spacing: -0.03em;
line-height: 1;
color: var(--hg-fg);
will-change: color;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="1.8"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="1.8" data-track-index="1">
<div class="stage">
<div class="bar">
<div class="fill a"></div>
<span class="label" id="l1">READY</span>
</div>
<div class="bar">
<div class="fill b"></div>
<span class="label" id="l2">SET</span>
</div>
<div class="bar">
<div class="fill c"></div>
<span class="label" id="l3">FILL</span>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Bar 1 — sweeps left -> right, landing on the beat at ~0.55s.
tl.to(".fill.a", { scaleX: 1, duration: 0.35, ease: "power2.out" }, 0.2);
tl.set("#l1", { color: "var(--hg-bg)" }, 0.55);
// Bar 2 — sweeps right -> left, landing at ~0.85s.
tl.to(".fill.b", { scaleX: 1, duration: 0.35, ease: "power2.out" }, 0.5);
tl.set("#l2", { color: "var(--hg-bg)" }, 0.85);
// Bar 3 — sweeps left -> right, landing at ~1.15s.
tl.to(".fill.c", { scaleX: 1, duration: 0.35, ease: "power2.out" }, 0.8);
tl.set("#l3", { color: "var(--hg-bg)" }, 1.15);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>09 · dolly_zoom</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.layers {
position: absolute;
inset: 0;
overflow: hidden;
}
.bg-far,
.bg-mid,
.bg-near {
position: absolute;
inset: 0;
will-change: transform, filter;
}
.bg-far {
background:
radial-gradient(2px 2px at 20% 30%, var(--hg-dim) 50%, transparent 51%),
radial-gradient(2px 2px at 70% 80%, var(--hg-dim) 50%, transparent 51%),
radial-gradient(1px 1px at 45% 60%, var(--hg-dim) 50%, transparent 51%),
radial-gradient(1px 1px at 85% 25%, var(--hg-dim) 50%, transparent 51%),
radial-gradient(2px 2px at 10% 75%, var(--hg-dim) 50%, transparent 51%),
radial-gradient(1px 1px at 60% 15%, var(--hg-dim) 50%, transparent 51%);
background-size: 400px 400px;
filter: blur(2px);
}
.bg-mid {
background:
linear-gradient(90deg, transparent 49.7%, rgba(168, 85, 247, 0.25) 50%, transparent 50.3%)
0 0 / 200px 200px,
linear-gradient(
180deg,
transparent 49.7%,
rgba(168, 85, 247, 0.15) 50%,
transparent 50.3%
)
0 0 / 200px 200px;
}
.bg-near {
background:
radial-gradient(circle at 15% 50%, rgba(168, 85, 247, 0.4) 0%, transparent 25%),
radial-gradient(circle at 85% 50%, rgba(124, 58, 237, 0.4) 0%, transparent 25%);
}
.subject {
position: relative;
z-index: 5;
font-family: "Playfair Display", serif;
font-style: italic;
font-weight: 900;
font-size: 180px;
letter-spacing: -0.02em;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="layers">
<div class="bg-far"></div>
<div class="bg-mid"></div>
<div class="bg-near"></div>
</div>
<div class="stage"><div class="subject">subject.</div></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo(
".bg-far",
{ scale: 1, filter: "blur(2px)" },
{ scale: 1.6, filter: "blur(4px)", duration: 1.8, ease: "power2.inOut" },
0.1,
);
tl.fromTo(".bg-mid", { scale: 1 }, { scale: 1.4, duration: 1.8, ease: "power2.inOut" }, 0.1);
tl.fromTo(".bg-near", { scale: 1 }, { scale: 1.2, duration: 1.8, ease: "power2.inOut" }, 0.1);
tl.fromTo(
".subject",
{ opacity: 0, scale: 1.1 },
{ opacity: 1, scale: 1, duration: 0.6, ease: "power3.out" },
0.15,
);
tl.to(".layers", { skewY: -0.5, duration: 1.8, ease: "power2.inOut" }, 0.1);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>11 · electric_arc</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-cyan: #2afff0;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.arc-svg {
position: absolute;
inset: 0;
}
.arc-path {
fill: none;
stroke: var(--hg-fg);
stroke-width: 3;
stroke-linecap: round;
filter: drop-shadow(0 0 8px var(--hg-cyan)) drop-shadow(0 0 16px var(--hg-cyan));
stroke-dasharray: 1200;
stroke-dashoffset: 1200;
will-change: stroke-dashoffset, opacity;
}
.arc-flash {
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 50%, rgba(42, 255, 240, 0.3) 0%, transparent 40%);
opacity: 0;
will-change: opacity;
}
.arc-label {
font-weight: 900;
font-size: 240px;
letter-spacing: -0.04em;
color: var(--hg-fg);
position: relative;
z-index: 2;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<svg class="arc-svg" viewBox="0 0 1920 1080" preserveAspectRatio="none">
<path
class="arc-path"
id="arc1"
d="M 200 540 L 380 480 L 440 580 L 620 500 L 720 600 L 880 510 L 960 540"
/>
<path
class="arc-path"
id="arc2"
d="M 1720 540 L 1540 600 L 1480 500 L 1300 580 L 1200 480 L 1040 570 L 960 540"
/>
<path
class="arc-path"
id="arc3"
d="M 960 200 L 920 360 L 1040 420 L 940 520 L 960 540"
/>
</svg>
<div class="arc-flash"></div>
<div class="arc-label">CHARGE</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo(
".arc-label",
{ opacity: 0, scale: 0.9 },
{ opacity: 1, scale: 1, duration: 0.3, ease: "power3.out" },
0.5,
);
const arcs = ["#arc1", "#arc2", "#arc3"];
arcs.forEach((p, i) => {
tl.fromTo(
p,
{ strokeDashoffset: 1200, opacity: 0 },
{ strokeDashoffset: 0, opacity: 1, duration: 0.18, ease: "power4.out" },
0.15 + i * 0.05,
);
});
tl.to(arcs.join(","), { opacity: 0.3, duration: 0.04, ease: "steps(1)" }, 0.55);
tl.to(arcs.join(","), { opacity: 1, duration: 0.04, ease: "steps(1)" }, 0.6);
tl.to(arcs.join(","), { opacity: 0.5, duration: 0.04, ease: "steps(1)" }, 0.7);
tl.to(arcs.join(","), { opacity: 1, duration: 0.04, ease: "steps(1)" }, 0.78);
tl.fromTo(
".arc-flash",
{ opacity: 0 },
{ opacity: 1, duration: 0.06, ease: "steps(1)" },
0.5,
);
tl.to(".arc-flash", { opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>flash_cut</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.word {
position: absolute;
font-weight: 900;
font-size: 260px;
letter-spacing: -0.04em;
line-height: 0.95;
white-space: nowrap;
color: var(--hg-fg);
will-change: opacity;
}
.accent {
position: absolute;
bottom: 360px;
width: 220px;
height: 14px;
border-radius: 999px;
background: var(--hg-violet);
will-change: background, transform;
}
.flash {
position: absolute;
inset: 0;
background: #ffffff;
opacity: 0;
pointer-events: none;
will-change: opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="1.8"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="1.8" data-track-index="1">
<div class="stage">
<div class="accent" id="accent"></div>
<div class="word" id="w0">READY</div>
<div class="word" id="w1">IMPACT</div>
<div class="word" id="w2">DONE</div>
</div>
<div class="flash" id="flash"></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Initial state: first word visible, others hidden, accent violet.
tl.set("#w0", { opacity: 1 }, 0);
tl.set("#w1", { opacity: 0 }, 0);
tl.set("#w2", { opacity: 0 }, 0);
tl.set("#accent", { backgroundColor: "#7c3aed", scaleX: 1 }, 0);
tl.set("#flash", { opacity: 0 }, 0);
// Two hits; during each white frame, swap underlying word + accent colour.
const hits = [
{ t: 0.5, from: "#w0", to: "#w1", color: "#a855f7", scaleX: 1.45 },
{ t: 1.2, from: "#w1", to: "#w2", color: "#f5f5fa", scaleX: 0.7 },
];
hits.forEach((h) => {
// Instant full-frame white flash on the hit.
tl.set("#flash", { opacity: 1 }, h.t);
// While white (~2 frames at 30fps), swap the revealed state.
tl.set(h.from, { opacity: 0 }, h.t + 0.03);
tl.set(h.to, { opacity: 1 }, h.t + 0.03);
tl.set("#accent", { backgroundColor: h.color, scaleX: h.scaleX }, h.t + 0.03);
// Snap the flash back out within ~3 frames.
tl.set("#flash", { opacity: 0 }, h.t + 0.06);
});
// Graceful loop: dip back to white at the very end and restore the start
// state so the cycle re-enters cleanly on the next pass.
tl.set("#flash", { opacity: 1 }, 1.74);
tl.set("#w2", { opacity: 0 }, 1.77);
tl.set("#w0", { opacity: 1 }, 1.77);
tl.set("#accent", { backgroundColor: "#7c3aed", scaleX: 1 }, 1.77);
tl.set("#flash", { opacity: 0 }, 1.8);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>17 · gooey_metaball</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-cyan: #2afff0;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.hero {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
color: var(--hg-fg);
z-index: 5;
pointer-events: none;
}
.gooey-stage {
position: absolute;
inset: 0;
filter: url(#gooey-filter);
}
.ball {
position: absolute;
border-radius: 50%;
background: var(--hg-violet-2);
top: 50%;
left: 50%;
will-change: transform;
opacity: 0;
}
.ball.a {
width: 280px;
height: 280px;
background: #7c3aed;
}
.ball.b {
width: 240px;
height: 240px;
background: #a855f7;
}
.ball.c {
width: 200px;
height: 200px;
background: #d946ef;
}
.ball.d {
width: 320px;
height: 320px;
background: #8b5cf6;
}
.ball.e {
width: 180px;
height: 180px;
background: #c084fc;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<svg style="position: absolute; width: 0; height: 0">
<defs>
<filter id="gooey-filter">
<feGaussianBlur in="SourceGraphic" stdDeviation="20" result="blur" />
<feColorMatrix
in="blur"
mode="matrix"
values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 22 -10"
result="gooey"
/>
<feComposite in="SourceGraphic" in2="gooey" operator="atop" />
</filter>
</defs>
</svg>
<div class="gooey-stage">
<div class="ball a" id="ballA"></div>
<div class="ball b" id="ballB"></div>
<div class="ball c" id="ballC"></div>
<div class="ball d" id="ballD"></div>
<div class="ball e" id="ballE"></div>
</div>
<div class="hero">GOOEY</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Initial: all balls collapsed at center
["#ballA", "#ballB", "#ballC", "#ballD", "#ballE"].forEach((id) => {
tl.set(id, { xPercent: -50, yPercent: -50, scale: 0, opacity: 0 }, 0);
});
// Spawn outward
tl.to(
"#ballA",
{
xPercent: -50 - 120,
yPercent: -50 + 20,
scale: 1,
opacity: 1,
duration: 0.5,
ease: "back.out(1.6)",
},
0.15,
);
tl.to(
"#ballB",
{
xPercent: -50 + 100,
yPercent: -50 + 50,
scale: 1,
opacity: 1,
duration: 0.55,
ease: "back.out(1.6)",
},
0.2,
);
tl.to(
"#ballC",
{
xPercent: -50 + 30,
yPercent: -50 - 80,
scale: 1,
opacity: 1,
duration: 0.55,
ease: "back.out(1.6)",
},
0.25,
);
tl.to(
"#ballD",
{
xPercent: -50 - 50,
yPercent: -50 - 30,
scale: 1,
opacity: 1,
duration: 0.55,
ease: "back.out(1.6)",
},
0.3,
);
tl.to(
"#ballE",
{
xPercent: -50 + 80,
yPercent: -50 - 40,
scale: 1,
opacity: 1,
duration: 0.55,
ease: "back.out(1.6)",
},
0.18,
);
// Idle drift
tl.to(
"#ballA",
{ xPercent: "+=20", yPercent: "+=12", duration: 1.0, ease: "sine.inOut" },
0.7,
);
tl.to(
"#ballB",
{ xPercent: "-=18", yPercent: "-=10", duration: 1.0, ease: "sine.inOut" },
0.7,
);
tl.to(
"#ballC",
{ xPercent: "-=15", yPercent: "+=20", duration: 1.0, ease: "sine.inOut" },
0.7,
);
tl.to(
"#ballD",
{ xPercent: "+=25", yPercent: "+=8", duration: 1.0, ease: "sine.inOut" },
0.7,
);
tl.to(
"#ballE",
{ xPercent: "-=22", yPercent: "+=18", duration: 1.0, ease: "sine.inOut" },
0.7,
);
// Reveal text behind
tl.fromTo(
".hero",
{ opacity: 0, scale: 0.95 },
{ opacity: 1, scale: 1, duration: 0.5, ease: "power3.out" },
0.6,
);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>hard_cut</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
}
.panel {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
will-change: opacity;
}
.word {
font-weight: 900;
font-size: 340px;
letter-spacing: -0.05em;
line-height: 0.9;
text-transform: uppercase;
white-space: nowrap;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage" id="stage"></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Deterministic palette + word list. Variation is derived from index only.
const BG = ["#7c3aed", "#0a0a0f", "#ff2a4a", "#2afff0", "#a855f7", "#f5f5fa", "#0a0a0f"];
const FG = ["#f5f5fa", "#a855f7", "#0a0a0f", "#0a0a0f", "#0a0a0f", "#7c3aed", "#f5f5fa"];
const WORDS = ["CUT", "NOW", "HARD", "SNAP", "STOP", "GO", "CUT"];
const STEP = 0.28; // beat interval
const N = WORDS.length;
// Build one full-frame panel per cut, all hidden but the first.
const stage = document.getElementById("stage");
for (let i = 0; i < N; i++) {
const panel = document.createElement("div");
panel.className = "panel";
panel.id = "p" + i;
panel.style.background = BG[i];
panel.style.opacity = i === 0 ? "1" : "0";
const word = document.createElement("div");
word.className = "word";
word.style.color = FG[i];
word.textContent = WORDS[i];
panel.appendChild(word);
stage.appendChild(panel);
}
// Sample-accurate 0ms cuts: at each beat, hide current panel, show next.
// tl.set has no duration and no ease — brutal instant switching.
for (let i = 1; i < N; i++) {
const t = i * STEP;
tl.set("#p" + (i - 1), { opacity: 0 }, t);
tl.set("#p" + i, { opacity: 1 }, t);
}
// Loop gracefully: snap back to the first panel before the clip ends so a
// re-entry starts clean on the same color/word it began with.
tl.set("#p" + (N - 1), { opacity: 0 }, N * STEP);
tl.set("#p0", { opacity: 1 }, N * STEP);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>01 · hypercut_whip</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hero {
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
line-height: 0.95;
color: var(--hg-fg);
white-space: nowrap;
will-change: transform, filter;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage"><div class="hero" id="hero">HYPERCUT</div></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.set("#hero", { x: 0, filter: "blur(0px)" }, 0);
tl.to("#hero", { x: 1800, filter: "blur(30px)", duration: 0.18, ease: "power4.in" }, 1.65);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>10 · iris_open</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.iris-stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
clip-path: circle(0% at 50% 50%);
will-change: clip-path;
background: linear-gradient(135deg, #1a0033 0%, #0a0a0f 60%);
}
.iris-stage .hero {
font-family: "Playfair Display", serif;
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
line-height: 0.95;
color: var(--hg-fg);
white-space: nowrap;
}
.iris-ring {
position: absolute;
border-radius: 50%;
border: 2px solid var(--hg-violet);
box-shadow: 0 0 40px var(--hg-violet);
opacity: 0;
will-change: transform, opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="iris-stage">
<div class="iris-ring" style="width: 200px; height: 200px"></div>
<div class="hero">IRIS</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.set(".iris-stage", { clipPath: "circle(0% at 50% 50%)" }, 0);
tl.fromTo(
".iris-ring",
{ scale: 0, opacity: 0.9 },
{ scale: 9, opacity: 0, duration: 0.85, ease: "expo.out" },
0.2,
);
tl.to(
".iris-stage",
{ clipPath: "circle(75% at 50% 50%)", duration: 0.9, ease: "expo.out" },
0.3,
);
tl.from(".iris-stage .hero", { y: 40, opacity: 0, duration: 0.6, ease: "power3.out" }, 0.6);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>02 · kinetic_letter_in</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hero {
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
line-height: 0.95;
color: var(--hg-fg);
white-space: nowrap;
}
.char {
display: inline-block;
will-change: transform, opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="hero" id="hero">
<span class="char">K</span><span class="char">I</span><span class="char">N</span
><span class="char">E</span><span class="char">T</span><span class="char">I</span
><span class="char">C</span>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from(
".char",
{ y: 80, opacity: 0, stagger: 0.045, ease: "back.out(2)", duration: 0.55 },
0.05,
);
tl.to("#hero", { letterSpacing: "0.04em", duration: 0.6, ease: "power2.out" }, 0.7);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>16 · liquid_morph</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.blob-wrap {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.blob {
position: absolute;
width: 1100px;
height: 800px;
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 60%, #d946ef 100%);
clip-path: polygon(
50% 5%,
65% 8%,
78% 16%,
88% 30%,
92% 50%,
88% 70%,
78% 84%,
65% 92%,
50% 95%,
35% 92%,
22% 84%,
12% 70%,
8% 50%,
12% 30%,
22% 16%,
35% 8%
);
will-change: clip-path, transform;
filter: blur(0.5px);
}
.hero {
font-family: "Playfair Display", serif;
font-style: italic;
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
color: var(--hg-fg);
white-space: nowrap;
position: relative;
z-index: 2;
mix-blend-mode: difference;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="blob-wrap"><div class="blob"></div></div>
<div class="stage"><div class="hero">liquid</div></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
const BLOB_A =
"polygon(50% 5%, 65% 8%, 78% 16%, 88% 30%, 92% 50%, 88% 70%, 78% 84%, 65% 92%, 50% 95%, 35% 92%, 22% 84%, 12% 70%, 8% 50%, 12% 30%, 22% 16%, 35% 8%)";
const BLOB_B =
"polygon(38% 10%, 58% 4%, 75% 14%, 90% 28%, 95% 46%, 86% 60%, 80% 78%, 62% 88%, 46% 94%, 30% 86%, 16% 74%, 6% 58%, 10% 40%, 20% 22%, 30% 14%, 38% 10%)";
const BLOB_C =
"polygon(46% 8%, 62% 10%, 80% 20%, 92% 36%, 90% 52%, 92% 70%, 76% 86%, 58% 90%, 44% 94%, 28% 88%, 14% 76%, 10% 60%, 12% 44%, 18% 26%, 30% 12%, 38% 8%)";
tl.set(".blob", { clipPath: BLOB_A, scale: 0.95 }, 0);
tl.to(".blob", { clipPath: BLOB_B, scale: 1.05, duration: 0.7, ease: "power2.inOut" }, 0.2);
tl.to(".blob", { clipPath: BLOB_C, scale: 1.0, duration: 0.7, ease: "power2.inOut" }, 0.9);
tl.fromTo(
".hero",
{ opacity: 0, y: 30 },
{ opacity: 1, y: 0, duration: 0.6, ease: "power3.out" },
0.4,
);
tl.to(".blob", { rotation: 6, duration: 1.5, ease: "sine.inOut" }, 0.2);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>05 · mask_reveal</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hero {
font-weight: 900;
font-size: 280px;
letter-spacing: -0.04em;
line-height: 0.95;
color: var(--hg-fg);
white-space: nowrap;
}
.mask-wrap {
position: relative;
overflow: hidden;
padding: 0 60px;
}
.mask-clip {
clip-path: inset(0 100% 0 0);
will-change: clip-path;
}
.sweep {
position: absolute;
top: 0;
bottom: 0;
width: 8px;
background: var(--hg-violet);
box-shadow:
0 0 40px var(--hg-violet),
0 0 80px var(--hg-violet);
left: 0;
opacity: 0;
will-change: transform, opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="mask-wrap">
<div class="hero mask-clip">REVEAL</div>
<div class="sweep"></div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.set(".mask-clip", { clipPath: "inset(0 100% 0 0)" }, 0);
tl.set(".sweep", { x: 0, opacity: 0 }, 0);
tl.to(".sweep", { opacity: 1, duration: 0.08, ease: "power2.out" }, 0.1);
tl.to(".sweep", { x: 1300, duration: 0.75, ease: "expo.out" }, 0.15);
tl.to(".mask-clip", { clipPath: "inset(0 0% 0 0)", duration: 0.75, ease: "expo.out" }, 0.18);
tl.to(".sweep", { opacity: 0, duration: 0.3, ease: "power2.in" }, 0.85);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>mosaic_pack</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.grid {
display: grid;
grid-template-columns: repeat(6, 200px);
grid-template-rows: repeat(4, 200px);
gap: 18px;
}
.tile {
border-radius: 14px;
will-change: transform, opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2.4"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2.4" data-track-index="1">
<div class="stage">
<div class="grid" id="grid"></div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Deterministic palette + grid build (6x4 = 24 cells).
const PALETTE = ["#7c3aed", "#a855f7", "#f5f5fa", "#2a2a3a", "#b56dff", "#6b6b78"];
const COLS = 6;
const ROWS = 4;
const COUNT = COLS * ROWS;
const grid = document.getElementById("grid");
const tiles = [];
for (let i = 0; i < COUNT; i++) {
const tile = document.createElement("div");
tile.className = "tile";
tile.style.background = PALETTE[i % PALETTE.length];
grid.appendChild(tile);
tiles.push(tile);
}
// Scatter each tile from a deterministic offset/rotation, then pack into place.
// Land order ripples diagonally (col + row) for a deterministic "assembling" feel.
const STEP = 0.07;
for (let i = 0; i < COUNT; i++) {
const col = i % COLS;
const row = Math.floor(i / COLS);
const dx = (((i * 53) % 200) - 100) * 9; // far-flung scatter X
const dy = (((i * 89) % 200) - 100) * 7; // far-flung scatter Y
const rot = ((i * 37) % 80) - 40; // deterministic tilt
const order = col + row; // diagonal land sequence
tl.fromTo(
tiles[i],
{ x: dx, y: dy, rotation: rot, scale: 0.6, autoAlpha: 0.15 },
{
x: 0,
y: 0,
rotation: 0,
scale: 1,
autoAlpha: 1,
duration: 0.6,
ease: "back.out(1.6)",
},
0.1 + order * STEP,
);
}
// Settle the packed poster with a tiny breath so the loop seats cleanly.
tl.to("#grid", { scale: 1.02, duration: 0.18, ease: "power2.out" }, 1.9);
tl.to("#grid", { scale: 1, duration: 0.32, ease: "power2.inOut" }, 2.08);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>12 · neon_flicker</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.neon {
font-family: "Playfair Display", serif;
font-style: italic;
font-weight: 900;
font-size: 280px;
letter-spacing: -0.02em;
color: #ff64a0;
text-shadow:
0 0 8px #ff64a0,
0 0 24px #ff64a0,
0 0 60px #ff1a7a,
0 0 120px #ff1a7a;
will-change: opacity, text-shadow;
}
.neon-bg {
position: absolute;
inset: 0;
background: radial-gradient(
circle at 50% 50%,
rgba(255, 26, 122, 0.12) 0%,
transparent 60%
);
opacity: 0;
will-change: opacity;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="neon-bg"></div>
<div class="stage"><div class="neon">neon</div></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.set(".neon", { opacity: 0 }, 0);
const neonPattern = [
{ t: 0.2, o: 1 },
{ t: 0.26, o: 0 },
{ t: 0.3, o: 1 },
{ t: 0.38, o: 0.3 },
{ t: 0.42, o: 1 },
{ t: 0.5, o: 0.5 },
{ t: 0.55, o: 1 },
];
neonPattern.forEach((p) => {
tl.to(".neon", { opacity: p.o, duration: 0.02, ease: "steps(1)" }, p.t);
});
tl.to(".neon-bg", { opacity: 1, duration: 0.4, ease: "power2.out" }, 0.5);
window.__timelines["main"] = tl;
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>21 · outline_to_fill</title>
<script src="../assets/gsap.min.js"></script>
<style>
:root {
--hg-violet: #7c3aed;
--hg-violet-2: #a855f7;
--hg-bg: #0a0a0f;
--hg-fg: #f5f5fa;
--hg-dim: #6b6b78;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--hg-bg);
font-family: "Inter", system-ui, sans-serif;
color: var(--hg-fg);
-webkit-font-smoothing: antialiased;
}
#scene {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
}
.stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.outline-stack {
position: relative;
font-family: "Playfair Display", serif;
font-weight: 900;
font-size: 320px;
letter-spacing: -0.04em;
line-height: 0.95;
white-space: nowrap;
}
.stroke-layer {
position: absolute;
top: 0;
left: 0;
color: transparent;
-webkit-text-stroke: 3px var(--hg-violet-2);
text-shadow: 0 0 30px rgba(168, 85, 247, 0.5);
will-change:
opacity,
-webkit-text-stroke;
}
.fill-layer {
position: relative;
color: var(--hg-fg);
clip-path: inset(100% 0 0 0);
will-change: clip-path;
}
.spacer {
color: transparent;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="2"
data-width="1920"
data-height="1080"
>
<div id="scene" class="clip" data-start="0" data-duration="2" data-track-index="1">
<div class="stage">
<div class="outline-stack">
<span class="spacer">Premium</span>
<span class="stroke-layer">Premium</span>
<span class="fill-layer">Premium</span>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Stroke draws in
tl.fromTo(
".stroke-layer",
{ opacity: 0, scale: 1.04 },
{ opacity: 1, scale: 1, duration: 0.45, ease: "power3.out" },
0.15,
);
// Fill rises from bottom
tl.set(".fill-layer", { clipPath: "inset(100% 0 0 0)" }, 0);
tl.to(".fill-layer", { clipPath: "inset(0% 0 0 0)", duration: 1.0, ease: "expo.out" }, 0.55);
// Stroke recedes once fill arrives
tl.to(".stroke-layer", { opacity: 0.35, duration: 0.6, ease: "power2.out" }, 1.0);
window.__timelines["main"] = tl;
</script>
</body>
</html>
{
"_": "Default card-flyby program. `theme` picks a cohesive palette; `cards` is the deck (each card = a big centered title; `color` is OPTIONAL — omit it and the card takes the next color from the theme ramp); `landings` is one onset per card (accelerating cadence; omit to auto-derive). Pass these as the matching composition-variables; empty vars fall back to these values.",
"theme": "aurora",
"cards": [
{ "title": "FADEGLOW" },
{ "title": "SAUL BASS" },
{ "title": "EULER" },
{ "title": "HERMÈS" },
{ "title": "DOSSIER" },
{ "title": "BLACK HOLES" }
],
"landings": [0.4, 1.7, 2.7, 3.45, 4.0, 4.4]
}
[
{
"op": "staggerInOut",
"theme": 0,
"enter": [0.23, 0.37, 0.46, 0.6, 0.7, 0.84, 0.91, 1.07, 1.14, 1.28],
"hold": 1.324,
"exit": [1.37, 1.51, 1.62, 1.74, 1.83, 1.97, 2.07]
},
{
"op": "holdRecolor",
"on": 2.35,
"onTheme": 1,
"recolor": [2.42, 2.53, 2.81, 2.93],
"themes": [2, 3, 1, 2],
"off": 2.98
},
{
"op": "snakeFillOverlay",
"baseTheme": 0,
"fill": [3.04, 3.181, 3.44, 3.65, 3.79, 3.9],
"swap": 4.0,
"swapTheme": 4,
"overlay": [4.11, 4.34, 4.48, 4.81, 4.97, 5.25, 5.48]
}
]
Related skills
How it compares
Choose music-to-video for audio-driven beat-sync creation; use talking-head-recut when packaging an existing interview with graphic overlays.
FAQ
Does music-to-video require images or video assets?
music-to-video can render a complete beat-synced video using only typography and built-in HyperFrames templates. Optional images or videos supplied by the user are cut into frames on the same beat grid with beat-cut or Ken Burns motion.
How does music-to-video choose video style?
music-to-video never branches on genre labels. One analyzer reads the track, the orchestrator plans frames, and per-frame choices produce lyric videos, slideshows, or kinetic promos organically from the beat layout.
Is Music To Video safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.