この記事はtraP新歓ブログリレー2024 10日目の記事です。
こんにちは@d_etteiu8383です。@eyemono.moeでもあります。
本記事では自作のアイコンメーカー (https://icon.eyemono.moe) の開発について紹介します。
アイコン
弊サークルで使用されているメッセージングアプリtraQでは、ユーザーアイコンを設定することが可能です。このアイコンは本ブログ上でも表示されていますね。
対面イベント以上にtraQでのコミュニケーションが活発なtraQでは、アイコン画像が部員を象徴する重要な要素となっています。
私が現在使用しているアイコンは2020年に(グラフィック班によるクリスタ講習会中に)作成したものです。
その後「エンジニアたるもの自分のアイコンも管理しやすい形式にすべきだ」との思いからSVG形式で作成し直し、オリジナルデータをFigmaで管理するようになり、Figmaのcomponent機能を使って色々なバリエーションを作成できるようになりました。
せっかくならWebアプリ化してしまおうということで、アイコンメーカーを作成することにしました。
実はアイコンメーカー自体は2022年に一度作っていたのですが、メンテナンス性の悪さから放置してしまっていました。今回はアイコンメーカーを再構築し、より使いやすく、メンテナンスしやすいものにしました。
開発
使用技術
- package.json
- SolidStart
- Solidをベースにしたフレームワークです。ReactにおけるNext.js、VueにおけるNuxt.jsのようなものです。以前アイコンメーカーを作成した際もSolidを使用していましたが、当時は静的なサイトとして作成していました。今回はサーバーサイドでの画像生成を行いたかったため、SolidStartを使用しました。
SolidもSolidStartも、ReactやVueと比較するとまだまだマイナーなフレームワークですが、とても使いやすいと感じているので今後も積極的に使用していきたいです。(ただしSolidStartはまだまだ開発段階であり、ドキュメントも不足しているのでオススメはしない)
- Solidをベースにしたフレームワークです。ReactにおけるNext.js、VueにおけるNuxt.jsのようなものです。以前アイコンメーカーを作成した際もSolidを使用していましたが、当時は静的なサイトとして作成していました。今回はサーバーサイドでの画像生成を行いたかったため、SolidStartを使用しました。
- Vercel
- ホスティング
- Vercelといえば静的ファイルのホスティングがメジャーな印象ですが、Vercel Functionsを利用することによりサーバーサイドの処理も行うことができます。SolidStart自体もVercel Functionsに対応しているため、追加のライブラリを使用することなくVercelにデプロイすることができます。
- より正確には、SolidStartはv0.4.0からサーバーフレームワークとしてNitroを採用しており、このNitroがVercel Functionsに対応しているため、Vercelにデプロイすることができます。NitroはNuxt.jsで使用されていることでも有名ですね。
実装方針
基本的な仕組みとしては、アイコンの目, 口, 髪等の各パーツのsvgをそれぞれをcomponentとし、これらを組み合わせてアイコンを生成しています。
パーツの種類(口の形など)や色はcontextで管理し、各パーツcomponent内でこれを参照して色の変更等をしています。
↓目のパーツ例
<Portal>
コンポーネントについては後述します。
各パーツの切り替えには<Switch>
コンポーネントを使用しています。contextから参照したパーツ種類を<Parts>
コンポーネント内で出し分けている形です。
以下に示す<Icon>
コンポーネントがアイコンの本体となっているコンポーネントで、この中で<Parts>
コンポーネントを呼び出して各パーツを表示しています。
<Portal>
コンポーネントの利用
Solidには、ReactにおけるcreatePortal
と同様の機能を持つ<Portal>
コンポーネントが用意されています:Portal - SolidDocs。
例えば先述した目のパーツは、ただ肌部分の上に乗っているわけではなく、「白目部分」 < 「髪部分」 < 「まつ毛部分」のレイヤー順序になっており、間に別パーツである髪部分があるため2箇所に分けて表示する必要があります。
このほかにも、髪パーツでは「後ろ髪」「顔にかかる影」「前髪」の3パーツをそれぞれ異なるレイヤーで表示する必要があります。
このようなパーツも<Portal>
コンポーネントを使用することで、単一コンポーネント内の記述で複数のレイヤーにまたがるパーツを表示することができました。
この<Portal>
コンポーネントの表示先を、先述の<Icon>
コンポーネント内に記述しています(<g id="eye-upper-target" />
等の部分)。
サーバーサイドでの画像生成
アイコンメーカーの本体はクライアントサイドで動作し、画像の保存処理等もクライアントサイドで可能になっています。
しかし、利便性を考えると、URLから直接アイコンをimage/png
やimage/svg+xml
として取得できると便利そうです。
本アイコンメーカーでは、API Routesとして/image
へのGETリクエストを受け付け、サーバーサイドで動的に画像を生成して返すようにしています。
このAPI Routes内では、先述の<Icon>
コンポーネントをsolidのrenderToString()
関数で文字列に変換して返しています。
アイコンメーカーページでは、パーツ種類や色情報をlz-stringを利用してクエリパラメータ内に保存しているのですが、このパラメータを/image
エンドポイントへのGETリクエストにそのまま渡すことで、サーバーサイドでもそのパーツ・色のアイコンを生成することができます。
Node.jsで動作する画像処理ライブラリのsharpを使用したpngへの変換等も行っています。
サーバーサイドでの<Portal>
コンポーネントの描画
<Portal>
コンポーネントを使用した別要素への描画は、クライアントサイドでelement.appendChild()
を使用することで実現されています。そのため、サーバーサイドで実行されるrenderToString()
内では、通常の<Portal>
コンポーネントをそのまま使用しただけでは描画されません。
↓SSR時は何もしない<Portal>
コンポーネント君
そのため本アイコンメーカーでは、<Portal>
コンポーネントをラップした独自の(闇の)コンポーネント<ServerPortal>
を作成し、サーバーサイドでの描画を可能にしています。
絶対もっとスマートに書けそうですが、とりあえず動いているので...
とにかくこれで何とかサーバーサイドでの画像生成に成功しました。これによりアイコンメーカー自体のOGPでも動的に生成したアイコン画像を使用できるようになりました。
アイコンメーカーページのURLをSNS等に貼ると、その時の編集内容がOGPに表示されます。
まとめ
Webアプリとして動作するアイコンメーカーを作成しました。SolidStartを使用したことで、クライアントサイドとサーバーサイドの両方での画像生成を実現することができました。
皆さんもぜひ自分だけのアイコンメーカーを作ってみてはいかがでしょうか。
最後までお読みいただきありがとうございます。明日の新歓ブログリレー2024担当者は@comaviusです。楽しみ~