Sizing HTML Canvas Elements

最終更新: 2024-08-12 15:47:28 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

HTML canvas には 2 つのサイズがあります。

  1. width と height 属性は描画バッファーを定義します。
  2. CSS の幅と高さは表示ボックスを定義します。

2 つのサイズを混同すると、ぼやけた出力、引き伸ばされた座標、過剰なメモリ使用が発生します。

描画バッファーと表示サイズ #

canvas 属性には単位なしのピクセル数を指定します。

html
<canvas width="800" height="400"></canvas>

既定の描画バッファーは 300 x 150 ピクセルです。CSS でそのバッファーを別のサイズに表示できます。

css
canvas {
  inline-size: 100%;
  block-size: auto;
}

CSS ボックスと描画バッファーのアスペクト比が異なると、ブラウザーはビットマップを引き伸ばします。 属性は CSS の長さではなく整数なので、width="800px" は意図として無効です。

canvas.width または canvas.height を変更するとビットマップがクリアされ、変換、クリッピング領域、スタイル、現在のパスを含む レンダリングコンテキストの状態がリセットされます。

デバイスピクセル比への一致 #

高密度ディスプレイでは 1 CSS ピクセルが複数のデバイスピクセルに対応します。描画用バッキングストアのサイズは、レンダリングされた CSS ボックスから求めます。

js
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 のリサイズだけを監視せず、要素を監視します。

js
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 ピクセルで届きます。描画コードがバッキングストア座標を使う場合は、描画バッファーへマッピングします。

js
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 で閲覧・コメントする(投稿にはログインが必要です)