在所有设备上显示像素完美的画布

Max*_*Max 7 html html5-canvas

我有一些画布,我想在每个(现代)浏览器中以像素完美的方式显示它们。默认情况下,具有高 DPI 屏幕的设备会缩放我的页面,以便所有内容看起来都大小正确,但它破坏了*我画布的外观。

如何确保画布中的一个像素 = 屏幕上的一个像素?最好这不会影响页面上的其他元素,因为我仍然希望例如文本能够针对设备进行适当缩放。

我已经尝试根据 设计画布尺寸window.devicePixelRatio。这使得画布尺寸合适,但内容看起来更糟。我猜这只是在它们已经错误地放大之后缩小它们。

*如果你关心的话,因为画布使用抖动并且浏览器正在执行某种 lerp 而不是最近邻居

gma*_*man 8

当前标记的答案是错误的

2022 年,我们无法在所有设备、所有浏览器上获得像素完美的画布。

您可能认为它正在使用

canvas {
    image-rendering: pixelated;
    image-rendering: crisp-edges;
}
Run Code Online (Sandbox Code Playgroud)

但要看到它失败,您所要做的就是按“放大”/“缩小”。缩放并不是唯一失败的地方。许多设备和操作系统都有小数 devicePixelRatio。

换句话说。假设您制作了 100x100 的画布(放大)。

100x100 图像画布

用户的devicePixelRatio是1.25(这就是我的台式电脑)。然后,画布将以 125x125 像素显示,而您的 100x100 像素画布将通过最近邻过滤(图像渲染:像素化)缩放至 125x125,并且看起来非常糟糕,因为某些像素为 1x1 像素大,而其他像素为 2x2 像素大。

在此输入图像描述

所以不,使用

image-rendering: pixelated;
image-rendering: crisp-edges;
Run Code Online (Sandbox Code Playgroud)

其本身并不是一个解决方案。

更差。浏览器可以以小数形式工作,但设备不能。例子:

canvas {
    image-rendering: pixelated;
    image-rendering: crisp-edges;
}
Run Code Online (Sandbox Code Playgroud)
image-rendering: pixelated;
image-rendering: crisp-edges;
Run Code Online (Sandbox Code Playgroud)
let totalWidth = 0;
let totalDeviceWidth = 0;
document.querySelectorAll(".split>*").forEach(elem => {
  const rect = elem.getBoundingClientRect();
  const width = rect.width;
  const deviceWidth = width * devicePixelRatio;
  totalWidth += width;
  totalDeviceWidth += deviceWidth;
  log(`${elem.className.padEnd(6)}: width = ${width}, deviceWidth: ${deviceWidth}`);
});

const elem = document.querySelector('.split');
const rect = elem.getBoundingClientRect();
const width = rect.width;
const deviceWidth = width * devicePixelRatio;

log(`
totalWidth      : ${totalWidth}
totalDeviceWidth: ${totalDeviceWidth}

elemWidth       : ${width}
elemDeviceWidth : ${deviceWidth}`);

function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = args.join(' ');
  document.body.appendChild(elem);
}
Run Code Online (Sandbox Code Playgroud)

上面的示例有 299 CSS 像素宽的 div,内部有 3 个子 div,每个子 div 占其父级的 1/3。getBoundingClientRect通过在 Chrome 102 中调用我的 MacBook Pro来询问尺寸,我得到

left  : width = 99.6640625, deviceWidth: 199.328125
middle: width = 99.6640625, deviceWidth: 199.328125
right : width = 99.6640625, deviceWidth: 199.328125

totalWidth      : 298.9921875
totalDeviceWidth: 597.984375

elemWidth       : 299
elemDeviceWidth : 598
Run Code Online (Sandbox Code Playgroud)

把它们加起来,你可能会发现一个问题。根据getBoundingClientRect每一个大约是设备像素的 1/3 宽度 (199.328125)。实际上,您不可能拥有 0.328125 个设备像素,因此需要将它们转换为整数。让我们使用Math.round这样它们都变成199。

199 + 199 + 199 = 597
Run Code Online (Sandbox Code Playgroud)

但根据浏览器,父级的大小为 598 设备像素大。缺失的像素在哪里?

我们去问问吧

.split {
  width: 299px;
  display: flex;
}
.split>* {
  flex: 1 1 auto;
  height: 50px;
}
.left {
  background: pink;
}
.middle {
  background: lightgreen;
}
.right {
  background: lightblue;
}
pre { margin: 0; } 
Run Code Online (Sandbox Code Playgroud)
<div class="split">
  <div class="left"></div>
  <div class="middle"></div>
  <div class="right"></div>
</div>
  
Run Code Online (Sandbox Code Playgroud)
left  : width = 99.6640625, deviceWidth: 199.328125
middle: width = 99.6640625, deviceWidth: 199.328125
right : width = 99.6640625, deviceWidth: 199.328125

totalWidth      : 298.9921875
totalDeviceWidth: 597.984375

elemWidth       : 299
elemDeviceWidth : 598
Run Code Online (Sandbox Code Playgroud)

上面的代码询问使用ResizeObserveranddevicePixelContentBoxSize目前仅在 Chromium 浏览器和 Firefox 上支持。对我来说,中间的 div 得到了额外的像素

left  : width = 199
middle: width = 200
right : width = 199
Run Code Online (Sandbox Code Playgroud)

所有这一切的重点是

  1. 你不能只是天真地设定image-rendering: pixelated; image-rendering: crisp-edges
  2. 如果您想知道画布中有多少像素,您可能需要询问,并且只能在 Chrome/Firefox ATM 上找到

解决方案:TL;DR 2022 年没有跨浏览器解决方案

  • 在 Chrome/Firefox 中,您可以使用上面的 ResizeObserver 解决方案。

  • 在 Chrome 和 Firefox 中,您还可以计算分数大小的画布

    换句话说。典型的“填充页面”画布

    <style>
    html, body, canvas { margin: 0; width: 100%; height: 100%; display: block; }
    <style>
    <body>
      <canvas>
      </canvas>
    <body>
    
    Run Code Online (Sandbox Code Playgroud)

    不管用。请参阅上面的原因。但你可以这样做

    1. 获取容器的大小(在本例中为主体)
    2. 乘以 devicePixelRatio 并向下舍入
    3. 除以 devicePixelRatio
    4. 使用该值作为画布 CSS 宽度和高度,使用 #2 值作为画布的宽度和高度。

    这最终会在右侧和底部边缘留下 1 到 3 个设备像素的尾部间隙,但它应该为您提供 1x1 像素的画布。

199 + 199 + 199 = 597
Run Code Online (Sandbox Code Playgroud)
const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    let good = false;
    if (entry.devicePixelContentBoxSize) {
      // NOTE: Only this path gives the correct answer
      // The other paths are imperfect fallbacks
      // for browsers that don't provide anyway to do this
      width = entry.devicePixelContentBoxSize[0].inlineSize;
      height = entry.devicePixelContentBoxSize[0].blockSize;
      good = true;
    } else {
      if (entry.contentBoxSize) {
        if (entry.contentBoxSize[0]) {
          width = entry.contentBoxSize[0].inlineSize;
          height = entry.contentBoxSize[0].blockSize;
        } else {
          width = entry.contentBoxSize.inlineSize;
          height = entry.contentBoxSize.blockSize;
        }
      } else {
        width = entry.contentRect.width;
        height = entry.contentRect.height;
      }
      width *= devicePixelRatio;
      height *= devicePixelRatio;
    }
    log(`${entry.target.className.padEnd(6)}: width = ${width} measure = ${good ? "good" : "bad (not accurate)"}`);    
  }
});

document.querySelectorAll('.split>*').forEach(elem => {
  observer.observe(elem);
});

function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = args.join(' ');
  document.body.appendChild(elem);
}
Run Code Online (Sandbox Code Playgroud)
.split {
  width: 299px;
  display: flex;
}
.split>* {
  flex: 1 1 auto;
  height: 50px;
}
.left {
  background: pink;
}
.middle {
  background: lightgreen;
}
.right {
  background: lightblue;
}
pre { margin: 0; }
Run Code Online (Sandbox Code Playgroud)

在 Safari 中,没有解决方案:Safari 既不支持缩放devicePixelContentBoxSize,也不调整缩放devicePixelRatio以响应缩放。从好的方面来说,Safari 永远不会返回小数devicePixelRatio,至少从 2022 年开始是这样,但如果用户缩放,您将不会在 Safari 上获得 1x1 设备像素。

有一个关于CSS 属性的image-resolution提案,如果将其设置为 ,snap将有助于解决 CSS 中的这个问题。不幸的是,截至 2023 年 5 月,它还没有在任何浏览器中实现。