【Three.js】Tweenを使ってオブジェクトを動かす

JavaScript,Three.js

カッコいい!!

はじめに

ツイーンでもなく、ツウィーンでもなく、トゥウィーンでもなく、トゥイーンだそうです。

Tween.jsというJavaScriptのライブラリがあります。これを使うとプロパティの変化を簡単に定義することができます。

例えば、あるオブジェクトをx軸方向に移動させるとします。普通だったら描画ループ内でx座標を少しずつ変更してオブジェクトをアニメーションさせることになります。

Tweenの場合は、x座標の開始点と終了点だけ決めてあげれば、その中間点の計算はTweenがやってくれます。大量のオブジェクトを扱う場合に、その管理がとても楽になるという優れモノ。これは使わない手はない。というワケで早速使ってみました。

今回はthree.jsの公式サイトを参考にさせてもらいました。

できた

デモサイトはこちら

「解散」を押すとパネルがランダムな位置に移動して、「集合」を押すと画像のようにキレイに整列します。このパネルの移動にTweenを使っています。

解説

/***** パネルのデータ群 *****/
var panelData = [
  "輝",-3,1,0,
  "き",-2,1,0,
  "続",-1,1,0,
  "け",0,1,0,
  "る",1,1,0,
  "ワ",-3,0,0,
  "ン",-2,0,0,
  "ダ",-1,0,0,
  "ー",0,0,0,
  "ウ",1,0,0,
  "ー",2,0,0,
  "マ",3,0,0,
  "ン",4,0,0,
  "キ",-3,-1,0,
  "ヨ",-2,-1,0,
  "子",-1,-1,0,
  "私",-3,1,-1,
  "は",-2,1,-1,
  "人",-3,0,-1,
  "の",-2,0,-1,
  "心",-1,0,-1,
  "に",0,0,-1,
  "寄",-3,-1,-1,
  "り",-2,-1,-1,
  "添",-1,-1,-1,
  "う",0,-1,-1,
  "仕",-3,-2,-1,
  "事",-2,-2,-1,
  "を",-1,-2,-1,
  "す",0,-2,-1,
  "る",1,-2,-1
];

まずはパネルのデータ群です。配列の要素4つ分が1つのパネルの情報になります。「表示する文字」「x座標」「y座標」「z座標」です。この座標に係数を掛けることで、集合時のパネルの位置が決まります。

/***** パネル *****/
var panels = [];
for (var i = 0; i < panelData.length; i+=4) {
  
  // パネルを生成
  var panelElem = document.createElement('div');
  panelElem.className = 'panel';

  // 文字を生成
  var charElem = document.createElement('div');
  charElem.className = 'char';
  charElem.textContent = panelData[i];

  // 文字をパネルの中に入れる
  panelElem.appendChild(charElem);

  // パネルオブジェクトを生成
  var panelObj = new THREE.CSS3DObject(panelElem);

  // 初期位置はランダム
  panelObj.position.x = Math.random() * 2000 - 1000;
  panelObj.position.y = Math.random() * 2000 - 1000;
  panelObj.position.z = Math.random() * 2000 - 1000;

  // シーンに追加
  scene.add(panelObj);

  // 配列に格納
  panels.push(panelObj);
}

次はパネルを作る処理です。"document.createElement"で<div>要素を生成し、それを使ってCSS3DObjectを作ります。このオブジェクトがパネルになります。

あとでループ処理を使って、パネルの1つ1つにあれこれと設定をするので、全てのパネルを配列に格納しておきます。

// 集合ボタン
document.getElementById('gather').addEventListener('click', function() {
  gather();
});

// 解散ボタン
document.getElementById('dismiss').addEventListener('click', function() {
  dismiss();
});

次はボタンにイベントリスナーを設定します。クリックしたらそれぞれの移動処理を呼び出すだけです。

// 集合
function gather() {
  TWEEN.removeAll();
  var n = 0;
  for (var i = 0; i < panels.length; i++) {
    new TWEEN.Tween(panels[i].position)
      .to({x: panelData[n+1] * 180, y: panelData[n+2] * 270, z: panelData[n+3] * 500},1000)
      .easing(TWEEN.Easing.Exponential.InOut)
      .start();
      n += 4;
  }
}

// 解散
function dismiss() {
  TWEEN.removeAll();
  for (var i = 0; i < panels.length; i++) {
    new TWEEN.Tween(panels[i].position)
      .to({x: Math.random() * 2000 - 1000, y: Math.random() * 2000 - 1000, z: Math.random() * 2000 - 1000},1000)
      .easing(TWEEN.Easing.Exponential.InOut)
      .start();
  }
}

そしてこれが移動処理。for文で"panels.length"として、全てのパネルにTweenの設定をしていきます。

コンストラクタに移動前の座標を、"to()"に移動後の座標を渡してあげて"start()"で移動開始です。"easing()"で設定するのは動き方です。今回のは「ゆっくり~速い~ゆっくり」という動きになります。他にも「ずっと一定」とか「最初速くて最後ゆっくり」とか色々な種類があります。

処理の始めに"TWEEN.removeAll()"とありますね。これが無いと「集合」「解除」「集合」みたいにボタンを連打されるとパネルの挙動がおかしくなります。

“removeAll()"を書いておけば、前のTweenの処理を中断して、その位置から新しいTweenを実行するのでそれが解消されます。

// 描画
var clock = new THREE.Clock();
function render() {
  var delta = clock.getDelta();
  trackballControls.update(delta);
  TWEEN.update();
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}

Tweenの設定ができたら、描画関数内で"TWEEN.update()"を呼びます。これでTweenに設定した通りにアニメーションが行われます。なんとも便利なモノです。Tweenの解説は以上になります。参考までに全コードも載せておきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>DOM要素の移動</title>
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <script type="text/javascript" src="../libs/three.min.js"></script>
  <script type="text/javascript" src="../libs/renderers/CSS3DRenderer.js"></script>
  <script type="text/javascript" src="../libs/Tween.js"></script>
  <script type="text/javascript" src="../libs/controls/TrackballControls.js"></script>
  <style>
      body {
          margin: 0;
          overflow: hidden;
      }
      #screen {
        background-color: #000000;
      }
      .panel {
        width: 120px;
        height: 180px;
        background-color: rgba(255, 255, 255, 0.1);
        box-shadow: 0px 0px 20px rgba(255, 255, 255, 1.0);
        border: 1px solid rgba(255, 255, 255, 0.5);
      }
        .panel .char {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          font-size: 60px;
          font-weight: bold;
          color: rgba(255, 255, 255, 0.5);
          text-shadow: 0px 0px 10px rgba(255, 255, 255, 1.0);
        }
      #menu {
        display: flex;
        position: absolute;
        width: 100%;
        bottom: 50px;
        justify-content: center;
      }
      #menu .button {
        padding: 5px 10px;
        cursor: pointer;
        color: rgb(255, 255, 0);
        border: 1px solid rgba(255, 255, 50, 1.0);
        background-color: rgba(255, 255, 50, 0.1);
        margin: 0px 10px;
      }
      #menu .button:active {
        background-color: rgba(255,255, 50,0.5);
      }
  </style>
</head>
<body>

  <!-- 3D空間描画領域 -->
  <div id="screen"></div>
  
  <!-- ボタン -->
  <div id="menu">
    <div id="gather" class="button">集合</div>
    <div id="dismiss" class="button">解散</div>
  </div>
  
  <script type="text/javascript">

    window.onload = function() {
      
/***** パネルのデータ群 *****/
var panelData = [
  "輝",-3,1,0,
  "き",-2,1,0,
  "続",-1,1,0,
  "け",0,1,0,
  "る",1,1,0,
  "ワ",-3,0,0,
  "ン",-2,0,0,
  "ダ",-1,0,0,
  "ー",0,0,0,
  "ウ",1,0,0,
  "ー",2,0,0,
  "マ",3,0,0,
  "ン",4,0,0,
  "キ",-3,-1,0,
  "ヨ",-2,-1,0,
  "子",-1,-1,0,
  "私",-3,1,-1,
  "は",-2,1,-1,
  "人",-3,0,-1,
  "の",-2,0,-1,
  "心",-1,0,-1,
  "に",0,0,-1,
  "寄",-3,-1,-1,
  "り",-2,-1,-1,
  "添",-1,-1,-1,
  "う",0,-1,-1,
  "仕",-3,-2,-1,
  "事",-2,-2,-1,
  "を",-1,-2,-1,
  "す",0,-2,-1,
  "る",1,-2,-1
];

      /***** シーン *****/
      var scene = new THREE.Scene();

      /***** パネル *****/
      var panels = [];
      for (var i = 0; i < panelData.length; i+=4) {
        
        // パネルを生成
        var panelElem = document.createElement('div');
        panelElem.className = 'panel';

        // 文字を生成
        var charElem = document.createElement('div');
        charElem.className = 'char';
        charElem.textContent = panelData[i];

        // 文字をパネルの中に入れる
        panelElem.appendChild(charElem);

        // パネルオブジェクトを生成
        var panelObj = new THREE.CSS3DObject(panelElem);

        // 初期位置はランダム
        panelObj.position.x = Math.random() * 2000 - 1000;
        panelObj.position.y = Math.random() * 2000 - 1000;
        panelObj.position.z = Math.random() * 2000 - 1000;

        // シーンに追加
        scene.add(panelObj);

        // 配列に格納
        panels.push(panelObj);
      }

      /***** カメラ *****/
      // アスペクト比
      var aspRatio = window.innerWidth / window.innerHeight;

      // 視野角
      var fov;
      if (aspRatio > 1) {
        fov = 20;
      } else if (aspRatio > 0.8) {
        fov = 30;
      } else if (aspRatio > 0.6) {
        fov = 40;
      } else if (aspRatio > 0.5) {
        fov = 50;
      } else {
        fov = 60;
      }

      var camera = new THREE.PerspectiveCamera(fov, aspRatio, 1, 10000);
      camera.position.z = 3000;
      
      /***** レンダラー *****/
      var renderer = new THREE.CSS3DRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.getElementById("screen").appendChild(renderer.domElement);

      /***** カメラのコントロール *****/
      var trackballControls = new THREE.TrackballControls(camera);
      trackballControls.rotateSpeed = 3.0;
      trackballControls.panSpeed = 0.2;

      /***** 関数 *****/
      // 描画
      var clock = new THREE.Clock();
      function render() {
        var delta = clock.getDelta();
        trackballControls.update(delta);
        TWEEN.update();
        requestAnimationFrame(render);
        renderer.render(scene, camera);
      }

      // 集合
      function gather() {
        TWEEN.removeAll();
        var n = 0;
        for (var i = 0; i < panels.length; i++) {
          new TWEEN.Tween(panels[i].position)
            .to({x: panelData[n+1] * 180, y: panelData[n+2] * 270, z: panelData[n+3] * 500},1000)
            .easing(TWEEN.Easing.Exponential.InOut)
            .start();
            n += 4;
        }
      }

      // 解散
      function dismiss() {
        TWEEN.removeAll();
        for (var i = 0; i < panels.length; i++) {
          new TWEEN.Tween(panels[i].position)
            .to({x: Math.random() * 2000 - 1000, y: Math.random() * 2000 - 1000, z: Math.random() * 2000 - 1000},1000)
            .easing(TWEEN.Easing.Exponential.InOut)
            .start();
        }
      }

      /***** イベントリスナー *****/
      // ウィンドウサイズ変更
      window.addEventListener("resize", function() {
        camera.aspect = window.Width / window.Height;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        render();
      });

      // 集合ボタン
      document.getElementById('gather').addEventListener('click', function() {
        gather();
      });

      // 解散ボタン
      document.getElementById('dismiss').addEventListener('click', function() {
        dismiss();
      });

      // 描画
      render();
    }
  </script>
</body>
</html>

あとがき

余談になりますが、このパネルは3D空間にあるとは言っても、普通のwebサイトで使っているのと同じHTML要素です。なのでCSSが効きます。

.panel {
  width: 120px;
  height: 180px;
  background-color: rgba(255, 255, 255, 0.1);
  box-shadow: 0px 0px 20px rgba(255, 255, 255, 1.0);
  border: 1px solid rgba(255, 255, 255, 0.5);
}
  .panel .char {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 60px;
    font-weight: bold;
    color: rgba(255, 255, 255, 0.5);
    text-shadow: 0px 0px 10px rgba(255, 255, 255, 1.0);
  }

こんな感じでパネルとその中の文字にCSSで装飾をする事ができます。という事はあんな事やこんな事もできてしまうのです。ワクワクしますよねぇ。これは。

今回は以上です。ありがとうございました。

おしゃれ度

★★★★☆

Posted by ナカタ