畫布「實際」有多少像素?
自 Chrome 84 起,ResizeObserver 支援名為 devicePixelContentBox
的新盒子測量方式,可以實體像素測量元素的尺寸。這可讓您算繪像素完美的圖形,尤其是在高密度螢幕的情況下。
背景:CSS 像素、畫布像素和實體像素
雖然我們經常使用抽象的長度單位,例如 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 值,例如 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
,您會看到小數值出現。
讓我們在混合中加入 <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%
的元素最終會變成如下所示的矩形:
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
都會一律提供 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
。