2026, solo, end to endNext.js · React Three Fiber · GLSL · WebAudio
Éclosion
A 3D web experience where scrolling is time: eight acts, from the Void to the Dawn, travelled with the wheel, from a seed's first beat to the world's first sunrise.
- Next.js
- React Three Fiber
- GLSL
- WebAudio
One hundred per cent procedural.
No 3D models, no textures, no audio files, no external requests at runtime. Geometry, materials (noise and FBM in GLSL), over a hundred thousand particles and the music itself (pure WebAudio synthesis) are produced by the code. One deliberate exception: the typeface, committed to the repository.
Roughly 11,600 lines across 195 files. Next.js 15, React 19, strict TypeScript, Three.js and React Three Fiber, GSAP ScrollTrigger, Lenis, zustand, WebAudio.

Technical decisions
Three decisions that shape everything
The scroll contract: zero React renders at 60 frames per second
One smoothing layer (Lenis), one master ScrollTrigger scrubbing a GSAP timeline of duration 1, and a registry of mutable uniforms that the tweens write and the renderer reads every frame, never through a React render. React only wakes up on act changes. The DOM text follows through a single MotionValue, also outside React.
One world, one truth
An analytic function groundHeight(x, z) generates the visible terrain, constrains walking in free-roam, bounces the physics and bounds the birds' flight. What you see is exactly what you touch. Same logic for the camera: a single continuous CatmullRom curve, each act travelling to the first point of the next. No cut across the whole film.
Stateless particles
Every behaviour (rain, curl-noise fireflies, embers, the final vortex) is a closed function of (seed, time) evaluated in the vertex shader. No simulation to maintain, no GPU round trip. Buffers are allocated once at the highest quality level; changing tier only moves a setDrawRange.
Measurements
The engineering, in numbers
| Measurement | Before | After |
|---|---|---|
| First-visit payload | 422 Kio | 342 Kio |
| Worst frame at act boundaries | 617 ms | 167 ms |
| Forest trees, at equal frame cost | 420 | 630 |
| Lighthouse : accessibility, best practices, SEO | 100 / 100 / 100 |
The 19%
The italic font, 80 KB preloaded at high priority, was rendered nowhere. A single occurrence of the word "italic" in the entire codebase: its own declaration.
The 617 ms
Three stalls stacked at every act boundary, untangled with the CPU profiler and GL hooks placed on linkProgram:
- the cache-key trap: post-processing renders the scene under its own renderer state, so all shader warm-up was compiling programs the real render never used, and the first visible frame relinked everything;
- the link wait: an act would draw as soon as the camera swept it, ready or not;
- the driver's deferred reflection: even linked, a program pays for its first introspection, about 180 ms each on an integrated GPU.
The answer: each act mounts invisible, compiles under the composer's state, gets one program introspected per frame, then reveals itself, all of it covered by the scene's fog. No visual change, smooth boundaries.
Nothing gets fixed without being seen.
A CDP harness drives a real Chrome: enter the experience, jump to a precise beat, capture, before and after, for every rendering change. Speed-linked effects have to be captured mid-scroll; a still capture shows nothing. The GPU tier is forced by injecting the renderer string.
The sun that wouldn't ignite
The dawn disc rendered grey. Exponential fog was eating 99.5% of it at that distance, and its luminance ceiling stayed below the bloom threshold: "bright enough to bloom", said the comment; it had never been true. The fix: the sun ignores the fog (the moon re-emulates it identically in JavaScript) and an ignition curve clears the threshold: ember below the horizon, gold at grazing, cream at the zenith.
The ocean's lines
One symptom, "white streaks", turned out to be five stacked defects, each masking the next: motion-blur ghosting, light-shaft edges, the night sky dome visible underwater, the Gerstner sheet's edge, an overlit floor. The method that finally won: mask components one by one to isolate the source before touching a single shader.




Quality and accessibility
Holding quality over time
- Continuous integration on every push: strict linting (the
anytype is banned), strict TypeScript, 16 unit tests (act tiling, seeded pseudo-random generator, analytic ground, FR/EN text parity), build. - One end-to-end test plays the whole film (boot, entry, every act, the emergence from the water, the fallback, in both languages) and requires zero console errors. It caught a real race the day it went live: a dispose during a shader link.
- Without WebGL2: a fallback page serves the complete narrative as server-rendered HTML, without JavaScript, in both languages.
- Reduced motion: native scrolling, particles at 4%, no shake, sound off.
aria-liveannouncements on each act, full keyboard navigation, visible focus throughout.