Open1

ScrollTriggerでpinした要素の子要素に対してScrollTrogger的なアニメーション適用するやつ

のっくんのっくん

前置き

  • ①スクロール準拠のアニメーションがウィンドウ内にいくつもある、タイミングがそれぞれ違うなど複雑なスクロールアニメーションがを要求された。
  • ②上記に加えて、スクロール固定した親要素の子要素がスクロール準拠のアニメーションする演出がいっぱいあった。
  • 単純にアニメーションする要素毎にScrollTriggerつけたら管理しづらい・パフォーマンス的に悪いかもってなった。
  • ②に関しては要素固定したタイミングから○割までスクロールしたらアニメーション開始させたい要望などがあり、アニメーション要素のDOM位置に準拠させたScrollTriggerの設定ではタイミングの調整が難しい場合があった。

作ったもの

https://codesandbox.io/s/scrolltrigger-pin-v9te9m

コード全体

ScrollFixed.js
import { gsap } from "gsap-trial";
import { ScrollTrigger } from "gsap-trial/ScrollTrigger";

/**
 * @param { object } param0
 * @param {HTMLElement} param0.element
 * @param {number} param0.scrollAmount
 */
class ScrollFixed {
  constructor({ element, scrollAmount }) {
    this._element = element;

    this._onEnter = () => {};
    this._onEnterBack = () => {};
    this._onLeave = () => {};
    this._onLeaveBack = () => {};
    this._onUpdate = () => {};

    this._init({ element, scrollAmount });
  }

  set onEnter(onEnter) {
    this._onEnter = onEnter;
  }

  set onEnterBack(onEnterBack) {
    this._onEnterBack = onEnterBack;
  }

  set onLeave(onLeave) {
    this._onLeave = onLeave;
  }

  set onLeaveBack(onLeaveBack) {
    this._onLeaveBack = onLeaveBack;
  }

  set onUpdate(onUpdate) {
    this._onUpdate = onUpdate;
  }

  /**
   * 初期化
   * @param { object } param0
   * @param {HTMLElement} param0.element
   * @param {number} param0.scrollAmount
   */
  _init({ element, scrollAmount }) {
    this._element = element;
    this._scollAmount = scrollAmount
      ? scrollAmount
      : this._element.clientHeight;
    this._setScrollFixed();
  }

  /**
   * スクロール固定にする
   */
  _setScrollFixed() {
    ScrollTrigger.create({
      markers: true,
      trigger: this._element,
      start: "top top",
      end: `top+=${this._scollAmount} top`,
      scrub: true,
      pin: true,
      onEnter: () => this._onEnter(),
      onEnterBack: () => this._onEnterBack(),
      onLeave: () => this._onLeave(),
      onLeaveBack: () => this._onLeaveBack(),
      onUpdate: (self) => this._onUpdate(self.progress)
      // pinSpacing: false
    });
  }
}

export { ScrollFixed };

ScrollAnimation.js
import { ScrollFixed } from "../src/ScrollFixed";
import { gsap } from "gsap-trial";

/**
 * 値のマッピング
 * num     : マッピングする値
 * toMin   : 変換後の最小値
 * toMax   : 変換後の最大値
 * fromMin : 変換前の最小値
 * fromMax : 変換前の最大値
 *
 * @export
 * @param {number} num
 * @param {number} fromMin
 * @param {number} fromMax
 * @param {number} toMin
 * @param {number} toMax
 * @returns {number}
 */
const map = (num, fromMin, fromMax, toMin, toMax) => {
  if (num <= fromMin) return toMin;
  if (num >= fromMax) return toMax;

  const p = (toMax - toMin) / (fromMax - fromMin);
  return (num - fromMin) * p + toMin;
};

/**
 * @param {Object} param0
 * @param {HTMLElement} param0.element [data-message]
 */
class ScrollAnimations {
  constructor({ element }) {
    this._init({ element });
  }

  /**
   * 初期化
   * @param {object} param0.element [data-message]
   */
  _init({ element }) {
    this._element = element;
    this._textElement = this._element.querySelector(
      '[data-scroll-animations="text"]'
    );
    this._imgElement = this._element.querySelector(
      '[data-scroll-animations="img"]'
    );

    this.imgIsVisible = false;

    // ScrollFixedインスタンス
    this._scrollFixedInstance = new ScrollFixed({
      element,
      scrollAmount: 3000
    });

    // ScrollTriggerアップデート時の処理
    this._scrollFixedInstance.onUpdate = (progress) => {
      // アニメーションする要素のタイムラインを0~1で準備してあげる
      const backGroundTimeline = map(progress, 0, 1, 0, 1);
      const messageTimeline = map(progress, 0.5, 0.8, 0, 1);
      const imgTimeline = map(progress, 0.8, 0.9, 0, 1);

      // アニメーションのアップデートを実行
      this._setUpdateBackGround(backGroundTimeline);
      this._setUpdateMessage(messageTimeline);
      this._setUpdateImg(imgTimeline);
    };
  }

  /**
   *
   * @param {Number} progress //0~1
   */
  _setUpdateBackGround(progress) {
    const c = [120 + 125 * progress, 20 + 125 * progress, 80 + 125 * progress];
    gsap.set(this._element, {
      background: `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${progress})`
    });
  }

  /**
   *
   * @param {Number} progress //0~1
   */
  _setUpdateMessage(progress) {
    gsap.set(this._textElement, {
      autoAlpha: progress
    });
  }

  /**
   *
   * @param {Number} progress //0~1
   */
  _setUpdateImg(progress) {
    if (progress > 0 && !this.imgIsVisible) {
      // 表示処理
      gsap.to(this._imgElement, {
        autoAlpha: 1,
        duration: 1.0
      });
      this.imgIsVisible = true;
    } else if (progress <= 0 && this.imgIsVisible) {
      // 非表示処理
      gsap.to(this._imgElement, {
        autoAlpha: 0,
        duration: 1.0
      });
      this.imgIsVisible = false;
    }
  }
}

export { ScrollAnimations };

要点

  _setScrollFixed() {
    ScrollTrigger.create({
      markers: true,
      trigger: this._element,
      start: "top top",
      end: `top+=${this._scollAmount} top`,
      scrub: true,
      pin: true,
      onEnter: () => this._onEnter(),
      onEnterBack: () => this._onEnterBack(),
      onLeave: () => this._onLeave(),
      onLeaveBack: () => this._onLeaveBack(),
      onUpdate: (self) => this._onUpdate(self.progress)
      // pinSpacing: false
    });
  }

スクロールを固定するための親要素を作成、ScrollTriggerを設定し、スクロール進捗率はprogressで取得。onUpdate関数の中でアニメーションの定義を行う。

this._scrollFixedInstance.onUpdate = (progress) => {
      // アニメーションする要素のタイムラインを0~1で準備してあげる
      const backGroundTimeline = map(progress, 0, 1, 0, 1);
      const messageTimeline = map(progress, 0.5, 0.8, 0, 1);
      const imgTimeline = map(progress, 0.8, 0.9, 0, 1);

      // アニメーションのアップデートを実行
      this._setUpdateBackGround(backGroundTimeline);
      this._setUpdateMessage(messageTimeline);
      this._setUpdateImg(imgTimeline);
    };

progress(0~1)の値をmap関数でアニメーション開始させたいタイミングと終了タイミングを設定。