【入門】mo.jsでSVGアニメーション

SVGのアニメーションを実装する機会があり**mo.js**を使用したので、備忘録も兼ねて基本的な使い方を紹介したいと思います。

[公式チュートリアル](https://mojs.github.io/tutorials/)が充実しているのでこちらを読めば大丈夫だと思いますが、日本語の記事がほとんどないので英語アレルギーの方は参考にしてみてください。

執筆時点での**mo.js**のバージョンは**0.288.2**です。

#セットアップ

##npm
**mo.js**をインストール。

```sh
npm i @mojs/core
```

インストールしたら**mo.js**を使用するjsファイルで読み込む。

```js
import mojs from '@mojs/core';
```

##CDN
以下をHTMLに記述する。

```html
<script src="https://cdn.jsdelivr.net/npm/@mojs/core"></script>
```

#SVGを生成する
自分でSVGを用意しなくても、簡単な形であれば**mo.js**を使って生成することができます。
（自分で用意したSVGを使う方法は後述。）

##円を描画する

```js
new mojs.Shape({
  parent: '#parent', // SVGの親要素を指定する
  shape: 'circle', // 描画するSVGの指定
  radius: 10, // 円のサイズ
  fill: 'deeppink', // 塗りの色
  isShowStart: true // 最初から描画しておくかどうか
});
```

`shape`に`'circle'`を指定すると`ellipse`タグを使ったSVGを生成してくれます。
`circle`タグではなく`ellipse`タグで生成されるため、楕円にすることもできます。
楕円にしたい場合は`radius`ではなく`radiusX`、`radiusY`で縦幅、横幅をそれぞれ指定しましょう。

また、デフォルトでは`isShowStart`が`false`になっています。
これを`true`にしておかないとアニメーションが実行されるまで非表示になってしまうため、最初から表示しておきたいものは`true`指定するのを忘れないようにしましょう。

##四角形を描画する

```js
new mojs.Shape({
  parent: '#parent',
  shape: 'rect',
  radius: 10,
  fill: 'cyan',
  isShowStart: true
});
```

`shape`に`'rect'`を指定すると`rect`タグを使ったSVGを生成してくれます。
オプションの指定は`circle`と同じ感じで大丈夫です。

##多角形を描画する

```js
new mojs.Shape({
  parent: '#parent',
  shape: 'polygon',
  points: 6, // 角の数
  radius: 10,
  fill: 'yellow',
  isShowStart: true
});
```

shapeに`'polygon'`を指定すると`path`タグでSVGを生成してくれます。
オプションの指定は基本的に`'circle'`、`'rect'`と同じですが、`points`で角の数を指定することができます。
デフォルト値は3なので指定していなければ三角形が描画されます。

##ジグザグ線を描画する

```js
new mojs.Shape({
  parent: '#parent',
  shape: 'zigzag',
  points: 11,
  radius: 25,
  fill: 'none',
  stroke: 'deeppink', // アウトラインの色
  isShowStart: true
});
```

shapeに`'zigzag'`を指定すると`path`タグでSVGを生成してくれます。
オプションの指定は`'polygon'`と同じ感じで大丈夫です。
`fill`に`none`を指定して塗りを無効にしておかないと、三角形が横並びになっているような感じで描画されます。
合わせて`stroke`に線の色を指定しておきましょう。

##曲線を描画する

```js
new mojs.Shape({
  parent: '#parent',
  shape: 'curve',
  radius: 25,
  fill: 'none',
  stroke: 'cyan',
  isShowStart: true
});
```

shapeに`'curve'`を指定すると`path`タグでSVGを生成してくれます。
オプションの指定は`'circle'`、`'rect'`と同じ感じで大丈夫です。
`fill`に`none`を指定して塗りを無効にしておかないと、線の内側が塗りつぶされた状態で描画されます。

##交差した線を描画する

```js
new mojs.Shape({
  parent: '#parent',
  shape: 'cross',
  radius: 25,
  stroke: 'yellow',
  isShowStart: true
});
```

shapeに`'cross'`を指定すると`path`タグでSVGを生成してくれます。
オプションの指定は基本的に`'circle'`、`'rect'`と同じ感じですが、`fill`を指定しても何も変わりません。


その他の指定可能なオプションやコールバックは[こちら](https://mojs.github.io/api/shape/)に一覧が載っています。

##実際の使用例
<p class="codepen" data-height="497" data-theme-id="0" data-default-tab="result" data-user="srbangs" data-slug-hash="RwbxMWr" style="height: 497px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="mo.js - Draw Shapes Sample">
  <span>See the Pen <a href="https://codepen.io/srbangs/pen/RwbxMWr/">
  mo.js - Draw Shapes Sample</a> by yumetomo (<a href="https://codepen.io/srbangs">@srbangs</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

#アニメーションを実装する前に

`Bounce`系のアニメーションを付けると、素の状態だとはみ出した分が見切れてしまいます。
CSSで以下の指定をしておきましょう。

```css
svg {
  overflow: visible;
}
```

#無限ループのアニメーション

円のサイズが20→80→20→80→20→...と無限リピートするアニメーションを作ってみます。

```js
new mojs.Shape({
  parent: '#parent',
  shape: 'circle',
  fill: '#F64040',
  radius: { 20 : 80 }, // { start : end }
  duration: 2000, // アニメーションの秒数
  isYoyo: true, // リピートする時に再生→逆再生→再生にするかどうか
  isShowStart: true,
  easing: 'elastic.inout', // アニメーションのイージング
  repeat: 1, // リピートする回数

  // リピート分を含めてアニメーションが全て終わったら呼び出されるcallback
  onComplete() {
    // この時thisの参照先はShapeオブジェクトになる
    this.replay();
  }
}).play();
```

アニメーションで変化させたいオプションは`radius: { 20 : 80 }`のようにオブジェクトで渡す。

`isYoyo`オプションは初期値`false`なので、指定していなければ**再生→初期値に戻る→再生**の繰り返し、`true`を指定すると**再生→逆再生→再生**の繰り返しになる。
また、逆再生はリピート扱いなので`repeat`オプションに最低でも1を指定していないと逆再生されない点に注意しましょう。

リピートする回数が決まっている場合はその回数を`repeat`オプションにそのまま渡せば大丈夫ですが、`repeat`オプションで無限ループの指定をすることはできないので、無限ループしたい場合は`onComplete()`のコールバック関数内で`replay()`メソッドを呼び出しもう一度最初からアニメーションをやり直します。

`easing`オプションに指定できる値は[こちら](https://mojs.github.io/api/#easing)を参照してみてください。

#複数のアニメーションを繋げる

四角形が回転→内側から塗りが消えていくという流れを作ってみます。

```js
new mojs.Shape({
  parent: '#parent',
  shape: 'rect',
  fill: 'none',
  stroke: '#FC46AD',
  radius: 10,
  strokeWidth: 20,
  angle: { '-180': 0 },
  duration: 900,
  easing: 'bounce.out',
})
.then({
  strokeWidth: 0,
  scale: 2,
  duration: 800,
  easing: 'sin.in',
  onComplete() {
    this.replay();
  }
})
.play();
```

`then`で繋げることで一つのSVGに対して順番にアニメーションを実行することができます。
最初に指定したオプションは後から上書きしない限り維持されます（上の例だと`fill`と`stroke`は後から変わっていないのでずっと初期値のまま）。

#複数のSVGを順番にアニメーションさせる

こういう時は`Timeline`の出番です。

```js
const CIRCLE = new mojs.Shape({
  parent: '#parent',
  shape: 'circle',
  radius: 30,
  scale: { 0: 1 },
  fill: 'none',
  stroke: 'deeppink',
  strokeWidth: { 30: 0 },
  x: 'rand(-50, 50)', // 引数を元にランダムな値が入る - (min, max)
  y: 'rand(-50, 50)',
  duration: 300,
  // このSVGのアニメーションが完了した時点で呼び出される
  onComplete() {
    // rand()の値を再生成する
    this.generate();
  }
});

const RECT = new mojs.Shape({
  parent: '#parent',
  shape: 'rect',
  radius: 30,
  scale: { 0: 1 },
  fill: 'none',
  stroke: 'cyan',
  strokeWidth: { 30: 0 },
  x: 'rand(-50, 50)',
  y: 'rand(-50, 50)',
  delay: 150,
  duration: 300,
  onComplete() {
    this.generate();
  }
});

const POLYGON = new mojs.Shape({
  parent: '#parent',
  shape: 'polygon',
  radius: 30,
  points: 5,
  scale: { 0: 1 },
  fill: 'none',
  stroke: 'yellow',
  strokeWidth: { 30: 0 },
  x: 'rand(-50, 50)',
  y: 'rand(-50, 50)',
  delay: 300,
  duration: 300,
  onComplete() {
    this.generate();
  }
});

const TIMELINE = new mojs.Timeline({
  // Timelineに登録されている全てのアニメーションが完了したら呼び出される
  onComplete() {
    this.replay();
  }
});
TIMELINE.add(CIRCLE, RECT, POLYGON);
TIMELINE.play();
```

円と四角形と五角形のSVGを生成し、`Timeline`オブジェクトに`add()`メソッドで追加して、`play()`メソッドを実行すると登録されているSVGのアニメーションが実行されます。

## Staggerと組み合わせる
`Timeline`に登録するSVGがそれぞれ全く違う性質であれば良いですが、↑のようにほとんど同じようなSVGが複数ある場合1つ1つ生成していくのは非常に冗長です。
共通の部分だけ変数に持たせておくという手もありますが、`Stagger`を使うとより簡潔にまとめられます。

```js
const STAGGER = mojs.stagger(mojs.Shape);
const SHAPES = new STAGGER({
  parent: '#parent',
  quantifier: 3, // 生成するSVGの数
  shape: ['circle', 'rect', 'polygon'], // 左から順番に参照される
  radius: 30,
  scale: { 0: 1 },
  fill: 'none',
  stroke: ['deeppink', 'cyan', 'yellow'], // 左から順番に参照される
  strokeWidth: { 30: 0 },
  x: 'rand(-50, 50)',
  y: 'rand(-50, 50)',
  duration: 300,
  delay: 'stagger(150)', // 0から始まり、150刻みで増えていく
  onComplete() {
    this.generate();
  }
});

const TIMELINE = new mojs.Timeline({
  onComplete() {
    this.replay();
  }
});
TIMELINE.add(SHAPES);
TIMELINE.play();
```

###quantifier
コメントで書いてある通り、SVGを何個生成するか指定します。
整数を渡せばその数だけ生成されますが、例えば`'shape'`という文字列を渡すと`shape`オプションに指定されている配列の中身の数が渡されます。

```js:例
quantifier: 'shape', // shapeオプションの配列の中身の数（3）が渡される
shape: ['circle', 'rect', 'polygon']
```

###quantifierの値 > 配列の中身の数な場合
例えば以下のような場合。

```js:例
quantifier: 5,
shape: ['circle', 'rect', 'polygon']
```

この場合は`circle`→`rect`→`polygon`→`circle`→`rect`という順番で値が渡される。
つまり`quantifier`の値の方が大きい場合、溢れた分はもう１度配列の先頭から参照するようになっている。

###stagger()
渡された値刻みで増えていく。
第1引数が初期値、第2引数が増加量。
引数を1つだけにした場合は第2引数の増加量として扱われ、初期値は0になる。

```js:例
quantifier: 3,
duration: 'stagger(150)', // 0→150→300
delay: 'stagger(100, 50)', // 100→150→200
x: 'stagger(1, .5)', // 1→1.5→2
y: 'stagger(-100, rand(100, 200))' // マイナスの値やrand()も使える
```

#自分で用意したSVGを使用する
**mo.js**の機能を使わず、自分で用意したSVGを使う場合は以下のように

```js
class Thunder extends mojs.CustomShape {
  getShape() { return '<path d="M47.31 54.73L25.81 54.73L74.19 0L52.26 40.75L74.19 40.75L25.81 100L47.31 54.73Z" />' }
  getLength() { return 289.36328125; } // optional
}
```

`mojs.CustomShape`クラスを継承したクラスを作成し、その中にSVGのパスを返す`getShape`メソッドを書きます。
また、アニメーションさせる際に`strokeDasharray`や`strokeDashoffset`に対して%の値を指定する場合、SVGのアウトラインの長さを返す`getLength`メソッドも追加で書く必要があります。

クラスを作成したら、以下のように`mojs.addShape`メソッドに作成したクラスを渡すことで使えるようになります。

```js
mojs.addShape('thunder', Thunder);

new mojs.Shape({
  shape: 'thunder',
  // 略
});
```

##実際の使用例

<p class="codepen" data-height="400" data-theme-id="0" data-default-tab="result" data-user="srbangs" data-slug-hash="GRKyyPP" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="mo.js - CustomShape Sample">
  <span>See the Pen <a href="https://codepen.io/srbangs/pen/GRKyyPP/">
  mo.js - CustomShape Sample</a> by yumetomo (<a href="https://codepen.io/srbangs">@srbangs</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>


#ShapeSwirlモジュール
語彙力が足りずうまく言葉で説明できないので、`ShapeSwirl`モジュールを使うとどういうアニメーションができるかは以下のCodepenをご覧ください。
使い方は今まで紹介してきた`Shape`とほとんど同じです。

<p class="codepen" data-height="570" data-theme-id="0" data-default-tab="result" data-user="srbangs" data-slug-hash="YzKEWYV" style="height: 570px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="mo.js - ShapeSwirl Sample">
  <span>See the Pen <a href="https://codepen.io/srbangs/pen/YzKEWYV/">
  mo.js - ShapeSwirl Sample</a> by yumetomo (<a href="https://codepen.io/srbangs">@srbangs</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>


基本的なオプションは`Shape`と同じですが、`ShapeSwirl`用のものがいくつか増えています。
・**swirlSize**：揺れの大きさ（初期値は10）。
・**swirlFrequency**：揺れる回数（初期値は3）。
・**direction**：最初の揺れが左右どちらの向きから始まるか（初期値は1）。`1`なら右、`-1`なら左。
・**pathScale**：揺れを横の動きとすると、**縦の移動量**を調節できる（たぶんCodepenで軽く触ってみるとわかりやすいです）（初期値は1）。
・**degreeShift**：アニメーションの角度（初期値は0）。

#Burstモジュール
複数の`Shape`が飛び散るようなアニメーションを実装する時に便利なモジュールです。
オプションの指定方法は以下を参考にしてみてください。

```js
new mojs.Burst({
  parent: '#parent',
  left: 0,
  top: 0,
  radius: { 8: 64 }, // 個々のShapeの大きさではなく、飛び散る範囲に反映される
  angle: { 0: 180 },
  count: 7, // 飛び散らせるShapeの数
  degree: 180, // 飛び散らせる範囲（この場合は0°〜180°の範囲に絞られる）

  // 個々のShapeの設定はchildrenオブジェクトの中に書く
  children: {
    radius: 5,
    fill: ['deeppink', 'cyan', 'yellow'], // 配列が使える
    scale: { 1: 0 },
    duration: 1000,
    delay: 'stagger(200)', // staggerも使える
    easing: 'quint.out'
  }
});
```

##実際の使用例
<p class="codepen" data-height="574" data-theme-id="0" data-default-tab="result" data-user="srbangs" data-slug-hash="OJLzQEM" style="height: 574px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="mo.js - Burst Sample">
  <span>See the Pen <a href="https://codepen.io/srbangs/pen/OJLzQEM/">
  mo.js - Burst Sample</a> by yumetomo (<a href="https://codepen.io/srbangs">@srbangs</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

###tuneメソッド
上記Codepenのこの部分で使っている`tune`メソッドについて

```js
$WRAPPER.addEventListener('click', event => {
  BURST
    .tune({ // ←
      x: event.layerX,
      y: event.layerY
    })
    .replay();
});
```

`tune`メソッドを使用すると、指定のオプションの値を上書きすることができます。
今回の場合だと`replay`メソッドの実行前に、`x`と`y`にクリックした位置を渡して上書きしているので、クリックした位置を中心にアニメーションが実行されています。