Overview

Please note that this tutorial was originally posted on Medium here.

WebGL drag slider gif animation

This article is the second part of our tutorial. In the first part, we’ve created a javascript drag slider. If you haven’t read it yet, go check it here.

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 few lines of javascript, you will be able to create WebGL textured planes bound to our slider items HTML elements and then post-process the whole scene.

We can then easily see the advantages of using curtains.js:

  1. Clean & SEO friendly HTML code.

  2. You don’t have to worry about your WebGL objects sizes and positions as most of the stuff (like resize) will be handled under the hood by the library.

  3. If in any case there’s an error during the WebGL initialization or in your shaders, the slider will still work!

Even tho we’re going to see how it 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 & CSS

In order to implement the WebGL we’ll have to add a few modifications to the HTML and the CSS.

HTML

There’s not many changes in the HTML. We will add a div container that will hold our WebGL canvas and data-sampler attribute on the image tags: it will be used as our texture sampler name in our fragment shader. We will also add our libraries and our main javascript file just before the body closing tag.

  <body>

    <!-- div that will hold our WebGL canvas -->
   <div id="canvas"></div>

   <div id="content">

        <h1 id="title">My awesome slider</h1>

        <!-- drag slider -->
      <div id="planes">

         <div class="plane-wrapper">
            <span class="plane-title">Slide 1</span>
                <div class="plane">
                    <img src="images/slide-1.jpg" data-sampler="planeTexture" alt="First slide" />
                </div>
         </div>

         <div class="plane-wrapper">
            <span class="plane-title">Slide 2</span>
                <div class="plane">
                    <img src="images/slide-2.jpg" data-sampler="planeTexture" alt="Second slide" />
                </div>
         </div>

         <div class="plane-wrapper">
            <span class="plane-title">Slide 3</span>
                <div class="plane">
                    <img src="images/slide-3.jpg" data-sampler="planeTexture" alt="Third slide" />
                </div>
         </div>

         <!-- add more slides if you want... -->

      </div>

        <div id="drag-tip">
            Drag to explore
        </div>

   </div>

   <script src="js/anime.min.js" type="text/javascript"></script>
   <script src="js/curtains.min.js" type="text/javascript"></script>
   <script src="js/slider.setup.js" type="text/javascript"></script>
</body>

CSS

Because our images will be displayed as WebGL planes in our canvas, we will have to hide the original images. Each time a plane will be created, we’ll add a “loaded” class to its parent HTML element to animate the according title opacity. Finally, we will catch errors during our WebGL initialization (or if there’s any trouble while compiling the shaders) and add a “no-curtains” class to the document body. We thus need to handle that case in the CSS to display our original images back.

  @media screen {

    html, body {
        min-height: 100%;
    }

    body {
        margin: 0;
        font-size: 18px;
        font-family: 'Arvo', Verdana, sans-serif;
        background: #ece5d1;
        line-height: 1.4;
        overflow: hidden;
    }

    /*** canvas ***/

    /* our canvas will have the size of our window */
    #canvas {
        position: fixed;
        top: 0;
        right: 0;
        left: 0;
        height: 100vh;
        z-index: 1;
    }

    /*** content ***/

    #content {
        position: relative;
        z-index: 2;
        overflow: hidden;
    }

    #title {
        position: fixed;
        top: 20px;
        right: 20px;
        left: 20px;
        z-index: 1;
        pointer-events: none;
        font-size: 1.5em;
        line-height: 1;
        margin: 0;
        text-transform: uppercase;
        color: #032f4d;
        text-align: center;
    }

    #planes {
        /* width of items * number of items */
        width: calc(((100vw / 1.75) + 10vw) * 7);

        padding: 0 2.5vw;
        height: 100vh;
        display: flex;
        align-items: center;

        cursor: move;
    }

    .plane-wrapper {
        position: relative;

        width: calc(100vw / 1.75);
        height: 70vh;
        margin: auto 5vw;
        text-align: center;
    }

    /* disable pointer events and text selection during drag */
    #planes.dragged .plane-wrapper {
        pointer-events: none;

        -webkit-touch-callout: none;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
    }

    .plane-title {
        position: absolute;
        top: 50%;
        left: 50%;
        z-index: 1;
        transform: translate3D(-50%, -50%, 0);
        font-size: 4vw;
        font-weight: 700;
        line-height: 1.2;
        text-transform: uppercase;
        color: #032f4d;
        text-stroke: 1px white;
        -webkit-text-stroke: 1px white;

        opacity: 0;

        transition: color 0.5s, opacity 0.5s;
    }

    #planes.dragged .plane-title {
        color: transparent;
    }

    .plane-wrapper.loaded .plane-title, .no-curtains .plane-title {
        opacity: 1;
    }

    .plane {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
    }

    .plane img {
        /* hide original images if there's no WebGL error */
        display: none;
        /* prevent original image from dragging */
        pointer-events: none;
        -webkit-user-drag: none;
        -khtml-user-drag: none;
        -moz-user-drag: none;
        -o-user-drag: none;
        user-drag: none;
    }

    #drag-tip {
        position: fixed;
        right: 20px;
        bottom: 20px;
        left: 20px;
        z-index: 1;
        pointer-events: none;
        font-size: 0.9em;
        text-transform: uppercase;
        color: #032f4d;
        text-align: center;
    }


    /*** handling WebGL errors ***/

    .no-curtains #planes {
        transition: background-color 0.5s;
    }

    .no-curtains #planes.dragged {
        background-color: #03879a;
    }

    .no-curtains .plane-title {
        opacity: 1;
    }

    .no-curtains .plane {
        display: flex;
        overflow: hidden;
        transition: filter 0.5s;
    }

    .no-curtains #planes.dragged .plane {
        filter: grayscale(1);
    }

    .no-curtains .plane img {
        display: block;
        min-width: 100%;
        min-height: 100%;
        object-fit: cover;
    }

}




@media screen and (orientation: portrait) {

    #content {
        max-height: 100vh;
    }

    #planes {
        overflow: hidden;
        width: 100vw;

        padding: 2.5vh 0;
        height: auto;
        flex-direction: column;
    }

    .plane-wrapper {
        width: 70vw;
        height: calc(100vh / 1.75);
        margin: 5vw 0;
    }

    .plane-title {
        font-size: 10vw;
    }

}

That’s it, nothing difficult here. Now let’s move on tothe WebGL!

Part 2. Shaders

Before we’ll extend the Slider class to add all the WebGL related javascript, we will write our shaders. We have 2 different elements: our planes (which will all use the same shaders) and our shader pass. We will then have to write 2 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 in our javascript later.

Planes

Plane vertex shader

The planes vertex shader is just positioning our planes relative to their HTML elements thanks 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.

  <script id="slider-planes-vs" type="x-shader/x-vertex">
    #ifdef GL_ES
    precision mediump float;
    #endif

    // default mandatory attributes
    attribute vec3 aVertexPosition;
    attribute vec2 aTextureCoord;

    // those projection and model view matrices are generated by the library
    // it will position and size our plane based on its HTML element CSS values
    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    // this is generated by the library based on the sampler name we provided
    // it will be used to map adjust our texture coords so the texture will fit the plane
    uniform mat4 planeTextureMatrix;

    // texture coord varying that will be passed to our fragment shader
    varying vec2 vTextureCoord;

    void main() {
        // apply our vertex position based on the projection and model view matrices
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

        // varying
        // use texture matrix and original texture coords to generate accurate texture coords
        vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;
    }
</script>

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 and the distance from each pixel to the left edge:

  <script id="slider-planes-fs" type="x-shader/x-fragment">
    #ifdef GL_ES
    precision mediump float;
    #endif

    // our texture coords varying
    varying vec2 vTextureCoord;

    // our texture sampler (see how its name matches the data-sampler attribute on our image tags)
    uniform sampler2D planeTexture;
    // our opacity uniform that goes from 0 to 1
    uniform float uOpacity;

    void main( void ) {
        // map our texture to the varying texture coords
        vec4 finalColor = texture2D(planeTexture, vTextureCoord);

        // the distance from this point to the left edge is a float from 0 to 1
        float distanceToLeft = distance(vec2(0.0, vTextureCoord.y), vTextureCoord);

        // calculate an effect that goes from 0 to 1 depenging on uOpacity and distanceToLeft
        float spreadFromLeft = clamp((uOpacity * (1.0 - distanceToLeft) - 1.0) + uOpacity * 2.0, 0.0, 1.0);

        // handle pre-multiplied alpha on rgb values and use spreadFromLeft as alpha.
        finalColor = vec4(vec3(finalColor.rgb * spreadFromLeft), spreadFromLeft);

        // this is it
        gl_FragColor = finalColor;
    }
</script>

Post processing

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’re going to repeat and offset this texture so it looks like it’s following our planes. We then have to use a pattern image to obtain a seamless effect.

This is the image we’ll use:

The black and white displacement image we'll be using

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 0 to 1 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 farther 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 farther from the mouse, the more displacement). See figure 2.
  • Apply a grayscale and background color effect based on spreadFromMouse. See figure 3.

The 3 main steps of our fragment shaders

Post processing vertex shader

A shader pass don’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.

  <script id="distortion-vs" type="x-shader/x-vertex">
    #ifdef GL_ES
    precision mediump float;
    #endif

    // default mandatory attributes
    attribute vec3 aVertexPosition;
    attribute vec2 aTextureCoord;

    // our displacement texture matrix uniform
    uniform mat4 displacementTextureMatrix;

    // mouse position and direction uniforms
    uniform vec2 uMousePos;
    uniform float uDirection;

    // custom varyings
    varying vec2 vTextureCoord;
    varying vec2 vDispTextureCoord;
    varying vec2 vMouseTexCoords;

    void main() {
        gl_Position = vec4(aVertexPosition, 1.0);

        // varyings
        vTextureCoord = aTextureCoord;
        vDispTextureCoord = (displacementTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;

        // we will handle our mouse coords here for better performance
        // get our texture coords for both directions
        vec2 mouseHorizontalTexCoords = (uMousePos + 1.0) / 2.0;
        mouseHorizontalTexCoords.y = 0.5;

        vec2 mouseVerticalTexCoords = (uMousePos + 1.0) / 2.0;
        mouseVerticalTexCoords.x = 0.5;

        // use the right value for the right direction
        vMouseTexCoords = mix(mouseHorizontalTexCoords, mouseVerticalTexCoords, uDirection);
    }
</script>

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

  <script id="distortion-fs" type="x-shader/x-fragment">
    #ifdef GL_ES
    precision mediump float;
    #endif

    // varyings
    varying vec2 vTextureCoord;
    varying vec2 vDispTextureCoord;
    varying vec2 vMouseTexCoords;

    // our render texture is basically what's being drawn in our canvas
    uniform sampler2D renderTexture;
    // the displacement texture we've loaded into our shader pass
    uniform sampler2D displacementTexture;

    // all our uniforms
    uniform float uDragEffect;
    uniform vec2 uMousePos;
    uniform vec2 uOffset;
    uniform float uDirection;
    uniform vec3 uBgColor;

    void main( void ) {
        vec2 textureCoords = vTextureCoord;

        // repeat and offset our displacement map texture coords for both slider directions
        vec2 horizontalPhase = fract(vec2(vDispTextureCoord.x + uOffset.x, vDispTextureCoord.y + (uOffset.y / 3000.0)) / vec2(1.0, 1.0));
        vec2 verticalPhase = fract(vec2(vDispTextureCoord.x * (uOffset.x / 3000.0), vDispTextureCoord.y + uOffset.y) / vec2(1.0, 1.0));

        // use the correct repeated and offseted texture coords
        vec2 phase = mix(horizontalPhase, verticalPhase, uDirection);
        vec4 displacement = texture2D(displacementTexture, phase);

        // use our varying mouse texture coords
        vec2 mouseTexCoords = vMouseTexCoords;

        float distanceToMouse = distance(mouseTexCoords, textureCoords);

        // calculate an effect that goes from 0 to 1 depenging on uDragEffect and distanceToMouse
        float spreadFromMouse = clamp((uDragEffect * (1.0 - distanceToMouse) - 1.0) + uDragEffect * 2.0, 0.0, 1.0);

        // calculate our fish eye like distortions
        vec2 fishEye = (vec2(textureCoords - mouseTexCoords).xy) * pow(distanceToMouse, 2.5);

        // add a displacement based on our map and our time uniform
        float displacementEffect = displacement.r * 1.25;

        // spread our fish eye and displacement effects from our mouse
        // calculate for both directions
        vec2 horizontalTexCoords = textureCoords;
        horizontalTexCoords.x -= spreadFromMouse * fishEye.x * displacementEffect / 4.0;
        horizontalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect;

        vec2 verticalTexCoords = textureCoords;
        verticalTexCoords.x -= spreadFromMouse * fishEye.x * displacementEffect;
        verticalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect / 4.0;

        // use the right value for the right direction
        textureCoords = mix(horizontalTexCoords, verticalTexCoords, uDirection);


        // get our final colored and BW vec4
        vec4 finalColor = texture2D(renderTexture, textureCoords);
        float grey = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
        vec4 finalGrey = vec4(vec3(grey), 1.0);

        // mix our both vec4 based on our spread value
        finalColor = mix(finalColor, finalGrey, spreadFromMouse * finalColor.a);

        //finalColor.a = mix(displacementEffect * distanceToMouse, 1.0, finalColor.a);

        // apply a grey background where we don't have nothing to draw
        finalColor = mix(vec4(uBgColor.r * spreadFromMouse / 255.0, uBgColor.g * spreadFromMouse / 255.0, uBgColor.b * spreadFromMouse / 255.0, spreadFromMouse), finalColor, finalColor.a);

        gl_FragColor = finalColor;
    }
</script>

Part 3. The WebGL

Okay, now that we’ve written our shaders, let’s go back to the javascript and add the WebGL part. We will extend our Slider class (be sure to insert our Slider class code seen in the previous article before what we’ll write next) and use almost the same code structure : constructor, helpers, hooks, set up and destroy methods, except that for the sake of clarity, we’ll write all the set up functions before helper and hooks.

Initialize

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.

  class WebGLSlider extends Slider {

    /*** CONSTRUCTOR ***/

    constructor(options) {
        super(options);

        // tweening
        this.animation = null;
        // value from 0 to 1 to pass as uniform to the WebGL
        // will be tweened on mousedown / touchstart and mouseup / touchend events
        this.effect = 0;

        // our WebGL variables
        this.curtains = null;
        this.planes = [];
        this.shaderPass = null;

        // set up the WebGL part
        this.setupWebGL();
    }

    /*** WEBGL INIT ***/

    // set up WebGL context and scene
    setupWebGL() {
        // set up our WebGL context, append the canvas to our wrapper and create a requestAnimationFrame loop
        // the canvas will be our scene containing all our planes
        // this is the scene we will post process
        this.curtains = new Curtains("canvas");

        this.curtains.onError(function() {
            // onError handles all errors during WebGL context initialization or plane creation
            // we will add a class to the document body to display original images (see CSS)
            document.body.classList.add("no-curtains");
        });

        // planes and shader pass
        this.setupPlanes();
        this.setupShaderPass();
    }

};

As you can see we are calling setupPlanes and 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 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 videos children of that element. In our case we just have one image.
  • A parameter object. This is where we’ll specify our shaders scripts IDs and our uniforms.

Once our 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.

  class WebGLSlider extends Slider {

    /*** CONSTRUCTOR ***/

    ...

    /*** WEBGL INIT ***/

    ...

    /*** PLANES CREATION ***/

    setupPlanes() {
        // Planes

        // each plane is bound to a HTML element to copy its size and position
        // in this case this will be the slider inner items
        // it will automatically create a WebGL texture for each image, canvas and video child of that element
        var planeElements = document.getElementsByClassName("plane");

        // our planes params
        // we just pass our shaders tag ID and a uniform to animate opacity on load
        var params = {
            vertexShaderID: "slider-planes-vs",
            fragmentShaderID: "slider-planes-fs",
            uniforms: {
                opacity: {
                    name: "uOpacity", // variable name inside our shaders
                    type: "1f", // this means our uniform is a float
                    value: 0,
                },
            },
        };

        // add all our planes and handle them
        for(var i = 0; i < planeElements.length; i++) {
            // addPlane method adds a plane to our WebGL scene
            // takes 2 params: our HTML referent element and the params set above
            // it returns a Plane class object if creation is successful, false otherwise
            var plane = this.curtains.addPlane(planeElements[i], params);

            // if our plane has been successfully created
            if(plane) {
                // push it into our planes array
                this.planes.push(plane);

                // onReady is called once our plane is ready and all its texture have been created
                plane.onReady(function() {
                    // inside our onReady function scope, this represents our plane
                    var currentPlane = this;

                    // add a "loaded" class to display the title
                    currentPlane.htmlElement.closest(".plane-wrapper").classList.add("loaded");

                    // animate plane opacity once they are loaded
                    var opacity = {
                        value: 0,
                    };

                    anime({
                        targets: opacity,
                        value: 1,
                        easing: "linear",
                        duration: 750,
                        update: function() {
                            // continualy increase opacity from 0 to 1
                            currentPlane.uniforms.opacity.value = opacity.value;
                        },
                    });
                });
            }
        }
    }

};

Adding the shader pass

Next up is the shader pass. Adding a shader pass is even easier than adding a plane thanks to the addShaderPass method of our Curtains object. It doesn’t need to be bound to a HTML element as it will in fact be bound to our canvas instead. It will only need a parameter object with shaders scripts IDs and uniforms.

Once it has been added, we will load the displacement image into it using loadImage. This method accepts an image HTML element as parameter so we first need to create one. No need to listen to the load event of the image tho, 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.

  class WebGLSlider extends Slider {

    /*** CONSTRUCTOR ***/

    ...

    /*** WEBGL INIT ***/

    ...

    /*** PLANES CREATION ***/

    ...

    /*** SHADER PASS CREATION ***/

    setupShaderPass() {
        // Shader pass
        // we will post process our scene
        // that means we will apply shaders to our whole scene

        // like for regular planes we will need params
        // they will contain vertex and fragment shaders ID and our uniforms
        var shaderPassParams = {
            vertexShaderID: "distortion-vs",
            fragmentShaderID: "distortion-fs",
            uniforms: {
                // apply the whole effect
                // 0: no effect
                // 1: full effect
                dragEffect: {
                    name: "uDragEffect", // variable name inside our shaders
                    type: "1f", // this means our uniform is a float
                    value: 0,
                },
                // our mouse position (in WebGL clip space coordinates)
                mousePos: {
                    name: "uMousePos",
                    type: "2f", // this means our uniform is a length 2 array of floats
                    value: [0, 0],
                },
                // direction of our slider
                // 0: horizontal drag
                // 1: vertical drag
                direction: {
                    name: "uDirection",
                    type: "1f",
                    value: this.direction,
                },
                // the background color when effect is applied
                bgColor: {
                    name: "uBgColor",
                    type: "3f", // this means our uniform is a length 3 array of floats
                    value: [3, 135, 154], // rgb values
                },
                // our displacement texture offset
                offset: {
                    name: "uOffset",
                    type: "2f",
                    value: [0, 0],
                },
            },
        };

        // addShaderPass adds a shader pass (Frame Buffer Object) to our WebGL scene
        // returns a ShaderPass class object if successful, false otherwise
        this.shaderPass = this.curtains.addShaderPass(shaderPassParams);

        // if our shader pass has been successfully created
        if(this.shaderPass) {
            // we will add our displacement map texture

            // first we load a new image
            // please check the path to your displacement image!!
            var image = new Image();
            image.src = "images/displacement.jpg";
            // then we set its data-sampler attribute to use in fragment shader
            image.setAttribute("data-sampler", "displacementTexture");
            // finally we load it into our shader pass via the loadImage method
            this.shaderPass.loadImage(image);

            var self = this;

            // onRender is called at each requestAnimationFrame call
            this.shaderPass.onRender(function() {
                // we will continuously offset our displacement texture on secondary axis
                var secondaryDirection = self.direction === 0 ? 1 : 0;
                self.shaderPass.uniforms.offset.value[secondaryDirection] = self.shaderPass.uniforms.offset.value[secondaryDirection] + 1;
            });
        }
    }

};

Using the hooks

We’ve set up all our WebGL objects, now we need to bind them to the slider thanks to 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. But 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.

  class WebGLSlider extends Slider {

    /*** CONSTRUCTOR ***/

    ...

    /*** WEBGL INIT ***/

    ...

    /*** PLANES CREATION ***/

    ...

    /*** SHADER PASS CREATION ***/

    ...

    /*** HELPER ***/

    // this will update our shader pass mouse position uniform
    updateMousePosUniform(mousePosition) {
        // if our shader pass exists, update the mouse position uniform
        if(this.shaderPass) {
            // mouseToPlaneCoords converts window coordinates to WebGL clip space
            var relativeMousePos = this.shaderPass.mouseToPlaneCoords(mousePosition[0], mousePosition[1]);
            this.shaderPass.uniforms.mousePos.value = [relativeMousePos.x, relativeMousePos.y];
        }
    }

    /*** HOOKS ***/

    // this is called once our mousedown / touchstart event occurs and the drag started
    onDragStarted(mousePosition) {
        // pause and remove previous animation
        if(this.animation) this.animation.pause();
        anime.remove(slider);

        // get a ref
        var self = this;

        // animate our mouse down effect
        this.animation = anime({
            targets: self,
            effect: 1,
            easing: 'easeOutCubic',
            duration: self.options.duration,
            update: function() {
                if(self.shaderPass) {
                    // update our shader pass uniforms
                    self.shaderPass.uniforms.dragEffect.value = self.effect;
                }
            }
        });

        // enableDrawing to re-enable drawing again if we disabled it earlier
        this.curtains.enableDrawing();

        // update our shader pass mouse position uniform
        this.updateMousePosUniform(mousePosition);
    }

    // this is called while we are currently dragging the slider
    onDrag(mousePosition) {
        // update our shader pass mouse position uniform
        this.updateMousePosUniform(mousePosition);
    }

    // this is called once our mouseup / touchend event occurs and the drag started
    onDragEnded(mousePosition) {
        // calculate duration based on easing
        var duration = 100 / this.options.easing;
        var easing = 'linear';

        // if there's no movement just tween the shader pass effect
        if(Math.abs(this.translation - this.currentPosition) < 5) {
            easing = 'easeOutCubic';
            duration = this.options.duration;
        }

        // pause remove previous animation
        if(this.animation) this.animation.pause();
        anime.remove(slider);

        // get a ref
        var self = this;

        this.animation = anime({
            targets: self,
            effect: 0,
            easing: easing,
            duration: duration,
            update: function() {
                if(self.shaderPass) {
                    // update drag effect
                    self.shaderPass.uniforms.dragEffect.value = self.effect;
                }
            }
        });

        // update our shader pass mouse position uniform
        this.updateMousePosUniform(mousePosition);
    }

    // this is called continuously while the slider is translating
    onTranslation() {
        // keep our WebGL planes position in sync with their HTML elements
        for(var i = 0; i < this.planes.length; i++) {
            // updatePosition is a useful method that apply the HTML element position to our WebGL plane
            this.planes[i].updatePosition();
        }

        // shader pass displacement texture offset
        if(this.shaderPass) {
            // we will offset our displacement effect on main axis so it follows the drag
            var offset = ((this.direction - 1) * 2 + 1) * this.translation / this.boundaries.referentSize;

            this.shaderPass.uniforms.offset.value[this.direction] = offset;
        }
    }

    // this is called once the translation has ended
    onTranslationEnded() {
        // we will stop rendering our WebGL until next drag occurs
        if(this.curtains) {
            this.curtains.disableDrawing();
        }
    }

    // this is called after our slider has been resized
    onSliderResized() {
        // update our direction uniform
        if(this.shaderPass) {
            // update direction
            this.shaderPass.uniforms.direction.value = this.direction;
        }
    }

};

We’re almost done here. Last thing we’ll do is add a way to cleanly destroy the WebGL part of our slider and override our Slider class initial destroy method:

  class WebGLSlider extends Slider {

    /*** CONSTRUCTOR ***/

    ...

    /*** WEBGL INIT ***/

    ...

    /*** PLANES CREATION ***/

    ...

    /*** SHADER PASS CREATION ***/

    ...

    /*** HELPER ***/

    ...

    /*** HOOKS ***/

    ...

    /*** DESTROY ***/

    // destroy all WebGL related things
    destroyWebGL() {
        // if you want to totally remove the WebGL context uncomment next line
        // and remove what's after
        //this.curtains.dispose();

        // if you want to only remove planes and shader pass and keep the context available
        // that way you could re init the WebGL later to display the slider again
        if(this.shaderPass) {
            this.curtains.removeShaderPass(this.shaderPass);
        }

        for(var i = 0; i < this.planes.length; i++) {
            this.curtains.removePlane(this.planes[i]);
        }
    }

    // call this method publicly to destroy our slider and the WebGL part
    // override the destroy method of the Slider class
    destroy() {
        // destroy everything related to WebGL and the slider
        this.destroyWebGL();
        this.destroySlider();
    }

};

// custom options
var options = {
    easing: 0.075,
    duration: 500,
    dragSpeed: 1.75,
}

// let's go!
var slider = new WebGLSlider(options);

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