🙆‍♀️

Three.jsでスクロール量によってカメラの動きを変更する+慣性スクロール

2022/09/28に公開

概要

スクロール量が全体の何%かを取得して、そのスクロール率がどの範囲かにあるかによって実行する関数を変更します。
スクロール率の範囲内で、動きに関しての線形補完を行います。
各範囲でそれぞれアニメーションが開始・終了するように調整する必要があります。

イメージ図

デモ

実装

スクロール

通常、スクロール値などを扱う場合はscrollイベント時に発火する関数内にあることが多いかと思います。
しかし慣性スクロールを使う際にはrequestAnimationFrameで動かすことが必要になります。

function setScrollPercent() {
  inertialScroll +=
    ((document.documentElement.scrollTop || document.body.scrollTop) 
    - inertialScroll) * 0.08;
  // 慣性スクロールでのパーセント
  inertialScrollPercent = (
    inertialScroll / (
        (document.documentElement.scrollHeight || document.body.scrollHeight) 
        - document.documentElement.clientHeight
    ) * 100).toFixed(2);
  
 // 検証用の通常のパーセント
 const scroll = (
   (document.documentElement.scrollTop || document.body.scrollTop) 
   / ((document.documentElement.scrollHeight
     || document.body.scrollHeight) 
     - document.documentElement.clientHeight)
   ) * 100;
 document.getElementById('percent').innerText = inertialScrollPercent;
 document.getElementById('scroll').innerText = Number(scroll).toFixed(2);
}

慣性スクロール

スクロールに依存するアニメーションで、そのスクロールに慣性をつける方法
luxy.jsを参考にしています。

該当箇所はこちら
https://github.com/min30327/luxy.js/blob/78c640956ef009b6d78987e7a9ce1507792e467e/src/js/luxy.js#L151-L152

通常スクロールの値などを扱う場合はscrollイベントで発火する関数内で動かすことが多いかと思いますが、luxy.jsでは上記の関数をrequestAnimationFrameで動かしています。
現在のスクロール値(this.scrollTop)から今の値(this.wapperOffset)を引き、その差分に割合(this.settings.wrapperSpeed)をかけることで徐々にスクロールするようになっています。
これにより、慣性が働いているかのようなスクロールを実現しています。

これをthree.js上でスクロールの値として使うことにより、スクロールで動くアニメーションに慣性がつくように見えます。

参考

https://github.com/min30327/luxy.js

カメラの動きについて

仕組みとしては簡単です。
下記のような配列を用意しrequestAnimationFrameで動かす関数の中で、今のスクロールの値が配列のstartとendの間であればその要素の中の関数を実行する、というものです。

これによりある期間によってカメラの動きを変更することができます。
lerpの中の値は初期値や配列の前の要素から連続した値ではないと、急に画面が切り替わるようになってしまいます。

今回は変化量、アニメーション切り替えの位置を手打ちで実装していますが、スクロールする箇所の要素の高さの割合を取得して動的に切り替えの位置を変更すればレスポンシブにも対応できると思います

const animationScripts = [
  {
    start: 0,
    end: 25,
    func: () => {
      // leap, scalePercentは線形補完のための関数
      camera.position.z = lerp(
        50, 30, scalePercent(0, 25)
      );
    }
  },
 ]
function playScrollAnimations() {
  animationScripts.forEach((item) => {
    if (inertialScrollPercent >= item.start && inertialScrollPercent < item.end) {
      item.func();
    }
  });
}

function render() {
  renderer.render(scene, camera);
  setScrollPercent();
  window.requestAnimationFrame(render);
  playScrollAnimations();
}

参考

https://sbcode.net/threejs/animate-on-scroll/

その他

線形補完

// 線形補完の関数
function lerp(x, y, a) {
  return x + (y - x) * easeOutQuad(a);
}

// startとendの2つの数字を受け取って、0~1に変更する関数
function scalePercent(start, end) {
  return (inertialScrollPercent - start) / (end - start);
}

// イーズアウト
function easeOutQuad(x) {
  let t = x; const b = 0; const c = 1; const d = 1;
  return -c * (t /= d) * (t - 2) + b;
}

参考

https://sbcode.net/threejs/animate-on-scroll/
https://blog.design-nkt.com/osyare-threejs22/
https://github.com/min30327/luxy.js

Discussion