HTML canvas には 2 つのサイズがあります。
widthとheight属性は描画バッファーを定義します。- CSS の幅と高さは表示ボックスを定義します。
2 つのサイズを混同すると、ぼやけた出力、引き伸ばされた座標、過剰なメモリ使用が発生します。
描画バッファーと表示サイズ #
canvas 属性には単位なしのピクセル数を指定します。
<canvas width="800" height="400"></canvas>既定の描画バッファーは 300 x 150 ピクセルです。CSS でそのバッファーを別のサイズに表示できます。
canvas {
inline-size: 100%;
block-size: auto;
}CSS ボックスと描画バッファーのアスペクト比が異なると、ブラウザーはビットマップを引き伸ばします。
属性は CSS の長さではなく整数なので、width="800px" は意図として無効です。
canvas.width または canvas.height を変更するとビットマップがクリアされ、変換、クリッピング領域、スタイル、現在のパスを含む
レンダリングコンテキストの状態がリセットされます。
デバイスピクセル比への一致 #
高密度ディスプレイでは 1 CSS ピクセルが複数のデバイスピクセルに対応します。描画用バッキングストアのサイズは、レンダリングされた CSS ボックスから求めます。
function resizeCanvas(canvas, context) {
const rect = canvas.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
}
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.round(rect.width * dpr);
const height = Math.round(rect.height * dpr);
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
const scaleX = width / rect.width;
const scaleY = height / rect.height;
context.setTransform(scaleX, 0, 0, scaleY, 0, 0);
return true;
}変換後は、バッキングストアが高密度の詳細を保持しながら、描画コードで CSS ピクセル座標を使えます。 大きな canvas が過剰なメモリを消費する場合はスケールに上限を設けます。RGBA バッファーは実装固有のオーバーヘッドを除いて、 1 ピクセルあたり約 4 バイトを必要とします。
各再描画で変換を設定するには、scale の繰り返しではなく setTransform を使います。
そうしないと現在の変換を繰り返し乗算し、リサイズ通知のたびに canvas が拡大します。
どちらかの寸法を代入すると、現在値と同じ場合でもコンテキストがリセットされるため、先に比較し、実際に代入した後は必ず再描画します。
レイアウト変更への対応 #
canvas のサイズは、ビューポートのリサイズ、コンテナークエリ、フォント読み込み、周囲のコンテンツによって変わります。 window のリサイズだけを監視せず、要素を監視します。
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const observer = new ResizeObserver(() => {
if (resizeCanvas(canvas, context)) {
render(context);
}
});
observer.observe(canvas);描画バッファー属性の変更が CSS サイズを変更するフィードバックループを避けます。表示ボックスは CSS が独立して制御できるようにします。
ResizeObserverEntry.devicePixelContentBoxSize は CSS ピクセルへの丸めを経ず、デバイスピクセル単位でコンテンツボックスを提供できます。
古いエンジンでは普遍的に対応されていないため、本番コードには contentBoxSize または getBoundingClientRect() へのフォールバックが必要です。
座標マッピングと入力 #
ポインター座標はビューポートの CSS ピクセルで届きます。描画コードがバッキングストア座標を使う場合は、描画バッファーへマッピングします。
const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * (canvas.width / rect.width);
const y = (event.clientY - rect.top) * (canvas.height / rect.height);CSS 変換によって表示ジオメトリは単純なスケールより複雑になることがあります。変換された対話型 canvas では幅の比率ではなく、
適用される DOMMatrix を逆変換します。前節の CSS ピクセル変換を描画で使う場合、rect.left と rect.top を引くだけで論理描画座標になるため、
バッキングストア比率を再適用すると入力を二重にスケーリングします。
ライブラリとオフスクリーンレンダリング #
グラフィックスライブラリは、レンダラーのサイズ変更時に canvas 属性を更新できます。 たとえばライブラリは CSS ボックスとは独立して描画バッファーを設定できます。外部からの代入でレンダラー状態が破棄される可能性があるため、 属性を直接代入する前にライブラリのサイズ API を確認します。
OffscreenCanvas には描画バッファーサイズがありますが CSS ボックスはありません。メインスレッドは必要なピクセル寸法をワーカーへ伝える必要があります。
ワーカーがレイアウトを検査できると仮定せず、画像またはレンダリング済みフレームを転送します。
transferControlToOffscreen() の後はオフスクリーン canvas がレンダリングの所有権を持つため、
元の DOM canvas から別のレンダリングコンテキストを要求するのは無効です。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)