chore(skills): remove 1,685 lines of redundant skill content (#283)

* chore(skills): remove 1,685 lines of redundant and irrelevant skill content

- Remove 5 GSAP references irrelevant to HyperFrames (scrolltrigger,
  plugins, react, frameworks, utils) — no scroll, no frameworks, no
  interactive plugins in video compositions
- Remove shader-setup.md and shader-transitions.md — duplicated by
  @hyperframes/shader-transitions package (packages/shader-transitions/)
- Remove marker-highlight.md and examples.md — JS library docs superseded
  by css-patterns.md (deterministic, GSAP-driven, fully seekable)
- Trim CLAUDE.md to dev-only instructions — move product docs (transcription,
  TTS, player) to skills where they belong
- Deduplicate house-style.md typography/motion sections — point to
  dedicated references instead of repeating rules
- Clean up stale references to deleted files across SKILL.md and catalog.md
- Update gsap skill description to reflect HyperFrames-only scope

Skills: 5,230 → 3,714 lines (29% reduction)
CLAUDE.md: 204 → 50 lines (75% reduction)

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

* fix(skills): update broken marker-highlight.md references in captions.md

Point to css-patterns.md instead of deleted marker-highlight.md.

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

* fix(skills): update stale shader CSS rule to reference package API

BG_COLOR was from the old manual setup. Now it's bgColor in the
@hyperframes/shader-transitions init() config.

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

* fix(skills): address 6 doc gaps surfaced by eval agents

P0: Document HyperShader as IIFE global name in shader-transitions README
P1: Replace async fetch() with sync XHR in effects.md audio data loading
    (fetch violates synchronous timeline construction rule in SKILL.md)
P1: Change <div> to <span> in css-patterns.md marker highlight patterns
    (<div> inside <p> is invalid HTML, breaks layout in inline contexts)
P2: Clarify bgColor as fallback color in shader-transitions README
P2: Add data-start to Composition Clips table in SKILL.md
    (root composition element needs data-start="0", linter enforces it)

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

* fix(templates): update init templates to match trimmed skill scope

- Remove ScrollTrigger/plugins/React/Vue/Svelte from gsap skill description
- Replace class="clip" with accurate pattern examples in skill intro text
  (class="clip" is still in Key Rules where it belongs)

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

* fix(skills): remove contradictory 5:1 contrast threshold from house-style

house-style.md said 5:1 minimum, but hyperframes validate enforces
WCAG AA (4.5:1 normal text, 3:1 large text). Now defers to validate
instead of stating a conflicting number.

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-15 10:51:10 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent acf8223171
commit a262ad59f3
21 changed files with 84 additions and 1757 deletions
+2 -2
View File
@@ -44,7 +44,7 @@ Scan for words deserving distinct treatment:
- **Numbers/statistics** — bold weight, accent color
- **Emotional keywords** — exaggerated animation (overshoot, bounce)
- **Call-to-action** — highlight, underline, color pop
- **Marker highlight** — for beyond-color emphasis, see [marker-highlight.md](marker-highlight.md)
- **Marker highlight** — for beyond-color emphasis, see [css-patterns.md](css-patterns.md)
## Script-to-Style Mapping
@@ -121,7 +121,7 @@ tl.seek(0);
- [dynamic-techniques.md](dynamic-techniques.md) — karaoke, clip-path reveals, slam words, scatter exits, elastic, 3D rotation
- [transcript-guide.md](transcript-guide.md) — transcription commands, whisper models, external APIs
- [marker-highlight.md](marker-highlight.md) — animated text emphasis paired with per-word styling
- [css-patterns.md](css-patterns.md) — CSS+GSAP marker highlighting (deterministic, fully seekable)
## Constraints
+36 -34
View File
@@ -15,16 +15,16 @@ Pure CSS + GSAP implementations of all five MarkerHighlight.js drawing modes. Us
Yellow marker sweep behind text. The most common mode.
```html
<div class="mh-highlight-wrap">
<div class="mh-highlight-bar" id="hl-1"></div>
<span class="mh-highlight-wrap">
<span class="mh-highlight-bar" id="hl-1"></span>
<span class="mh-highlight-text">highlighted text</span>
</div>
</span>
```
```css
.mh-highlight-wrap {
position: relative;
display: inline-block;
display: inline;
}
.mh-highlight-bar {
position: absolute;
@@ -75,16 +75,16 @@ tl.to(
Hand-drawn circle around text. Use `border-radius: 50%` with a slight rotation for organic feel.
```html
<div class="mh-circle-wrap">
<span class="mh-circle-wrap">
<span class="mh-circle-text" id="circle-word">IMPORTANT</span>
<div class="mh-circle-ring" id="circle-1"></div>
</div>
<span class="mh-circle-ring" id="circle-1"></span>
</span>
```
```css
.mh-circle-wrap {
position: relative;
display: inline-block;
display: inline;
}
.mh-circle-text {
position: relative;
@@ -148,29 +148,29 @@ tl.to(
Radiating lines from text center. Each line is a positioned div rotated to its angle.
```html
<div class="mh-burst-wrap">
<span class="mh-burst-wrap">
<span class="mh-burst-text">WOW</span>
<div class="mh-burst-container" id="burst-1">
<div class="mh-burst-line" style="--angle: 0deg; --len: 70px;"></div>
<div class="mh-burst-line" style="--angle: 30deg; --len: 55px;"></div>
<div class="mh-burst-line" style="--angle: 60deg; --len: 80px;"></div>
<div class="mh-burst-line" style="--angle: 90deg; --len: 45px;"></div>
<div class="mh-burst-line" style="--angle: 120deg; --len: 65px;"></div>
<div class="mh-burst-line" style="--angle: 150deg; --len: 75px;"></div>
<div class="mh-burst-line" style="--angle: 180deg; --len: 50px;"></div>
<div class="mh-burst-line" style="--angle: 210deg; --len: 60px;"></div>
<div class="mh-burst-line" style="--angle: 240deg; --len: 80px;"></div>
<div class="mh-burst-line" style="--angle: 270deg; --len: 40px;"></div>
<div class="mh-burst-line" style="--angle: 300deg; --len: 70px;"></div>
<div class="mh-burst-line" style="--angle: 330deg; --len: 55px;"></div>
</div>
</div>
<span class="mh-burst-container" id="burst-1">
<span class="mh-burst-line" style="--angle: 0deg; --len: 70px;"></span>
<span class="mh-burst-line" style="--angle: 30deg; --len: 55px;"></span>
<span class="mh-burst-line" style="--angle: 60deg; --len: 80px;"></span>
<span class="mh-burst-line" style="--angle: 90deg; --len: 45px;"></span>
<span class="mh-burst-line" style="--angle: 120deg; --len: 65px;"></span>
<span class="mh-burst-line" style="--angle: 150deg; --len: 75px;"></span>
<span class="mh-burst-line" style="--angle: 180deg; --len: 50px;"></span>
<span class="mh-burst-line" style="--angle: 210deg; --len: 60px;"></span>
<span class="mh-burst-line" style="--angle: 240deg; --len: 80px;"></span>
<span class="mh-burst-line" style="--angle: 270deg; --len: 40px;"></span>
<span class="mh-burst-line" style="--angle: 300deg; --len: 70px;"></span>
<span class="mh-burst-line" style="--angle: 330deg; --len: 55px;"></span>
</span>
</span>
```
```css
.mh-burst-wrap {
position: relative;
display: inline-block;
display: inline;
}
.mh-burst-text {
position: relative;
@@ -186,6 +186,7 @@ Radiating lines from text center. Each line is a positioned div rotated to its a
}
.mh-burst-line {
position: absolute;
display: block;
width: 3px;
height: var(--len);
background: #1e88e5;
@@ -214,7 +215,7 @@ tl.fromTo(
Wavy SVG underlines and strikethroughs that draw themselves via `stroke-dashoffset`.
```html
<div class="mh-scribble-wrap">
<span class="mh-scribble-wrap">
<span class="mh-scribble-text">underlined text</span>
<svg class="mh-scribble-svg" viewBox="0 0 500 24" preserveAspectRatio="none">
<path
@@ -232,7 +233,7 @@ Wavy SVG underlines and strikethroughs that draw themselves via `stroke-dashoffs
```css
.mh-scribble-wrap {
position: relative;
display: inline-block;
display: inline;
}
.mh-scribble-text {
position: relative;
@@ -283,19 +284,19 @@ Scale the path's viewBox width to match text width. The wave pattern `Q x1,y1 x2
Cross-hatch lines over de-emphasized text. Multiple angled lines create a "crossed out" effect.
```html
<div class="mh-sketchout-wrap">
<span class="mh-sketchout-wrap">
<span class="mh-sketchout-text">old price</span>
<div class="mh-sketchout-lines" id="sketchout-1">
<div class="mh-sketchout-line mh-sketchout-fwd"></div>
<div class="mh-sketchout-line mh-sketchout-bwd"></div>
</div>
</div>
<span class="mh-sketchout-lines" id="sketchout-1">
<span class="mh-sketchout-line mh-sketchout-fwd"></span>
<span class="mh-sketchout-line mh-sketchout-bwd"></span>
</span>
</span>
```
```css
.mh-sketchout-wrap {
position: relative;
display: inline-block;
display: inline;
}
.mh-sketchout-text {
position: relative;
@@ -312,6 +313,7 @@ Cross-hatch lines over de-emphasized text. Multiple angled lines create a "cross
}
.mh-sketchout-line {
position: absolute;
display: block;
top: 50%;
left: 0;
width: 100%;
-146
View File
@@ -1,146 +0,0 @@
# Marker Highlight Examples
## Recipes
### Underline
```html
<mark
data-height="0.15"
data-offset="0.8"
data-padding="0"
data-highlight='{"amplitude":0.2,"wavelength":5,"roughEnds":0}'
data-color="rgba(30, 136, 229, 0.6)"
>important</mark
>
```
### Strikethrough
```html
<mark
data-drawing-mode="highlight"
data-height="0.1"
data-offset="0"
data-highlight='{"amplitude":0.1,"wavelength":3}'
data-color="rgba(229, 57, 53, 0.8)"
>wrong answer</mark
>
```
### Circled Annotation
```html
<mark
data-drawing-mode="circle"
data-circle='{"curve":0.8,"wobble":0.4,"loops":2,"thickness":3}'
data-animation-speed="1200"
data-color="rgba(229, 57, 53, 0.6)"
>this one</mark
>
```
## Full Example in a Composition
```html
<div data-composition-id="highlight-demo" data-width="1920" data-height="1080">
<div
id="content"
style="
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
font-family: 'Bricolage Grotesque', sans-serif; font-size: 72px; color: #fff;
background: #111;
"
>
<p id="hero">
The <mark id="m1" data-color="rgba(255, 220, 50, 0.5)">fastest</mark> way to
<mark
id="m2"
data-drawing-mode="circle"
data-circle='{"curve":0.8,"wobble":0.3,"loops":2,"thickness":3}'
data-color="rgba(229, 57, 53, 0.6)"
>ship</mark
>
</p>
</div>
<style>
[data-composition-id="highlight-demo"] mark {
background-color: transparent;
color: inherit;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="marker-highlight.global.js"></script>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Set colors via data attribute (no visible flash)
document.querySelectorAll("mark[data-color]").forEach(function (m) {
m.setAttribute("data-original-bgcolor", m.getAttribute("data-color"));
});
// Init once after fonts, then hide all canvases
var hl;
document.fonts.ready.then(function () {
setTimeout(function () {
hl = new MarkerHighlighter(document.getElementById("hero"), {
animate: false,
animationSpeed: 800,
padding: 0.3,
highlight: { amplitude: 0.3, wavelength: 5 },
});
setTimeout(function () {
document.querySelectorAll(".highlight").forEach(function (d) {
d.style.opacity = "0";
});
}, 100);
}, 50);
});
function addHighlight(markId, time) {
tl.to(
{},
{
duration: 0.001,
onStart: function () {
var mark = document.getElementById(markId);
var ref = mark.getAttribute("data-mark-ref");
if (!ref || !hl) return;
mark.parentElement
.querySelectorAll('.highlight[data-mark-id="' + ref + '"]')
.forEach(function (div) {
var c = div.querySelector("canvas");
if (c) c.getContext("2d").clearRect(0, 0, c.width, c.height);
div.style.opacity = "1";
});
hl.reanimateMark(mark);
},
onReverseComplete: function () {
var mark = document.getElementById(markId);
var ref = mark.getAttribute("data-mark-ref");
if (!ref) return;
mark.parentElement
.querySelectorAll('.highlight[data-mark-id="' + ref + '"]')
.forEach(function (div) {
div.style.opacity = "0";
});
},
},
time,
);
}
gsap.set("#hero", { opacity: 0 });
tl.to("#hero", { opacity: 1, duration: 0.6 }, 0);
addHighlight("m1", 0.8);
addHighlight("m2", 1.6);
window.__timelines["highlight-demo"] = tl;
</script>
</div>
```
@@ -1,158 +0,0 @@
# Marker Highlight
Animated canvas-based text highlighting using MarkerHighlight.js. Wraps text in `<mark>` tags and renders effects (marker pen, circle, burst, scribble, sketchout) on a canvas overlay without modifying text DOM.
The library runs its own requestAnimationFrame loop — **not** GSAP-driven. Use `tl.call()` to trigger at specific timeline points.
## Required Script
Download and convert to global script:
```bash
curl -sL "https://cdn.jsdelivr.net/gh/Robincodes-Sandbox/marker-highlight@main/dist/marker-highlight.min.js" \
| sed 's/export{[^}]*};$/window.MarkerHighlighter=W;/' > marker-highlight.global.js
```
```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="marker-highlight.global.js"></script>
```
## Color Setup
Set via `data-color`, copy to `data-original-bgcolor` before constructing. Never set `background-color` in CSS.
```css
mark {
color: inherit;
background-color: transparent;
}
```
```html
<mark id="m1" data-color="rgba(255, 220, 50, 0.5)">highlighted</mark>
```
```js
document
.querySelectorAll("mark[data-color]")
.forEach((m) => m.setAttribute("data-original-bgcolor", m.getAttribute("data-color")));
```
## GSAP Integration Pattern
ONE MarkerHighlighter per container with `animate: false`, hide all canvases, then clear+show+reanimate per mark at trigger time.
```js
var hl = new MarkerHighlighter(document.getElementById("text-container"), {
animate: false,
animationSpeed: 800,
padding: 0.3,
highlight: { amplitude: 0.3, wavelength: 5 },
});
setTimeout(function () {
document.querySelectorAll(".highlight").forEach((div) => (div.style.opacity = "0"));
}, 100);
function addHighlight(highlighter, markId, time) {
tl.to(
{},
{
duration: 0.001,
onStart: function () {
var mark = document.getElementById(markId);
var ref = mark.getAttribute("data-mark-ref");
var divs = mark.parentElement.querySelectorAll('.highlight[data-mark-id="' + ref + '"]');
divs.forEach(function (div) {
var canvas = div.querySelector("canvas");
if (canvas) canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
div.style.opacity = "1";
});
highlighter.reanimateMark(mark);
},
onReverseComplete: function () {
var mark = document.getElementById(markId);
var ref = mark.getAttribute("data-mark-ref");
mark.parentElement
.querySelectorAll('.highlight[data-mark-id="' + ref + '"]')
.forEach((div) => (div.style.opacity = "0"));
},
},
time,
);
}
addHighlight(hl, "m1", 1.0);
```
## Drawing Modes
| Mode | Effect | Best for |
| ----------- | ---------------------------- | -------------------------- |
| `highlight` | Wavy marker stroke (default) | Phrases, key terms |
| `circle` | Hand-drawn ellipse | Single words, annotations |
| `burst` | Radiating lines/curves/puffs | Excitement, energy |
| `scribble` | Chaotic scribble | Crossing out, messy energy |
| `sketchout` | Rough rectangle outline | Boxed callouts, blueprint |
```html
<mark data-drawing-mode="circle" data-color="rgba(229, 57, 53, 0.6)">critical</mark>
<mark
data-drawing-mode="burst"
data-burst='{"style":"cloud","count":20}'
data-color="rgba(255, 220, 50, 0.5)"
>amazing</mark
>
```
## Configuration
### Global (constructor)
| Option | Default | Description |
| ---------------- | ------------- | ------------------------- |
| `animate` | `true` | `false` to defer for GSAP |
| `animationSpeed` | `5000` | Duration in ms |
| `drawingMode` | `"highlight"` | Default mode |
| `height` | `1` | Relative to line height |
| `offset` | `0` | Vertical shift |
| `padding` | `0` | Horizontal padding |
### Per-Mode
**highlight**: `amplitude` (0.25), `wavelength` (1), `roughEnds` (5), `jitter` (0.1)
**circle**: `curve` (0.5), `wobble` (0.3), `loops` (3), `thickness` (5)
**burst**: `style` ("lines"/"curve"/"cloud"), `count` (10), `power` (1), `randomness` (0.5)
### Named Styles
```js
MarkerHighlighter.defineStyle("underline", {
animationSpeed: 400,
height: 0.15,
offset: 0.8,
padding: 0,
highlight: { amplitude: 0.2, wavelength: 5, roughEnds: 0 },
});
```
## Mode-to-Caption Energy Mapping
| Energy | Mode | Use for |
| ----------- | --------------------- | ------------------- |
| High | `burst` + `highlight` | Launches, hype |
| Medium-high | `circle` | Key stats, terms |
| Medium | `highlight` | Standard emphasis |
| Medium-low | `scribble` | Subtle, tutorials |
| Low | `sketchout` | Contrast, blueprint |
## Notes
- One highlighter per container (clears all `.highlight` divs on init)
- Canvas pre-draw + clear pattern for clean reveals
- rAF-based — not seekable mid-stroke
- Use `onReverseComplete` for rewind support
For CSS+GSAP fallback (no library, fully seekable), see [css-patterns.md](css-patterns.md).
For full examples, see [examples.md](examples.md).
+2 -2
View File
@@ -90,7 +90,7 @@ Avoid: star iris, tilt-shift, lens flare, hinge/door. See catalog.md for why.
CSS transitions animate scene containers with opacity, transforms, clip-path, and filters. Shader transitions composite both scene textures per-pixel on a WebGL canvas — they can warp, dissolve, and morph in ways CSS cannot.
**Both are first-class options.** Shaders require setup boilerplate (~200 lines, copied from [transitions/shader-setup.md](transitions/shader-setup.md)) but produce richer, more cinematic effects. CSS transitions are simpler to set up. Choose based on the effect you want, not based on which is easier.
**Both are first-class options.** Shaders are provided by the `@hyperframes/shader-transitions` package — import from the package instead of writing raw GLSL. CSS transitions are simpler to set up. Choose based on the effect you want, not based on which is easier.
When a composition uses shader transitions, ALL transitions in that composition should be shader-based (the WebGL canvas replaces DOM-based scene switching). Don't mix CSS and shader transitions in the same composition.
@@ -103,7 +103,7 @@ Shader transitions capture DOM scenes to WebGL textures via html2canvas. The can
3. **No CSS variables (`var()`) on elements visible during capture.** html2canvas doesn't reliably resolve custom properties. Use literal color values in inline styles.
4. **Mark uncapturable decorative elements with `data-no-capture`.** The capture function skips these. They're present on the live DOM but absent from the shader texture. Use for elements that can't follow the rules above.
5. **No gradient opacity below 0.15.** Gradient elements below 10% opacity render differently in canvas vs CSS. Increase to 0.15+ or use a solid color at equivalent brightness.
6. **Every `.scene` div must have explicit `background-color`, AND set `BG_COLOR` in the shader setup to the same value.** html2canvas captures the scene element, not the body. Both the CSS `background-color` on `.scene` and the `backgroundColor` option in `html2canvas()` must be set to the composition's background color. Without either, the texture renders as black.
6. **Every `.scene` div must have explicit `background-color`, AND pass the same color as `bgColor` in the `init()` config.** The package captures scene elements via html2canvas. Both the CSS `background-color` on `.scene` and the `bgColor` config must match. Without either, the texture renders as black.
These rules only apply to shader transition compositions. CSS-only compositions have no restrictions.
@@ -28,19 +28,9 @@ These cause real bugs if violated.
**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)
## Shader Transitions
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:** DOM scenes play normally with GSAP animations during holds — canvas is hidden (`display:none`). When a transition starts: capture outgoing scene with full content, capture incoming scene with `.scene-content` hidden (background + decoratives only), show canvas, run shader. When transition ends: hide canvas, show next DOM scene. GSAP entrance animations play on the live DOM. The incoming scene's content is never visible in the shader — it only shows the background layer, preventing un-animated elements from flashing.
**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.)`.
Shader setup, WebGL init, capture, and fragment shaders are handled by `@hyperframes/shader-transitions` (`packages/shader-transitions/`). Read the package source for API details. Compositions using shaders must follow the CSS rules in [transitions.md](../transitions.md) § "Shader-Compatible CSS Rules".
## Scene Template
@@ -124,9 +114,4 @@ All code examples use `old` for the outgoing scene-inner selector and `new` for
## 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 (13 transitions: domain warp, ridged burn, whip pan, SDF iris, ripple waves, gravitational lens, cinematic zoom, chromatic split, glitch, swirl vortex, thermal distortion, cross-warp morph, light leak) | [shader-transitions.md](./shader-transitions.md) |
WebGL shader transitions are provided by `@hyperframes/shader-transitions` (`packages/shader-transitions/`). The package handles setup, capture, WebGL init, render loop, and GSAP integration. Read the package source for available shaders and API — do not copy raw GLSL manually.
@@ -1,282 +0,0 @@
# Shader Transition Setup
Complete boilerplate for WebGL shader transitions in HyperFrames. Copy the setup code, then plug in the fragment shader from the catalog.
**Rendering model:** DOM scenes play normally with GSAP animations. The WebGL canvas is hidden (`display:none`) between transitions. When a transition starts, `beginTrans` uses html2canvas to capture the outgoing scene with full content, and the incoming scene with `.scene-content` hidden (background + decorative elements only). This prevents un-animated content from flashing during the transition. When the transition ends, `endTrans` hides the canvas and reveals the incoming DOM scene — GSAP entrance animations then play on live elements.
**Shader-compatible CSS:** Compositions using shader transitions must follow the rules in transitions.md § "Shader-Compatible CSS Rules" — no `transparent` in gradients, no gradient backgrounds on sub-4px elements, no `var()` on captured elements, `data-no-capture` on uncapturable decoratives.
## 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
```js
var sceneTextures = {};
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);
```
## 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
DOM scenes play normally with GSAP animations during holds. The canvas is only visible during shader transitions — hidden the rest of the time. Capture uses html2canvas (loaded from CDN alongside GSAP).
Add this script tag alongside GSAP:
```html
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
```
```js
// Patch createPattern for html2canvas bug with 0-dimension elements
var _origCP = CanvasRenderingContext2D.prototype.createPattern;
CanvasRenderingContext2D.prototype.createPattern = function (img, rep) {
if (img && (img.width === 0 || img.height === 0)) return null;
return _origCP.call(this, img, rep);
};
function uploadTexture(sceneId, canvas) {
if (!sceneTextures[sceneId]) {
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);
sceneTextures[sceneId] = tex;
}
gl.bindTexture(gl.TEXTURE_2D, sceneTextures[sceneId]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
}
// BG_COLOR must match your composition's background color (e.g. "#0a0a1a").
// html2canvas backgroundColor: null means transparent, which renders as black
// in WebGL textures. Always pass the explicit color.
var BG_COLOR = "#000"; // ← set to your composition's background
function captureScene(sceneEl) {
return html2canvas(sceneEl, {
width: 1920,
height: 1080,
scale: 1,
backgroundColor: BG_COLOR,
logging: false,
ignoreElements: function (el) {
return el.tagName === "CANVAS" || el.hasAttribute("data-no-capture");
},
});
}
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 trans = {
active: false,
prog: null,
fromId: null,
toId: null,
progress: 0,
};
function beginTrans(prog, fromId, toId) {
if (!gl) return;
var fromScene = document.getElementById(fromId);
var toScene = document.getElementById(toId);
// Capture outgoing scene (DOM stays visible during async capture)
captureScene(fromScene)
.then(function (fromCanvas) {
uploadTexture(fromId, fromCanvas);
// Show incoming scene BEHIND outgoing (z-index -1) for capture
toScene.style.zIndex = "-1";
toScene.style.opacity = "1";
var contentEl = toScene.querySelector(".scene-content");
if (contentEl) contentEl.style.visibility = "hidden";
// Wait 2 rAFs for browser to render with correct fonts
return new Promise(function (resolve) {
requestAnimationFrame(function () {
requestAnimationFrame(function () {
captureScene(toScene).then(function (toCanvas) {
if (contentEl) contentEl.style.visibility = "";
toScene.style.opacity = "0";
toScene.style.zIndex = "";
uploadTexture(toId, toCanvas);
resolve();
});
});
});
});
})
.then(function () {
// Both textures ready — swap DOM for canvas
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
glCanvas.style.display = "block";
trans.prog = prog;
trans.fromId = fromId;
trans.toId = toId;
trans.progress = 0;
trans.active = true;
});
}
function updateTrans() {
if (!trans.active || !gl) return;
renderShader(trans.prog, sceneTextures[trans.fromId], sceneTextures[trans.toId], trans.progress);
}
function endTrans(showId) {
trans.active = false;
glCanvas.style.display = "none";
document.getElementById(showId).style.opacity = "1";
}
```
## GSAP Timeline Integration
Scene 1 starts visible on the DOM. GSAP animates elements normally. The canvas is hidden until a transition begins. After each transition, the canvas hides and the next scene's DOM takes over.
```js
// Canvas starts hidden — DOM scene 1 is visible
glCanvas.style.display = "none";
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
updateTrans();
},
});
// Scene 1 entrance animations go here (normal GSAP on DOM)...
// Transition 1→2:
tl.call(
function () {
beginTrans(myShaderProg, "scene1", "scene2");
},
null,
T,
);
var tw1 = { p: 0 };
tl.to(
tw1,
{
p: 1,
duration: DUR,
ease: "power2.inOut",
onUpdate: function () {
trans.progress = tw1.p;
},
},
T,
);
tl.call(
function () {
endTrans("scene2");
},
null,
T + DUR,
);
// Scene 2 entrance animations go here (normal GSAP on DOM)...
window.__timelines["main"] = tl;
```
@@ -1,329 +0,0 @@
# 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.);
}
```