透過 devicePixelContentBox 完美呈現象素風格

一個畫布「確實」有多少像素?

自 Chrome 第 84 版起,ResizeObserver 支援名為 devicePixelContentBox 的新盒子測量方式,用於測量實際像素中的元素尺寸。這可讓算繪完美像素的圖像,在高密度螢幕中更是如此。

瀏覽器支援

  • 84
  • 84
  • 93
  • x

背景:CSS 像素、畫布像素和實體像素

雖然我們經常使用長度為抽象的單位 (例如 em%vh),但全都可以抵銷像素。每當我們在 CSS 中指定元素的大小或位置時,瀏覽器的版面配置引擎最終會將該值轉換為像素 (px)。這些「CSS 像素」雖然擁有大量記錄,且與螢幕上的像素之間的關聯度只有明顯。

長久以來,估計任何人的螢幕像素密度是 96DPI (「每英寸像素數」) 就相當合理,亦即任何指定螢幕的每公分單位約有 38 像素。手錶隨著時間的推移,螢幕變長和/或縮小,或是同一個表面區域的像素越來越多。再加上網路上許多內容都定義了大小 (包括字型大小) 的 px,因此在這些高密度 (「HiDPI」) 螢幕上最終會有難以閱讀的文字。做為計數器,瀏覽器會隱藏監視器的實際像素密度,並假設使用者螢幕為 96 DPI 螢幕。CSS 中的 px 單位代表這個虛擬 96 DPI 螢幕上的一個像素大小,因此稱為「CSS Pixel」。這個單位只會用於測量和定位。實際轉譯作業發生前,系統便完成一次實體像素轉換。

如何將這個虛擬螢幕移至使用者的實際螢幕?請輸入 devicePixelRatio。這個全域值代表構成單一 CSS 像素所需的「實體」像素數量。如果 devicePixelRatio (dPR) 為 1,表示您使用的是約 96 DPI 的螢幕。如果您使用的是 Retina 螢幕,DPR 可能是 2。在手機上,可能會遇到較高 (和奇怪) 的 dPR 值,例如 232.65。請注意,這個值是精確,但無法讓您推斷監控的「實際」DPI 值2 的 dPR 表示 1 個 CSS 像素可對應至「完全」2 個實體像素。

範例
根據 Chrome,我的螢幕顯示 1 的 dPR...

寬度為 3440 像素,顯示區域寬度為 79 公分。 解析度為 110 DPI。接近 96,但不是很正確。 這也是為什麼在大多數螢幕上,<div style="width: 1cm; height: 1cm"> 無法準確測量大小為 1 公分的原因。

最後,瀏覽器的縮放功能也會影響 dPR。放大時,瀏覽器會增加回報的 dPR,導致所有項目加大。如果在縮放時檢查開發人員工具控制台中的 devicePixelRatio,則可看到分數值。

開發人員工具顯示縮放後,顯示各種小數的 devicePixelRatio

讓我們在組合中加入 <canvas> 元素。您可以使用 widthheight 屬性來指定畫布使用的像素數量。因此 <canvas width=40 height=30> 是 40 x 30 像素的畫布。不過,這並不代表這個值可以設為 40 x 30 像素顯示。根據預設,畫布會使用 widthheight 屬性來定義其內建尺寸,但您可以使用自己熟悉且喜愛的所有 CSS 屬性,任意調整畫布大小。鑒於目前為止學到的教訓,有些情況可能不盡理想。畫布上的 1 像素可能會覆蓋多個實體像素,也可能只佔到實際像素的一小部分。這可能會導致視覺構件失真。

摘要:Canvas 元素會指定大小,用來定義您可以繪製的區域。畫布像素數量與畫布的顯示大小 (以 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() 呼叫產生的分數像素值。

CSS 像素屬於虛擬形式,理論上即使有一部分的像素沒有問題,但瀏覽器如何判斷與實體像素的對應情形?這是因為分數的「實體」像素並不是一切。

像素貼齊功能

單位轉換程序中用來將元素與實體像素對齊的單位稱為「像素貼齊」,它的運作方式如下:將小數像素值貼至整數的實體像素值。互動情形因瀏覽器而異。如果在 dPR 為 1 的螢幕上有一個寬度為 791.984px 的元素,其中一個瀏覽器可能會以 792px 實體像素為單位算繪元素,而其他瀏覽器則可能會在 791px 轉譯該元素。這只發生了 1 像素的差點,不過如果算繪出一個像素需要完美的像素,則不如單一像素好壞。這可能會導致模糊不清,或甚至更明顯的「莫列效應」等人造痕跡。

上圖是不同顏色像素的光柵圖片,下的圖片與上述相同,但寬度和高度利用雙線性縮放功能減少了 1 個像素。「新出現的模式」稱為「莫列效應」
(您可能需要在新分頁中開啟這張圖片,才能查看且不會套用任何縮放比例)。

devicePixelContentBox

devicePixelContentBox 會以裝置像素 (即實體像素) 為單位,提供元素的內容方塊。是 ResizeObserver 的一部分。雖然從 Safari 13.1 版開始,所有主要瀏覽器現在都支援 ResizeObserver,但 devicePixelContentBox 屬性目前僅適用於 Chrome 84 以上版本。

ResizeObserver 所述:這就像元素的 document.onresize,系統會在繪製前和版面配置後呼叫 ResizeObserver 的回呼函式。這表示回呼的 entries 參數會包含繪製前所有觀察元素的大小。就上述畫布問題而言,我們可以藉此機會調整畫布的像素數量,以確保畫布像素與實體像素之間的一對一對應。

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() 選項物件中的 box 屬性可讓您定義要觀察的大小。因此,雖然每個 ResizeObserverEntry 一律會提供 borderBoxSizecontentBoxSizedevicePixelContentBoxSize (前提是瀏覽器支援這項功能),但只有在任何「觀察到」方塊指標有變更時,系統才會叫用回呼。

有了這項新屬性,我們甚至可以在畫布大小和位置加上動畫效果 (有效保證部分像素值),而且對於算繪結果不會造成 Moiré 影響。如果你想查看 Moiré 對使用 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
}

結論

Pixel 在網路上是大家都想不到的主題,直到無法全面掌握使用者螢幕上的元素像素實際數量。ResizeObserverEntry 上的新 devicePixelContentBox 屬性可提供這項資訊,並可讓您透過 <canvas> 進行像素完美的算繪。Chrome 84 以上版本支援 devicePixelContentBox