Jump to content
Search Community

GreenSock last won the day on May 5

GreenSock had the most liked content!

GreenSock

Administrators
  • Posts

    23,164
  • Joined

  • Last visited

  • Days Won

    818

Community Answers

  1. GreenSock's post in How to have an easing equivalent to css ease-in-out was marked as the answer   
    Yeah, I would never expect CSS and JS animations to remain synchronized. They use completely different timing mechanisms and threads. GSAP has lag smoothing too, but CSS wouldn't match that. In short, if you're trying to build something such that CSS and JS animations are 100% perfectly synchronized, I'd give up immediately and adjust your strategy. Obviously I'd recommend just using GSAP because you'll get way more flexibility overall and everything will remain synchronized. 
  2. GreenSock's post in gsap scroll and pin issue was marked as the answer   
    If I understand your goal correctly, that's just a CSS thing - for an element to have a stacking order like that, it must have position: relative (or something besides static). 
     

    See the Pen BaEgEOB?editors=0110 by GreenSock (@GreenSock) on CodePen
  3. GreenSock's post in Horizontal scroll animation with sections of different sizes. was marked as the answer   
    That's because you're using xPercent which is based on the width of each element. For example, xPercent: 100 would be a different number of pixels for an element that's 200px wide vs. one that's 500px wide. 
     
    I think you meant to do something more like this, right?: 

    See the Pen abxgjrp?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    It just involved using pixels instead of percentages. 
  4. GreenSock's post in Absolute to relative flip was marked as the answer   
    Yeah, @OSUblake is right. I was rather confused by the original demo and what your goal was there with those delays and animating things after the flip, etc. But here's a fork that I assume might be what you were after:

    See the Pen OJWexWX?editors=0010 by GreenSock (@GreenSock) on CodePen
     
  5. GreenSock's post in Timeline visualizer was marked as the answer   
    Oh, that's not an actual thing you can install - that was purely a custom interface built for that particular demo that's aimed at helping folks understand the mechanics of timelines. 
     
    There's GSDevTools for showing a basic scrubber and playback controls: https://gsap.com/docs/v3/Plugins/GSDevTools (it's a Club GSAP membership benefit). 
     
    Enjoy your learning adventure! 💚
  6. GreenSock's post in Join 2 Svgs into one Position and make them bigger was marked as the answer   
    It looks like your math calculations were off - you were calculating the offset from the current position but you were tweening to absolute numbers rather than relative ones. Is this what you meant to do?: 

    See the Pen XWQQWJK by GreenSock (@GreenSock) on CodePen
     
    But the problem with that is it's not really responsive. You're animating to specific x/y values but the original top/left absolute positioning is different, so resizing your window will result in different offsets. 
     
    I think it'd be much cleaner to use a Flip animation instead: 

    See the Pen rNbbNVe?editors=0010 by GreenSock (@GreenSock) on CodePen
     
    Does that help? 
  7. GreenSock's post in Problem with gsap in react was marked as the answer   
    You weren't doing proper cleanup. That's a React thing. It calls the useEffect() multiple times in strict mode, and you weren't reverting the tweens/ScrollTriggers properly. So you had duplicates conflicting. That's why it's a good idea to use the useGSAP() hook instead of useEffect() - it handles that for you. 
     
    You also had a different structure to your markup than in your plain HTML version. I'm not sure exactly what you're trying to do, but maybe this will get you going in the right direction: 
    https://stackblitz.com/edit/react-ztc5vg?file=src%2FApp.js
  8. GreenSock's post in ScrollTrigger.refresh doesn't do anything after pinned sections are dynamically loaded was marked as the answer   
    Yes, like @Rodrigo said, you're creating your ScrollTriggers out-of-order. You're supposed to create them in the order they would be encountered (top to bottom). You're creating the top and bottom first, then the middle, so the refreshing order goes: 1, 3, 2 instead of 1, 2, 3. 
     
    For relatively simple setups, it could be adequate to just call ScrollTrigger.sort() which will order them by whatever their "start" is calculated to be. But you can explicitly control the order of things by setting a refreshPriority on each one so you have total control of the order. 

    See the Pen PogLyGO?editors=1010 by GreenSock (@GreenSock) on CodePen
     
    And here's a verticalSort() helper function that'll sort them by their proximity to the very top of the viewport: 

    See the Pen ExJMdXj?editors=0010 by GreenSock (@GreenSock) on CodePen
  9. 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. 
  10. 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. 
  11. 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. 
  12. 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(?) 
  13. 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!
  14. 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! 💚
  15. 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().
  16. 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? 
  17. 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
  18. 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
  19. 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? 
  20. 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? 
  21. 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. 👍
  22. 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
  23. 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 
  24. 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
  25. 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? 
×
×
  • Create New...