SVG を使った 2次ベジェ曲線アニメーション - Vue.js サンプル付き ٩(ˊωˋ*)و

`<canvas>` や `<svg>` を利用し、JavaScript でベジェ曲線を描画することは難しくはありません。しかし、曲線を一筆書きするようなアニメーションをプログラムで作るにはどうすれば良いでしょうか？

さまざまな方法があるかと思いますが、数時間ググった結果、素晴らしい解説を掲載したページを見つけました。とても美しいですね..。:princess:
[Animated Bézier Curves - Jason Davies](https://www.jasondavies.com/animated-bezier/)

このページに掲載されているベジェ曲線の図解アニメーションは [D3.js](https://d3js.org/) で作成しているようですが、今回は説明されているロジックに基づき [Vue.js](https://jp.vuejs.org/) と SVG で再現してみました。

<p class="codepen" data-height="482" data-theme-id="0" data-default-tab="js,result" data-user="megurock" data-slug-hash="wNxaKg" style="height: 482px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="CSV Path Animation with Vue.js">
  <span>See the Pen <a href="https://codepen.io/megurock/pen/wNxaKg/">
  CSV Path Animation with Vue.js</a> by Eiji Meguro (<a href="https://codepen.io/megurock">@megurock</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

**この記事では Vue.js の実装についての説明はしません。主にプログラミングで曲線をアニメーションさせるためのロジックについての解説を行います。**

## SVG or Canvas ?

JavaScript でベジェ曲線を描画するには、`<svg>` を使う方法と `<canvas>` を使う方法がありますが、今回は Vue.js の利用を想定していたため、`<svg>` を使うことにしました。`<path>` や `<line>`、`<circle>` などの HTML タグを Vue.js のテンプレートに組み込むことが可能なため、**「プログラムはデータの処理に集注し、ビュー（見た目）はテンプレートにお任せ」** という Vue.js の素敵な仕組みをより有効に活用することができます。 

## 1. 始点・制御点・終点の作成

まず初めに **2次ベジェ曲線を形成するために必要な 始点 (P0)・制御点 (P1)・終点 (P2)** を用意します。
2 次ベジェ曲線では、制御点を通過しません。ベジェ曲線の特性を知りたい方は、ページ下の参考サイトなどにて確認ください。
![img_01.png](https://qiita-image-store.s3.amazonaws.com/0/37412/05e63acf-ee26-c932-976c-7a01639bf19d.png)
なお、SVG で円を描画するタグは以下のようなものになります。

```html
<circle cx="100" cy="100" r="5" fill="red" stroke="#999" stroke-width="0.5" />
```

## 2. 内分点の作成

それぞれの点の間に**内分点 (IP0 と IP1)** を用意します。まずは分かりやすいようにそれぞれの点を直線で結んでみます。
![img_02.png](https://qiita-image-store.s3.amazonaws.com/0/37412/18e863d3-58a8-8ae3-aea6-b04ad3c7b835.png)
なお、SVG で線を描画するタグは以下のようなものになります。

```html
<line x1="100" y1="100" x2="200" y2="200" stroke-width="2" stroke="black" />
```

ひとまず内分点を線の始端に配置します。すなわち、**P0 - P1 間の内分点 (IP0)** は P0 と同じ座標に、そして **P1 - P2 間の内分点 (IP1)** は P1 と同じ座標に配置します。
![img_03.png](https://qiita-image-store.s3.amazonaws.com/0/37412/5e2b04a5-fc9f-52c0-fd1c-06261b972700.png)


## 3. 内分点の移動
**内分点 (IP0 と IP1)** は、2 点を結んだ直線の始端から終端に向けて**同じ比率で移動させる**必要があります。
これを実現するために、ここで **変数 `t`** を用意します。**この変数はアニメーション開始の瞬間は 0 ですが、進捗とともに徐々に増加させてください。そして最終的にアニメーションが終了する時点では、値が 1.0 になるようにします。**


以下のイメージは、変数 `t` の値を基に、各内分点がどの座標に位置するかを示したものです。繰返しになりますが、各内分点は、それぞれが位置する直線上の始端から**同じ比率だけ**移動します。**同じ距離ではない**ことに注意ください。
![img_04.png](https://qiita-image-store.s3.amazonaws.com/0/37412/c1f5b052-7cfd-34a6-2d2b-022bf28653d7.png)

## 4. ベジェ曲線の終点の作成

アニメーションさせる **2 次ベジェ曲線の終点 (MP)** を作成します。なお、曲線の始点は P0 と同じ座標になりますので、今回は作成しておりません。
**MP は IP0 と IP1 を結んだ直線上の始端から終端までを、変数 `t` の比率で移動します。**

以下のイメージは、変数 `t` の値を基に、ベジェ曲線の終点がどの座標に位置するかを示したものです。
![imag_05.png](https://qiita-image-store.s3.amazonaws.com/0/37412/a1071765-2319-aeea-9225-d67e6ecc6034.png)

## 5. ベジェ曲線の描画

いよいよ最後のステップですが、実はすでにこの段階でベジェ曲線を描画する要素はすべて揃っています。解説の最初でも述べたように、2 次ベジェ曲線は、始点・制御点・終点の 3 つのポイントで構成されます。そして、私たちが**求めるベジェ曲線を描画するためのこれらの 3 つのポイントは以下のようになります。**

|始点|制御点|終点|
|:--:|:--:|:--:|
|**P0**|**IP0**|**MP**|

![img_07.png](https://qiita-image-store.s3.amazonaws.com/0/37412/fe942afd-e4a2-e6d6-ab75-87af5344a871.png)

### SVG で 2次ベジェ曲線を描画する

数字が羅列されているため最初は戸惑うかもしれませんが、分れば簡単です。
**属性 `d` には、「始点」「制御点」「終点」の順でそれぞれのポイントの座標が記述されています。**
具体的には、`M10 80` が始点の座標、`Q 95 10` が制御点の座標、最後の `180 80` が終点の座標を表しています。座標は最初の数が x 座標、2 番目が y 座標です。

```html
<path d="M10 80 Q 95 10 180 80" stroke="red" fill="transparent" />
```

## 数学が苦手な方のための Tips

実装に入るにあたり、「なんだか難しそう..」という人向けに、ちょっとした Tips を書いてみました。:sunglasses:
一見難しそうに思えるかもしれませんが、今回は基礎的な三角関数さえ分かれば大丈夫です！

### 角度と半径から座標を取得する

円周上の任意のポイント (P) は、その角度 (radians) と半径 (radius) が分かっていれば座標を求めることができます。
![img_06.png](https://qiita-image-store.s3.amazonaws.com/0/37412/aea4cbac-8299-d4c9-d0c0-4992bd6e5c76.png)

```js
// radians にはポイントの角度、radius には半径が入っているものとします。 
const x = Math.cos(radians) * radius
const y = Math.sin(radians) * radius
```

### 2 点間の距離を取得する

2 つのポイント間の距離を取得する例です。

```js
function getDistance(pointA, pointB) {
 const distanceX = pointA.x - pointB.x
 const distanceY = pointA.y - pointB.y
 return Math.sqrt(distanceX * distanceX + distanceY * distanceY)
}

const pointA = { x: 50, y: 100 }
const pointB = { x: 200, y: 200 }
const distance = getDistance(pointA, pointB) 
console.log(distance) // -> 180.27756377319946
```

### 任意の点の角度を取得する

任意の点の角度（ラジアン）を取得する例です。`centerPoint` には円の中心座標を設定します。

```js
function getRadians(point, centerPoint = { x: 0, y: 0 }) {
  return Math.atan2(point.y - centerPoint.y, point.x - centerPoint.x)
}

const pointA = { x: 150, y: 150 }
const pointB= { x: 50, y: 50 }
const radians = getRadians(pointA, centerPoint)
console.log(radians) // -> 0.5880026035475675
```

### 内分点の座標を t から取得する

今回はこのやり方を使っていませんが、こちらの方がよりシンプルに実装できますね。三角関数も不要だわ..:sweat:。

```js
function getDividingPoint(pointA, pointB, t) {
  return {
    x: pointA.x * (1 - t) + pointB.x * t,
    y: pointA.y * (1 - t) + pointB.y * t,
  }
}

const pointA = { x: 150, y: 150 }
const pointB = { x: 50, y: 50 }
const dividingPoint = getDividingPoint(pointA, pointB, 0.5)
console.log(dividingPoint.x, dividingPoint.y) // -> 100, 100
```

## 所感

一見複雑そうに見える動きですが、こうしてロジックを紐解いてみると案外と簡単な仕組みであることが分かりました。そして、今回解説はしませんでしたが、この仕組みを実装するにあたり、Vue.js は非常に扱いやすく、SVG との親和性も抜群でした！:smiley:

SVG ベースのビジュアライゼーションは、[D3.js](https://d3js.org/) や [Raphael.js](http://dmitrybaranovskiy.github.io/raphael/) といったライブラリを利用するのが一般的かもしれませんが、今回のようにシンプルなものであれば Vue.js でやってみるのも良いかもしれませんね！

## 参考

- [ベジエ曲線を使ってみよう](https://qiita.com/hart_edsf/items/cec5af01a70b62ca93f2)
- [SVG: Scalable Vector Graphics | MDN](https://developer.mozilla.org/ja/docs/Web/SVG)