Instructions

Lenis Scroll

<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.15/dist/lenis.css" />
<script src="https://unpkg.com/lenis@1.3.15/dist/lenis.min.js"></script>

<script>
  const lenis = new Lenis({ duration: 1.4 });

  lenis.on('scroll', ScrollTrigger.update);
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });
  gsap.ticker.lagSmoothing(0);
</script>

Image Parallax

<script>
  document.addEventListener("DOMContentLoaded", () => {
  gsap.registerPlugin(ScrollTrigger);

  const mm = gsap.matchMedia();

  mm.add(
    {
      mobile: "(max-width: 479px)",
      mobileLandscape: "(min-width: 480px) and (max-width: 767px)",
      tablet: "(min-width: 768px) and (max-width: 991px)",
      desktop: "(min-width: 992px)"
    },
    (context) => {
      const {
        mobile,
        mobileLandscape,
        tablet,
        desktop
      } = context.conditions;

      document
        .querySelectorAll('[data-parallax="trigger"]')
        .forEach((trigger) => {

          // Disable parallax on selected breakpoints
          const disable = trigger.getAttribute("data-parallax-disable");

          if (
            (disable === "mobile" && mobile) ||
            (disable === "mobileLandscape" && mobileLandscape) ||
            (disable === "tablet" && tablet) ||
            (disable === "desktop" && desktop)
          ) {
            return;
          }

          // Target element
          const target =
            trigger.querySelector('[data-parallax="target"]') || trigger;

          // Direction
          const direction =
            trigger.getAttribute("data-parallax-direction") || "vertical";

          const property =
            direction === "horizontal" ? "xPercent" : "yPercent";

          // Parallax values
          const startValue = parseFloat(
            trigger.getAttribute("data-parallax-start")
          ) || -5;

          const endValue = parseFloat(
            trigger.getAttribute("data-parallax-end")
          ) || -20;

          // Scrub
          const scrubAttribute =
            trigger.getAttribute("data-parallax-scrub");

          const scrub =
            scrubAttribute !== null
              ? parseFloat(scrubAttribute)
              : true;

          // ScrollTrigger positions
          const scrollStart =
            trigger.getAttribute("data-parallax-scroll-start") ||
            "top bottom";

          const scrollEnd =
            trigger.getAttribute("data-parallax-scroll-end") ||
            "bottom top";

          gsap.fromTo(
            target,
            {
              [property]: startValue
            },
            {
              [property]: endValue,
              ease: "none",
              scrollTrigger: {
                trigger: trigger,
                start: `clamp(${scrollStart})`,
                end: `clamp(${scrollEnd})`,
                scrub: scrub,
                invalidateOnRefresh: true
              }
            }
          );
        });

      ScrollTrigger.refresh();
    }
  );
});
</script>

Coaches Section

<script>
  gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);
  function initImagesOnPathScroll() {
    const wrap = document.querySelector('[data-motionpath="wrap"]');
    const path = wrap.querySelector('[data-motionpath="path"]');
    const items = wrap.querySelectorAll('[data-motionpath="item"]');
    const itemDetails = wrap.querySelectorAll('[data-motionpath="item-details"]');

    // Set z-index on items, to make sure the 1st item is on top
    gsap.set(items, {
      zIndex: (i, target, all) => all.length - i,
    });

    // if there’s an old timeline, grab its progress, reset it, then kill it
    const oldTl = initImagesOnPathScroll.tl;
    let progress = 0;

    if (oldTl) {
      progress = oldTl.progress();
      oldTl.progress(0).kill();
    }

    // create a new timeline + ScrollTrigger
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: wrap,
        start: 'top top',
        end: 'bottom bottom',
        scrub: true,
      },
      defaults: {
        ease: 'none',
        stagger: 0.3, // Define the space between each item
      },
    });

    tl.to(items, {
      duration: 1,
      motionPath: { path, align: path, curviness: 2, alignOrigin: [0.5, 0.5] },
    })
      .fromTo(items, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.1 }, 0)
      .fromTo(items, { filter: 'blur(1.5em)' }, { filter: 'blur(0em)', duration: 0.5 }, 0)
      .fromTo(itemDetails, { autoAlpha: 0, yPercent: 25 }, { autoAlpha: 1, yPercent: 0, duration: 0.1 }, 0.5)
      .fromTo(items, { scale: 0.4 }, { scale: 1, duration: 0.65 }, 0)
      .to(items, { autoAlpha: 0, filter: 'blur(1em)', duration: 0.15 }, 0.85)
      .to(itemDetails, { autoAlpha: 0, duration: 0.05 }, 0.9);

    // jump back to previous spot and refresh
    tl.progress(progress);
    ScrollTrigger.refresh();

    // store it on the function so we can grab it next time
    initImagesOnPathScroll.tl = tl;

    // on first run bind a single debounced resize listener
    if (!initImagesOnPathScroll.resizeHandler) {
      initImagesOnPathScroll.resizeHandler = debounce(() => {
        initImagesOnPathScroll();
      }, 200);
      window.addEventListener('resize', initImagesOnPathScroll.resizeHandler);
    }

    return tl;
  }

  function debounce(fn, delay = 200) {
    let timeout;
    return () => {
      clearTimeout(timeout);
      timeout = setTimeout(fn, delay);
    };
  }

  // Initialize Follow SVG Path on Scroll
  document.addEventListener('DOMContentLoaded', () => {
    initImagesOnPathScroll();
  });
</script>