Get the scroll position associated with a particular ScrollTriggered animation
Perhaps you want to scroll the page to the exact spot where a particular scroll-triggered animation starts (or ends or any progress value) - just feed this helper function your animation (it must have a ScrollTrigger of course) and optionally a progress value (0 is when the animation starts, 0.5 is halfway through, 1 is the end) and it'll return the scroll position which you could feed into a scrollTo tween, for example:
function getScrollPosition(animation, progress) {
let p = gsap.utils.clamp(0, 1, progress || 0),
st = animation.scrollTrigger,
containerAnimation = st.vars.containerAnimation;
if (containerAnimation) {
let time = st.start + (st.end - st.start) * p;
st = containerAnimation.scrollTrigger;
return (
st.start + (st.end - st.start) * (time / containerAnimation.duration())
);
}
return st.start + (st.end - st.start) * p;
}
It even works with the "containerAnimation" feature:
<div class="panel description">
<h1>getScrollPosition() helper</h1>
<p>This helper function even works with the "containerAnimation" feature as seen here! Click the button below to scroll this page to the exact spot where the red section's tween starts.
<br><br><button id="scrollTo">scroll to pink tween start</button>
</p>
</div>
<div class="scroll-down">Scroll down<div class="arrow"></div>
</div>
<div class="container">
<section class="panel">
<div>
<pre class="code-block"><code class="language-js">gsap.to(".box-1", {
y: -130,
duration: 2,
ease: "elastic",
scrollTrigger: {
trigger: ".box-1",
containerAnimation: scrollTween,
start: "left center",
toggleActions: "play none none reset"
}
});</code></pre>
<p>Fire an animation at a particular spot...</p>
</div>
<div class="box-1 box gradient-pink">box-1</div>
</section>
<section class="panel gray">
<div>
<pre class="code-block"><code class="language-js">gsap.to(".box-2", {
y: -120,
ease: "none",
scrollTrigger: {
trigger: ".box-2",
containerAnimation: scrollTween,
start: "center 80%",
end: "center 20%",
scrub: true
}
});</code></pre>
<p>...or scrub it back & forth with the scrollbar</p>
</div>
<div class="box-2 box">box-2</div>
</section>
<section class="panel">
<div>
<pre class="code-block"><code class="language-js">ScrollTrigger.create({
trigger: ".box-3",
containerAnimation: scrollTween,
toggleClass: "active",
start: "center 60%"
});</code></pre>
<p>Toggle a CSS class</p>
</div>
<div class="box-3 box">box-3</div>
</section>
<section class="panel green">
<div>
<pre class="code-block"><code class="language-js">ScrollTrigger.create({
trigger: ".green",
containerAnimation: scrollTween,
start: "center 65%",
end: "center 51%",
onEnter: () => console.log("enter"),
onLeave: () => console.log("leave"),
onEnterBack: () => console.log("enterBack"),
onLeaveBack: () => console.log("leaveBack"),
onToggle: self => console.log("active", self.isActive)
});</code></pre>
<p>Use the rich callback system</p>
</div>
</section>
</div>
<div class="panel plain final">
<div>
<p>Here are a few caveats to keep in mind:</p>
<ul>
<li>The fake-scrolling animation (just the part that's moving the container horizontally) must have no easing (<code>ease: "none"</code>).</li>
<li>Pinning and snapping won't work on ScrollTriggers with a <code>containerAnimation</code>.</li>
<li>The mapping of scroll position trigger points are based on the trigger element itself not being animated horizontally (inside the container). If you need to animate the trigger, you can either wrap it in a <div> and use that as the trigger instead or just factor the trigger's movement into your end position. For example, if you animate it left 100px, make the <code>end</code> 100px further to the left.</li>
<li>Requires ScrollTrigger 3.8.0 or later</li>
</ul>
</div>
</div>.box.active {
background: var(--gradient-orange-crush),
url('/demo-assets/noise.webp');
background-blend-mode: color-dodge;
}
.description, .final {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
width: 500%;
height: 100%;
display: flex;
flex-wrap: nowrap;
}
p {
max-width: 75ch;
}gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
// the magic helper function...
function getScrollPosition(animation, progress) {
let p = gsap.utils.clamp(0, 1, progress || 0),
nested = !animation.scrollTrigger,
st = nested ? animation.parent.scrollTrigger : animation.scrollTrigger,
containerAnimation = st.vars.containerAnimation,
range = st.end - st.start,
position = st.start + range * p;
if (containerAnimation) {
st = containerAnimation.scrollTrigger;
return (st.start + (st.end - st.start) * (position / containerAnimation.duration()));
} else if (nested) {
let start = st.start + (animation.startTime() / animation.parent.duration()) * range,
end = st.start + ((animation.startTime() + animation.duration()) / animation.parent.duration()) * range;
return start + (end - start) * p;
}
return position;
}
let sections = gsap.utils.toArray(".container .panel");
let scrollTween = gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none", // <-- IMPORTANT!
scrollTrigger: {
trigger: ".container",
pin: true,
scrub: 0.1,
//snap: directionalSnap(1 / (sections.length - 1)),
end: "+=3000",
markers: true,
}
});
gsap.set(".box-1, .box-2", {y: 100});
ScrollTrigger.defaults({markers: {startColor: "white", endColor: "white"}});
// pink section
let pinkTween = gsap.to(".box-1", {
y: -130,
duration: 2,
ease: "elastic",
scrollTrigger: {
trigger: ".box-1",
containerAnimation: scrollTween,
start: "left center",
toggleActions: "play none none reset",
id: "1",
}
});
document.querySelector("#scrollTo").addEventListener("click", () => {
gsap.to(window, {scrollTo: getScrollPosition(pinkTween)});
});
// gray section
gsap.to(".box-2", {
y: -120,
ease: "none",
scrollTrigger: {
trigger: ".box-2",
containerAnimation: scrollTween,
start: "center 80%",
end: "center 20%",
scrub: true,
id: "2"
}
});
// purple section
ScrollTrigger.create({
trigger: ".box-3",
containerAnimation: scrollTween,
toggleClass: "active",
start: "center 60%",
id: "3"
});
// green section
ScrollTrigger.create({
trigger: ".green",
containerAnimation: scrollTween,
start: "left 30%",
end: "right 80%",
onEnter: () => console.log("enter"),
onLeave: () => console.log("leave"),
onEnterBack: () => console.log("enterBack"),
onLeaveBack: () => console.log("leaveBack"),
onToggle: self => console.log("active", self.isActive),
id: "4"
});
// only show the relevant section's markers at any given time
gsap.set(".gsap-marker-start, .gsap-marker-end, .gsap-marker-scroller-start, .gsap-marker-scroller-end", {autoAlpha: 0});
["red","gray","purple","green"].forEach((triggerClass, i) => {
ScrollTrigger.create({
trigger: "." + triggerClass,
containerAnimation: scrollTween,
start: "left 30%",
end: i === 3 ? "right right" : "right 30%",
markers: false,
onToggle: self => gsap.to(".marker-" + (i+1), {duration: 0.25, autoAlpha: self.isActive ? 1 : 0})
});
});
// helper function for causing the sections to always snap in the direction of the scroll (next section) rather than whichever section is "closest" when scrolling stops.
// function directionalSnap(increment) {
// let snapFunc = gsap.utils.snap(increment);
// return (raw, self) => {
// let n = snapFunc(raw);
// return Math.abs(n - raw) < 1e-4 || (n < raw) === self.direction < 0 ? n : self.direction < 0 ? n - increment : n + increment;
// };
// }