Reactで、SVGをぬるぬる動かす

# はじめに
ReactでSVGを使ってお絵描きした要素をクリックして、要素を動かしたい要件があります。

先日下記の記事で、プロパティ経由でサイズを変更するというのをやりましたが、カクカク動くのもだるいし、手作り感がすごいので、今回はライブラリを使います。

https://qiita.com/yo16/items/df72b6ac904c42242395

使うライブラリは、framer-motion。正式名称が、framerなのか、motionなのか、framer-motionなのか、実際よくわかりません。すみません。ただ、インストールするのは、`npm install framer-motion`なので、ここではframer-motionと呼びます。

https://www.framer.com/motion/

## ゴール
こんなものを作りました。
Elm1のスイッチをクリックすると、Elm2、Elm3、Elm4、Elm5が動くよという単純なサンプルです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/78464168-2697-5e08-1071-b853bb136bc4.png)

動作確認用のページはこちらに置きました。

https://small-pieces-sandbox.onrender.com/contents/react_svg_linking_prac

ソースはgithubのこちら。

https://github.com/yo16/react_svg_linking_prac



# 解説
## Elm1: スイッチ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/f87fb871-6a89-edee-3a0a-5ba964e51026.png)

Elm1は、クリックされたら、呼び出し元へクリックされた旨を通知するボタンです。
これはよくあるパターンで、呼び出し元からプロパティの`onClick`へ関数を渡して、クリックされたら下位はそれを実行するというシンプルなものなので割愛します。framer-motionも使ってません。
```typescript: MyElm1.tsx（interfaceだけ）
interface MyElm1Props {
    onClick: (e: React.MouseEvent<SVGElement>) => void
}
```

## Elm2: 即時連動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/fb082f00-154d-d986-32e3-e915141f902d.png)

Elm2は、Elm1がクリックされたらすぐ発火する即時連動型です。ぶっちゃけこれだけあればいいかもしれません。
単純ですが、framer-motionの説明を少し濃いめで書きます。

```TypeScript:MyElm2.tsx
import { useEffect } from "react";
import { useAnimate } from "framer-motion";   // ＜１＞ import

const stableColor: string = "#A28B55";
const changedColor: string = "#ff0";

interface MyElm2Pros {
    isSwitchOn: boolean;
}
export function MyElm2({
    isSwitchOn
}: MyElm2Pros) {
    const [scope, animate] = useAnimate();    // ＜２＞ useAnimate

    useEffect(() => {    // ＜３＞useEffectでSwitchON/OFFで動作
        if (isSwitchOn) {
            // 点灯
            animate("rect", {fill: changedColor}, {duration: 1});
        } else {
            // 消灯
            animate("rect", {fill: stableColor}, {duration: 1});
        }
    }, [isSwitchOn]);

    return (
        <>
            <g
                ref={scope}
            >
                <rect
                    x={0}
                    y={0}
                    width={150}
                    height={30}
                    rx={8}
                    ry={8}
                    fill={stableColor}
                />
                <text
                    x={28}
                    y={21}
                    fill={"#000"}
                >Elm2: {isSwitchOn? "ON": "OFF"}</text>
                <text
                    x={160}
                    y={21}
                    fill={"#000"}
                >スイッチと即時連動</text>
            </g>
        </>
    )
}
```
＜１＞で、＜２＞で使う`useAnimate`をimport。

＜２＞の`useAnimate`フックの戻り値は、１項目目でアニメーションを設定する要素のスコープ。returnの中にある`<g>`要素に`ref={scope}`と設定しています。２項目目は、それを操る`animate`関数。キモです。

`animate`関数は、＜３＞の`useEffect`で`isSwitchOn`が変更されたタイミングで呼び出されます。

`animate()`の第一引数は、scope内の動かす**対象**の要素。ここでは"rect"としていますが、cssのようなフィルターが使えます。
第二引数は、animateした後の**最終系の属性**。framer-motionはhtmlやsvgのタグの属性値を徐々に変化させるのですが、その属性とゴール値をここで設定しています。
第三引数は、animateの動作に関するオプション、**変更の仕方**です。`duration`は変化にかかる秒数です。

つまり `animate("rect", {fill: changedColor}, {duration: 1});`の
場合は、**対象**はscopeの中の"rect"で、**最終系**はfillの値を`changedColor`（＝#ff0）へ、**変更の仕方は**`duration`（変化にかかる時間）を1秒間で、という指示。
なお**初期値**は、JSXで、`<rect fill="#A28B55" />`などと指定しておけばOK。

私の場合は、１つの要素に対して点灯と消灯の２つの変化を、スイッチONかOFFかという状況によって使い分けているということになります。

### ちなみに`useAnimate()`以外にも
framer-motionの[公式HP](https://www.framer.com/motion/)には、下記のようなサンプルがたくさんあります。
```JSX
<motion.div
  className="box"
  initial={{ opacity: 0, scale: 0.5 }}
  animate={{ opacity: 1, scale: 1 }}
  transition={{ delay: 0.5, duration: 0.8 }}
/>
```
これは、
- `motion.div`によって、**対象**はこのdiv
- 変更前後の値は、**初期値**`{opacity:0, scale:0.5}`から、**最終系**`{opacity:1, scale:1}`へ
- **変更の仕方**は、0.5秒後に開始(delay)し、0.8秒間かけて(duration)、変化する

という意味です。
`useAnimate`と書き方はちょっと違いますが、内容は同じです。

こちらの方が、処理っぽくなくてJSXの中に直接かけるので便利ですが、複雑な処理（例えばif文や状況によって動かし方を変えたいとか）になると、JSXではかえって煩雑です。なので、この記事では、`useAction`を使った書き方で統一します。

公式HPには、[「`useAnimate()` vs `<motion.div />`」](https://www.framer.com/motion/use-animate/##useanimate-vs-motiondiv)というセクションがあり、複雑な処理とか、一時的に止めたり再生したりしたいとか、変更値を直接いじりたいとかの場合は、複雑なことができる`useAnimate()`を使えばいいよと書いてあります。私は先に`useAnimate()`を覚えたら、`<motion.div />`の書き方がすんなりわかったので、まずは`useAnimate()`を使っていればいいと思います。



## Elm3: バネのような動き
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/49dc185a-fa8b-a80f-35f9-f1040ab573eb.png)

アニメーションするというとよくあるバネの動きも、メニューの開け閉めのような形で使いたくなりそうだったので、メインスイッチに連動する形で作ってみました。

```TypeScript:MyElm3.tsx
import { useEffect } from "react";
import { useAnimate } from "framer-motion";

interface MyElm3Pros {
    isSwitchOn: boolean;
}
export function MyElm3({
    isSwitchOn
}: MyElm3Pros) {
    const [scope, animate] = useAnimate();

    useEffect(() => {
        if (isSwitchOn) {
            // 伸ばす
            animate(
                "rect",
                {height: 100},
                {
                    type: "spring",
                    stiffness: 700,
                    damping: 10
                }
            );
        } else {
            // 縮める
            animate(
                "rect",
                {height: 30},
                {
                    type: "spring",
                    stiffness: 1000,
                    damping: 30
                }
            );
        }
    }, [isSwitchOn]);

    return (
        <>
            <g
                ref={scope}
            >
                <rect
                    x={0}
                    y={0}
                    width={150}
                    height={30}
                    rx={8}
                    ry={8}
                    fill={"#86AB89"}
                />
                <text
                    x={28}
                    y={21}
                    fill={"#000"}
                >Elm3: {isSwitchOn? "open": "close"}</text>
                <text
                    x={160}
                    y={21}
                    fill={"#000"}
                >バネで伸縮</text>
            </g>
        </>
    )
}
```
Elm2で、`useAnimate()`周辺をだいぶ説明したので、その話は割愛。大まかには同じです。違いは、`animate()`の引数です。

伸ばすときのコードを再掲します。
```TypeScript:MyElm3.tsxのuseEffectの一部
// 伸ばす
animate(
    "rect",
    {height: 100},
    {
        type: "spring",
        stiffness: 700,
        damping: 10
    }
);
```
`animate()`の、**対象**が"rect"で、**最終系**は"height=100"、ここまではElm2と同じです。**変更の仕方**で、`type="spring"`と指定しています。

`stiffness`はバネの堅さ。堅いバネを伸ばした後に戻るスピード、縮めた後に伸びるスピードを想像すると、堅い方が速いですね。つまり、バネの伸び縮みのスピードだと思うとよいです。

`damping`は摩擦。この値が小さいと、いつまでもフニャフニャ動いている感じ。大きいと、あまりバネっぽくなくふわっと終わります。ふんわり変化させたいときに、この値を大きめにして使えばいいかなと。
動作確認用のElm3では、広げるときはdamping小さめ、縮めるときは大きめでやってます。

springはこの２つのパラメータでいいでしょう。もっと複雑に変化させる方法もたくさんありますが、現実的に使うところだけのいいとこどりで行きます。

他のtypeは、Tween（細かな調整ができるやつ・デフォルト）、Inertia（慣性）があります。スピード・加速の調整は「イージング」という分野です。詳しく知りたい方は、下記をご覧ください。（大体の場合、いらないと私は思う。でも面白い。）

https://ics.media/entry/18730/

https://easings.net/ja

## Elm4: 線をつなげる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/69aa0ee8-3899-1b19-709c-d6f048ef5eee.png)

できるとかっこいい感じのやつです。線を描くやつ。英語の筆記体とかできたらかっこいいですね。（自由曲線を定義するのが大変だけど）

なんと、ここまでの知識があれば、簡単です。
```TypeScript:MyElm4.tsx
import { useEffect } from "react";
import { useAnimate } from "framer-motion";

interface MyElm4Pros {
    isSwitchOn: boolean;
}
export function MyElm4({
    isSwitchOn
}: MyElm4Pros) {
    const [scope, animate] = useAnimate();

    useEffect(() => {
        if (isSwitchOn) {
            // 描く
            animate("path",
                { pathLength: 1 },
                { duration: 2.0 }
            );
        } else {
            // 消す
            animate("path",
                { pathLength: 0 }, 
                { duration: 0.5 }
            );
        }
    }, [isSwitchOn]);

    return (
        <>
            <g
                ref={scope}
            >
                <path
                    d={
                        "M 20 0 " +
                        "C 20 30, 10 130, -10 110 " +
                        "C -20 100, 0 70, 20 90 " +
                        "C 40 110, -40 205, -10 235 " +
                        "C 0 245, 30 235, 50 235"
                    }
                    stroke="#fa6"
                    strokeWidth={2}
                    fill="none"
                    pathLength="0"
                    id="cablePath"
                />
                <text
                    x={-20}
                    y={50}
                    fill={"#000"}
                >
                    <textPath
                        href="#cablePath"
                        startOffset="0.5"
                        textLength="150"
                    >
                        Elm4:伸びる線
                    </textPath>
                </text>
            </g>
        </>
    )
}
```
まず、SVG要素の`<path />`で自由曲線を定義します。これはプログラミングというか、数学です。パラメータ`d`で３次ベジエ曲線を書いてますが、この説明は割愛！
ご自分で調べたい方は、３次ベジエ、コントロールポイント(CP)、などのキーワードでググるとよいかもしれません。Illustratorで書いてSVG出力もできるかも。
ちなみに私は、紙に始終点とコントロールポイントを描いて座標を仮決めした後、`<path d=～ />`に数字を入れて、ブラウザで見て、微調整してこの線を描きました。泥臭すぎる。

さて、話を戻して、framer-motionで変化させる値は、`pathLength`です。`pathLength`は、SVGの`<path />`要素のパラメータの１つで、0～1で設定した値を0%～100%とみて、始点から何%まで線を描くかというものです。
つまり、`<path pathLength="0.0" />`を`<path pathLength="1.0" />`へ連続的に変更させることで、線が描かれているようなアニメーションになります。

そう思ってコードを見てみると、初期はJSXで`<path pathLength="0" />`となっていて、下記です。
```TypeScript:MyElm4.tsx（useEffectの部分）
    useEffect(() => {
        if (isSwitchOn) {
            // 描く
            animate("path",
                { pathLength: 1 },
                { duration: 2.0 }
            );
        } else {
            // 消す
            animate("path",
                { pathLength: 0 }, 
                { duration: 0.5 }
            );
        }
    }, [isSwitchOn]);
```
`pathLength`を変更しているだけです。描くときと消すときで、`duration`（かかる時間）を変えてます。
それだけ。

ちなみに線に沿わせた文字列「Elm4:伸びる線」で使った`<textPath />`は、SVG要素の話なので割愛！

## Elm5: 遅延連動
Elm5は、Elm4がつながったから色が変わったかのように見えますが、全然関係ないです。Elm4の`duration`（かかる時間）と、Elm5の`delay`（開始まで待つ時間）を一緒にすることで、Elm4の終わるタイミングとElm5の始まるタイミングが同じというだけです。"遅延連動"なんて書きましたが、"遅延連動っぽく見せる"です。

もうそこまで言ったら簡単だと思いますが、一応コードを。
```TypeScript:MyElm5.tsx
import { useState, useEffect } from "react";
import { useAnimate } from "framer-motion";

const stableColor: string = "#A28B55";
const changedColor: string = "#fa0";

interface MyElm5Pros {
    isSwitchOn: boolean;
}
export function MyElm5({
    isSwitchOn
}: MyElm5Pros) {
    const [scope, animate] = useAnimate();
    const [textOnOff, setTextOnOff] = useState<string>("OFF");

    useEffect(() => {
        if (isSwitchOn) {
            // 点灯
            animate("rect", {fill: changedColor}, {delay: 2.0});    // 2秒後に開始
            setTimeout(()=>{
                setTextOnOff("ON");
            }, 2.0*1000);
        } else {
            // 消灯
            animate("rect", {fill: stableColor}, {duration: 0.5});
            setTextOnOff("OFF");
        }
    }, [isSwitchOn]);

    return (
        <>
            <g
                ref={scope}
            >
                <rect
                    x={0}
                    y={0}
                    width={150}
                    height={30}
                    rx={8}
                    ry={8}
                    fill={stableColor}
                />
                <text
                    x={28}
                    y={21}
                    fill={"#000"}
                >Elm5: {textOnOff}</text>
                <text
                    x={160}
                    y={21}
                    fill={"#000"}
                >遅延して連動</text>
            </g>
        </>
    )
}
```
`animate()`のパラメータはもういいでしょう。第三パラメータの**変更の仕方**で`delay`を設定することで、開始タイミングを遅らせています。消すときは`delay`を設定していないので、すぐ消し始めます。

ついでに、"ON"と"OFF"という文字列を`<text />`で書いているので、`setTimeout()`を使って、開始タイミングと同時に`setTextOnOff()`を呼び出すように仕掛けてあります。ここも、さも連動しているかのようだけど、秒数を合わせているだけ。

ここでは簡単のために、Elm4は"2秒"で描き終わり、Elm5は"2秒後"に描き始め、textも"2秒"待って変更するのを、各々で設定していますが、本番では１か所で定義してパラメータで渡すべきです。2秒でなく3秒に変更！というときに変更箇所が多いとめんどくさいしミスるので。
それを"連動"と呼ぶかというと、やっぱり連動ではないですね。同じ値を設定しているだけ。


# まとめ
結局、「アニメーションしたい！」という要望は、「どのプロパティ値をいくつからいくつへ変更したい！」と言い換えて、それを`animate()`関数へ渡せばいいってことでした。
Elm2のところで書いた、**対象**、**初期値**、**最終系**、**変更の仕方**を指定するんだと思えば、そんなに難しくなくできると思います！

今回は、色、サイズ等のパラメーターを１つずつ変更しただけですが、複数を同時に変更すると、より驚くようなアニメーションができます。

ではよいSVGアニメーションライフを！