自 Chrome 84 以来,ResizeObserver支持一种名为 的新框测量,它以物理devicePixelContentBox像素为单位测量元素的尺寸。这使得渲染像素完美的图形成为可能,尤其是在高密度屏幕的情况下。

浏览器支持

  • 84
  • 93
  • 84
  • ×

背景:CSS 像素、画布像素和物理像素#

虽然我们经常使用抽象的长度单位em,如 、%vh,但它们都归结为像素。每当我们在 CSS 中指定元素的大小或位置时,浏览器的布局引擎最终会将那个值转换为像素 ( px)。这些是“CSS 像素”,它们有很多历史并且与屏幕上的像素只有松散的关系。

长期以来,用 96DPI(“每英寸点数”)来估计任何人的屏幕像素密度是相当合理的,这意味着任何给定的显示器每厘米大约有 38 个像素。随着时间的推移,显示器会变大和/或缩小或开始在同一表面区域具有更多像素。结合 web 上的许多内容定义其尺寸(包括字体大小)这一事实px,我们最终会在这些高密度(“HiDPI”)屏幕上看到难以辨认的文本。作为一种反制措施,浏览器会隐藏显示器的实际像素密度,并假装用户拥有 96 DPI 显示器。CSS中的单位表示这个虚拟px上一个像素的大小96 DPI 显示,因此得名“CSS Pixel”。本机仅用于测量和定位。在任何实际渲染发生之前,会发生到物理像素的转换。

我们如何从这个虚拟展示到用户的真实展示?输入devicePixelRatio。这个全局值告诉您需要多少个物理像素才能形成一个 CSS 像素。如果devicePixelRatio(dPR) 是1,则您正在使用大约 96DPI 的显示器。如果你有视网膜屏幕,你的 dPR 可能是22在手机上,遇到更高(和更奇怪)的 dPR 值(例如,3甚至 )并不少见2.65。必须注意,此值是准确的,但不会让您得出显示器的实际DPI 值。dPR 意味着1 个 CSS 像素将恰好2映射到2 个物理像素。

例子

1根据 Chrome,我的显示器的 dPR 为……

最后,dPR 也会受到浏览器缩放功能的影响。如果放大,浏览器会增加报告的 dPR,导致一切都呈现更大。如果您devicePixelRatio在缩放时签入 DevTools 控制台,您会看到分数值出现。

devicePixelRatioDevTools因缩放而显示各种小数。

让我们将<canvas>元素添加到组合中。width您可以使用和属性指定希望画布具有多少像素height<canvas width=40 height=30>40 x 30 像素的画布也是如此。但是,这并不意味着它将以 40 x 30 像素显示。默认情况下,画布将使用widthheight属性来定义其固有大小,但您可以使用您了解和喜爱的所有 CSS 属性任意调整画布的大小。到目前为止,我们所学到的一切,您可能会想到这并不是在所有情况下都是理想的。画布上的一个像素最终可能会覆盖多个物理像素,或者只是一个物理像素的一小部分。这会导致令人不快的视觉伪影。

总结一下:Canvas 元素有一个给定的大小来定义您可以绘制的区域。画布像素的数量完全独立于以 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%可以以这样的矩形结尾:

DevTools 显示作为调用结果的小数像素值getBoundingClientRect()

CSS 像素是纯虚拟的,所以理论上像素的分数是可以的,但浏览器如何确定到物理像素的映射?因为分数物理像素不是东西。

像素捕捉#

单位转换过程中负责将元素与物理像素对齐的部分称为“像素捕捉”,它按照罐头上的说明进行操作:它将小数像素值捕捉为整数、物理像素值。具体如何发生因浏览器而异。791.984px如果我们在 dPR 为 1 的显示器上有一个宽度为 的元素,一个浏览器可能会以792px物理像素呈现该元素,而另一个浏览器可能会以791px. 这只是一个像素的偏差,但单个像素可能会对需要像素完美的渲染产生不利影响。这会导致模糊或更明显的瑕疵,如莫尔效应

顶部图像是不同颜色像素的光栅。底部图像与上图相同,但使用双线性缩放将宽度和高度减少了一个像素。出现的图案称为莫尔效应。
(您可能必须在新选项卡中打开此图像才能看到它没有应用任何缩放。)

devicePixelContentBox #

devicePixelContentBox以设备像素(即物理像素)为单位为您提供元素的内容框。它的一部分ResizeObserver。虽然自 Safari 13.1 以来所有主流浏览器现在都支持 ResizeObserverdevicePixelContentBox ,但该属性目前仅在 Chrome 84+ 中可用。

ResizeObserver就像在document.onresizeelements中提到的,a 的回调函数ResizeObserver将在 paint 之前和 layout 之后被调用。这意味着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
}

结论#

像素在 web 上是一个非常复杂的话题,直到现在,您还没有办法知道元素在用户屏幕上占据的物理像素的确切数量。devicePixelContentBoxa 上的新属性ResizeObserverEntry为您提供了那条信息,并允许您使用 进行像素完美的渲染<canvas>devicePixelContentBoxChrome 84+ 支持。

使用 devicePixelContentBox 进行像素完美渲染