軽量なツールの設計は「何をしないか」から始まる — Yomogi の具体例で考える
2026-04-19
はじめに
自作のドット絵エディタ Yomogi を開発していて、一番時間をかけたのは機能の実装ではありませんでした。「この機能は入れない」と決めることに、もっとも頭を使いました。
レイヤーは何枚にするか。キャンバスの上限はいくつか。タイルサイズは自由入力にするかプリセットにするか。こうした問いに対して「増やさない」「広げない」と決断するたびに、コードは単純になり、UI は分かりやすくなりました。
この記事では、Yomogi で実際に設けた制約を具体例として挙げながら、「制約を決めることも立派な設計である」という考え方を整理します。
軽量ツールの設計では、何を「できるようにするか」よりも何を「できなくするか」が重要です。Yomogi のレイヤー数・キャンバスサイズ・タイルサイズなどの制約を実例に、拡張性を追わない設計のメリットと判断基準をまとめます。
「制約の設計」とは
設計というと「拡張性を担保する」「変更に強い構造にする」という方向がまず思い浮かびます。チーム開発や長期運用のプロダクトでは、その方向が正解になることも多いでしょう。
しかし、個人開発の軽量ツールでは事情が異なります。ユーザーは限られ、開発者は自分一人、スコープは狭い。このような環境で拡張性を追うと、使われない抽象化のためにコードが膨れ上がり、結果として開発もメンテナンスも止まります。
さらに、ユーザ視点に立ってみます。高機能なものは既製品を使う方が良いという場合が多いと思います。私自身、自分のやりたいことに対して、機能が多すぎてわかりづらいと感じることがこれまでも多くありましたから、そのような体験をできる限り減らしたいということも考えています。
以前の記事では、個人開発でオーバーエンジニアリングをした経験を書きました。あの記事は「技術のコスト」を知るという話でした。今回はその逆の視点で、コストを払わないために制約を先に決めるというアプローチについて書きます。
オーバーエンジニアリングの記事はこちら⬇️

拡張性を重視する設計と制約を重視する設計は、目的が違うだけでどちらも「設計」です。何を目的にするかで正解は変わります。
Yomogi の制約一覧
まず、Yomogi に設けた主な制約を一覧にします。
| 対象 | 制約 | 値 |
|---|---|---|
| キャンバスサイズ | 上限 | 128×128 |
| レイヤー数 | 固定 | 3(背景・中景・前景) |
| タイルサイズ | プリセット | 8, 16, 24, 32, 48, 64 |
| タイルマップ | 上限 | 64×64 セル |
| デフォルトパレット | 色数 | 28色 |
| 描画ツール | 種類 | 5(ペン・消しゴム・塗りつぶし・スポイト・移動) |
| SVG エクスポート | 方式 | 1ピクセル = 1rect、最適化なし |
これらはどれも「もっと増やせる」ものばかりです。レイヤーは10枚にできるし、キャンバスは1024×1024にもできます。あえてそうしなかった理由を、以降で具体的に見ていきます。
実例1 3レイヤー固定
タイルマップエディタを設計するとき、真っ先に悩んだのは「レイヤーを何枚にするか」でした。
ユーザーが自由にレイヤーを追加・削除できる方式にすれば柔軟性は上がります。しかし、そうすると描画順の管理(Z-index)、レイヤー一覧 UI のスクロール、レイヤー間のドラッグ並べ替えなど、実装しなければならない機能が一気に増えます。
結論として、背景・中景・前景の3枚に固定しました。理由は以下の3点となります。
- 描画順を
background → middle → foregroundの固定順にでき、Z-index の管理コードが不要 - 「背景に地面、中景にキャラ、前景に葉っぱ」のように役割を説明しやすく、シンプル
- レイヤー選択タブが常に3つで、UI が複雑化しない
描画コードも制約の恩恵で単純になっています。
for (const layerId of LAYER_IDS) {
const layer = state.layers[layerId]
if (!layer.visible) continue
for (const sprite of layer.sprites) {
const img = spriteImageMap.get(sprite.projectId)
if (!img) continue
const dx = offsetX + sprite.col * cellSize
const dy = offsetY + sprite.row * cellSize
const dw = (img.width / state.tileSize) * cellSize
const dh = (img.height / state.tileSize) * cellSize
ctx.drawImage(img, dx, dy, dw, dh)
}
}
LAYER_IDS は ['background', 'middle', 'foreground'] の固定配列です。動的なレイヤー管理が不要なので、レイヤーの追加・削除・並べ替えに関するコードが一切ありません。
- Z-index 管理コードが不要で、描画順のバグが原理的に発生しない
- UI が常に3タブ固定で、初見のユーザーでも迷わない
- レイヤーの CRUD 操作を実装しなくて済む
- 3枚では足りないユーザー(例: 複雑なパララックスを組みたい人)には対応できない
「3枚では足りない」ケースは確かに存在します。しかし Yomogi の対象は「ちょっとしたドット絵を描きたい人」であり、複雑なマップエディタを求めるユーザーには別のツールがあります。ターゲットを絞ったからこそ、この制約が成り立ちます。
実例2 キャンバス・タイルマップのサイズ上限
キャンバスの上限を 128×128 ピクセル、タイルマップの上限を 64×64 セルに設定しています。
この制約には実用上の根拠があります。
ピクセルアートは本質的に小さな画像です。昔のゲームのスプライトは 8×8や16×16、32×32が基本でした。128×128でもピクセルアートとしてはかなり大きい部類に入ります。
上限を決めたことで、データ構造は Uint32Array(128 * 128) で済みます。チャンク分割や仮想スクロールのような大規模データ向けの仕組みは不要です。Canvas 2D のパフォーマンスもこの範囲なら問題になりません。
上限がなければ「1000×1000 のキャンバスで描画が遅い」というバグ報告に対応する羽目になります。上限を設けることで、そのカテゴリの問題を丸ごと排除できます。
実例3 タイルサイズのプリセット値
タイルサイズは自由入力ではなく、[8, 16, 24, 32, 48, 64] のプリセットから選ぶ方式にしました。
自由入力にすると「3」や「7」のような半端な値が入力される可能性があります。すると、タイルの境界線がピクセルの中間に来てにじむ、大きなスプライトとの整数比が崩れるなど、エッジケースの対応が必要になります。
プリセットにしたことで得られたものは明確です。
- 入力バリデーションが「配列に含まれるか」の一行で済む
- すべてのプリセット値が 8 の倍数なので、スプライトとの整数比が保証される
- UI がドロップダウン1つで完結し、数値入力フィールドのエラーハンドリングが不要
制約を決めるときの判断基準
Yomogi の各制約を振り返ると、判断の軸は3つに整理できます。
ターゲットユーザーの用途に合っているか
Yomogi は「ちょっとしたドット絵を描いてみたい人」向けのツールです。プロ向けの機能セットは最初から目指していません。ターゲットが明確であれば、「この人には不要」と言い切れる機能が見えてきます。
Yomogiを最初に作ろうと思ったきっかけ自体、私が個人開発で必要なツールだったことが理由なので、最小限の機能でも実用に足るという判断をしました。
制約によってどれだけの複雑さを排除できるか
レイヤーを3枚に固定することで Z-index 管理が消え、キャンバスに上限を設けることでパフォーマンス問題のカテゴリごと消えました。制約1つで排除できるエッジケースや実装コストが大きいほど、その制約の価値は高いです。
後から緩められるか
制約は後から緩めることができます。レイヤーを3枚から5枚に増やすのは、5枚から3枚に減らすより遥かに簡単です。迷ったら厳しい方を選び、ユーザーのフィードバックを見て緩めるかどうかを判断すれば十分です。
「制約」と「手抜き」は違います。制約は考えた結果として「やらない」と決めること。考えずにやらなかったのは単なる未実装です。判断の根拠、特に設計の意図・目的を言語化できるようにすることが大事です。
まとめ
- 軽量ツールでは「何をしないか」を決めることが設計の中心になる
- Yomogi ではレイヤー数・キャンバスサイズ・タイルサイズ・エクスポート方式のすべてに意図的な上限を設けた
- 制約はコードの単純さ・UI のわかりやすさ・パフォーマンスの安定を同時にもたらす
- 拡張性を重視する設計と制約を重視する設計は、目的が違うだけでどちらも「設計」である
- 迷ったら厳しい方を選び、後からフィードバックを見て緩める
制約を決めるのは地味な作業ですが、軽量ツールにとってはそれが一番クリエイティブな瞬間だと思います。



