<!-- fork https://codepen.io/martinlaxenaire/pen/GVROee -->

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

<div id="content">

  <h1 id="title">Islands</h1>

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

    <div class="plane-wrapper">
      <span class="plane-title">Bahamas</span>
      <div class="plane">
        <img src="https://source.unsplash.com/2fFd7SIVpz8/1280x720" alt="Photo by Gregory Culmer on Unsplash" data-sampler="planeTexture" crossorigin />
      </div>
    </div>

    <div class="plane-wrapper">
      <span class="plane-title">Philippines</span>
      <div class="plane">
        <img src="https://source.unsplash.com/SDaJRmZYMDA/1280x720" alt="Photo by James Connolly on Unsplash" data-sampler="planeTexture" crossorigin />
      </div>
    </div>

    <div class="plane-wrapper">
      <span class="plane-title">Indonesia</span>
      <div class="plane">
        <img src="https://source.unsplash.com/DxmBSgUYKis/1600x900" alt="Photo by sutirta budiman on Unsplash" data-sampler="planeTexture" crossorigin />
      </div>
    </div>

    <div class="plane-wrapper">
      <span class="plane-title">Maldives</span>
      <div class="plane">
        <img src="https://source.unsplash.com/lT9rqfG7lcQ/1280x720" alt="Photo by Jailam Rashad on Unsplash" data-sampler="planeTexture" crossorigin />
      </div>
    </div>

    <div class="plane-wrapper">
      <span class="plane-title">Greece</span>
      <div class="plane">
        <img src="https://source.unsplash.com/QXW1YEMhq_4/1280x720" data-sampler="planeTexture" alt="Photo by Chris Karidis on Unsplash" crossorigin />
      </div>
    </div>

    <div class="plane-wrapper">
      <span class="plane-title">Fiji</span>
      <div class="plane">
        <img src="https://source.unsplash.com/3jtm7BMsaPA/1280x720" alt="Photo by Lili Ortiz on Unsplash" data-sampler="planeTexture" crossorigin />
      </div>
    </div>

    <div class="plane-wrapper">
      <span class="plane-title">Thailand</span>
      <div class="plane">
        <img src="https://source.unsplash.com/Mwg_MdX-Jx4/1280x720" data-sampler="planeTexture" alt="Photo by Samule Sun on Unsplash" crossorigin />
      </div>
    </div>

  </div>

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


</div>


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


<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>
<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, 0.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, vec2(0, 0), uDirection);
  // vec4 displacement = texture2D(displacementTexture, verticalPhase);

  // 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 * 0.25 * (1.0 - uDirection);
  horizontalTexCoords.y -= spreadFromMouse * fishEye.y * 0.25 * uDirection;

  vec2 verticalTexCoords = textureCoords;
  verticalTexCoords.x -= spreadFromMouse * fishEye.x * 0.25 * (1.0 - uDirection);
  verticalTexCoords.y -= spreadFromMouse * fishEye.y * 0.25 * uDirection;

  // 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);

  // 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>
@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;
    }

}
class Slider {

    /*** CONSTRUCTOR ***/

    constructor(options = {}) {
        // our options
        this.options = {
            // slider state and values
            // the div we are going to translate
            element: options.element || document.getElementById("planes"),
            // easing value, the lower the smoother
            easing: options.easing || 0.1,
            // translation speed
            // 1: will follow the mouse
            // 2: will go twice as fast as the mouse, etc
            dragSpeed: options.dragSpeed || 1,
            // duration of the in animation
            duration: options.duration || 750,
        };

        // if we are currently dragging
        this.isMouseDown = false;
        // if the slider is currently translating
        this.isTranslating = false;

        // current position
        this.currentPosition = 0;
        // drag start position
        this.startPosition = 0;
        // drag end position
        this.endPosition = 0;

        // slider translation
        this.translation = 0;

        this.animationFrame = null;

        // set up the slider
        this.setupSlider();
    }

    /*** HELPERS ***/

    // lerp function used for easing
    lerp(value1, value2, amount) {
        amount = amount < 0 ? 0 : amount;
        amount = amount > 1 ? 1 : amount;
        return (1 - amount) * value1 + amount * value2;
    }

    // return our mouse or touch position
    getMousePosition(e) {
        var mousePosition;
        if(e.targetTouches) {
            if(e.targetTouches[0]) {
                mousePosition = [e.targetTouches[0].clientX, e.targetTouches[0].clientY];
            }
            else if(e.changedTouches[0]) {
                // handling touch end event
                mousePosition = [e.changedTouches[0].clientX, e.changedTouches[0].clientY];
            }
            else {
                // fallback
                mousePosition = [e.clientX, e.clientY];
            }
        }
        else {
            mousePosition = [e.clientX, e.clientY];
        }

        return mousePosition;
    }

    // set the slider boundaries
    // we will translate it horizontally in landscape mode
    // vertically in portrait mode
    setBoundaries() {
        if(window.innerWidth >= window.innerHeight) {
            // landscape
            this.boundaries = {
                max: -1 * this.options.element.clientWidth + window.innerWidth,
                min: 0,
                sliderSize: this.options.element.clientWidth,
                referentSize: window.innerWidth,
            };

            // set our slider direction
            this.direction = 0;
        }
        else {
            // portrait
            this.boundaries = {
                max: -1 * this.options.element.clientHeight + window.innerHeight,
                min: 0,
                sliderSize: this.options.element.clientHeight,
                referentSize: window.innerHeight,
            };

            // set our slider direction
            this.direction = 1;
        }
    }

    /*** HOOKS ***/

    // this is called once our mousedown / touchstart event occurs and the drag started
    onDragStarted(mousePosition) {
    }

    // this is called while we are currently dragging the slider
    onDrag(mousePosition) {
    }

    // this is called once our mouseup / touchend event occurs and the drag started
    onDragEnded(mousePosition) {
    }

    // this is called continuously while the slider is translating
    onTranslation() {
    }

    // this is called once the translation has ended
    onTranslationEnded() {
    }

    // this is called after our slider has been resized
    onSliderResized() {
    }

    /*** ANIMATIONS ***/

    // this will translate our slider HTML element and set up our hooks
    translateSlider(translation) {
        translation = Math.floor(translation * 100) / 100;

        // should we translate it horizontally or vertically?
        var direction = this.direction === 0 ? "translateX" : "translateY";
        // apply translation
        this.options.element.style.transform = direction + "(" + translation + "px)";

        // if the slider translation is different than the translation to apply
        // that means the slider is still translating
        if(this.translation !== translation) {
            // hook function to execute while we are translating
            this.onTranslation();
        }
        else if(this.isTranslating && !this.isMouseDown) {
            // if those conditions are met, that means the slider is no longer translating
            this.isTranslating = false;

            // hook function to execute after translation has ended
            this.onTranslationEnded();
        }

        // finally set our translation
        this.translation = translation;
    }

    // this is our request animation frame loop where we will translate our slider
    animate() {
        // interpolate values
        var translation = this.lerp(this.translation, this.currentPosition, this.options.easing);

        // apply our translation
        this.translateSlider(translation);

        this.animationFrame = requestAnimationFrame(this.animate.bind(this));
    }

    /*** EVENTS ***/

    // on mouse down or touch start
    onMouseDown(e) {
        // start dragging
        this.isMouseDown = true;

        // apply specific styles
        this.options.element.classList.add("dragged");

        // get our touch/mouse start position
        var mousePosition = this.getMousePosition(e);
        // use our slider direction to determine if we need X or Y value
        this.startPosition = mousePosition[this.direction];

        // drag start hook
        this.onDragStarted(mousePosition);
    }

    // on mouse or touch move
    onMouseMove(e) {
        // if we are not dragging, we don't do nothing
        if(!this.isMouseDown) return;

        // get our touch/mouse position
        var mousePosition = this.getMousePosition(e);

        // get our current position
        this.currentPosition = this.endPosition + ((mousePosition[this.direction] - this.startPosition) * this.options.dragSpeed);

        // if we're not hitting the boundaries
        if(this.currentPosition > this.boundaries.min && this.currentPosition < this.boundaries.max) {
            // if we moved that means we have started translating the slider
            this.isTranslating = true;
        }
        else {
            // clamp our current position with boundaries
            this.currentPosition = Math.min(this.currentPosition, this.boundaries.min);
            this.currentPosition = Math.max(this.currentPosition, this.boundaries.max);
        }

        // drag hook
        this.onDrag(mousePosition);
    }

    // on mouse up or touchend
    onMouseUp(e) {
        // we have finished dragging
        this.isMouseDown = false;

        // remove specific styles
        this.options.element.classList.remove("dragged");

        // update our end position
        this.endPosition = this.currentPosition;

        // send our mouse/touch position to our hook
        var mousePosition = this.getMousePosition(e);

        // drag ended hook
        this.onDragEnded(mousePosition);
    }

    // on resize we will need to apply old translation value to new sizes
    onResize(e) {
        // get our old translation ratio
        var ratio = this.translation / this.boundaries.sliderSize;

        // reset boundaries and properties bound to window size
        this.setBoundaries();

        // reset all translations
        this.options.element.style.transform = "tanslate3d(0, 0, 0)";

        // calculate our new translation based on the old translation ratio
        var newTranslation = ratio * this.boundaries.sliderSize;
        // clamp translation to the new boundaries
        newTranslation = Math.min(newTranslation, this.boundaries.min);
        newTranslation = Math.max(newTranslation, this.boundaries.max);

        // apply our new translation
        this.translateSlider(newTranslation);

        // reset current and end positions
        this.currentPosition = newTranslation;
        this.endPosition = newTranslation;

        // call our resize hook
        this.onSliderResized();
    }

    /*** SET UP AND DESTROY ***/

    // set up our slider
    // init its boundaries, add event listeners and start raf loop
    setupSlider() {
        this.setBoundaries();

        // event listeners

        // mouse events
        window.addEventListener("mousemove", this.onMouseMove.bind(this), {
            passive: true,
        });
        window.addEventListener("mousedown", this.onMouseDown.bind(this));
        window.addEventListener("mouseup", this.onMouseUp.bind(this));

        // touch events
        window.addEventListener("touchmove", this.onMouseMove.bind(this), {
            passive: true,
        });
        window.addEventListener("touchstart", this.onMouseDown.bind(this), {
            passive: true,
        });
        window.addEventListener("touchend", this.onMouseUp.bind(this));

        // resize event
        window.addEventListener("resize", this.onResize.bind(this));

        // launch our request animation frame loop
        this.animate();
    }

    // will be called silently to cleanly remove the slider
    destroySlider() {
        // remove event listeners

        // mouse events
        window.removeEventListener("mousemove", this.onMouseMove, {
            passive: true,
        });
        window.removeEventListener("mousedown", this.onMouseDown);
        window.removeEventListener("mouseup", this.onMouseUp);

        // touch events
        window.removeEventListener("touchmove", this.onMouseMove, {
            passive: true,
        });
        window.removeEventListener("touchstart", this.onMouseDown, {
            passive: true,
        });
        window.removeEventListener("touchend", this.onMouseUp);

        // resize event
        window.removeEventListener("resize", this.onResize);

        // cancel request animation frame
        cancelAnimationFrame(this.animationFrame);
    }

    // call this method publicly to destroy our slider
    destroy() {
        // destroy everything related to the slider
        this.destroySlider();
    }

};

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({
          container: "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();
    }

    /*** 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;
                        },
                    });
                });
            }
        }
    }

    /*** 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) {
        //     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;
        //     });
        // }
    }


    /*** 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;
        }
    }


    /*** 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.1,
    duration: 500,
    dragSpeed: 1.75,
}

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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/animejs/2.2.0/anime.min.js
  2. https://www.curtainsjs.com/build/curtains.min.js