WebGL Enhanced Drag Slider Tutorial With Curtains.js — Part 2
Here’s what you’ll end up with:
As a reminder, we’ll be using curtains.js to add everything related to WebGL.
The advantages of using curtains.js become apparent:
- Clean and SEO-friendly HTML code.
- You don’t have to worry about your WebGL object’s sizes and positions as most of the stuff (like resize) will be handled under the hood by the library.
- If there’s an error during the WebGL initialization or in your shaders, the slider will still work.
We’re also going to use anime.js as a tweening engine for our animations.
Part 1 — HTML and CSS
To implement WebGL, we’ll have to make a few modifications to the HTML and CSS.
There are not many changes to the HTML.
We will add a
div container that holds the WebGL
canvas and data-sampler attribute on the image tags; it will be used as the texture sampler name in the fragment shader.
As our images will be displayed as WebGL planes in our canvas, we will have to hide the original images.
Each time a plane is created, we’ll add a
loaded class to its parent HTML element to animate the corresponding title’s opacity.
Finally, we will catch errors during our WebGL initialization (or any trouble while compiling the shaders) and add a
no-curtains class to the document body.
Therefore, we need to handle that case in the CSS to display our original images again:
That’s it, nothing difficult here. Now let’s move on to the WebGL!
Part 2 — Shaders
We have two different elements: our planes (which will all use the same shaders) and our shader pass. We then have to write two pairs of shaders.
The shaders will be put inside
<script> tags, just before our body closing tag. Pay attention to their
Plane vertex shader
The plane vertex shader positions our planes relative to their HTML elements, due to the projection and view matrices generated by the library.
It will also pass the new texture coords to our fragment shader. By using the texture matrix uniform to calculate new texture coords, we ensure that the texture will always fit the plane without breaking its natural aspect ratio.
Plane fragment shader
We will map our texture to the texture coords we’ve just passed and animate its opacity based on our
uOpacity uniform, as well as the distance from each pixel to the left edge:
Shader passes use frame buffer objects to render your whole scene to a texture, apply shaders, and then render this texture back to the canvas.
Most of the post-processing effect will happen inside our fragment shader. We are going to use a displacement texture to spice up the overall effect. We’ll use this black-and-white image’s RGB values to calculate how much displacement we’ll apply to each pixel.
We’ll repeat and offset this texture so it looks like it’s following our planes. We then use a pattern image to obtain a seamless effect.
This is the image we’ll use:
Before we’ll have a detailed look at those shaders, let’s decompose what will occur in our shaders:
- Calculate our mouse position relative to texture coords in our vertex shader and pass it as a varying to our fragment shader.
- Calculate a
spreadFromMousefloat varying from zero to one based on our
uDragEffectuniform and the distance from the mouse to the far edges (just like we did for opacity with the planes).
- Apply a kind of fish-eye effect based on
spreadFromMouse(the further from the mouse, the more distortion we’ll get). See figure 1.
- Apply a displacement based on our displacement map RGB values and on
spreadFromMouse(the further from the mouse, the more displacement). See figure 2.
- Apply a grayscale and background-color effect based on
spreadFromMouse. See figure 3.
Post-processing vertex shader
A shader pass doesn’t use projection and model-view matrices because the render texture (which represents what is actually drawn on the canvas) and the canvas always have the same size and position.
For the same reason, you won’t need to use the texture matrix on the render texture coords.
We will, however, need to use our displacement image texture matrix to calculate its accurate texture coords:
Post-processing fragment shader
Here you will find all the steps we defined above.
Some effects depend on the slider direction; we will calculate both effects and choose the right one based on the slider direction.
We could have used
else statements but those tend to decrease performance in GLSL and should be used with caution.
Part 3 — The WebGL
We will extend the
Slider class (be sure to insert the
Slider class code seen in the previous article before moving on.) and use almost the same code structure: constructor, helpers, hooks, set up and destroy methods. The only difference is, for the sake of clarity, we’ll write all the setup functions before the helper and hooks.
Let’s start by creating a new curtains instance. We need to pass the ID of the div that will wrap our canvas as a parameter.
This will silently append a canvas, get our WebGL context, start a
requestAnimationFrame loop, draw our scene, etc. It will return an object that we will use later to add our planes and shader pass.
As you can see, we are calling the
setupShaderPass methods in our init function. We are going to code them right now.
Adding the Planes
To add our planes, we will use the
addPlane method of our curtains object.
This method takes 2 parameters:
- An HTML element that will be bound to the plane. The plane will copy its CSS sizes and positions. On a window-resize event, it will update to the new sizes and dimensions under the hood. It will also automatically create a texture for all images, canvases, and video’s children of that element. In our case, we only have one image.
- A parameter object. This is where we’ll specify the shader’s script IDs and our uniforms.
Once the plane has been created, we will push it into our planes array for later use.
Planes have a convenient
onReady event that fires once all their initial textures have been created — this is where we are going to animate their opacity and add the
loaded class to its parent HTML element.
Adding the Shader Pass
Next up is the shader pass.
Adding a shader pass is easier than adding a plane, due to the
addShaderPass method of our curtain’s object.
It doesn’t need to be bound to an HTML element as it will be bound to our canvas instead. It only needs a parameter object with shader’s script IDs and uniforms.
Once it has been added, we’ll load the displacement image into it using
This method accepts an image’s HTML element as parameter so we first need to create one. There’s no need to listen to the load event of the image, the library will take care of that.
Finally, we will use the
onRender event of the shader pass to continuously offset our texture along the secondary axis.
Using the Hooks
We’ve set up all our WebGL objects. Now we need to bind them to the slider with the hooks we’ve declared in our
Slider class. We’re going to override them with new methods.
Our planes are automatically resized when you resize your browser. That’s because curtains.js knows when a window resize event occurs and can handle the calculation of the new sizes and positions.
We need to tell our planes to update their positions with a simple call to the
updatePosition method. We’ll put that in our
We will also need to update the shader pass mouse position, drag effect and slider direction in our various helper and hook handlers.
We’re almost done.
Finally, we’ll add a way to cleanly destroy the WebGL part of our slider and override our
Slider class initial destroy method:
There you have your awesome WebGL drag slider!
In the last part we’ll see how to improve the performance by removing all unnecessary layout / reflow calls.