畫布實際上有多少像素?
自 Chrome 84 起,ResizeObserver 支援名為 devicePixelContentBox
的新方塊測量,可測量元素在實體像素中的維度。這項功能可讓您以像素完美的品質算繪圖形,在高密度螢幕上尤其如此。
背景: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 值,例如 2
、3
,甚至是 2.65
。請務必注意,這個值是確切值,但無法用來推導螢幕的實際 DPI 值。dPR 為 2
表示 1 個 CSS 像素會完全對應 2 個實體像素。
1
…寬度為 3440 像素,顯示區域寬 79 公分。
因此解析度為 110 DPI。接近 96,但不是。
這也是為什麼在大多數螢幕上,<div style="width: 1cm; height: 1cm">
不會剛好是 1 公分。
最後,瀏覽器的縮放功能也會影響 dPR。如果放大畫面,瀏覽器會提高回報的 dPR,導致所有內容都放大。在縮放時檢查開發人員工具控制台中的 devicePixelRatio
,您會看到分數值。

devicePixelRatio
。現在加入 <canvas>
元素。您可以使用 width
和 height
屬性,指定畫布要擁有的像素數量。因此 <canvas width=40 height=30>
會是 40 x 30 像素的畫布。不過,這不代表系統會以 40 x 30 像素的尺寸顯示圖片。根據預設,畫布會使用 width
和 height
屬性定義其內建大小,但您可以使用所有已知的 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
一律會提供 borderBoxSize
、contentBoxSize
和 devicePixelContentBoxSize
(前提是瀏覽器支援),但只有在任何觀察到的方塊指標變更時,才會叫用回呼。
有了這項新屬性,我們甚至可以為畫布的大小和位置製作動畫 (有效保證分數像素值),且不會在算繪時看到任何疊紋效果。如要查看使用 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
。