01 12

wgpu · webgpu · wgsl

This is not

a website.

It is a single breath of scroll. Everything below is text, one color, and a shader accent — nothing more. No buttons. No menus. No cards. Nothing to click.

text— color— shader— accent— text— color— shader— accent—
03 / wgpu the specification

wgpu is a specification, not a renderer.

It defines a contract between a host language and a GPU backend — Vulkan, Metal, D3D12, or a software fallback. WebGPU is its browser implementation. WGSL is the language it speaks. Everything else — three.js, curtains, points, slug — is a layer on top.

Three names, one pipeline. The confusion is historical: wgpu existed in Rust before WebGPU existed in browsers. WGSL was designed after. They were never the same thing.

04 / three names

The same abstraction, described three times.

wgpu

Rust. The reference implementation of the WebGPU specification. Compiles to native, compiles to wasm. Used by Firefox for WebGPU. Not a browser API.

WebGPU

JavaScript. The W3C standard. Shipped in Chrome 113, Safari 17, Firefox 141. Exposes command encoders, pipelines, buffers, and queues to script.

WGSL

The shading language. Text-based, typed, no preprocessor. Written in files that end in .wgsl. Compiles to SPIR-V, MSL, or HLSL at runtime.

01 / ENCODE CommandEncoder Every frame begins here. You record commands into a buffer. Nothing executes yet. The encoder is a script you write, not a script that runs.
02 / PASS RenderPass Begin. Bind a pipeline. Bind a bind group. Draw. End. Everything between beginRenderPass and end is scoped — no state leaks out.
03 / VERTEX Vertex stage Position, UV, normal, color. For a fullscreen pass, this is a triangle large enough to cover the viewport. Three vertices. Always three.
04 / FRAGMENT Fragment stage Once per pixel. This is where the abstraction lives — noise, warps, interference, the entire visual language, computed in parallel across every pixel of the canvas.
05 / SUBMIT Queue.submit The encoder is finished. Its command buffer is pushed to the queue. The GPU begins work. The CPU moves on. This is the seam between two worlds.
06 / PRESENT Present Swap the texture to the compositor. The browser draws it. The frame is done. Sixteen milliseconds from here to the next one, or the illusion breaks.

06 / the stack

Eleven libraries. One job each.

01gsaptimeline orchestration
02scrolltriggerscroll → animation binding
03lenissmooth scroll input
04pointswebgpu render pass
05gpu-curtainsdom ↔ gpu sync
06slug-webgpuvector text on gpu
07split-typetext segmentation
08wgpu-matrixmatrix math
09text-shaperglyph shaping
10stats.jsframe monitoring
11resizeobserverdpr-aware sizing

On the GPU, text is not an image. It is a set of quadratic Bézier curves — one per glyph — evaluated per fragment. No texture atlas. No bitmap resolution. No memory cost that scales with font size.

The Slug algorithm, published by Eric Lengyel in 2017, is the reference. It reconstructs a curve inside the fragment shader from a compact band-based lookup, then evaluates winding order to decide coverage. It is the reason vector text can be rendered at any scale without blur.

08 / frame budget

TARGET FRAME TIME
0ms
REFRESH RATE
0fps

Within those sixteen milliseconds: Lenis reads the input, GSAP ticks, ScrollTrigger recalculates, the shader uniforms update, the command encoder records, the queue submits, the compositor presents. Miss the window and the frame drops. Drop enough and the illusion of smoothness dies.

09 / synchronization

Two worlds, one frame.

DOM

The source of truth for layout. Where text lives. Where the browser decides line breaks, kerning, and accessibility. Read via getBoundingClientRect once per frame — never per scroll event.

WebGPU

The source of truth for color. Where the shader runs, once per pixel, in parallel. Written via uniform buffer — a small typed array uploaded to the GPU each frame with scroll, time, and viewport.

The seam between them is a single requestAnimationFrame callback. Lenis feeds its RAF into gsap.ticker, ScrollTrigger reads the position, the shader reads the scroll value. One loop, one frame, two truths.

10 / velocity

Scroll faster. The shader responds.

Lenis exposes its scroll delta. We pass it to the shader as an unsigned float. Higher velocity means more displacement in the fragment field — the noise stretches along the direction of motion. Stop scrolling, and the field settles into a slow drift.

A frame is not a moment. It is a breath — sixteen milliseconds in, sixteen out, and the space between them is where everything happens.

— on the nature of the render loop

12 / end

One breath.

The page ends where it began — a black field, one color, a shader drifting behind the text. Nothing to subscribe to. Nothing to click. Just text, and the space it moves through.

Hosted & Compiled with HtmlRunner.com