许多示例都显示使用
ResizeObserver
类似的东西
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
function draw() {
const { width, height } = canvas;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(width / 2, height / 2);
const size = Math.min(width, height);
ctx.beginPath();
ctx.arc(0, 0, size / 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
canvas.width = entry.contentBoxSize[0].inlineSize;
canvas.height = entry.contentBoxSize[0].blockSize;
draw();
}
})
observer.observe(canvas);
html, body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<canvas></canvas>
这有效。
ResizeObserver
将在启动时触发一次,然后在画布显示尺寸发生变化时触发
但是,如果你切换到
requestAnimationFrame
循环,你会看到一个问题
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
function draw() {
const { width, height } = canvas;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(width / 2, height / 2);
const size = Math.min(width, height);
ctx.beginPath();
ctx.arc(0, 0, size / 3, 0, Math.PI * 2);
ctx.fill();
ctx.rotate(performance.now() * 0.001);
ctx.strokeStyle = 'red';
ctx.strokeRect(-5, -5, size / 4, 10);
ctx.restore();
}
function rAFLoop() {
draw();
requestAnimationFrame(rAFLoop);
}
requestAnimationFrame(rAFLoop);
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
canvas.width = entry.contentBoxSize[0].inlineSize;
canvas.height = entry.contentBoxSize[0].blockSize;
}
})
observer.observe(canvas);
html, body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<canvas></canvas>
上面,我所做的就是从
ResizeObserver
中进行调用并在 requestAnimationFrame
循环中调用它(并添加了一些动作)
如果现在调整窗口大小,您会看到圆圈闪烁
我该如何解决这个问题?
这里的问题是规范说
ResizeObserver
回调发生在requestAnimationFrame回调之后。这意味着上例中的操作顺序是
解决方案 1:也画入
ResizeObserver
。
解决方案 2:不要使用
ResizeObserver
canvas.clientWidth
、
canvas.clientHeight
(返回整数)、(2)
canvas.getBoundingClientRect()
(返回有理数)例如:
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
function draw() {
const { width, height } = canvas;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(width / 2, height / 2);
const size = Math.min(width, height);
ctx.beginPath();
ctx.arc(0, 0, size / 3, 0, Math.PI * 2);
ctx.fill();
ctx.rotate(performance.now() * 0.001);
ctx.strokeStyle = 'red';
ctx.strokeRect(-5, -5, size / 4, 10);
ctx.restore();
}
function rAFLoop() {
// Only set canvas.width and canvas.height
// if they need to be set because setting them is a
// heavy operation
if (canvas.width !== canvas.clientWidth ||
canvas.height !== canvas.clientHeight) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
draw();
requestAnimationFrame(rAFLoop);
}
requestAnimationFrame(rAFLoop);
html, body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<canvas></canvas>
调整窗口大小,您会发现它不再闪烁
此解决方案可行,但这些方法(
canvas.clientWidth/height
和/或
getBoundingClientRect
)仅返回 CSS 像素,而不返回设备像素。您可以将任一数字乘以
devicePixelRatio
,但这也不会始终为您提供正确的答案。您可以在此答案中阅读原因:https://stackoverflow.com/a/72611819/128511 解决方案 3:检查 rAF 画布的大小是否要更改,如果要更改则不要渲染,而是在
ResizeObserver
回调中渲染。
getBoundingClientRect
改变大小,画布是否会调整大小。即使我们无法正确地从
getBoundingClientRect
转换为设备像素,但如果画布大小已调整,该值将比我们上次调用它时发生变化。
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
function draw() {
const { width, height } = canvas;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(width / 2, height / 2);
const size = Math.min(width, height);
ctx.beginPath();
ctx.arc(0, 0, size / 3, 0, Math.PI * 2);
ctx.fill();
ctx.rotate(performance.now() * 0.001);
ctx.strokeStyle = 'red';
ctx.strokeRect(-5, -5, size / 4, 10);
ctx.restore();
}
let lastSize = {};
function rAFLoop() {
const rect = canvas.getBoundingClientRect();
if (lastSize.width !== rect.width || lastSize.height !== rect.height) {
lastSize = rect;
canvas.width = rect.width;
canvas.height = rect.height;
}
draw();
requestAnimationFrame(rAFLoop);
}
requestAnimationFrame(rAFLoop);
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
canvas.height = entry.devicePixelContentBoxSize[0].blockSize;
draw();
}
})
observer.observe(canvas, { box: 'device-pixel-content-box' });
html, body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<canvas></canvas>
使用此解决方案,我们每帧仅渲染一次,并获得实际的设备像素大小
您选择哪种解决方案取决于您的需求。如果你的绘图不是那么重,绘制两次,一次在 rAF 中,一次在调整大小观察器中可能就可以了。或者,如果您不希望用户经常调整大小(注意:这不仅仅是调整窗口大小,很多网页都有带有滑块的窗格,可让您调整存在所有相同问题的区域的大小)
如果您不关心像素完美度,那么第二个解决方案也可以。例如,据我所知,大多数 3D 游戏并不关心像素完美度。他们已经在渲染双线性过滤纹理,并且经常渲染到较低分辨率并重新缩放。
如果您确实关心像素完美度,那么第三种解决方案就可以了。
注意:截至2014年1月18日Safari仍不支持
devicePixelContentBoxSize