HTMLでSVGアニメーションを行う

# HTMLでSVGを使う
svgはHTML内のimgタグで使用できる、ベクター形式の画像フォーマットです。
ベクター形式のため拡大縮小による画像の表示劣化がないので、PCと比べ解像度の高いスマホやRetinaディスプレイでの表示に向いています。
またSVGのデータを調整することで色やサイズをブラウザ側で変更することが可能であり、それの応用としてアニメーションも行うことができます。
このようなメリットから、最近では一部にSVGを使うWEBサイトが増えてきています。

ただしSVGをそのまま表示する（imgタグでSVGファイルを指定）場合はそれほど問題はないものの、ブラウザで色々と調整できる恩恵を受けるためには実は「SVGをHTML内にインラインで書く」＋「高度なJavaScript処理を書く必要がある」ため、なかなか気軽にはできません。。

ここでは

* SVGは別ファイルで管理
* できるだけ簡単なJavaScriptコードでアニメーションする

という方法を解説します。

#Snap.svg
前述の2つの壁を乗り越えるために、Snap.svgというライブラリを活用します。
[Snap.svg](http://snapsvg.io/)
Snap.svgはAdobeが提供しているSVG操作用ライブラリで、SVGの複雑な処理をかなり簡単にしてくれます。

## webpackでの利用
ビルドツールであるwebpackでsnap.svgを使うにはconfigに少し追記が必要になります。

まずライブラリそのものはnpmにあるためそのまま持ってきます。

```
npm i -S snapsvg
```

そしてwebpackの設定ファイル（webpack.config.js）で以下のように記述します。
※私の環境ではwebpackバージョンは4系ですが3系でも動くはず。

```webpack.config.js
// 関連する部分のみ抜粋して記載しています
module.exports = {

  /* ...略... */

  resolve: {
    alias: {
      snapsvg: 'snapsvg/dist/snap.svg.js',
    }
  },

  /* ...略... */

  module: {
    rules: [

      /* ...略... */

      {
        test: require.resolve('snapsvg'),
        use: [
          {
            loader: 'imports-loader',
            options: {
              this : '>window',
              fix : '>module.exports=0'
            }
          }
        ]
      },
    ]
  }
}
```

これで実際に組み込むページのjsにて、以下のようにimportで使用できます。

```index.js
import Snap from 'snapsvg'

// …なんか処理
```

# SVGのインライン化
Snap.svgでsvgを操作するためには、HTML内にインラインで展開されている必要があります。ただ実際サイト運用するときにHTML内にSVGがそのまま書かれているのは保守性が悪く、運用するひとはなかなかストレスが溜まります。笑
そこでSnap.svgのload機能を使ってimgタグでの記述をインラインに置き換えます。
ここではlogoのSVGはそのまま、scrollのSVGをインラインに置き換えます。

```index.html
<!--　抜粋 -->
<section class="sec-01">
  <h2><img src="./logo.svg"></h2>
  <div class="scroll"><img src="./button-scroll.svg" class="svgImg"></div>
</section>
```

```index.js
import Snap from 'snapsvg'

let svgObj = document.querySelector('img.svgImg');
const svgPath = svgObj.getAttribute('src');

const paper = Snap().remove();

// 指定パスのsvgファイル読み込み
Snap.load(svgPath, function(svg) {
  paper.attr('viewBox', svg.node.viewBox.baseVal.x + " " + svg.node.viewBox.baseVal.y + " " + svg.node.viewBox.baseVal.width + " " + svg.node.viewBox.baseVal.height);
  paper.attr('xmlns', 'http://www.w3.org/2000/svg');
  paper.append(svg.selectAll('svg>*'));
  
  // 生成したsvgオブジェクトをimgタグのDOMと置き換え
  const parentDom = svgObj.parentNode;
  parentDom.replaceChild(paper.node, svgObj);

  // 置き換え後の処理

});
```

svgの置き換えを関数にすると使い回しができてかつ見通しも良くなります。

```index.js
import Snap from 'snapsvg'

// 共通関数
const commonFunc = {
  svgLoaded: function(fileName) {
    return new Promise((resolve, reject) => {
      const paper = Snap().remove();
      Snap.load(fileName, function(svg) {
        paper.attr('viewBox', svg.node.viewBox.baseVal.x + " " + svg.node.viewBox.baseVal.y + " " + svg.node.viewBox.baseVal.width + " " + svg.node.viewBox.baseVal.height);
        paper.attr('xmlns', 'http://www.w3.org/2000/svg');
        paper.append(svg.selectAll('svg>*'));
        resolve(paper);
      });
    });
  }
}

// インライン化するオブジェクトのセレクタ
const objList = ['img.svgImg'];
objList = objList.map(selector => document.querySelector(selector));

Promise.all(objList.map(elm => {
  return new Promise((resolve, reject) => {
    let path = elm.getAttribute('src');
    Promise.resolve()
      .then(function() {
        return commonFunc.svgLoaded(path);
      })
      .then(targetSvg => {
        let parentDom = elm.parentNode;
        parentDom.replaceChild(targetSvg.node, elm);
        resolve(targetSvg);
      })
   })
}))
.then(svgObj => {
  // 置き換え後の処理
});
```

# SVGアニメーションの実装
Snap.svgを使うとアニメーションも簡単に実装できます。
アニメーションしたいsvgファイルはこんな感じ。

<img src="https://qiita-image-store.s3.amazonaws.com/0/187744/9936f69a-47bb-c2f5-7f7c-b4cfb0a4febf.gif" width="200">

これの●部分がピコピコと上下するアニメーションです。
SVGの中身（コード）はこちら。

```button-scroll.svg
<svg id="button-scroll" xmlns="http://www.w3.org/2000/svg" viewBox="933 946 56 60">
  <defs>
    <style>
      .cls-1 {
        fill: none;
        stroke: #000;
        stroke-miterlimit: 10;
      }

      .cls-2, .cls-3 {
        fill: #000;
      }
    </style>
  </defs>
  <g id="svg-scroll-group-1" transform="translate(0 117)">
    <line id="svg-scroll-line" class="cls-1" y2="60" transform="translate(960.1 829)"/>
    <circle id="svg-scroll-circle" class="cls-2" cx="5.1" cy="5.1" r="5.1" transform="translate(955 847)"/>
  </g>
</svg>
```

前述の処理でインライン化できたとして、置き換え後の処理のところでアニメーション処理を記述します。

```index.js

Promise.all(objList.map(elm => {

 // ...前述の処理...

}))
.then(svgObj => {
  // circleのアニメーション
  const circleObj = svgObj[0].select('circle');
  const cursorAnimation = () => {
    circleObj.animate({
      'cy': 5
    }, 600, mina.easein,function() {
      this.animate({
          'cy': 20
      }, 600, mina.easein,function() {
          cursorAnimation();
      });
    });
  };
  cursorAnimation();
});
```

## 解説

処理としてはかなり簡単で、
selectで●のオブジェクトを選択し、
animateでアニメーション処理を行っています。
具体的な挙動は

1. y軸5pxの位置に600m秒かけてアニメーション
2. y軸20pxの位置に600m秒かけてアニメーション
3. 自分自身（cursorAnimation）を呼び出すことにより、1に戻って繰り返し

という単純なものです。
mina.easeinのところはアニメーションのイージングで、Snap.Svgでは"mina"というイージングライブラリがあるのでこれを利用しています。
参考：[Snap.svgのイージング](http://snapsvg.io/docs/#mina.linear)

全体的な書き方はjQueryライクな記述方法なので、jQueryを触ったことがある方はわりと直感的に理解できると思います。


# まとめ
以上SVGアニメーションの手順でした。
これくらい簡単なアニメーションの場合、そもそもSnap.Svgを使わなくても、SVG自身にCSSアニメーションを記述することでも実現できます。
ただし旧バージョンのブラウザではSVG自身のCSSアニメーションは動かないことが多く、逆にSnap.SvgはIE10でも動作保証しているためこちらを使った方が確実かと思います。

また今回はかなり簡単なアニメーションですが、Snap.svgはもっと凝ったアニメーションも可能ですので、以下公式ドキュメントを参考にいろいろ試してみてください。

# 参考
[Snap.svg API Reference](http://snapsvg.io/docs/)