使用 devicePixelContentBox 实现像素完美的渲染
从 Chrome 84 开始,ResizeObserver 支持一种名为 devicePixelContentBox
的新框测量,以测量元素的尺寸(以物理像素为单位)。这样可以渲染像素完美的图形,尤其是在高密度屏幕环境中。
背景:CSS 像素、画布像素和物理像素
虽然我们经常使用 em
、%
或 vh
等抽象长度单位,但这一切都可以归结为像素。每当我们在 CSS 中指定元素的大小或位置时,浏览器的布局引擎最终都会将该值转换为像素 (px
)。这些像素就是“CSS 像素”,这些像素有着大量的历史记录,只与屏幕上的像素存在松散关系。
长久以来,用 96DPI(“每英寸的点数”)估算任何人的屏幕像素密度是相当合理的,这意味着任何给定显示器的每厘米大约有 38 像素。随着时间的推移,显示器的同一表面区域出现像素变大和/或缩小,或像素数量开始增多。再加上网络上的许多内容都会以 px
定义尺寸,包括字体大小,最终会导致这些高密度(“HiDPI”)屏幕上的文字难以辨认。作为应对措施,浏览器会隐藏显示器的实际像素密度,改为假定用户的显示屏分辨率为 96 DPI。CSS 中的 px
单位表示此虚拟 96 DPI 屏幕上的一个像素大小,因此名称为“CSS Pixel”。该单位仅用于测量和定位。在实际呈现之前,会先转换为物理像素。
如何从虚拟显示屏转到用户的真实显示屏?输入 devicePixelRatio
。通过该全局值,您可以确定单个 CSS 像素需要多少个物理像素。如果 devicePixelRatio
(dPR) 为 1
,则表示您正在使用的是具有约 96DPI 的显示器。如果您有视网膜屏幕,您的 dPR 可能为 2
。在手机上,遇到更高(且更奇怪)的 dPR 值(例如 2
、3
甚至 2.65
)并不罕见。请务必注意,此值精确,但您无法推导出显示器的实际 DPI 值。dPR 2
表示 1 个 CSS 像素将恰好映射到 2 个物理像素。
1
…最后,dPR 还可能受到浏览器缩放功能的影响。如果放大,浏览器会增加报告的 dPR,导致所有内容呈现得更大。如果您在缩放时在开发者工具控制台中查看 devicePixelRatio
,可以看到小数值。
下面,我们将 <canvas>
元素添加到组合中。您可以使用 width
和 height
属性指定画布所需的像素数。因此,<canvas width=40 height=30>
将是一张 40×30 像素的画布。不过,这并不意味着视频将以 40×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 像素纯粹是虚拟的,因此在理论上,一个像素的小数部分是没问题的,但浏览器如何计算到物理像素的映射呢?因为小数“物理像素”并不存在,
像素贴靠
单位转换流程中负责将元素与物理像素对齐的部分称为“像素贴靠”,其作用是将部分像素值贴靠到整数物理像素值。具体展示方式因浏览器而异。如果屏幕上有一个宽度为 791.984px
的元素,并且 dPR 为 1,则一个浏览器可能会以 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
。