Three.jsで3D ラーメンタイマー作ってみた。

どうもこんにちは、ウマシバ([@UMASHIBA](https://twitter.com/UMASHIBA))と言います。

今回[こちら](https://qiita.com/aocattleya/items/3faa42b5d27a0f608785)の記事に触発されてThree.jsでラーメンタイマーを作成してみました。

その中でいくつかの機能をどのように実装したかを紹介してみようと思います。

# 作ったモノ🍜

3Dラーメンタイマー


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Three.js使ってラーメンタイマー🍜作りました！<br>PWAに対応させたのでオフラインで使うことができます。<br>良かったら使ってみてください！ <a href="https://t.co/GHrhWlFie8">pic.twitter.com/GHrhWlFie8</a></p>&mdash; UMASHIBA (@UMASHIBA) <a href="https://twitter.com/UMASHIBA/status/1231838717263798272?ref_src=twsrc%5Etfw">February 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



アプリURL
https://3d-ramen-timer.umashiba.dev/
GitHubコード
https://github.com/UMASHIBA1/3DRamenTimer

# 使用した技術
- Three.js
- Typescript
- webpack
- PWA  

# 赤いリングの作成方法
この赤い円をどうやって作成したか説明します。
<img width="300" alt="赤い円" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307076/3debbab5-c23e-7fd6-c9bd-30cbe7c98c89.png">

## 考え方
Three.jsには[**TorusGeometry**](https://threejs.org/docs/#api/en/geometries/TorusGeometry)という下の写真のようなチューブ状の円を表現するジオメトリがありました。
<img width="300" alt="チューブの円" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307076/fdf4923f-a197-4a19-95ef-8e97e9a7d6a7.png">

そのTorusGeometryは下の写真のようにチューブ状であった円を弧にすることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307076/0dc04161-11d9-4568-c452-b098ba6824fb.png)

その弧をいくつか組み合わせて下の写真のような小さな一重の円を作成し、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307076/27c41c6d-4fd9-9e16-5eb2-e426f38610e5.png)

その円を重ねて、下の写真のような大きい円を作成します。
<img width="300" alt="赤い円" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307076/3debbab5-c23e-7fd6-c9bd-30cbe7c98c89.png">

## コード
コードは以下のような感じです。

### 弧のコード

ここでもともとチューブの円形であったTorusGeometryを弧の形にしています。
またここで色なども付けています。

```typescript:弧のコード

import * as THREE from "three";

class RingFragment extends THREE.Mesh {
  constructor(
    fragmentIndex: number,
    radius: number,
    tube: number = 3,
    arc: number = 0.63
  ) {
    const thisAngleDiff = (2 / 9) * Math.PI * fragmentIndex;
    const circleDiameter = 10;
    const geometry = new THREE.TorusGeometry(radius, tube, 2, 100, arc);
    const material = new THREE.MeshToonMaterial({ color: "#FF0000" });
    super(geometry, material);
    this.position.x = Math.sin(thisAngleDiff) * circleDiameter;
    this.position.y = Math.cos(thisAngleDiff) * circleDiameter;
    this.rotation.y = 0;
    this.rotation.x = 0;
    // 1.23は初期でのangleの傾きを修正するための値
    this.rotation.z = -thisAngleDiff + 1.23;
  }
}

export default RingFragment;
```

### 一重の円のコード

1. 弧を作っているRingFragmentというクラスのインスタンスをいくつか作りThree.jsのGroup機能でまとめます。
2. Groupのrotation.zをフレームごとに少し進めて円を回転させます(tickというメソッドが毎フレームごとに呼ばれるようになってます。)

``` typescript:一重の円のコード
import * as THREE from "three";
import RingFragment from "../component/RingFragment";

type RotateDirectionType = "right" | "left";

class OneRing {
  private _group: THREE.Group;
  private _rotateDirection: RotateDirectionType;
  constructor(
    rotateDirection: RotateDirectionType,
    radius: number,
    tube?: number,
    arc?: number
  ) {
    this._group = new THREE.Group();
    this._rotateDirection = rotateDirection;
    for (let i = 0; i < 9; i++) {
      const ringFrag = new RingFragment(i, radius, tube, arc);
      this._group.add(ringFrag);
    }
    this._group.rotation.z = Math.random();
  }

  public get ring() {
    return this._group;
  }

  public tick() {
    const rotateSpeed = 0.005;
    if (this._rotateDirection === "left") {
      this._group.rotation.z += rotateSpeed;
    } else {
      this._group.rotation.z -= rotateSpeed;
    }
  }
}

export default OneRing;

```

### 複数重ねの円

1. 複数の大きさの一重の円のインスタンスを作成して、これまたThree.jsのGroupでまとめます。
2. 1重の円のtickメソッドをこちらのtickメソッドに組み込んで回るようにして完成です。

```typescript:複数重ねの円

import * as THREE from "three";
import OneRing from "./OneRing";

class MultiRing {
  private _group: THREE.Group;
  private _oneRings: OneRing[];
  constructor(
    outlineRadius: number = 50,
    ringNum: number = 3,
    ringSizeDiff: number = 10,
    tube?: number,
    arc?: number
  ) {
    this._group = new THREE.Group();
    this._oneRings = [];

    for (
      let [count, nowRadius] = [0, outlineRadius];
      count < ringNum;
      count++, nowRadius -= ringSizeDiff
    ) {
      const rotateDirection = count % 2 ? "left" : "right";
      const ring = new OneRing(rotateDirection, nowRadius, tube, arc);
      this._oneRings.push(ring);
    }

    for (let i of this._oneRings) {
      this._group.add(i.ring);
    }
  }

  public addToScene(scene: THREE.Scene) {
    scene.add(this._group);
  }

  public tick() {
    for (let i of this._oneRings) {
      i.tick();
    }
  }
}

export default MultiRing;
```

以上が赤いリングの作り方です。

# タイマーの作り方

次はタイマーの実装方法について説明します。
タイマーは普通にsetIntervalで時間を計測しました。

手順としては
1. 1秒経った際にoneSecondFlagといったような「一秒経ったよ」フラグをtrueにする
2. 毎フレーム呼ばれるメソッドであるtickでoneSecondFlagがtrueの際のみ一秒をカウントする処理を実行

このような流れになります。

基本的に他のアニメーションなどもフラグを作成してtickで処理をするという流れで作りました。

コードにすると以下のようなイメージです。

```typescript
class Second {
  private _oneSecondFlag: boolean;
  constructor() {
    this._oneSecondFlag = false;
  }

  public startCount() {
    setInterval(() => {
      this._oneSecondFlag = true;
    }, 1000);
  }

  public tick() {
    if (this._oneSecondFlag) {
      // 一秒経った際にする処理
      this._oneSecondFlag = false;
    }
  }
}

```

今回私が作ったラーメンタイマーでは「一秒経った際にする処理」としてタイマーを回転させる処理を挟みました。

以上がタイマーの作り方です。

# イージングの作成

今回作ったラーメンタイマーではほとんどのアニメーションにイージングを付けました。

そのコードは以下のようなものになりました。

```typescript
const easing = (
  nowLocation: number,
  targetLocation: number,
  easingSpeed: number = 0.09
) => {
  return (targetLocation - nowLocation) * easingSpeed;
};

```

この関数を毎フレームごとにnowLocationを変えて呼びだす事でイージングを実装しました。

# 終わりに
以上が3Dラーメンタイマーの技術紹介になります。

Three.jsは今まで触ったことがなかったのですがこのラーメンタイマーを作ったら何となくわかるようになりました。

やっぱりどんなフレームワークや言語でもラーメンタイマーは良い題材なんですね！

最後まで読んでいただきありがとうございました。