畫布「實際」有多少像素?
自 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
,表示你使用的是約 96 DPI 的螢幕。如果您使用的是 Retina 螢幕,dPR 可能為 2
。在手機上,您可能會遇到較高的 (且較奇怪) dPR 值,例如 2
、3
,甚至是 2.65
。請注意,這個值是「完全」,但不可讓您算出監控器的「實際」DPI 值。2
的 dPR 表示 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 像素數量與實體像素數量不同。
像素完美
在某些情況下,最好能精確對應畫布像素和實體像素。如果達成這種對應,就稱為「像素完美」。對於文字能否清楚顯示而言,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%
的元素最終可能變成如下矩形:
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
一律會提供 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
}
結論
Pixel 在網路上是一個非常複雜的主題,一直以來都無法得知元素在使用者螢幕上所佔的確切像素數量。ResizeObserverEntry
上的新 devicePixelContentBox
屬性可提供這項資訊,並讓您使用 <canvas>
進行完美的像素算繪。devicePixelContentBox
適用於 Chrome 84 以上版本。