LAYER 0 · WebGL film — position: fixed, z-index: 0
SCROLLFILMSCENE 1 / 4-- fps
A scroll-driven 3D film
SCROLL FILM
The film runs under the page as you scroll. Every word on top of it is real HTML.
↓ scroll to play
Chapter one
Every word is real HTML.
There is no video file here. Behind this text a live 3D scene renders sixty times a second — and this headline is still a genuine <h2>. Select it. Search it. Translate it. A film you can read.
Chapter two
Scroll is the playhead.
Your scrollbar is the timeline scrubber. Drag it fast and the film jumps; drag it slow and every frame breathes. The page never autoplays — you are the projectionist.
Chapter three
20 fps in. 40 fps out.
Scroll events arrive janky — about 20 per second on a busy laptop. The render loop interpolates toward the target every frame instead of snapping to it, so motion stays smooth even when your input isn't. Watch the live frame counter, bottom right.
Final scene
THE END
Now watch this exact site get reverse-engineered.
↓ the blueprint
Reverse-engineered
How this exact page works.
Four tricks. No video files, no frameworks, no build step. Open your devtools and check — the canvas never moves, only the camera inside it does.
01 A fixed canvas, a very tall page
The 3D film is a <canvas> pinned with position: fixed. The page body is ~700 viewport-heights tall, so scrolling gives the film a long timeline to scrub.
This is the 20→40 fps trick. Scroll handlers fire irregularly, so the render loop eases the rendered position toward the target every frame, with frame-rate-independent damping. Janky input in, buttery motion out.
function frame() {
requestAnimationFrame(frame);
const dt = Math.min(clock.getDelta(), 0.1);
const k = 1 - Math.exp(-dt * 5.5); // damping
smooth += (target - smooth) * k; // interpolate
render(smooth); // draw the film
}
04 Real HTML on top
Chapters are ordinary sections in normal document flow. Each one fades in only while the playhead is inside its range — the film and the words stay in sync because they read the same number.
document.querySelectorAll('.chapter').forEach(el => {
const a = +el.dataset.from, b = +el.dataset.to;
el.classList.toggle('is-active', smooth > a && smooth < b);
});
The lab — poke the machine
Two instruments, live on this page:
Live render rate: -- fps
X-ray outlines the fixed WebGL layer and the HTML layer so you can see the trick with your own eyes. If the frame rate dips, the page automatically lowers its render resolution to protect smoothness — that's the adaptive-quality half of the 40 fps promise.
Why it holds up on a normal laptop
One scene, zero video. Procedural geometry — no 4K video decode, no giant downloads.
Interpolated playhead. Input sampled at ~20 Hz, rendered at display rate. Damping hides the gaps.
Adaptive resolution. Pixel ratio steps down if fps drops, and back up when there's headroom.
Cheap materials. Additive points and wireframes — no post-processing pipeline, no shadows.
Respects reduced motion. With prefers-reduced-motion the film renders one still frame.
WebGL isn't available — the film can't play, but every word above is still here, because it's real HTML.