The compositing model
A mix-blend-mode: multiply layer sits above DOM content. White areas become effectively transparent — the underlying content shows through. Grey areas darken the surface. The effect is a monochromatic light-and-shadow mask, not a coloured filter. The feel: sunlight filtering through foliage.
Light-mode override
Dappled light reads best on warm light-to-mid-tone surfaces, so this page overrides the site-wide dark theme. A warm parchment base lets the shadow pattern feel natural rather than synthetic. On pure white, the shadows are crispest but can feel a little stark.
Two approaches, one gobo
Shadow projection gives each panel its own ::after pseudo-element with background-attachment: fixed. All panels reference the same gobo texture locked to viewport coordinates — a coherent light field that drifts across everything.
Gobo element places a single position: fixed div above the page. Full animation freedom, but flat — no per-element depth. Toggle between them with the controls below.
The gobo texture
The shadow pattern comes from a single photographic gobo image provided to us by Gemini. Simply a WebP of organic branch-and-foliage shapes. Desaturated, contrast-pushed, and slightly brightened before compositing. The grain and natural edge softness are already in the texture; no additional blur pass is needed.
The transform constraint
The shadow-projection approach cannot use CSS transform on the ::after pseudo-elements — it pulls them into a new compositing layer, breaking background-attachment: fixed alignment and destroying the shared light field. Panels would drift out of sync.
Animation without transforms
Since transforms are off-limits, animation happens through background-position keyframe drift (moves the gobo slowly across the viewport) and an opacity pulse (breathes the shadow intensity). Both loop seamlessly with matching start/end keyframes. A separate filter: blur() animation could add extra level of dynamism, but this creates issues around edges as the blur radius changes.
What we learned
The original plan was a WebGL fragment shader: procedural domain-warped fBM noise generating the mask. But the CSS approaches turned out to be simpler, lighter, and already quite convincing. The gobo image has organic grain that pure noise would need extra work to match. The effect reads best on light to mid-tone surfaces; on black, it dissolves into near-invisibility.
The video gobo
A video gobo source is available but not enabled by default — the <video> element already sits in the gobo container ready to swap in. An MP4 of drifting shadows would animate organically without any CSS keyframes at all, at the cost of a larger asset. The image-based approach with keyframe drift gives comparable results for kilobytes.