雑記帳

整理しない情報集

更新情報

CanvasとSVG

公開日:

カテゴリ: JavaScript

CanvasとSVGを簡単にまとめたメモ書きです。

CanvasとSVG

Canvas

  • ビットマップデータ(ラスタ画像)
  • データはピクセル単位に色情報を持つ
  • 内容の更新時には1から再描画が必要
  • 一般的なコンピュータ上での図形描画と同様の描画方法
    • 利用者目線では簡易的なペイントソフト(レイヤー機能などが無い)のイメージ
    • 開発者目線ではマークアップ言語やUIツールを使わずにGUIを1から作成するイメージ

SVG

  • ベクトルデータ(ベクタ画像)
  • データはオブジェクトとして持つ
  • 各要素に対してスタイルやスクリプトを適用可能
  • XMLベースのためHTMLなどのWeb系と同じ描画方法

使い分け

Canvasは単独で完結する描画を行うものに、SVGはHTMLの一部として描画するものに向いています。

基本的な使い方

Canvas

JavaScriptで描画します。Canvas要素自体はJavaScriptではなく<canvas>タグで作成してもOKです。

const canvas = document.createElement("canvas");
canvas.width = 100;
canvas.height = 100;
document.body.appendChild(canvas);

const ctx = canvas.getContext("2d");
ctx.strokeStyle = "#000";
ctx.lineWidth = 1;
ctx.moveTo(0, 0);
ctx.lineTo(100, 100)
ctx.moveTo(0, 100);
ctx.lineTo(100, 0);
ctx.stroke();

SVG (XML)

SVGの基本的な書き方です。XMLですので、すべての要素は必ず閉じている必要があります(空要素の閉じタグを省略する場合は末尾にスラッシュ/)。

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
	<path d="M0 0L100 100M0 100L100 0" stroke="#000" />
</svg>

SVG (JavaScript)

JavaScriptでSVGを生成する場合は、HTMLと名前空間が異なるので要素の作成方法が少し異なります。

要素の作成には、通常のcreateElement()の代わりにcreateElementNS()を用い、第1引数にSVGの名前空間を指定します。SVGの各要素のほとんどの属性はJavaScriptオブジェクトのプロパティとしてはアクセスできないので、属性はsetAttribute()を用いて指定します。

const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 100 100");
document.body.appendChild(svg);

const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M0 0L100 100M0 100L100 0");
path.style.stroke = "#000";
svg.appendChild(path);

Tips

描画をもとにポインタイベントを拾う

Canvas

描画したものはすべてピクセルデータでしかないため、すべて座標から計算してイベントを処理する必要があります。

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ccc";
ctx.fillRect(20, 20, 100, 40);

canvas.addEventListener("mousedown", (e) => {
	const { offsetX: x, offsetY: y } = e;
	if (x >= 20 && x < 120 && y >= 20 && y < 60) {
		console.log(e);
	}
});

SVG

描画したものは1つのオブジェクトかつXML要素であるため、オブジェクト単位でイベントリスナを登録することができます。

<svg viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg">
	<rect id="target" x="20" y="20" width="100" height="40" fill="#ccc" />
</svg>
document.getElementById("target").addEventListener("mousedown", console.log);

レスポンシブ対応

Canvas

Canvasは自動でリサイズされません。CSSで引き伸ばしても良いですが、Canvasはラスタ画像であるため拡大すると補完されてぼやけます。

リサイズの補足方法は主に2種類あります。windowオブジェクトのresizeイベントを捕捉する方法とResizeObserverを使用する方法があります。

前者はウィンドウサイズが変更された場合に発火するため、幅もしくは高さを画面いっぱいに描画している場合に向いています。後者は要素のサイズが変更された場合に発火するため、CSSでブレイクポイントを設けていて要素のサイズが必ずしも変化するとは限らない場合に向いています。

// windowのresizeを捕捉する方法
window.addEventListener("resize", (e) => {
	// 再描画処理
});

// ResizeObserverを使用する方法
const observer = new ResizeObserver((entries) => {
	for (const entry of entries) {
		// 再描画処理
	}
});
observer.observe(document.getElementById("container")); // 捕捉対象の要素

SVG

基本的に通常の画像と同様に扱います。ベクタ画像であるためSVG内にラスタ画像を埋め込まない限り拡大してもぼやけません。CSSで幅もしくは高さに対して%指定でOKです。

ダークモード対応

Canvas

Canvas関連のAPIにはカラーモードに応じて場合分けするような手段は存在しないため、window.matchMedia()でCSSメディアクエリに一致するかどうかを捕捉します。

また、常時再描画していないCanvasの場合、カラーモードの反映には再描画が必要です。window.matchMedia()changeイベントのリスナを登録することで、カラーモードの変更時にイベントを発火させることができます。

const isDarkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");

// この行の実行時点でのカラーモード
if (isDarkModeQuery.matches) {
	// ダークモードの場合
} else {
	// ダークモード以外の場合
}

// カラーモードの変更時のイベントリスナ
isDarkModeQuery.addEventListener("change", (e) => {
	if (e.matches) {
		// ダークモードに切り替わった場合
	} else {
		// ダークモード以外に切り替わった場合
	}
});

SVG

CSSがそのまま使用できるので、メディアクエリもしくはlight-dark()で適用します。

.example1 {
	stroke: #000;

	@media (prefers-color-scheme: dark) {
		stroke: #fff;
	}
}

.example2 {
	stroke: light-dark(#000, #fff);
}

高DPI(解像度)対応

Canvas

ピクセル比が1より大きい環境では、ビューポートの指定に合わせて描画後に拡大されます。そのため、ノートPCやスマートフォンなどの高DPIディスプレイではそのままでは補完されぼやけます。

Canvas要素のサイズ指定ではビューポート指定に合わせたサイズになります。高DPIに対応するにはwindow.devicePixelRatioの値でアップスケールしたCanvasを作成し、CSSで元のサイズに縮小します。

// サイズ固定の場合
const width = 100;
const height = 100;

const canvas = document.createElement("canvas");
document.body.appendChild(canvas);

const pixelRatio = window.devicePixelRatio;
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
canvas.style.width = `${width}px`;
canvas.style.height = `${width}px`;
// 親要素に合わせる場合
const canvas = document.getElementById("canvas");
const parent = canvas.parentElement;

const pixelRatio = window.devicePixelRatio;
canvas.width = parent.clientWidth * pixelRatio;
canvas.height = parent.clientHeight * pixelRatio;
canvas.style.width = `${parent.clientWidth}px`;
canvas.style.height = `${parent.clientHeight}px`;

デバイスのピクセル比に合わせてCanvasのサイズを広げているので、描画時の座標には拡大したサイズに合わせた座標を指定する必要があります。

座標指定時に自分で倍率を掛け合わせる方法と、自動で行列変換を行う方法があります。

// 自分で倍率を掛け合わせる場合
const paint = (ctx) => {
	const r = window.devicePixelRatio;

	// 線幅やフォントサイズにも指定が必要です
	ctx.lineWidth = 1 * r;

	// 0にピクセル比を掛ける必要はありませんが
	// 値を変更した際に忘れないよう、掛けたままにしています
	ctx.moveTo(0 * r, 0 * r);
	ctx.lineTo(100 * r, 100 * r)
	ctx.moveTo(0 * r, 100 * r);
	ctx.lineTo(100 * r, 0 * r);
	ctx.stroke();
};
// 自動で行列変換を行う場合
const paint = (ctx) => {
	const r = window.devicePixelRatio;
	ctx.scale(r, r);

	ctx.lineWidth = 1;
	ctx.moveTo(0, 0);
	ctx.lineTo(100, 100)
	ctx.moveTo(0, 100);
	ctx.lineTo(100, 0);
	ctx.stroke();
};

CSSのメディアクエリでDPIに合わせたスタイル指定が可能であるため、ダークモード対応同様にwindow.matchMedia()changeイベントで捕捉できますが、値を指定する必要があるためchangeイベント発生ごとにイベントリスナを再定義する必要があります。

const changeDPI = () => {
	window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
		.addEventListener("change", changeDPI, { once: true });
	// 再描画処理
};
changeDPI();

SVG

レスポンシブ対応と同様に通常の画像と同じ扱いとなるため、特段の考慮は不要です。

その他

Canvas

  • 座標はピクセルとピクセルの境目が整数値となるため、整数値の座標から水平または垂直に1pxの線を引くと2pxにまたがりぼやける

    • 2pxにまたがるのを避けるには0.5pxずらした座標を指定します
  • ピクセルデータのため、各座標の色情報と透明度を取得できる

    ctx.getImageData(x, y, 1, 1).data; // [R, G, B, A]

    複数ピクセルを取得した場合は、左上から各座標のRGBAがZ字の順番に連結した1次元配列(Uint8ClampedArray)として返ってくる([(0,0)のR, (0,0)のG, (0,0)のB, (0,0)のA, (1,0)のR, (1,0)のG, (1,0)のB, (1,0)のA, ...])

  • 同様に各座標の色情報と透明度を直接書き換えることができる

    ctx.putImageData(new ImageData(Uint8ClampedArray.from([0, 0, 0, 255]), 1, 1), 0, 0);
  • getImageData()などのピクセルデータの取得はCORSの制約を受ける

    • 一度でもputImageData()などでCORS制限のかかったリソースをCanvasに入れるとTaintedな(汚染された)Canvasとなり、ピクセルデータの取得ができなくなる
  • Canvas要素は上記のXY座標による2次元描画APIの他に、WebGLやWebGPUを用いた3次元描画APIも存在する

SVG

  • CSSプロパティz-indexは使用できない
    • 元々はSVG2.0の仕様に盛り込まれていましたが、将来バージョンへ先送りになっています
    • SVG内の要素の順番に下から描画されるため、JavaScriptなどで要素の順番を入れ替えるのが手っ取り早いでしょう

カテゴリ: JavaScript