Canoply

GSAP Guide

(Footer) Marquee animations

This script enables customizable marquee animations on elements marked with specific attributes. It allows horizontal or vertical scrolling, reverse direction, and scroll-based interaction (like changing speed or direction based on scroll velocity). The animation can also be paused via hover or click interactions. Configuration is done using marquee-* custom attributes directly in the HTML.

Attributes and their functions:

marquee-element="component"
Identifies the main container that controls the marquee animation.

marquee-element="panel"
The moving content inside the marquee. This is the element that scrolls.

marquee-element="triggerhover"
Optional element that pauses the marquee animation on hover (only works on devices with a pointer, like a mouse).

marquee-element="triggerclick"
Optional element that toggles play/pause on click. It adds or removes the is-paused class.

marquee-speed
A number that controls the animation speed. Larger values = slower scroll. Calculated as panel width or height divided by this number.

marquee-vertical
If set to true, the marquee scrolls vertically instead of horizontally.

marquee-reverse
If set to true, reverses the scroll direction (e.g., scrolls right-to-left becomes left-to-right).

marquee-scrolldirection
If true, changes the scroll direction based on the user’s page scroll direction (down = forward, up = backward).

marquee-scrollscrub
If true, the speed of the marquee responds to the velocity of the page scroll — faster scroll = faster marquee.

<script>

// This Embed is for: Marquee animations on this page.

// MARQUEE POWER-UP
window.addEventListener("DOMContentLoaded", (event) => {
  // attribute value checker
  function attr(defaultVal, attrVal) {
    const defaultValType = typeof defaultVal;
    if (typeof attrVal !== "string" || attrVal.trim() === "") return defaultVal;
    if (attrVal === "true" && defaultValType === "boolean") return true;
    if (attrVal === "false" && defaultValType === "boolean") return false;
    if (isNaN(attrVal) && defaultValType === "string") return attrVal;
    if (!isNaN(attrVal) && defaultValType === "number") return +attrVal;
    return defaultVal;
  }
  // marquee component
  $("[marquee-element='component']").each(function (index) {
    let componentEl = $(this),
      panelEl = componentEl.find("[marquee-element='panel']"),
      triggerHoverEl = componentEl.find("[marquee-element='triggerhover']"),
      triggerClickEl = componentEl.find("[marquee-element='triggerclick']");
    let speedSetting = attr(100, componentEl.attr("marquee-speed")),
      verticalSetting = attr(false, componentEl.attr("marquee-vertical")),
      reverseSetting = attr(false, componentEl.attr("marquee-reverse")),
      scrollDirectionSetting = attr(false, componentEl.attr("marquee-scrolldirection")),
      scrollScrubSetting = attr(false, componentEl.attr("marquee-scrollscrub")),
      moveDistanceSetting = -100,
      timeScaleSetting = 1,
      pausedStateSetting = false;
    if (reverseSetting) moveDistanceSetting = 100;
    let marqueeTimeline = gsap.timeline({ repeat: -1, onReverseComplete: () => marqueeTimeline.progress(1) });
    if (verticalSetting) {
      speedSetting = panelEl.first().height() / speedSetting;
      marqueeTimeline.fromTo(panelEl, { yPercent: 0 }, { yPercent: moveDistanceSetting, ease: "none", duration: speedSetting });
    } else {
      speedSetting = panelEl.first().width() / speedSetting;
      marqueeTimeline.fromTo(panelEl, { xPercent: 0 }, { xPercent: moveDistanceSetting, ease: "none", duration: speedSetting });
    }
    let scrubObject = { value: 1 };
    ScrollTrigger.create({
      trigger: "body",
      start: "top top",
      end: "bottom bottom",
      onUpdate: (self) => {
        if (!pausedStateSetting) {
          if (scrollDirectionSetting && timeScaleSetting !== self.direction) {
            timeScaleSetting = self.direction;
            marqueeTimeline.timeScale(self.direction);
          }
          if (scrollScrubSetting) {
            let v = self.getVelocity() * 0.006;
            v = gsap.utils.clamp(-60, 60, v);
            let scrubTimeline = gsap.timeline({ onUpdate: () => marqueeTimeline.timeScale(scrubObject.value) });
            scrubTimeline.fromTo(scrubObject, { value: v }, { value: timeScaleSetting, duration: 0.5 });
          }
        }
      }
    });
    function pauseMarquee(isPausing) {
      pausedStateSetting = isPausing;
      let pauseObject = { value: 1 };
      let pauseTimeline = gsap.timeline({ onUpdate: () => marqueeTimeline.timeScale(pauseObject.value) });
      if (isPausing) {
        pauseTimeline.fromTo(pauseObject, { value: timeScaleSetting }, { value: 0, duration: 0.5 });
        triggerClickEl.addClass("is-paused");
      } else {
        pauseTimeline.fromTo(pauseObject, { value: 0 }, { value: timeScaleSetting, duration: 0.5 });
        triggerClickEl.removeClass("is-paused");
      }
    }
    if (window.matchMedia("(pointer: fine)").matches) {
      triggerHoverEl.on("mouseenter", () => pauseMarquee(true));
      triggerHoverEl.on("mouseleave", () => pauseMarquee(false));
    }
    triggerClickEl.on("click", function () {
      !$(this).hasClass("is-paused") ? pauseMarquee(true) : pauseMarquee(false);
    });
  });
});
</script>

(Site settings) Footer code - Text animations

This script reveals text words (and characters) with a smooth upward animation as they scroll into view. It uses GSAP, ScrollTrigger, and SplitText plugins. The animation is applied to any element with data-word-reveal="true". You can customize the animation speed and delay using HTML attributes.

Attributes and their functions:

data-word-reveal="true"
Required. Applies the scroll-based text reveal animation to the element’s child text.

data-word-duration
Optional. A number that controls the duration (speed) of the animation per word. Default is 0.8 seconds.

data-word-delay
Optional. A number that adds a delay before the animation starts. Default is 0 seconds.

<!-- GSAP Text animation -->

<script>
document.addEventListener("DOMContentLoaded", () => {
	if (typeof window.gsap === "undefined") document.documentElement.classList.add("gsap-not-found");
	gsap.registerPlugin(ScrollTrigger, SplitText);
});
</script>

<!-- GSAP Text animation -->

<script>
document.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll("[data-word-reveal='true']").forEach((text) => {
    const split = SplitText.create(text.children, {
      type: "words, chars",
      mask: "words",
      wordsClass: "word",
      charsClass: "char",
    });

    // Read custom attributes or use default values
    const duration = parseFloat(text.getAttribute("data-word-duration")) || 0.8;
    const delay = parseFloat(text.getAttribute("data-word-delay")) || 0;

    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: text,
        start: "top bottom",
        end: "top 90%",
        toggleActions: "none play none reset",
      },
    });

    tl.from(split.words, {
      yPercent: 110,
      delay: delay,
      duration: duration,
      stagger: { amount: 0.1 },
    });

    gsap.set(text, { visibility: "visible" });
  });
});
</script>

(Site settings) Footer code - Lenis Smooth Scroll

This script adds smooth scrolling to your website using Lenis. It also ensures compatibility with GSAP ScrollTrigger, so scroll-based animations work properly. GSAP drives Lenis via its internal ticker for full animation synchronization.

<!-- Lenis Smooth Scroll -->

<script src="https://unpkg.com/lenis@1.3.1/dist/lenis.min.js"></script>

<script>
document.addEventListener("DOMContentLoaded", () => {
  // Register necessary plugins (required for validation)
  gsap.registerPlugin(ScrollTrigger);

  // Initialize Lenis
  const lenis = new Lenis({
    duration: 1.2,
    easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // expo.out
    smooth: true,
    smoothTouch: false,
    touchMultiplier: 2
  });

  // Sync ScrollTrigger with Lenis
  lenis.on("scroll", ScrollTrigger.update);

  // Use GSAP's ticker to drive Lenis — this is required to pass GSAP validation
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });

  // Optional: Fire custom event to trigger delayed animations
  window.dispatchEvent(new CustomEvent("GSAPReady", {
    detail: { lenis }
  }));
});
</script>