Quirebound
Free

Aether — Scroll Film Landing Page

A full landing page whose hero video is scrubbed by the scrollbar and rendered through a WebGL shader — scroll velocity bends the lens and splits the colour channels, four chapters cross-fade over one unbroken shot, and the whole thing falls back to a plain looping video where WebGL isn't available.

Aether — Scroll Film Landing Page

The prompt — 8 sections

Film engine

Build a scroll-driven film component for a Next.js App Router project using
three.js. Call it `ScrollFilm`.
Structure: a section `length` viewports tall (default 5) containing a
`sticky top-0 h-svh` stage. Inside the stage, in order: a muted, `playsInline`,
`preload="auto"` `<video>` filling the frame with `object-cover`; a `<canvas>`
absolutely positioned over it; an overlay slot; then the children.
Drive it from a single `requestAnimationFrame` loop:
- Compute raw progress as `-rect.top / (rect.height - innerHeight)`, clamped to
0–1.
- Smooth it: `smooth += (raw - smooth) * 0.11`. The lag is the point — a
trackpad flick should read as a camera move, not a jump cut.
- Derive signed velocity from the frame-to-frame delta and smooth that too.
- Write `--film-p` and `--film-v` onto the host element as CSS custom
properties.
- Set `video.currentTime = smooth * video.duration`, but skip the write when
the difference is under 1/50s — sub-frame seeks are wasted work and make
Safari drop frames.
Never put progress in React state. The animation must cause zero re-renders.
Call `video.play().then(() => video.pause())` once on mount: a paused video
that has never played may only fetch its first fragment, which makes scrubbing
stutter.

Shader surface

Render the video onto a full-bleed WebGL quad instead of showing the `<video>`
directly, so the frame can react to input.
Import `three` inside the effect, not at module scope, so it stays out of the
initial bundle. Build an orthographic-free setup: a `PlaneGeometry(2, 2)`, a
bare `Camera`, and a vertex shader that writes `gl_Position = vec4(position.xy,
0, 1)`. Use a `VideoTexture` with `LinearFilter`, no mipmaps, and set
`texture.needsUpdate = true` every frame — a paused, scrubbed video never fires
the events `VideoTexture` listens for.
Uniforms: `uTex`, `uCanvas`, `uMedia`, `uTime`, `uProgress`, `uVelocity`,
`uReveal`, `uGrain`, `uTint`, `uPointer`.
The fragment shader, in order:
1. **Cover fit.** Compare canvas and media aspect, scale UVs about the centre
so the video fills without stretching.
2. **Barrel warp.** `1.0 + 0.11 * abs(velocity) * dot(c, c)` — the lens swells
as you move, and breathes very slightly at rest.
3. **Liquid drift.** Displace UVs by a 4-octave value-noise fbm, scrolling
slowly on `uTime` and offset by `uProgress`. Amplitude `0.005 + av * 0.045`.
4. **Pointer lens.** Push UVs radially by `0.014 * exp(-distance(uv, uPointer)
* 7.0)`.
5. **Chromatic split.** Sample R, G and B along the radial vector at
`0.0014 + av * 0.028`. Invisible at rest, violent under a flick.
6. **Tear.** Past `abs(velocity) > 0.5`, for a hashed 1.6% of scanlines,
mix in a copy of the frame offset horizontally by `0.055 * sign(velocity)`.
7. **Grade.** Lift shadows toward `uTint` by `(1 - smoothstep(0, 0.38, lum)) *
0.18`, then `(col - 0.5) * 1.09 + 0.5`.
8. **Vignette**, then a noise dissolve gated on `uReveal`, then per-device-pixel
grain.
Report ready only after the first frame is genuinely painted, then cross-fade
the underlying `<video>` to `opacity-0`. Handing over any earlier flashes black.

Chapters

Chapters are absolutely positioned over the sticky stage and faded entirely in
CSS, reading the `--film-p` the loop writes.
For a chapter living between `enter` and `exit` on the 0–1 timeline:
```
opacity: clamp(0, min(calc((var(--film-p) - ENTER) / FADE),
calc((EXIT - var(--film-p)) / FADE)), 1);
transform: translate3d(0, calc((var(--film-p) - MID) * DRIFT), 0)
scale(calc(1 + (var(--film-p) - MID) * ZOOM));
```
with `FADE` around 0.055 and `MID` the midpoint.
Separate consecutive ranges by about `FADE`, do **not** overlap them. Two
display headlines both sitting at half opacity reads as a rendering bug; a
short beat of bare film between them reads as a dissolve. Ranges of
`[-0.08, 0.25] [0.30, 0.50] [0.55, 0.75] [0.80, 1.08]` give four acts and three
clean handovers.
Give the first chapter a negative `enter` (about -0.08) and the last an `exit`
past 1, or they will be invisible at the ends of the track.

Page

Four acts over one unbroken shot, sharing a single 1240px grid:
1. **Hero** — eyebrow rule + label, a two-line display headline at
`clamp(3.4rem, 10vw, 9rem)` with `leading-[0.86]` and `-0.045em` tracking,
one line of body copy, and a circled ↓ reading "Scroll to play".
2. **Silence** — a headline about removing things, a paragraph, and three
statistics in a bordered three-column row.
3. **Chamber** — headline plus a definition list of five specification rows,
each a mono term left and a value right, separated by hairlines.
4. **Order** — the product name at display size, availability copy, a solid
accent pill and a bordered ghost pill.
Over all four: a fixed `mix-blend-difference` masthead, a top-and-bottom
darkening gradient so type never fights the footage, and a hairline scrubber
pinned to the bottom with a tick per chapter.

Masked type

Below the film, show the footage only through the letterforms of a single huge
word.
Wrap the section in `isolate`. Put an ambient film surface at `inset-0`, then
over it a solid black block carrying `mix-blend-multiply` with the word in
white at `clamp(4rem, 20vw, 17rem)`, weight 800, `-0.05em` tracking. White
passes the backdrop through; black crushes it. No SVG mask and no
`background-clip`, so it works with a webfont, a canvas and a `<video>` alike.
Brighten the backdrop (around `1.7`) — dark footage disappears inside the
letters.

Rest of page

- A hairline ticker: two identical copies of the item list in a `w-max` flex
track, each padded to its own full width, animated `translate3d(-50%, 0, 0)`
over 34s linear infinite.
- Three pillar cards in a `gap-px` grid over a hairline-coloured background, so
the gaps read as rules. Each card gets an accent hairline that scales in from
the left on hover.
- A closing band at `70svh` with a second ambient film, a heavy gradient, one
headline and one call to action.
- A four-column footer, hairline separated.

Palette and type

Pure black canvas. Ink `#EDEAE4`, muted `#8A8781`, accent `#F2C17A`, hairlines
`rgba(237,234,228,0.14)`. Tint the shader toward a cool `#5C7AB0` so the warm
accent stays the only warm thing on the page.
Syne 600–800 for display, Inter Tight for body, JetBrains Mono for every
uppercase label at `0.2em`–0.32em tracking. Labels are always 10–11px.

Degradation

Three fallbacks, all required:
- **No WebGL, or `three` fails to import.** Never report ready; the plain
`<video>` underneath stays visible and the chapters still fade on scroll.
- **No footage at all.** Run a second fragment shader that generates the shot:
fold screen space into tunnel coordinates (`atan(c.y, c.x)` around,
`0.42 / length(c)` forward), push the forward term with `uProgress`, texture
the walls with a 5-octave ridged fbm, add angular blades of light, an
exponential bloom at the vanishing point that grows with progress, hashed
dust motes, and a mirrored, rippling lower third.
- **`prefers-reduced-motion: reduce`.** Drop the sticky stage entirely, stack
every chapter in normal flow at full opacity, and put a single looping video
behind the page at low opacity.

More like this