DotCraft は、写真をドット絵(ピクセルアート)に変換するブラウザツールです。縮小・減色・ディザリングの 3 段で、ゲームボーイやファミコンのような配色に寄せます。処理はすべて端末の中で完結します。
#🎯 縮小するだけではドット絵にならない
最初にやってみたのは「画像を小さくして、そのまま拡大する」でした。結果はただのぼやけた画像です。
理由は 2 つありました。
- 縮小時の補間で色が混ざり、中間色が大量に生まれる
- 拡大時の補間でドットの角が丸まる
ドット絵は「少ない色数」と「はっきりした四角い画素」でできています。つまり必要なのは、縮小・減色・補間なしの拡大の 3 つでした。
#📉 縮小はボックス平均で
縮小はブラウザ標準の描画に任せず、自前でボックス平均を取っています。出力の 1 マスに対応する元画像の矩形をまるごと平均する方式です。
標準の縮小はブラウザによってアルゴリズムが違い、結果が環境ごとにぶれます。自分で平均を取れば、どこで見ても同じマス目になります。
#🎨 メディアンカットで代表色を選ぶ
減色はメディアンカットを使いました。k-means より速く、色の偏った画像でも破綻しにくい手法です。
考え方はこうです。
- 全画素を 1 つの箱に入れる
- いちばん幅の広いチャンネル(R/G/B のどれか)を探す
- その軸で中央値を境に箱を 2 つに割る
- 目標の色数になるまで 2 と 3 を繰り返す
function channelRange(box: Box): { ch: number; range: number } {
// R / G / B それぞれの min と max を取り、
// いちばんレンジの広いチャンネルを返す
}**「散らばりの大きい方向から割る」**というのが肝です。ランダムに割ると、ほとんど使われていない色に代表色を 1 つ消費してしまいます。幅で選べば、実際に使われている色域に代表色が配られます。
k-means と違って反復も乱数もないので、同じ画像からは必ず同じパレットが出ます。ドット絵は「作り直したら色が変わった」が困る用途なので、決定的であることを優先しました。
#🌗 ディザリング ── 少ない色で階調を作る
色数を 4 色や 16 色まで落とすと、グラデーションが縞になります(バンディング)。これを散らすのがディザリングです。2 種類入れました。
Bayer(順序ディザ) は、あらかじめ決まった閾値の行列を画面に敷き詰めて、そのマスの閾値と比べて色を決めます。規則的な網目が出るのが特徴で、レトロゲームの見た目に近くなります。
Floyd-Steinberg(誤差拡散) は、丸めたときに出た誤差を隣の画素に配ります。
function diffuse(rgb, gw, gh, x, y, er, eg, eb, f) {
const gi = (y * gw + x) * 3
rgb[gi] += er * f
rgb[gi + 1] += eg * f
rgb[gi + 2] += eb * f
}右に 7/16、左下に 3/16、下に 5/16、右下に 1/16 という配分で誤差を送ります。捨てるはずの端数を隣に押し付けるだけの発想ですが、これで少ない色数でも滑らかな階調に見えます。
どちらが良いかは用途次第で、規則的な模様が欲しければ Bayer、自然な階調なら Floyd-Steinberg です。両方選べるようにしました。
#🕹️ レトロ機のパレット
ゲームボーイ(緑 4 階調)、ファミコン、PICO-8 などのパレットを用意しました。これらは実機の仕様として色が決まっているので、そこへ最近傍で写せば当時の見た目になります。
function nearest(palette: RGB[], r: number, g: number, b: number): RGB自由に減色するモードと、固定パレットに寄せるモードの両方を持たせています。前者は写真の色を活かし、後者は「その機種の絵」になります。
#🔍 拡大は補間なしで
最後の拡大はニアレストネイバー(最近傍)です。Canvas なら次の一行で切り替わります。
ctx.imageSmoothingEnabled = falseこれを忘れると、せっかく作ったドットが拡大時にぼやけて全部台無しになります。ドット絵ツールで一番効く一行がこれでした。
#😓 苦労したところ
明るさとコントラストの前処理が要った。 元の写真をそのまま減色すると、暗い写真は暗いまま潰れます。減色の前に明るさとコントラストを調整する工程を入れて、階調が残っている状態にしてから色を削る順序にしました。
「何色になったか」を見せる。 減色は結果を見ても何色使われているか分かりません。変換後にユニーク色数を数えて表示するようにしました。
export function countUniqueColors(result: PixelResult): number数字が出るだけで、パレットのつまみを触る意味が分かるようになります。
グリッドサイズと色数のどちらを触るべきか迷わせない。 つまみが多いと止まります。プリセット(ゲームボーイ / ファミコン / PICO-8 など)を先に選ばせて、細かい調整は下に畳む構成にしました。
#🔭 今後の拡張
- アニメーション GIF のフレームごと変換
- パレットの手動編集(色を差し替える)
- スプライトシート書き出し
- 輪郭を強調してから減色するモード(キャラクター向け)
#💡 このサービスから言えること
「小さくする」と「ドット絵にする」は別物でした。 縮小・減色・補間なし拡大の 3 つが揃って初めてドット絵になります。どれか 1 つ欠けると、ぼやけた画像か、縞の出た画像にしかなりません。
そして減色では、決定的であることを性能より優先しました。メディアンカットは k-means より少しだけ粗いですが、同じ入力から必ず同じ結果が出ます。作り直しが前提の道具では、そちらのほうが価値があります。
[ ./next_action ]
読んだら、 DotCraft を実際に動かす。
この開発ログは DotCraft をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。