Jump to content
Search Community

GreenSock last won the day on April 21

GreenSock had the most liked content!

GreenSock

Administrators
  • Posts

    23,151
  • Joined

  • Last visited

  • Days Won

    817

Posts posted by GreenSock

  1. Hi @denglertarea23

     

    I noticed two problems: 

    1. You don't have a "Business" Club GSAP membership, but you're trying to install the members-only package via the private repository. You don't have access to that. You must sign up first. https://gsap.com/pricing
    2. You must not have configured the .npmrc file properly, because according to your error code, it's trying to find that package in the main npmjs.org registry instead of the private npm.greensock.com registry. Make sure you follow the installation instructions carefully. https://gsap.com/install 
  2. In case it wasn't already clear, that "jumping" that occurs when you don't have enough elements to "fill" the area horizontally is not a bug or anything - an element cannot exist in two places at once. Like you can't have half of the element showing on the left side of the screen while the other half of the element shows on the right side of the screen. It's in one place or the other. 

  3. 5 hours ago, Jake H said:

    I did try something along those lines, but I used the scroll trigger directly rather than getting it from the timeline.  Like this:

    That's not valid - you should pass a ScrollTrigger configuration object to the timeline, not a ScrollTrigger instance itself: 

    // BAD
    let st = ScrollTrigger.create({...});
    gsap.timeline({ scrollTrigger: st });
    
    // GOOD
    let st = {...};
    gsap.timeline({ scrollTrigger: st });

    Alternatively, you could do the inverse, where you create the animation and then pass it into the ScrollTrigger.create(): 

    let tl = gsap.timeline();
    tl.to(...)
      .to(...);
    
    ScrollTrigger.create({
      animation: tl,
      ...
    });

    Does that clear things up? 

    • Like 1
  4. 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? 

    • Like 2
  5. 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

    • Like 2
  6. 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. 

  7. 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? 

    • Thanks 1
  8. 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: 

     

    See the Pen QWPmRxz?editors=0010 by GreenSock (@GreenSock) on CodePen

     

    If you remove line 29, you'll see that the logo doesn't render properly at the y: 200 value. 

     

    I hope that helps. 

  9. 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);

     

    • Thanks 1
  10. Sure, that's one way you could do it. A few suggestions:

    1. Don't use "new": 
      // BAD
      let childTl = new gsap.timeline({});
      
      // GOOD
      let childTl = gsap.timeline();

       

    2. This can be simplified: 
      // OLD
      elements.forEach((element, index, array) => {
        element.style.display = "none";
      });
      
      // NEW
      gsap.set(elements, {display: "none"});

       

    3. 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: 

    See the Pen YzMaBME?editors=0010 by GreenSock (@GreenSock) on CodePen

     

    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. 

    • Thanks 1
  11. 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? 

  12. I had a very difficult seeing any issue even in your video. 🤷‍♂️

     

    6 hours ago, Paul Yabsley said:

    Very curious why it happens in iOS but not desktop browsers and (I'm told) not on Android devices. 

    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. 

  13. 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. 👍

    • Like 2
    • Thanks 1
  14. 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. 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!

×
×
  • Create New...