🫡

jQueryを使わないスムーススクロールの実装の手順を順序立てて解説します

2023/05/18に公開

はじめに

こんにちは、ポートのフロントエンドを担当している @fujita です。この記事では jQuery を使用しないで書くスムーススクロールの実装について、なるべく順序立てて解説していきます。

DOMContentLoaded イベントの設定

まず最初のステップとして HTML が完全にロードされたあとに JavaScript コードが実行されることを確認します。

document.addEventListener('DOMContentLoaded', () => {
  // ここに続きのコードを書いていきます
});

addEventListener 関数は、指定したイベントが発生した時に実行する関数を登録します。今回はドキュメントの読み込みが完了した時にコードを実行するのでDOMContentLoadedを指定します。

ボタンとスクロール先のエレメントの取得

次に、スクロールするトリガーとなるボタンと、スクロール先のエレメントを取得します。これは document.querySelectordocument.getElementById を使用して行います。

document.addEventListener('DOMContentLoaded', () => {
  const actionButton = document.querySelector('.js-action-btn');
  const targetSection = document.getElementById('js-target-section');
});

querySelectorは、指定した CSS セレクタに一致する最初のエレメントを返します。一方、getElementByIdは、指定した ID を持つエレメントを返します。

イージング関数の定義

スムーススクロールのためのイージング関数を定義します。この関数は、アニメーションの開始と終了をゆっくりとし、中間を速くすることで、自然な動きを作り出します。

document.addEventListener('DOMContentLoaded', () => {
  const actionButton = document.querySelector('.js-action-btn');
  const targetSection = document.getElementById('js-target-section');
  
  const easingFunction = (t) => (
    t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
  );
});

イージング関数について

イージング関数はアニメーションの中間状態を計算するために使われます。具体的には、アニメーションの進行度(0 から 1 までの値)を入力として受け取り、その進行度に基づいた中間状態(同じく 0 から 1 までの値)を出力します。

上記のコードは、"easeInOutCubic"と呼ばれる一般的なイージング関数の一つで、イージング関数についてはMDNこちらのチートシートがとても参考になります。

具体的には、進行度 t が 0.5 未満のとき、つまりアニメーションの前半部分では、4 * t * t * t という計算が行われます。これは t の 3 乗を 4 倍したもので、t が 0 から 0.5 に変化するとき、この関数の値は 0 から 0.5 に変化します。この計算により、アニメーションの進行は初めはゆっくりと始まり、徐々に速度を上げていきます。

進行度 t が 0.5 以上のとき、つまりアニメーションの後半部分では、(t - 1) * (2 * t - 2) * (2 * t - 2) + 1 という計算が行われます。これは t の値を 2 倍してから 2 を引き、それを 3 乗してから 1 を足したもので、t が 0.5 から 1 に変化するとき、この関数の値は 0.5 から 1 に変化します。この計算により、アニメーションの進行は最初は速く進み、徐々に速度を落としてゆっくりと終わります。

スクロールアニメーション関数の作成

次に、スクロールアニメーションを制御する関数を作成します。

document.addEventListener('DOMContentLoaded', () => {
  const actionButton = document.querySelector('.js-action-btn');
  const targetSection = document.getElementById('js-target-section');

  const easingFunction = (t) => (
    t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
  );

  const animateScroll = (target, duration = 1000) => {
    const initialPosition = window.pageYOffset;
    const targetPosition = target.getBoundingClientRect().top + initialPosition;
    const animationStart = performance.now();
  };
});

ここでは、window.pageYOffsetgetBoundingClientRectメソッドを使ってスクロールの初期位置と目標位置を計算し、performance.nowを使ってアニメーションの開始時間を取得しています。

https://developer.mozilla.org/ja/docs/Web/API/Element/getBoundingClientRect

アニメーションを実行する関数の作成

アニメーションを実行するための関数を作成します。この関数では、requestAnimationFrameを使って、次の描画が行われる前に関数を再度呼び出すようブラウザに要求します。

document.addEventListener('DOMContentLoaded', () => {
  const actionButton = document.querySelector('.js-action-btn');
  const targetSection = document.getElementById('js-target-section');

  const easingFunction = (t) => (
    t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
  );

  const animateScroll = (target, duration = 1000) => {
    const initialPosition = window.pageYOffset;
    const targetPosition = target.getBoundingClientRect().top + initialPosition;
    const animationStart = performance.now();

    const performAnimation = (currentTime) => {
      const elapsedTime = currentTime - animationStart;
      const progress = elapsedTime / duration;

      if (progress < 1) {
        const easedProgress = easingFunction(progress);
        const currentPosition = initialPosition +
          ((targetPosition - initialPosition) * easedProgress);
        window.scrollTo(0, currentPosition);
        requestAnimationFrame(performAnimation);
      } else {
        window.scrollTo(0, targetPosition);
      }
    };

    requestAnimationFrame(performAnimation);
  };
});

ここでは、経過時間を計算し、それをアニメーションの全体の持続時間で割ることで進行度を計算します。その進行度をイージング関数に渡し、その結果を使って現在の位置を計算します。そして、その位置にスクロールします。

https://developer.mozilla.org/ja/docs/Web/API/window/requestAnimationFrame

ボタンクリックイベントの設定

最後に、ボタンがクリックされたときにスクロールアニメーションを実行するイベントリスナーを設定します。

document.addEventListener('DOMContentLoaded', () => {
  const actionButton = document.querySelector('.js-action-btn');
  const targetSection = document.getElementById('js-target-section');

  const easingFunction = (t) => (
    t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
  );

  const animateScroll = (target, duration = 1000) => {
    const initialPosition = window.pageYOffset;
    const targetPosition = target.getBoundingClientRect().top + initialPosition;
    const animationStart = performance.now();

    const performAnimation = (currentTime) => {
      const elapsedTime = currentTime - animationStart;
      const progress = elapsedTime / duration;

      if (progress < 1) {
        const easedProgress = easingFunction(progress);
        const currentPosition = initialPosition +
          ((targetPosition - initialPosition) * easedProgress);
        window.scrollTo(0, currentPosition);
        requestAnimationFrame(performAnimation);
      } else {
        window.scrollTo(0, targetPosition);
      }
    };

    requestAnimationFrame(performAnimation);
  };

  if (actionButton) {
    actionButton.addEventListener('click', (event) => {
      event.preventDefault();
      animateScroll(targetSection);
    });
  }
});

ここでは、addEventListenerを使ってクリックイベントをactionButtonに登録します。クリックイベントが発生すると、event.preventDefault()を呼び出してデフォルトのクリックアクションをキャンセルし、代わりにanimateScroll関数を呼び出してスムーズスクロールアニメーションを開始します。

さいごに

いかがでしたか? 以上がスムーススクロール機能を実装するまでの手順になります。この記事がなにかしらの参考になれば幸いです。

こちらの実装に関しては就活会議というサービスでリリース済みなのでぜひ確認してみてください!

就活会議のページスクショ

Discussion