【React/TypeScript】今年のクリスマスはreact-konvaで雪を降らす

https://qiita.com/advent-calendar/2023/future

[フューチャー Advent Calendar 2023](https://qiita.com/advent-calendar/2023/future)の6日目の記事です。
昨日は、 @yu1Ro5 さんの[CoreData に登録したデータを JSON で出力したい！](https://qiita.com/yu1Ro5/items/e30e21232e3e5820bf0d)でした。

2022年6月にフューチャーに入社し、React/TypeScriptのWebアプリ開発をメインにしている[starswirl_k](https://github.com/starswirl)と申します。
フューチャーでの記事、アドベントカレンダーへの投稿は今回が初めてですが、よろしくお願いします。

# はじめに

普段React/TypeScriptにて開発を行っていますが、実務でCanvasを使ったアプリケーションを作る機会がなかったので、今回のタイミングで何か作ってみることにしました。
せっかくなのでクリスマス感を出すものを作ります🎄🎁

調査をしてみたところ、Canvas上での描画を容易に行うためのJavaScriptフレームワークの一つとしてKonvaが使いやすそうでした。

https://konvajs.org/

Reactを使用してKonvaを作成する為のライブラリのreact-konvaもあり、今回はこちらを使って、Canvasを使ったアプリ開発を行ってみました。

https://github.com/konvajs/react-konva

# 完成したもの

[DEMOサイト](https://quiet-daffodil-412b13.netlify.app/)はこちら

画面上の`UPLOAD FILE`ボタンを押し、クリスマスに取った画像を読み込みます。
そのあとに`LET IT SNOW`ボタンを押すと用意した画像上に雪を降らすことができます。

![chrome-capture-2023-11-3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67904/1fbc19b0-48f5-fe24-ef44-30a3bbe823dd.gif)

https://github.com/starswirl/2023-christmas


低確率で流れ星が出るようにしたので、もし見つけたら教えてください！

# 実装内容
基本的には[ソース](https://github.com/starswirl/2023-christmas)の内容通りですが、ポイントを押さえて説明します。
実装は下記の順番で行いました、順を追って説明します。

1. 環境構築
1. キャンバス作成
1. SHAPESの追加
1. アニメーションロジック作成
1. 雪玉追加
1. 画像追加の実装
1. スタイル修正
1. demoのデプロイ


## 環境構築

create-react-appのコマンドでReact/Typescriptのアプリを作成しました。

Reactの公式ドキュメントで紹介される方法ではなくなってしまったようなので、使い方は省略します。
詳細は下記記事を参考にしてください。

https://zenn.dev/nekoya/articles/dd0f0e8a2fa35f


### 依存パッケージのインストール

依存パッケージは下記をインストールしました。
今回お試しするKonva系ライブラリとボタン等のスタイル用にmaterial-uiのライブラリを追加しました。
```
yarn add react-konva konva @material-ui/core @material-ui/icons
```

## キャンバス作成
Konvaの[概要のページ](https://konvajs.org/docs/overview.html)に記載がありますが、基本的には下記のツリー構造でStage->Layer->Shapeの順の階層構造になっていて、これを元に作成を進めました。

```
              Stage
                |
         +------+------+
         |             |
       Layer         Layer
         |             |
   +-----+-----+     Shape
   |           |
 Group       Group
   |           |
   +       +---+---+
   |       |       |
Shape   Group    Shape
           |
           +
           |
         Shape
```

実装する上では、react-konvaのREADME.mdに記載された、[Example](https://github.com/konvajs/react-konva#example)がわかりやすかったです。

https://github.com/konvajs/react-konva

# 雪玉の作成

雪を降らすためには、まず落ちる雪の玉を作成しないといけません。
雪の玉は階層構造のShapeに当たる部分です。
ShapeはKonvaにて複数用意されていて、React-konvaではShapeを[jsx](https://kinsta.com/jp/knowledgebase/what-is-jsx/)に直接宣言できるのでかなり直感的で良いなと思いました。

https://konvajs.org/docs/react/Shapes.html

今回は雪の玉なので、Circleで作成しました。

```html
<Circle
  x={0}
  y={0}
  radius={5}
  fill="white"
  opacity={1}
/>
```

# アニメーションロジック作成

時間経過とともに下に落ちていく処理を追加するので、タイマーを作成し雪玉の位置を移動させていく処理を作りました。
タイマーの作り方は下記記事を参考に実装しています。

https://zenn.dev/akhr_s/articles/065e18ab3c4883

```
useInterval(snowMoveLogic, 10);
```

## 雪玉の落ちる処理
雪は基本的に、等速に落ちるイメージなのでとくに考えず、useInterval実行時の度に下に移動する処理を入れています。

## 左右に揺れ動くの雪玉の処理
ただ落ちるだけだと雪の落ちる動きを表現できないので、今回は左右に揺れ動く処理を入れました。
三角関数で表現し、useInterval実行時の度に加算した値をsin,cosで処理しています。

```js
Math.cos(x)
```

# 雪玉追加
作った雪の玉を複数落とす処理です。
ブラウザの画面サイズによって、雪玉の量を変えたかったので
下記を参考に画面サイズを動的に取得するHooksを追加しています。

https://zenn.dev/kenghaya/articles/6020b6192dadec

雪玉の生成
```js
const newList = [...Array(ySnowBallQuantity)].flatMap((_e, i) =>
  [...Array(xSnowBallQuantity)].map((_e, j) => {
    const xShift = i % 2 === 1 ? 0 : X_SNOW_DIFF / 2;
    const xRundom = parseInt(`${Math.random() * 50}`);
    const yRundom = parseInt(`${Math.random() * 50}`);
    return {
      x: INIT_X + X_SNOW_DIFF * j + xRundom + xShift,
      y: INIT_Y + Y_SNOW_DIFF * i + yRundom,
      radius: SNOW_SIZE,
    };
  })
);
```

主な処理としては、算出した雪玉の数の分配列を宣言し、その情報を元に複数の雪玉を画面上に描画しています。
等間隔過ぎると違和感があったので、雪玉の配置をある程度ランダムにズラす処理も入れています。

# 背景画像変更ボタンの実装
好きな画像に雪を降らせたかったので、追加しました。
写真の大きさに合わせて画面いっぱいに表示する処理に苦戦。。。意外と実装方法がわからず困りました。

```js
const onChange = (e: any) => {
  setFilePath(URL.createObjectURL(e.target.files[0]));
  const img = new Image();
  img.onload = () => {
    const ratio = height / img.height;
    setImageDimensions({ width: img.width * ratio, height: img.height });
  };
  img.src = URL.createObjectURL(e.target.files[0]);
};
```


実装としては、Imageのロードが完了した時に発火するイベントonloadにてイメージのサイズを取得することで画面いっぱいに表示する処理を追加しています。

https://lab.syncer.jp/Web/API_Interface/Reference/IDL/GlobalEventHandlers/onload/

# スタイル修正
主なコンポーネントはボタンぐらいですが、このままだともちろんショボいのでマテリアルUIで簡単にスタイル修正しました。
あとは画像の位置等を微調整しました。
ここでアプリ完成です。

# DEMOのデプロイ

自宅サーバーを撤去してしまったので、レンタルサーバーでデプロイするか迷いましたが実は一度も無料デプロイ系のサービスを利用したことがなかった為、netlifyを利用してみました。

https://www.netlify.com/

gitに公開したReactアプリケーションに対してそのまま`yarn build`してデプロイしてくれるので、お手軽過ぎて感動しました！
もっと早く使っていればよかった。。。

再度の記載になりますが、[DEMOサイト](https://quiet-daffodil-412b13.netlify.app/)はこちら

# 作ってみた感想
konvaを使った感想としては、かなり直観的で使いやすかったし、
実装に起こす際もDEMOが大量にあったのでサンプルから実装を真似して試せたのもよかったです。

他にも会議のボードみたいなものも作れそうなので、また機会があればぜひ使ってみたいなと思いました。

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

明日は、 @tng527 さんの CodeBuild上のdocker in docker でハマった話です。
お楽しみに。