Scroll-triggered Card Carousel With GSAP

Category: Javascript , Slider | February 17, 2021
Author: GreenSock
Views Total: 639 views
Official Page: Go to website
Last Update: February 17, 2021
License: MIT

Preview:

Scroll-triggered Card Carousel With GSAP

Description:

An infinitely scrolling card carousel implemented with the GSAP Animation Library.

See it in action:

See the Pen
Infinite Scrolling Cards with GSAP and ScrollTrigger (continuous snap)
by GreenSock (@GreenSock)
on CodePen.

How to use it:

1. Add cards together with navigation controls to the carousel.

<div class="carousel">
  <ul class="cards">
    <li>0</li>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    ...
  </ul>
  <div class="actions">
    <button class="prev">Prev</button>
    <button class="next">Next</button>
  </div>
</div>

2. The core CSS/CSS3 styles for the carousel.

.carousel {
  position: absolute;
  width: 100%;
  height: 100vh;
  overflow: hidden;
}

.cards {
  position: absolute;
  width: 14rem;
  height: 18rem;
  top: 40%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.cards li {
  list-style: none;
  padding: 0;
  margin: 0;
  width: 14rem;
  height: 18rem;
  text-align: center;
  line-height: 18rem;
  font-size: 2rem;
  font-family: sans-serif;
  background-color: #9d7cce;
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 0.8rem;
}

.actions {
  position: absolute;
  bottom: 25px;
  left: 50%;
  transform: translateX(-50%);
}

3. Load the needed GSAP and ScrollTrigger JavaScript libraries in the document.

<script src='https://unpkg.co/[email protected]/dist/gsap.min.js'></script>
<script src='https://unpkg.com/[email protected]/dist/ScrollTrigger.min.js'></script>

4. The core JavaScript to activate the carousel.

gsap.registerPlugin(ScrollTrigger);
// gets iterated when we scroll all the way to the end or start and wraps around - allows us to smoothly continue the playhead scrubbing in the correct direction.
let iteration = 0; 
// spacing of the cards (stagger)
const spacing = 0.1,    
  snap = gsap.utils.snap(spacing), // we'll use this to snap the playhead on the seamlessLoop
  cards = gsap.utils.toArray('.cards li'),
  seamlessLoop = buildSeamlessLoop(cards, spacing),
  scrub = gsap.to(seamlessLoop, { // we reuse this tween to smoothly scrub the playhead on the seamlessLoop
    totalTime: 0,
    duration: 0.5,
    ease: "power3",
    paused: true
  }),
  trigger = ScrollTrigger.create({
    start: 0,
    onUpdate(self) {
      if (self.progress === 1 && self.direction > 0 && !self.wrapping) {
        wrapForward(self);
      } else if (self.progress < 1e-5 && self.direction < 0 && !self.wrapping) {
        wrapBackward(self);
      } else {
        scrub.vars.totalTime = snap((iteration + self.progress) * seamlessLoop.duration());
        scrub.invalidate().restart(); // to improve performance, we just invalidate and restart the same tween. No need for overwrites or creating a new tween on each update.
        self.wrapping = false;
      }
    },
    end: "+=3000",
    pin: ".gallery"
  });
// when the ScrollTrigger reaches the end, loop back to the beginning seamlessly
function wrapForward(trigger) { 
  iteration++;
  trigger.wrapping = true;
  trigger.scroll(trigger.start + 1);
}
// when the ScrollTrigger reaches the start again (in reverse), loop back to the end seamlessly
function wrapBackward(trigger) { 
  iteration--;
  if (iteration < 0) { // to keep the playhead from stopping at the beginning, we jump ahead 10 iterations
    iteration = 9;
    seamlessLoop.totalTime(seamlessLoop.totalTime() + seamlessLoop.duration() * 10);
    scrub.pause(); // otherwise it may update the totalTime right before the trigger updates, making the starting value different than what we just set above. 
  }
  trigger.wrapping = true;
  trigger.scroll(trigger.end - 1);
}
// moves the scroll position to the place that corresponds to the totalTime value of the seamlessLoop, and wraps if necessary.
function scrubTo(totalTime) { 
  let progress = (totalTime - seamlessLoop.duration() * iteration) / seamlessLoop.duration();
  if (progress > 1) {
    wrapForward(trigger);
  } else if (progress < 0) {
    wrapBackward(trigger);
  } else {
    trigger.scroll(trigger.start + progress * (trigger.end - trigger.start));
  }
}
document.querySelector(".next").addEventListener("click", () => scrubTo(scrub.vars.totalTime + spacing));
document.querySelector(".prev").addEventListener("click", () => scrubTo(scrub.vars.totalTime - spacing));
function buildSeamlessLoop(items, spacing) {
  let overlap = Math.ceil(1 / spacing), // number of EXTRA animations on either side of the start/end to accommodate the seamless looping
    startTime = items.length * spacing + 0.5, // the time on the rawSequence at which we'll start the seamless loop
    loopTime = (items.length + overlap) * spacing + 1, // the spot at the end where we loop back to the startTime 
    rawSequence = gsap.timeline({paused: true}), // this is where all the "real" animations live
    seamlessLoop = gsap.timeline({ // this merely scrubs the playhead of the rawSequence so that it appears to seamlessly loop
      paused: true,
      repeat: -1, // to accommodate infinite scrolling/looping
      onRepeat() { // works around a super rare edge case bug that's fixed GSAP 3.6.1
        this._time === this._dur && (this._tTime += this._dur - 0.01);
      }
    }),
    l = items.length + overlap * 2,
    time = 0,
    i, index, item;
  // set initial state of items
  gsap.set(items, {xPercent: 400, opacity: 0, scale: 0});
  // now loop through and create all the animations in a staggered fashion. Remember, we must create EXTRA animations at the end to accommodate the seamless looping.
  for (i = 0; i < l; i++) {
    index = i % items.length;
    item = items[index];
    time = i * spacing;
    rawSequence.fromTo(item, {scale: 0, opacity: 0}, {scale: 1, opacity: 1, zIndex: 100, duration: 0.5, yoyo: true, repeat: 1, ease: "power1.in", immediateRender: false}, time)
               .fromTo(item, {xPercent: 400}, {xPercent: -400, duration: 1, ease: "none", immediateRender: false}, time);
    i <= items.length && seamlessLoop.add("label" + i, time); // we don't really need these, but if you wanted to jump to key spots using labels, here ya go.
  }
  // here's where we set up the scrubbing of the playhead to make it appear seamless. 
  rawSequence.time(startTime);
  seamlessLoop.to(rawSequence, {
    time: loopTime,
    duration: loopTime - startTime,
    ease: "none"
  }).fromTo(rawSequence, {time: overlap * spacing + 1}, {
    time: startTime,
    duration: startTime - (overlap * spacing + 1),
    immediateRender: false,
    ease: "none"
  });
  return seamlessLoop;
}

You Might Be Interested In:


Leave a Reply