WebGL Enhanced Drag Slider Tutorial With Curtains.js — Part 2

Martin Laxenaire
Jul 18, 2019 · 7 min read
Image for post
Image for post

This article is the second part of a tutorial. In the first part, we created a JavaScript drag slider. We are now going to write the WebGL part.

Here’s what you’ll end up with:

As a reminder, we’ll be using curtains.js to add everything related to WebGL.

curtains.js is an open-source Vanilla JavaScript library. It will be particularly useful here as its main purpose is to enhance DOM elements with WebGL effects.

With a few lines of JavaScript, you’ll be able to create WebGL textured planes, bound to our slider item’s HTML elements and then post-process the whole scene.

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.

Even though we’re going to see how this works step-by-step, with a fully commented code, you may want to learn a bit more about WebGL and shaders if you’re unfamiliar with those concepts.

You may also want to look at the curtains.js API documentation or check its GitHub repo.

We’re also going to use anime.js as a tweening engine for our animations.

Part 1 — HTML and CSS


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.

We will also add our libraries and the main JavaScript file just before the body closing tag.


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 ID attributes — we’ll use them later in our JavaScript.


Plane vertex shader

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

Post Processing

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:

Image for post
Image for post

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 spreadFromMouse float varying from zero to one based on our uDragEffect uniform 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.
Image for post
Image for post

Post-processing vertex shader

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

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 if and 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.


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 setupPlanes and setupShaderPass methods in our init function. We are going to code them right now.

Adding the Planes

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

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 loadImage.

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

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.

However, the library can’t know when you’re moving your planes, via CSS or JavaScript, and we are indeed translating their parent div.

We need to tell our planes to update their positions with a simple call to the updatePosition method. We’ll put that in our onTranslation function.

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.

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store