画布中实际有多少像素?
从 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,导致所有内容都呈现得更大。如果您在缩放时在 DevTools 控制台中选中 devicePixelRatio
,则会看到出现小数值。
我们来添加 <canvas>
元素。您可以使用 width
和 height
属性指定画布应具有的像素数。因此,<canvas width=40 height=30>
将是一个尺寸为 40 x 30 像素的画布。不过,这并不意味着它会以 40 x 30 像素的尺寸显示。默认情况下,画布将使用 width
和 height
属性来定义其固有尺寸,但您可以使用自己熟悉的所有 CSS 属性来任意调整画布的大小。通过我们到目前为止学到的所有内容,您可能会发现,这种方法并不适用于所有场景。画布上的某个像素最终可能会覆盖多个实体像素,也可能只覆盖实体像素的一小部分。这可能会导致不美观的视觉伪影。
总结一下:画布元素具有给定大小,用于定义可绘制区域。画布像素数完全独立于以 CSS 像素为单位指定的画布的显示大小。CSS 像素数与实际像素数不同。
像素精致
在某些情况下,最好能将画布像素与实际像素进行精确映射。如果实现了这种映射,就称为“像素级完美”。像素级完美呈现对于清晰呈现文本至关重要,尤其是在使用亚像素渲染或显示亮度交替的紧密对齐线条的图形时。
为了在 Web 上实现尽可能接近像素级精确的画布效果,以下方法或多或少是首选方法:
<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 个像素,但对于需要像素级完美的渲染而言,1 个像素也可能造成不利影响。这可能会导致模糊不清,甚至导致更明显的伪影,例如莫尔埃效应。
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()
的 options 对象中的 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
}
总结
像素是 Web 上一个令人惊讶的复杂主题,到目前为止,您还无法知道某个元素在用户屏幕上占用的确切物理像素数。ResizeObserverEntry
上的新 devicePixelContentBox
属性可为您提供这项信息,并允许您使用 <canvas>
进行像素级渲染。Chrome 84 及更高版本支持 devicePixelContentBox
。