自分は最近スプラトゥーン3にハマっています。
もともとはリアルの友達と一緒にやっていたのですが、なかなか時間の都合が合わず、一人で遊ぶことも多くなりました。
そこでTwitterなどで一緒に遊ぶ人を探したりしていたのですが、その時見かけたのが自己紹介カードでした。
自分のプレーヤーネームやランクなどの情報を記載したカードを作り、ハッシュタグをつけて共有して、ゲームを一緒に遊んでくれる人とつながるわけです。
自己紹介カードは記入欄が空のテンプレート画像を有志で公開してくださっている方がいたりします。
その中で気になったのがWeb上でフォームから入力するだけで自己紹介カードを作成することができるアプリです。
非常に便利そうでかつ、実装が気になったので自分で作ってみたいと思います。
利用するライブラリ等
-
React.js
- v18.2.0
- UI作成のために利用します
- Reactでなくても可能ですが、使い慣れているのでこちらを採用しました
-
Konva(react-konva)
- v8.3.14(konva), v18.2.3(react-konva)
- 2DのCanvas JavaScriptライブラリ
- 自己紹介カードの描画や画像への変換などを行います
- 今回はReactアプリケーションのため、react-konvaを利用します
実装
今回の実装ではフォームのスタイリングは行いません。
あくまで自己紹介カード作成アプリっぽいものができたら良しとします
- フォーム上で自己紹介カードに記載する情報を入力できるようにする
- 入力した情報を元に自己紹介カードの内容が更新される
- 作成した自己紹介カードを画像ファイルとして保存できる
一旦プレイヤーネームを入力して反映させるまでを実装してみます。
フォームの作成
useState
を利用した単純な入力フォームを作成します。
function App() {
const [playerName, setPlayerName] = useState("ここに名前を入力");
return (
<input
value={playerName}
onChange={(e) =>
setProfile(e.target.value)
}
/>
)
}
自己紹介カードの表示
自己紹介カードのテンプレート画像をまずは用意します。
Figmaで雑に作成しました。
こちらをKonvaを使ってCanvasで描画し、その上に入力したプレイヤーネームを表示させていきます。
まずはKonvaを使って画像を表示させていきます。
<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
の文字列を表示させます。
<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コンポーネントの指定したサイズが異なる場合は異なるので注意。
ここまで作成すると実際に入力した内容が自己紹介カードに反映されることが確認できると思います。
ローカル画像ファイルの表示
自己紹介カード左側の画像は、ローカルの画像ファイルを選択して表示されるようにします。
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
は、選択したローカルの画像を自己紹介カードの画像配置箇所に収まるよう、クロップするために必要な値を取得するための関数です。
画像中心を基準としてクロップされるようになります。
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
を呼び出します。
そうすると、ローカル画像ファイルを選択後、画像中心を基準としてクロップされた画像が自己紹介カードに表示されます。
自己紹介カードを画像ファイルとしてDLできるようにする
すべての項目のフォームが用意できてませんが、基本的にはほとんど実装内容は変わらないので割愛します。
自己紹介カードを画像ファイルとして保存できるようにしましょう。
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ボタンを押したときに自己紹介カードを保存することができます。
終わりに
今回は個人的に気になっていた、ゲーム自己紹介カードを作成するアプリを簡易的ではありますが作成してみました。
実装しなかった項目のフォームやスタイリング、自己紹介カード自体のデザインを充実させるなど色々やりたいことが残ってしまいましが、
基本的には最低限の機能の実装はできたかなと思います。参考になりましたら幸いです。