CanvasのPath2DとSVG:適切なサイズでパスを再利用する
viewBoxの変換、色、穴、ピクセル密度を正しく扱い、SVGのパスをcanvasで再利用します。独自の例を使い、編集可能なSVGマスターも保持します。
目次
パスのd属性をnew Path2D(d)へ渡し、canvasのコンテキストで描画します。SVGと一致させるには、座標の対応付け、描画設定、塗りの規則も引き継ぎます。コンストラクターが受け取るのはパスデータです。SVG文書の読み込みや、文書のviewBox、CSS、グループ変換の適用は行いません(MDNのPath2Dコンストラクター)。
独自アイコンがSVGでは正しく見えるのに、canvasでは小さい、ずれる、黒くなる、穴が埋まる場合に、この違いが重要です。以下の例では、2本のパスと明示的なフィッティング変換で差を直します。既存のベクター形状があれば、それから始めてください。ベクター化が役立つのは、再利用したい画像がまだピクセルとしてしか存在しない場合です。
Path2Dオブジェクトが持つもの
パスデータは、輪郭の説明と考えます。他のSVGの指示はその文字列の外にあり、canvasの描画コードが必要なものを提供しなければなりません。
| SVGの入力 | Canvas側の役割 |
|---|---|
パスのd値 | Path2Dの形状を構築します |
viewBoxと表示サイズ | 描画変換を計算します |
fillとfill-rule | fillStyleを設定し、fill()に規則を渡します |
親やパスのtransform | 同等の変換を別途適用します |
| CSS、グラデーション、マスク、文字、画像 | 必要な描画を実装するか、別の方法を選びます |
このサンプルは、直接指定した塗りと、直接の子である2本のパスだけを使い、他のSVG機能は使いません。確認しやすいよう意図して小さくしています。この抽出ループを任意のSVG読み込み処理にコピーすると、継承したスタイルや非対応の要素が失われます。
元データと未設定のcanvasを比較する
独自のエンブレムには、丸みのある青緑の本体、オレンジの先端、四角い開口部があります。viewBoxは(0, 0)ではなく(20, 10)から始まります。内側のサブパスは外側の境界と同じ方向に進むため、指定したevenoddの規則が重要です。
このSVGとcanvasをHTMLページに置きます。
<svg id="source" xmlns="http://www.w3.org/2000/svg"
viewBox="20 10 120 80" width="240" height="160">
<path fill="#126d70" fill-rule="evenodd"
d="M40 20 H90 Q110 20 110 40 V60 Q110 80 90 80 H40 Q30 80 30 70 V30 Q30 20 40 20 Z M50 35 H80 V60 H50 Z"/>
<path fill="#f19b38" d="M115 25 L135 50 L115 75 Z"/>
</svg>
<canvas id="result"></canvas>2つのd値を順に処理し、他に何も設定せずctx.fill(new Path2D(d))を呼ぶと、形状はcanvas座標、既定の描画設定と塗りの規則を使います。SVGの見た目を自動で引き継ぐわけではありません。Canvasのfill()は、パスと明示的なevenoddまたはnonzeroの規則を受け取り、既定値はnonzeroです(MDN fill())。

方法:手書きのSVGをブラウザーで描画し、両方のcanvasの方法で2本のパスを描き、公開コードを別のHTMLページで再実行しました。スクリーンショットは実際のブラウザー出力です。座標と描画設定の処理を示すもので、PerfectVectorの変換結果ではありません。
viewBoxをcanvasに対応させる
SVGの既定の縦横比保持では、viewBoxをビューポート内に収めて中央に配置します。SVGの座標系仕様が、その対応付けを定義しています。この平坦な例では、1つの等倍率と移動でxMidYMid meetを再現します。
scale = min(cssWidth / viewBoxWidth, cssHeight / viewBoxHeight)
tx = (cssWidth - viewBoxWidth * scale) / 2 - viewBoxX * scale
ty = (cssHeight - viewBoxHeight * scale) / 2 - viewBoxY * scale最後の項がゼロ以外の原点を取り除きます。240 × 160では倍率が2、txが-40、tyが-20です。300 × 120では倍率が1.5となり、幅180ピクセルに収めたviewBoxの左右にそれぞれ60ピクセルの余白ができます。元の原点を考慮した後の移動量は(30, -15)です。
上のHTMLの後に次のスクリプトを使います。このサンプルのパスに直接指定した属性を扱い、canvas全体を描き直し、CSSサイズと内部ビットマップサイズを分けます。
const source = document.querySelector("#source");
const vb = source.viewBox.baseVal;
const layers = [...source.querySelectorAll("path")].map(el => ({
path: new Path2D(el.getAttribute("d")),
fill: el.getAttribute("fill"),
rule: el.getAttribute("fill-rule") || "nonzero"
}));
function draw(canvas, cssWidth, cssHeight, dpr = window.devicePixelRatio || 1) {
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
const ctx = canvas.getContext("2d");
const scale = Math.min(cssWidth / vb.width, cssHeight / vb.height);
const tx = (cssWidth - vb.width * scale) / 2 - vb.x * scale;
const ty = (cssHeight - vb.height * scale) / 2 - vb.y * scale;
const px = canvas.width / cssWidth;
const py = canvas.height / cssHeight;
ctx.setTransform(px * scale, 0, 0, py * scale, px * tx, py * ty);
for (const layer of layers) {
ctx.fillStyle = layer.fill;
ctx.fill(layer.path, layer.rule);
}
}
draw(document.querySelector("#result"), 240, 160);
canvas.widthとcanvas.heightの設定は、コンテキストを消去してリセットします。その後、関数が完全な変換を設定し、すべてのレイヤーを描き直すので、繰り返し呼んでも倍率が累積しません。幅と高さが正の有効なviewBoxと、正の要求寸法を前提としています。
pxとpyは、実際に丸めたビットマップ寸法を使います。そのため、指定した密度で端数の寸法が生じても、CSS座標を確保したピクセルに対応させられます。MDNのdevicePixelRatioの例は、canvasの表示サイズとピクセル寸法を別に扱う理由を説明しています。
サイズとピクセル密度を別々に確認する
2種類のCSSサイズで、同じ関数にDPR値1と2を明示して渡しました。これらは制御した密度入力であり、2台の実機をテストしたという意味ではありません。既定の呼び出しでブラウザーが報告したデバイスピクセル比は2でした。
| CSSサイズ | DPR引数 | 観察したcanvasのビットマップ |
|---|---|---|
| 240 × 160 | 1 | 240 × 160 |
| 240 × 160 | 2 | 480 × 320 |
| 300 × 120 | 1 | 300 × 120 |
| 300 × 120 | 2 | 600 × 240 |
4つのケースすべてで、開口部の中心は透明のままでした。高い密度は内部の解像度を変えますが、誤った変換を直したり、欠けた塗りの規則を復元したりはしません。意図するCSS寸法やデバイスピクセル比が変わったら描き直します。CSSだけで拡大すると、canvasにある既存のビットマップが引き伸ばされます。
canvasに入る前から元データが間違っている場合は、SVGのサイズとviewBoxの確認を使います。canvasだけが間違っているなら、まずその変換と描画設定を、正しく動作するSVGの基準表示と比較します。
元データを保持し、適切な読み込み方法を選ぶ
派生したパスデータとともに、SVGマスターとその属性を保持します。少数の輪郭とスタイルを自分で管理する場合に、Path2Dの描画処理が役立ちます。複雑なSVGでブラウザー描画の見た目だけが必要なら、画像として読み込んでその画像を描くと、すべての機能を再構築せずに済みます。その方法もcanvasのビットマップに描画します。
ベクターパスから描き始めても、canvasの出力をPNGとして保存するとラスター画像になります。Canvasの書き出しAPIは画像のblobを生成し、PNG対応は必須です(MDN toBlob())。後のパス編集用にSVGを保持します。同様に、SVGファイルには有用なパスデータではなくラスター画像が入っていることもあります。埋め込み画像の診断で、それらを区別できます。
独自アイコンがPNGやJPGでしか残っておらず、再利用できる輪郭が必要な場合に、PerfectVectorが適しています。画像をSVGへ変換し、輪郭、別々の色、小さな開口部を確認してから、canvas用のパスを取り出します。きれいなSVGマスターは、直接準備の工程に進めてください。トレースはviewBoxの対応付けを実装したり、CSSをコピーしたり、canvasのPNGを編集可能なベクターにしたりはしません。
アイコン一式では、各描画処理を組み込む前に、UIキット共通のグリッドと描画規則を決めます。SVG準備の他の例はブログにあります。
インターフェースで使う前に
- canvasと同じ表示寸法で、基準のSVGを確認します。
- viewBoxの幅と高さだけでなく、原点も確認します。
- 描画設定と意図した塗りの規則を移し、すべての開口部を確認します。
dだけをコピーせず、グループ変換を考慮します。- 実際の
canvas.widthとcanvas.heightを読み、想定する最大表示サイズをテストします。 - SVGマスターを保持し、インターフェースに適したアクセシブルな名前や周囲のテキストを提供します。
よくある質問
Path2DはSVGファイル全体を読み込めますか? いいえ。文字列の入力は、パスのd属性の値のようなSVGパスデータです。文書構造、viewBoxの対応付け、スタイル、グループ変換は別途扱う必要があります。
canvasでSVGの穴が埋まったのはなぜですか? 塗りの規則とサブパスの方向を確認してください。この例にはevenoddが必要ですが、canvasのfillの既定値はnonzeroです。Path2Dオブジェクトを塗るとき、意図した規則を渡します。
canvasへSVGのパスを描くと、ベクターで書き出せますか? パスはプログラム内で再利用できる描画指示として残りますが、canvasをPNGへ書き出すとピクセルになります。編集とベクター納品には元SVGを保持します。
参考資料
- MDN — Path2Dコンストラクター — コンストラクターが受け取るパスデータ文字列を定義しています。
- W3C — SVGの座標系 — viewBox原点、ビューポートへの対応付け、縦横比を保つ整列を定義しています。
- MDN — Canvas fill() — パスの塗りとevenodd、nonzero規則を説明しています。
- MDN — devicePixelRatio — CSS表示サイズとcanvas内部の解像度を説明しています。
- MDN — Canvas toBlob() — canvasからのラスター画像の書き出しを説明しています。
ラスターのアイコンしかない場合は、PerfectVectorでSVGを準備し、輪郭と開口部を確認してから、実際のインターフェースのサイズでcanvas描画とSVGを比較してください。

