透過 devicePixelContentBox 完美呈現象素風格

畫布實際上有多少像素?

自 Chrome 84 起,ResizeObserver 支援名為 devicePixelContentBox 的新方塊測量,可測量元素在實體像素中的維度。這項功能可讓您以像素完美的品質算繪圖形,在高密度螢幕上尤其如此。

Browser Support

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93.
  • Safari: not supported.

Source

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

雖然我們經常使用 em%vh 等抽象長度單位,但最終還是會歸結為像素。每當我們在 CSS 中指定元素的大小或位置時,瀏覽器的版面配置引擎最終都會將該值轉換為像素 (px)。這些是「CSS 像素」,具有悠久的歷史,且與螢幕上的像素只有鬆散的關係。

在很長一段時間內,以 96 DPI (每英吋點數) 估算任何人的螢幕像素密度都相當合理,也就是說,任何螢幕每公分大約有 38 像素。隨著時間推移,螢幕變大/變小,或在相同表面積上開始有更多像素。此外,網路上許多內容都會以 px 定義尺寸 (包括字型大小),因此在這些高密度 (「HiDPI」) 螢幕上,文字會變得難以辨識。為防範這類攻擊,瀏覽器會隱藏螢幕的實際像素密度,並假裝使用者擁有 96 DPI 的螢幕。CSS 中的 px 單位代表這個虛擬 96 DPI 螢幕上一個像素的大小,因此稱為「CSS 像素」。這個單位僅用於測量和定位。在實際算繪前,系統會先轉換為實體像素。

如何從這個虛擬螢幕轉移到使用者的實際螢幕?輸入 devicePixelRatio。這個全域值會告訴您需要多少實體像素才能形成單一 CSS 像素。如果 devicePixelRatio (dPR) 為 1,表示您使用的螢幕大約為 96 DPI。如果是 Retina 螢幕,dPR 可能為 2。在手機上,您可能會遇到較高 (且較奇怪) 的 dPR 值,例如 23,甚至是 2.65。請務必注意,這個值是確切值,但無法用來推導螢幕的實際 DPI 值。dPR 為 2 表示 1 個 CSS 像素會完全對應 2 個實體像素。

範例
根據 Chrome,我的螢幕 dPR 為 1

寬度為 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 屬性任意調整畫布大小。根據目前所學內容,您可能會發現這並非適用於所有情境的理想做法。畫布上的一個像素可能最終會涵蓋多個實體像素,或僅涵蓋一小部分的實體像素。這可能會導致不悅目的視覺效果。

總結來說,畫布元素具有特定大小,可定義可繪製的區域。畫布像素數量完全獨立於以 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 算繪。這只差了一個像素,但如果需要精準呈現,一個像素的差異就可能造成損害。這可能會導致影像模糊,甚至出現更明顯的失真,例如疊紋效應

頂端圖片是不同顏色像素的點陣圖。下方的圖片與上方相同,但寬度和高度都減少了一像素,並使用雙線性縮放。這種新興模式稱為莫瑞效應。
(您可能必須在新分頁中開啟這張圖片,才能看到未套用任何縮放比例的圖片。)

devicePixelContentBox

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

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']});

選項物件的 box 屬性 (適用於 observer.observe()) 可讓您定義要觀察的大小。因此,雖然每個 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> 進行像素完美的算繪作業。Chrome 84 以上版本支援 devicePixelContentBox