Instructions

Webflow  Template User  Guide
GSAP Setup & Main Functions
All animations and interactions in this script use GSAP.
The code is structured into three main features: marquee animation, custom cursor, and project card hover effects.

You can find and edit this code inside the Embed Code section of the template.
1. Marquee Animation
This script creates an infinite horizontal scrolling effect.
It clones the content automatically so the animation runs smoothly without gaps.
The speed can be controlled using the duration value.
When the window is resized, the animation recalculates to stay accurate.
  /* ── 1. MARQUEE ─────────────────────────────────────── */
function initMarquee() {
  var marquee = document.querySelector(".marquee");
  if (!marquee) return;

  var content = marquee.querySelector(".marque-content");
  if (!content) return;

  var duration = 20;

  // Make sure a clone exists (avoid duplicates on resize)
  if (!marquee.querySelector(".marque-content + .marque-content")) {
    var clone = content.cloneNode(true);
    marquee.appendChild(clone);
  }

  var allItems = marquee.querySelectorAll(".marque-content");
  var tween;

  function getWidth() {
    // Force reflow so offsetWidth is accurate on mobile
    return content.getBoundingClientRect().width;
  }

  function start() {
    var width = getWidth();

    // Guard: if width is still 0, retry after the browser finishes painting
    if (width === 0) {
      requestAnimationFrame(start);
      return;
    }

    if (tween) tween.kill();
    gsap.set(allItems, { x: 0 });

    tween = gsap.fromTo(
      allItems,
      { x: 0 },
      {
        x: -width,
        duration: duration,
        ease: "none",
        repeat: -1,
        modifiers: {
          x: gsap.utils.unitize(function (x) {
            return parseFloat(x) % width; // seamless modulo loop
          })
        }
      }
    );
  }

  // Small delay so the browser can compute layout on mobile
  setTimeout(start, 100);

  var resizeTimer;
  window.addEventListener("resize", function () {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(start, 300);
  });
}
2. Custom Cursor
This script replaces the default mouse cursor with a smooth animated cursor.
The cursor follows the mouse position using a lerp (smooth interpolation) effect.
It automatically fades in when the mouse moves and can be paused when interacting with specific elements like cards.
  /* ── 2. CUSTOM CURSOR ───────────────────────────────── */
  function initCursor() {
    var cursor = document.querySelector(".cursor-core");
    if (!cursor) return;

    var mouseX = window.innerWidth / 2;
    var mouseY = window.innerHeight / 2;
    var x = mouseX;
    var y = mouseY;
    var speed = 0.15;
    var visible = false;
    var paused = false;

    cursor.style.opacity = "0";

    function setPos(px, py) {
      cursor.style.transform =
        "translate(" + px + "px, " + py + "px) translate(-50%, -50%)";
    }

    function animate() {
      x += (mouseX - x) * speed;
      y += (mouseY - y) * speed;
      setPos(x, y);
      requestAnimationFrame(animate);
    }

    function showCursor() {
      if (visible) return;
      visible = true;
      cursor.style.opacity = "1";
    }

    window.addEventListener("mousemove", function (e) {
      if (paused) return;
      mouseX = e.clientX;
      mouseY = e.clientY;
      showCursor();
    });

    animate();

    window._cursorPause = function () {
      paused = true;
      cursor.style.opacity = "0";
    };
    window._cursorResume = function () {
      paused = false;
      cursor.style.opacity = "1";
    };
  }
3. Project Card Hover Effect
This script controls the hover interaction for:
.project-image-wrap
.testimonial-content-wrap
When the user hovers over these elements:
A custom icon appears smoothly.
The icon follows the mouse movement inside the card.
When the mouse leaves, the icon fades out.
/* ── 3. PROJECT CARD HOVER ICON ─────────────────────── */
  function initProjectCards() {
    var cards = document.querySelectorAll(
  ".project-image-wrap, .testimonial-content-wrap");

    cards.forEach(function (card) {
      var icon = card.querySelector(".icon-project-wrap");
      if (!icon) return;

      gsap.set(icon, {
        xPercent: -50,
        yPercent: -50,
        opacity: 0,
        scale: 0.6,
        pointerEvents: "none"
      });

      var isInside = false;

      card.addEventListener("mouseenter", function (e) {
        isInside = true;
        if (window._cursorPause) window._cursorPause();

        var rect = card.getBoundingClientRect();
        gsap.killTweensOf(icon);
        gsap.set(icon, {
          left: e.clientX - rect.left,
          top: e.clientY - rect.top
        });
        gsap.to(icon, {
          opacity: 1, scale: 1,
          duration: 0.4, ease: "back.out(1.5)"
        });
      });

      card.addEventListener("mousemove", function (e) {
        if (!isInside) return;
        var rect = card.getBoundingClientRect();
        gsap.to(icon, {
          left: e.clientX - rect.left,
          top: e.clientY - rect.top,
          duration: 0.35,
          ease: "power2.out",
          overwrite: "auto"
        });
      });

      card.addEventListener("mouseleave", function () {
        isInside = false;
        if (window._cursorResume) window._cursorResume();
        gsap.killTweensOf(icon);
        gsap.to(icon, {
          opacity: 0, scale: 0.6,
          duration: 0.25, ease: "power2.out"
        });
      });

      document.addEventListener("mouseover", function (e) {
        if (!isInside) return;
        if (!card.contains(e.target)) {
          isInside = false;
          if (window._cursorResume) window._cursorResume();
          gsap.killTweensOf(icon);
          gsap.to(icon, {
            opacity: 0, scale: 0.6,
            duration: 0.25, ease: "power2.out"
          });
        }
      });
    });
  }
4. Card Image (Hero Section)
<script>
document.addEventListener('DOMContentLoaded', function () {

  var wrapper = document.querySelector('.hero-image-wrapper');
  if (!wrapper) return;

  var cards = [
    document.querySelector('.image-hero-wrap.badges-1'),
    document.querySelector('.image-hero-wrap.badges-2'),
    document.querySelector('.image-hero-wrap.badges-3')
  ].filter(Boolean);

  if (cards.length < 3) return;

  /* Compute slot offsets in pixels using viewport width.
     Slot 0 = front, Slot 1 = mid, Slot 2 = back.
     Using x/y (transform) instead of left/bottom keeps GSAP
     within Webflow's approved animation pattern. */
  function getSlots() {
    var unit = window.innerWidth / 100; // 1vw in px
    return [
      { x: 0,        y: 0,         zIndex: 3, scale: 1,    opacity: 1    },
      { x: unit,     y: -unit,     zIndex: 2, scale: 0.97, opacity: 0.92 },
      { x: unit * 2, y: -unit * 2, zIndex: 1, scale: 0.94, opacity: 0.84 }
    ];
  }

  var order = [0, 1, 2];
  var isAnimating = false;
  var DUR = 1.1;
  var DELAY = 2760; // ms before first cycle

  // Initialize cards at their starting slots
  var initial = getSlots();
  cards.forEach(function (card, i) {
    gsap.set(card, {
      x: initial[i].x,
      y: initial[i].y,
      scale: initial[i].scale,
      opacity: initial[i].opacity,
      zIndex: initial[i].zIndex
    });
  });

  function cycle() {
    if (isAnimating) return;
    isAnimating = true;

    var s = getSlots();
    var front = cards[order[0]];
    var mid   = cards[order[1]];
    var back  = cards[order[2]];

    var tl = gsap.timeline({
      defaults: { duration: DUR, ease: "power3.inOut" },
      onComplete: function () {
        order.push(order.shift());
        isAnimating = false;
      }
    });

    // FRONT: shrink + fade out (first half of the cycle)
    tl.to(front, {
      scale: 0.6,
      opacity: 0,
      duration: DUR * 0.5
    }, 0);

    // MID moves up to the front slot
    tl.to(mid, {
      x: s[0].x,
      y: s[0].y,
      scale: s[0].scale,
      opacity: s[0].opacity,
      zIndex: s[0].zIndex
    }, 0.05);

    // BACK moves up to the mid slot
    tl.to(back, {
      x: s[1].x,
      y: s[1].y,
      scale: s[1].scale,
      opacity: s[1].opacity,
      zIndex: s[1].zIndex
    }, 0.1);

    // The moment FRONT finishes fading out, instantly teleport it
    // to the back slot (still invisible), then fade it back in so
    // it lands at full back-slot opacity by the time the rest
    // of the timeline finishes. No visible gap.
    tl.to(front, {
      duration: 0,
      x: s[2].x,
      y: s[2].y,
      scale: 0.94,
      zIndex: s[2].zIndex
    }, DUR * 0.5);

    tl.to(front, {
      opacity: s[2].opacity,
      duration: DUR * 0.5
    }, DUR * 0.5);
  }

  // Delay the first cycle by DELAY, then run every 3500ms
  setTimeout(function () {
    cycle();
    setInterval(cycle, 3500);
  }, DELAY);

});
</script>