How to create a buttery-smooth scroll trigger image & video effect with GSAP & Lenis

SHARE

SIGN UP FOR THE NEWSLETTER

Your subscription could not be saved. Please try again.
Your subscription has been successful.


Code:

Javascript/GSAP

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://unpkg.com/lenis@1.1.13/dist/lenis.min.js"></script>
   
<script>
 gsap.registerPlugin(ScrollTrigger);

// Initialize Lenis smooth scrolling
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
  lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);

const pinnedSection = document.querySelector(".pinned");
const pinnedHeight = window.innerHeight * 10; // Adjust based on your layout
const images = gsap.utils.toArray(".img");

function animateImageEntry(img) {
  gsap.fromTo(
    img,
    {
      scale: 1.25,
      clipPath: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
      opacity: 0,
    },
    {
      scale: 1,
      clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
      opacity: 1,
      duration: 1,
      ease: "power2.inOut",
    }
  );

  gsap.fromTo(
    img.querySelector("img"),
    {
      filter: "contrast(2) brightness(10)",
    },
    {
      filter: "contrast(1) brightness(1)",
      duration: 1,
      ease: "power2.inOut",
    }
  );
}

function animateImageExitForward(img) {
  gsap.to(img, {
    scale: 0.5,
    opacity: 0,
    duration: 1,
    ease: "power2.inOut",
  });
}

function animateImageExitReverse(img) {
  gsap.to(img, {
    scale: 1.25,
    clipPath: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
    duration: 1,
    ease: "power2.inOut",
  });

  gsap.to(img.querySelector("img"), {
    filter: "contrast(2) brightness(10)",
    duration: 1,
    ease: "power2.inOut",
  });
}

// Start by animating the first image if it exists
if (images.length > 0) {
  animateImageEntry(images[0]);
}

let lastCycle = 0;

ScrollTrigger.create({
  trigger: pinnedSection,
  start: "top top",
  end: `+=${pinnedHeight} * 2`,
  pin: true,
  pinSpacing: true,
  scrub: 0.1,
  onUpdate: (self) => {
    const totalProgress = self.progress * images.length; // Adjust for the number of images
    const currentCycle = Math.floor(totalProgress);
    const cycleProgress = (totalProgress % 1) * 100;

    if (currentCycle < images.length) {
      const currentImage = images[currentCycle];
      const scale = 1 - (0.25 * cycleProgress) / 100;
      gsap.to(currentImage, {
        scale: scale,
        duration: 0.1,
        overwrite: "auto",
      });

      if (currentCycle !== lastCycle) {
        if (self.direction > 0) {
          if (lastCycle < images.length) animateImageExitForward(images[lastCycle]);
          if (currentCycle < images.length) {
            animateImageEntry(images[currentCycle]);
          }
        } else {
          if (currentCycle < images.length) {
            animateImageEntry(images[currentCycle]);
          }
          if (lastCycle < images.length) animateImageExitReverse(images[lastCycle]);
        }
        lastCycle = currentCycle;
      }
    }
  },
});
</script>

CSS

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}


.lenis.lenis-smooth iframe {
  pointer-events: none;
}

.img {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(1.25);
  width: 65%;
  height: 90%;
  clip-path: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%);
   
}

.img img {
  filter: contrast(1) brightness(1);
}
/* add this in the parent container or the container holding your images & video */

.pinned {
    transition: 0s;
}



Video:

SIGN UP FOR THE NEWSLETTER

Your subscription could not be saved. Please try again.
Your subscription has been successful.