Ritank Jaikar Posted July 30 Posted July 30 (edited) User has attempted to build a multi-state scroll-triggered animation, by building out a state switcher. Example code from a prototype I was working on: let currentState = -1; let isTransitioning = false; const TRANSITION_LOCK_DURATION = 900; const switcherTimeline = gsap.timeline({ paused: true }); ScrollTrigger.create({ trigger: ".section", start: "top center", end: "bottom bottom", onUpdate: (self) => { const progress = self.progress; // Map progress to states const newState = Math.min( Math.floor((progress / (1 - threshold)) * totalStates), totalStates - 1 ); if (newState !== currentState) { // Handle fast scroll if (isTransitioning) { switcherTimeline.progress(1, false); } isTransitioning = true; transition(newState, newState > currentState ? 1 : -1); currentState = newState; setTimeout(() => { isTransitioning = false; }, TRANSITION_LOCK_DURATION); } } }); function transition(targetIndex, direction) { switcherTimeline.clear(); switcherTimeline.play(0); const isForward = direction === 1; // Exit previous state switcherTimeline.to(prevElements, { opacity: 0, y: isForward ? -40 : 40, duration: 0.35, ease: "power2.out", overwrite: "auto" }, 0); // Enter next state switcherTimeline.fromTo( nextElements, { opacity: 0, y: isForward ? 100 : 40 }, { opacity: 1, y: 0, duration: 0.7, delay: 0.15, ease: "power4.out", overwrite: "auto" }, 0.1 ); } Full Code:https://github.com/RitankJaikar/Learn-GSAP/tree/main/tests-and-mockups/gsap-scroll-switcher Proposes a dedicated triggered timeline API that handles all of this declaratively: gsap.scrollTimeline(".section", { mode: "triggered", // not scrubbed, fires instantly at trigger points reverse: true, // reverse animation when scrolling back lock: true // prevent overlapping animations on fast scroll }) .state("state1", { at: 0 }) // at: 0 = fires at the very start of the section .to(text1, { opacity: 1, y: 0, duration: 0.7, ease: "power4.out" }) .to(line1, { x: "0%", duration: 0.55 }, "<") .state("state2", { at: 0.33 }) // custom breakpoint, fires 33% through the scroll distance .from(text2, { opacity: 0, y: 100 }) .to(line2, { x: "0%" }, "<") .state("state3", { at: 0.66 }) // fires 66% through, leaving state3 to run the final third .from(text3, { opacity: 0, y: 100 }) .to(line3, { x: "0%" }, "<"); // "at" is optional per state. omit it and the API falls back to // dividing the scroll distance equally across all defined states What this hypothetical API would handle automatically: Trigger point calculation — explicit at values let you set custom breakpoints (as shown above), or omit at entirely and let the API divide scroll distance equally between states Reverse behavior — automatically reverse each state's timeline when scrolling back Fast scroll locking — complete current animation before starting next Direction-aware from/to — different entry/exit values for forward vs reverse Isolated execution — no interference with other GSAP animations on the page Edited August 2 by Cassie edited to remove inaccurate information 1
Solution Cassie Posted July 31 Solution Posted July 31 Hey Ritank! Thanks for the suggestion. I like the hypothetical API. However - you can actually do all of this already using separate ScrollTriggers, maybe that's the part you were missing? It's a little more code, but only a little. Here's a demo. (apologies for the wonky styling, I tried to copy your filed across but some of it's a bit broken) See the Pen c59ee970b8f3d25277a85b0925ba4cef?editors=0010 by GreenSock (@GreenSock) on CodePen. Also in response to these points. Quote toggleActions is per-animation, not timeline-level. ToggleActions can be used on a timeline, it's not just a tween setting. You can set toggleActions on a scrollTrigger which controls a timeline. You can also create multiple scrolltriggers with different scroll points. Quote Reverse behavior (animate backwards when scrolling back up) This is achievable with toggleActions - in fact, it's exactly what toggleActions does! Quote State locking (prevent overlapping animations on fast scroll) We have two features here for exactly this. PreventOverlaps and fastScrollEnd https://gsap.com/docs/v3/Plugins/ScrollTrigger/#preventOverlaps https://gsap.com/docs/v3/Plugins/ScrollTrigger/#fastScrollEnd Quote Direction-aware transitions (different animation for forward vs reverse) If you need different animations for forward and reverse - you can use callbacks in a timeline and fire off dynamic tweens and read direction with this property https://gsap.com/docs/v3/Plugins/ScrollTrigger/direction Hope this helps! 2
Ritank Jaikar Posted August 2 Author Posted August 2 Hey Cassie, Thanks for the detailed reply and the demo, really appreciate you taking the time to rebuild it. I tried it out properly (stress tested with fast scroll, slow scroll, and reverse scroll) and wanted to share what I found, since I think it's a genuinely interesting edge case rather than something obviously wrong with the approach. When scrolling straight through, start to end, forward or reverse, it works great. But when I scroll partway into a state's zone and then reverse direction before reaching the end of that 200px zone, I get overlapping animations. Two states end up partially visible at once. Attaching a screenshot of what that looks like when it happens. I dug into why: toggleActions: "play none reverse none" maps "reverse" to onEnterBack, which only fires when the scroll crosses the end boundary going backward. If you scroll into a zone but reverse before reaching that end point, onEnterBack never fires for that trigger, so the tween that already started playing just keeps running to completion in the background. Meanwhile you've scrolled back into the previous zone, which fires its own onEnterBack and starts reversing that one. Now two tweens are animating the same elements in opposite directions at the same time. preventOverlaps helped reduce it but didn't fully eliminate it in my testing, small back-and-forth scrolling (which happens a lot with trackpads) seems to dodge the zone-width-dependent commit point pretty easily. Do you know of a workaround for this within the ScrollTrigger/toggleActions approach, or is this just an inherent limitation of using separate triggers per state? Also, the mapper trick for finding trigger points as percentages was a nice reminder, I know mapRange exists but hadn't thought to apply it this way myself, so thanks for that, useful regardless of how this particular issue shakes out.
Ritank Jaikar Posted August 2 Author Posted August 2 Update: setting end: "+=0px" on each trigger (so start and end are the same pixel) combined with preventOverlaps: true seems to fix it. Stress tested with fast scroll, slow scroll, reverse mid-zone, and hard flicks across multiple zones, no overlap in any case so far. let common = { end: "+=0px", toggleActions: "play none reverse none", preventOverlaps: true, } Makes sense now, making the zone width zero removes the gap between where forward triggers and where reverse triggers, which was causing the overlap in the first place. Thanks again for the demo and the pointers, genuinely learned a lot working through this and got to explore some newer ways of doing things I hadn't tried before. Will definitely try out ScrollTrigger.direction (https://gsap.com/docs/v3/Plugins/ScrollTrigger/direction) for the direction-aware transitions piece next. This has been really helpful, marking as resolved. Appreciate you taking the time! 1
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now