Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

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

<div id="content">

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

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

        <div class="plane-wrapper">
            <span class="plane-title">Hong Kong</span>
            <div class="plane">
                <img src="https://source.unsplash.com/4hluhoRJokI/1280x720" data-sampler="planeTexture"alt="Photo by Simon Zhu on Unsplash" crossorigin />
            </div>
        </div>

        <div class="plane-wrapper">
            <span class="plane-title">Chicago</span>
            <div class="plane">
                <img src="https://source.unsplash.com/waedcXSIAKk/1280x720" data-sampler="planeTexture" alt="Photo by Pedro Lastra on Unsplash" crossorigin />
            </div>
        </div>

        <div class="plane-wrapper">
            <span class="plane-title">Shanghai</span>
            <div class="plane">
                <img src="https://source.unsplash.com/D8iZPlX-2fs/1280x720" data-sampler="planeTexture" alt="Photo by Denys Nevozhai on Unsplash" crossorigin />
            </div>
        </div>

        <div class="plane-wrapper">
            <span class="plane-title">New York</span>
            <div class="plane">
                <img src="https://source.unsplash.com/y9WmMWaiftc/1280x720" data-sampler="planeTexture" alt="Photo by Pedro Lastra on Unsplash" crossorigin />
            </div>
        </div>

        <div class="plane-wrapper">
            <span class="plane-title">Tokyo</span>
            <div class="plane">
                <img src="https://source.unsplash.com/IocJwyqRv3M/1280x720" data-sampler="planeTexture" alt="Photo by Louie Martinez on Unsplash" crossorigin />
            </div>
        </div>

        <div class="plane-wrapper">
            <span class="plane-title">Singapore</span>
            <div class="plane">
                <img src="https://source.unsplash.com/6wvbIX02V9M/1280x720" data-sampler="planeTexture" alt="Photo by Sonali Sharma on Unsplash" crossorigin />
            </div>
        </div>

        <div class="plane-wrapper">
            <span class="plane-title">Toronto</span>
            <div class="plane">
                <img src="https://source.unsplash.com/451DMKNITAM/1280x720" data-sampler="planeTexture" alt="Photo by Syed Ahmed on Unsplash" crossorigin />
            </div>
        </div>

        <div class="plane-wrapper">
            <span class="plane-title">Kuala Lumpur</span>
            <div class="plane">
                <img src="https://source.unsplash.com/a1TnJHVCGN0/1280x720" data-sampler="planeTexture" alt="Photo by Azlan Baharudin 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 top edge is a float from 0 to 1
        float distanceToTop = distance(vec2(vTextureCoord.x, 1.0), vTextureCoord);

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

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

        // 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 uRenderTexture;
    // 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 / 3600.0)) / vec2(1.0, 1.0));
        vec2 verticalPhase = fract(vec2(vDispTextureCoord.x * (uOffset.x / 3600.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, 3.0);

        // 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 / 2.0;
        horizontalTexCoords.y += spreadFromMouse * fishEye.y * displacementEffect * 3.0;

        vec2 verticalTexCoords = textureCoords;
        verticalTexCoords.x += spreadFromMouse * fishEye.x * displacementEffect * 3.0;
        verticalTexCoords.y -= spreadFromMouse * fishEye.y * displacementEffect / 2.0;

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


        // get our final colored and BW vec4
        vec4 finalColor = texture2D(uRenderTexture, 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);

        float spreadFromMouseAdjusted = spreadFromMouse / sqrt(2.0);

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

        gl_FragColor = finalColor;
    }
</script>
              
            
!

CSS

              
                @media screen {

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

    body {
        margin: 0;
        font-size: 18px;
        font-family: 'Oswald', Verdana, sans-serif;
        background: black;
        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: white;
        text-align: center;
    }

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

        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: black;
        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 {
        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;
        pointer-events: none;
        font-size: 0.9em;
        text-transform: uppercase;
        color: #888;
        text-align: center;
    }


    /*** handling WebGL errors ***/

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

    .no-curtains #planes.dragged {
        background-color: #0d0d0d;
    }

    .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 {
        position: relative;

        width: 70vw;
        height: calc(100vh / 1.75);
        margin: 5vw 0;
    }

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

}
              
            
!

JS

              
                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 before our slider has been resized
    onBeforeResize() {
    }

    // 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) {
        this.onBeforeResize();

        // 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 = [];
        // we will keep track of the previous translation values on resize
        this.previousTranslation = {
            x: 0,
            y: 0,
        };
        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: [13, 13, 13], // 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
            var image = new Image();
            image.src = "https://www.martin-laxenaire.fr/medium/medias/skylines-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;
            });
        }
    }


    /*** 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() {
        // get our slider translation and take our previous translation into account
        var planeTranslation = {
            x: this.direction === 0 ? this.translation - this.previousTranslation.x : 0,
            y: this.direction === 1 ? this.translation - this.previousTranslation.y : 0,
        };

        // keep our WebGL planes position in sync with their HTML elements
        for(var i = 0; i < this.planes.length; i++) {
            // in the previous CodePen we were using updatePosition the method which handles positioning automatically
            // however this method internally calls getBoundingClientRect() which causes a reflow and therefore impacts performance
            // so we will position our planes manually with setRelativePosition instead, which does not trigger a layout repaint call
          this.planes[i].setRelativePosition(planeTranslation.x, planeTranslation.y);
        }

        // 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() {
        // we need to update our previous translation value
        this.previousTranslation = {
            x: this.direction === 0 ? this.translation : 0,
            y: this.direction === 1 ? this.translation : 0,
        };

        // reset our slides relative positions
        // because during the resize their positions has already been updated internally
        for(var i = 0; i < this.planes.length; i++) {
            this.planes[i].setRelativePosition(0, 0);
        }

        // 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 = {
    duration: 500,
    dragSpeed: 1.5,
}

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

Console