画像のトレース
image レイヤーはデザイン時のラスタ参照にすぎません。製造されるパネルはベクターレイヤーだけで構成されます。画像インスペクタの Convert to vector… アクション(インスペクタ → Imageを参照)は、ラスタをトレースパイプラインに通し、1 つ以上の PathLayer に変換して、固定のパネルパレットへマッピングします。このページでは、そのパイプラインを最初から最後まで解説します。
パイプラインの概要
HTMLImageElement
→ ImageData (trace-pipeline.ts: imageToImageData)
→ SVG string (trace-pipeline.ts: traceToSvg, via @image-tracer-ts/browser)
→ PathLayer[] (svg-to-path-layers.ts: svgToPathLayers) パイプラインは、明確な境界に沿って 2 つのファイルに分割されています。ブラウザ専用(本物の <canvas> の 2D コンテキストと ImageData グローバルを必要とする)と、純粋/DOM 非依存(文字列を解析し、直接ユニットテストできる)です。
1. ラスタ → ImageData
trace-pipeline.ts の imageToImageData() は、ソース画像の最長辺が最大 600px(MAX_TRACE_SIDE)になるようダウンスケールし(アップロードされた画像の解像度にかかわらずライブプレビューの応答性を保つため)、オフスクリーンの <canvas> に描画します。描画の前にキャンバスを不透明な白で塗りつぶすため、ソースの透明な背景が幻の色としてトレースパレットにサンプリングされることはありません。
2. ImageData → SVG
traceToSvg() は @image-tracer-ts/browser の ImageTracerBrowser.fromImageData() を呼び出します。カラーモードは 2 つあり、トレースダイアログの 3-color palette チェックボックスで切り替えます。
Palette モード(デフォルト、
usePalette: true) —CreatePaletteMode.PALETTEにより、固定のパネル 3 色(@zpd/coreのPALETTEを RGB に変換したもの)へ直接量子化します。Free モード(
usePalette: false) — 代わりにユーザーが選んだ 2〜8 色(numberOfColors)へ量子化します。どちらのモードで生成されたかにかかわらず、塗りはこの後ステップ 3 でパネルパレットにマッチングされます。
さらに 2 つのオプションがトレース自体を調整します。min shape outline(このピクセルしきい値を下回る小さなトレース領域を除外する)と blur radius(ノイズの多いソースラスタを滑らかにするための事前ブラー)です。どちらもトレースダイアログでスライダーとして公開され、250ms デバウンスされているため、スライダーをドラッグしても途中の値ごとに再トレースが走ることはありません。
Note
@image-tracer-ts/browser は、呼び出したときだけでなく、モジュール自身のトップレベルでグローバルの ImageData を読み取ります。ところが jsdom(テスト環境)はそれを提供しません。そのため trace-pipeline.ts は、この読み取りを実際にトレースが走るまで遅らせるよう、traceToSvg() の内側で 動的 な import() を使って読み込みます。静的インポートにすると、import.meta.glob が dialogs/* をすべて eager 読み込みする(拡張アーキテクチャを参照)ため、dialogs/ フォルダを間接的にインポートするすべてのテストがクラッシュします。
3. SVG → PathLayer[]
svg-to-path-layers.ts の svgToPathLayers(svg, target) は純粋です — キャンバスも DOM も使わない — ので、フィクスチャの SVG 文字列に対して直接テストできます。処理は次のとおりです。
トレースされた SVG の
<path d="..." fill="...">タグを、完全な XML パーサーではなく、いくつかの的を絞った正規表現で解析します。入力は@image-tracer-tsによって機械生成されたもの(任意/ユーザー作成のマークアップではない)であり、その出力の形が狭い(1 つの<svg>、フラットな<path>子要素)ため、ここでは安全です。各パスの
d属性をsvg-pathdataによりMOVE_TO/LINE_TO/CURVE_TO/CLOSE_PATHコマンドへ正規化します。ただしnormalizeHVZ(false)を指定します。デフォルトではZのクローズパスが始点への単なる直線に書き換えられ、CLOSE_PATHコマンドが完全に落とされてしまい、トレースされたすべての領域が黙って開いたパスになってしまうため、これが必要です。コマンドをサブパスにグループ化します(各
MOVE_TOで分割)。ある領域の複合サブパス — 外周の境界と、穴/島の輪郭 — は 1 つの レイヤーにまとめておきます(extraSubpaths)。こうすることで、レンダラーのevenoddの塗りルール(レンダリングとカメラを参照)が、穴を塗りつぶさずに穴のまま保ちます。各パスの
fillの色を最も近いパネルパレットのインデックス(下記)にマッピングし、そもそも fill を解析できないパス(とくにfill="none")は除外します。すべての点を、ソース SVG 空間からターゲットの mm 矩形 — ソース画像レイヤーの
{x, y, width, height}— へスケールします。寸法のソースには@image-tracer-ts自身のものを使い、<svg>に明示的なwidth/height属性があればそれを、なければviewBoxにフォールバックします。結果を最大 300 レイヤー(
MAX_TRACE_LAYERS)に制限します。込み入ったソース画像は、そうしなければ数千もの小さな色領域を生み出し、エディタが使いものにならなくなるためです。
最近傍パレット色のマッチング(OKLab)
nearest-palette-color.ts は、任意のトレース塗り色を、生の RGB 距離ではなく OKLab 知覚距離によって、固定のパネル 3 色のうち最も近いものにマッピングします。素の RGB 距離は、知覚的な近さの指標としては不十分です。暗くくすんだ金は、生の RGB では単に両方が暗いという理由だけで「最も近いのは黒」と読まれてしまいます。OKLab 距離は上流の pgen トレーサーパイプラインが使うのと同じ指標なので、ここでトレースされた塗りは、同じソースに対してそのパイプラインが選んだであろうものと一致します。
マッチングは 3 ステップで行われます。
parseColor()が#rgb、#rrggbb、rgb()/rgba()、そしていくつかの名前付き色(black、white)を正規の 6 桁 hex に正規化します。解析できないもの(意図的に名前付き色テーブルに入れていないfill="none"を含む)にはnullを返します。hexToOklab()が Björn Ottosson の OKLab 行列を使い、sRGB → 線形 sRGB → CIE XYZ(D65) → OKLab と変換します。nearestPaletteIndex()が、OKLab 距離の二乗が最小のパレットエントリを返します。入力色またはすべてのパレットエントリが解析に失敗した場合はnullを返します。
結果のコミット
トレースダイアログに戻り、Apply はソース画像レイヤーの現在のジオメトリを基準にトレース後レイヤーを構築し、そのすべての差し替えを 1 回の ctx.commit()(1 つの undo エントリ)で行います。ソース画像レイヤーは削除せず hidden: true を付けて残し(隠れたデザイン時の参照として残る)、トレースされた PathLayer をスタックのそのすぐ上に挿入し、最初のトレース後レイヤーを選択します。
Warning
プレビューはトレースされた SVG を <img src="data:image/svg+xml;..."> を通して描画し、決して dangerouslySetInnerHTML / innerHTML は使いません。<img> は SVG を「画像モード」でデコードし、埋め込みスクリプトを実行しません。そのため、悪意あるトレース結果(あるいはトレーサーに与えられた悪意あるソース画像)がアプリ内でスクリプトを実行することはできません。