A membership site for a living poet, built as a quiet reading room instead of a paywall page. One fixed WebGL backdrop carries the whole identity: pale grey mist, a torn black ink cloud that holds the poems in white, and a soft dark blob that trails the cursor. Content is a calm column of light serif headings, hairline rules and small sans captions.
Rebuild: design-inspiration-rebuilds/culture-education/david-whyte-experience (fictional content, generated images, open-source fonts; installed with the optional design-inspiration-rebuilds skill). Run it with npm install && npm run dev.
Design Tokens
| Role | Value |
|---|---|
| Mist (page, shader base) | #e6e7e1 |
| Mist glow | #f4f4f1 |
| Ink | #0b0b0c |
| Cursor blob | #212121 at up to 50% |
| Hairlines | rgba(0,0,0,.25) |
:root {
--mist: #e6e7e1; --ink: #0b0b0c;
--serif: 'Newsreader', serif; /* original: Canela Text, weight ~250, tracking -.025em */
--sans: 'DM Sans', sans-serif; /* original: Roobert, 12-14px captions */
--ease: cubic-bezier(.22, .61, .36, 1);
}
.blend { color: #fff; mix-blend-mode: difference; } /* dark on mist, white on ink */
- Type: one light serif for everything that is read (display
clamp(3rem, 8.4vw, 8.5rem),line-height: .98,letter-spacing: -.035em; titles ~4.6rem; FAQ questions ~2rem) and a small grotesk for captions and links. No bold anywhere. - Colour: monochrome. No accent colour; the only "colour" is the photography (muted watercolour-like cut-outs with feathered edges).
- Shape: no radii, no shadows, 1px hairlines for lists. Images are masked with radial gradients so they dissolve into the mist like wet paper.
Motion Spec
| What | Trigger | Animation | Duration / easing | Rebuild file |
|---|---|---|---|---|
| Smooth scroll | always | Lenis { lerp: .09 } on the GSAP ticker, lagSmoothing(0) |
— | src/scroll.js |
| Mist + ink + blob backdrop | always | one fragment shader: fbm mist, fbm-eaten rectangle (ink cloud) with white wisps on the rim, cursor-trailing dark blob | continuous | src/fog.js |
| Ink panel per section | section crosses 55% | ink opacity, blob opacity and the rectangle's vertical extent tween toward data-ink, data-blob, data-rect |
1.2-1.4s power2.inOut / power3.out |
src/fog.js |
| Heading reveal | enters at 88% | SplitText line masks, yPercent: 105 → 0 |
1.3s power3.out, stagger .12 |
src/reveals.js |
| Poem reading | scroll | lines go opacity .12 → 1 one after another while the poem block stays sticky in the ink |
scrubbed | src/reveals.js |
| Hero landscape tiles | scroll | cut-out images drift at different speeds | scrubbed parallax | src/reveals.js |
| Cursor ring | pointer move | hairline ring follows with lag, scales 1.6 over links | quickTo .5s power3 |
src/cursor.js |
| FAQ accordion | click | answer opens via grid-template-rows 0fr → 1fr, "View" becomes "Close" |
.8s cubic-bezier(.22,.61,.36,1) |
src/faq.js |
Key excerpts
Ink cloud: rectangle eaten by noise: src/fog.js
vec2 d2 = abs(q - c) - h;
float d = length(max(d2, 0.)) + min(max(d2.x, d2.y), 0.); // box sdf in aspect space
float n = fbm(q * 3.2 + vec2(0., -t * .05));
float edge = d + (n - .42) * .32; // noise displaces the edge
float ink = (1.0 - smoothstep(-.015, .03, edge)) * uInk;
float rim = smoothstep(-.2, .0, edge) * (1. - smoothstep(.0, .05, edge));
float wisp = smoothstep(.52, .72, fbm(q * 5.5 + vec2(t * .04, t * .02)));
vec3 inkCol = mix(vec3(.012), vec3(.93), wisp * rim * .9); // white smoke only near the rim
Section-driven backdrop state: src/fog.js
document.querySelectorAll('[data-ink]').forEach((sec) => {
const [y0, y1] = (sec.dataset.rect || '.25,.78').split(',').map(Number);
const go = () => {
gsap.to(u.uInk, { value: +sec.dataset.ink, duration: 1.2, ease: 'power2.inOut' });
gsap.to(u.uBlob, { value: +sec.dataset.blob, duration: 1.4, ease: 'power2.inOut' });
if (sec.dataset.ink === '1') gsap.to(u.uY.value, { x: y0, y: y1, duration: 1.4, ease: 'power3.out' });
};
ScrollTrigger.create({ trigger: sec, start: 'top 55%', end: 'bottom 55%', onEnter: go, onEnterBack: go });
});
The page content never touches the canvas; markup only declares what the backdrop should look like.
Text that works on mist and ink: src/style.css
#fog { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
.blend { color: #fff; mix-blend-mode: difference; }
The canvas sits at z-index: -1 and main creates no stacking context, otherwise the blend would have no backdrop.
Heading line-mask reveal: src/reveals.js
const split = SplitText.create(el, { type: 'lines', mask: 'lines', linesClass: 'ln' });
gsap.from(split.lines, { yPercent: 105, duration: 1.3, ease: 'power3.out', stagger: 0.12,
scrollTrigger: { trigger: el, start: 'top 88%', once: true } });
Smooth scroll on the GSAP ticker: src/scroll.js
lenis = new Lenis({ lerp: 0.09, smoothWheel: true, autoRaf: false });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.lagSmoothing(0);
gsap.ticker.add((t) => {
if (lenis) lenis.raf(t * 1000);
frameCallbacks.forEach((fn) => fn(t)); // shader renders in the same tick
});
prefers-reduced-motion: Lenis, reveals, parallax and shader drift are off; text is simply visible.
Why Award-Worthy
- One backdrop, no sections-as-slides. A single shader changes state per section, so the page feels like one continuous atmosphere rather than stacked blocks.
- Restraint. Monochrome, one light serif, hairlines. The mood (fog, ink, poetry) is carried by the surface, not by decoration.
- Text and image share one logic. Difference blending makes copy readable on both mist and ink, and feathered cut-out images read as part of the same wet-paper world.
- Slow, consistent motion. Long eases, lagging cursor ring, sticky poem lines that light up as you read. Nothing snaps.
- A paywall that does not feel like one. FAQ and offer are quiet list rows in the same system.
Anti-patterns it avoids: no drop shadows or cards with borders, no accent colour, no loud hover effects, no generic fade-up (headings rise out of masks); the canvas never intercepts input.
Credits
- Original: davidwhyte.com/experience by Immersive Garden. Design and content are the intellectual property of their creators.
- Recognition: Awwwards Site of the Month (Dec 2024), Site of the Day.
- This study is an independent clean-room analysis. The rebuild uses a fictional poet, invented poems, AI-generated images and open-source fonts; it contains no original code, assets, copy or likeness.