如何正确使用ResizeObserver和requestAnimationFrame

问题描述 投票:0回答:1

许多示例都显示使用

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
循环中调用它(并添加了一些动作)

如果现在调整窗口大小,您会看到圆圈闪烁

我该如何解决这个问题?

javascript html html5-canvas resize-observer
1个回答
0
投票

这里的问题是规范说

ResizeObserver
回调发生在requestAnimationFrame回调之后。这意味着上例中的操作顺序是

    requestAnimationFrame回调
  1. 画圆圈
  2. 调整观察者回调大小
  3. 设置画布大小(设置大小会清除画布)
  4. 浏览器组合页面。
有几种解决方法,但都有问题或半涉及

解决方案 1:也画入

ResizeObserver

这会起作用。不幸的是,这意味着您在一个框架中绘制了两次。如果你画得很重,那么这会导致调整大小感觉缓慢

解决方案 2:不要使用

ResizeObserver

您可以通过多种方式查找画布的显示尺寸。 (1)

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


    

© www.soinside.com 2019 - 2024. All rights reserved.