🐧

JS汎用クラスとかを作って貯めていきたい

2022/11/19に公開約11,900字

記事の意図

汎用的に使えそうなクラスを作って、貯めていこうと思います。
自分のメモ用なので、もし活用される方がいらっしゃれば
その点御理解ください。

クラス

IntersectionObserverで画面内交差検知

JavaScript
class ScrollObserver {
  constructor(els, cb,rootMargin,options) {
    this.els = els;//NodeListを渡す
    const defaultOptions = {
      root: null, //交差対象
      rootMargin: rootMargin, //交差判定境界線
      threshold: 0,//targetのどこで交差判定するか
      once:true
    };
    this.cb = cb;
    this.options = Object.assign(defaultOptions, options); //オブジェクトを合体させる
    this.once = this.options.once;
    this._init();
  }
  
  //初期化
  _init() {
    const callback = function (entries, observer) {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          //画面内に入った時
          this.cb(entry.target, true);
          if (this.once) {
            observer.unobserve(entry.target); //監視を終了する
          }
        } else {
          //画面外に出た時
          this.cb(entry.target, false);
        }
      });
    };
    
    this.io = new IntersectionObserver(callback.bind(this), this.options);
    this.els.forEach(el => this.io.observe(el));
  }

  destroy() {
    this.io.disconnect();//IOの監視を終了する
  }
}

//使い方
//1.コールバック関数を定義する
// const cb = function (el, isIntersecting) {
//   if (isIntersecting) {
//     ここに画面内に入ったら行いたい処理をかく
//   }
// }
//※上記のelにはセレクタではなくElement(entry.target)が渡ることに注意する
//2.インスタンス化する(第一引数にNodeListを渡す)
// const so = new ScrollObserver(document.querySelectorAll('.監視したい要素'), cb, rootMargin,options:あってもなくても良い,{once:false});
//once:falseだと何度も監視をする。デフォルトはtrueで画面内に入った時に一度だけ処理を実行する

テキスト分割アニメーション

JavaScript
class SplitTextAnimation {
  constructor(el) {
    this.el = el;//Elementを渡す
    this.chars = this.el.innerText.trim();
    this.concatStr = "";
    this.el.innerHTML = this._splitText(); //クラスに渡された引数が分割された状態のDOM
    this.animations = [];
    this.chars = '';
    this.transY = "170px";//transformY
    this.outer = document.createElement('div');//対象の親にdivを追加
    this._init();
  }

  //テキストがcharクラスをもつspanで1文字ずつ分割される関数
  _splitText() {
    for (let c of this.chars) {
      c = c.replace(" ", " ");
      this.concatStr += `<span class="char">${c}</span>`;
    }
    return this.concatStr;
  }
 
  //分割したテキストにデフォルトスタイルを付与
  _init() {
    this.chars = this.el.querySelectorAll(".char"); //指定した要素(el)のcharクラスを取得する
    this.chars.forEach(char => {
      char.style.display = 'inline-block';
      // char.style.opacity = 0;
      char.style.transform = `translateY(${this.transY})`;
    })
  }

  //アニメーションの対象をdivで囲みclip-path
  _clip() {
    this.outer.classList.add('js-outer');//囲むdivにクラスを付与
    this.el.parentNode.insertBefore(this.outer, this.el);//対象の親要素の子要素にdivを挿入
    this.outer.appendChild(this.el);//生成したdivの中に対象を入れる
    this.outer.style.clipPath = "polygon(0 0,100% 0,100% 100%,0 100%)";//divより外側をclip-pathで切り取る
  }

  //1文字ずつfadeUpする
  fadeUpText() {
    this._clip();//isIntersectingになったら実行される
    //タイミング制御用オブジェクトを定義
    let timings = {
      easing: "ease-in-out",
      fill: "forwards",
    };
    let x,easing;

    this.chars = this.el.querySelectorAll(".char"); //指定した要素(el)のcharクラスを取得する
    this.chars.forEach((char, i) => {
      x = i / (this.chars.length - 1);//0 ~ 1
      const maxDelay = 170;//delay最大値
      //イージング関数
      function ease(x){
        return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
        }
      easing = ease(x);
      timings.delay = easing * maxDelay;
      timings.duration = 550;
      const animation1 = char.animate(
        [{ transform: `translateY(${this.transY}) rotateZ(20deg)` }, { transform: "translateY(0px) rotateZ(0)" }],
        timings
      );
      animation1.cancel();
      this.animations.push(animation1);
  
      const animation2 = char.animate([{ opacity: 0 }, { opacity: 1 }], timings);
      animation2.cancel();
      // this.animations.push(animation2);
    });
    this.animations.forEach((anim) => {
      anim.play();
    });
  }
}

//使い方
//1.インスタンス化
// const ta = new SplitTextAnimation(ここにアニメーションさせたい要素のセレクタを渡す)
//2.アニメーション関数の実行
// ta.fadeUpText()

スクロール量、率、正規化値の取得

JavaScript
//スクロール量、率、正規化した値を取得するクラス
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); //スクロール率
  }
}

//使い方
//インスタンス化して、スクロールイベント内でgetResultを実行する。
//欲しい値を引っ張り出す

任意のスクロール量で、任意の範囲変化させられる数値を得る

JavaScript
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.scrollY = window.pageYOffset; //画面上部からのスクロール量
    this.targetPos = this.target.getBoundingClientRect().top + this.scrollY; //targetのページ上部からの位置
    this.targetScroll = ""; ///任意の要素がrootMarginの位置に来たときにスクロール量を0としてscrollMaxまで変化する
    this.winH = innerHeight;
    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 < this.winH + this.target.clientHeight) {
      window.addEventListener(
        "scroll",
        function () {
          this.scrollY = window.pageYOffset;
          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
          ).toFixed(2); //イージングなし
          this.resultEase = this._lerp(this.min, this.max, easeNum).toFixed(2); //イージングが効いた値
        }.bind(this)
      );
    } else {
      window.addEventListener(
        "scroll",
        function () {
          this.scrollY = window.pageYOffset;
          this.targetScroll =
            this.scrollY - this.targetPos + this.winH + 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
          ).toFixed(2); //イージングなし
          this.resultEase = this._lerp(this.min, this.max, easeNum).toFixed(2); //イージングが効いた値
        }.bind(this)
      );
    }
  }
}

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

テキスト分割

JavaScript
//classNameに任意のクラス名を渡すことで、テキスト分割時のspanのクラスを指定できる
class SplitText {
  constructor(els,className) {
    this.els = els;//NodeListを渡す
    this.className = className
    this.els.forEach((el) => {
      this.chars = el.innerText.trim();
      this.concatStr = "";
      el.innerHTML = this._splitText();
    })
  }
  //テキストがcharクラスをもつspanで1文字ずつ分割される関数
  _splitText() {
    for (let c of this.chars) {
      c = c.replace(" ", "&nbsp;");
      this.concatStr += `<span class="${this.className}">${c}</span>`;
    }
    return this.concatStr;
  }
}

活用例

ScrollObserver + ScrollFunction

JavaScript
//活用例
//ID:target1,target2をつけた要素を用意
//target1は500pxスクロールする間にtransformX 300px => 1000px
//target2は500pxスクロールする間にtransformX 100px => 500px,scale(1) => 2
//要素が画面内にある時のみ処理を実行
//fps制御,スクロールイベントではなくrAFで制御,rAFはスクロールしている時のみループ

//requestAnimationFrameをリセットするためのIDを格納
let frameId,frameId2;
//直前のスクロール位置を格納
let lastPosition = -1;
//フレームレート設定
const framesPerSecond = 60;
const interval = Math.floor(1000 / framesPerSecond);
const startTime = performance.now();
let previousTime = startTime;
let currentTime = 0;
let elapsed = 0;

//ScrollFunctionの引数定義
const target1 = document.getElementById('target1');
const target2 = document.getElementById('target2');
const scrollMax = 500;
const rootMargin = -200;//画面下から100pxの位置に要素が来たらアニメーション開始
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 cb = function (el, isIntersecting) {
  const sf = new ScrollFunction(target1, 500,rootMargin, ease, 300, 1000);
  if (isIntersecting) {
    //要素が画面内の時の処理
    const raf = (timestamp) => {
      //スクロール位置に変化があった時のみループを実行
      if (lastPosition === sf.scrollY) {
        frameId = requestAnimationFrame(raf);
        return;
      }
      lastPosition = sf.scrollY;

      //fps制御
      currentTime = timestamp;
      if (currentTime) {
        elapsed = currentTime - previousTime;
      }
      if (elapsed <= interval) {
        frameId = requestAnimationFrame(raf);
        return;
      }
      previousTime = currentTime - (elapsed % interval);

      //ここからアニメーションさせたいコード書く
      target1.setAttribute(
        "style",
        `transform:translateX(${sf.resultEase}px)`
      );
      frameId = requestAnimationFrame(raf);
    };
    raf();
  } else {
    elapsed = 0;
    cancelAnimationFrame(frameId);
  }
};

const cb2 = function (el, isIntersecting) {
  const sf2 = new ScrollFunction(target2, 300, rootMargin, ease, 100, 500);
  const sf3 = new ScrollFunction(target2, 300, rootMargin, ease, 1, 3);
  if (isIntersecting) {
    //要素が画面内の時の処理
    const raf = (timestamp) => {
      //スクロール位置に変化があった時のみループを実行
      if (lastPosition === sf2.scrollY) {
        frameId2 = requestAnimationFrame(raf);
        return;
      }
      lastPosition = sf2.scrollY;

      //fps制御
      currentTime = timestamp;
      if (currentTime) {
        elapsed = currentTime - previousTime;
      }
      if (elapsed <= interval) {
        frameId2 = requestAnimationFrame(raf);
        return;
      }
      previousTime = currentTime - (elapsed % interval);

      //ここからアニメーションさせたいコード書く
      target2.setAttribute(
        "style",
        `transform:translateX(${sf2.resultNormal}px) scale(${sf3.resultNormal})`
      );
      frameId2 = requestAnimationFrame(raf);
    };
    raf();
  } else {
    elapsed = 0;
    cancelAnimationFrame(frameId2);
  }
};

const so = new ScrollObserver(
  document.querySelectorAll(".target"),
  cb,
  `0px 0px ${rootMargin}px 0px`,
  { once: false }
);
const so2 = new ScrollObserver(
  document.querySelectorAll(".target2"),
  cb2,
  `0px 0px ${rootMargin}px 0px`,
  { once: false }
);

課題

  • ScrollObserberのcbで画面内にある時はrAFを回して、画面外でrAFを止める処理をしているが
    複数の要素を同時に監視した時、一つの要素が最初から画面内にあってその他が画面外にあるという条件下では
    isIntersectingがtrue->falseとなるため、最初から画面内の要素のcbが実行されずにcancelされてしまう。
  • 上記理由より、動かしたい要素が複数ある場合は、別々にScrollObserverで監視する必要があり
    その度に、cbとcancel用のid格納変数を用意する必要がある。

Discussion

ログインするとコメントできます