feat(skills): add WebGL shader transitions and restructure catalog (#213)

* feat(skills): add WebGL shader transitions and restructure catalog

Add 14 WebGL fragment shader transitions to the transitions skill:
domain warp dissolve, ridged burn, whip pan, SDF iris, ripple waves,
gravitational lens, cinematic zoom, chromatic radial split, glitch,
swirl vortex, thermal distortion, flash through white, cross-warp
morph, and light leak (shader).

Restructure catalog.md from a 1045-line monolith into a 105-line
routing layer with 15 reference files. SKILL.md loads at 101 lines,
catalog.md loads at 105 lines — reference files loaded on demand
only for the transition type being implemented.

Key additions:
- Full WebGL setup boilerplate with media capture (images, video,
  object-fit: cover, live video re-upload during transitions)
- Hard rules for shader transitions capturing all bugs found during
  development (Y-flip, preserveDrawingBuffer, fwidth, boomerang,
  tween proxy reuse, tl.call vs onComplete)
- CSS vs Shader decision guide in SKILL.md
- Visual pattern warning against repeating geometric patterns
- Shader transitions slotted into mood/energy mapping tables
- Noise libraries: quintic C2, ridged, erosion FBM, cosine palette

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(skills): fold transitions into hyperframes skill

Move transitions from a standalone skill (4th top-level) into
hyperframes/references/transitions/, aligning with the consolidation
in #211 that reduced 15 skills to 3. Fewer standalone skills means
higher trigger reliability for multi-skill tasks.

Also removes stale text-burn-dom.html reference from css-destruction.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-04-06 14:50:13 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 0a0d5d3654
commit 569513145b
20 changed files with 1565 additions and 745 deletions
+6 -1
View File
@@ -1,6 +1,6 @@
---
name: hyperframes
description: Create video compositions, animations, title cards, overlays, captions, voiceovers, and audio-reactive visuals in HyperFrames HTML. Use when asked to build any HTML-based video content, add captions or subtitles synced to audio, generate text-to-speech narration, create audio-reactive animation (beat sync, glow, pulse driven by music), or add animated text highlighting (marker sweeps, hand-drawn circles, burst lines, scribble, sketchout). Covers composition authoring, timing, media, and the full video production workflow. For CLI commands (init, lint, preview, render, transcribe, tts) see the hyperframes-cli skill.
description: Create video compositions, animations, title cards, overlays, captions, voiceovers, audio-reactive visuals, and scene transitions in HyperFrames HTML. Use when asked to build any HTML-based video content, add captions or subtitles synced to audio, generate text-to-speech narration, create audio-reactive animation (beat sync, glow, pulse driven by music), add animated text highlighting (marker sweeps, hand-drawn circles, burst lines, scribble, sketchout), or add transitions between scenes (crossfades, wipes, reveals, shader transitions). Covers composition authoring, timing, media, and the full video production workflow. For CLI commands (init, lint, preview, render, transcribe, tts) see the hyperframes-cli skill.
---
# HyperFrames
@@ -147,4 +147,9 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
- **[references/transcript-guide.md](references/transcript-guide.md)** — Transcription commands, whisper models, external APIs, troubleshooting.
- **[references/dynamic-techniques.md](references/dynamic-techniques.md)** — Dynamic caption animation techniques (karaoke, clip-path, slam, scatter, elastic, 3D).
- **[references/transitions.md](references/transitions.md)** — Scene transitions: crossfades, wipes, reveals, shader transitions. Energy/mood selection, narrative position, CSS vs WebGL guidance. Read when a composition has multiple scenes that need visual handoffs.
- [transitions/catalog.md](references/transitions/catalog.md) — Hard rules, scene template, and routing to per-type implementation code.
- [transitions/shader-setup.md](references/transitions/shader-setup.md) — WebGL boilerplate for shader transitions.
- [transitions/shader-transitions.md](references/transitions/shader-transitions.md) — 14 fragment shaders.
GSAP patterns and effects are in the `/gsap` skill.
@@ -0,0 +1,96 @@
# Scene Transitions
A transition tells the viewer how two scenes relate. A crossfade says "this continues." A push slide says "next point." A flash cut says "wake up." A blur crossfade says "drift with me." Choose transitions that match what the content is doing emotionally, not just technically.
## Energy → Primary Transition
| Energy | Primary | Accent for key moments | Duration | Easing |
| ---------------------------------------- | ---------------------------- | ------------------------------ | --------- | ---------------------- |
| **Calm** (wellness, brand story, luxury) | Blur crossfade, focus pull | Light leak, circle iris | 0.5-0.8s | `sine.inOut`, `power1` |
| **Medium** (corporate, SaaS, explainer) | Push slide, staggered blocks | Squeeze, vertical push | 0.3-0.5s | `power2`, `power3` |
| **High** (promos, sports, music, launch) | Flash cut, zoom through | Staggered blocks, gravity drop | 0.15-0.3s | `power4`, `expo` |
Pick ONE primary (60-70% of scene changes) + 1-2 accents. Never use a different transition for every scene.
## Mood → Transition Type
Think about what the transition _communicates_, not just what it looks like.
| Mood | Transitions | Why it works |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Warm / inviting** | Light leak, blur crossfade, focus pull, film burn, light leak (shader), thermal distortion | Soft edges, warm color washes. Nothing sharp or mechanical. The transition feels like sunlight. |
| **Cold / clinical** | Squeeze, zoom out, blinds, shutter, grid dissolve, gravitational lens | Content transforms mechanically — compressed, shrunk, sliced, gridded. Zoom out creates clinical distance. No softness. |
| **Editorial / magazine** | Push slide, vertical push, diagonal split, shutter, whip pan | Like turning a page or slicing a layout. Clean directional movement. Whip pan is a fast editorial camera move. |
| **Tech / futuristic** | Grid dissolve, staggered blocks, blinds, chromatic aberration, glitch (shader), chromatic split (shader) | Grid dissolve is the core "data" transition. Shader glitch adds posterization + scan lines. |
| **Tense / edgy** | Glitch, VHS, chromatic aberration, flash cut, ripple, ridged burn, glitch (shader) | Instability, distortion, digital breakdown. Ridged burn adds sharp lightning-crack edges. |
| **Playful / fun** | Elastic push, 3D flip, circle iris, morph circle, clock wipe, ripple waves, swirl vortex | Overshoot, bounce, rotation, expansion. Swirl vortex adds organic spiral distortion. |
| **Dramatic / cinematic** | Zoom through, zoom out, gravity drop, overexposure, diagonal split, color dip to black, cinematic zoom (shader), gravitational lens, domain warp, flash through white | Scale, weight, light extremes. Shader transitions add per-pixel depth. |
| **Premium / luxury** | Focus pull, blur crossfade, color dip to black, slow crossfade, cross-warp morph, thermal distortion | Restraint. Cross-warp morph flows both scenes into each other organically. |
| **Retro / analog** | Film burn, light leak, VHS, clock wipe | Organic imperfection. Warm color bleeds, scan line displacement. Clock wipe evokes broadcast TV. |
## Narrative Position
| Position | Use | Why |
| -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |
| **Opening** | Your most distinctive transition. Match the mood. 0.4-0.6s | Sets the visual language for the entire piece. |
| **Between related points** | Your primary transition. Consistent. 0.3s | Don't distract — the content is continuing. |
| **Topic change** | Something different from your primary. Staggered blocks, shutter, squeeze. | Signals "new section" — the viewer's brain resets. |
| **Climax / hero reveal** | Your boldest accent. Fastest or most dramatic. | This is the payoff — spend your best transition here. |
| **Wind-down** | Return to gentle. Blur crossfade, crossfade. 0.5-0.7s | Let the viewer exhale after the climax. |
| **Outro** | Slowest, simplest. Crossfade, color dip to black. 0.6-1.0s | Closure. Don't introduce new energy at the end. |
## Blur Intensity by Energy
| Energy | Blur | Duration | Hold at peak |
| ---------- | ------- | -------- | ------------ |
| **Calm** | 20-30px | 0.8-1.2s | 0.3-0.5s |
| **Medium** | 8-15px | 0.4-0.6s | 0.1-0.2s |
| **High** | 3-6px | 0.2-0.3s | 0s |
## Presets
| Preset | Duration | Easing |
| ---------- | -------- | ----------------- |
| `snappy` | 0.2s | `power4.inOut` |
| `smooth` | 0.4s | `power2.inOut` |
| `gentle` | 0.6s | `sine.inOut` |
| `dramatic` | 0.5s | `power3.in` → out |
| `instant` | 0.15s | `expo.inOut` |
| `luxe` | 0.7s | `power1.inOut` |
## Implementation
Read [transitions/catalog.md](transitions/catalog.md) for GSAP code and hard rules for every transition type.
| Category | Transitions |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Content-transforming | Push slide, vertical push, elastic push, squeeze, zoom through, zoom out, gravity drop, 3D flip |
| Reveal/mask | Circle iris, diamond iris, diagonal split, clock wipe, shutter |
| Dissolve | Crossfade, blur crossfade, focus pull, color dip |
| Cover | Staggered blocks, horizontal blinds, vertical blinds |
| Light | Light leak, overexposure burn, film burn |
| Distortion | Glitch, chromatic aberration, ripple, VHS tape |
| Pattern | Grid dissolve |
| Instant | Flash cut, morph circle |
| Shader (WebGL) | Domain warp, ridged burn, whip pan, SDF iris, ripple waves, gravitational lens, cinematic zoom, chromatic split, glitch, swirl vortex, thermal distortion, flash through white, cross-warp morph, light leak (shader) |
## Transitions That Don't Work in CSS
Avoid: star iris, tilt-shift, lens flare, hinge/door. See catalog.md for why.
## CSS vs Shader: When to Use Which
Most compositions should use **CSS/GSAP transitions** (the other categories above). They're simpler, lighter, and handle most needs. Use **shader transitions** only when you need an effect that CSS can't achieve:
| Use CSS/GSAP when | Use Shader when |
| ---------------------------------------------- | ----------------------------------------------- |
| Opacity, transform, clip-path, filter effects | Per-pixel noise dissolves, domain warping |
| Simple crossfades, wipes, slides | Both scenes actively morph into each other |
| No images/video in scenes (text + shapes only) | Live video needs to play through the transition |
| Quick to set up, no boilerplate | Willing to add WebGL setup layer (~200 lines) |
Shader transitions require setup boilerplate (canvas, scene capture, WebGL init). Read [transitions/shader-setup.md](transitions/shader-setup.md) for the complete code. The fragment shaders themselves are in the Shader section of [transitions/catalog.md](transitions/catalog.md).
## Visual Pattern Warning
Avoid transitions that create visible repeating geometric patterns — grids of tiles, hexagonal cells, uniform dot arrays, evenly-spaced blob circles. These look cheap and artificial regardless of the math behind them. Organic noise (FBM, domain warping) is good because it's irregular. Geometric repetition is bad because the eye instantly sees the grid.
@@ -0,0 +1,132 @@
# Transition Catalog
Hard rules, scene template, and routing to implementation code. Read the reference file for the transition type you need — don't load all of them.
## Hard Rules (CSS)
These cause real bugs if violated.
**Scene visibility:** Scene 1 visible by default (no `opacity: 0`). Scenes 2+ have `opacity: 0` on the CONTAINER div. GSAP reveals them. No visibility shim (`timedEls`).
**Iframe compatibility:** No external font links (`<link>` to Google Fonts, `@import`). They block sandboxed iframes. Use system fonts.
**Element structure:** No `class="clip"` on scene divs in standalone compositions. Only the root div gets `data-composition-id`/`data-start`/`data-duration`.
**Overlay elements:** Staggered blocks = full-screen 1920x1080, NOT thin strips. Glitch RGB overlays = normal blending at 35% opacity, NOT `mix-blend-mode: multiply` (invisible on dark backgrounds). Light leak overlays = larger than the frame (2400px+), never a visible shape. Overexposure = use `filter: brightness()` on the scene, not just a white overlay.
**VHS tape:** Clone actual scene content with `cloneNode(true)`, NOT colored bars. Each strip: wider than frame (2020px at left:-50px). Red+blue chromatic copies at z-index above main strip. Seeded PRNG for deterministic random offsets.
**Z-index:** Gravity drop, zoom out, diagonal split need outgoing scene ON TOP (`zIndex: 10`) so it exits while revealing the new scene behind (`zIndex: 1`).
**Page burn:** Content burns with the page — no falling debris. Hide scene1 via `tl.set` at burn end, NEVER `onComplete` (not reversible). `onUpdate` must restore `clipPath: "none"` when `wp <= 0` for rewind support. Incoming scene fades from black at 90% through burn.
**Clock wipe:** 9-point polygon with intermediate edge positions. Step through 4 quadrants with separate tweens.
**Grid dissolve:** Cycle 5 palette colors per cell, not monochrome.
**Blinds count by energy:** Calm: 4h/6v. Medium: 6-8h/8v. High: 12-16h/16v.
**Don't use:** Star iris (polygon interpolation broken), tilt-shift (no selective CSS blur), lens flare (visible shape, not optical), hinge/door (distorts too fast).
## Hard Rules (Shader)
Read [shader-setup.md](./shader-setup.md) for the full setup code these rules apply to.
**WebGL setup:** `gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)` — NOT true. Vertex shader flips Y: `v_uv.y = 1.0 - v_uv.y`. `preserveDrawingBuffer: true` required for HyperFrames capture. No `fwidth()` without extension — use constant `0.003`.
**Rendering model:** GL canvas stays visible entire composition — DOM scenes remain `opacity: 0`. Passthrough shader for holds. Scene capture = static snapshots at load; animated GSAP tweens on elements won't appear.
**Scene capture:** Canvas `fillText` doesn't match CSS fonts exactly (known, not a bug). No CSS gradients or SVGs. Images and videos ARE supported via `ctx.drawImage()`. Video scenes re-capture every frame during transitions via `recaptureVideoScene()`.
**Timeline:** Use `tl.call()` for begin/end — NOT `onStart`/`onComplete`. Each tween proxy `{p:0}` must be unique. Never boomerang (`u_progress*(1.-u_progress)*4.`). Morph both scenes.
**Shader code:** One noise library per shader (NQ or ND, not both). Always `clamp(uv, 0., 1.)`.
## Scene Template
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
.scene {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#scene1 {
z-index: 1;
background: #color;
}
#scene2 {
z-index: 2;
background: #color;
opacity: 0;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="TOTAL"
>
<div id="scene1" class="scene"><!-- visible --></div>
<div id="scene2" class="scene"><!-- hidden --></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Transition code here
window.__timelines["main"] = tl;
</script>
</body>
</html>
```
Every transition follows: position new scene → animate outgoing → swap → animate incoming → clean up overlays.
## CSS Transitions
All code examples use `old` for the outgoing scene-inner selector and `new` for the incoming, with `T` as the transition start time. Read the reference file for the type you need.
| Type | Transitions | Reference |
| -------------- | ---------------------------------------------------- | ------------------------------------------ |
| Push | Push slide, vertical push, elastic push, squeeze | [css-push.md](./css-push.md) |
| Radial / Shape | Circle iris, diamond iris, diagonal split | [css-radial.md](./css-radial.md) |
| 3D | 3D card flip | [css-3d.md](./css-3d.md) |
| Scale / Zoom | Zoom through, zoom out | [css-scale.md](./css-scale.md) |
| Dissolve | Crossfade, blur crossfade, focus pull, color dip | [css-dissolve.md](./css-dissolve.md) |
| Cover | Staggered blocks, horizontal blinds, vertical blinds | [css-cover.md](./css-cover.md) |
| Light | Light leak, overexposure burn, film burn | [css-light.md](./css-light.md) |
| Distortion | Glitch, chromatic aberration, ripple, VHS tape | [css-distortion.md](./css-distortion.md) |
| Mechanical | Shutter, clock wipe | [css-mechanical.md](./css-mechanical.md) |
| Grid | Grid dissolve | [css-grid.md](./css-grid.md) |
| Other | Flash cut, gravity drop, morph circle | [css-other.md](./css-other.md) |
| Blur | Blur through, directional blur | [css-blur.md](./css-blur.md) |
| Destruction | Page burn | [css-destruction.md](./css-destruction.md) |
## Shader Transitions
WebGL fragment shaders that composite between scene textures per-pixel. Require setup boilerplate.
| What | Reference |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| Setup (canvas, capture, WebGL init, render loop, GSAP integration) | [shader-setup.md](./shader-setup.md) |
| Fragment shaders (14 transitions: domain warp, ridged burn, whip pan, SDF iris, ripple waves, gravitational lens, cinematic zoom, chromatic split, glitch, swirl vortex, thermal distortion, flash through white, cross-warp morph, light leak) | [shader-transitions.md](./shader-transitions.md) |
@@ -0,0 +1,12 @@
## 3D
### 3D Card Flip
180° Y-axis rotation. Requires CSS: `backface-visibility: hidden; transform-style: preserve-3d;` on both scene-inners. Parent needs `perspective: 1200px`.
```js
tl.set(new, { rotationY: -180, opacity: 1 }, T);
tl.to(old, { rotationY: 180, duration: 0.6, ease: "power2.inOut" }, T);
tl.to(new, { rotationY: 0, duration: 0.6, ease: "power2.inOut" }, T);
tl.set(old, { opacity: 0 }, T + 0.6);
```
@@ -0,0 +1,51 @@
## Blur
All blur transitions scale with energy. See SKILL.md "Blur Intensity by Energy" for the full table.
### Blur Through
Content becomes fully abstract before resolving. The heaviest blur transition.
**Calm (default for this type — it's inherently heavy):**
```js
tl.to(old, { filter: "blur(30px)", scale: 1.08, duration: 0.5, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.3, ease: "power1.in" }, T + 0.3);
// Hold: both scenes in abstract blur state
tl.fromTo(new,
{ filter: "blur(30px)", scale: 0.92, opacity: 0 },
{ filter: "blur(30px)", scale: 0.92, opacity: 1, duration: 0.2, ease: "none" }, T + 0.5);
// Slow resolve
tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.7, ease: "power1.out" }, T + 0.7);
```
**Medium:**
```js
tl.to(old, { filter: "blur(15px)", scale: 1.05, opacity: 0, duration: 0.4, ease: "power2.in" }, T);
tl.fromTo(new,
{ filter: "blur(15px)", scale: 0.95, opacity: 0 },
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.4, ease: "power2.out" }, T + 0.2);
```
### Directional Blur
Blur + skew simulating motion in one direction. Scale blur and skew with energy.
**Medium (default):**
```js
tl.to(old, { filter: "blur(12px)", skewX: -8, x: -200, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
tl.fromTo(new,
{ filter: "blur(12px)", skewX: 8, x: 200, opacity: 0 },
{ filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.4, ease: "power3.out" }, T + 0.15);
```
**Calm (heavier blur, gentler motion):**
```js
tl.to(old, { filter: "blur(20px)", skewX: -4, x: -100, opacity: 0, duration: 0.6, ease: "power1.in" }, T);
tl.fromTo(new,
{ filter: "blur(20px)", skewX: 4, x: 100, opacity: 0 },
{ filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.6, ease: "power1.out" }, T + 0.3);
```
@@ -0,0 +1,43 @@
## Cover
### Staggered Color Blocks
Full-screen (1920x1080) colored divs slide across staggered. Scene swaps while covered.
**2-block** (standard):
```js
tl.set("#wipe-a", { x: -1920 }, T - 0.01);
tl.set("#wipe-b", { x: -1920 }, T - 0.01);
tl.to("#wipe-a", { x: 0, duration: 0.25, ease: "power3.inOut" }, T);
tl.to("#wipe-b", { x: 0, duration: 0.25, ease: "power3.inOut" }, T + 0.06);
tl.set(old, { opacity: 0 }, T + 0.2);
tl.set(new, { opacity: 1 }, T + 0.2);
tl.to("#wipe-a", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.28);
tl.to("#wipe-b", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.34);
```
**5-block** (dense variant): same pattern with 5 blocks at 0.04s stagger. Use composition palette colors.
### Horizontal Blinds
Full-width strips slide across staggered. Each strip: `width: 1920px; height: Xpx`.
**6 strips** (180px each): `0.03s` stagger
**12 strips** (90px each): `0.018s` stagger
```js
for (var i = 0; i < N; i++) {
tl.set("#blind-h-" + i, { x: -1920 }, T - 0.01);
tl.fromTo("#blind-h-" + i, { x: -1920 }, { x: 0, duration: 0.2, ease: "power3.inOut" }, T + i * stagger);
}
tl.set(old, { opacity: 0 }, T + coverTime);
tl.set(new, { opacity: 1 }, T + coverTime);
for (var i = 0; i < N; i++) {
tl.to("#blind-h-" + i, { x: 1920, duration: 0.2, ease: "power3.inOut" }, T + exitStart + i * stagger);
}
```
### Vertical Blinds
Same as horizontal but strips are tall and narrow, moving on Y axis.
@@ -0,0 +1,95 @@
## Destruction
### Page Burn
The outgoing scene literally burns away from a corner. A fire front expands with noise-based irregular edges, a canvas draws the scorched char line at the burn boundary, and individual text characters/elements chip off and fall with gravity as the fire reaches them. The incoming scene reveals behind the burn.
This transition has three systems working together:
1. **Fire geometry** — a radial front expanding from a corner (e.g., bottom-right) with noise-based irregularity for organic edges
2. **Scene clipping** — the outgoing scene uses an SVG clip-path (with `fill-rule: evenodd`) that cuts a hole matching the fire front. As the fire expands, more of the scene is clipped away. All content (text, images, lines) burns with the page — no separate debris.
3. **Scorched edge** — a `<canvas>` overlay draws a radial gradient fringe at the fire boundary to simulate charring
**When to use:** Dramatic reveals, edgy/destructive mood, gaming, cyberpunk. This is the most dramatic transition in the catalog — reserve it for hero moments.
**Requirements:**
- A `<canvas>` element for the burn edge overlay
- A noise function for organic fire edge geometry
- SVG clip-path with evenodd fill-rule for the inverted clip
**Fire geometry (deterministic noise):**
```js
function noise(x) {
var ix = Math.floor(x),
fx = x - ix;
var a = Math.sin(ix * 127.1 + 311.7) * 43758.5453;
var b = Math.sin((ix + 1) * 127.1 + 311.7) * 43758.5453;
var t = fx * fx * (3 - 2 * fx);
return a - Math.floor(a) + (b - Math.floor(b) - (a - Math.floor(a))) * t;
}
function fireRadiusAtAngle(angle, progress) {
var base = progress * maxRadius;
return (
base +
noise(angle * 3 + progress * 4) * 50 +
noise(angle * 8 + progress * 9) * 20 +
noise(angle * 15 + progress * 15) * 8
);
}
```
**Incoming scene timing:** The incoming scene should NOT be visible during the burn. As the fire consumes the outgoing scene, **black shows through the holes** — this is the dramatic part. The viewer watches content being destroyed against blackness.
At ~90% through the burn, the incoming scene fades in SLOWLY from black — the background first, then content staggered. Use long, gentle fades (`power1.out`, 0.8-1.2s durations) so it feels like the new scene materializes from darkness, not a hard swap.
```js
// Scene 2 stays at opacity: 0 during the burn — black behind the fire
tl.set("#s2-title", { opacity: 0 }, T);
tl.set("#s2-subtitle", { opacity: 0 }, T);
// At 90% through, scene bg fades in slowly from black
var contentReveal = T + BURN_DURATION * 0.9;
tl.to("#scene2", { opacity: 1, duration: 1.2, ease: "power1.out" }, contentReveal);
// Content fades in staggered on top, even slower
tl.to("#s2-title", { opacity: 1, duration: 1.0, ease: "power1.out" }, contentReveal + 0.5);
tl.to("#s2-subtitle", { opacity: 1, duration: 0.8, ease: "power1.out" }, contentReveal + 0.7);
```
**Content burns with the page — no falling debris.** The clip-path on scene1 IS the effect — as the fire shape expands, everything behind the fire edge (text, images, lines) disappears naturally. Don't clone elements, don't create falling debris. The content is part of the page being consumed. The scorched canvas edge provides the visual char line at the burn boundary.
**Hide scene1 via `tl.set` at burn end — NEVER in `onComplete`.** Using `onComplete` to hide scene1 is not reversible when scrubbing. Instead, use a `tl.set` at the exact burn end time:
```js
tl.to(
burnState,
{
progress: 1,
duration: BURN_DURATION,
ease: "none",
onUpdate: function () {
var wp = burnState.progress;
var scene1 = document.getElementById("scene1");
if (wp <= 0) {
scene1.style.clipPath = "none"; // fully visible when rewound
} else if (wp < 1) {
scene1.style.clipPath = buildClipPath(wp);
}
drawEdge(wp);
},
// NO onComplete — use tl.set instead
},
T,
);
// Hide scene1 at exact burn end — reversible via timeline
tl.set("#scene1", { opacity: 0 }, T + BURN_DURATION);
tl.set("#scene1", { clipPath: "none" }, T + BURN_DURATION);
```
The `onUpdate` handles clip-path and canvas edge per-frame. The `tl.set` handles the final hide — and GSAP automatically reverses it when scrubbing backward, restoring scene1 to `opacity: 1`.
The `onUpdate` callback is the key — it runs every frame to advance the clip-path and canvas edge in sync with the timeline.
@@ -0,0 +1,66 @@
## Dissolve
### Crossfade
Simple opacity swap. The baseline.
```js
tl.to(old, { opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, T);
```
### Blur Crossfade
Dissolve with blur + scale shift. **Scale blur amount by energy** — see SKILL.md "Blur Intensity by Energy" section. The examples below show the medium (default) version. For calm compositions, increase to 20-30px with a 0.3-0.5s hold at peak blur. For high-energy, decrease to 3-6px with no hold.
**Medium (default):**
```js
tl.to(old, { filter: "blur(10px)", scale: 1.03, opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
tl.fromTo(new,
{ filter: "blur(10px)", scale: 0.97, opacity: 0 },
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.5, ease: "power2.inOut" }, T + 0.1);
```
**Calm (wellness, luxury) — heavy blur, holds at abstract color:**
```js
tl.to(old, { filter: "blur(25px)", scale: 1.05, duration: 0.6, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.4, ease: "power1.in" }, T + 0.4);
tl.fromTo(new,
{ filter: "blur(25px)", scale: 0.95, opacity: 0 },
{ filter: "blur(25px)", scale: 0.95, opacity: 1, duration: 0.3, ease: "power1.inOut" }, T + 0.5);
tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.6, ease: "power1.out" }, T + 0.8);
```
### Focus Pull
Outgoing slowly blurs while incoming fades in sharp. Depth-of-field feel. **Scale blur amount and hold duration by energy.**
**Medium:**
```js
tl.to(old, { filter: "blur(15px)", duration: 0.5, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.3, ease: "power2.in" }, T + 0.25);
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: "power2.out" }, T + 0.25);
```
**Calm — slow rack focus with long hold at peak defocus:**
```js
tl.to(old, { filter: "blur(30px)", duration: 0.8, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.5, ease: "power1.in" }, T + 0.6);
tl.fromTo(new, { opacity: 0, filter: "blur(20px)" },
{ opacity: 1, filter: "blur(20px)", duration: 0.3, ease: "power1.inOut" }, T + 0.7);
tl.to(new, { filter: "blur(0px)", duration: 0.6, ease: "power1.out" }, T + 1.0);
```
### Color Dip
Fade to solid color, hold, fade up new scene.
```js
tl.to(old, { opacity: 0, duration: 0.2, ease: "power2.in" }, T);
// Background color shows through
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.2, ease: "power2.out" }, T + 0.25);
```
@@ -0,0 +1,45 @@
## Distortion
### Glitch
RGB-tinted overlays (NOT multiply blend — use normal blending at 35% opacity) jitter with large offsets. Scene itself also jitters.
```js
tl.set("#glitch-r", { opacity: 1, x: 40, y: -8 }, T);
tl.set("#glitch-g", { opacity: 1, x: -30, y: 12 }, T);
tl.set("#glitch-b", { opacity: 1, x: 15, y: -20 }, T);
tl.set(old, { x: -15 }, T);
// 6 jitter frames at 0.03s intervals with big offsets (±30-60px)
// ... swap and clear at T + 0.2
```
### Chromatic Aberration
RGB overlays start aligned then spread apart (±80px), scene fades, converge on new scene.
```js
tl.set("#glitch-r", { opacity: 0.6, x: 0 }, T);
tl.set("#glitch-g", { opacity: 0.6, x: 0 }, T);
tl.set("#glitch-b", { opacity: 0.6, x: 0 }, T);
tl.to("#glitch-r", { x: -80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
tl.to("#glitch-b", { x: 80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
tl.to("#glitch-g", { y: 30, duration: 0.3, ease: "power2.in" }, T);
// Swap at T + 0.3, converge back at T + 0.3
```
### Ripple
Rapid oscillation (±30px) + scale distortion (0.97-1.03) + increasing blur. Swap at peak distortion.
```js
tl.to(old, { x: 30, scale: 1.02, duration: 0.04, ease: "none" }, T);
tl.to(old, { x: -25, scale: 0.98, filter: "blur(4px)", duration: 0.04, ease: "none" }, T + 0.04);
// ... more oscillations with increasing blur
// Swap at peak, incoming stabilizes with decreasing wobble
```
### VHS Tape
Clone scene into 20 horizontal strips (each 54px, clip-path'd). Each strip shifts x independently with seeded pseudo-random offsets at per-bar random intervals. Add red+blue chromatic offset copies on each strip (z-index above main, 35% opacity). Make strips wider than frame (2020px at left:-50px) so edges never show.
See SKILL.md for clone-based implementation pattern.
@@ -0,0 +1,10 @@
## Grid
### Grid Dissolve
Grid of colored cells covers the frame in a ripple from center. Scene swaps at 50% coverage. Cells fade out in ripple.
**12-cell** (4x3, each 480x270): standard
**120-cell** (12x10, each 160x108): dense variant — lower opacity (0.75), tighter ripple
Cells are created dynamically in JS, sorted by distance from center for ripple stagger.
@@ -0,0 +1,49 @@
## Light
### Light Leak
Multiple warm-colored overlays wash across frame. Needs: a flat warm tint layer + 2-3 bright radial gradient divs, all larger than the frame so edges are never visible.
```js
// Warm tint washes over entire frame
tl.to("#leak-warm", { opacity: 0.4, duration: 0.3, ease: "power1.in" }, T);
// Bright leak elements drift in
tl.to("#leak-1", { opacity: 0.9, x: 300, duration: 0.5, ease: "sine.inOut" }, T + 0.05);
tl.to("#leak-2", { opacity: 0.8, x: 200, duration: 0.6, ease: "sine.inOut" }, T + 0.1);
// Peak warmth then swap
tl.to("#leak-warm", { opacity: 0.6, duration: 0.15, ease: "power2.in" }, T + 0.35);
tl.set(old, { opacity: 0 }, T + 0.45);
tl.set(new, { opacity: 1 }, T + 0.45);
// Leak fades
tl.to("#leak-warm", { opacity: 0, duration: 0.4, ease: "power2.out" }, T + 0.5);
tl.to("#leak-1", { opacity: 0, x: 600, duration: 0.35, ease: "power1.out" }, T + 0.5);
```
### Overexposure Burn
Scene progressively blows out to white using CSS `filter: brightness()`, then white overlay fades in. Swap at peak white. White recedes to reveal new scene.
```js
tl.to(old, { filter: "brightness(1.5)", scale: 1.03, duration: 0.2, ease: "power1.in" }, T);
tl.to(old, { filter: "brightness(3)", scale: 1.06, duration: 0.2, ease: "power2.in" }, T + 0.2);
tl.to("#flash-overlay", { opacity: 0.5, duration: 0.25, ease: "power1.in" }, T + 0.15);
tl.to("#flash-overlay", { opacity: 1, duration: 0.15, ease: "power2.in" }, T + 0.4);
tl.set(old, { opacity: 0, filter: "brightness(1)", scale: 1 }, T + 0.55);
tl.set(new, { opacity: 1 }, T + 0.55);
tl.to("#flash-overlay", { opacity: 0, duration: 0.35, ease: "power2.out" }, T + 0.55);
```
### Film Burn
Staggered warm overlays (amber, orange, red) bleed from one edge. Each overlay is a large radial gradient div at high z-index.
```js
tl.to("#burn-a", { opacity: 1, x: -300, duration: 0.4, ease: "power1.in" }, T);
tl.to("#burn-b", { opacity: 1, x: -500, duration: 0.5, ease: "power1.in" }, T + 0.05);
tl.to("#burn-c", { opacity: 1, x: -200, duration: 0.45, ease: "power1.in" }, T + 0.1);
tl.set(old, { opacity: 0 }, T + 0.35);
tl.set(new, { opacity: 1 }, T + 0.35);
tl.to("#burn-a", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.45);
tl.to("#burn-b", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.5);
tl.to("#burn-c", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.55);
```
@@ -0,0 +1,30 @@
## Mechanical
### Shutter
Two full-screen halves close from top and bottom, meet in the middle. Swap while closed. Open again.
```js
tl.to("#shutter-top", { y: 0, duration: 0.25, ease: "power3.in" }, T);
tl.to("#shutter-bot", { y: 0, duration: 0.25, ease: "power3.in" }, T);
tl.set(old, { opacity: 0 }, T + 0.25);
tl.set(new, { opacity: 1 }, T + 0.25);
tl.to("#shutter-top", { y: -540, duration: 0.25, ease: "power3.out" }, T + 0.3);
tl.to("#shutter-bot", { y: 540, duration: 0.25, ease: "power3.out" }, T + 0.3);
```
### Clock Wipe
Radial polygon sweep stepping through quadrants. Use 9-point polygon with intermediate edge positions for smooth sweep.
```js
tl.set(new, { opacity: 1, zIndex: 10 }, T);
var d = 0.1; // duration per quadrant
tl.set(new, { clipPath: "polygon(50% 50%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%)" }, T);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%)", duration: d, ease: "none" }, T);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%)", duration: d, ease: "none" }, T + d);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 50%)", duration: d, ease: "none" }, T + d*2);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 0%)", duration: d, ease: "none" }, T + d*3);
tl.set(new, { clipPath: "none", zIndex: "auto" }, T + d*4 + 0.02);
tl.set(old, { opacity: 0, zIndex: "auto" }, T + d*4 + 0.02);
```
@@ -0,0 +1,36 @@
## Other
### Flash Cut
White full-screen overlay flashes at swap point. 0.03s on, 0.1s off.
```js
tl.to("#flash-overlay", { opacity: 1, duration: 0.03, ease: "power4.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.03);
tl.set(new, { opacity: 1 }, T + 0.03);
tl.to("#flash-overlay", { opacity: 0, duration: 0.1, ease: "power2.out" }, T + 0.05);
```
### Gravity Drop
Old scene falls down with slight rotation. New scene was behind it. Needs z-index.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10 }, T);
tl.to(old, { y: 1200, rotation: 4, duration: 0.5, ease: "power3.in" }, T);
tl.set(old, { opacity: 0, zIndex: "auto" }, T + 0.5);
tl.set(new, { zIndex: "auto" }, T + 0.5);
```
### Morph Circle
A circle scales up from center to fill frame (becoming the new scene's background color). New scene content fades in on top.
```js
tl.set("#morph-circle", { background: newBgColor, opacity: 1, scale: 0 }, T);
tl.to("#morph-circle", { scale: 30, duration: 0.5, ease: "power3.in" }, T);
tl.set(old, { opacity: 0 }, T + 0.4);
tl.set(new, { opacity: 1 }, T + 0.4);
tl.to("#morph-circle", { opacity: 0, duration: 0.15, ease: "power2.out" }, T + 0.5);
```
@@ -0,0 +1,41 @@
## Linear / Push
### Push Slide
Both scenes move together — new pushes old out.
```js
tl.to(old, { x: -1920, duration: 0.5, ease: "power3.inOut" }, T);
tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 0, duration: 0.5, ease: "power3.inOut" }, T);
```
### Vertical Push
Same as push slide but vertical.
```js
tl.to(old, { y: -1080, duration: 0.5, ease: "power3.inOut" }, T);
tl.fromTo(new, { y: 1080, opacity: 1 }, { y: 0, duration: 0.5, ease: "power3.inOut" }, T);
```
### Elastic Push
Push with overshoot bounce on the incoming scene.
```js
tl.to(old, { x: -1920, duration: 0.5, ease: "power3.in" }, T);
tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 30, duration: 0.4, ease: "power4.out" }, T + 0.1);
tl.to(new, { x: -15, duration: 0.15, ease: "sine.inOut" }, T + 0.5);
tl.to(new, { x: 0, duration: 0.1, ease: "sine.out" }, T + 0.65);
```
### Squeeze
Old compresses, new expands from opposite side.
```js
tl.to(old, { scaleX: 0, transformOrigin: "left center", duration: 0.4, ease: "power3.inOut" }, T);
tl.fromTo(new, { scaleX: 0, transformOrigin: "right center", opacity: 1 },
{ scaleX: 1, duration: 0.4, ease: "power3.inOut" }, T + 0.1);
tl.set(old, { opacity: 0 }, T + 0.5);
```
@@ -0,0 +1,37 @@
## Radial / Shape
### Circle Iris
Expanding circle from center reveals new scene.
```js
tl.set(new, { opacity: 1 }, T);
tl.fromTo(new,
{ clipPath: "circle(0% at 50% 50%)" },
{ clipPath: "circle(75% at 50% 50%)", duration: 0.5, ease: "power2.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.5);
```
### Diamond Iris
Expanding diamond shape from center.
```js
tl.set(new, { opacity: 1 }, T);
tl.fromTo(new,
{ clipPath: "polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)" },
{ clipPath: "polygon(50% -20%, 120% 50%, 50% 120%, -20% 50%)", duration: 0.5, ease: "power2.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.5);
```
### Diagonal Split
Old scene shrinks to a triangle in one corner.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10, clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" }, T);
tl.to(old, { clipPath: "polygon(60% 0%, 100% 0%, 100% 40%, 60% 0%)", duration: 0.5, ease: "power3.inOut" }, T);
tl.set(old, { opacity: 0, zIndex: "auto", clipPath: "none" }, T + 0.5);
tl.set(new, { zIndex: "auto" }, T + 0.5);
```
@@ -0,0 +1,24 @@
## Scale / Zoom
### Zoom Through
Old zooms past camera + blurs, new zooms in from behind.
```js
tl.to(old, { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, T);
tl.fromTo(new,
{ scale: 0.5, opacity: 0, filter: "blur(8px)" },
{ scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, T + 0.15);
```
### Zoom Out
Old shrinks away, new was behind it. Needs z-index management.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10, transformOrigin: "50% 50%" }, T);
tl.to(old, { scale: 0.3, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
tl.set(old, { zIndex: "auto" }, T + 0.4);
tl.set(new, { zIndex: "auto" }, T + 0.4);
```
@@ -0,0 +1,463 @@
# Shader Transition Setup
Complete boilerplate for WebGL shader transitions in HyperFrames. Read this when implementing a shader transition — copy the setup code, then plug in the fragment shader from the catalog.
## HTML
```html
<canvas
id="gl-canvas"
width="1920"
height="1080"
style="position:absolute;top:0;left:0;width:1920px;height:1080px;z-index:100;pointer-events:none;display:none;"
>
</canvas>
```
## WebGL Init + Scene Capture
Handles images, video, shapes, and text. Supports `object-fit: cover` on images and live video re-upload during transitions.
```js
var sceneTextures = {};
var sceneHasVideo = {}; // tracks which scenes have live video
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
gl.viewport(0, 0, 1920, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
// Wait for all media to load before capturing
function waitForMedia() {
return new Promise(function (resolve) {
var promises = [];
document.querySelectorAll("img").forEach(function (img) {
if (!img.complete)
promises.push(
new Promise(function (r) {
img.onload = r;
img.onerror = r;
}),
);
});
document.querySelectorAll("video").forEach(function (vid) {
if (vid.readyState < 2)
promises.push(
new Promise(function (r) {
vid.addEventListener("loadeddata", r, { once: true });
}),
);
});
Promise.all(promises).then(resolve);
});
}
function captureScene(sceneId) {
return new Promise(function (resolve) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
if (scene.querySelector("video")) sceneHasVideo[sceneId] = scene.querySelector("video");
requestAnimationFrame(function () {
requestAnimationFrame(function () {
var c = document.createElement("canvas");
c.width = 1920;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1920, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i];
var cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
// <img> elements (with object-fit: cover support)
if (el.tagName === "IMG" && el.complete && el.naturalWidth > 0) {
try {
if (cs.objectFit === "cover") {
var iR = el.naturalWidth / el.naturalHeight,
bR = w / h;
var sx = 0,
sy = 0,
sw = el.naturalWidth,
sh = el.naturalHeight;
if (iR > bR) {
sw = sh * bR;
sx = (el.naturalWidth - sw) / 2;
} else {
sh = sw / bR;
sy = (el.naturalHeight - sh) / 2;
}
ctx.drawImage(el, sx, sy, sw, sh, x, y, w, h);
} else {
ctx.drawImage(el, x, y, w, h);
}
} catch (e) {}
ctx.restore();
continue;
}
// <video> elements (grabs current frame)
if (el.tagName === "VIDEO" && el.readyState >= 2) {
try {
var vR = el.videoWidth / el.videoHeight,
bR2 = w / h;
var vx = 0,
vy = 0,
vw = el.videoWidth,
vh = el.videoHeight;
if (vR > bR2) {
vw = vh * bR2;
vx = (el.videoWidth - vw) / 2;
} else {
vh = vw / bR2;
vy = (el.videoHeight - vh) / 2;
}
ctx.drawImage(el, vx, vy, vw, vh, x, y, w, h);
} catch (e) {}
ctx.restore();
continue;
}
// Background color
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
var br = parseInt(cs.borderRadius) || 0;
if (br >= Math.min(w, h) / 2 - 1 && Math.abs(w - h) < 4) {
ctx.beginPath();
ctx.arc(x + w / 2, y + h / 2, Math.min(w, h) / 2, 0, Math.PI * 2);
ctx.fill();
} else if (br > 0) {
ctx.beginPath();
ctx.moveTo(x + br, y);
ctx.arcTo(x + w, y, x + w, y + h, br);
ctx.arcTo(x + w, y + h, x, y + h, br);
ctx.arcTo(x, y + h, x, y, br);
ctx.arcTo(x, y, x + w, y, br);
ctx.closePath();
ctx.fill();
} else {
ctx.fillRect(x, y, w, h);
}
}
// Text (leaf nodes only, with text-shadow)
var hasChildEls = el.querySelector("div, span, img, video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
var shadow = cs.textShadow;
if (shadow && shadow !== "none") {
var sp = shadow.match(/rgba?\([^)]+\)\s+(-?\d+)px\s+(-?\d+)px\s+(-?\d+)px/);
if (sp) {
ctx.shadowColor = shadow.match(/rgba?\([^)]+\)/)[0];
ctx.shadowOffsetX = parseFloat(sp[1]);
ctx.shadowOffsetY = parseFloat(sp[2]);
ctx.shadowBlur = parseFloat(sp[3]);
}
}
if (cs.textAlign === "center" || w > 1800) {
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
} else {
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(text, x, y + h / 2);
}
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
resolve();
});
});
});
}
// Re-capture video scenes every frame (call in updateTrans and during holds)
function recaptureVideoScene(sceneId) {
var video = sceneHasVideo[sceneId];
if (!video || video.readyState < 2) return;
var scene = document.getElementById(sceneId);
var c = document.createElement("canvas");
c.width = 1920;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1920, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none") continue;
var r = el.getBoundingClientRect();
if (r.width < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
if (el.tagName === "VIDEO" && el.readyState >= 2) {
try {
var vR = el.videoWidth / el.videoHeight,
bR = w / h;
var sx = 0,
sy = 0,
sw = el.videoWidth,
sh = el.videoHeight;
if (vR > bR) {
sw = sh * bR;
sx = (el.videoWidth - sw) / 2;
} else {
sh = sw / bR;
sy = (el.videoHeight - sh) / 2;
}
ctx.drawImage(el, sx, sy, sw, sh, x, y, w, h);
} catch (e) {}
} else if (el.tagName === "IMG" && el.complete) {
try {
ctx.drawImage(el, x, y, w, h);
} catch (e) {}
} else {
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var txt = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) txt += el.childNodes[j].textContent;
txt = txt.trim();
if (txt && !el.querySelector("div,span,img,video")) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(txt, x, y + h / 2);
}
}
ctx.restore();
}
gl.bindTexture(gl.TEXTURE_2D, sceneTextures[sceneId]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
}
```
## Shader Compilation + Shared Constants
```js
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
// Shared uniform header — every fragment shader starts with this
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
```
## Noise Libraries
Include only what each shader needs. Do NOT include multiple libraries that redefine `hash()` in the same shader.
```js
// Quintic C2 noise + inter-octave rotation FBM
var NQ =
"float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}" +
"float vnoise(vec2 p){vec2 i=floor(p),f=fract(p);" +
"f=f*f*f*(f*(f*6.-15.)+10.);" + // quintic interpolation — C2 continuous
"return mix(mix(hash(i),hash(i+vec2(1,0)),f.x)," +
"mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);}" +
"float fbm(vec2 p){float v=0.,a=.5;" +
"mat2 R=mat2(.8,.6,-.6,.8);" + // inter-octave rotation (~37deg)
"for(int i=0;i<5;i++){v+=a*vnoise(p);p=R*p*2.02;a*=.5;}return v;}";
// Noise with analytical derivatives (quintic) + erosion FBM
// Use for transitions that need gradient-based edge lighting
var ND =
"float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}" +
"vec3 noised(vec2 p){vec2 i=floor(p),f=fract(p);" +
"vec2 u=f*f*f*(f*(f*6.-15.)+10.),du=30.*f*f*(f*(f-2.)+1.);" +
"float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));" +
"return vec3(a+(b-a)*u.x+(c-a)*u.y+(a-b-c+d)*u.x*u.y," +
"du*vec2(b-a+(a-b-c+d)*u.y,c-a+(a-b-c+d)*u.x));}" +
"float erosionFBM(vec2 p){float v=0.,a=.5;vec2 d=vec2(0);mat2 R=mat2(.8,.6,-.6,.8);" +
"for(int i=0;i<6;i++){vec3 n=noised(p);d+=n.yz;v+=a*n.x/(1.+dot(d,d));p=R*p*2.02;a*=.5;}return v;}";
// Cosine palette: a + b*cos(2pi(c*t + d))
var CP = "vec3 palette(float t,vec3 a,vec3 b,vec3 c,vec3 d){" + "return a+b*cos(6.2832*(c*t+d));}";
```
## Render + State Machine
```js
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1920, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var trans = {
active: false,
prog: null,
fromId: null,
toId: null,
progress: 0,
};
function beginTrans(prog, fromId, toId) {
trans.prog = prog;
trans.fromId = fromId;
trans.toId = toId;
trans.progress = 0;
trans.active = true;
}
function updateTrans() {
if (!trans.active) return;
// Re-capture video scenes every frame during transition
if (sceneHasVideo[trans.fromId]) recaptureVideoScene(trans.fromId);
if (sceneHasVideo[trans.toId]) recaptureVideoScene(trans.toId);
renderShader(trans.prog, sceneTextures[trans.fromId], sceneTextures[trans.toId], trans.progress);
}
function endTrans(showId) {
trans.active = false;
renderShader(progPass, sceneTextures[showId], sceneTextures[showId], 0);
}
```
## GSAP Timeline Integration
```js
// Wait for media, start videos, capture all scenes, then build timeline
var sceneIds = ["scene1", "scene2" /* ... */];
waitForMedia()
.then(function () {
// Start any background videos (muted)
document.querySelectorAll("video").forEach(function (v) {
v.play();
});
return Promise.all(sceneIds.map(captureScene));
})
.then(function () {
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["scene1"], sceneTextures["scene1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
updateTrans();
},
});
// For each transition:
tl.call(
function () {
beginTrans(myShaderProg, "scene1", "scene2");
},
null,
T,
);
var tw = { p: 0 };
tl.to(
tw,
{
p: 1,
duration: DUR,
ease: "power2.inOut",
onUpdate: function () {
trans.progress = tw.p;
},
},
T,
);
tl.call(
function () {
endTrans("scene2");
},
null,
T + DUR,
);
window.__timelines["main"] = tl;
});
```
@@ -0,0 +1,329 @@
# Shader Transition Fragment Shaders
Each shader below is just the GLSL fragment body. Plug it into `mkProg(H + NQ + "...")` using the setup from [shader-setup.md](./shader-setup.md).
### Domain Warp Dissolve
Cascaded `fbm(p + fbm(p))` — both scenes displace along the warp field in opposite directions. Iridescent cosine palette edge glow.
```glsl
// Requires: NQ + CP
void main() {
vec2 q = vec2(fbm(v_uv * 3.), fbm(v_uv * 3. + vec2(5.2, 1.3)));
vec2 r = vec2(fbm(v_uv * 3. + q * 4. + vec2(1.7, 9.2)),
fbm(v_uv * 3. + q * 4. + vec2(8.3, 2.8)));
float n = fbm(v_uv * 3. + r * 2.);
vec2 warpDir = (q - .5) * .4;
vec4 A = texture2D(u_from, clamp(v_uv + warpDir * u_progress, 0., 1.));
vec4 B = texture2D(u_to, clamp(v_uv - warpDir * (1. - u_progress), 0., 1.));
float e = smoothstep(u_progress - .08, u_progress + .08, n);
float ed = abs(n - u_progress);
float em = smoothstep(.1, 0., ed) * (1. - step(1., u_progress));
vec3 ec = palette(ed * 8., vec3(.5), vec3(.5), vec3(1.), vec3(0., .33, .67));
gl_FragColor = vec4(mix(B, A, e).rgb + ec * em * 2., 1.);
}
```
### Ridged Burn
`abs(noise)` creates sharp lightning-crack edges. Blackbody color gradient (dark red → orange → yellow → white) + high-frequency ember sparks near the edge.
```glsl
// Requires: NQ
float ridged(vec2 p) {
float v = 0., a = .5;
mat2 R = mat2(.8, .6, -.6, .8);
for (int i = 0; i < 5; i++) {
v += a * abs(vnoise(p) * 2. - 1.);
p = R * p * 2.02; a *= .5;
}
return v;
}
void main() {
vec4 A = texture2D(u_from, v_uv), B = texture2D(u_to, v_uv);
float n = ridged(v_uv * 4.);
float e = smoothstep(u_progress - .04, u_progress + .04, n);
float heat = smoothstep(.12, 0., abs(n - u_progress)) * (1. - step(1., u_progress));
vec3 burn = mix(vec3(.4, 0, 0), vec3(1, .4, 0), smoothstep(0., .25, heat));
burn = mix(burn, vec3(1, .85, .3), smoothstep(.25, .5, heat));
burn = mix(burn, vec3(1), smoothstep(.5, 1., heat));
float sparks = step(.92, vnoise(v_uv * 80.)) * heat * 3.;
gl_FragColor = vec4(mix(B, A, e).rgb + burn * heat * 3.5
+ vec3(1., .7, .3) * sparks, 1.);
}
```
### Whip Pan
Both scenes slide horizontally in opposite directions with 10-sample directional motion blur. No noise needed.
```glsl
void main() {
float fromOff = u_progress * 1.5;
vec3 fromC = vec3(0.);
for (int i = 0; i < 10; i++) {
float f = float(i) / 10.;
vec2 fuv = vec2(v_uv.x + fromOff + u_progress * .08 * f, v_uv.y);
fromC += texture2D(u_from, clamp(fuv, 0., 1.)).rgb;
}
fromC /= 10.;
float toOff = (1. - u_progress) * 1.5;
vec3 toC = vec3(0.);
for (int i = 0; i < 10; i++) {
float f = float(i) / 10.;
vec2 tuv = vec2(v_uv.x - toOff - (1. - u_progress) * .08 * f, v_uv.y);
toC += texture2D(u_to, clamp(tuv, 0., 1.)).rgb;
}
toC /= 10.;
gl_FragColor = vec4(mix(fromC, toC, u_progress), 1.);
}
```
### SDF Iris
Aspect-corrected circle SDF opening from center. Triple onion ring glow.
```glsl
void main() {
vec4 A = texture2D(u_from, v_uv), B = texture2D(u_to, v_uv);
vec2 uv = (v_uv - .5) * vec2(u_resolution.x / u_resolution.y, 1.);
float d = length(uv);
float radius = u_progress * 1.2;
float fw = .003;
float edge = smoothstep(radius + fw, radius - fw, d);
float ring1 = exp(-abs(d - radius) * 25.);
float ring2 = exp(-abs(d - radius + .04) * 20.) * .5;
float ring3 = exp(-abs(d - radius + .08) * 15.) * .25;
float glow = (ring1 + ring2 + ring3) * u_progress * (1. - u_progress) * 4.;
gl_FragColor = vec4(mix(A, B, edge).rgb + vec3(1., .85, .6) * glow * .6, 1.);
}
```
### Ripple Waves
Exponential sine waves (`exp(sin(x)-1)`) — sharp crests, broad troughs. Both scenes ripple in opposite phases.
```glsl
void main() {
vec2 uv = v_uv - .5;
float dist = length(uv);
vec2 dir = normalize(uv + .001);
float fromAmp = u_progress * .04;
float fw1 = exp(sin(dist * 25. - u_progress * 12.) - 1.);
float fw2 = exp(sin(dist * 50. - u_progress * 18.) - 1.) * .5;
vec2 fromUv = clamp(v_uv + dir * (fw1 + fw2) * fromAmp, 0., 1.);
float toAmp = (1. - u_progress) * .04;
float tw1 = exp(sin(dist * 25. + u_progress * 12.) - 1.);
float tw2 = exp(sin(dist * 50. + u_progress * 18.) - 1.) * .5;
vec2 toUv = clamp(v_uv - dir * (tw1 + tw2) * toAmp, 0., 1.);
vec4 A = texture2D(u_from, fromUv);
vec4 B = texture2D(u_to, toUv);
float peak = fw1 * u_progress;
vec3 tint = vec3(.9, .95, 1.) * peak * .1;
gl_FragColor = vec4(mix(A.rgb + tint, B.rgb, u_progress), 1.);
}
```
### Gravitational Lens
Content warps toward gravity well with chromatic aberration + event horizon darkening. Pull is monotonic — never reverses.
```glsl
void main() {
vec4 B = texture2D(u_to, v_uv);
vec2 uv = v_uv - .5;
float dist = length(uv);
float pull = u_progress * 2.;
float warpStr = pull * .3 / (dist + .1);
vec2 warped = clamp(v_uv - uv * warpStr, 0., 1.);
vec4 A = texture2D(u_from, warped);
float horizon = smoothstep(0., .3, dist / (1. - u_progress * .85 + .001));
float shift = pull * .02 / (dist + .2);
float r = texture2D(u_from, clamp(v_uv - uv * (warpStr + shift), 0., 1.)).r;
float b = texture2D(u_from, clamp(v_uv - uv * (warpStr - shift), 0., 1.)).b;
vec3 lensed = vec3(r, A.g, b) * horizon;
gl_FragColor = vec4(mix(lensed, B.rgb, smoothstep(.3, .9, u_progress)), 1.);
}
```
### Cinematic Zoom
Both scenes zoom-blur in opposite directions with per-channel radial offset (chromatic aberration). From zooms outward, to zooms inward from tight.
```glsl
void main() {
vec2 d = v_uv - vec2(.5);
float fromS = u_progress * .08;
float toS = (1. - u_progress) * .06;
float fr = 0., fg = 0., fb = 0.;
for (int i = 0; i < 12; i++) {
float f = float(i) / 12.;
fr += texture2D(u_from, v_uv - d * (fromS * 1.06) * f).r;
fg += texture2D(u_from, v_uv - d * fromS * f).g;
fb += texture2D(u_from, v_uv - d * (fromS * .94) * f).b;
}
vec3 fromBl = vec3(fr, fg, fb) / 12.;
float tr = 0., tg = 0., tb = 0.;
for (int i = 0; i < 12; i++) {
float f = float(i) / 12.;
tr += texture2D(u_to, v_uv + d * (toS * 1.06) * f).r;
tg += texture2D(u_to, v_uv + d * toS * f).g;
tb += texture2D(u_to, v_uv + d * (toS * .94) * f).b;
}
vec3 toBl = vec3(tr, tg, tb) / 12.;
gl_FragColor = vec4(mix(fromBl, toBl, u_progress), 1.);
}
```
### Chromatic Radial Split
Both scenes' RGB channels separate/converge radially. From-scene splits outward, to-scene converges inward.
```glsl
void main() {
vec2 c = v_uv - .5;
float fromShift = u_progress * .06;
float fr = texture2D(u_from, clamp(v_uv + c * fromShift, 0., 1.)).r;
float fg = texture2D(u_from, v_uv).g;
float fb = texture2D(u_from, clamp(v_uv - c * fromShift, 0., 1.)).b;
vec3 fromSplit = vec3(fr, fg, fb);
float toShift = (1. - u_progress) * .06;
float tr = texture2D(u_to, clamp(v_uv - c * toShift, 0., 1.)).r;
float tg = texture2D(u_to, v_uv).g;
float tb = texture2D(u_to, clamp(v_uv + c * toShift, 0., 1.)).b;
vec3 toSplit = vec3(tr, tg, tb);
gl_FragColor = vec4(mix(fromSplit, toSplit, u_progress), 1.);
}
```
### Glitch (shader)
Scan lines + block scramble + chromatic aberration + brightness flicker + color posterization. More aggressive than the CSS glitch.
```glsl
float rand(vec2 co) {
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
float inten = u_progress * (1. - u_progress) * 4.;
float lineY = floor(v_uv.y * 60.) / 60.;
float lineDisp = (rand(vec2(lineY, floor(u_progress * 17.))) - .5) * .18 * inten;
vec2 block = floor(v_uv * vec2(12., 8.));
float br = rand(block + vec2(floor(u_progress * 11.)));
float ba = step(.83, br) * inten;
vec2 bd = (vec2(rand(block * 2.1), rand(block * 3.7)) - .5) * .35 * ba;
vec2 uv = clamp(v_uv + vec2(lineDisp, 0.) + bd, 0., 1.);
float shift = inten * .035;
float r = texture2D(u_from, uv + vec2(shift, 0.)).r;
float g = texture2D(u_from, uv).g;
float b = texture2D(u_from, uv - vec2(shift, 0.)).b;
vec3 col = vec3(r, g, b);
col -= step(.5, fract(v_uv.y * u_resolution.y * .5)) * .05 * inten;
col *= 1. + (rand(vec2(floor(u_progress * 23.))) - .5) * .3 * inten;
float levels = mix(256., 8., inten * .5);
col = floor(col * levels) / levels;
gl_FragColor = mix(vec4(col, 1.), texture2D(u_to, v_uv), u_progress);
}
```
### Swirl Vortex
Both scenes swirl in opposite directions with FBM-warped spiral path.
```glsl
// Requires: NQ
void main() {
vec2 uv = v_uv - .5;
float dist = length(uv);
float warp = fbm(v_uv * 4.) * .5;
float fromAng = u_progress * (1. - dist) * 10. + warp * u_progress * 3.;
float fs = sin(fromAng), fc = cos(fromAng);
vec2 fromUv = clamp(vec2(uv.x*fc - uv.y*fs, uv.x*fs + uv.y*fc) + .5, 0., 1.);
float toAng = -(1. - u_progress) * (1. - dist) * 10.
- warp * (1. - u_progress) * 3.;
float ts = sin(toAng), tc = cos(toAng);
vec2 toUv = clamp(vec2(uv.x*tc - uv.y*ts, uv.x*ts + uv.y*tc) + .5, 0., 1.);
vec4 A = texture2D(u_from, fromUv);
vec4 B = texture2D(u_to, toUv);
gl_FragColor = mix(A, B, u_progress);
}
```
### Thermal Distortion
FBM-driven heat shimmer rising from bottom. Warps both scenes with sine displacement modulated by noise. Slight warm overexposure haze.
```glsl
// Requires: NQ
void main() {
float heat = u_progress * 1.5;
float yFade = smoothstep(1., 0., v_uv.y);
float shimmer = sin(v_uv.y * 40. + fbm(v_uv * 6.) * 8.)
* fbm(v_uv * 3. + vec2(0., u_progress * 2.));
float dispX = shimmer * heat * .03 * yFade;
vec2 fromUv = clamp(v_uv + vec2(dispX, 0.), 0., 1.);
vec4 A = texture2D(u_from, fromUv);
float invShimmer = sin(v_uv.y * 40. + fbm(v_uv * 6. + 3.) * 8.)
* fbm(v_uv * 3. + vec2(3., u_progress * 2.));
float dispX2 = invShimmer * (1. - u_progress) * .03 * yFade;
vec2 toUv = clamp(v_uv + vec2(dispX2, 0.), 0., 1.);
vec4 B = texture2D(u_to, toUv);
float haze = heat * yFade * .15 * (1. - u_progress);
gl_FragColor = vec4(mix(A.rgb, B.rgb, u_progress)
+ vec3(1., .9, .7) * haze, 1.);
}
```
### Flash Through White
Both scenes brighten to white midpoint. Works on dark backgrounds where color-dip-to-black is invisible.
```glsl
void main() {
vec4 A = texture2D(u_from, v_uv), B = texture2D(u_to, v_uv);
float toWhite = smoothstep(0., .45, u_progress);
vec3 fromC = mix(A.rgb, vec3(1.), toWhite);
float fromWhite = 1. - smoothstep(.5, 1., u_progress);
vec3 toC = mix(B.rgb, vec3(1.), fromWhite);
gl_FragColor = vec4(mix(fromC, toC, smoothstep(.35, .65, u_progress)), 1.);
}
```
### Cross-Warp Morph
Both scenes displace along a shared FBM noise field in opposite directions. Noise-driven blend boundary. Neither scene just sits there.
```glsl
// Requires: NQ
void main() {
vec2 disp = vec2(fbm(v_uv * 3.), fbm(v_uv * 3. + vec2(7.3, 3.7))) - .5;
vec2 fromUv = clamp(v_uv + disp * u_progress * .5, 0., 1.);
vec2 toUv = clamp(v_uv - disp * (1. - u_progress) * .5, 0., 1.);
vec4 A = texture2D(u_from, fromUv);
vec4 B = texture2D(u_to, toUv);
float n = fbm(v_uv * 4. + vec2(3.1, 1.7));
float blend = smoothstep(.4, .6, n + u_progress * 1.2 - .6);
gl_FragColor = mix(A, B, blend);
}
```
### Light Leak (shader)
Beer-Lambert exponential falloff + ACES tone mapping + directional flare streak. More physically accurate than the CSS overlay version.
```glsl
vec3 aces(vec3 x) {
return clamp((x * (2.51 * x + .03)) / (x * (2.43 * x + .59) + .14), 0., 1.);
}
void main() {
vec4 A = texture2D(u_from, v_uv), B = texture2D(u_to, v_uv);
vec2 lp = vec2(1.3, -.2);
float dist = length(v_uv - lp);
float leak = clamp(exp(-dist * 1.8) * u_progress * 4., 0., 1.);
vec3 warmColor = mix(vec3(1., .5, .15), vec3(1., .9, .75), dist * .7);
float flare = exp(-abs(v_uv.y - (-.2 + v_uv.x * .3)) * 15.) * leak * .3;
vec3 overexposed = A.rgb + warmColor * leak * 3. + vec3(1., .8, .5) * flare;
overexposed = aces(overexposed);
gl_FragColor = vec4(mix(overexposed, B.rgb, smoothstep(.15, .85, u_progress)), 1.);
}
```
-83
View File
@@ -1,83 +0,0 @@
---
name: transitions
description: Use when a HyperFrames composition has multiple scenes that need transitions between them. Also use when the user mentions crossfades, wipes, reveals, scene changes, or when a composition feels like scenes just pop in and out without visual handoffs.
---
# Scene Transitions
A transition tells the viewer how two scenes relate. A crossfade says "this continues." A push slide says "next point." A flash cut says "wake up." A blur crossfade says "drift with me." Choose transitions that match what the content is doing emotionally, not just technically.
## Energy → Primary Transition
| Energy | Primary | Accent for key moments | Duration | Easing |
| ---------------------------------------- | ---------------------------- | ------------------------------ | --------- | ---------------------- |
| **Calm** (wellness, brand story, luxury) | Blur crossfade, focus pull | Light leak, circle iris | 0.5-0.8s | `sine.inOut`, `power1` |
| **Medium** (corporate, SaaS, explainer) | Push slide, staggered blocks | Squeeze, vertical push | 0.3-0.5s | `power2`, `power3` |
| **High** (promos, sports, music, launch) | Flash cut, zoom through | Staggered blocks, gravity drop | 0.15-0.3s | `power4`, `expo` |
Pick ONE primary (60-70% of scene changes) + 1-2 accents. Never use a different transition for every scene.
## Mood → Transition Type
Think about what the transition _communicates_, not just what it looks like.
| Mood | Transitions | Why it works |
| ------------------------ | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Warm / inviting** | Light leak, blur crossfade, focus pull, film burn | Soft edges, warm color washes. Nothing sharp or mechanical. The transition feels like sunlight. |
| **Cold / clinical** | Squeeze, zoom out, blinds, shutter, grid dissolve | Content transforms mechanically — compressed, shrunk, sliced, gridded. Zoom out creates clinical distance. No softness. |
| **Editorial / magazine** | Push slide, vertical push, diagonal split, shutter | Like turning a page or slicing a layout. Clean directional movement. Diagonal split is a bold editorial cut. |
| **Tech / futuristic** | Grid dissolve, staggered blocks, blinds, chromatic aberration | Grid dissolve is the core "data" transition. Pattern-based, coordinated. Data-grid aesthetic. |
| **Tense / edgy** | Glitch, VHS, chromatic aberration, flash cut, ripple | Instability, distortion, digital breakdown. The medium itself is corrupted. |
| **Playful / fun** | Elastic push, 3D flip, circle iris, morph circle, clock wipe | Overshoot, bounce, rotation, expansion. Clock wipe has a whimsical mechanical quality. |
| **Dramatic / cinematic** | Zoom through, zoom out, gravity drop, overexposure, diagonal split, color dip to black | Scale, weight, light extremes. Big spatial movements. Diagonal split is a bold geometric cut. |
| **Premium / luxury** | Focus pull, blur crossfade, color dip to black, slow crossfade | Restraint. Long durations, soft easing, minimal movement. The transition barely exists — that IS the luxury. |
| **Retro / analog** | Film burn, light leak, VHS, clock wipe | Organic imperfection. Warm color bleeds, scan line displacement. Clock wipe evokes broadcast TV. |
## Narrative Position
| Position | Use | Why |
| -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |
| **Opening** | Your most distinctive transition. Match the mood. 0.4-0.6s | Sets the visual language for the entire piece. |
| **Between related points** | Your primary transition. Consistent. 0.3s | Don't distract — the content is continuing. |
| **Topic change** | Something different from your primary. Staggered blocks, shutter, squeeze. | Signals "new section" — the viewer's brain resets. |
| **Climax / hero reveal** | Your boldest accent. Fastest or most dramatic. | This is the payoff — spend your best transition here. |
| **Wind-down** | Return to gentle. Blur crossfade, crossfade. 0.5-0.7s | Let the viewer exhale after the climax. |
| **Outro** | Slowest, simplest. Crossfade, color dip to black. 0.6-1.0s | Closure. Don't introduce new energy at the end. |
## Blur Intensity by Energy
| Energy | Blur | Duration | Hold at peak |
| ---------- | ------- | -------- | ------------ |
| **Calm** | 20-30px | 0.8-1.2s | 0.3-0.5s |
| **Medium** | 8-15px | 0.4-0.6s | 0.1-0.2s |
| **High** | 3-6px | 0.2-0.3s | 0s |
## Presets
| Preset | Duration | Easing |
| ---------- | -------- | ----------------- |
| `snappy` | 0.2s | `power4.inOut` |
| `smooth` | 0.4s | `power2.inOut` |
| `gentle` | 0.6s | `sine.inOut` |
| `dramatic` | 0.5s | `power3.in` → out |
| `instant` | 0.15s | `expo.inOut` |
| `luxe` | 0.7s | `power1.inOut` |
## Implementation
Read [catalog.md](./catalog.md) for GSAP code and hard rules for every transition type.
| Category | Transitions |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| Content-transforming | Push slide, vertical push, elastic push, squeeze, zoom through, zoom out, gravity drop, 3D flip |
| Reveal/mask | Circle iris, diamond iris, diagonal split, clock wipe, shutter |
| Dissolve | Crossfade, blur crossfade, focus pull, color dip |
| Cover | Staggered blocks, horizontal blinds, vertical blinds |
| Light | Light leak, overexposure burn, film burn |
| Distortion | Glitch, chromatic aberration, ripple, VHS tape |
| Pattern | Grid dissolve |
| Instant | Flash cut, morph circle |
## Transitions That Don't Work in CSS
Avoid: star iris, tilt-shift, lens flare, hinge/door. See catalog.md for why.
-661
View File
@@ -1,661 +0,0 @@
# Transition Catalog
GSAP implementation code + hard rules for every transition type. All code examples use `old` for the outgoing scene-inner selector and `new` for the incoming, with `T` as the transition start time.
## Table of Contents
- [Hard Rules](#hard-rules)
- [Scene Template](#scene-template)
- [Linear / Push](#linear--push)
- [Radial / Shape](#radial--shape)
- [3D](#3d)
- [Scale / Zoom](#scale--zoom)
- [Dissolve](#dissolve)
- [Cover](#cover)
- [Light](#light)
- [Distortion](#distortion)
- [Mechanical](#mechanical)
- [Grid](#grid)
- [Other](#other)
- [Blur](#blur)
- [Destruction](#destruction)
## Hard Rules
These cause real bugs if violated.
**Scene visibility:** Scene 1 visible by default (no `opacity: 0`). Scenes 2+ have `opacity: 0` on the CONTAINER div. GSAP reveals them. No visibility shim (`timedEls`).
**Iframe compatibility:** No external font links (`<link>` to Google Fonts, `@import`). They block sandboxed iframes. Use system fonts.
**Element structure:** No `class="clip"` on scene divs in standalone compositions. Only the root div gets `data-composition-id`/`data-start`/`data-duration`.
**Overlay elements:** Staggered blocks = full-screen 1920x1080, NOT thin strips. Glitch RGB overlays = normal blending at 35% opacity, NOT `mix-blend-mode: multiply` (invisible on dark backgrounds). Light leak overlays = larger than the frame (2400px+), never a visible shape. Overexposure = use `filter: brightness()` on the scene, not just a white overlay.
**VHS tape:** Clone actual scene content with `cloneNode(true)`, NOT colored bars. Each strip: wider than frame (2020px at left:-50px). Red+blue chromatic copies at z-index above main strip. Seeded PRNG for deterministic random offsets.
**Z-index:** Gravity drop, zoom out, diagonal split need outgoing scene ON TOP (`zIndex: 10`) so it exits while revealing the new scene behind (`zIndex: 1`).
**Page burn:** Content burns with the page — no falling debris. Hide scene1 via `tl.set` at burn end, NEVER `onComplete` (not reversible). `onUpdate` must restore `clipPath: "none"` when `wp <= 0` for rewind support. Incoming scene fades from black at 90% through burn.
**Clock wipe:** 9-point polygon with intermediate edge positions. Step through 4 quadrants with separate tweens.
**Grid dissolve:** Cycle 5 palette colors per cell, not monochrome.
**Blinds count by energy:** Calm: 4h/6v. Medium: 6-8h/8v. High: 12-16h/16v.
**Don't use:** Star iris (polygon interpolation broken), tilt-shift (no selective CSS blur), lens flare (visible shape, not optical), hinge/door (distorts too fast).
## Scene Template
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
.scene {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#scene1 {
z-index: 1;
background: #color;
}
#scene2 {
z-index: 2;
background: #color;
opacity: 0;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="TOTAL"
>
<div id="scene1" class="scene"><!-- visible --></div>
<div id="scene2" class="scene"><!-- hidden --></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Transition: tl.to("#scene1", { opacity: 0 }, T); tl.to("#scene2", { opacity: 1 }, T);
window.__timelines["main"] = tl;
</script>
</body>
</html>
```
Every transition follows: position new scene → animate outgoing → swap → animate incoming → clean up overlays.
## Linear / Push
### Push Slide
Both scenes move together — new pushes old out.
```js
tl.to(old, { x: -1920, duration: 0.5, ease: "power3.inOut" }, T);
tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 0, duration: 0.5, ease: "power3.inOut" }, T);
```
### Vertical Push
Same as push slide but vertical.
```js
tl.to(old, { y: -1080, duration: 0.5, ease: "power3.inOut" }, T);
tl.fromTo(new, { y: 1080, opacity: 1 }, { y: 0, duration: 0.5, ease: "power3.inOut" }, T);
```
### Elastic Push
Push with overshoot bounce on the incoming scene.
```js
tl.to(old, { x: -1920, duration: 0.5, ease: "power3.in" }, T);
tl.fromTo(new, { x: 1920, opacity: 1 }, { x: 30, duration: 0.4, ease: "power4.out" }, T + 0.1);
tl.to(new, { x: -15, duration: 0.15, ease: "sine.inOut" }, T + 0.5);
tl.to(new, { x: 0, duration: 0.1, ease: "sine.out" }, T + 0.65);
```
### Squeeze
Old compresses, new expands from opposite side.
```js
tl.to(old, { scaleX: 0, transformOrigin: "left center", duration: 0.4, ease: "power3.inOut" }, T);
tl.fromTo(new, { scaleX: 0, transformOrigin: "right center", opacity: 1 },
{ scaleX: 1, duration: 0.4, ease: "power3.inOut" }, T + 0.1);
tl.set(old, { opacity: 0 }, T + 0.5);
```
## Radial / Shape
### Circle Iris
Expanding circle from center reveals new scene.
```js
tl.set(new, { opacity: 1 }, T);
tl.fromTo(new,
{ clipPath: "circle(0% at 50% 50%)" },
{ clipPath: "circle(75% at 50% 50%)", duration: 0.5, ease: "power2.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.5);
```
### Diamond Iris
Expanding diamond shape from center.
```js
tl.set(new, { opacity: 1 }, T);
tl.fromTo(new,
{ clipPath: "polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)" },
{ clipPath: "polygon(50% -20%, 120% 50%, 50% 120%, -20% 50%)", duration: 0.5, ease: "power2.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.5);
```
### Diagonal Split
Old scene shrinks to a triangle in one corner.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10, clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" }, T);
tl.to(old, { clipPath: "polygon(60% 0%, 100% 0%, 100% 40%, 60% 0%)", duration: 0.5, ease: "power3.inOut" }, T);
tl.set(old, { opacity: 0, zIndex: "auto", clipPath: "none" }, T + 0.5);
tl.set(new, { zIndex: "auto" }, T + 0.5);
```
## 3D
### 3D Card Flip
180° Y-axis rotation. Requires CSS: `backface-visibility: hidden; transform-style: preserve-3d;` on both scene-inners. Parent needs `perspective: 1200px`.
```js
tl.set(new, { rotationY: -180, opacity: 1 }, T);
tl.to(old, { rotationY: 180, duration: 0.6, ease: "power2.inOut" }, T);
tl.to(new, { rotationY: 0, duration: 0.6, ease: "power2.inOut" }, T);
tl.set(old, { opacity: 0 }, T + 0.6);
```
## Scale / Zoom
### Zoom Through
Old zooms past camera + blurs, new zooms in from behind.
```js
tl.to(old, { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, T);
tl.fromTo(new,
{ scale: 0.5, opacity: 0, filter: "blur(8px)" },
{ scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, T + 0.15);
```
### Zoom Out
Old shrinks away, new was behind it. Needs z-index management.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10, transformOrigin: "50% 50%" }, T);
tl.to(old, { scale: 0.3, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
tl.set(old, { zIndex: "auto" }, T + 0.4);
tl.set(new, { zIndex: "auto" }, T + 0.4);
```
## Dissolve
### Crossfade
Simple opacity swap. The baseline.
```js
tl.to(old, { opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, T);
```
### Blur Crossfade
Dissolve with blur + scale shift. **Scale blur amount by energy** — see SKILL.md "Blur Intensity by Energy" section. The examples below show the medium (default) version. For calm compositions, increase to 20-30px with a 0.3-0.5s hold at peak blur. For high-energy, decrease to 3-6px with no hold.
**Medium (default):**
```js
tl.to(old, { filter: "blur(10px)", scale: 1.03, opacity: 0, duration: 0.5, ease: "power2.inOut" }, T);
tl.fromTo(new,
{ filter: "blur(10px)", scale: 0.97, opacity: 0 },
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.5, ease: "power2.inOut" }, T + 0.1);
```
**Calm (wellness, luxury) — heavy blur, holds at abstract color:**
```js
tl.to(old, { filter: "blur(25px)", scale: 1.05, duration: 0.6, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.4, ease: "power1.in" }, T + 0.4);
tl.fromTo(new,
{ filter: "blur(25px)", scale: 0.95, opacity: 0 },
{ filter: "blur(25px)", scale: 0.95, opacity: 1, duration: 0.3, ease: "power1.inOut" }, T + 0.5);
tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.6, ease: "power1.out" }, T + 0.8);
```
### Focus Pull
Outgoing slowly blurs while incoming fades in sharp. Depth-of-field feel. **Scale blur amount and hold duration by energy.**
**Medium:**
```js
tl.to(old, { filter: "blur(15px)", duration: 0.5, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.3, ease: "power2.in" }, T + 0.25);
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: "power2.out" }, T + 0.25);
```
**Calm — slow rack focus with long hold at peak defocus:**
```js
tl.to(old, { filter: "blur(30px)", duration: 0.8, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.5, ease: "power1.in" }, T + 0.6);
tl.fromTo(new, { opacity: 0, filter: "blur(20px)" },
{ opacity: 1, filter: "blur(20px)", duration: 0.3, ease: "power1.inOut" }, T + 0.7);
tl.to(new, { filter: "blur(0px)", duration: 0.6, ease: "power1.out" }, T + 1.0);
```
### Color Dip
Fade to solid color, hold, fade up new scene.
```js
tl.to(old, { opacity: 0, duration: 0.2, ease: "power2.in" }, T);
// Background color shows through
tl.fromTo(new, { opacity: 0 }, { opacity: 1, duration: 0.2, ease: "power2.out" }, T + 0.25);
```
## Cover
### Staggered Color Blocks
Full-screen (1920x1080) colored divs slide across staggered. Scene swaps while covered.
**2-block** (standard):
```js
tl.set("#wipe-a", { x: -1920 }, T - 0.01);
tl.set("#wipe-b", { x: -1920 }, T - 0.01);
tl.to("#wipe-a", { x: 0, duration: 0.25, ease: "power3.inOut" }, T);
tl.to("#wipe-b", { x: 0, duration: 0.25, ease: "power3.inOut" }, T + 0.06);
tl.set(old, { opacity: 0 }, T + 0.2);
tl.set(new, { opacity: 1 }, T + 0.2);
tl.to("#wipe-a", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.28);
tl.to("#wipe-b", { x: 1920, duration: 0.25, ease: "power3.inOut" }, T + 0.34);
```
**5-block** (dense variant): same pattern with 5 blocks at 0.04s stagger. Use composition palette colors.
### Horizontal Blinds
Full-width strips slide across staggered. Each strip: `width: 1920px; height: Xpx`.
**6 strips** (180px each): `0.03s` stagger
**12 strips** (90px each): `0.018s` stagger
```js
for (var i = 0; i < N; i++) {
tl.set("#blind-h-" + i, { x: -1920 }, T - 0.01);
tl.fromTo("#blind-h-" + i, { x: -1920 }, { x: 0, duration: 0.2, ease: "power3.inOut" }, T + i * stagger);
}
tl.set(old, { opacity: 0 }, T + coverTime);
tl.set(new, { opacity: 1 }, T + coverTime);
for (var i = 0; i < N; i++) {
tl.to("#blind-h-" + i, { x: 1920, duration: 0.2, ease: "power3.inOut" }, T + exitStart + i * stagger);
}
```
### Vertical Blinds
Same as horizontal but strips are tall and narrow, moving on Y axis.
## Light
### Light Leak
Multiple warm-colored overlays wash across frame. Needs: a flat warm tint layer + 2-3 bright radial gradient divs, all larger than the frame so edges are never visible.
```js
// Warm tint washes over entire frame
tl.to("#leak-warm", { opacity: 0.4, duration: 0.3, ease: "power1.in" }, T);
// Bright leak elements drift in
tl.to("#leak-1", { opacity: 0.9, x: 300, duration: 0.5, ease: "sine.inOut" }, T + 0.05);
tl.to("#leak-2", { opacity: 0.8, x: 200, duration: 0.6, ease: "sine.inOut" }, T + 0.1);
// Peak warmth then swap
tl.to("#leak-warm", { opacity: 0.6, duration: 0.15, ease: "power2.in" }, T + 0.35);
tl.set(old, { opacity: 0 }, T + 0.45);
tl.set(new, { opacity: 1 }, T + 0.45);
// Leak fades
tl.to("#leak-warm", { opacity: 0, duration: 0.4, ease: "power2.out" }, T + 0.5);
tl.to("#leak-1", { opacity: 0, x: 600, duration: 0.35, ease: "power1.out" }, T + 0.5);
```
### Overexposure Burn
Scene progressively blows out to white using CSS `filter: brightness()`, then white overlay fades in. Swap at peak white. White recedes to reveal new scene.
```js
tl.to(old, { filter: "brightness(1.5)", scale: 1.03, duration: 0.2, ease: "power1.in" }, T);
tl.to(old, { filter: "brightness(3)", scale: 1.06, duration: 0.2, ease: "power2.in" }, T + 0.2);
tl.to("#flash-overlay", { opacity: 0.5, duration: 0.25, ease: "power1.in" }, T + 0.15);
tl.to("#flash-overlay", { opacity: 1, duration: 0.15, ease: "power2.in" }, T + 0.4);
tl.set(old, { opacity: 0, filter: "brightness(1)", scale: 1 }, T + 0.55);
tl.set(new, { opacity: 1 }, T + 0.55);
tl.to("#flash-overlay", { opacity: 0, duration: 0.35, ease: "power2.out" }, T + 0.55);
```
### Film Burn
Staggered warm overlays (amber, orange, red) bleed from one edge. Each overlay is a large radial gradient div at high z-index.
```js
tl.to("#burn-a", { opacity: 1, x: -300, duration: 0.4, ease: "power1.in" }, T);
tl.to("#burn-b", { opacity: 1, x: -500, duration: 0.5, ease: "power1.in" }, T + 0.05);
tl.to("#burn-c", { opacity: 1, x: -200, duration: 0.45, ease: "power1.in" }, T + 0.1);
tl.set(old, { opacity: 0 }, T + 0.35);
tl.set(new, { opacity: 1 }, T + 0.35);
tl.to("#burn-a", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.45);
tl.to("#burn-b", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.5);
tl.to("#burn-c", { opacity: 0, duration: 0.3, ease: "power2.out" }, T + 0.55);
```
## Distortion
### Glitch
RGB-tinted overlays (NOT multiply blend — use normal blending at 35% opacity) jitter with large offsets. Scene itself also jitters.
```js
tl.set("#glitch-r", { opacity: 1, x: 40, y: -8 }, T);
tl.set("#glitch-g", { opacity: 1, x: -30, y: 12 }, T);
tl.set("#glitch-b", { opacity: 1, x: 15, y: -20 }, T);
tl.set(old, { x: -15 }, T);
// 6 jitter frames at 0.03s intervals with big offsets (±30-60px)
// ... swap and clear at T + 0.2
```
### Chromatic Aberration
RGB overlays start aligned then spread apart (±80px), scene fades, converge on new scene.
```js
tl.set("#glitch-r", { opacity: 0.6, x: 0 }, T);
tl.set("#glitch-g", { opacity: 0.6, x: 0 }, T);
tl.set("#glitch-b", { opacity: 0.6, x: 0 }, T);
tl.to("#glitch-r", { x: -80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
tl.to("#glitch-b", { x: 80, opacity: 0.8, duration: 0.3, ease: "power2.in" }, T);
tl.to("#glitch-g", { y: 30, duration: 0.3, ease: "power2.in" }, T);
// Swap at T + 0.3, converge back at T + 0.3
```
### Ripple
Rapid oscillation (±30px) + scale distortion (0.97-1.03) + increasing blur. Swap at peak distortion.
```js
tl.to(old, { x: 30, scale: 1.02, duration: 0.04, ease: "none" }, T);
tl.to(old, { x: -25, scale: 0.98, filter: "blur(4px)", duration: 0.04, ease: "none" }, T + 0.04);
// ... more oscillations with increasing blur
// Swap at peak, incoming stabilizes with decreasing wobble
```
### VHS Tape
Clone scene into 20 horizontal strips (each 54px, clip-path'd). Each strip shifts x independently with seeded pseudo-random offsets at per-bar random intervals. Add red+blue chromatic offset copies on each strip (z-index above main, 35% opacity). Make strips wider than frame (2020px at left:-50px) so edges never show.
See SKILL.md for clone-based implementation pattern.
## Mechanical
### Shutter
Two full-screen halves close from top and bottom, meet in the middle. Swap while closed. Open again.
```js
tl.to("#shutter-top", { y: 0, duration: 0.25, ease: "power3.in" }, T);
tl.to("#shutter-bot", { y: 0, duration: 0.25, ease: "power3.in" }, T);
tl.set(old, { opacity: 0 }, T + 0.25);
tl.set(new, { opacity: 1 }, T + 0.25);
tl.to("#shutter-top", { y: -540, duration: 0.25, ease: "power3.out" }, T + 0.3);
tl.to("#shutter-bot", { y: 540, duration: 0.25, ease: "power3.out" }, T + 0.3);
```
### Clock Wipe
Radial polygon sweep stepping through quadrants. Use 9-point polygon with intermediate edge positions for smooth sweep.
```js
tl.set(new, { opacity: 1, zIndex: 10 }, T);
var d = 0.1; // duration per quadrant
tl.set(new, { clipPath: "polygon(50% 50%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0%)" }, T);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%, 100% 50%)", duration: d, ease: "none" }, T);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%)", duration: d, ease: "none" }, T + d);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 50%)", duration: d, ease: "none" }, T + d*2);
tl.to(new, { clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 100% 100%, 50% 100%, 0% 100%, 0% 50%, 0% 0%)", duration: d, ease: "none" }, T + d*3);
tl.set(new, { clipPath: "none", zIndex: "auto" }, T + d*4 + 0.02);
tl.set(old, { opacity: 0, zIndex: "auto" }, T + d*4 + 0.02);
```
## Grid
### Grid Dissolve
Grid of colored cells covers the frame in a ripple from center. Scene swaps at 50% coverage. Cells fade out in ripple.
**12-cell** (4x3, each 480x270): standard
**120-cell** (12x10, each 160x108): dense variant — lower opacity (0.75), tighter ripple
Cells are created dynamically in JS, sorted by distance from center for ripple stagger.
## Other
### Flash Cut
White full-screen overlay flashes at swap point. 0.03s on, 0.1s off.
```js
tl.to("#flash-overlay", { opacity: 1, duration: 0.03, ease: "power4.out" }, T);
tl.set(old, { opacity: 0 }, T + 0.03);
tl.set(new, { opacity: 1 }, T + 0.03);
tl.to("#flash-overlay", { opacity: 0, duration: 0.1, ease: "power2.out" }, T + 0.05);
```
### Gravity Drop
Old scene falls down with slight rotation. New scene was behind it. Needs z-index.
```js
tl.set(new, { opacity: 1, zIndex: 1 }, T);
tl.set(old, { zIndex: 10 }, T);
tl.to(old, { y: 1200, rotation: 4, duration: 0.5, ease: "power3.in" }, T);
tl.set(old, { opacity: 0, zIndex: "auto" }, T + 0.5);
tl.set(new, { zIndex: "auto" }, T + 0.5);
```
### Morph Circle
A circle scales up from center to fill frame (becoming the new scene's background color). New scene content fades in on top.
```js
tl.set("#morph-circle", { background: newBgColor, opacity: 1, scale: 0 }, T);
tl.to("#morph-circle", { scale: 30, duration: 0.5, ease: "power3.in" }, T);
tl.set(old, { opacity: 0 }, T + 0.4);
tl.set(new, { opacity: 1 }, T + 0.4);
tl.to("#morph-circle", { opacity: 0, duration: 0.15, ease: "power2.out" }, T + 0.5);
```
## Blur
All blur transitions scale with energy. See SKILL.md "Blur Intensity by Energy" for the full table.
### Blur Through
Content becomes fully abstract before resolving. The heaviest blur transition.
**Calm (default for this type — it's inherently heavy):**
```js
tl.to(old, { filter: "blur(30px)", scale: 1.08, duration: 0.5, ease: "power1.in" }, T);
tl.to(old, { opacity: 0, duration: 0.3, ease: "power1.in" }, T + 0.3);
// Hold: both scenes in abstract blur state
tl.fromTo(new,
{ filter: "blur(30px)", scale: 0.92, opacity: 0 },
{ filter: "blur(30px)", scale: 0.92, opacity: 1, duration: 0.2, ease: "none" }, T + 0.5);
// Slow resolve
tl.to(new, { filter: "blur(0px)", scale: 1, duration: 0.7, ease: "power1.out" }, T + 0.7);
```
**Medium:**
```js
tl.to(old, { filter: "blur(15px)", scale: 1.05, opacity: 0, duration: 0.4, ease: "power2.in" }, T);
tl.fromTo(new,
{ filter: "blur(15px)", scale: 0.95, opacity: 0 },
{ filter: "blur(0px)", scale: 1, opacity: 1, duration: 0.4, ease: "power2.out" }, T + 0.2);
```
### Directional Blur
Blur + skew simulating motion in one direction. Scale blur and skew with energy.
**Medium (default):**
```js
tl.to(old, { filter: "blur(12px)", skewX: -8, x: -200, opacity: 0, duration: 0.4, ease: "power3.in" }, T);
tl.fromTo(new,
{ filter: "blur(12px)", skewX: 8, x: 200, opacity: 0 },
{ filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.4, ease: "power3.out" }, T + 0.15);
```
**Calm (heavier blur, gentler motion):**
```js
tl.to(old, { filter: "blur(20px)", skewX: -4, x: -100, opacity: 0, duration: 0.6, ease: "power1.in" }, T);
tl.fromTo(new,
{ filter: "blur(20px)", skewX: 4, x: 100, opacity: 0 },
{ filter: "blur(0px)", skewX: 0, x: 0, opacity: 1, duration: 0.6, ease: "power1.out" }, T + 0.3);
```
## Destruction
### Page Burn
The outgoing scene literally burns away from a corner. A fire front expands with noise-based irregular edges, a canvas draws the scorched char line at the burn boundary, and individual text characters/elements chip off and fall with gravity as the fire reaches them. The incoming scene reveals behind the burn.
This transition has three systems working together:
1. **Fire geometry** — a radial front expanding from a corner (e.g., bottom-right) with noise-based irregularity for organic edges
2. **Scene clipping** — the outgoing scene uses an SVG clip-path (with `fill-rule: evenodd`) that cuts a hole matching the fire front. As the fire expands, more of the scene is clipped away. All content (text, images, lines) burns with the page — no separate debris.
3. **Scorched edge** — a `<canvas>` overlay draws a radial gradient fringe at the fire boundary to simulate charring
**When to use:** Dramatic reveals, edgy/destructive mood, gaming, cyberpunk. This is the most dramatic transition in the catalog — reserve it for hero moments.
**Requirements:**
- A `<canvas>` element for the burn edge overlay
- A noise function for organic fire edge geometry
- SVG clip-path with evenodd fill-rule for the inverted clip
**Fire geometry (deterministic noise):**
```js
function noise(x) {
var ix = Math.floor(x),
fx = x - ix;
var a = Math.sin(ix * 127.1 + 311.7) * 43758.5453;
var b = Math.sin((ix + 1) * 127.1 + 311.7) * 43758.5453;
var t = fx * fx * (3 - 2 * fx);
return a - Math.floor(a) + (b - Math.floor(b) - (a - Math.floor(a))) * t;
}
function fireRadiusAtAngle(angle, progress) {
var base = progress * maxRadius;
return (
base +
noise(angle * 3 + progress * 4) * 50 +
noise(angle * 8 + progress * 9) * 20 +
noise(angle * 15 + progress * 15) * 8
);
}
```
**Incoming scene timing:** The incoming scene should NOT be visible during the burn. As the fire consumes the outgoing scene, **black shows through the holes** — this is the dramatic part. The viewer watches content being destroyed against blackness.
At ~90% through the burn, the incoming scene fades in SLOWLY from black — the background first, then content staggered. Use long, gentle fades (`power1.out`, 0.8-1.2s durations) so it feels like the new scene materializes from darkness, not a hard swap.
```js
// Scene 2 stays at opacity: 0 during the burn — black behind the fire
tl.set("#s2-title", { opacity: 0 }, T);
tl.set("#s2-subtitle", { opacity: 0 }, T);
// At 90% through, scene bg fades in slowly from black
var contentReveal = T + BURN_DURATION * 0.9;
tl.to("#scene2", { opacity: 1, duration: 1.2, ease: "power1.out" }, contentReveal);
// Content fades in staggered on top, even slower
tl.to("#s2-title", { opacity: 1, duration: 1.0, ease: "power1.out" }, contentReveal + 0.5);
tl.to("#s2-subtitle", { opacity: 1, duration: 0.8, ease: "power1.out" }, contentReveal + 0.7);
```
**Content burns with the page — no falling debris.** The clip-path on scene1 IS the effect — as the fire shape expands, everything behind the fire edge (text, images, lines) disappears naturally. Don't clone elements, don't create falling debris. The content is part of the page being consumed. The scorched canvas edge provides the visual char line at the burn boundary.
**Hide scene1 via `tl.set` at burn end — NEVER in `onComplete`.** Using `onComplete` to hide scene1 is not reversible when scrubbing. Instead, use a `tl.set` at the exact burn end time:
```js
tl.to(
burnState,
{
progress: 1,
duration: BURN_DURATION,
ease: "none",
onUpdate: function () {
var wp = burnState.progress;
var scene1 = document.getElementById("scene1");
if (wp <= 0) {
scene1.style.clipPath = "none"; // fully visible when rewound
} else if (wp < 1) {
scene1.style.clipPath = buildClipPath(wp);
}
drawEdge(wp);
},
// NO onComplete — use tl.set instead
},
T,
);
// Hide scene1 at exact burn end — reversible via timeline
tl.set("#scene1", { opacity: 0 }, T + BURN_DURATION);
tl.set("#scene1", { clipPath: "none" }, T + BURN_DURATION);
```
The `onUpdate` handles clip-path and canvas edge per-frame. The `tl.set` handles the final hide — and GSAP automatically reverses it when scrubbing backward, restoring scene1 to `opacity: 1`.
**Note:** This is the most complex transition in the catalog. Reference the full implementation at `text-burn-dom.html` for the complete standalone example.
See `/Users/vanceingalls/src/claude/text-burn-dom.html` for the complete standalone implementation.