SVGのアニメーションをSMILとReactでつくる

# 概要

SVGにアニメーションを実装する方法は、CSSやSMIL、JavaScriptのライブラリなどが候補となると思います。この中でSMILは、 **SVGファイルの中にアニメーションを記述できる** というメリットがありますが、反対にコード量が多くなり見通しが悪いという悩ましい点がありました。
またアニメーションを開発中には

* SVGの各パーツを別のファイルで管理したい
* ファイルを編集したらすぐに反映させたい
* アニメーションのパラメータを変えたい

なんてことを考えていたら、SMILによるアニメーションはReactと相性がいいんじゃないかと思いつきました。
アニメーション開発はずぶの素人なので、かなり自由気ままにですが、実際に実装して試してみました。

# 制作したもの

私の趣味はロードバイクなので、ペダリングの動作をアニメーションにしました。
[こちらが完成したもの](https://tour-maker.vercel.app/cycleman)で、ペダリングのアニメーションはSMILで記述し、ポーズを変えるパラメータはReactで制御しています。SAVE CYCLEMANボタンを押すと、その状態のSVGファイルをダウンロードでき、そのまま動くロゴとして使えます(SMILのメリット)。

# 作り方

## アニメーションを定式化する

![text4710.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591762/6094d4f7-87a9-5648-e0e9-a0cb0385e2f2.png)

ペダリングの動きを図のようにモデル化します。ペダルは角速度一定で回転し、太ももの付け根とペダルの中心は固定位置とします。太ももはこの２点OAを通る直線に対し、反時計回りにα度回転させれば良さそうです。膝下部分は太ももの直線に対し、時計回りにβ度回転させればいいですね。
SVG+SMILではこのように各パーツの回転中心や移動方向などが異なっていても比較的容易に連結でき、複雑なアニメーションができます。

点RA間に補助線を引き、三角関数を駆使してペダルがθ度のときのαとβを計算します。関数化するとこんな感じでしょうか。

```javascript
function calcAlphaAndBeta(theta, OA, r, l) { // rはペダル lは太もも、膝下の長さ
  const rad = theta / 180 * Math.PI
 　// 余弦定理
  const RA = Math.sqrt(
    OA**2 + r**2 - 2 * OA * r * Math.cos(rad)
  )
  // cosの定義から
  const betaDiv2 = Math.acos(RA / 2 / l) * 180 / Math.PI
　 // 正弦定理
  const angleOAR = Math.asin(r / RA * Math.sin(rad)) * 180 / Math.PI
  const alpha = betaDiv2 + angleOAR
  const beta = betaDiv2 * 2
  return {alpha, beta}
}
```

これでペダルの位置と足の角度の関係を計算できました。

## パーツを作る

パーツづくりのポイントは\<defs>要素内にパーツを定義して\<use>要素で配置することです。
\<defs>要素内ではパーツの回転中心を原点座標(0,0)にとり、\<use>要素のx、yに連結する位置を定義します。xとyをpropsで渡せるようにしておくと、呼び出し側で位置を決められるため、パーツ同士の連結に便利です。ちなみに\<g>要素のtransform属性で移動できるかと思いましたが、線が画像の端で切れてしまってうまくいきませんでした。
膝下部分をReactで記述すると以下のようになります。

```react
const OA = ...
const r = ...
const l = ...

function LowerLeg({ id, x, y }) {
  const thetaArr = [0, 60, 120, 180, 240, 300, 360]
  const betaArr = thetaArr.map(theta => calcAlphaAndBeta(theta, OA, r, l).beta)
  const betaStr = betaArr.map(beta => `${beta.toFixed(2)};`).join('')
  
  return (
    <>
      <defs>
        <g id={id}>
          <line stroke="yellow" strokeWidth="50" strokeLinecap="round"
            x1="0" y1="0" x2="0" y2={l}
          />
          <animateTransform
            attributeName="transform" attributeType="XML"
            type="rotate"
            values={betaStr}
            dur="3s" repeatCount="indefinite"
          />
        </g>
      </defs>
      <use href={`#${id}`} x={x} y={y} />
    </>
  )
}
```

パーツ原点から下方に膝下(長さl)を伸ばし、原点を中心にβ度回転させるアニメーションを書いています。SMILでは、\<animateTransform>要素のvaluesで定義した数値を等間隔で遷移します。valuesの元になるthetaArr(ペダルの角度θ)を等間隔に定義することで、なめらかなペダリングを再現します。
idをpropsで渡しているのは、パーツの再利用を考慮していて、同じパーツを少し変えて使いたい時に\<defs>要素内でのidの重複がないようにするためです。

この膝下を太ももと接続します。

```react
function Leg({id, x, y}) {
  const thetaArr = [0, 60, 120, 180, 240, 300, 360]
  const AlphaArr = thetaArr.map(theta => calcAlphaAndBeta(theta, OA, r, l).alpha)
  const alphaStr = alphaArr.map(alpha => `${-alpha.toFixed(2)};`).join('')

  return (
    <>
      <defs>
        <g id={id}>
          <line stroke="yellow" strokeWidth="50" strokeLinecap="round"
            x1="0" y1="0" x2="0" y2={l}
          />
          <LowerLeg
            id={id + 'lowerLeg'}
            x='0'
            y={l}
            l={l}
          />
          <animateTransform
            attributeName="transform" attributeType="XML"
            type="rotate"
            values={alphaStr}
            dur="3s" repeatCount="indefinite"
          />
        </g>
      </defs>
      <use href={`#${id}`} x={x} y={y} />
    </>
  )
}
```

太ももと膝下を\<g>要素でまとめ、その中に\<animateTransform>要素を記述することで、\<g>要素全体にこの動きが適用されます。膝下で定義したアニメーションと太もものアニメーションは加算的に動き、足の先端がなめらかに円を描きます。
これでペダリングする足が片足分できました。反対の足はこのコンポーネントを拡張して作り、胴体は別のコンポーネントとして作成するとReactらしいファイル管理ができると思います。

## SVGのDOMを取得する

最終的にSVGをファイルにするにはいくつか方法があると思うんですが、思いついたのは以下の2つです。

1. ブラウザの検証ツールを使う
2. useRefを使ってJavaScriptで取得する

1の方法はやや原始的ですが、ブラウザの検証ツールを開き、HTMLからSVGタグを探してコピーします。
2の方法はスマートですが、例えば以下のように自分でUIをつけなければいけません。

```react
import { useRef } from 'react'

function SvgLogo() {

  const svgRef = useRef()
  return (
    <>
      <button onClick={() => console.log(svgRef.current.outerHTML)} >
        get SVG
      </button>
      <svg ref={svgRef} >
        <MySvgParts />
      </svg>
    </>
  )
}
```

これでボタンを押せばSVGのテキストをコンソールに取得できます。onClickの処理でファイルにしてダウンロード処理すればOKです。

# 終わりに

結構複雑になっちゃって、それならいいライブラリがあるよなんて意見もあるかもしれませんが、サンデープログラマーの戯言と思って笑ってください。