当前标记的答案是错误的
2022 年,我们无法在所有设备、所有浏览器上获得像素完美的画布。
您可能认为它正在使用
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
}
但要看到它失败,您所要做的就是按“放大”/“缩小”。缩放并不是唯一失败的地方。许多设备和操作系统都有小数 devicePixelRatio。
换句话说。假设您制作了 100x100 的画布(放大)。
![100x100 image canvas](https://i.stack.imgur.com/0OV9t.png)
用户的devicePixelRatio是1.25(这就是我的台式电脑)。然后,画布将以 125x125 像素显示,而您的 100x100 像素画布将通过最近邻过滤(图像渲染:像素化)缩放到 125x125,并且看起来非常糟糕,因为某些像素为 1x1 像素大,而其他像素为 2x2 像素大。
![enter image description here](https://i.stack.imgur.com/YU4hK.png)
所以不,使用
image-rendering: pixelated;
image-rendering: crisp-edges;
其本身并不是一个解决方案。
更差。浏览器可以以小数形式工作,但设备不能。例子:
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);
}
.split {
width: 299px;
display: flex;
}
.split>* {
flex: 1 1 auto;
height: 50px;
}
.left {
background: pink;
}
.middle {
background: lightgreen;
}
.right {
background: lightblue;
}
pre { margin: 0; }
<div class="split">
<div class="left"></div>
<div class="middle"></div>
<div class="right"></div>
</div>
上面的示例有 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
把它们加起来,你可能会发现一个问题。根据getBoundingClientRect
每个宽度约为设备像素的 1/3 (199.328125)。实际上您不可能拥有 0.328125 个设备像素,因此需要将它们转换为整数。让我们使用Math.round
所以他们都变成了199。
199 + 199 + 199 = 597
但根据浏览器,父级的大小为 598 设备像素大。缺失的像素在哪里?
我们去问问吧
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);
}
.split {
width: 299px;
display: flex;
}
.split>* {
flex: 1 1 auto;
height: 50px;
}
.left {
background: pink;
}
.middle {
background: lightgreen;
}
.right {
background: lightblue;
}
pre { margin: 0; }
<div class="split">
<div class="left"></div>
<div class="middle"></div>
<div class="right"></div>
</div>
上面的代码要求使用ResizeObserver
and devicePixelContentBoxSize
目前仅在 Chromium 浏览器和 Firefox 上支持。对我来说,中间的 div 得到了额外的像素
left : width = 199
middle: width = 200
right : width = 199
所有这一切的重点是
- 你不能只是天真地设定
image-rendering: pixelated; image-rendering: crisp-edges
- 如果您想知道画布中有多少像素,您可能需要询问,并且只能在 Chrome/Firefox ATM 上找到
解决方案:TL;DR2022 年没有跨浏览器解决方案
-
在 Chrome/Firefox 中,您可以使用上面的 ResizeObserver 解决方案。
-
在 Chrome 和 Firefox 中,您还可以计算分数大小的画布
换句话说。典型的“填充页面”画布
<style>
html, body, canvas { margin: 0; width: 100%; height: 100%; display: block; }
<style>
<body>
<canvas>
</canvas>
<body>
不管用。请参阅上面的原因。但你可以这样做
- 获取容器的大小(在本例中为主体)
- 乘以 devicePixelRatio 并向下舍入
- 除以 devicePixelRatio
- 使用该值作为画布 CSS 宽度和高度,使用 #2 值作为画布宽度和高度。
这最终会在右侧和底部边缘留下尾部间隙
1 到 3 个设备像素,但它应该为您提供 1x1 像素的画布。
const px = v => `${v}px`;
const canvas = document.querySelector('canvas');
resizeCanvas(canvas);
draw(canvas);
window.addEventListener('resize', () => {
resizeCanvas(canvas);
draw(canvas);
});
function resizeCanvas(canvas) {
// how many devicePixels per pixel in the canvas we want
// you can set this to 1 if you always want 1 device pixel to 1 canvas pixel
const pixelSize = Math.max(1, devicePixelRatio) | 0;
const rect = canvas.parentElement.getBoundingClientRect();
const deviceWidth = rect.width * devicePixelRatio | 0;
const deviceHeight = rect.height * devicePixelRatio | 0;
const pixelsAcross = deviceWidth / pixelSize | 0;
const pixelsDown = deviceHeight / pixelSize | 0;
const devicePixelsAcross = pixelsAcross * pixelSize;
const devicePixelsDown = pixelsDown * pixelSize;
canvas.style.width = px(devicePixelsAcross / devicePixelRatio);
canvas.style.height = px(devicePixelsDown / devicePixelRatio);
canvas.width = pixelsAcross;
canvas.height = pixelsDown;
}
function draw(canvas) {
const ctx = canvas.getContext('2d');
for (let x = 0; x < canvas.width; ++x) {
let h = x % (canvas.height * 2);
if (h > canvas.height) {
h = 2 * canvas.height - h;
}
ctx.fillStyle = 'lightblue';
ctx.fillRect(x, 0, 1, h);
ctx.fillStyle = 'black';
ctx.fillRect(x, h, 1, 1);
ctx.fillStyle = 'pink';
ctx.fillRect(x, h + 1, 1, canvas.height - h);
}
}
html, body {
margin: 0;
width: 100%;
height: 100%;
/* set to red we can see the gap */
/* set to some other color to hide the gap */
background: red;
}
canvas {
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
<canvas></canvas>
在 Safari 中没有解决方案:Safari 不提供以下支持devicePixelContentBoxSize
它也不调整devicePixelRatio
响应缩放。从好的方面来说,Safari 永远不会返回小数devicePixelRatio
,至少从 2022 年开始,但如果用户缩放,您将无法在 Safari 上获得 1x1 设备像素。
有一个提议CSS 属性image-resolution,如果设置为snap
将有助于解决 CSS 中的这个问题。不幸的是,截至 2023 年 5 月,它还没有在任何浏览器中实现。