本文へスキップ

cat で読めるピクセルアート ​

tenten は、クリックするキャンバスを持たないピクセルアートツールです。スプライトは 1 ピクセル 1 文字のテキストの行で、ディスク上のファイルはその行を含む JSON です。スプライトは cat で読め、どのピクセルが動いたかは git diff で分かり、文字列を書けるものなら何でも描けます。シェルスクリプトでも、MCP 越しのコーディングエージェントでも構いません。Phaser、Pixi、debuff が読み込めるシートを書き出し、.aseprite の読み書きもできます。npx -y -p @debuffjp/tenten tenten で試せます。

スライムを 1 匹どうぞ。6 行のテキストと、そこに 1 ピクセルのアウトライン操作を加えて tenten がレンダリングした PNG です。2 枚あるのは、2 フレーム目が同じグリッドを 1 ピクセル下にずらしたものだからです。

..0000..
.011110.
01122110
01111110
.011110.
..3..3..
上のグリッドを 12 倍でレンダリングした 2 フレーム。2 枚目は 1 ピクセル低い
上のグリッドに 4 色目でアウトラインを付け、12 倍でレンダリングしたもの。文字の 0 はパレットのインデックス 1 なので、胴体はパレットの最初の色です。

. は透明です。0 から 9、次に a から z、次に A から Z がパレットのインデックス 1 から 61 に対応します。4 色あればスライムが描けますし、61 色のパレットでほとんどのピクセルアートをまかなえます。それを超えるとファイルはバイト列に切り替わります。

ファイル ​

保存すると、これが slime.tt の全体です。

json
{
 "format": "tenten/1",
 "name": "slime",
 "size": [8, 6],
 "palette": ["#2d6a4f", "#52b788", "#b7e4c7", "#081c15"],
 "layers": [{ "name": "base" }],
 "frames": [100, 100],
 "tags": [{ "name": "idle", "from": 0, "to": 1, "direction": "forward" }],
 "slices": [],
 "cels": [
  { "layer": "base", "frame": 0, "rows": ["..0000..", ".011110.", "01122110", "01111110", ".011110.", "..3..3.."] },
  { "layer": "base", "frame": 1, "rows": ["........", "..0000..", ".011110.", "01122110", "01111110", ".011110."] }
 ]
}

レイヤー、表示時間付きのフレーム、タグ、スライスは Aseprite のファイルにあるものと同じです。tenten は .aseprite のインポートとエクスポートに対応しているので、人間が同じスプライトを Aseprite で開いて、また戻すこともできます。違いは、こちらがテキストだということです。スライムの足を 1 ピクセルずつ外側に動かしてコミットすると、diff は変わった 1 行だけになります。

diff
-   "rows": ["..0000..", ".011110.", "01122110", "01111110", ".011110.", "..3..3.."]
+   "rows": ["..0000..", ".011110.", "01122110", "01111110", ".011110.", ".3....3."]

同じコミットに PNG があれば、バイナリの塊として表示されるだけです。フォーマットのページは 1 画面に収まります。

画面なしで描く ​

描くときも同じグリッドを使います。tenten の MCP サーバーは操作のバッチを受け取り、いちばん安いのが grid です。

json
{ "ops": [
  { "op": "grid", "rows": ["..0000..", ".011110.", "01122110", "01111110", ".011110.", "..3..3.."] },
  { "op": "outline", "c": 4 },
  { "op": "addFrame" }, { "op": "shift", "dx": 0, "dy": 1, "frame": 1 },
  { "op": "tag", "name": "idle", "from": 0, "to": 1 }
] }

描画呼び出しの結果はまたテキストのフレームなので、操作を送った側は同じ返信の中で自分が描いたものを見られます。ゲームを作っているコーディングエージェントは、仮の敵を描いて確認し、1 ピクセル直してシートを書き出すまでをスクリーンショットの往復なしにこなせます。人間はプレビューページを開いて、その様子を眺められます。

tenten のプレビューページ。スライムのフレームと、Aseprite ファイルから読み込んだスプライトのアニメーションが再生されている
プレビューは localhost 上のページで、変更のたびに描き直されます。ツールはなく、編集は API を通して行います。

grid のほかには、おなじみのプリミティブがあります。ピクセル、線、矩形、楕円、塗りつぶし、1 ピクセルのアウトライン、シフト、反転、色の置換、そして 2:1 のアイソメトリックな足元を描く diamond です。バッチは 1 回の取り消し単位で、どれかの操作が失敗すれば全体が巻き戻ります。同じ操作は CLI と TypeScript ライブラリのどちらからでも使えて、MCP サーバーは .mcp.json の 1 行です。

json
{ "mcpServers": { "tenten": { "command": "npx", "args": ["-y", "-p", "@debuffjp/tenten", "tenten-mcp"] } } }

npm create debuff で作ったプロジェクトにはこの行が最初から入っていて、assets/ の .tt ファイルは設定なしでスプライトシートになります。

このアイデアの出どころ ​

私たちは Overbuffed という小さなサバイバルゲームを、エージェントが Aseprite の Lua スクリプト経由ですべてのアートを描く形で作りました。スプライトを手描きしたパス(コミット 3720b6e)では、エージェントは各スプライトを Lua ファイルの中に # と . の行として書き、Aseprite の API はおよそ 10 個しか触っていません。スプライト・画像・フレームを作る・セルを置く・ピクセルを描いて読む・色を作る・表示時間を設定する・保存する・シートを書き出す・閉じる、の 10 個です。それ以外はすべて自前のコードでした。

つまりスクリプトにとって Aseprite は、フレーム付きのピクセルバッファとファイル形式とエクスポーターであり、スプライトはすでにテキストでした。tenten はそれを、テキストを第一級にして、およそ 1,200 行の TypeScript で作ったものです。Aseprite に着想を得ていますが、クローンではありません。ブラシも、選択ツールも、GUI もありません。

ここで示していないこと ​

陰影のある 32 px の手描きピクセルアートが、モデルにとってテキストのグリッドとしてちゃんと読めるかどうかは、まだ測っていません。Overbuffed のアートはフラットで、スプライトごとに 1 色で、小さいものです。テキストのグリッドが足りなくなるのは 48 px か 8 色を超えたあたりだろうと見ていて、そこから先は PNG のレンダリングが引き継ぐはずです。次のゲームで、実際にどこで切り替わるかを記録するつもりです。

このツールはできてから数日です。.aseprite のコーデックは、Aseprite が書いたファイルと、tenten が書いたものを実際の Aseprite が書き出した結果の両方でテストしています。ただし古いファイル、タイルマップレイヤー、sRGB 以外のカラープロファイルはカバーしていません。

何か描いて、送り返してください。

npx -y -p @debuffjp/tenten tenten show slime.tt