透過 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,表示您使用的螢幕大約為 96DPI。如果您使用的是 Retina 螢幕,dPR 可能為 2。在手機上,您可能會遇到較高的 (且較奇怪) dPR 值,例如 23,甚至是 2.65。請務必注意,這個值是「精確」的,但無法讓您推導出螢幕的「實際」DPI 值。如果 dPR 為 2,表示 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 像素數量與實體像素數量不同。

像素完美

在某些情況下,建議您將畫布像素與實體像素進行精確對應。如果達成這種對應,就稱為「像素完美」。若要讓文字清楚呈現,完美像素轉譯功能至關重要,尤其是在使用子像素轉譯或顯示亮度交替的緊密對齊線條圖形時。

為了在網頁上盡可能達到像素完美的畫布,我們通常會採用以下做法:

<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,但 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
}

結論

在網路上,像素是一個相當複雜的議題,而且到目前為止,您無法得知元素在使用者螢幕上所占用的確切像素數量。ResizeObserverEntry 上的新 devicePixelContentBox 屬性可提供該項資訊,並讓您使用 <canvas> 進行完美的像素算繪。Chrome 84 以上版本支援 devicePixelContentBox