zudo-panel-designer docs
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

画像のトレース

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.tsimageToImageData() は、ソース画像の最長辺が最大 600px(MAX_TRACE_SIDE)になるようダウンスケールし(アップロードされた画像の解像度にかかわらずライブプレビューの応答性を保つため)、オフスクリーンの <canvas> に描画します。描画の前にキャンバスを不透明な白で塗りつぶすため、ソースの透明な背景が幻の色としてトレースパレットにサンプリングされることはありません。

2. ImageData → SVG

traceToSvg()@image-tracer-ts/browserImageTracerBrowser.fromImageData() を呼び出します。カラーモードは 2 つあり、トレースダイアログの 3-color palette チェックボックスで切り替えます。

  • Palette モード(デフォルト、usePalette: true) — CreatePaletteMode.PALETTE により、固定のパネル 3 色(@zpd/corePALETTE を 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.globdialogs/* をすべて eager 読み込みする(拡張アーキテクチャを参照)ため、dialogs/ フォルダを間接的にインポートするすべてのテストがクラッシュします。

3. SVG → PathLayer[]

svg-to-path-layers.tssvgToPathLayers(svg, target) は純粋です — キャンバスも DOM も使わない — ので、フィクスチャの SVG 文字列に対して直接テストできます。処理は次のとおりです。

  1. トレースされた SVG の <path d="..." fill="..."> タグを、完全な XML パーサーではなく、いくつかの的を絞った正規表現で解析します。入力は @image-tracer-ts によって機械生成されたもの(任意/ユーザー作成のマークアップではない)であり、その出力の形が狭い(1 つの <svg>、フラットな <path> 子要素)ため、ここでは安全です。

  2. 各パスの d 属性を svg-pathdata により MOVE_TO / LINE_TO / CURVE_TO / CLOSE_PATH コマンドへ正規化します。ただし normalizeHVZ(false) を指定します。デフォルトでは Z のクローズパスが始点への単なる直線に書き換えられ、CLOSE_PATH コマンドが完全に落とされてしまい、トレースされたすべての領域が黙って開いたパスになってしまうため、これが必要です。

  3. コマンドをサブパスにグループ化します(各 MOVE_TO で分割)。ある領域の複合サブパス — 外周の境界と、穴/島の輪郭 — は 1 つの レイヤーにまとめておきます(extraSubpaths)。こうすることで、レンダラーの evenodd の塗りルール(レンダリングとカメラを参照)が、穴を塗りつぶさずに穴のまま保ちます。

  4. 各パスの fill の色を最も近いパネルパレットのインデックス(下記)にマッピングし、そもそも fill を解析できないパス(とくに fill="none")は除外します。

  5. すべての点を、ソース SVG 空間からターゲットの mm 矩形 — ソース画像レイヤーの {x, y, width, height} — へスケールします。寸法のソースには @image-tracer-ts 自身のものを使い、<svg> に明示的な width / height 属性があればそれを、なければ viewBox にフォールバックします。

  6. 結果を最大 300 レイヤー(MAX_TRACE_LAYERS)に制限します。込み入ったソース画像は、そうしなければ数千もの小さな色領域を生み出し、エディタが使いものにならなくなるためです。

最近傍パレット色のマッチング(OKLab)

nearest-palette-color.ts は、任意のトレース塗り色を、生の RGB 距離ではなく OKLab 知覚距離によって、固定のパネル 3 色のうち最も近いものにマッピングします。素の RGB 距離は、知覚的な近さの指標としては不十分です。暗くくすんだ金は、生の RGB では単に両方が暗いという理由だけで「最も近いのは黒」と読まれてしまいます。OKLab 距離は上流の pgen トレーサーパイプラインが使うのと同じ指標なので、ここでトレースされた塗りは、同じソースに対してそのパイプラインが選んだであろうものと一致します。

マッチングは 3 ステップで行われます。

  1. parseColor()#rgb#rrggbbrgb() / rgba()、そしていくつかの名前付き色(blackwhite)を正規の 6 桁 hex に正規化します。解析できないもの(意図的に名前付き色テーブルに入れていない fill="none" を含む)には null を返します。

  2. hexToOklab() が Björn Ottosson の OKLab 行列を使い、sRGB → 線形 sRGB → CIE XYZ(D65) → OKLab と変換します。

  3. nearestPaletteIndex() が、OKLab 距離の二乗が最小のパレットエントリを返します。入力色またはすべてのパレットエントリが解析に失敗した場合は null を返します。

結果のコミット

トレースダイアログに戻り、Apply はソース画像レイヤーの現在のジオメトリを基準にトレース後レイヤーを構築し、そのすべての差し替えを 1 回の ctx.commit()(1 つの undo エントリ)で行います。ソース画像レイヤーは削除せず hidden: true を付けて残し(隠れたデザイン時の参照として残る)、トレースされた PathLayer をスタックのそのすぐ上に挿入し、最初のトレース後レイヤーを選択します。

Warning

プレビューはトレースされた SVG を <img src="data:image/svg+xml;..."> を通して描画し、決して dangerouslySetInnerHTML / innerHTML は使いません。<img> は SVG を「画像モード」でデコードし、埋め込みスクリプトを実行しません。そのため、悪意あるトレース結果(あるいはトレーサーに与えられた悪意あるソース画像)がアプリ内でスクリプトを実行することはできません。