<section class="demo">
<svg class="demo__svg" viewBox="0 0 640 320" fill="none">
<path id="route" d="M40 220C120 60 200 280 320 120C430 -10 500 250 600 90"></path>
<circle id="dot" r="7" cx="40" cy="220"></circle>
</svg>
</section>.demo {
min-height: 200vh;
display: grid;
place-items: center;
padding: 20vh 20px;
background: #08080a;
}
.demo__svg { width: min(640px, 100%); }
#route {
stroke: #2a2a32;
stroke-width: 1.4;
fill: none;
}
#dot { fill: #f4f4f2; }const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const dot = document.querySelector("#dot");
const path = document.querySelector("#route");
function place(progress) {
if (!path || typeof path.getPointAtLength !== "function") return;
const len = path.getTotalLength();
const point = path.getPointAtLength(len * progress);
gsap.set(dot, { attr: { cx: point.x, cy: point.y } });
}
if (reduce) {
place(1);
} else if (window.ScrollTrigger) {
const proxy = { t: 0 };
place(0);
gsap.to(proxy, {
t: 1,
ease: "none",
onUpdate: function () { place(proxy.t); },
scrollTrigger: {
trigger: ".demo",
start: "top 75%",
end: "bottom 35%",
scrub: 0.45,
},
});
} else {
place(0.35);
}