Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...

Search the Community

Showing results for 'overwrite'.

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Forums

  • GreenSock Forums
    • GSAP
    • Banner Animation
    • Jobs & Freelance
  • Flash / ActionScript Archive
    • GSAP (Flash)
    • Loading (Flash)
    • TransformManager (Flash)

Product Groups

  • Club GreenSock
  • TransformManager
  • Supercharge

Categories

There are no results to display.


Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


Personal Website


Twitter


CodePen


Company Website


Location


Interests

  1. In general, what do GSAP users have in their arsenal to mitigate animations overlapping when users are triggering multiple non-scrubbed ScrollTriggers and/or tweens while scrolling/moving through a site at various speeds. These are what we've come across: Ensure timelines/tweens do not clash (not always simple) --- however, if you're scrolling through several ScrollTriggers, even if you have a delay or long duration between ScrollTriggers, they can clash if the user scrolls too quickly. I don't think this is the case but it appears that tweens/and callbacks can be skipped if the user scrolls too quickly; we've tested this with .set() and .call(). Add OnEnter/OnLeave logic to check if another timeline is "tweening" and then kill the offending timeline gracefully. I'm not seeing any examples of this between multiple ScrollTriggers, but the logic is fairly straightforward although can be extremely convoluted. Set overwrite on your tweens/timelines. I don't think this applies to separate ScrollTriggers --- I've tried multiple ways and haven't seen any affect between ScrollTriggered timelines that clash. I guess how could it; they are separate timelines? Set overflow hidden on the scrolling container so that the user cannot scroll when a tween is occurring; use very sparingly and only in critical areas; best used with a custom scrollbar to remove visual anomalies. Are there any other ways to mitigate clashes between ScrollTriggers and tweens if a user scrolls/moves too quickly through a site? It's not only too quickly in one direction, but when they reserve direction is when we probably see the most clashing. Is there a default overwrite setting that will have an effect between ScrollTriggered timelines? Does the overwrite already do this and we're just not seeing it/using it correctly? Kind Regards, Nitro Interactive
  2. I have these two animations, a box rotating and contact buttons popping onto the screen. ScrollTrigger.create({ trigger: "#topLetterTrigger", animation: gsap.fromTo("#topLetter", {rotateY: -180}, {duration: 2.2, rotateY: 0, ease: "back.out(1.7)"}), once: true }); gsap.set(".contact-icon", {scale: 0}); ScrollTrigger.batch(".contact-icon", { trigger: "#topLetterTrigger", interval: 3, batchMax: 4, once: true, onEnter: batch => gsap.to(batch, {delay: 0.5, duration: 0.8, scale: 1, stagger: {each: 0.25, grid: [1, 4]}, overwrite: true, ease: "back.out(1.7)"}).delay(1.4), }); The delay on the batch animation is supposed to make the animations end at roughly the same time. However it doesn't, it seems sometimes the second trigger doesn't fire, or it starts the second animation when the first animation has completed, so instead of completing together the second animation commences at the end of the 1st animation + 1.4 second delay. Note: is there an issue where you shouldn't have 2 animations given the same trigger: value? I think using a timeline and the ScrollTrigger-ing them both will make it more robust. Or there is some other better way?
  3. It'd definitely help to see a minimal demo so we can make sure we're addressing the real issue adequately, but here are some thoughts: If you've built a page in a way that requires that one ScrollTrigger-based animation finishes before another one starts (thus it looks weird if the user scrolls fast and they're both playing), you can force the previous one to completion with a callback: let anim1 = gsap.to(... { scrollTrigger: { onEnterBack: () => anim2.progress(0) } }); let anim2 = gsap.to(... { scrollTrigger: { onEnter: () => anim1.progress(1), onEnterBack: () => anim3.progress(0) } }); let anim3 = gsap.to(... { scrollTrigger: { onEnter: () => anim2.progress(1) } }); Overwriting Keep in mind that overwrite logic runs once: overwrite: true - immediately when the tween is created overwrite: "auto" when the tween renders for the first time And when overwriting occurs, it is permanent. So this is not ideal for what you're talking about because you don't actually want to kill...as in dead...permanently. You're just wanting the animation to finish right away, correct? That's why I suggested the code above. For the record, the reason overwriting occurs once (not every time the animation plays) is for performance reasons and because it's almost never helpful to run more than the first time. We're VERY performance-minded. If you're still struggling, please provide a minimal demo and we'd be happy to take a peek.
  4. It doesn't look like you are animating the blur property. Look at the proxy object. There is no filter property. gsap.to(proxy, {blur: 0, duration: 0.8, ease: "power3", overwrite: true, onUpdate: () => filterSetter("blur(" + proxy.blur + "px)")});
  5. @OSUblake Thanks a lot! Generally this seems to work, but I have the issue that in my setup, it starts blurring on scrolling, but it does not seem to animate back to 0 when I stop. Here is the code that I currently have: let proxy = { blur: 0 }, filterSetter = gsap.quickSetter(".shape", "filter"), // fast clamp = gsap.utils.clamp(0, 40); ScrollTrigger.create({ onUpdate: (self) => { let blur = clamp(self.getVelocity() / -300); if (Math.abs(blur) > Math.abs(proxy.blur)) { proxy.blur = blur; gsap.to(proxy, {filter: "blur(0)", duration: 0.8, ease: "power3", overwrite: true, onUpdate: () => filterSetter("blur(" + proxy.blur + "px)")}); } } }); Any idea if there is something wrong maybe? Thanks a lot!
  6. Try adding wrapper around your element, and separate your animations. You could also try adding overwrite: "auto" to your animation.
  7. Everything seems to be working fine. You're not scrolling, so there's that... const roll1 = this.rollText(".rollingText", {duration: 10}); gsap.to([roll1], {timeScale: direction * -1, overwrite: true}); this.direction doesn't exists. And your animation has an ease on it. Look at the original. vars = vars || {}; vars.ease || (vars.ease = "none");
  8. I have a long page with multiple scroll based simple animations, some of them use the same class name to run different places. At some point it seems the trigger position gets off for all the page and animations at the bottom of the screen are happening in the wrong place, If I open console in the browser the animations are back at the correct position. (function () { gsap.registerPlugin(ScrollTrigger); gsap.defaults({ease: "power4.out"}); ScrollTrigger.matchMedia({ // desktop "(min-width: 800px)": function () { mySplitText = new SplitText(".corona-hero-section h2", {type: "words,chars"}); chars = mySplitText.chars; //an array of all the divs that wrap each character gsap.set(".corona-hero-section h2", {perspective: 400}); var tl = gsap.timeline(); tl .from(".logo", {duration: 2, autoAlpha: 0, y: -60, stagger: 0.3}, 1) .from(chars, { duration: 0.8, opacity: 0, scale: 0, y: 80, rotationX: 180, transformOrigin: "0% 50% -50", ease: "back", stagger: 0.01 }, "+=0"); tl.restart(); scheneChange(5, 140, '.viewer', '.scene', false, 'bottom bottom'); scheneChange(4, 138, '.viewer2', '.scene2', false, 'bottom bottom'); scheneChange(1, 122, '.viewer4', '.scene4', false, 'bottom bottom'); scheneChange(3, 146, '.viewer5', '.scene5', false, 'bottom bottom'); scheneChange(2, 139, '.viewer6', '.scene6', false, 'bottom bottom'); animateImageReveal(); animateStaggerIcons('.icon-list', '.icon-list__item'); animateStaggerIcons('.icon-num', '.icon-num__item'); animateStaggerIcons('.icon-top', '.icon-top__item'); aminateStaggerLi('.map-wrap__item'); aminateStaggerLi('.program-list-wrap li'); }, // mobile "(max-width: 799px)": function () { // Icons and text animation gsap.utils.toArray('.icon-list__item').forEach((section) => { const tls = gsap.timeline({ scrollTrigger: { trigger: section, scrub: true, start: "top center", // markers: true, end: "+=20%", }, }); const delay = 0.5; const textIcon = section.querySelectorAll(".icon-list__icon"); const textTitle = section.querySelectorAll(".icon-list__title"); const textText = section.querySelector(".icon-list__text"); const textNum = section.querySelector(".icon-list__num"); tls .from(textNum, {duration: 3, autoAlpha: 0, y: -60, stagger: delay}, 0) .from(textIcon, {duration: 1, autoAlpha: 0, y: -60, stagger: delay}, 0.3) .from(textTitle, {duration: 1, autoAlpha: 0, y: -60, stagger: delay}, 0.6) .from(textText, {duration: 1, autoAlpha: 0, y: -60, stagger: delay}, 0.9) }); scheneChange(5, 140, '.viewer', '.scene'); scheneChange(4, 138, '.viewer2', '.scene2'); scheneChange(1, 122, '.viewer4', '.scene4', false); scheneChange(3, 146, '.viewer5', '.scene5'); scheneChange(2, 139, '.viewer6', '.scene6', false); scheneImageReveal(); animateIconNum(); scheneChange(1, 163.5, '.viewer3', '.scene3'); }, "all": function () { animateTitleSection(); morphImg('.scene3'); } }); function scheneChange(frames, offset, classToAnimate, action, pin = true, start = "center center") { var frame_count = frames, offset_value = offset; gsap.to(classToAnimate, { backgroundPosition: (-offset_value * frame_count * 2) + "px 50%", ease: "steps(" + frame_count + ")", // use a stepped ease for the sprite sheet scrollTrigger: { trigger: action, start: start, end: "+=" + (frame_count * offset_value), pin: pin, // markers: true, scrub: true, } }); } function morphImg(trigger) { const tls3 = gsap.timeline({ scrollTrigger: { trigger: trigger, scrub: true, start: "top center", // markers: true, end: "+=40%", }, }); tls3 .to('#inIsrael', {duration: 2, fill: "#9191a0", morphSVG: "#inMaoz"}, 0) .to('#israelText', {duration: 2, morphSVG: "#maozText"}, 0) .to('#israelUnderText', {duration: 2, morphSVG: "#maozUnderText"}, 0.2) } function animateIconNum() { const sections = gsap.utils.toArray('.icon-num__item'); sections.forEach((section) => { const tls = gsap.timeline({ scrollTrigger: { trigger: section, scrub: true, start: "top center", // markers: true, end: "+=20%", }, }); const delay = 0.5; const textIcon = section.querySelectorAll(".icon-num__icon"); const textTitle = section.querySelectorAll(".icon-num__title"); const textText = section.querySelector(".icon-num__text"); // const textNum = section.querySelector(".icon-list__num"); tls .from(textText, {duration: 3, autoAlpha: 0, y: -60, stagger: delay}, 0) .from(textIcon, {duration: 1, autoAlpha: 0, y: -60, stagger: delay}, 0.3) .from(textTitle, {duration: 1, autoAlpha: 0, y: -60, stagger: delay}, 0.6) // .from(textText, {duration: 1, autoAlpha: 0, y: -60, stagger: delay}, 0.9) }); } function animateTitleSection() { const sections = gsap.utils.toArray(".corona-header"); sections.forEach((section) => { const splitTimeline = gsap.timeline({ scrollTrigger: { trigger: section, scrub: true, end: "+=70%", // onToggle: self => gsap.to(".split-text", {opacity: self.isActive ? 1 : 0}), toggleActions: "restart pause restart none", //markers: true } }); const split = new SplitText(section); splitTimeline .to(section, {duration: 1, backgroundColor: '#fff', ease: "none"}, 0) .to(section, {duration: 1, backgroundColor: '#222241', ease: "none"}, 1) .from(split.chars, { duration: 2, opacity: 0, x: "random(-500, 500)", y: "random(-500, 500)", z: "random(-500, 500)", scale: .1, yoyo: true, stagger: 0.02 }); }); } function animateImageReveal() { let revealContainers = gsap.utils.toArray(".reveal"); revealContainers.forEach((container) => { let tlImage = gsap.timeline({ scrollTrigger: { trigger: container, scrub: true, start: "top 100%", end: "bottom 70%", } }); tlImage.from(container, {duration: 0.3, autoAlpha: 0, y: -60, stagger: 0.3}, 1); }); } function scheneImageReveal() { let revealContainers = gsap.utils.toArray(".reveal"); revealContainers.forEach((container) => { let image = container.querySelector('img'); let tlImage = gsap.timeline({ scrollTrigger: { trigger: container, start: "20% 70%", toggleActions: "restart none none reset", } }); tlImage.set(container, {autoAlpha: 1}); tlImage.from(container, 1.5, { xPercent: -100, ease: Power2.out }); tlImage.from(image, 1.5, { xPercent: 100, // scale: 0.7, delay: -1.5, ease: Power2.out }); }); } function aminateStaggerLi(trigger) { gsap.set(trigger, {y: 100}); ScrollTrigger.batch(trigger, { onEnter: batch => gsap.to(batch, {opacity: 1, y: 0, stagger: {each: 0.15, grid: [1, 3]}, overwrite: true}), onLeave: batch => gsap.set(batch, {opacity: 0, y: -100, overwrite: true}), onEnterBack: batch => gsap.to(batch, {opacity: 1, y: 0, stagger: 0.15, overwrite: true}), onLeaveBack: batch => gsap.set(batch, {opacity: 0, y: 100, overwrite: true}) }); ScrollTrigger.addEventListener("refreshInit", () => gsap.set(trigger, {y: 0})); } function animateStaggerIcons(list, listItem) { gsap.utils.toArray(list).forEach(section => { const elems = section.querySelectorAll(listItem); // Set things up gsap.set(elems, {y: 50, opacity: 0}); ScrollTrigger.create({ trigger: section, start: 'top 60%', onEnter: () => gsap.to(elems, { y: 0, opacity: 1, duration: 1, stagger: 0.2, delay: 0.3, ease: 'power3.out', overwrite: 'auto' }), onLeaveBack: () => gsap.to(elems, { y: 50, opacity: 0, duration: 1, stagger: 0.2, delay: 0.3, ease: 'power3.out', overwrite: 'auto' }) }); }) } })();
  9. Hi everyone, Sorry to revive this not so old topic, I have a similar problem but with React hooks, Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. I'm pretty new to React and I'm not sure what I can do to clean the gsap process. I'm using gsap in two useEffects to drive simultaneously a countdown (in seconds going down) and a loading bar (in percent going up) I tried the following : ///// COUNTDOWN ///// const [countdown, setCountdown] = useState(props.seconds); const proxyCount = useRef({ countdown }); useEffect(() => { gsap.to(proxyCount.current, { countdown: 0, duration: props.seconds, ease: "linear", overwrite: "auto", onUpdate: () => { setCountdown(proxyCount.current.countdown); } }); return function cleanup() { gsap.killTweensOf(proxyCount, "countdown"); }; }, [setCountdown]); ///// PERCENT ///// const [percent, setPercent] = useState(0); const proxy = useRef({ percent }); useEffect(() => { gsap.to(proxy.current, { percent: 100, duration: props.seconds, ease: "linear", overwrite: "auto", onUpdate: () => { setPercent(proxy.current.percent); } }); return function cleanup() { gsap.killTweensOf(proxy, "percent"); }; }, [setPercent]); I thought that would be it but gsap.killTweensOf, either with or without the 2nd argument ( "countdown" / "percent" ) doesn't seem to fix it. I'm not using timelines yet so I wondered if anyone has an alternative to the t1.kill() here. Thanks in advance ! Julien
  10. You can tie the progress in to values from a range slider like this. https://codepen.io/cassie-codes/pen/a7fd24fd8d7fd6bfdc25f866bf3c00b5?editors=1011 Doing both scroll and a slider will definitely be possible but you'll have to work out what to do when the slider and scroll conflict. Which values overwrite which?
  11. @mikel that's because you keep creating new "action" timelines that are conflicting (bad). The last one created always "wins" because it renders last, but the others are still rendering and stacking up. When you set the timeScale() to 0 on the latest one, of course that one doesn't keep rendering because it's essentially paused, thus the LAST one you created is getting rendered. So it makes it appear as though the timeScale(0) didn't work but it actually did. The problem is the old/conflicting ones that were never properly killed or overwritten. Those are what continue to animate. Also, there's no such thing as "overwrite: true" on a timeline (that's just for tweens because those have "targets").
  12. You might want to look into the "overwrite" special property: overwrite: true immediately kills all animations of the same targets, regardless of what properties they're affecting. overwrite: "auto" only kills the individual parts of other active tweens of the same targets that are affecting the same properties. But to answer your original question most directly, I assume you're looking for gsap.killTweensOf(). And you can kill() any animation. Tweens and Timelines both have that method. There are actually quite a few ways to do what you're asking. If you still need some help, please provide a minimal demo so that we can see the context and quickly show you how to tweak the code. Happy tweening!
  13. Thanks for the detailed explanation @OSUblake, I will go with overwrite: "auto" in this case then, as I indeed need to avoid this weird stuff, but also have other animations (of other properties) running on the same target.
  14. Technically it doesn't. You just see the changes from the newest animation. Once the old animation runs it course, it will be removed, so in that sense you really don't need to worry about it. This would only cause a problem if the durations are different, like here. If you quickly hover on and off the circles, it's going to do some weird stuff. https://codepen.io/osublake/pen/24b9ba7b0403dc58f3493c6cf1410c9e The solution is to use overwrite: true. This will immediately kill the previous animation. https://codepen.io/osublake/pen/331b8647c24abb889eec9fb9af2655a3
  15. Hi! This is a strange one. I assume it's because the tweens in the third timeline are conflicting with the second timeline but I can't quite pin it down, and as you say - it only occurs when you scroll quickly. I tried overwrite true and some other things, but no luck. It's annoying to suggest a fix without a reason, so apologies. But if I were in your position I would handle the section pinning in one timeline and the dot animation in another - then there aren't conflicts (however they're arising and however strange...) https://codepen.io/cassie-codes/pen/89e49c2af76bc0dca02749e75ce54dee?editors=0110
  16. I tweaked the helper function so that toIndex() will always go in the shortest direction: /* This helper function makes a group of elements animate along the x-axis in a seamless, responsive loop. Features: - Uses xPercent so that even if the widths change (like if the window gets resized), it should still work in most cases. - When each item animates to the left or right enough, it will loop back to the other side - Optionally pass in a config object with values like "speed" (default: 1, which travels at roughly 100 pixels per second), paused (boolean), and repeat. - The returned timeline will have the following methods added to it: - next() - animates to the next element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc. - previous() - animates to the previous element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc. - toIndex() - pass in a zero-based index value of the element that it should animate to, and optionally pass in a vars object to control duration, easing, etc. Always goes in the shortest direction - current() - returns the current index (if an animation is in-progress, it reflects the final index) - times - an Array of the times on the timeline where each element hits the "starting" spot. There's also a label added accordingly, so "label1" is when the 2nd element reaches the start. */ function horizontalLoop(items, config) { items = gsap.utils.toArray(items); config = config || {}; let tl = gsap.timeline({repeat: config.repeat, paused: config.paused, defaults: {ease: "none"}}), length = items.length, startX = items[0].offsetLeft, times = [], widths = [], xPercents = [], curIndex = 0, pixelsPerSecond = (config.speed || 1) * 100, snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural totalWidth, curX, distanceToStart, distanceToLoop, item, i; gsap.set(items, { // convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster. xPercent: (i, el) => { let w = widths[i] = parseFloat(gsap.getProperty(el, "width", "px")); xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / w * 100 + gsap.getProperty(el, "xPercent")); return xPercents[i]; } }); gsap.set(items, {x: 0}); totalWidth = items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX") for (i = 0; i < length; i++) { item = items[i]; curX = xPercents[i] / 100 * widths[i]; distanceToStart = item.offsetLeft + curX - startX; distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX"); tl.to(item, {xPercent: snap((curX - distanceToLoop) / widths[i] * 100), duration: distanceToLoop / pixelsPerSecond}, 0) .fromTo(item, {xPercent: snap((curX - distanceToLoop + totalWidth) / widths[i] * 100)}, {xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false}, distanceToLoop / pixelsPerSecond) .add("label" + i, distanceToStart / pixelsPerSecond); times[i] = distanceToStart / pixelsPerSecond; } function toIndex(index, vars) { vars = vars || {}; (Math.abs(index - curIndex) > length / 2) && (index += index > curIndex ? -length : length); // always go in the shortest direction let newIndex = gsap.utils.wrap(0, length, index), time = times[newIndex]; if (time > tl.time() !== index > curIndex) { // if we're wrapping the timeline's playhead, make the proper adjustments vars.modifiers = {time: gsap.utils.wrap(0, tl.duration())}; time += tl.duration() * (index > curIndex ? 1 : -1); } curIndex = newIndex; vars.overwrite = true; return tl.tweenTo(time, vars); } tl.next = vars => toIndex(curIndex+1, vars); tl.previous = vars => toIndex(curIndex-1, vars); tl.current = () => curIndex; tl.toIndex = (index, vars) => toIndex(index, vars); tl.times = times; return tl; } Here's a fork with a slightly different color scheme https://codepen.io/GreenSock/pen/ZELPxWW?editors=0010 Better?
  17. Another option is to set overwrite: true on your tween which will cause it to find all other tweens of the same target(s) and immediately kill them. Basically, you were creating conflicting tweens.
  18. is it possible for scrollColorElems to use gradients? html <div id="section1" class="mymodeheader-newmods mytop"><img class="hero__image" data-scrollcolor="linear-gradient(90deg, #46224e 0%, #30184f 85%)" ) src="https://parkviewhomes.info/energy-star/banner-top-01.svg" alt="" /></div> window.addEventListener("load", function () { const scrollColorElems = document.querySelectorAll("[data-scrollcolor]"); scrollColorElems.forEach((colorSection, i) => { const prevColor = i === 0 ? "#1b1521" : scrollColorElems[i - 1].dataset.scrollcolor; ScrollTrigger.create({ trigger: colorSection, start: "top bottom", onEnter: () => gsap.to("body", { backgroundColor: colorSection.dataset.scrollcolor, overwrite: "auto" }), onLeaveBack: () => gsap.to("body", { backgroundColor: prevColor, overwrite: "auto" }) }); }); });
  19. when I use a hex it works great, I was just wondering is it could do gradients as well I tried this but it failed badly :) gsap.registerPlugin(ScrollTrigger); window.addEventListener("load", function () { const scrollColorElems = document.querySelectorAll("[data-scrollcolor]"); scrollColorElems.forEach((colorSection, i) => { const prevColor = i === 0 ? "linear-gradient(180deg, #FBD786 0%, #f7797d 85%)" : scrollColorElems[i - 1].dataset.scrollcolor; ScrollTrigger.create({ trigger: colorSection, start: "top bottom", onEnter: () => gsap.to("body", { backgroundColor: colorSection.dataset.scrollcolor, overwrite: "auto" }), onLeaveBack: () => gsap.to("body", { background-image: prevColor, overwrite: "auto" }) }); }); });
  20. I'm not sure that helps but good to know. ...I still can't tell if gradients with data-scrollcolor think I have here ? data-scrollcolor="linear-gradient(90deg, #46224e 0%, #30184f 85%)" ) gsap.registerPlugin(ScrollTrigger); window.addEventListener("load", function () { const scrollColorElems = document.querySelectorAll("[data-scrollcolor]"); scrollColorElems.forEach((colorSection, i) => { const prevColor = i === 0 ? "linear-gradient(90deg, #46224e 0%, #30184f 85%)"" : scrollColorElems[i - 1].dataset.scrollcolor; ScrollTrigger.create({ trigger: colorSection, start: "top bottom", onEnter: () => gsap.to("body", { backgroundColor: colorSection.dataset.scrollcolor, overwrite: "auto" }), onLeaveBack: () => gsap.to("body", { backgroundColor: prevColor, overwrite: "auto" }) }); }); });
  21. Hi, In terms of performance, both are equal. None of the options is going to put a lot of stress in either the CPU or the GPU. In terms of cleanliness, I'd prefer the first one, because if your GSAP instance is not going to change at any point in time, there is no need to create it over and over again, probably generating some overwrite and initial position problems. I always prefer to store my GSAP instance and use them as needed and only rely on creating GSAP instance on every event handler call when I have no other choice. Happy Tweening!!!
  22. I'm not totally sure what effect you're after, but keep in mind: If you want to link the animation progress directly to the scrollbar so that it acts like a scrubber, use scrub: true (or a number for smooth scrubbing). The way you're doing it is with callbacks, but keep in mind that if the user scrolls very fast, those could fire in very quick succession (potentially on the same tick), so make sure you factor that into your logic. For example, you'd probably want to set overwrite: true or overwrite: "auto" to prevent conflicts where you're firing off multiple tweens attempting to control the same properties of the same objects. Are your callbacks being fired in the way/order you expected? As for your setup, there are tons of ways you could do it but in my brain this seems like a timeline that scrubs to me (or it should be) rather than callbacks that fire animations at a specific spot. But again, that's totally up to you regarding the effect you want. I'm just usually a fan of creating the animation(s) once and then just controlling them. Typically that's best for performance too. But if you want these dynamic so that they start using the property values wherever they're at currently (mid-tween), it's unavoidable.
  23. Welcome to the forums, @Nzhiti! Since this is a somewhat common effect (a group of elements that seamlessly loop on the horizontal axis), I whipped together a helper function that makes it significantly easier. Here's a fork with it in place: https://codepen.io/GreenSock/pen/wvgOmBO?editors=0110 Here's the helper function isolated: /* This helper function makes a group of elements animate along the x-axis in a seamless, responsive loop. Features: - Uses xPercent so that even if the widths change (like if the window gets resized), it should still work in most cases. - When each item animates to the left or right enough, it will loop back to the other side - Optionally pass in a config object with values like "speed" (default: 1, which travels at roughly 100 pixels per second), paused (boolean), and repeat. - The returned timeline will have the following methods added to it: - next() - animates to the next element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc. - previous() - animates to the previous element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc. - toIndex() - pass in a zero-based index value of the element that it should animate to, and optionally pass in a vars object to control duration, easing, etc. - current() - returns the current index (if an animation is in-progress, it reflects the final index) - times - an Array of the times on the timeline where each element hits the "starting" spot. There's also a label added accordingly, so "label1" is when the 2nd element reaches the start. */ function horizontalLoop(items, config) { items = gsap.utils.toArray(items); config = config || {}; let tl = gsap.timeline({repeat: config.repeat, paused: config.paused, defaults: {ease: "none"}}), length = items.length, startX = items[0].offsetLeft, times = [], widths = [], xPercents = [], curIndex = 0, pixelsPerSecond = (config.speed || 1) * 100, snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural totalWidth, curX, distanceToStart, distanceToLoop, item, i; // convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster. gsap.set(items, {xPercent: (i, el) => { let w = widths[i] = parseFloat(gsap.getProperty(el, "width", "px")); xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / w * 100 + gsap.getProperty(el, "xPercent")); return xPercents[i]; }}); gsap.set(items, {x: 0}); totalWidth = items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX") for (i = 0; i < length; i++) { item = items[i]; curX = xPercents[i] / 100 * widths[i]; distanceToStart = item.offsetLeft + curX - startX; distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX"); tl.to(item, {xPercent: snap((curX - distanceToLoop) / widths[i] * 100), duration: distanceToLoop / pixelsPerSecond}, 0) .fromTo(item, {xPercent: snap((curX - distanceToLoop + totalWidth) / widths[i] * 100)}, {xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false}, distanceToLoop / pixelsPerSecond) .add("label" + i, distanceToStart / pixelsPerSecond); times[i] = distanceToStart / pixelsPerSecond; } function toIndex(index, vars) { vars = vars || {}; let newIndex = gsap.utils.wrap(0, length, index), time = times[newIndex]; if (time > tl.time() !== index > curIndex) { // if we're wrapping the timeline's playhead, make the proper adjustments vars.modifiers = {time: gsap.utils.wrap(0, tl.duration())}; time += tl.duration() * (index > curIndex ? 1 : -1); } curIndex = newIndex; vars.overwrite = true; return tl.tweenTo(time, vars); } tl.next = vars => toIndex(curIndex+1, vars); tl.previous = vars => toIndex(curIndex-1, vars); tl.current = () => curIndex; tl.toIndex = (index, vars) => toIndex(index, vars); tl.times = times; return tl; } Usage const loop = horizontalLoop(".magnet", {paused: true}); next.addEventListener("click", () => loop.next({duration: 1, ease: "power1"})); prev.addEventListener("click", () => loop.previous({duration: 1, ease: "power1"})); Kinda fun, huh? And since I made all the movement based on xPercent, it's pretty much responsive, meaning you can alter the width and it should still work. here's a responsive version that makes the magnets always 20% of the width of that container (fitting nicely): https://codepen.io/GreenSock/pen/ZELPxWW?editors=0010 Does that help?
  24. Thanks for the reduced test case, @emsee. It is a bit of a tricky scenario. Solution: add these lines after you enable() the ScrollTrigger: g.scrollTrigger.refresh(); // re-calculates start/end gsap.set(g, {progress: g.scrollTrigger.progress, overwrite: true}); // sets the progress immediately instead of scrubbing, and overwrites the scrubbing tween // or you could replace the line above with: gsap.getTweensOf(g)[0].progress(1); When you enable() a ScrollTrigger, it automatically refreshes its start/end values to ensure they're correct, but timelines have to wait one tick to do that (I'll spare you the lengthy explanation, but just ask if you want one). Anyway, when you disabled the ScrollTrigger, it was at a progress of around 0.1 and then when you re-enable it, it must scrub to the new value (0.5) which is what you were seeing. To get the behavior you want, we've gotta basically make the progress jump there instead of tweening there. So we refresh() to ensure the start/end are set correctly, then we directly set the progress and use an overwrite: true in order to overwrite the scrubbing tween. Does that resolve things for you? https://codepen.io/GreenSock/pen/e500c7ad56ff97f35149ba60d194e521
×