手書き風SVGパスアニメーションを実装する

手書き風のパスアニメーションを実装するための基礎及び手順について記載します。
最終的に以下のようなものが作れるようになります。

![pathanim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/eb03edad-cb8e-eecf-19f4-943b7467c281.gif)

## パスアニメーションの基礎

パスアニメーションを行う上で重要なのはCSSの**`stroke-dasharray`と`stroke-dashoffset`**です。

`stroke-dasharray`は破線を実現するためのプロパティです。
値として実線部分と隙間部分の長さを指定することができます。

```css
path.line {
    /* 線の長さ(dash):10 隙間(gap):3 */
    stroke-dasharray: 10 3;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/f1e7a327-8011-d704-3fd3-666de2531f79.png)

`stroke-dashoffset`は破線のオフセットを指定するプロパティです。
少しわかりづらいですがこのプロパティを指定するとパスの開始方向に全体がスライドします。

```css
path.line {
    /* 線の長さ(dash):10 隙間(gap):3 */
    stroke-dasharray: 10 3;
    /* オフセット: 5 */
    stroke-dashoffset: 5;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/096ad886-2e1e-d5cf-a6e2-983a5138fe33.png)

画像は左から右のパスなので全体が左にスライドしています。
(オフセットの部分は実際には表示されません。)

実線の長さとオフセットを同じにした場合、パスが隙間から始まります。

```css
path.line {
    /* 線の長さ(dash):10 隙間(gap):3 */
    stroke-dasharray: 10 3;
    /* オフセット: 10 */
    stroke-dashoffset: 10;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/14c2c8b9-a000-46b9-989a-d575bd13e8f1.png)

実線、隙間、オフセットを全て同じ長さかつパスの長さ以上にした場合、隙間部分のみが描画領域に存在することになるのでなにも表示されなくなります。

```css
path.line {
    /* 線の長さと隙間が同じ場合は一つのみ指定する */
    stroke-dasharray: 30;
    /* オフセット: 30 */
    stroke-dashoffset: 30;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/71f3ba88-1b86-5479-32c2-db28263323b6.png)

この状態からオフセットを少しずつ0に近づけると少しずつ線が表示されます。

```css
path.line {
    /* 線の長さ(dash):30 隙間(gap):30 */
    stroke-dasharray: 30;

    animation: pathanim 3s;
    animation-iteration-count: infinite;
}

@keyframes pathanim {
    0% {
        stroke-dashoffset: 30;
    }
    100% {
        stroke-dashoffset: 0;
    }
}
```

![pathanim-base.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/b66e122a-faab-9b89-fc9e-fb1aa9c37855.gif)


これがパスアニメーションの基本です。
(アニメーションの開始時に微妙に線が表示されてしまう場合は`stroke-dasharray`を調整してください。)

### 補足:SVGファイルを作る

SVGパスアニメーションを作るためには何よりSVGファイルが必要です。
冒頭のSVGは[Inkscape](https://inkscape.org/ja/)で作成しました。
**Inkscape**でパスアニメーション用のSVGファイルを作成する際は保存時に**ファイルの種類を最適化SVG**にします。


### 補足2:SVGファイルを手動で編集する

Inkscapeが出力したSVGは思ったようなパスになっていない可能性があります。
その場合は[ここ](https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/Paths)を参考にしつつテキストエディタでパスを編集します。

## 手書き風パスアニメーション

今回は以下の二つを実装します

* 1パスずつ描画する
* 描いている最中のパスの色を変える

後者は人によってはいらないかもしれません。
この制御はjsを使わなければなりませんが、今回は[vivus.js](https://github.com/maxwellito/vivus)というライブラリを使用します。
[vivus.js](https://github.com/maxwellito/vivus)はよくできたライブラリで**1パスずつ描画する**という要望を満たすだけならほぼ何も書かずに実現することができます。

### 1パスずつ描画する

```html
<!-- vivus.js読み込み -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/vivus/0.4.4/vivus.js"></script>

<svg id="svg">
    <!-- ...複数のパス... -->
</svg>
```

```js
new Vivus('svg', {
    // アニメーション再生タイプ
    // 'oneByOne'を指定すると1パスずつのアニメーションになる
    type: 'oneByOne',

    // アニメーション全体の長さ
    // 総フレーム数で指定する
    // fpsで割ると秒数になる
    // 60fpsで動いている場合は5秒になる
    duration: 300,
});
```

![pathanim-vivus.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/99800fb0-d569-4a97-e84c-2ae8557a7bd0.gif)

これだけで1パスずつのアニメーションを行うことができます。
これで要件を満たしている場合は特にこれ以上やる必要はありません。

### 描いている最中のパスの色を変える

vivus.jsの標準ではこの機能が用意されていないので自分で実装する必要があります。
実装方法は単純で現在の再生時間から描いている最中のパスを特定し、そのパスの色を変えます。

```css
/* 描画中のパスに適応するスタイル */
path.active {
    stroke: tomato;
}
```

```js
const v = new Vivus('svg', {
    // アニメーション再生タイプ
    // 'oneByOne'を指定すると1パスずつのアニメーションになる
    type: 'oneByOne',

    // アニメーション全体の長さ
    // 総フレーム数で指定する
    // fpsで割ると秒数になる
    // 60fpsで動いている場合は5秒になる
    duration: 300,
    
    // 自動で更新されないようにする
    start: 'manual',
});

// 描画の更新
const f = () => {
    // vivusの内部フレームを進める
    const progress = (v.currentFrame + 1) / v.duration;
    v.setFrameProgress(progress);
    
    // vivusが管理しているパスのリスト
    const map = v.map;
    
    // 完了していない場合
    if (progress < 1) {
        let frame = 0;
        // パスのリストから現在描画しているパスを特定する
        for (let i = 0; i < map.length; i++) {
            const path = map[i];
            // パスの終了フレーム
            const next = frame + path.duration
            
            // 描画中のパスではない
            if (v.currentFrame < frame || v.currentFrame >= next) {
                // クラスを削除
                path.el.classList.remove('active');
            }
            // 描画中のパス
            else {
                // クラスを追加
                if (!path.el.classList.contains('active')) {
                    path.el.classList.add('active');
                }
            }
            frame = next;
        }
        
        requestAnimationFrame(f);
    }
    // 完了している場合
    else {
        for (let i = 0; i < map.length; i++) {
            map[i].el.classList.remove('active');
        }
        
        // ループさせる場合
        setTimeout(() => {
            v.setFrameProgress(0);
            f();
        }, 3000);
    }
};

requestAnimationFrame(f);
```

![pathanim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/eb03edad-cb8e-eecf-19f4-943b7467c281.gif)

## 最後に

基礎部分をわかっていなくても[vivus.js](https://github.com/maxwellito/vivus)なら簡単にパスアニメーションを実装できます。
ライブラリを使用せず自分で実装する場合や[vivus.js](https://github.com/maxwellito/vivus)で実現できないことを実現したい場合は基礎部分が役に立つこともあるかもしれません。