🥳 前置き:君は最高のバウンディングボックスを知っているか 🥳
「バウンディングボックス is 何?」って方、これです↓
出典: 複雑GUIの会( https://scrapbox.io/guiland/ )
イラレやFigmaのようなデザイン系のアプリには100%用意されてる、あのハコです。
知らん人にはマジどうでもいい話かもなのですが、GUI沼界隈においてはこのバウンディングボックスはHelloWorldであり鬼門であり永遠に辿り着けない理想の終着駅です。
界隈の人間が新しいプロダクト作る時は、とりあえずフルスクラッチでバウンディングボックスから作り始めたりするし、勉強会で集まるとバウンディングボックスだけで2時間くらい語れる人がウヨウヨでできます。
それくらいみんな大好きバウンディングボックス。既に若干引かれていそうな気もするけど、この記事はそんな憎愛を前提に温かい目でお読みください
🥳 作ったもの:Awesome Bounding Box 🥳
おや... 普通にCanvaの劣化版みたいなよくあるエディタですね。
でも、ちゃんとバウンディングボックスを使って画像の移動・リサイズ・回転ができますね 無駄に入れ子できたりします
ランダムな画像の配置しかできない時点で ではあるのですが、クソアプリとしてはいまいちパンチに欠けますね
そんな時は...
なんかありますね。押しましょう。
🎉🎉🎉 LET'S PARRRRRTY 🎉🎉🎉
以上!!! ご清聴ありがとうございました!!!
🥳 よくわかる解説 🥳
以下はこういうアプリどうやって作るのか気になるまじめな方向けの解説です。みんなでGUI沼に堕ちよう
▼ ソースコードはこちら
2日で作ったクソアプリなので、細部のグダグダはお許しください
まずは技術スタックから
逃げと言われるかもですが、今回は完全バニラJS(フレームワークやライブラリを一才使わないJS)ではないです。後述するけど、バニラだと状態管理がちょっと辛い。
■ ベースの環境
- Vite v5
- TypeScript v5.2
ここら辺は何の工夫もないです。そろそろ別な環境も手を出してみたい
■ ライブラリ
- React v18 ... DOM部分の管理。正直Jotaiを使うためだけに入れてる感はある
- Jotai v2.6 ... 状態の保持・更新とそのためのロジックを全部置くところ
- Kuma UI v1.5 ... 使ってみたかった。Zero Configで使えるEmotionだと思うとすごく楽
- transformation-matrix v2.15 ... 座標の行列計算やってくれる。DOMMatrixを便利にした感じ
描画そのものは生のCanvas(CanvasRenderingContext2D)です。Pixi.jsやThree.jsといった描画のためのライブラリは使いません。
🥳 どうやってつくる? 🥳
「Canvas お絵描き JavaScript」とかでググるとたくさん出てくる巷のチュートリアル・やってみた系の記事を読むと、大抵初っ端からマウスドラッグに合わせて線を引くようなコードが出てきます。
まちがいというか...これ自体がダメなわけではないのですが、お勧めしません。
CanvasはDOMのような後から要素を動かしたり消したりするような機能がないので、このアプローチで何かを作ろうとするとかなり早い段階で詰みます。
最初の一歩のチュートリアルとしては有益なのものの、必ず詰むことがわかっているので凝ったものを作りたいなら避けた方が良いです。
Canvasはそれ自体にレイヤーやオブジェクトのような情報を持たないため、多少面倒でも最初に「Canvasの全ての状態を入れる場所」を作ることがポイントです。
その上で、
- Canvasの状態を更新する関数
- 例:オブジェクトを新規に追加する / ペンの座標を追加する...など
- この関数は操作ごとにたくさんあって良い
- 状態一式をもとにCanvasを再描画する関数
- この関数は基本的に1つだけ存在する
の2種類のロジックを書きます。どちらも原則副作用を持たない純粋な関数です。
...とここまでくると現代のwebのエンジニアなら既視感あるはずです。ReactやFluxの考え方と一緒ですね
ゴリゴリのパフォーマンスチューニングが必要なゲームや表現を優先するアート系・クリエイティブコーディングの世界だとちょっと事情は変わってきますが、普通のアプリケーションとしてCanvasを使う場合には、この考え方に則っておくのが安牌かと思います 🥧
今回のコードだと↓のあたりにCanvasの全状態をおいています
https://github.com/yuneco/awesome-bounding-box/tree/main/src/state
このstateをもとにCanvas描画を呼び出しているのが↓のhookです。
https://github.com/yuneco/awesome-bounding-box/blob/main/src/useDrawStage.ts
// Canvas描画の入り口になるhook
// 何らかの状態変更があると、このhookが再レンダリングされる
export const useDrawStage = (ctx?: CanvasRenderingContext2D) => {
// 全レイヤーのツリー情報
const root = useAtomValue(layerTreeAtom);
// 選択やフォーカスといった描画に必要な付加情報
const options = useAtomValue(drawOptionAtom);
// アニメーションが必要かの判定
const shouldAnimate =
// using party mode
options.boundingOptions?.kind === "party" &&
// bounding box is displayed
options.selectedId !== undefined;
// アニメーション状態ならrequestAnimationFrameで再描画するhook
useAnimation(shouldAnimate);
// レイヤーで使われている画像がロードされたら再描画するhook
useLayerImage();
if (!ctx) return;
const draw = () => {
wrapCtx(ctx, () => {
ctx.resetTransform();
drawBgCheck(ctx);
// 描画関数本体の呼び出し
drawLayer(ctx, root, options);
});
};
// 🌟 ここで描画。ここ以外からはCanvasの描画は一切行わない
draw();
};
面倒ですが、慣れちゃうと考え方としては簡単ですね
🥳 PARRRTY MODEのつくりかた 🥳
これだけだとクソアプリ部分の解説になっていないので、パーリーなバウンディングボックスを作る部分も紹介しましょう。と言っても技術的に特別なことは特にないです。
まず、通常のバウンディングボックスがこちら。
ちょっと長いですが、このファイルからは以下の2つの関数をexportしています。
// 対象要素のサイズを受け取って、バウンディングボックスのハンドルのレイアウトを返す関数
export type CustomBoundingBoxLayout = (
size: Size,
scale: number
) => BoundingBoxLayout;
// 対象要素を受け取ってバウンディングホックスノ描画を行う関数
export type CustomBoundingBoxDraw = (
ctx: CanvasRenderingContext2D,
layer: Layer,
scale: number,
option: BoundingDrawOption
) => void;
バウンディングボックスは描画したら終わりではなく、カーソルを乗せたときの当たり判定を自力で行わないといけません。このため、描画関数とセットで配置情報を提供する関数も用意しています。
対するパーリーバウンディングボックスがこちら↓
通常のバウンディングボックスと同じ型の2関数をexportしさえすれば、中身はやりたい放題です。
具体的には以下の🎉パーリー🎉を開催しています
- 回転ハンドルを回転する にする
- リサイズハンドルを震えるパロットにする
- ボーダーを振動させながら7色にアニメーションのさせる
ここまでわかればあとは好きなバウンディングホックスをつくり放題ですね
ぜひフォークして世界に一つだけのバウンディングボックスを作ってみてください
🥳 感想 🥳
- 分かっていたけど開発時間の9割は普通のバウンディングボックス作る部分に消えた
- ほんとは「バウンドするバウンディングボックス」とか「ロシアンルーレットなバウンディングボックス」とかもつくりたかったんだ。誰か作ってくれ
- とはいえ、2日でそれなりのものが作れたのは満足
- Jotai超いいです
- Kuma UIは全然使いこなせてないけど好きです
- Copilotは神。XYとか上下左右とかの対称性があるコードは1つ書けば他はほぼ間違いなく補完してくれるのでGUI開発やグラフィック系の実装速度が段違いに速くなりました
明日は @Kan_Kikuchi さんですノシ