wtc-gl - recipe

One canvas.
Every scene.

Anchor independent WebGL programs to any DOM element. A single fixed canvas, scissor-tested viewports, one shared render loop. ScrollRenderer is a WebGL scene manager for inline WebGL effects that scale.

npm install wtc-gl View on GitHub
How it works

No iframes. No per-element canvases. Just one shared GL context.

Each ScrollScene tracks a DOM element via getBoundingClientRect() every frame. WebGL's scissor test clips each render to that element's exact pixel bounds. Scenes outside the viewport are culled automatically by IntersectionObserver.

DOM anchored

Every scene knows its element. u_origin and u_resolution update each frame automatically, with no event wiring required.

Scissor-tested viewports

The rings above are centred on this card's exact position in the canvas using u_origin.xy and gl_FragCoord, demonstrating element-relative fragment math.

Single render loop

One requestAnimationFrame drives all scenes. The shared GL context means zero per-canvas overhead, no synchronisation, and no scheduling jank between effects.

The problem

Multiple WebGL programs don't scale.

The traditional answer to "WebGL effects on multiple page elements" is one <canvas> per effect - a separate GPU context for each. Browsers cap active WebGL contexts. Each one competes for GPU memory, initialises its own GL state, and drives its own render loop with zero coordination between them.

In addition to CPU, GPU and memory load, you also run into issues with browswers dropping contexts in order to free up resources, causing your effects to disappear without warning. This requires you to create complex context-management solutions that are fragile and error-prone.

ScrollRenderer is designed to solve these problems with a single shared context and one coordinated render loop.

Composited layers

Your markup and your GL, in the same stack.

A single fixed <canvas> sits on top of your page. ScrollRenderer scissor-clips each scene to its DOM element, so WebGL effects land exactly where your HTML does

Scroll to see how the layers relate: the HTML document lays the page out, including the elements that serve as placeholders for the WebGL programs which are composed into the shared, full-screen canvas that renders everything in place via a scroll-renderer coordinator..

API

Three lines to anchor a shader to any element.

const renderer = new ScrollRenderer()
const scene    = new ScrollScene({ element, scene: drawable })
renderer.addScene(scene)

// Uniforms updated automatically every frame:
// u_time        float  - elapsed time
// u_resolution  vec2   - element size in physical pixels
// u_origin      vec4   - .xy pixel origin (gl_FragCoord space)
//                         .zw element centre in canvas NDC

Extend with onBeforeRender and onAfterRender hooks, or opt out of per-scene clearing and viewport locking independently.

Transform feedback

GPU simulations alongside fragment shaders.

The TransformFeedback class handles ping-pong buffers automatically. Complex particle systems run in the shared context alongside simple fragment-shader scenes with no extra overhead.

  • Curl-noise velocity field, GPU-side
  • Per-particle life, respawn, and seed
  • Composites over adjacent scenes via clearOnRender: false
  • Positioned with u_origin.zw when useViewport is false