<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>
        </div>
        <div class="slide" data-name="Cadillac" data-url="#">
          <img src="https://unsplash.it/1920/823?random=2" data-sampler="planeTexture" crossorigin>
        </div>
        <div class="slide" data-name="Laso" data-url="#">
          <img src="https://unsplash.it/1920/823?random=3" data-sampler="planeTexture" 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>
        </div>
        <div class="slide" data-name="Zach Saucier" data-url="#">
          <img src="https://unsplash.it/1920/823?random=5" data-sampler="planeTexture" 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>
        </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;
}

#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;
}
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;

//////////////////////////////
// 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
    onDragStart: () => webGLSlider.anim.timeScale(-3),
    onDrag: updateProgress,
    onRelease: function() { checkIndex(this.endX) }, // Update our index (and headings if need be)
    onThrowUpdate: updateProgress,
    onThrowComplete: () => webGLSlider.anim.play()
  })[0];
}

// Update the slider along with the necessary WebGL variables
function updateProgress() {
  // Update the actual slider
  animation.progress(wrapVal(this.x) / wrapWidth);
}


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

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.1.4/dist/curtains.umd.min.js