Jump to content
Search Community

GreenSock last won the day on April 21

GreenSock had the most liked content!

GreenSock

Administrators
  • Posts

    23,142
  • Joined

  • Last visited

  • Days Won

    817

Community Answers

  1. GreenSock's post in Video onscroll animation breaking issue was marked as the answer   
    Yep, same issue - you're creating things out of order, thus they refresh in the wrong order. For example, let's say elementA is 100px from the top of the screen, and there's a ScrollTrigger that triggers when that hits the top of the screen ("top top"). So normally, the start would be 100. But what if there's another ScrollTrigger that pins an element above that one for 1000px - that'd push everything down, thus that element should trigger at 1100px instead of 100px. If ScrollTrigger calculates them in the wrong order, it'd set the first one to a start of 100px (because the pinning one hasn't been factored in yet). 
     
    Here's a helper function that you can call after all of your elements are in place, and it'll order things based on their proximity to the top of the viewport: 
    function verticalSort() { let scroll = window.pageYOffset; ScrollTrigger.getAll().forEach(t => t._sortY = t.trigger ? scroll + t.trigger.getBoundingClientRect().top : t.start + window.innerHeight); ScrollTrigger.sort((a, b) => a._sortY - b._sortY); }
    See the Pen ZEZPqyd?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    Better? 
     
    Of course you could solve everything by explicitly stating the unique refreshPriority for each, but the above function seemed easier and it should work in most cases. 
  2. GreenSock's post in ScrollTrigger pin with DVH CSS units was marked as the answer   
    Not sure if this is helpful to you or not, but: 
     
    ignoreMobileResize is true by default in recent versions, but it sounds like maybe you actually want it to be false(?) so that ScrollTrigger.refresh() gets called when the window resizes on mobile. Is that correct? 
    ScrollTrigger.config({ ignoreMobileResize: false }); Or of course you can call ScrollTrigger.refresh() manually anytime, so wire it up however you please. 
  3. GreenSock's post in How to relocate an element on scroll using GSAP! No plugins was marked as the answer   
    I noticed two problems:
    You nested ScrollTriggers inside a timeline. That's logically impossible to accommodate - the playhead of a tween can't be controlled by BOTH a timeline's playhead AND the scroll position simultaneously.  You've got multiple tweens of the same element controlled by ScrollTriggers. By default, animations with ScrollTriggers are rendered immediately. So in your case, you just need to set the 2nd and 3rd to immediateRender: false so that they don't step on each other.  Is this what you're looking for?:

    See the Pen QWPzBXO?editors=1010 by GreenSock (@GreenSock) on CodePen
     
    And alternative might be to create a timeline that has ONE ScrollTrigger applied to the timeline itself (not nested ones), and have all your animations in there. That'd assume you can just make that one timeline span across the entire scroll area that you need. If not, then just stick with the individual tweens. 
     
    I hope that helps. 
  4. GreenSock's post in ScrollSmoother get scroll y value without adding transform to content was marked as the answer   
    Without a minimal demo, it's super difficult to offer a suggestion here or understand what you're asking for - are you saying you want to apply ScrollSmoother...but not have it actually work at all? You just want to have it pretend to work, feeding you the value that it would apply to the container's transform, without applying it? Maybe you could add a ticker listener with a high priority to grab the original transform, and then use an onUpdate on the ScrollSmoother where you grab the new value but immediately apply the old one to revert it(?) I'm totally guessing here in the absence of a minimal demo, but hopefully that gives you a little to go on at least. I wonder why you wouldn't adjust whatever you're doing with the <body> transform to more cleanly apply that in a way that integrates with ScrollSmoother(?) 
  5. GreenSock's post in ScrollTrigger snap duration remaining animation duration was marked as the answer   
    Yes, @Rodrigo is correct about that not being a built-in feature but you could probably get that functionality with enough custom code. For example, set up a listener for the "scrollEnd" event, and then create a gsap.to(...{ scrollTo }) tween that's however long you want, and apply your snapping logic inside the handler to calculate the scroll position. Just an idea. Good luck!
  6. GreenSock's post in Do I need a licence? was marked as the answer   
    Thanks for asking, @Marija1337! 
     
    No, you don't need the special commercial license for that - the standard "no charge" license covers that type of usage.
     
    If you answer "yes" to any of the following, you'd need the special commercial license that comes with "Business" Club GSAP memberships: 
    Do you have multiple developers that need access to the bonus plugins like SplitText, ScrollSmoother, MorphSVG, etc.? Does your GSAP-enhanced website/game/product require the user to pay a fee to use? (like Netflix.com) Are you selling a GSAP-enhanced product to multiple end users? (like a website template, theme or game) Are you using GSAP in a game/app with optional paid features/upgrades? If your website sells products that aren't GSAP-enhanced like clothing, widgets, food, etc., that doesn't require the commercial license. It only matters if the thing for which a fee is collected uses GSAP in some way. The standard "no charge" license even covers usage in projects where only a one-time development fee is charged, like an agency that's paid to build a fancy web site that's free for everyone to use.
     
    So you should be all set with the free/public license. Enjoy the tools and good luck with the new business! 💚
  7. GreenSock's post in ScrollTrigger .getVelocity returning highly variable results when driven by a scrollTo animation was marked as the answer   
    Yeah, that's the nature of dynamically calculating velocity because you've always gotta choose how much "recording" to do time-wise and then compare the values. But I'm not sure why you're not just pre-calculating the velocity based on the linear tween's distance and duration: 
    const velocity = ScrollTrigger.maxScroll(document.querySelector("#scroller")) / 10; // change / duration This demo shows a general idea of how the values are calculated, although in ScrollTrigger the measurements are always at least 50ms apart:

    See the Pen GRLXZXZ?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    In short, I think there's probably a much cleaner way to do what you're attempting without using getVelocity().
  8. GreenSock's post in autoKill default for scrollTo was marked as the answer   
    No, that isn't really possible to do a nested default value like that. But perhaps in the next release, I could add a ScrollToPlugin.config({ autoKill: true }) method. I don't think anyone has ever requested something like that before. It wouldn't be terribly expensive kb-wise. I assume you'd both vote for this addition? 
  9. GreenSock's post in With timeline.call(), is there any way to pass the direction to the callback? was marked as the answer   
    The ScrollTrigger has a direction property that's 1 if the last scroll was forward, and -1 if it was backward. 
     
    Sorta like: 
    let tl = gsap.timeline({ scrollTrigger: { scrub: true, ... } }); tl.to(...); tl.add(() => { console.log("direction", tl.scrollTrigger.direction); }); Notice I'm using add() for the callback just because it's a little simpler than call() which is only useful if you're passing parameters (uncommon). 
     
    Is that what you're looking for? 
     
    If you're not using a ScrollTrigger at all, there's also a helper function for tracking the direction of an animation: 
    https://gsap.com/docs/v3/HelperFunctions/helpers/trackDirection
  10. GreenSock's post in Extra space appear in end part of horizontal scroll and some question was marked as the answer   
    That's because your code is set up assuming each box will be the width of the viewport. But they're narrower. You just need to do the calculations properly.
     
    I assume this is what you're looking for: 

    See the Pen NWmMgjZ by GreenSock (@GreenSock) on CodePen
  11. GreenSock's post in Scroll zoom-in to text/SVG problem was marked as the answer   
    Yeah, that looks like a browser rendering thing, but you could try setting this on any transform-related animation: 
    force3D: false Does that help? 
  12. GreenSock's post in Clear horizontaloop helper function when resize was marked as the answer   
    Ah, that's because inside the helper function there was a "resize" event handler that was re-initiating things. I just edited the helper function to put it inside a gsap.context() that uses a cleanup function for the "resize" event handler to remove that: 
    https://stackblitz.com/edit/stackblitz-starters-jbsvf4?file=app%2Fhelper.js
     
    function horizontalLoop(items, config) { let timeline; items = gsap.utils.toArray(items); config = config || {}; gsap.context(() => { // use a context so that if this is called from within another context or a gsap.matchMedia(), we can perform proper cleanup like the "resize" event handler on the window let onChange = config.onChange, lastIndex = 0, tl = gsap.timeline({repeat: config.repeat, onUpdate: onChange && function() { let i = tl.closestIndex(); if (lastIndex !== i) { lastIndex = i; onChange(items[i], i); } }, paused: config.paused, defaults: {ease: "none"}, onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100)}), length = items.length, startX = items[0].offsetLeft, times = [], widths = [], spaceBefore = [], xPercents = [], curIndex = 0, indexIsDirty = false, center = config.center, 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 timeOffset = 0, container = center === true ? items[0].parentNode : gsap.utils.toArray(center)[0] || items[0].parentNode, totalWidth, getTotalWidth = () => items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + spaceBefore[0] + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX") + (parseFloat(config.paddingRight) || 0), populateWidths = () => { let b1 = container.getBoundingClientRect(), b2; items.forEach((el, i) => { widths[i] = parseFloat(gsap.getProperty(el, "width", "px")); xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / widths[i] * 100 + gsap.getProperty(el, "xPercent")); b2 = el.getBoundingClientRect(); spaceBefore[i] = b2.left - (i ? b1.right : b1.left); b1 = b2; }); gsap.set(items, { // convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster. xPercent: i => xPercents[i] }); totalWidth = getTotalWidth(); }, timeWrap, populateOffsets = () => { timeOffset = center ? tl.duration() * (container.offsetWidth / 2) / totalWidth : 0; center && times.forEach((t, i) => { times[i] = timeWrap(tl.labels["label" + i] + tl.duration() * widths[i] / 2 / totalWidth - timeOffset); }); }, getClosest = (values, value, wrap) => { let i = values.length, closest = 1e10, index = 0, d; while (i--) { d = Math.abs(values[i] - value); if (d > wrap / 2) { d = wrap - d; } if (d < closest) { closest = d; index = i; } } return index; }, populateTimeline = () => { let i, item, curX, distanceToStart, distanceToLoop; tl.clear(); for (i = 0; i < length; i++) { item = items[i]; curX = xPercents[i] / 100 * widths[i]; distanceToStart = item.offsetLeft + curX - startX + spaceBefore[0]; 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; } timeWrap = gsap.utils.wrap(0, tl.duration()); }, refresh = (deep) => { let progress = tl.progress(); tl.progress(0, true); populateWidths(); deep && populateTimeline(); populateOffsets(); deep && tl.draggable ? tl.time(times[curIndex], true) : tl.progress(progress, true); }, onResize = () => refresh(true), proxy; gsap.set(items, {x: 0}); populateWidths(); populateTimeline(); populateOffsets(); window.addEventListener("resize", onResize); 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 && index !== curIndex) { // if we're wrapping the timeline's playhead, make the proper adjustments time += tl.duration() * (index > curIndex ? 1 : -1); } if (time < 0 || time > tl.duration()) { vars.modifiers = {time: timeWrap}; } curIndex = newIndex; vars.overwrite = true; gsap.killTweensOf(proxy); return vars.duration === 0 ? tl.time(timeWrap(time)) : tl.tweenTo(time, vars); } tl.toIndex = (index, vars) => toIndex(index, vars); tl.closestIndex = setCurrent => { let index = getClosest(times, tl.time(), tl.duration()); if (setCurrent) { curIndex = index; indexIsDirty = false; } return index; }; tl.current = () => indexIsDirty ? tl.closestIndex(true) : curIndex; tl.next = vars => toIndex(tl.current()+1, vars); tl.previous = vars => toIndex(tl.current()-1, vars); tl.times = times; tl.progress(1, true).progress(0, true); // pre-render for performance if (config.reversed) { tl.vars.onReverseComplete(); tl.reverse(); } if (config.draggable && typeof(Draggable) === "function") { proxy = document.createElement("div") let wrap = gsap.utils.wrap(0, 1), ratio, startProgress, draggable, dragSnap, lastSnap, initChangeX, wasPlaying, align = () => tl.progress(wrap(startProgress + (draggable.startX - draggable.x) * ratio)), syncIndex = () => tl.closestIndex(true); typeof(InertiaPlugin) === "undefined" && console.warn("InertiaPlugin required for momentum-based scrolling and snapping. https://greensock.com/club"); draggable = Draggable.create(proxy, { trigger: items[0].parentNode, type: "x", onPressInit() { let x = this.x; gsap.killTweensOf(tl); wasPlaying = !tl.paused(); tl.pause(); startProgress = tl.progress(); refresh(); ratio = 1 / totalWidth; initChangeX = (startProgress / -ratio) - x; gsap.set(proxy, {x: startProgress / -ratio}); }, onDrag: align, onThrowUpdate: align, overshootTolerance: 0, inertia: true, snap(value) { //note: if the user presses and releases in the middle of a throw, due to the sudden correction of proxy.x in the onPressInit(), the velocity could be very large, throwing off the snap. So sense that condition and adjust for it. We also need to set overshootTolerance to 0 to prevent the inertia from causing it to shoot past and come back if (Math.abs(startProgress / -ratio - this.x) < 10) { return lastSnap + initChangeX } let time = -(value * ratio) * tl.duration(), wrappedTime = timeWrap(time), snapTime = times[getClosest(times, wrappedTime, tl.duration())], dif = snapTime - wrappedTime; Math.abs(dif) > tl.duration() / 2 && (dif += dif < 0 ? tl.duration() : -tl.duration()); lastSnap = (time + dif) / tl.duration() / -ratio; return lastSnap; }, onRelease() { syncIndex(); draggable.isThrowing && (indexIsDirty = true); }, onThrowComplete: () => { syncIndex(); wasPlaying && tl.play(); } })[0]; tl.draggable = draggable; } tl.closestIndex(true); lastIndex = curIndex; onChange && onChange(items[curIndex], curIndex); timeline = tl; return () => window.removeEventListener("resize", onResize); // cleanup }); return timeline; }  
    Is that better? 
  13. GreenSock's post in Timeline with delayed calls not playing smoothly was marked as the answer   
    It's not really a bug. It's just a fundamental logic problem in the way you're setting things up. Let me explain...
     
    In order for a callback to fire, the playhead must cross that spot on its parent timeline, or land directly on top of it. So it's based on the playhead moving (its new position). The timeline doesn't render for the first time until the next tick (it'd be silly to render right away by default because the playhead hasn't moved anywhere yet, so it'd be a waste of CPU cycles). That's why the very first one didn't fire right away. 
     
    The timeline's playhead updates on each "tick" which is typically about every 16.67ms but that really depends on the browser and how busy the CPU is, etc. 
     
    Your timeline is 2 seconds long and has repeat: -1. So let's say it renders almost at the end, at like 1.9857 seconds, and then on the next tick, the totalTime renders at 2.013 which means that it went past the end and wrapped around to the beginning, and 0.013 seconds into the timeline (from the start). In that ONE tick, it'd fire that callback that's at the very end of the timeline AND since it looped back to the beginning and went a little bit past, it ALSO triggers the callback that's sitting at the very start. Great. 

    BUT
     
    What if the playhead happens to land EXACTLY at the end of the timeline (2 seconds precisely)? What do you think should happen? Obviously the callback at the end should fire, but should the callback that's sitting at the very START of the timeline also fire? I mean the end of the timeline and the start of the timeline are not the same technically, so it'd be weird if both fired. The playhead can't be at 2 seconds AND at 0 seconds. It wouldn't make a lot of sense to fire the callbacks from BOTH places on that ONE tick. 
     
    See the problem? 
     
    There are many ways to accomplish what I think you're trying to do there (alter visibility of things in a synchronized way), but I'd need to see what other requirements you have in order to offer the best recommendation. 
     
    Thanks for the excellent minimal demo, by the way. 👍
  14. GreenSock's post in Gsap Flip & Scrolltrigger scroll effect was marked as the answer   
    I noticed a few problems:
    You weren't doing proper cleanup - React calls hooks TWICE in strict mode (annoying, I know). Since you didn't return the sphere to the original parent, the 2nd time the hook ran, the sphere was already reparented to the target, thus the Flip animation wasn't really doing anything (the state was identical) You had overflow: hidden on the second element, thus you couldn't see the sphere.  Is this more like what you were looking for? 
    https://stackblitz.com/edit/react-11fqur?file=src%2FApp.js
  15. GreenSock's post in How to animate a circle border as I scroll was marked as the answer   
    Here's a great article from @PointC:
    https://www.motiontricks.com/svg-dashed-line-animation/
     
    And yes, absolutely, GSAP's DrawSVGPlugin is great for that kind of thing. You'll need a Club GSAP membership. https://gsap.com/pricing 
  16. GreenSock's post in GSAP .kill() doesn't stop ScrollTrigger onUpdate() from running was marked as the answer   
    Great catch, @aviolin. Sorry about any confusion there - it should be resolved in the next release which you can preview at: 
    https://assets.codepen.io/16327/gsap-latest-beta.min.js
     
    As a workaround, you can just add this: 
    tween.scrollTrigger && tween.scrollTrigger.kill();
    See the Pen zYXpmBB?editors=0010 by GreenSock (@GreenSock) on CodePen
  17. GreenSock's post in Docs on start/end parameters for ScrollTrigger was marked as the answer   
    Sorry if that was confusing, but let me clarify...
     
    You define the start/end values in the vars object when creating a ScrollTrigger, but then that gets parsed into a numeric property value which is what you were looking at in the docs. That property on the ScrollTrigger instance is ALWAYS a number because it's the result of the calculations at that particular viewport size. If you resize the viewport, the number would likely change. But again, that's the result of all the calculations that are based on the start/end values you defined in your vars object. 
     
    So for example, let's say you did this: 
    let st = ScrollTrigger.create({ trigger: ".trigger", start: "top center" }); Notice the start was set to "top center" which just means "when the top of the trigger element hits the center of the viewport", but then if you check st.start you'll see a number like 523 which simply means that the ScrollTrigger will start when the page scrolls 523px from the top - that is where the top of the trigger hits the center of the viewport. 
     
    If you then resized the viewport and check st.start, it may be a completely different number, like 418 which means that now that ScrollTrigger will start when the page scrolls 418px from the top because now that is where the top of the trigger hits the center of the viewport. 
     
    So you were looking at that property in the docs rather than the vars configuration object properties. 
     
    Here's an excerpt from the ScrollTrigger docs for the vars.start property:
    Is that what you're looking for? 
  18. GreenSock's post in scrolltrigger use pin and once, but pin-spacer cause padding space when scroll back was marked as the answer   
    You're making one of the common ScrollTrigger mistakes - nesting ScrollTriggers inside of a timeline. That can't logically work because you can't have the parent timeline AND the scroll position both control the same playhead(s), as they could be going in different directions. So either separate out each tween into its own individual tween that has a ScrollTrigger on it, OR put your tweens into a timeline that has a ScrollTrigger applied to that timeline. 
     
    As for once: true, you could do something like this instead: 
    onLeave: self => { let max = ScrollTrigger.maxScroll(window), // record maximum scroll amount, and the current scroll position scroll = self.scroll(); self.kill(true, true); self.scroll(scroll - (max - ScrollTrigger.maxScroll(window))); // adjust the scroll based on how much was lost when self.kill() was called. }  

    See the Pen jORLdmy?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    (I didn't fix all your nested ScrollTriggers - you'll need to work through those). 
  19. GreenSock's post in ScrollTrigger: i want to fixed the position of element at center of screen while moving along SVG Path was marked as the answer   
    I built a helper function specifically for this: 

    See the Pen vYMJdjB?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    Related forums post: 
     
    I hope that helps!
  20. GreenSock's post in ScrollSmoother and getBoundingClientRect was marked as the answer   
    Yeah, that's because you're listening to the window's "scroll" event, but ScrollSmoother gradually applies that. So you're using stale values. Just set up an onUpdate event handler on the ScrollSmoother, and for maximum performance you can leverage a cache boolean to only trigger updates when absolutely necessary: 

    See the Pen RwOZBjp?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    Is that what you're looking for? 
  21. GreenSock's post in Problems resizing a scene with a pinned ScrollTrigger + SplitText with absolute position was marked as the answer   
    Are you trying to do something like this?: 

    See the Pen GRPdENx?editors=0010 by GreenSock (@GreenSock) on CodePen
  22. GreenSock's post in Animating to auto height while using grid display with media query breakpoints was marked as the answer   
    When an animation renders for the first time, it records the start/end values internally so that it can very quickly interpolate between them during the animation. You've created a scenario where you actually want to CHANGE those values on resize. invalidate() will do exactly that - it flushes any recorded start/end values so that on the next render of that animation, it'll re-calculate those. 
     
    With that in mind, you can just invalidate the animation and restore its progress: 
    window.addEventListener("resize", () => { let progress = tl.progress(); // remember the progress tl.progress(0).invalidate().progress(progress); // rewind to the beginning, invalidate to flush any recorded start/end values, then restore the progress });
    See the Pen BaEdQBy?editors=0110 by GreenSock (@GreenSock) on CodePen
     
    Is that what you're looking for? 
     
    You could also consider using a gsap.matchMedia(). There are many ways to accomplish something similar 🙂
  23. GreenSock's post in Strange behaviour of a pin element using Scrolltriggers was marked as the answer   
    That sounds like it could simply be a consequence of the fact that most modern browsers handle scrolling on a completely different thread than the main JS thread, thus it renders the content as if it was scrolled (without any ScrollTrigger logic applied) first, and THEN when the JS thread executes, ScrollTrigger does what it must do to pin the element, making it look like it jumps back. 
     
    Two things you can try (assuming that's the problem you're noticing):
     
    1) Try setting ScrollTrigger.normalizeScroll(true) which forces all the scrolling to occur on the main thread. 
     
    2) Use a scroll smoothing library like ScrollSmoother or Lenis which accomplishes a similar thing, unhooking the native scroll and implementing JS-based scrolling instead. 
  24. GreenSock's post in Variable height stacked pinning with combined lateral split sections was marked as the answer   
    You can't have nested pinning, but you could probably use position: sticky like this: 

    See the Pen MWRoQoV?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    Is that what you're looking for? 
  25. GreenSock's post in GSAP unable to animate clip-path: inset(... was marked as the answer   
    That's because the browser's getComputedStyle() doesn't return the calc() data properly. So when GSAP gets the starting (from) value, the browser gives it a value with no calc(), and then the end string has calc() in it with multiple numbers. It's the same issue with your other demos (mis-matched data/quantities). There are two solutions: 
     
    1) Use CSS variables:

    See the Pen RwOgRJQ by GreenSock (@GreenSock) on CodePen
     
    2) Use a normal .fromTo() tween so that GSAP understands what you're trying to do and can use the raw starting value instead of a computed one from the browser: 

    See the Pen wvZeWXQ?editors=0110 by GreenSock (@GreenSock) on CodePen
     
×
×
  • Create New...