🐧

Three.jsと三角関数の練習でスクロール連動アニメーションを作ってみた

2022/10/02に公開

完成形

コード全体像

HTML
<canvas id="canvas"></canvas>
    <div class="box2">
      <p>Hello everyone!</p>
    </div>
    <div class="box">
      <p>This is My portfolio site.</p>
    </div>
    <div class="box">
      <p>My skill is...</p>
    </div>
    <div class="box3">
      <p>HTML</p>
    </div>
    <div class="box3">
      <p>CSS</p>
    </div>
    <div class="box3">
      <p>JavaScript</p>
    </div>
    <div class="box3">
      <p>and...??</p>
      <p>I am in to studying ThreeJS.</p>
    </div>
    <div class="box">
    </div>
    <div class="box2">
      <p>See you!</p>
    </div>
CSS
*{
  margin:0;
  padding:0;
}

p{
  font-size:50px;
  color:white;
  text-align:center;
}

#canvas{
  position:fixed;
  top:0;
  left:0;
  z-index: -1;
}

.box{
  height:100vh;
  color:white;
  display:flex;
  align-items:center;
  justify-content:center;
}

.box2{
  height:200vh;
  color:white;
  display:flex;
  align-items:flex-end;
  justify-content:center;
}

.box2 p{
  padding-bottom: 50vh;
}

.box3{
  height:200vh;
  color:white;
  text-align:center;
}

.box3 p:nth-of-type(1){
  position:sticky;
  margin-bottom: 50px;
  top:50vh;
}

.box3 p:nth-of-type(2){
  position:sticky;
  top:55vh;
}
JavaScript
/*
//canvasを定義
*/
const canvas = document.getElementById("canvas");
let time = Date.now() / 2000;//msを変数に格納

/*
サイズを定義
*/
const sizes = {
  width: innerWidth,
  height: innerHeight,
};

/*
//シーンを定義
*/
const scene = new THREE.Scene();

//背景用のテクスチャ
const textureLoader = new THREE.TextureLoader();
const bgTexture = textureLoader.load("bg/bg.jpg");
scene.background = bgTexture;

/*
//カメラ
*/
//デフォルトではカメラは、原点に位置しており、z軸マイナス方向に向いている
const fov = 75; //視野角(degree)
const aspect = sizes.width / sizes.height; //アスペクト比
const near = 0.1; //撮影開始距離
const far = 3000; //撮影終了距離
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); //カメラを定義
scene.add(camera); //カメラをシーンに追加

/*
レンダラーを定義
*/
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
  antialias: true, //アンチエイリアスを適応
});
renderer.setSize(sizes.width, sizes.height); //サイズを指定
renderer.setPixelRatio(window.devicePixelRatio); //アスペクト比を指定

//ジオメトリを定義
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const octahedronGeometry = new THREE.OctahedronGeometry(1, 0, 1);
const sphereGeometry = new THREE.SphereGeometry(1, 16, 16);
const torusGeometry = new THREE.TorusGeometry(1, 0.3, 8, 100);


//マテリアルを定義
const material = new THREE.MeshNormalMaterial({
  side:THREE.DoubleSide,
});

//メッシュ化
const mesh1 = new THREE.Mesh(boxGeometry, material);
const mesh2 = new THREE.Mesh(octahedronGeometry, material);
const mesh3 = new THREE.Mesh(sphereGeometry, material);
const mesh4 = new THREE.Mesh(torusGeometry, material);
scene.add(mesh1, mesh2, mesh3, mesh4);

const meshes = [mesh1, mesh2, mesh3, mesh4];


/*
ブラウザのリサイズに対応
*/
window.addEventListener("resize", () => {
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(window.devicePixelRatio);
});

//線形補完用関数を定義
function lerp(x, y, a) {
  return (1 - a) * x + a * y;
}

//ブラウザのスクロール率を取得
let scrollPercent = 0;
document.body.onscroll = () => {
  scrollPercent =
    (document.documentElement.scrollTop /
      (document.documentElement.scrollHeight -
        document.documentElement.clientHeight)) *
    100;
};

//各区間におけるスクロール率を取得
function scalePercent(start, end) {
  return (scrollPercent - start) / (end - start);
}

//アニメーション配列を用意
const animationScripts = [];

//======================================================
//以下アニメーション配列

//カメラを奥(0,0,-10)から手前(0,0,17)に移動させてオブジェクトが奥に進んでいっているように見せる
animationScripts.push({
  start: 0,
  end: 10,
  function() {
    camera.position.z = lerp(-10, 17, scalePercent(0, 10));
    camera.position.x = 0;
  },
});

//カメラを左(-13,0,17)に移動させて、オブジェクトが右に移動しているように見せる
animationScripts.push({
  start: 10,
  end: 20,
  function() {
    camera.position.x = lerp(0, -13, scalePercent(10, 20));
  },
});

//mesh1の移動=========================================
//x座標を円周上(Math.sin(time) * 4)から[-13]へ移動
//y座標を(Math.cos(time) * 4)から[0]へ移動
animationScripts.push({
  start: 20,
  end: 25,
  function() {
    mesh1.position.x = lerp(Math.sin(time) * 4,-13, scalePercent(20, 25));
    mesh1.position.y = lerp(Math.cos(time) * 4,0, scalePercent(20, 25));
  },
});

//z座標を奥[0]から手前[14]に持ってきて、近づける
animationScripts.push({
  start: 25,
  end: 30,
  function() {
    mesh1.position.x = -13;
    mesh1.position.y = 0;
    mesh1.position.z = lerp(0,14, scalePercent(25, 30));
  },
});

//その場で維持
animationScripts.push({
  start: 30,
  end: 35,
  function() {
    mesh1.position.x = -13;
    mesh1.position.y = 0;
    mesh1.position.z = 14;
  },
});

//円周上に戻す
animationScripts.push({
  start: 35,
  end: 40,
  function() {
    mesh1.position.x = lerp(-13, Math.sin(time) * 4, scalePercent(35,40));
    mesh1.position.y = lerp(0,Math.cos(time) * 4, scalePercent(35,40));
    mesh1.position.z = lerp(14,0, scalePercent(35,40));
  },
});

//mesh2の移動(以降meshの移動は同様)
animationScripts.push({
  start: 40,
  end: 45,
  function() {
    mesh2.position.x = lerp(Math.sin(time + Math.PI / 2) * 4,-13, scalePercent(40,45));
    mesh2.position.y = lerp(Math.cos(time + Math.PI / 2) * 4,0, scalePercent(40,45));
  },
});
animationScripts.push({
  start: 45,
  end: 50,
  function() {
    mesh2.position.x = -13;
    mesh2.position.y = 0;
    mesh2.position.z = lerp(0,14, scalePercent(45, 50));
  },
});
animationScripts.push({
  start: 50,
  end: 55,
  function() {
    mesh2.position.x = -13;
    mesh2.position.y = 0;
    mesh2.position.z = 14;
  },
});
animationScripts.push({
  start: 55,
  end: 60,
  function() {
    mesh2.position.x = lerp(-13, Math.sin(time + Math.PI / 2) * 4, scalePercent(55,60));
    mesh2.position.y = lerp(0,Math.cos(time + Math.PI / 2) * 4, scalePercent(55,60));
    mesh2.position.z = lerp(14,0, scalePercent(55,60));
  },
});

//mesh3の移動
animationScripts.push({
  start: 60,
  end: 65,
  function() {
    mesh3.position.x = lerp(Math.sin(time + Math.PI) * 4,-13, scalePercent(60,65));
    mesh3.position.y = lerp(Math.cos(time + Math.PI) * 4,0, scalePercent(60,65));
  },
});
animationScripts.push({
  start: 65,
  end: 70,
  function() {
    mesh3.position.x = -13;
    mesh3.position.y = 0;
    mesh3.position.z = lerp(0,14, scalePercent(65,70));
  },
});
animationScripts.push({
  start: 70,
  end: 75,
  function() {
    mesh3.position.x = -13;
    mesh3.position.y = 0;
    mesh3.position.z = 14;
  },
});

animationScripts.push({
  start: 75,
  end: 80,
  function() {
    mesh3.position.x = lerp(-13, Math.sin(time + Math.PI) * 4, scalePercent(75,80));
    mesh3.position.y = lerp(0,Math.cos(time + Math.PI) * 4, scalePercent(75,80));
    mesh3.position.z = lerp(14,0, scalePercent(75,80));
  },
});

//カメラのx座標を左(-13)から右(0)へ移動させて、オブジェクトが左に移動しているように見せる
animationScripts.push({
  start: 80,
  end: 90,
  function() {
    camera.position.x = lerp(-13, 0, scalePercent(80, 90));
  }
})

//カメラを下(y座標:0 → -10)奥(z座標:17 → 3)に移動させて、オブジェクトを見上げる形にする
//オブジェクトが近づいてきて奥に倒れていくように見せる
animationScripts.push({
  start: 90,
  end: 95,
  function() {
    camera.position.y = lerp(0,-10, scalePercent(90, 95));
    camera.position.z = lerp(17, 3, scalePercent(90, 95));
    camera.lookAt(0, 0, 0);
  }
})

//オグジェクト全体の回転の半径を徐々に大きくする
animationScripts.push({
  start: 95,
  end: 100,
  function() {
    r += 0.01;
  }
})

//アニメーション配列ここまで
//======================================================


//アニメーション配列を実行する用関数
function playScrollAnimation() {
  time = Date.now() / 2000;
  animationScripts.forEach((animation) => {
    if (scrollPercent >= animation.start && scrollPercent <= animation.end) {
      animation.function();
    }
  });
}

//Ïアニメーション配列の実行
const tick = () => {
  window.requestAnimationFrame(tick);
  playScrollAnimation();
  renderer.render(scene, camera);
};

let r = 4;//ジオメトリ全体の回転の半径
//ジオメトリ全体を回転させる
function rot() {
  time = Date.now() / 2000;
  mesh1.position.set(Math.sin(time) * r, Math.cos(time) * r, 0);
  mesh2.position.set(
    Math.sin(time + Math.PI / 2) * r,
    Math.cos(time + Math.PI / 2) * r,
    0
    );
    mesh3.position.set(
      Math.sin(time + Math.PI) * r,
      Math.cos(time + Math.PI) * r,
      0
      );
      mesh4.position.set(
        Math.sin(time + (Math.PI / 2) * 3) * r,
        Math.cos(time + (Math.PI / 2) * 3) * r,
        0
        );
        requestAnimationFrame(rot);
}

//ジオメトリ個々の回転用アニメーション
function animate() {
  for (const mesh of meshes) {
    mesh.rotation.x = time;
    mesh.rotation.y = time;
  }
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

animate();
rot();
tick();

やっていること

  • アニメーション開始前の設定
  • スクロール量の取得
  • アニメーション配列を作成し、スクロール量に応じてアニメーションを実行する

アニメーション開始前の設定

  • 回転するオブジェクトを4つ作成
  • 作成したオブジェクトを中心(0,0,0)半径4の円周上に並べる
  • 並べたオブジェクト全体を、三角関数を用いてx座標y座標を変化させ回転移動させる
  • カメラをオブジェクトより奥(0,0,-10)に移動させて、オブジェクトが映らないようにする

スクロール量の取得

こちらに関しては、とてもわかりやすい記事を教えて頂いたので
そちらを共有いたします。

https://zenn.dev/sinoguro/articles/fa25b6ae171778

アニメーション配列の作成

配列の中身については、コード内にコメントで説明を書いているので
ご参照ください。

終わりに

今回は自分の理解の整理と、コードを晒してあわよくばアドバイスをもらおう精神での記事でしたので
ちょっと手抜きで申し訳ないです。
ご不明点や、おかしなところがありましたら、教えて頂けますとありがたいです。

Discussion