👌

スクロールした時、指定した画面領域内にいる時のみアニメーションを実行するテスト

2022/11/29に公開

スクロールした際、指定した画面領域内でのみアニメーションを発火させるサンプルです。
交差オブザーバーで検知しているので、scrollイベントよりパフォーマンスは良いかと思います。

どちびさん https://zenn.dev/con_ns_pgm が作成したアニメーションクラス
サカタさん https://zenn.dev/sakata_kazuma が追加したRAF,FPS関係を落とし込み
対象をquerySelectorからquerySelectorAllに変更、rootMarginは数値のみでなく"-10%"など%対応、初期位置追加など改良を加えました。

new ScrollFunctionAll(".js-sf-target01", 500, -150, 300, 1000);
new ScrollFunctionAll(".js-sf-target02", 500, "-30%", 300, 1000);
new ScrollFunctionAll(".js-sf-target03", 500, "-30%", "5%", "80%");
'use strict';
import { GetScrollNum } from './_GetScrollNum';


export class ScrollFunctionAll {
  constructor(target, scrollMax, rootMargin, min, max, fps = 60) {
    this.target = document.querySelectorAll(target); //ターゲット
    if (!this.target) return;
    this.scrollMax = scrollMax; //任意のスクロール量の最大値
    this.rootMargin = rootMargin; //画面のどこにtargetが来たら検知開始するか
    this.min = min; //任意の範囲の最小値
    this.max = max; //任意の範囲の最大値
    this.framesPerSecond = fps; //フレームレート値
    this.resultNormal = 0; //linearでmin~maxの間を変化する変数
    this.resultEase = 0; //イージングを適応してmin~maxまで変化する変数
    this._scrollAnimation();
  }

  _scrollAnimation() {
    const _that = this;

    let rootMarginPercentFlg = false;
    const rootMarginPercent = _that.rootMargin;
    let rootMarginNum;
    if(typeof rootMarginPercent == "string" && rootMarginPercent.includes("%")) {
      rootMarginPercentFlg = true;
      const length = rootMarginPercent.lastIndexOf("%");
      const regexp = new RegExp('.{0,' + length + '}', 'g');
      rootMarginNum = rootMarginPercent.match(regexp)[0];
    }

+   /**
+    * 最小値・最大値が%だった時の処理
+    */
+   let winWidth;
+   const percentProcessingMin = () => {
+     let convertedNum;
+     if(typeof this.min == "string" && this.min.includes("%")) {
+       winWidth = window.innerWidth;
+       const length = this.min.lastIndexOf("%");
+       const regexp = new RegExp('.{0,' + length + '}', 'g');
+       convertedNum = this.min.match(regexp)[0];
+       this.min = winWidth * convertedNum /100;
+     }
+   }
+   percentProcessingMin();

+   const percentProcessingMax = () => {
+     let convertedNum;
+     if(typeof this.max == "string" && this.max.includes("%")) {
+       winWidth = window.innerWidth;
+       const length = this.max.lastIndexOf("%");
+       const regexp = new RegExp('.{0,' + length + '}', 'g');
+       convertedNum = this.max.match(regexp)[0];
+       this.max = winWidth * convertedNum /100;
+     }
+   }
+   percentProcessingMax();

    this.target.forEach(el => {
      /*
        リフローを起こす処理はスクロール中に実行しない
        https://gist.github.com/paulirish/5d52fb081b3570c81e3a
      */
      let offsetTop;   //ページの一番上からの位置
      let endPosition; //要素の終了位置
      let winHeight;   //ウィンドウ高さ

      /**
       * ポジション取得
       */
      const getPosition = () => {
        const scrollTop = window.pageYOffset;
        /*
          getBoundingClientRect().topは画面上の相対値を返すので、
          「現在のスクロール量 + 画面内の相対値」で
          ページの一番上からの位置が取得できる(jQueryのoffset().topと同じ)
        */
        offsetTop = el.getBoundingClientRect().top + scrollTop;
        //要素の終了位置 clientHeightやinnerHeightなどサイズ取得系もリフローが発生する
        endPosition = offsetTop + el.clientHeight;
        winHeight = window.innerHeight;
      }
      getPosition();
      window.addEventListener('resize', getPosition);

      //requestAnimationFrameをリセットするためのIDを格納
      let frameId;
      //直前のスクロール位置を格納する変数
      let lastPosition = -1;
      //繰り返し処理を行っても良いか判別用フラグ
      let isVisible = false;


      /**
       * フレームレート設定
       * 参考:https://www.kirupa.com/animations/fixing_frame_rate_for_consistent_animations.htm
       */
      // const framesPerSecond = 60;
      const interval = Math.floor(1000 / _that.framesPerSecond);
      const startTime = performance.now();
      let previousTime = startTime;
      let currentTime = 0;
      let elapsed = 0;

+     //要素の初期位置
+     el.setAttribute("style", `transform: translateX(${_that.min}px)`);


      /**
       * スクロールアニメ実行関数
       */
      const scrollAnime = (timestamp) => {
        /*
          window.pageYOffsetは↓のリフローを起こすリストにもないので、
          リフローは発生していない(?)
          https://gist.github.com/paulirish/5d52fb081b3570c81e3a
      
          以下の記事でも「window.pageYOffsetで取得しろ」と書いてありました。
          https://gist.github.com/Warry/4254579#beware-of-reflows
          "use window.pageYOffset to get the scroll position"
        */
        //スクロール量を取得
        const scrollTop = window.pageYOffset;
        //直前のスクロール位置と比較して変化がない場合
        if (lastPosition === scrollTop) {
          //リクエストID取得
          frameId = requestAnimationFrame(scrollAnime);
          return;
        }
        //直前のスクロール位置と比較して変化がある場合のみ、直前の位置を更新
        lastPosition = scrollTop;


        /**
         * フレームレート調整
         * https://www.kirupa.com/animations/fixing_frame_rate_for_consistent_animations.htm
         */
        currentTime = timestamp;
        elapsed = currentTime - previousTime;
        if (elapsed <= interval) {
          frameId = requestAnimationFrame(scrollAnime);
          return;
        }
        previousTime = currentTime - (elapsed % interval);


        //画面下部のスクロール位置
        const scrollBottom = scrollTop + winHeight;

        //isIntersecting領域に入る位置を取得する関数
        function targetScroll() {
          // return scrollTop - offsetTop + innerHeight + this.rootMargin;
          if(rootMarginPercentFlg) {
            return scrollTop - offsetTop + winHeight + ( winHeight * rootMarginNum / 100);
          }

          return scrollTop - offsetTop + winHeight + _that.rootMargin;
        }

        //アニメーション量の計算
        const calculation = () => {
          const normalizeNum = _that._norm(targetScroll(), 0, _that.scrollMax); //0~1
          const easeNum = _that._easeInOutBack(normalizeNum); //イージングが効いた値
          _that.resultNormal = _that._lerp(_that.min, _that.max, normalizeNum);
          _that.resultEase = _that._lerp(_that.min, _that.max, easeNum);
        }
        calculation();


-       //要素の初期位置
-       el.style.transform = `translateX(${_that.min}px)`;

        //要素が画面下から完全に出るまでの範囲
        if (scrollBottom > offsetTop && scrollBottom < endPosition) {
          //console.log('こんにちはあああああ!!');

          //要素が画面上から出ていくまでの範囲
        } else if (scrollTop > offsetTop && scrollTop < endPosition) {
          //console.log('さようならあああああ!!');

          //画面内で完全に見えている状態
        } else if (scrollBottom > offsetTop && scrollTop < endPosition) {
          //console.log('完全に画面内にいるよおおおお!!');

          if (_that.resultNormal < _that.min) {
            _that.resultNormal = _that.min
          } else {
            if (_that.resultNormal > _that.max) {
              _that.resultNormal = _that.max
            }
          }

-         el.style.transform = `translateX(${_that.resultNormal}px)`;
+	  el.setAttribute("style", `transform: translateX(${_that.resultNormal}px)`);
        }

        /*
          entry.isIntersectingがtrueの時だけループする
          これがないと画面外に出た時、
          出る直前に実行された関数でループ処理が持続してしまうことがある
        */
        if (isVisible) {
          frameId = requestAnimationFrame(scrollAnime);
        }
      }
      frameId = requestAnimationFrame(scrollAnime);

      //画面に入った時だけ処理する
      const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          isVisible = entry.isIntersecting;
          if (isVisible) {
            frameId = requestAnimationFrame(scrollAnime);
          } else {
            //画面外のときは削除
            cancelAnimationFrame(frameId);
          }
        });
      });
      //observer監視開始
      observer.observe(el);
    });
  }

  /**
   * イージング関数
   */
  _easeInOutQuint(x) {
    return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
  }

  _easeInQuint(x) {
    return x * x * x * x * x;
  }

  _easeInOutBack(x) {
    const c1 = 1.70158;
    const c2 = c1 * 1.525;

    return x < 0.5
      ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
      : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
  };

  //正規化
  _norm(v, a, b) {
    return (v - a) / (b - a);
  }

  //線形補完
  _lerp(a, b, t) {
    return a + (b - a) * t;
  }
}
'use strict';

export class GetScrollNum {
  constructor() {
    this.scrollY = scrollY;
    this.normalizeScrollY = "";
    this.scrollRate = "";
    this.viewH = document.documentElement.scrollHeight;
    this.totalScrollY = this.viewH - innerHeight;
    this._getResult();
  }

  _getResult() {
    this.scrollY = window.pageYOffset;//スクロール量
    this.normalizeScrollY = this.scrollY / this.totalScrollY;//スクロール量正規化
    this.scrollRate = Math.floor(this.normalizeScrollY * 100);//スクロール率
  }
}

Discussion