ランダムに表示されるモニョっとした形状のSVGを作るまでの道のり

## これは何

- リロードする度に形状が変わるSVGを作ってみました
- ステップバイステップで作り方を説明します
- 完成系は以下です

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/7c2b0e3c-0abf-c991-240c-2f154d09b9e4.gif)

なお、コードはGitHubに上げています。

https://github.com/xrxoxcxox/qiita-random-svg

## はじめに：なぜそんなものを作ったか

ある日遊びでUIのモックアップを作っていて、雰囲気を出すために背景にモニョっとした形状をちりばめていました。
イメージとしてはこんな具合です。

https://www.pinterest.jp/pin/525443481518545492/

普通にIllustratorやFigmaで作ったSVGを書き出して配置しても良いのですが……なんだか微妙に物足りない気持ち。
少し考えているうちに「リロードする度に微妙に形状が変わったら面白いんじゃないか？」と~~要らんことを~~思いついたのです。

## SVGの理解

ランダムに作る前に、そもそもどういうルールで記載されているのかを知らないとお話になりません。
私はこちらの記事に助けていただきました。

https://qiita.com/takeshisakuma/items/777e3cb0a54ea7b1dbe7

## 見本の用意

いきなりコードだけでSVGを描き始めるのは無謀に感じられました。
そのためまずはFigmaで見本を用意して、それを少しずつ変えていく作戦でいきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/fe0aeb91-285e-3bee-865f-51df94c3374c.png" alt="" width="400" />

こんな感じでそれっぽい形状を用意して、`Copy as SVG`を実行→エディタに貼り付け。
現時点のコードはこのようになっています。

```html
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path
    d="
      M 320 128
      C 220 178 258 50 134 32
      C 10 14 -20 238 100 258
      C 220 278 158 378 268 368
      C 378 358 420 78 320 128
      Z"
    fill="#C4C4C4"
  />
</svg>
```

見やすくするためにインデントや改行を整形しています。
また、理解をしやすくするために各座標に小数点が入らないように最初に綺麗に作っておきました。

更に、これを単純なReact componentにしておきます。
（私が単によく使うのがReactなだけで、VueでもSvelteでもなんでも大丈夫です）

```typescript:randomSvg.tsx
export const RandomSvg = (): JSX.Element => {
  return (
    <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path
        d="
          M 320 128
          C 220 178 258 50 134 32
          C 10 14 -20 238 100 258
          C 220 278 158 378 268 368
          C 378 358 420 78 320 128
          Z"
        fill="#C4C4C4"
      />
    </svg>
  );
};
```

## ハンドルの座標を定数に置き換える

先ほどSVGのコードにはたくさんの数字が並んでいますが「アンカーポイントの座標」か「ハンドルの座標」のどちらかでしかありません。
後で説明しますが、まずはハンドルの座標から定数化していきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/869d8ead-1783-7cf1-402a-a5f9c8be351d.png" alt="" width="400" />

現在選択中のハンドルの座標は[220, 178]で、コードで言うと1つ目の`C`の先頭から1つ目と2つ目の数値です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/1f01d020-46f9-f2fa-e0a5-a86e0058b567.png" alt="" width="400" />

同様にこちらのハンドルの座標は[258, 50]で1つ目の`C`の先頭から3つ目と4つ目の数値です。

これらを踏まえて、まずはこのように置き換えました。

```diff_typescript:randomSvg.tsx
  export const RandomSvg = (): JSX.Element => {
+   const firstPointX1: number = 220;
+   const firstPointY1: number = 178;
+   const firstPointX2: number = 258;
+   const firstPointY2: number = 50;
    return (
      <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path
          d={`
            M 320 128
-           C 220 178 258 50 134 32
+           C ${firstPointX1} ${firstPointY1} ${firstPointX2} ${firstPointY2} 134 32
            C 10 14 -20 238 100 258
            C 220 278 158 378 268 368
            C 378 358 420 78 320 128
            Z`}
          fill="#C4C4C4"
        />
      </svg>
    );
  };
```

この調子で、気合いで全ての数値を設定します。

```diff_typescript:randomSvg.tsx
  export const RandomSvg = (): JSX.Element => {
    const firstPointX1: number = 220;
    const firstPointY1: number = 178;
    const firstPointX2: number = 258;
    const firstPointY2: number = 50;
+   const secondPointX1: number = 10;
+   const secondPointY1: number = 14;
+   const secondPointX2: number = -20;
+   const secondPointY2: number = 238;
+   const thirdPointX1: number = 220;
+   const thirdPointY1: number = 278;
+   const thirdPointX2: number = 158;
+   const thirdPointY2: number = 378;
+   const fourthPointX1: number = 378;
+   const fourthPointY1: number = 358;
+   const fourthPointX2: number = 420;
+   const fourthPointY2: number = 78;
    return (
      <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path
          d={`
            M 320 128
            C ${firstPointX1} ${firstPointY1} ${firstPointX2} ${firstPointY2} 134 32
-           C 10 14 -20 238 100 258
-           C 220 278 158 378 268 368
-           C 378 358 420 78 320 128
+           C ${secondPointX1} ${secondPointY1} ${secondPointX2} ${secondPointY2} 100 258
+           C ${thirdPointX1} ${thirdPointY1} ${thirdPointX2} ${thirdPointY2} 268 368
+           C ${fourthPointX1} ${fourthPointY1} ${fourthPointX2} ${fourthPointY2} 320 128
            Z`}
          fill="#C4C4C4"
        />
      </svg>
    );
  };
```

## アンカーポイントの座標を計算する

次はアンカーポイントの座標ですが……こちらは既に出ている座標から求めた方が良いです。
具体的には、以下のアンカーポイントのX座標は`firstPointX2`と`secondPointX1`の平均をとり、Y座標も同様に`firstPointY2`と`secondPointY1`の平均をとった場所に位置するからです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/76afd738-af49-235f-74dc-d0e0097fa191.png" alt="" width="400" />

それぞれのハンドルの座標の平均以外の位置にアンカーポイントを配置してしまうと、次のような見た目になって出力されてしまう可能性もあります。

| 凹んでしまう | 角ができてしまう |
| --- | --- |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/d5a44c6f-5eb3-dc94-3fae-c03155b661f3.png" alt="" width="400" /> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/4e385233-ccd6-26ec-45a3-541ccbeaa0a2.png" alt="" width="400" /> |

これはこれで面白い形状かもしれませんが、今回の狙いからは外れてしまうのでNG。

結果的にコードは次のようになります。

```diff_typescript:randomSvg.tsx
  export const RandomSvg = (): JSX.Element => {
    const firstPointX1: number = 220;
    const firstPointY1: number = 178;
    const firstPointX2: number = 258;
    const firstPointY2: number = 50;
    const secondPointX1: number = 10;
    const secondPointY1: number = 14;
    const secondPointX2: number = -20;
    const secondPointY2: number = 238;
    const thirdPointX1: number = 220;
    const thirdPointY1: number = 278;
    const thirdPointX2: number = 158;
    const thirdPointY2: number = 378;
    const fourthPointX1: number = 378;
    const fourthPointY1: number = 358;
    const fourthPointX2: number = 420;
    const fourthPointY2: number = 78;
+   const firstPointX: number = (firstPointX2 + secondPointX1) / 2;
+   const firstPointY: number = (firstPointY2 + secondPointY1) / 2;
+   const secondPointX: number = (secondPointX2 + thirdPointX1) / 2;
+   const secondPointY: number = (secondPointY2 + thirdPointY1) / 2;
+   const thirdPointX: number = (thirdPointX2 + fourthPointX1) / 2;
+   const thirdPointY: number = (thirdPointY2 + fourthPointY1) / 2;
+   const fourthPointX: number = (fourthPointX2 + firstPointX1) / 2;
+   const fourthPointY: number = (fourthPointY2 + firstPointY1) / 2;
    return (
      <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path
          d={`
-           M 320 128
-           C ${firstPointX1} ${firstPointY1} ${firstPointX2} ${firstPointY2} 134 32
-           C ${secondPointX1} ${secondPointY1} ${secondPointX2} ${secondPointY2} 100 258
-           C ${thirdPointX1} ${thirdPointY1} ${thirdPointX2} ${thirdPointY2} 268 368
-           C ${fourthPointX1} ${fourthPointY1} ${fourthPointX2} ${fourthPointY2} 320 128
+           M ${fourthPointX} ${fourthPointY}
+           C ${firstPointX1} ${firstPointY1} ${firstPointX2} ${firstPointY2} ${firstPointX} ${firstPointY}
+           C ${secondPointX1} ${secondPointY1} ${secondPointX2} ${secondPointY2} ${secondPointX} ${secondPointY}
+           C ${thirdPointX1} ${thirdPointY1} ${thirdPointX2} ${thirdPointY2} ${thirdPointX} ${thirdPointY}
+           C ${fourthPointX1} ${fourthPointY1} ${fourthPointX2} ${fourthPointY2} ${fourthPointX} ${fourthPointY}
            Z`}
          fill="#C4C4C4"
        />
      </svg>
    );
  };
```

## ランダムに変わるようにする

現段階のSVGはこのようになっています。
当たり前ですが、リロードしてもうんともすんとも言いません。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/84a477f2-9ec7-0547-9ebc-3b11df41b091.gif)

それぞれのX1, Y1, X2, Y2がランダムに変わるようにコードを書きます。

```diff_typescript:randomSvg.tsx
  export const RandomSvg = (): JSX.Element => {
+   const randomize = (min: number, max: number) => {
+     return Math.random() * (max - min) + min;
+   };
-   const firstPointX1: number = 220;
-   const firstPointY1: number = 178;
-   const firstPointX2: number = 258;
-   const firstPointY2: number = 50;
-   const secondPointX1: number = 10;
-   const secondPointY1: number = 14;
-   const secondPointX2: number = -20;
-   const secondPointY2: number = 238;
-   const thirdPointX1: number = 220;
-   const thirdPointY1: number = 278;
-   const thirdPointX2: number = 158;
-   const thirdPointY2: number = 378;
-   const fourthPointX1: number = 378;
-   const fourthPointY1: number = 358;
-   const fourthPointX2: number = 420;
-   const fourthPointY2: number = 78;
+   const firstPointX1: number = randomize(0.6, 1.2) * 220;
+   const firstPointY1: number = randomize(0.6, 1.2) * 178;
+   const firstPointX2: number = randomize(0.6, 1.2) * 258;
+   const firstPointY2: number = randomize(0.6, 1.2) * 50;
+   const secondPointX1: number = randomize(0.6, 1.2) * 10;
+   const secondPointY1: number = randomize(0.6, 1.2) * 14;
+   const secondPointX2: number = randomize(0.6, 1.2) * -20;
+   const secondPointY2: number = randomize(0.6, 1.2) * 238;
+   const thirdPointX1: number = randomize(0.6, 1.2) * 220;
+   const thirdPointY1: number = randomize(0.6, 1.2) * 278;
+   const thirdPointX2: number = randomize(0.6, 1.2) * 158;
+   const thirdPointY2: number = randomize(0.6, 1.2) * 378;
+   const fourthPointX1: number = randomize(0.6, 1.2) * 378;
+   const fourthPointY1: number = randomize(0.6, 1.2) * 358;
+   const fourthPointX2: number = randomize(0.6, 1.2) * 420;
+   const fourthPointY2: number = randomize(0.6, 1.2) * 78;
    const firstPointX: number = (firstPointX2 + secondPointX1) / 2;
    const firstPointY: number = (firstPointY2 + secondPointY1) / 2;
    const secondPointX: number = (secondPointX2 + thirdPointX1) / 2;
    const secondPointY: number = (secondPointY2 + thirdPointY1) / 2;
    const thirdPointX: number = (thirdPointX2 + fourthPointX1) / 2;
    const thirdPointY: number = (thirdPointY2 + fourthPointY1) / 2;
    const fourthPointX: number = (fourthPointX2 + firstPointX1) / 2;
    const fourthPointY: number = (fourthPointY2 + firstPointY1) / 2;
    return (
      <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path
          d={`
            M ${fourthPointX} ${fourthPointY}
            C ${firstPointX1} ${firstPointY1} ${firstPointX2} ${firstPointY2} ${firstPointX} ${firstPointY}
            C ${secondPointX1} ${secondPointY1} ${secondPointX2} ${secondPointY2} ${secondPointX} ${secondPointY}
            C ${thirdPointX1} ${thirdPointY1} ${thirdPointX2} ${thirdPointY2} ${thirdPointX} ${thirdPointY}
            C ${fourthPointX1} ${fourthPointY1} ${fourthPointX2} ${fourthPointY2} ${fourthPointX} ${fourthPointY}
            Z`}
          fill="#C4C4C4"
        />
      </svg>
    );
  };
```

これで完成です。
冒頭に貼ったgifのように、リロードする度に形状が変わるSVGを生成することができました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/7c2b0e3c-0abf-c991-240c-2f154d09b9e4.gif)

あとは上手いこと背景に敷き詰めたり色を変えたりすれば、良い感じにスタイルを組めるはずです。
なんかもう少し良い方法があるような気がしますし、ランダム度合いやアンカーポイントの数の違いをpropsで制御できるくらいのコンポーネントにできれば尚使い勝手が良いように思いますが、今回はここまで。

最後まで読んでいただいてありがとうございました。