透過 devicePixelContentBox 完美呈現象素風格

畫布「實際」有多少像素?

自 Chrome 第 84 版起,ResizeObserver 支援名為 devicePixelContentBox 的新方塊測量,能夠以實際像素為單位測量元素的尺寸。這可以呈現完美像素的圖像,尤其是在高密度螢幕的情況下。

瀏覽器支援

  • Chrome:84.
  • Edge:84。
  • Firefox:93。
  • Safari:不支援。

資料來源

我們通常採用長度的抽象單位 (例如 em%vh),但會得到像素。每當我們在 CSS 中指定元素的大小或位置時,瀏覽器的版面配置引擎最終就會將該值轉換為像素 (px)。這些是「CSS 像素」,這個像素有大量記錄,而且與您的螢幕上的像素關係過於鬆散。

長久以來,我們都會以 96DPI (「每英寸像素數」) 來估算螢幕像素密度,也就是說,任何螢幕大約都有每公分 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 值。2 的 dPR 表示 1 CSS 像素會剛好對應到 2 個實體像素。

寬度為 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 像素數量與實體像素數量不同。

像素完美

在某些情況下,最好能精確對應畫布像素和實體像素。如果達成這種對應,就稱為「像素完美」。對於文字能否清楚顯示而言,Pixel 完美轉譯功能是確保文字清晰易讀的關鍵,特別是使用子像素轉譯,或是顯示交替亮度有線對齊的圖像時。

為了在網頁上盡可能達到像素完美的畫布,以下是幾乎是首選做法:

<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 個像素,但單一像素在算繪時可能需要完美像素。這可能會導致模糊不清,或 Moiré 特效等更清晰的構件。

上方圖片是不同顏色像素的光柵。下方的圖片與上述圖片相同,但寬度和高度已使用雙線性縮放功能縮減一個像素。這種圖案稱為莫列效應。
(您可能需要在新分頁中開啟這張圖片,才能查看未套用任何縮放比例的圖片。)

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 (前提是瀏覽器支援這項功能),但只有在任何「觀測」方塊指標有所變更時,才會叫用回呼。

有了這個新屬性,我們甚至可以為畫布的大小和位置製作動畫 (有效保證像素值的部分值),且不會在算繪時看到任何摩爾紋效果。如要查看使用 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> 進行完美的像素算繪。devicePixelContentBox 適用於 Chrome 84 以上版本。