ツイッターとかで見かけるゲーム自己紹介カードを作成するアプリをつくってみる

自分は最近スプラトゥーン3にハマっています。
もともとはリアルの友達と一緒にやっていたのですが、なかなか時間の都合が合わず、一人で遊ぶことも多くなりました。
そこでTwitterなどで一緒に遊ぶ人を探したりしていたのですが、その時見かけたのが自己紹介カードでした。

自分のプレーヤーネームやランクなどの情報を記載したカードを作り、ハッシュタグをつけて共有して、ゲームを一緒に遊んでくれる人とつながるわけです。
自己紹介カードは記入欄が空のテンプレート画像を有志で公開してくださっている方がいたりします。

その中で気になったのが[Web上でフォームから入力するだけで自己紹介カードを作成することができるアプリ](https://gamewith.jp/splatoon3/article/show/364005)です。
非常に便利そうでかつ、実装が気になったので自分で作ってみたいと思います。

## 利用するライブラリ等

- [React.js](https://ja.reactjs.org/)
    - v18.2.0
    - UI作成のために利用します
        - Reactでなくても可能ですが、使い慣れているのでこちらを採用しました
- [Konva(react-konva)](https://konvajs.org/)
    - v8.3.14(konva), v18.2.3(react-konva)
    - 2DのCanvas JavaScriptライブラリ
    - 自己紹介カードの描画や画像への変換などを行います
    - 今回はReactアプリケーションのため、[react-konva](https://github.com/konvajs/react-konva)を利用します

## 実装

今回の実装ではフォームのスタイリングは行いません。
あくまで自己紹介カード作成アプリっぽいものができたら良しとします

- フォーム上で自己紹介カードに記載する情報を入力できるようにする
- 入力した情報を元に自己紹介カードの内容が更新される
- 作成した自己紹介カードを画像ファイルとして保存できる

一旦プレイヤーネームを入力して反映させるまでを実装してみます。

### フォームの作成

`useState`を利用した単純な入力フォームを作成します。

```jsx
function App() {
    const [playerName, setPlayerName] = useState("ここに名前を入力");

    return (
        <input
            value={playerName}
            onChange={(e) =>
                setProfile(e.target.value)
            }
        />
    )
}
```

### 自己紹介カードの表示

自己紹介カードのテンプレート画像をまずは用意します。
Figmaで雑に作成しました。

![card.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215011/10be7a14-6627-0a8d-1ac2-6770436d89f8.png)

こちらをKonvaを使ってCanvasで描画し、その上に入力したプレイヤーネームを表示させていきます。

まずはKonvaを使って画像を表示させていきます。

```jsx
<Stage width={1000} height={500}>
    <Layer>
      <Image image={cardImage} width={1000} height={500} />
    </Layer>
</Stage>
```

`react-konva`の`Stage`, `Layer`, `Image`を利用していきます。
元画像のサイズが`1000x500`なのですが、表示もそのまま`1000x500`としています。

### 自己紹介カードに入力した内容を表示する

ここからは入力したプレイヤーネームを自己紹介カードに反映させていきます。
`react-konva`の`Text`を使って`playerName`の文字列を表示させます。

```jsx
<Text
    text={playerName}
    x={325}
    y={25}
    fontSize={24}
    fontStyle="bold"
    align="center"
    verticalAlign="middle"
    offsetY={-6}
    height={75}
    width={300}
/>
```

テキストのフォントサイズや配置位置を指定します。
プレイヤーネームは中央に配置したいので`align="center"`, `verticalAlign="middle"`としています。
微妙に中央より高い位置になっており、ベースラインを調整するようなプロパティがなかったので`offsetY={-6}`で微調整しています。

高さや幅は名前記入箇所のサイズを指定します。`x`, `y`座標も同様です。
元画像のサイズとImageコンポーネントの指定したサイズが異なる場合は異なるので注意。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215011/65c3fa64-ac3a-bb49-c34f-555c69b34452.png)

ここまで作成すると実際に入力した内容が自己紹介カードに反映されることが確認できると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215011/055dac11-a891-9b69-3338-f999ac28beb1.png)

### ローカル画像ファイルの表示

自己紹介カード左側の画像は、ローカルの画像ファイルを選択して表示されるようにします。

```js
const getCrop = (image, size) => {
  const width = size.width;
  const height = size.height;
  const aspectRatio = width / height;

  let newWidth;
  let newHeight;

  const imageRatio = image.width / image.height;

  if (aspectRatio >= imageRatio) {
    newWidth = image.width;
    newHeight = image.width / aspectRatio;
  } else {
    newWidth = image.height * aspectRatio;
    newHeight = image.height;
  }

  return {
    x: (image.width - newWidth) / 2,
    y: (image.height - newHeight) / 2,
    width: newWidth,
    height: newHeight,
  };
};
```

上の`getCrop`は、選択したローカルの画像を自己紹介カードの画像配置箇所に収まるよう、クロップするために必要な値を取得するための関数です。

画像中心を基準としてクロップされるようになります。

```jsx
const [image, setImage] = useState(null);
const [crop, setCrop] = useState({
    x: 0,
    y: 0,
    width: 250,
    height: 300,
});

const handleImageFile = (e) => {
    const url = URL.createObjectURL(e.target.files[0]);
    const img = new window.Image();
    img.src = url;
    img.onload = () => setCrop(getCrop(img, { width: 250, height: 300 }));
    setImage(img);
};

return (
    //...
    <input type="file" accept="image/*" onChange={handleImageFile}></input>
    //...
    {image && (
        <Image
          image={image}
          x={25}
          y={125}
          width={250}
          height={300}
          crop={crop}
        />
    )}
    //...
)
```

`handleImageFile`で画像ファイルを選択したあとに、画像ファイルを元にImageインスタンスを作成します。
画像のクロップに必要な値は、元画像のサイズを取得する必要があるため、`img.onload`のコールバック関数内で`getCrop`を呼び出します。

そうすると、ローカル画像ファイルを選択後、画像中心を基準としてクロップされた画像が自己紹介カードに表示されます。

↓元画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215011/b69af0ab-240c-914b-ae59-9f3f4a348994.png)
↓画像が反映された自己紹介カード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215011/ebefbd21-7474-034c-5bc6-c4c0fdb43fd8.png)

### 自己紹介カードを画像ファイルとしてDLできるようにする

すべての項目のフォームが用意できてませんが、基本的にはほとんど実装内容は変わらないので割愛します。
自己紹介カードを画像ファイルとして保存できるようにしましょう。

```jsx
const downloadURI = (uri, name) => {
    const link = document.createElement("a");
    link.download = name;
    link.href = uri;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
};

const handleExportImage = () => {
    const uri = stageRef.current.toDataURL();
    downloadURI(uri, "card.png");
};

const stageRef = useRef(null);

return (
    //...
    <button onClick={handleExportImage}>Download</button>
    //...
    <Stage width={1000} height={500} ref={stageRef}>
    //...
)
```

`useRef`を利用して`handleExportImage`内で自己紹介カード(Canvas)をPNG形式のdata URIに変換します。
`downloadURI`で`card.png`としてローカルに保存できるように一時的に`a`タグを設置してクリックした状態にします。
このようにすることでDownloadボタンを押したときに自己紹介カードを保存することができます。

↓実際にダウンロードした画像
![card (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215011/f2ab283b-ab34-e6cb-d093-569d9fc05b5c.png)

### 終わりに

今回は個人的に気になっていた、ゲーム自己紹介カードを作成するアプリを簡易的ではありますが作成してみました。
実装しなかった項目のフォームやスタイリング、自己紹介カード自体のデザインを充実させるなど色々やりたいことが残ってしまいましが、
基本的には最低限の機能の実装はできたかなと思います。参考になりましたら幸いです。