🐧

スクロール連動アニメーションが捗るかもしれないクラスを作ってみた

2022/11/25に公開

作成したクラスで可能なこと

  • 動かしたい要素を指定
  • 動かしたい要素が、画面内のどこにきたらアニメーションを開始するか指定
  • スクロール量を取得し、任意の数値へ変換(正規化して線形補完)
  • アニメーションにイージングを効かせる

改善したいところ

  • 変化させたい数値があるたびに、インスタンス化する必要がある
  • 一度インスタンス化すると永遠にスクロールイベントが呼ばれ続けることになるのでパフォーマンスが心配

作成したクラスと使い方(コメントアウト)

JavaAcript
class ScrollFunction {
  constructor(target, scrollMax, rootMargin, ease, min, max) {
    this.target = target; //変化させたいターゲット
    this.scrollMax = scrollMax; //任意のスクロール量の最大値
    this.rootMargin = rootMargin; //画面のどこにtargetが来たら検知開始するか
    this.ease = ease; //イージング関数を入れる
    this.min = min; //任意の範囲の最小値
    this.max = max; //任意の範囲の最大値
    this.sn = new GetScrollNum();
    this.sn.getResult();//スクロール量取得用関数を実行
    this.scrollY = this.sn.scrollY; //画面上部からのスクロール量
    this.targetPos = this.target.getBoundingClientRect().top + this.scrollY; //targetのページ上部からの位置
    this.targetScroll =
      this.scrollY - this.targetPos + innerHeight + this.rootMargin; ///任意の要素がrootMarginの位置に来たときにスクロール量を0としてscrollMaxまで変化する
    this.resultNormal = 0; //linearでmin~maxの間を変化する変数
    this.resultEase = 0; //イージングを適応してmin~maxまで変化する変数
    this._getResultNum();//スクロール量を取得して値を更新する
  }

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

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

  _getResultNum() {
    //要素が画面内に最初からいる時(FV内にある時)スクロール量をそのまま正規化に使用する
    if (this.targetPos < innerHeight + this.target.clientHeight) {
      window.addEventListener('scroll', function () {
        this.sn.getResult();
        this.scrollY = this.sn.scrollY;
        if (this.scrollY > this.scrollMax) {
          this.scrollY = this.scrollMax;
        }
        const normalizeNum = this._norm(this.scrollY, 0, this.scrollMax); //0~1
        const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
        this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
        this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
      }.bind(this));
    } else {
      window.addEventListener(
        "scroll",
        function () {
          this.sn.getResult();
          this.scrollY = this.sn.scrollY;
          this.targetScroll =
          this.scrollY - this.targetPos + innerHeight + this.rootMargin;
          //要素が画面内に入ってからのスクロール量を0~this.scrollMaxに留める
          if (this.targetScroll !== undefined && this.targetScroll < 0) {
            this.targetScroll = 0;
          } else if (
            this.targetScroll !== undefined &&
            this.targetScroll > this.scrollMax
          ) {
            this.targetScroll = this.scrollMax;
          }
          const normalizeNum = this._norm(this.targetScroll, 0, this.scrollMax); //0~1
          const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
          this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
          this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
        }.bind(this)
      );
    }
  }
}

//使い方
//target(第一引数)にはElementを入れる = document.getElementByID('任意の要素');
//イベント外でインスタンス化
//スクロールイベント内で欲しい値を取得(resultEase or resultNormal)
//targetの変更したいスタイルに取得した数値を適応する

クラス内での処理の流れ

コンストラクタ内での処理

  • クラス内変数に引数を格納
  • GetScrollNumクラスをインスタンス化
  • GetScrollNumクラスのgetResultメソッドを実行
  • スクロール量を取得
  • targetのページ上部からの位置を変数に格納
  • targetが任意の位置(rootMargin)に来たときにスクロール量を0とする変数「this.targetScroll」を定義
  • クラス内メソッドgetScrollNumを実行

_normについて

正規化用関数です。
正規化とは、ある範囲の値を0~1の値に変換することです。
例:スクロール量 100px ~ 500px を 100pxの時0,500pxの時1として 0~1の値に変換する

_lerpについて

線形補完用関数です。
ちょっとこれは、僕もとても理解が浅いのですが
正規化した値を、tに入れると任意の範囲(a~b)の中の対応する位置を返す関数と思っています。
例:正規化した値(0~1で変化する)が0.5の時
線形補完関数にa:100,b,300を入れていた場合、戻り値は200になります。(100~300の0.5の位置)

_getResultNumについて

スクロールイベントの中で、スクロール量、targetScrollの値を更新しています。
要素が最初から画面内にいるとき(FV内にある時)とそうでないときで、正規化に使用する値を変えています。
そして、条件式を設けてスクロール量を0~scrollMaxの間に留めるようにしています。
norm関数を用いてthis.targetScrollを正規化し変数normalizeNumに格納
イージング関数に正規化した数値を引数として渡して、イージングが効いて正規化された変数:easeNumを定義
それぞれを用いて、線形補完。
そのままではthisがwindowオブジェクトを指してしまうため、bindする。

活用例とコード

活用したクラスとイージング関数も一緒に載せています。

JavaScript
//----------------------------------------------------------------
//スクロール量、率、正規化した値を取得するクラス
//インスタンス化して、スクロールイベント内でgetResultを実行する。
class GetScrollNum {
  constructor() {
    this.scrollY = "";
    this.normalizeScrollY = "";
    this.scrollRate = "";
    this.viewH = document.documentElement.scrollHeight;
    this.totalScrollY = this.viewH - innerHeight;
  }

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

//使い方
//スクロールイベント内でインスタンス化
//欲しい値を引っ張り出す
//----------------------------------------------------------------

//------------------------------------------------
//イージング関数

function easeInOutQuint(x) {
  return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
}

function easeInQuint(x) {
  return x * x * x * x * x;
}

function 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;
};
//------------------------------------------------

//---------------------------------------------------------------

class ScrollFunction {
  constructor(target, scrollMax, rootMargin, ease, min, max) {
    this.target = target; //変化させたいターゲット
    this.scrollMax = scrollMax; //任意のスクロール量の最大値
    this.rootMargin = rootMargin; //画面のどこにtargetが来たら検知開始するか
    this.ease = ease; //イージング関数を入れる
    this.min = min; //任意の範囲の最小値
    this.max = max; //任意の範囲の最大値
    this.sn = new GetScrollNum();
    this.sn.getResult();//スクロール量取得用関数を実行
    this.scrollY = this.sn.scrollY; //画面上部からのスクロール量
    this.targetPos = this.target.getBoundingClientRect().top + this.scrollY; //targetのページ上部からの位置
    this.targetScroll =
      this.scrollY - this.targetPos + innerHeight + this.rootMargin; ///任意の要素がrootMarginの位置に来たときにスクロール量を0としてscrollMaxまで変化する
    this.resultNormal = 0; //linearでmin~maxの間を変化する変数
    this.resultEase = 0; //イージングを適応してmin~maxまで変化する変数
    this._getResultNum();//スクロール量を取得して値を更新する
  }

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

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

  _getResultNum() {
    //要素が画面内に最初からいる時(FV内にある時)スクロール量をそのまま正規化に使用する
    if (this.targetPos < innerHeight + this.target.clientHeight) {
      window.addEventListener('scroll', function () {
        this.sn.getResult();
        this.scrollY = this.sn.scrollY;
        if (this.scrollY > this.scrollMax) {
          this.scrollY = this.scrollMax;
        }
        const normalizeNum = this._norm(this.scrollY, 0, this.scrollMax); //0~1
        const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
        this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
        this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
      }.bind(this));
    } else {
      window.addEventListener(
        "scroll",
        function () {
          this.sn.getResult();
          this.scrollY = this.sn.scrollY;
          this.targetScroll =
          this.scrollY - this.targetPos + innerHeight + this.rootMargin;
          //要素が画面内に入ってからのスクロール量を0~this.scrollMaxに留める
          if (this.targetScroll !== undefined && this.targetScroll < 0) {
            this.targetScroll = 0;
          } else if (
            this.targetScroll !== undefined &&
            this.targetScroll > this.scrollMax
          ) {
            this.targetScroll = this.scrollMax;
          }
          const normalizeNum = this._norm(this.targetScroll, 0, this.scrollMax); //0~1
          const easeNum = this.ease(normalizeNum);//正規化した数値にイージングを効かせる
          this.resultNormal = this._lerp(this.min, this.max, normalizeNum);//イージングなし
          this.resultEase = this._lerp(this.min, this.max, easeNum);//イージングが効いた値
        }.bind(this)
      );
    }
  }
}

//使い方
//target(第一引数)にはElementを入れる = document.getElementByID('任意の要素');
//イベント外でインスタンス化
//スクロールイベント内で欲しい値を取得(resultEase or resultNormal)
//targetの変更したいスタイルに取得した数値を適応する
//--------------------------------------------------------

//ここから活用例
//イージング関数を変数に格納
const ease = function 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;
};


const complete = new ScrollFunction(
  document.getElementById("target"),
  500,
  -100,
  ease,
  300,
  1000
  );
  
window.addEventListener('scroll', function () {
  const transX = complete.resultEase;
  complete.target.style.transform = `translateX(${transX}px)`;
}, { passive: true });

上記のコードでは、targetが画面端から100px(rootMargin)の位置にきたらアニメーションを開始。
500px(scrollMax)スクロールする間に、300(min)から1000(max)に変化する変数を定義(transX)
それをスクロールイベント内でtargetのスタイルに適応することで、
transform:translateX(300px) ←→ transform:translateX(1000px)
の間で変化させることができます。
以下,codepenで挙動をご確認ください。(PC推奨)

使っているイージング関数はこちらを参照しています。
https://easings.net/ja

codepenに3つだけイージング関数を用意しているので
変更して動きがどうなるか試してみてください。

最後に

自分で作ったクラスながら、いざ言語化して説明するとなるとなかなか難しかったです。
まだまだ不備やパフォーマンスへの懸念点も多々あるので、改善できたらまた共有させて頂きます。

Discussion