devicePixelContentBox を使用したピクセル完璧なレンダリング

キャンバスには実際に何ピクセルありますか?

Chrome 84 以降、ResizeObserverdevicePixelContentBox という新しいボックス測定をサポートしています。これは、要素の寸法を物理ピクセルで測定します。これにより、特に高密度画面のコンテキストで、ピクセル単位のグラフィックをレンダリングできます。

対応ブラウザ

  • Chrome: 84。
  • Edge: 84.
  • Firefox: 93.
  • Safari: サポートされていません。

ソース

em%vh などの抽象的な長さの単位を使用することが多いですが、最終的にはすべてピクセルに帰着します。CSS で要素のサイズや位置を指定するたびに、ブラウザのレイアウト エンジンは最終的にその値をピクセル(px)に変換します。これらは「CSS ピクセル」と呼ばれ、長い歴史があり、画面上のピクセルとはゆるやかな関係しかありません。

長い間、画面のピクセル密度を 96 DPI(「インチあたりのドット数」)と見積もることは、かなり妥当でした。つまり、任意のモニターでは 1 cm あたり約 38 ピクセルになります。時間の経過とともに、モニターのサイズが拡大または縮小されたり、同じ表面積にピクセルが増えたりしてきました。ウェブ上の多くのコンテンツで、フォントサイズを含むサイズが px で定義されているという事実と組み合わせると、高密度(HiDPI)画面でテキストが判読できなくなります。ブラウザは対策として、モニターの実際のピクセル密度を非表示にして、ユーザーが 96 DPI のディスプレイを使用していると偽装します。CSS の px 単位は、この 96 DPI の仮想ディスプレイ上の 1 ピクセルのサイズを表します。そのため、CSS ピクセルという名前が付けられています。この単位は測定と配置にのみ使用されます。実際のレンダリングが行われる前に、物理ピクセルに変換されます。

この仮想ディスプレイからユーザーの実際のディスプレイに移動するにはどうすればよいですか?「devicePixelRatio」と入力します。このグローバル値は、1 つの CSS ピクセルを形成するために必要な物理ピクセル数を示します。devicePixelRatio(dPR)が 1 の場合、お使いのモニターの DPI は約 96 です。レチナ ディスプレイを使用している場合、dPR は 2 です。スマートフォンでは、232.65 など、より高い(そして奇妙な)dPR 値が返されることは珍しくありません。この値は正確ですが、モニターの実際の DPI 値を導き出すことはできません。dPR が 2 の場合、1 つの CSS ピクセルが 2 つの物理ピクセルに正確にマッピングされます。

幅は 3,440 ピクセルで、表示領域の幅は 79 cm です。解像度は 110 DPI になります。96 に近いものの、正解ではありません。これが、ほとんどのディスプレイで <div style="width: 1cm; height: 1cm"> のサイズが正確に 1 cm にならない理由でもあります。

最後に、dPR はブラウザのズーム機能の影響を受けることもあります。ズームインすると、ブラウザは報告された dPR を増やし、すべてを大きくレンダリングします。ズーム中に DevTools コンソールで devicePixelRatio をオンにすると、小数値が表示されます。

ズームにより、さまざまな小数値の devicePixelRatio が DevTools に表示されます。

<canvas> 要素を追加しましょう。キャンバスのピクセル数は、width 属性と height 属性を使用して指定できます。したがって、<canvas width=40 height=30> は 40 x 30 ピクセルのキャンバスになります。ただし、40 x 30 ピクセルで表示されるわけではありません。デフォルトでは、キャンバスは width 属性と height 属性を使用して固有のサイズを定義しますが、よく知られているすべての CSS プロパティを使用してキャンバスのサイズを任意に変更できます。ここまで学んだことを踏まえると、この方法がすべてのシナリオで理想的とは限らないことがわかります。キャンバス上の 1 つのピクセルが、複数の物理ピクセルをカバーする場合もあれば、物理ピクセルの一部をカバーする場合もあります。これにより、視覚的なアーティファクトが発生する可能性があります。

要約すると、キャンバス要素には特定のサイズがあり、描画できる領域を定義します。キャンバス ピクセルの数は、CSS ピクセルで指定されるキャンバスの表示サイズとは完全に独立しています。CSS ピクセルの数は、物理ピクセルの数とは異なります。

ピクセル パーフェクト

キャンバス ピクセルから物理ピクセルに正確にマッピングすることが望ましい場合があります。このマッピングが実現されている場合、それは「ピクセル パーフェクト」と呼ばれます。ピクセル単位のレンダリングは、特にサブピクセル レンダリングを使用する場合や、明るさが交互に並んだ線をきっちりと配置したグラフィックを表示する場合に、テキストを判読可能な状態でレンダリングするために重要です。

ウェブ上で可能な限りピクセル パーフェクトなキャンバスを実現するために、これまでは次のようなアプローチが一般的でした。

<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% を含む要素は、次のような長方形になります。

getBoundingClientRect() 呼び出しの結果として小数値のピクセル値を示す DevTools。

CSS ピクセルは完全に仮想的であるため、理論上はピクセルの小数値を使用できますが、ブラウザは物理ピクセルにどのようにマッピングするかを判断します。小数点以下の物理ピクセルというものは存在しないためです。

ピクセル スナップ

単位変換プロセスの一部で、要素を物理ピクセルに配置する処理を「ピクセル スナップ」と呼びます。これは、小数点以下のピクセル値を整数の物理ピクセル値にスナップする処理です。具体的な処理方法はブラウザによって異なります。dPR が 1 のディスプレイに幅 791.984px の要素がある場合、あるブラウザでは要素が 792px 物理ピクセルでレンダリングされる一方で、別のブラウザでは 791px でレンダリングされる可能性があります。1 ピクセルだけずれているだけですが、ピクセル単位で正確にレンダリングする必要がある場合は、1 ピクセルが致命的になる可能性があります。これにより、ぼやけが発生したり、モアレ効果などのアーティファクトが目立つようになったりする可能性があります。

上段の画像は、異なる色のピクセルのラスターです。下の画像は上と同じですが、バイリニア スケーリングを使用して幅と高さが 1 ピクセルずつ縮小されています。このようなパターンはモアレ効果と呼ばれます。
(この画像を拡大縮小なしで表示するには、新しいタブで開く必要がある場合があります)。

devicePixelContentBox

devicePixelContentBox は、デバイスのピクセル(物理ピクセル)単位で要素のコンテンツ ボックスを返します。これは ResizeObserver の一部です。ResizeObserver は Safari 13.1 以降、すべての主要ブラウザでサポートされていますが、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 は常に borderBoxSizecontentBoxSizedevicePixelContentBoxSize を提供しますが(ブラウザがサポートしている場合)、コールバックは、監視対象のボックス指標のいずれかが変更された場合にのみ呼び出されます。

この新しいプロパティを使用すると、キャンバスのサイズと位置をアニメーション化することもできます(効果的に小数ピクセル値を保証します)。レンダリングにモアレ効果は発生しません。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 以降でサポートされています。