<div id="canvas"></div>

<div id="content">
  <div class="webGLSlider">
    <div class="wrapper">
      <div class="slide-container">
        <div class="slide" data-name="Patterns and Waves" data-url="#">
          <img src="https://unsplash.it/1920/823?random=1" data-sampler="planeTexture" crossorigin>
          <img class="texture" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/58281/rock-_disp.png" data-sampler="displacementTexture" crossorigin>
        </div>
        <div class="slide" data-name="Cadillac" data-url="#">
          <img src="https://unsplash.it/1920/823?random=2" data-sampler="planeTexture" crossorigin>
          <img class="texture" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/58281/rock-_disp.png" data-sampler="displacementTexture" crossorigin>
        </div>
        <div class="slide" data-name="Laso" data-url="#">
          <img src="https://unsplash.it/1920/823?random=3" data-sampler="planeTexture" crossorigin>
          <img class="texture" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/58281/rock-_disp.png" data-sampler="displacementTexture" crossorigin>
        </div>
        <div class="slide" data-name="WebGL is fun" data-url="#">
          <img src="https://unsplash.it/1920/823?random=4" data-sampler="planeTexture" crossorigin>
          <img class="texture" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/58281/rock-_disp.png" data-sampler="displacementTexture" crossorigin>
        </div>
        <div class="slide" data-name="Zach Saucier" data-url="#">
          <img src="https://unsplash.it/1920/823?random=5" data-sampler="planeTexture" crossorigin>
          <img class="texture" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/58281/rock-_disp.png" data-sampler="displacementTexture" crossorigin>
        </div>
        <div class="slide" data-name="You found me!" data-url="#">
          <img src="https://unsplash.it/1920/823?random=6" data-sampler="planeTexture" crossorigin>
          <img class="texture" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/58281/rock-_disp.png" data-sampler="displacementTexture" crossorigin>
        </div>
      </div>
    </div>

    <h6 class="line-reveal">Recent work</h6>
    <a id="slider-link" class="arrow-link" href="#">
      <h3 id="slider-title" class="slider-heading">Project name</h3>
      <h3 class="slider-heading text-right"><div class="text">View Project</div><svg class="hover-arrow" width="77" height="35" viewBox="0 0 77 35" xmlns="http://www.w3.org/2000/svg"><path d="M58.5295 0L54.8195 3.85L66.5095 14.7H0.189453V19.81H66.5795L54.8195 30.73L58.5295 34.58L76.4495 17.29L58.5295 0Z" fill="#fff"/></svg></h3>
    </a>
  </div>
</div>
* {
  box-sizing: border-box;
}

html {
  font-size: 25px;
}

body {
  background: #1d1d1d;
  font-family: Asap, sans-serif;
}

#canvas {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  height: 100vh;
}
#content {
  position: relative;
  z-index: 1;
}

.wrapper {
  position: relative;
  overflow: hidden;
}

.slide-container {
  position: relative;
}
.slide-container::before {
  display: block;
  content: "";
  width: 100%;
  padding-top: 42.857%; /* 9 / 21 */
}

.slide {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}
.slide img {
  width: 100%;
}


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


.webGLSlider {
  color: white;
}
.slider-heading {
  text-transform: uppercase;
  margin: 0;
}

.webGLSlider a {
  color: inherit;
  text-decoration: none;
}

.text-right {
  text-align: right;
}
.text-right .text {
  display: inline-block;
  vertical-align: middle;
  margin-right: 2rem;
}
.hover-arrow {
  vertical-align: middle;
}







.split-child {
  display: inline-block;
}
.split-parent {
  overflow: hidden;
}



.webGLSlider img {
  display: none;
}
.no-curtains img:not(.texture) {
  display: block;
}
View Compiled
// Save references to elements that we will use
const wrapper = document.querySelector(".webGLSlider .wrapper");
const slideContainer = document.querySelector(".webGLSlider .slide-container");
const slides = gsap.utils.toArray(".webGLSlider .slide");
const proxy = document.createElement("div");
const webGLSlider = document.querySelector('.webGLSlider');
let sliderTitle = document.querySelector('#slider-title');
const sliderLink = document.querySelector('#slider-link');

const numSlides = slides.length;
const wrapIndex = gsap.utils.wrap(0, numSlides);
const gap = 0;
let lastIndex = 0; // Used in resize and to change out headings
let firstRun = true;

// These are set in the resize function
let slideHeight;
let slideWidth;
let wrapWidth;
let wrapVal;
let animation;
let draggable;

// Variables related to the WebGL functionality
const planes = [];
const maxVelocity = 10000;
const halfMaxVelocity = maxVelocity / 2;

// Our shaders
const v300Shader = `#version 300 es
// Determines how much precision for the GPU to use
#ifdef GL_ES
precision mediump float;
#endif

// in = passed in from a data buffer
// uniforms = passed in from CPU (our program)
// out = passed from our vertex shader to our fragment shader

// Default mandatory attributes
in vec3 aVertexPosition;
in vec2 aTextureCoord;

// Mandatory projection and model view matrices are generated by curtains
// It will position and size our plane based on its HTML element CSS values
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

// This is generated by curtains 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;

// The un-transformed mouse position
uniform vec2 uMouse;

// Texture coord varying that will be passed to our fragment shader
out vec2 vTextureCoord;
// Our transformed mouse position that will be passed to our fragment shader
out vec2 vMouse;

void main() {
    // Use texture matrix and original texture coords to generate accurate texture coords
    vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;

    // Convert from vertex pos to texture pos and apply texture matrix to the mouse position as well 
    vMouse = (planeTextureMatrix * vec4((uMouse + 1.0) * 0.5, 0.0, 1.0)).xy - 0.5;

    // Apply our vertex position based on the projection and model view matrices
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}`;
const f300Shader = `#version 300 es
#ifdef GL_ES
precision mediump float;
#endif

// Our texture samplers - Their names match the data-sampler attributes on our image tags
uniform sampler2D planeTexture; // The image texture
uniform sampler2D displacementTexture; // The displacement texture

// Our texture coords (on a pixel-by-pixel basis)
in vec2 vTextureCoord;

in vec2 vMouse; // The mouse position in texture coordinates

out vec4 outputColor; // The output color of each pixel (what was gl_FragColor in WebGL1)

// FOR MOUSE EFFECTS
uniform float uRadius; // Radius of pixels to warp/invert
const float transAmnt = 0.05; // Amount to translate the texture by

const float PI = 3.1415926535;

// FOR DISPLACEMENT
// The power and intensity of our displacement - passed in
uniform float uPower;
uniform float uIntensity;

const float aspect = 2.33333333; // 21 / 9

// FOR ANTIALIAS
uniform vec2 uResolution;
#define R    uResolution
// This basically gives you antialias for free without the need of a second pass
// If you don't need antialias in another place, local antiliasing is probably best
// From here: https://www.shadertoy.com/view/3sjGDh
// Which is explained here: https://shadertoyunofficial.wordpress.com/
// (Look for antialias in the search box)
#define S(v) smoothstep(2.0/R.y, 0.0, v)

void main() {    
  vec2 myUV = vTextureCoord;
  outputColor.a = 1.0;

  // Check if pixel is within the given radius of the mouse
  vec2 diff = myUV - vMouse - 0.5;
  diff.x *= aspect;
  float distance = length(diff);

  // Create the fish-eye effect
  if (distance <= uRadius) {
    float scale = (1.0 - cos(distance/uRadius * PI * 0.5));
    myUV = vMouse + normalize(diff) * uRadius * scale + 0.5;
  }

  // Translate the texture
  myUV += -vMouse * transAmnt;

  // Displacement
  // Based on https://codepen.io/ReGGae/pen/bmyYEj
  vec4 disp = texture(displacementTexture, myUV);
  vec2 dispVec = vec2(disp.x, disp.y);
  vec2 distPos = myUV + (dispVec * uIntensity * uPower);

  vec3 tex = texture(planeTexture, distPos).rgb;

  // Antialiasing
  vec3 inverted = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);
  if(uRadius > 0.0) {
    outputColor.rgb = mix( inverted, tex, S(distance - uRadius));
  } else {
    outputColor.rgb = inverted.rgb;
  }

  // Not antialiased
  // if(distance <= uRadius) {
  //   outputColor.rgb = tex;
  // } else { // invert
  //   outputColor.rgb = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);
  // }
}`;
const v100Shader = `
// Determines how much precision for the GPU to use
#ifdef GL_ES
precision mediump float;
#endif

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

// Mandatory projection and model view matrices are generated by curtains
// It will position and size our plane based on its HTML element CSS values
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

// This is generated by curtains 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;

// The un-transformed mouse position
uniform vec2 uMouse;

// Texture coord varying that will be passed to our fragment shader
varying vec2 vTextureCoord;
// Our transformed mouse position that will be passed to our fragment shader
varying vec2 vMouse;

void main() {
    // Use texture matrix and original texture coords to generate accurate texture coords
    vTextureCoord = (planeTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy;

    // Convert from vertex pos to texture pos and apply texture matrix to the mouse position as well 
    vMouse = (planeTextureMatrix * vec4((uMouse + 1.0) * 0.5, 0.0, 1.0)).xy - 0.5;

    // Apply our vertex position based on the projection and model view matrices
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}`;
const f100Shader = `
#ifdef GL_ES
precision mediump float;
#endif

// Our texture samplers - Their names match the data-sampler attributes on our image tags
uniform sampler2D planeTexture; // The image texture
uniform sampler2D displacementTexture; // The displacement texture

// Our texture coords (on a pixel-by-pixel basis)
varying vec2 vTextureCoord;

varying vec2 vMouse; // The mouse position in texture coordinates

// FOR MOUSE EFFECTS
uniform float uRadius; // Radius of pixels to warp/invert
const float transAmnt = 0.05; // Amount to translate the texture by

const float PI = 3.1415926535;

// FOR DISPLACEMENT
// The power and intensity of our displacement - passed in
uniform float uPower;
uniform float uIntensity;

const float aspect = 2.33333333; // 21 / 9

// FOR ANTIALIAS
uniform vec2 uResolution;
#define R    uResolution
// This basically gives you antialias for free without the need of a second pass
// If you don't need antialias in another place, local antiliasing is probably best
// From here: https://www.shadertoy.com/view/3sjGDh
// Which is explained here: https://shadertoyunofficial.wordpress.com/
// (Look for antialias in the search box)
#define S(v) smoothstep(2.0/R.y, 0.0, v)

void main() {    
  vec2 myUV = vTextureCoord;
  gl_FragColor.a = 1.0;

  // Check if pixel is within the given radius of the mouse
  vec2 diff = myUV - vMouse - 0.5;
  diff.x *= aspect;
  float distance = length(diff);

  // Create the fish-eye effect
  if (distance <= uRadius) {
    float scale = (1.0 - cos(distance/uRadius * PI * 0.5));
    myUV = vMouse + normalize(diff) * uRadius * scale + 0.5;
  }

  // Translate the texture
  myUV += -vMouse * transAmnt;

  // Displacement
  // Based on https://codepen.io/ReGGae/pen/bmyYEj
  vec4 disp = texture2D(displacementTexture, myUV);
  vec2 dispVec = vec2(disp.x, disp.y);
  vec2 distPos = myUV + (dispVec * uIntensity * uPower);

  vec3 tex = texture2D(planeTexture, distPos).rgb;

  // Antialiasing
  vec3 inverted = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);
  if(uRadius > 0.0) {
    gl_FragColor.rgb = mix( inverted, tex, S(distance - uRadius));
  } else {
    gl_FragColor.rgb = inverted.rgb;
  }

  // Not antialiased
  // if(distance <= uRadius) {
  //   gl_FragColor.rgb = tex;
  // } else { // invert
  //   gl_FragColor.rgb = vec3(1.0 - tex.r, 1.0 - tex.g, 1.0 - tex.b);
  // }
}`;

//////////////////////////////
// Heading change functions //
//////////////////////////////
function setupHeadingAnim() {
  // Split it twice to be able to reveal it from hidden overflow
  webGLSlider.childSplit = new SplitText('.slider-heading', {
    type: 'lines',
    linesClass: 'split-child'
  });
  webGLSlider.parentSplit = new SplitText('.slider-heading', {
    type: 'lines',
    linesClass: 'split-parent'
  });
  
  // The text reveal animation
  webGLSlider.anim = gsap.from(webGLSlider.childSplit.lines, {
    paused: true,
    yPercent: 100, 
    stagger: 0.2
  });
  if(firstRun) {
    webGLSlider.anim.play();
    firstRun = false;
  }
}

// Check to see if the index should be changed and set up the heading changes
function checkIndex(endX) {
  // Get the new index
  endX = -endX || 0;
  const newIndex = wrapIndex(endX / (slideWidth + gap));
  
  // Only do stuff if it's a new index
  if(typeof webGLSlider.anim === "undefined" || lastIndex !== newIndex) {
    // Undo pre anim
    if(webGLSlider.anim) {
      webGLSlider.anim.progress(1).kill();
      webGLSlider.parentSplit.revert();
      webGLSlider.childSplit.revert();
    }
    
    // Update index, text, and link
    sliderTitle = document.querySelector('#slider-title');
    lastIndex = newIndex;
    const dataset = slides[newIndex].dataset;
    sliderTitle.innerText = dataset.name;
    sliderLink.href = dataset.url;
    
    // Create new anim
    setupHeadingAnim();
  
    setupArrowLinks();
  }
}

//////////////////////////////////
// Create and handle the slider //
//////////////////////////////////
function resize() {
  // Get the new values
  slideWidth  = gsap.getProperty('.slide', 'width');
  slideHeight = gsap.getProperty('.slide', 'height');
  
  const widthUnit = (slideWidth + gap);
  wrapWidth = numSlides * widthUnit;
  wrapVal = gsap.utils.wrap(0, wrapWidth);
  
  // Setup our slider with the new values
  const pxOffset = lastIndex !== numSlides - 1 ? lastIndex * -widthUnit : widthUnit;
  gsap.set(slideContainer, { left: -widthUnit });
  gsap.set(proxy, { x: pxOffset });
  
  // The animation that's used to do the sliding
  animation = gsap.fromTo(slides, {
    x: i => wrapVal(i * widthUnit)
  }, {
    duration: 1,
    x: `+=${wrapWidth}`,
    ease: "none",
    paused: true,
    // This creates the infinite looping
    modifiers: {
      x: function(x, target) {
        return `${parseInt(x) % wrapWidth}px`;
      }
    }
  })
  // Set progress to correct value
  .progress(1 - lastIndex / numSlides);
  
  // Kill off the old draggable instance
  if(draggable) {
    draggable.kill();
  }
  
  // Recreate the draggable with the new values
  draggable = Draggable.create(proxy, {
    type: "x",
    trigger: ".wrapper",
    inertia: true,
    snap: { 
      x: (x) => {
        return Math.round(x / widthUnit) * widthUnit;
      } 
    },
    
    // Our event listeners
    onPress: onPress,
    onDragStart: () => webGLSlider.anim.timeScale(-3),
    onDrag: updateProgress,
    onRelease: onRelease,
    onDragEnd: endPress,
    onThrowUpdate: updateProgress,
    onThrowComplete: () => webGLSlider.anim.play()
  })[0];
  
  // Update the resolution in the WebGL planes
  planes.forEach(plane => plane.uniforms.resolution.value = [innerWidth, innerHeight]);
}

// Update the slider along with the necessary WebGL variables
function updateProgress() {
  // Update the actual slider
  animation.progress(wrapVal(this.x) / wrapWidth);
  
  // Update the WebGL slider planes
  planes.forEach(plane => plane.updatePosition());
  
  // Update the WebGL "mouse"
  updateWebGLMouse(0);
  
  // Affect the displacement based on the drag velocity
  if(this.isDragging) {
    let velocity = InertiaPlugin.getVelocity(proxy, "x");
    velocity > halfMaxVelocity ? velocity = halfMaxVelocity : null;
    velocity = Math.abs(velocity);
    gsap.to(disp, {
      power: velocity / maxVelocity + 0.5,
      onUpdate: updatePower,
      overwrite: 'auto',
      duration: 0.2,
      ease: "power4.out"
    });
  }
}


//////////////////////////////////
// Keep mouse synced with WebGL //
//////////////////////////////////
const mouse = new Vec2(0, 0);
function addMouseListeners() {
  if ("ontouchstart" in window) {
    wrapper.addEventListener("touchstart", updateMouse, false);
    wrapper.addEventListener("touchmove", updateMouse, false);
    wrapper.addEventListener("blur", mouseOut, false);
  } else {
    wrapper.addEventListener("mousemove", updateMouse, false);
    wrapper.addEventListener("mouseleave", mouseOut, false);
  }
}

// Update the stored mouse position along with WebGL "mouse"
function updateMouse(e) {
  radiusAnim.play();
  
  if (e.changedTouches && e.changedTouches.length) {
    e.x = e.changedTouches[0].pageX;
    e.y = e.changedTouches[0].pageY;
  }
  if (e.x === undefined) {
    e.x = e.pageX;
    e.y = e.pageY;
  }
  
  mouse.x = e.x;
  mouse.y = e.y;
  
  
  updateWebGLMouse();
}

// Updates the mouse position for all planes
function updateWebGLMouse(dur) {
  // update the planes mouse position uniforms
  planes.forEach((plane, i) => {
    const webglMousePos = plane.mouseToPlaneCoords(mouse);
    updatePlaneMouse(plane, webglMousePos, dur);
  });
}

// Updates the mouse position for the given plane
function updatePlaneMouse(plane, endPos = new Vec2(0, 0), dur = 0.1) {
  gsap.to(plane.uniforms.mouse.value, {
    x: endPos.x,
    y: endPos.y,
    duration: dur,
    overwrite: true,
  });
}

// When the mouse leaves the slider, animate the WebGL "mouse" to the center of slider
function mouseOut(e) {
  planes.forEach((plane, i) => updatePlaneMouse(plane, new Vec2(0, 0), 1) );
  
  radiusAnim.reverse();
}

////////////////////////////////
// Radius hover functionality //
////////////////////////////////
const radius = { val: 0.1 };
const radiusAnim = gsap.from(radius, { 
  val: 0, 
  duration: 0.3, 
  paused: true,
  onUpdate: updateRadius
});
function updateRadius() {
  planes.forEach((plane, i) => {
    plane.uniforms.radius.value = radius.val;
  });
}

//////////////////////////////////////
// Image displacement functionality //
//////////////////////////////////////
const disp = { power: 0, intensity: 0.1 };
const intensityAnim = gsap.to(disp, {
  intensity: 0.2,
  onUpdate: updateIntensity,
  yoyo: true, 
  repeat: -1,
  duration: 4,
  ease: "power1.inOut"
});
// Animate in the displacement
function onPress() {
  gsap.to(disp, {
    power: 0.5,
    onUpdate: updatePower,
    overwrite: 'auto',
    duration: 0.5,
  });
}

function onRelease() {
  // Update our index (and headings if need be)
  checkIndex(this.endX);
  
  endPress();
}

// Animate out the displacement
function endPress() {
  gsap.to(disp, {
    power: 0,
    onUpdate: updatePower,
    overwrite: 'auto',
    duration: draggable.tween ? draggable.tween.duration() : 0.5,
  });
}

// Update the displacement power value in our planes
function updatePower() {
  planes.forEach((plane, i) => plane.uniforms.power.value = disp.power );
}
// Update the displacement intensity value in our planes
function updateIntensity() {
  planes.forEach((plane, i) =>  plane.uniforms.intensity.value = disp.intensity );
}


//////////////////////
// Arrow hover code //
//////////////////////
const arrowLinks = document.querySelectorAll(".arrow-link");
const playLinkAnim = (e) => e.target.anim.play();
const reverseLinkAnim = (e) => e.target.anim.reverse();
function setupArrowLinks() {
  arrowLinks.forEach(link => {
    if(link.anim) {
      link.anim.kill();
      link.removeEventListener("mouseenter", playLinkAnim);
      link.removeEventListener("mouseleave", reverseLinkAnim);
    }

    link.svg = link.querySelector("svg");
    link.path = link.svg.querySelector("path");
    link.anim = gsap.timeline({
      paused: true,
      defaults: {
        duration: 0.3
      }
    })
      .from(link.svg, {
      attr: { 
        width: 44,
        height: 36,
        viewBox: "0 0 44 36"
      }
    })
      .fromTo(link.path, {
      attr: { d: "M25.5295 0.48999L21.8195 4.33999L33.5095 15.19H0.189453V20.3H33.5795L21.8195 31.22L25.5295 35.07L43.4495 17.78L25.5295 0.48999Z" }
    }, {
      attr: { d: "M58.5295 0L54.8195 3.85L66.5095 14.7H0.189453V19.81H66.5795L54.8195 30.73L58.5295 34.58L76.4495 17.29L58.5295 0Z" }
    }, 0)

    link.addEventListener("mouseenter", playLinkAnim);
    link.addEventListener("mouseleave", reverseLinkAnim);
  });
}


////////////////
// Init stuff //
////////////////
window.addEventListener("load", e => {
  window.addEventListener("resize", resize);
  resize();

  checkIndex();
  
  // Create a new curtains instance
  const curtains = new Curtains({ container: "canvas", autoRender: false });
  // Use a single rAF for both GSAP and Curtains
  function renderScene() {
    curtains.render();
  }
  gsap.ticker.add(renderScene);
  // Create a curtains plane for each slide
  const planeElements = document.querySelectorAll(".slide");
  
  // Params passed to the curtains instance
  const supportsWebGL2 = curtains.renderer._isWebGL2;
  const params = {
    vertexShader: supportsWebGL2 ? v300Shader : v100Shader, // The vertex shader we want to use
    fragmentShader: supportsWebGL2 ? f300Shader : f100Shader, // The fragment shader we want to use
    
    // The variables that we're going to be animating to update our WebGL state
    uniforms: {
      // For the cursor effects
      mouse: { 
        name: "uMouse", // The shader variable name
        type: "2f",     // The type for the variable - https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html
        value: mouse    // The initial value to use
      },
      radius: { 
        name: "uRadius",
        type: "1f",
        value: radius.val
      },
      
      // For the displacement
      power: { 
        name: "uPower",
        type: "1f", 
        value: disp.power
      },
      intensity: { 
        name: "uIntensity",
        type: "1f", 
        value: disp.intensity
      },
      
      // For the antialiasing
      resolution: { 
        name: "uResolution",
        type: "2f", 
        value: [innerWidth, innerHeight] 
      }
    },
  };
  
  // Create a new plane for each slider
  planeElements.forEach((planeEl, i) => {
    const plane = new Plane(curtains, planeEl, params);

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

        // onReady is called once our plane is ready and all its texture have been created
        plane.onLoading(function(texture) {
          // Scale up the texture (for the translation effect)
          texture.setScale(new Vec2(1.2, 1.2));
        }).onReady(function() {
          // One could use "this" instead of "plane" here 
          
          // Add a "loaded" class to display image container
          plane.htmlElement.closest(".slide").classList.add("loaded");
        });
    }
  });
  
  // Add our mouse listeners to our slider
  addMouseListeners();
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.5.1/gsap.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.5.1/Draggable.min.js
  3. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/InertiaPlugin.min.js
  4. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/SplitText3.min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.5.1/ScrollTrigger.min.js
  6. https://cdn.jsdelivr.net/npm/curtainsjs@7.2.1/dist/curtains.umd.min.js