ScrollRenderer · TransformFeedback · Scroll Image
Scroll Image.
The Scroll Renderer recipe provides a framework for scroll-linked
WebGL effects on arbitrary DOM elements. It comes with a couple of
different classes to link to WebGL drawables:
ScrollScene forms the base layer, linking arbitrary
DOM elements to WebGL effects, and ScrollImage links
individual images to scroll-reactive WebGL effects.
It controls a single WebGL context and render loop, allowing for complex layered effects with multiple scroll-linked elements without the overhead of multiple contexts or loops.
In this demo, we've linked the background shader to a
ScrollScene anchored to a fixed full-viewport
element, and each of the four foreground images to individual
ScrollImages. The scroll position of each image is
tracked separately via onBeforeRender, allowing for
independent parallax and distortion effects.
Hit the "WEBGL" toggle, top-left to switch between WebGL and DOM views so you can see how the image positioning informs the shader layout.
01 · How to
Example.
The WebGL program for each image is a simple mesh shader with transform feedback, allowing the vertex shader to update the position and velocity of each vertex on the fly. Scroll-linked distortion and mouse interaction are added as uniform offsets to the vertex positions, and a noise function is used to create an organic erosion effect around the edges.
const renderer = new ScrollRenderer()
document.body.appendChild(renderer.canvas)
document.querySelectorAll('.blade-img').forEach((imgEl) => {
const drawable = new Drawable(gl)
const scene = new ScrollImage({ gl, element: imgEl, scene: drawable })
const program = new Program(gl, {
vertex: imageVert,
fragment: imageFrag,
uniforms: { ...scene.uniforms },
transparent: true,
})
new Mesh(gl, {
geometry: new Plane(gl, { widthSegments: 24, heightSegments: 18 }),
program,
}).setParent(drawable)
renderer.addScene(scene)
})
renderer.playing = true
02 · ScrollRenderer
One canvas. Many scenes.
ScrollRenderer manages a single fixed
<canvas> and one
requestAnimationFrame loop for all registered scenes.
Each scene is scissor-tested to its DOM element's exact pixel
bounds, so multiple WebGL effects share one context with no
overdraw outside their element.
Scenes outside the viewport are skipped automatically via
IntersectionObserver. The aurora background is a
ScrollScene anchored to a fixed full-viewport
element. Which shows off the fact that scroll renderer just knows
where to draw things.
03 · Uniforms
Wired up automatically.
ScrollScene maintains a set of uniforms that are
updated every frame and ready to spread directly into your
Program: u_time (elapsed),
u_resolution (element size in physical pixels), and
u_origin (element position in both pixel and NDC
space). ScrollImage adds u_image and
u_imageSize for the GPU-uploaded texture.
Custom uniforms - like the scroll velocity and mouse position used
for the cloth simulation here - are created with
Uniform and merged in alongside them. Update
.value each frame inside onBeforeRender
and the program picks up the change automatically.
const u_mouse = new Uniform({
name: 'u_mouse', value: [0.5, 0.5], kind: 'float_vec2',
})
const program = new Program(gl, {
uniforms: {
...scene.uniforms, // u_time, u_resolution, u_origin,
// u_image, u_imageSize
u_mouse,
},
})
scene.onBeforeRender = () => {
u_mouse.value = mouse
}
04 · Another image
We had a fourth image.
It needed to go somewhere. Here it is. It looks great. The cloth effect works on it too.
There's no technical insight here. We just didn't want to waste it.