From our sponsor: Chromatic - Visual testing for Storybook, Playwright & Cypress. Catch UI bugs before your users do.
This tutorial is going to demonstrate how to create a smooth WebGL transition on scroll using Phenomenon (based on three.js).
If you are not familiar, I highly recommend checking out the official documentation and examples .
Let’s get started
Interactive elements on websites can enhance the user experience a lot. In the demo, a mix of WebGL and regular UI elements will transition based on the scroll position.
The following libraries are used for this demo:
- Three.js: Provides the structure for everything in the WebGL environment.
- THREE.Phenomenon: Makes it easy to create an instanced mesh that can be transitioned smoothly.
- updateOnScroll: Observes scroll position changes based on percentage values.
About Phenomenon
Phenomenon is a small wrapper around three.js built for high-performance WebGL experiences. It started out as a no-dependency library that I created to learn more about WebGL and was later made compatible with the powerful features of three.js.
With Phenomenon it’s possible to transition thousands of objects in 3D space in a smooth way. This is done by combining all the separate objects as one. The objects will share the same logic but can move or scale or look different based on unique attributes. To make the experience as smooth as possible it’s important to make it run almost entirely on the GPU. This technique will be explained further below.
Animate an instance
To create the animated instances in the demo there are a few steps we need to go through.
Provide base properties
Define what Geometry to multiply:
const geometry = new THREE.IcosahedronGeometry(1, 0);
Define how many of objects we want to combine:
const multiplier = 200;
Define what Material it should have:
const material = new THREE.MeshPhongMaterial({
color: '#448aff',
emissive: '#448aff',
specular: '#efefef',
shininess: 20,
flatShading: true,
});
Here we only define the behavior for a single instance. To add to this experience you can add more objects, add lights and shadow or even post processing. Have a look at the Three.js documentation for more information.
Build the transition
The transition of the instance is a little more complex as we will write a vertex shader that will later be combined with our base properties. For this example, we’ll start by moving the objects from point A to point B.
We can define these points through attributes which are stored directly on the GPU (for every object) and can be accessed from our program. In Phenomenon these attributes are defined with a name so we can use it in our shader and a data function which can provide a unique value for every object.
The code below will define a start- and end position between -10 and 10 for every instance randomly.
function r(v) {
return -v + Math.random() * v * 2;
}
const attributes = [
{
name: 'aPositionStart',
data: () => [r(10), r(10), r(10)},
size: 3,
},
{
name: 'aPositionEnd',
data: () => [r(10), r(10), r(10)]
size: 3,
},
];
After all of the objects have a unique start and end position we’ll need a progress value to transition between them. This variable is a uniform that is updated at any time from our main script (for example based on scroll or time).
const uniforms = {
progress: {
value: 0,
},
};
Once this is in place the only thing left for us is writing the vertex shader, which isn’t our familiar Javascript syntax, but instead GLSL. We’ll keep it simple for the example to explain the concept, but if you’re interested you can check out the more complex source.
In the vertex shader a `gl_Position` should be set that will define where every point in our 3d space is located. Based on these positions we can also move, rotate, ease or scale every object separately.
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
Below we define our two position attributes, the progress uniform, and the required main function. In this case, we mix the positions together with the progress which will let the instances move around.
attribute vec3 aPositionStart;
attribute vec3 aPositionEnd;
uniform float progress;
void main(){
gl_Position = position + mix(aPositionStart, aPositionEnd, progress);
}
The position value is defined in the core of three.js and is based on the selected geometry.
Transition on scroll
When we put all the above code together and give each instance a slight offset (so they move after each other) we can start updating our progress based on the scroll position.
With the updateOnScroll library we can easily observe the scroll position based on percentage values. In the example below a value of 0 to 1 is returned between 0% and 50% of the total scroll height. By setting the progress uniform to that value our interaction will be connected to the transition in WebGL!
const phenomenon = new THREE.Phenomenon({ ... });
updateOnScroll(0, 0.5, progress => {
phenomenon.uniforms.progress.value = progress;
});
In the demo every instance (and the UI elements in between) have their own scroll handler (based on a single listener).
Next steps
With all of the above combined our experience is off to a great start, but there’s a lot more we can do:
- Add color, lights and custom material
- Add more types user interaction
- Transition with easing
- Transition scale or rotation
- Transition based on noise
Have a look at the WebGL Wonderland collection for multiple experiments showcasing the possibilities!
Conclusion
Learning about WebGL has been an interesting journey for me and I hope this tutorial has inspired you!
Feel free to ask questions or share experiments that you’ve created, thank you for reading! 🙂
Cool demo! I was missing some momentum while scrolling so the animation doesn’t stop abruptly.
Looking really nice. Hope you can remove the horizontal scroll (on the demo), that would make it even greater 🙂