Jump to content
Search Community

GreenSock last won the day on April 12

GreenSock had the most liked content!

GreenSock

Administrators
  • Posts

    23,122
  • Joined

  • Last visited

  • Days Won

    816

Everything posted by GreenSock

  1. 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?
  2. 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
  3. Unfortunately we can't really troubleshoot a live site (way too many factors and impossible to tweak/experiment), but it does look like you're using lazy-loading images which can be problematic. Basically, you need to call ScrollTrigger.refresh() when the layout is done shifting around so that the calculations are correct. You could explicitly set the width/height on your images so that they don't cause layout shifts, or you could use a helper function like the one in this thread: If you still need some help, please make sure you create a minimal demo (like CodePen/Stackblitz) that clearly illustrates the issue and then we'd be happy to look at that and answer any GSAP-related questions.
  4. Just check the docs for the scroller property. scroller: "#your-scroller"
  5. Yeah, that looks like a browser rendering thing, but you could try setting this on any transform-related animation: force3D: false Does that help?
  6. 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?
  7. 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: https://codepen.io/GreenSock/pen/NWmMgjZ
  8. Personally, I would take an entirely different approach to this: https://codepen.io/GreenSock/pen/wvZjeGN?editors=1010 That gives you much more flexibility and the transitions are more intuitive.
  9. I don't have time to dig into that right now, but wouldn't it be?: end: .5 + cardSpaceAround[index] / 2
  10. Are you trying to do something like this?: https://codepen.io/GreenSock/pen/KKpLdWW There are a lot of demos here: https://codepen.io/collection/AEbkkJ
  11. There's a demo on the docs page I linked you to already. You can try the plugin for FREE on CodePen, Stackblitz, etc. See https://gsap.com/trial I'm not sure what you mean by "...where we can make it more smooth??" 🤷‍♂️
  12. That looks like exactly what ScrambleTextPlugin does: https://gsap.com/docs/v3/Plugins/ScrambleTextPlugin It's a membership benefit of Club GSAP, just so you know. 👍
  13. Yeah, definitely a browser rendering issue. I did poke around a bit and it looks like the fundamental problem has to do with the mask, so the key is to make some kind of change that forces the browser to kinda wake up and realize it should re-render. So here's a fork where I set x: 0.01 on the mask element (I added a "test" class to it) AFTER the y position of the logo is updated: https://codepen.io/GreenSock/pen/QWPmRxz?editors=0010 If you remove line 29, you'll see that the logo doesn't render properly at the y: 200 value. I hope that helps.
  14. Also, you can easily force a render of a tween or timeline, and even improve runtime performance slightly by forcing all the tweens inside a timeline to initialize and grab their start/end values like this: // jump to the end and immediately back to the start animation.progress(1).progress(0);
  15. Sure, that's one way you could do it. A few suggestions: Don't use "new": // BAD let childTl = new gsap.timeline({}); // GOOD let childTl = gsap.timeline(); This can be simplified: // OLD elements.forEach((element, index, array) => { element.style.display = "none"; }); // NEW gsap.set(elements, {display: "none"}); Since you're not using params anyway, just use .add() instead of .call(): // OLD childTl.call(() => { //... }, [], 1); // NEW childTl.add(() => { //... }, 1); I think you could greatly simplify the logic too: https://codepen.io/GreenSock/pen/YzMaBME?editors=0010 Like I said, there are many, many ways to tackle this. Hopefully this helps get you on your way to something that works well for you.
  16. Oh, that's a logic issue in the way you coded it - you're depending on the onToggle to always fire, but if you scroll very quickly it might not because the active state may not toggle. For example, let's say you've got a ScrollTrigger that starts at a scroll position of 100 and ends at 150, and the user scroll really fast such that the scroll position goes from 98 to 161 (skipping over that ScrollTrigger) - it'd never toggle. You could just create a simple timeline with callbacks positioned on it like this: https://stackblitz.com/edit/stackblitz-starters-hgyqdb?file=components%2FValue.tsx Is that better?
  17. I had a very difficult seeing any issue even in your video. 🤷‍♂️ iOS is absolutely TERRIBLE with scroll-related things. There are a bunch of bugs in the browser itself, some of which were reported years ago and still haven't been fixed. It's unbelievable to me. If I remember correctly, iOS runs the requestAnimationFrame() updates at only 30fps instead of 60fps in certain scenarios, like in an iframe, until the user interacts with it (taps or drags or does something like that). I assume they're trying to "optimize" things to reduce battery drain. But of course that has very annoying side effects. What's very odd to me when I glanced at your code was that you're attaching a ScrollTrigger to a timeline, thus it by default will use toggleActions, but you're controlling that very same timeline inside of the onEnter/onLeave/onEnterBack/onLeaveBack. Why are you even attaching the ScrollTrigger to that timeline itself? It seems counter-intuitive, since you're potentially fighting with toggleActions. Why not just do a normal ScrollTrigger.create()? ScrollTrigger.create({ trigger: middle, start: "top 40%", end: "+=" + (middle.offsetHeight / 1.5), markers: true, onEnter: () => { tl.timeScale(1).play(); }, onLeave: () => { tl.timeScale(3).reverse(); }, onEnterBack: () => { tl.timeScale(1).play(); }, onLeaveBack: () => { tl.timeScale(3).reverse(); }, }); (Do that instead of putting it in the gsap.timeline({...})) In any case, the overall issue here really doesn't seem GSAP-related. It sounds like challenges related to iOS itself.
  18. 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. 👍
  19. 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
  20. Looks to me like putting all those in a container <div> and then animating the rotationX/rotationY/x/y of that, while having a perspective applied. Give it a shot and if you get stuck and have a GSAP-specific problem, feel free to post back here with a minimal demo (like a CodePen) 👍 Good luck!
  21. There's also a helper function that may help: https://codepen.io/GreenSock/pen/vYMdMZN?editors=0010
  22. 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.
  23. You don't have to use CSS variables. Here's another way you can just use a normal object as a proxy: https://codepen.io/GreenSock/pen/PogELWo?editors=0010
  24. Yes, exactly - you'll need a Premium or Business Club GSAP membership to get access to the bonus plugins like MotionBlurPlugin. 👍
  25. This is the pretty much the same issue (and answer) as your other posts. Let me try to explain the process... When you do a normal tween, like a .to() or .from(), it must get the CURRENT value from the browser using window.getComputedStyle(element). You're using calc() values which get calculated and the result gets returned by the browser. So, for example: // you set: `inset(0% 0% 0% calc(0% - 0px) round 200px)` // browser returns via window.getComputedStyle(): `inset(0% round 200px)` So now GSAP has to figure out how to interpolate between those strings which have different amounts of numbers (thus it isn't really feasible). That is what's biting you. If, however, you use a .fromTo() tween where you're feeding in BOTH the start and the end, GSAP can discern what you're asking it to discern - it doesn't have to pull the current style from window.getComputedStyle() which is why it works: https://codepen.io/GreenSock/pen/ExJorrv?editors=0010 Since you've got the same amount of numbers in the start/end strings, they'll interpolate cleanly. We've already given you several solutions in the past - use the fromTo() or you can use CSS variables instead, and animate those with GSAP.
×
×
  • Create New...