キャンバスには実際にピクセルがいくつあるのでしょうか。
Chrome 84 以降、ResizeObserver は devicePixelContentBox
という新しいボックス測定をサポートしています。これは、要素の寸法を物理ピクセルで測定します。これにより、特に高密度画面のコンテキストで、ピクセルパーフェクトなグラフィックをレンダリングできます。
背景: CSS ピクセル、キャンバス ピクセル、物理ピクセル
em
、%
、vh
などの抽象的な長さの単位を使用することが多いですが、最終的にはすべてピクセルに帰着します。CSS で要素のサイズや位置を指定すると、ブラウザのレイアウト エンジンが最終的にその値をピクセル(px
)に変換します。これは「CSS ピクセル」であり、履歴が長く、画面上のピクセルとの間に緩やかな関係があります。
長い間、画面のピクセル密度を 96DPI(「1 インチあたりのドット数」)で推定することはかなり合理的でした。つまり、任意のモニターのピクセル密度は約 38 ピクセル/cm でした。時間の経過とともに、モニターのサイズが拡大または縮小されたり、同じ表面積にピクセルが増えたりしてきました。ウェブ上の多くのコンテンツで、フォントサイズを含むサイズが px
で定義されているという事実と組み合わせると、高密度(HiDPI)画面でテキストが判読できなくなります。対策として、ブラウザはモニターの実際のピクセル密度を隠し、ユーザーが 96 DPI のディスプレイを使用していると想定します。CSS の px
単位は、この仮想 96 DPI ディスプレイの 1 ピクセルのサイズを表すため、「CSS Pixel」と呼ばれます。この単位は測定と配置にのみ使用されます。実際のレンダリングが行われる前に、物理ピクセルに変換されます。
この仮想ディスプレイからユーザーの実際のディスプレイに移動するにはどうすればよいでしょうか。「devicePixelRatio
」と入力します。このグローバル値は、1 つの CSS ピクセルを形成するために必要な物理ピクセルの数を示します。devicePixelRatio
(dPR)が 1
の場合、約 96 DPI のモニターで作業しています。レチナ ディスプレイを使用している場合、dPR は 2
です。スマートフォンでは、2
、3
、2.65
など、より高い(そして奇妙な)dPR 値が返されることは珍しくありません。この値は正確ですが、モニターの実際の DPI 値を導出することはできません。dPR が 2
の場合は、1 CSS ピクセルが 2 つの物理ピクセルに正確にマッピングされることを意味します。
1
です。横幅 3,440 ピクセル、表示領域幅 79 cm です。
解像度は 110 DPI になります。96 に近いものの、正解ではありません。そのため、<div style="width: 1cm; height: 1cm">
はほとんどのディスプレイで 1 cm のサイズを正確に測定しません。
最後に、dPR はブラウザのズーム機能の影響を受けることもあります。ズームインすると、ブラウザは報告された dPR を増やし、すべてを大きくレンダリングします。ズーム中に DevTools コンソールで devicePixelRatio
をオンにすると、小数値が表示されます。
<canvas>
要素を追加しましょう。width
属性と height
属性を使用して、キャンバスのピクセル数を指定できます。したがって、<canvas width=40 height=30>
は 40 x 30 ピクセルのキャンバスになります。ただし、これは 40 x 30 ピクセルで表示されるという意味ではありません。デフォルトでは、キャンバスは width
属性と height
属性を使用して固有のサイズを定義しますが、使い慣れたすべての CSS プロパティを使用してキャンバスのサイズを変更できます。これまで学んできたことを踏まえると、これがすべてのシナリオに適しているわけではないことに気が付くかもしれません。キャンバス上の 1 つのピクセルが複数の物理ピクセルを覆ったり、物理ピクセルのごく一部を覆ったりすることがあります。これにより、視覚的なアーティファクトが好ましくなくなる可能性があります。
要約すると、キャンバス要素には特定のサイズがあり、描画可能な領域を定義します。キャンバス ピクセルの数は、CSS ピクセルで指定されるキャンバスの表示サイズとは完全に独立しています。CSS ピクセルの数は、物理ピクセルの数とは異なります。
Google Pixel の完璧さ
キャンバス ピクセルから物理ピクセルに正確にマッピングすることが望ましい場合があります。このマッピングが達成されると、「pixel-perfect」と呼ばれます。ピクセル パーフェクト レンダリングは、テキストの判読性に優れたレンダリングに不可欠です。サブピクセル レンダリングを使用する場合や、明るさが交互に並ぶように配置されたグラフィックを表示する場合に特に重要です。
ウェブ上で可能な限りピクセル パーフェクトなキャンバスを実現するために、これまでは次のようなアプローチが一般的でした。
<style>
/* … styles that affect the canvas' size … */
</style>
<canvas id="myCanvas"></canvas>
<script>
const cvs = document.querySelector('#myCanvas');
// Get the canvas' size in CSS pixels
const rectangle = cvs.getBoundingClientRect();
// Convert it to real pixels. Ish.
cvs.width = rectangle.width * devicePixelRatio;
cvs.height = rectangle.height * devicePixelRatio;
// Start drawing…
</script>
鋭い読み手は、dPR が整数値でない場合にどうなるのか疑問に思うかもしれません。これは良い質問であり、まさにこの問題全体の核心にあります。また、割合、vh
、その他の間接的な値を使用して要素の位置やサイズを指定した場合、小数点の CSS ピクセル値で解決される場合があります。margin-left: 33%
を含む要素は、最終的に次のような長方形になります。
CSS ピクセルは完全に仮想的であるため、理論上はピクセルの小数値を使用できますが、ブラウザは物理ピクセルにどのようにマッピングするかを判断します。小数値の物理ピクセルは重要ではないからです。
Pixel のスナップ
単位変換プロセスの中で、要素を物理ピクセルと整列させる処理は「ピクセル スナップ」と呼ばれ、基準値と同じ処理を行います。つまり、小数点以下のピクセル値を整数の物理ピクセル値にスナップします。具体的な処理方法はブラウザによって異なります。dPR が 1 のディスプレイに幅 791.984px
の要素がある場合、あるブラウザでは要素が 792px
物理ピクセルでレンダリングされる一方で、別のブラウザでは 791px
でレンダリングされる可能性があります。これは 1 ピクセルだけずれるだけですが、1 ピクセルでは、完全なピクセルを必要とするレンダリングには支障をきたします。そのため、ぼやけたり、モアレ効果のような見えやすいアーティファクトが生じたりすることがあります。
devicePixelContentBox
devicePixelContentBox
は、要素のコンテンツ ボックスをデバイスのピクセル(物理ピクセル)単位で指定します。これは ResizeObserver
の一部です。Safari 13.1 以降、ResizeObserver はすべての主要なブラウザでサポートされるようになりましたが、devicePixelContentBox
プロパティは現在のところ Chrome 84 以降でのみ使用可能です。
ResizeObserver
: 要素の document.onresize
に似ていますで説明したように、ResizeObserver
のコールバック関数はペイントの前に、レイアウトの後に呼び出されます。つまり、コールバックの entries
パラメータには、ペイントされる直前に確認されたすべての要素のサイズが含まれます。先ほど説明したキャンバスの問題では、この機会を利用してキャンバスのピクセル数を調整し、キャンバス ピクセルと物理ピクセルが正確に 1 対 1 でマッピングされるようにします。
const observer = new ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === canvas);
canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
canvas.height = entry.devicePixelContentBoxSize[0].blockSize;
/* … render to canvas … */
});
observer.observe(canvas, {box: ['device-pixel-content-box']});
observer.observe()
の options オブジェクトの box
プロパティを使用すると、モニタリングするサイズを定義できます。そのため、各 ResizeObserverEntry
は常に borderBoxSize
、contentBoxSize
、devicePixelContentBoxSize
を提供しますが(ブラウザがサポートしている場合)、コールバックは、監視対象のボックス指標のいずれかが変更された場合にのみ呼び出されます。
この新しいプロパティを使用すると、キャンバスのサイズと位置をアニメーション化でき(実質的に小数のピクセル値を保証)、レンダリングに対するモアレの影響はまったく見えません。getBoundingClientRect()
を使用したアプローチに対するモアレの影響と、新しい ResizeObserver
プロパティを使用してそれを回避する方法を確認するには、Chrome 84 以降のデモをご覧ください。
機能検出
ユーザーのブラウザが devicePixelContentBox
をサポートしているかどうかを確認するには、任意の要素を監視して、プロパティが ResizeObserverEntry
に存在するかどうかを確認します。
function hasDevicePixelContentBox() {
return new Promise((resolve) => {
const ro = new ResizeObserver((entries) => {
resolve(entries.every((entry) => 'devicePixelContentBoxSize' in entry));
ro.disconnect();
});
ro.observe(document.body, {box: ['device-pixel-content-box']});
}).catch(() => false);
}
if (!(await hasDevicePixelContentBox())) {
// The browser does NOT support devicePixelContentBox
}
まとめ
ピクセルはウェブ上では驚くほど複雑なトピックであり、これまでは、要素がユーザーの画面上で占有する物理ピクセルの正確な数を知る方法はありませんでした。ResizeObserverEntry
の新しい devicePixelContentBox
プロパティを使用すると、その情報を取得して、<canvas>
でピクセル単位のレンダリングを行うことができます。devicePixelContentBox
は Chrome 84 以降でサポートされています。