JSFiddle - React, Tailwind, and code Playground

by Michael Mandarino

HTML

<script src="https://unpkg.co/gsap@3/dist/gsap.min.js"></script>
<script src="https://unpkg.com/gsap@3/dist/MotionPathPlugin.min.js"></script>
<script src="https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js"></script>
<div id="scrollDist"></div>

<div id="container">
<style type="text/css">
                  .st0 {
                    fill: #003878;
                }

                .st1 {
                    fill: #FFFFFF;
                }

                .st2 {
                    fill: none;
                    stroke: #000000;
                    stroke-width: 3;
                    stroke-miterlimit: 10;
                }

                .st3 {
                    fill: #D60909;
                }
            </style>

  <svg id="map" width="1600" height="2000">

    
    <path id="route" class="st2" d="M281,164.7C166.8,420.6,1457.9,140,975.7,309.8S200.6,447,237.2,776.8
		C274.6,1113.3,1389,56.5,1473.6,568.2" />

    <path id="point" class="st3" d="M821.767 105.048c0-5.546-4.503-10.05-10.05-10.05h-24.531c-5.547 0-10.05 4.504-10.05 10.05l20.367 30.516c5.547 0 24.264-30.516 24.264-30.516z" />

  </svg>

</div>

CSS

html,
body {
  width: 100%;
  height: 100vh;
}

#map {
  position: absolute;
  overflow: visible;
}

JavaScript

// special definition of 'duration'
// of the scrubbed ScrollTrigger animation
// The higher the value, the slower the animation
var scrollDist = 25;

gsap.set("#scrollDist", {
  width: "100%",
  height: () => window.innerHeight * (scrollDist + 3)
});

// ==========
var map = document.getElementById("map"),
  mapWidth = map.getBoundingClientRect().width,
  mapHeight = map.getBoundingClientRect().height;

gsap.set("#container", {
  position: "fixed",
  width: mapWidth,
  height: mapHeight,
  left: "50%",
  xPercent: -50,
  top: 0,
  autoAlpha: 1
});

//tween the route + point
gsap
  .timeline({
    defaults: {
      duration: 1,
      ease: "none"
    },
    scrollTrigger: {
      trigger: "#scrollDist",
      start: "top top",
      end: () => window.innerHeight * scrollDist,
      scrub: 0.3,
    }
  })
  .to(
    "#point", {
      motionPath: {
        path: "#route",
        align: "#route"
      }
    },
    0
  );


//move the container so that the moving 'point'
//is always in the same place

let povDelay = 1, // 1 = no delay
  pos = {
    x: -mapWidth / 2,
    y: -mapHeight / 2
  },
  xSet = gsap.quickSetter("#container", "x", "px"),
  ySet = gsap.quickSetter("#container", "y", "px");

gsap.ticker.add(() => {
  pos.x += (-gsap.getProperty("#point", "x") - pos.x) * 1;
  pos.y += (-gsap.getProperty("#point", "y") - pos.y) * 1;
  xSet(pos.x);
  ySet(pos.y);
});

// = onresize ===========
window.onresize = () => {
  gsap.set("#scrollDist", {
    width: "100%",
    height: () => window.innerHeight * (scrollDist + 3)
  });
};