Gsap结合scrollTrigger动画实现

1,578 阅读3分钟

使用场景: 随着滚动条的滚动,外层盒子sticky定住,并随着滚动条滚动切换盒子中的图片。 解决方案:

  1. 滚动一次,执行切换的动作,即触发后执行一次完成的切换,如下示例。使用observer监听并替换原生滚动事件实现。
 //   注册插件
    gsap.registerPlugin(ScrollTrigger);

    //   判断是否有滚动进行中
    let allowScroll = true; // sometimes we want to ignore scroll-related stuff, like when an Observer-based section is transitioning.
    //   稳定滚动效果,这个延迟调用在创建后被立即暂停,所以它不会自动开始倒计时。
    let scrollTimeout = gsap.delayedCall(0.8, () => (allowScroll = true)).pause(); // controls how long we should wait after an Observer-based animation is initiated before we allow another scroll-related action
    //   当前滚动到的面板索引
    let currentIndex = 0;
    //   滚动的图片数组
    let swipePanels = gsap.utils.toArray("#right-part .picture-box");
    console.log("swipePanels", swipePanels);
    $('#right-part').height = $('#right-part .picture-box').offsetHeight;

    // set z-index levels for the swipe panels
    //   给图片添加数值,用于给图片添加层级
    gsap.set(swipePanels, { zIndex: (i) => swipePanels.length - i });

    // create an observer and disable it to start
    let intentObserver = ScrollTrigger.observe({
      // 监听滚动和触摸滚动
      type: "wheel,touch",
      // onUp: () => allowScroll && gotoPanel(currentIndex - 1, false),
      onUp: (self) => {
          console.log("self.event.type", self.event.type);
          if (self.event.type === "wheel") {
            allowScroll && gotoPanel(currentIndex - 1, false);
          } else if (self.event.type === "touchmove") {
            allowScroll && gotoPanel(currentIndex + 1, true);
          }
        },
      // onDown: () => allowScroll && gotoPanel(currentIndex + 1, true),
      onDown: (self) => {
          console.log("self.event.type", self.event.type);
          if (self.event.type === "wheel") {
            allowScroll && gotoPanel(currentIndex + 1, true);
          } else if (self.event.type === "touchmove") {
            allowScroll && gotoPanel(currentIndex - 1, false);
          }
        },
      // 容忍度,防止轻微的滚动触发事件
      tolerance: 10,
      preventDefault: true,
      onEnable(self) {
        allowScroll = false;
        // 重启延迟调用
        scrollTimeout.restart(true);
        // when enabling, we should save the scroll position and freeze it. This fixes momentum-scroll on Macs, for example.
        let savedScroll = self.scrollY();
        // 如果原生滚动重新定位,则强制它回到保存的位置
        self._restoreScroll = () => self.scrollY(savedScroll); // if the native scroll repositions, force it back to where it should be
        document.addEventListener("scroll", self._restoreScroll, {
          passive: false,
        });
      },
      // 禁用观察者时移除事件监听器
      onDisable: (self) =>
        document.removeEventListener("scroll", self._restoreScroll),
    });
    // 初始化时禁用观察者
    intentObserver.disable();

    // handle the panel swipe animations
    function gotoPanel(index, isScrollingDown) {
      // return to normal scroll if we're at the end or back up to the start
      // 如果我们已经到达列表的末尾(向下滚动)或返回到开头(向上滚动),恢复正常的滚动行为
      if (
        (index === swipePanels.length && isScrollingDown) ||
        (index === -1 && !isScrollingDown)
      ) {
        // 禁用意图观察器,恢复原生滚动
        intentObserver.disable(); // resume native scroll
        return;
      }
      // 禁用滚动
      allowScroll = false;
      scrollTimeout.restart(true);

      let target = isScrollingDown
        ? swipePanels[currentIndex]
        : swipePanels[index];
      gsap.to(target, {
        yPercent: isScrollingDown ? -100 : 0,
        duration: 0.55,
        opacity:isScrollingDown ? 0 : 1,
      });

      currentIndex = index;
    }

    // pin swipe section and initiate observer
    ScrollTrigger.create({
      trigger: "#right-part",
      pin: true,
      start: "center center",
      end: "+=0", // just needs to be enough to not risk vibration where a user's fast-scroll shoots way past the end
      markers: true, // 显示开始和结束标记(用于调试)
      onEnter: (self) => {
        if (intentObserver.isEnabled) {
          return;
        } // in case the native scroll jumped past the end and then we force it back to where it should be.
        self.scroll(self.start + 1); // jump to just one pixel past the start of this section so we can hold there.
        intentObserver.enable(); // STOP native scrolling
      },
      onEnterBack: (self) => {
        if (intentObserver.isEnabled) {
          return;
        } // in case the native scroll jumped backward past the start and then we force it back to where it should be.
        self.scroll(self.end - 1); // jump to one pixel before the end of this section so we can hold there.
        intentObserver.enable(); // STOP native scrolling
      },
    });

  1. 后续方案更改,滚动条将控制执行的整个过程,即滚动一点执行一点:

  // 注册插件
  gsap.registerPlugin(ScrollTrigger);
  const images = gsap.utils.toArray(
    ".invisible-fast-focus-box #right-part .picture-box"
  );

  gsap.set(images, {
    zIndex: (i) => -(images.length - i),
    // opacity: (i) => (i === 0 ? 1 : 0),
    // visibility: "hidden",
    yPercent: (i) => (i === 0 ? 0 : 100),
  });

  /*   时间线写法在同一时间添加两个动画帧 */
  let timeLine = gsap.timeline({
    scrollTrigger: {
      trigger: ".invisible-fast-focus-box #right-part",

      // 根据索引值调整起始位置
      start: () => `center center`,
      end: () => (window.innerWidth > 992 ? `+=4000` : "+=1500"), // 结束位置
      scrub: true,
      pin: ".invisible-fast-focus-box",
      //   markers: true,
    },
  });
  images.forEach((frame, index) => {
    timeLine
      .to(
        frame,
        { yPercent: 0, ease: "power4.out", duration: index === 0 ? 0 : 3 },
        // (index - 1) * 4
        index * 4
      )
      .to(
        images[index - 1] ? images[index - 1] : {},
        { opacity: 0, scale: 0.8, ease: "power4.out", duration: 3 },
        // (index - 1) * 4
        index * 4
      )
      .to({}, { duration: 1 }); // 添加1秒的空余等待时间
  });

实现的方式多种多样,为了方便后续的更新迭代和更大的可操作性,推荐使用时间线形式来进行动画控制。上面代码中就是用了时间线,在当前图片切换进入时,给上一张图片添加了一段离场动画,二者通过duration持续时间和 .to 的第三个参数来控制个动画的占比。 若有不当之处欢迎指出,若有问题也可私聊探讨~