Canvas 的绘制速度不一致(requestAnimationFrame)

Ste*_*ven 1 javascript canvas requestanimationframe

我有最简单直接的画布动画:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.canvas.width  = 700;
ctx.canvas.height = 300;

var x = 0;

var update = function() {
  x = x + 6;
}

var draw = function() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillRect(x, 10, 30, 30);
}

let lastRenderTime = 0
const frameRate = 60;

function main(currentTime) {
  window.requestAnimationFrame(main)
  const secondsSinceLastRender = (currentTime - lastRenderTime) / 1000
  if (secondsSinceLastRender < 1 / frameRate) return


  lastRenderTime = currentTime

  update()
  draw()
}

window.requestAnimationFrame(main)
Run Code Online (Sandbox Code Playgroud)
<canvas id="canvas"></canvas>
Run Code Online (Sandbox Code Playgroud)

它只是一个从左向右移动的矩形。

然而,即使在我功能强大的 PC 上,它的运行也不一致(您可以看到它对于 60 fps 来说不够流畅,而且速度也各不相同)。

我做错了什么还是这就是画布的工作原理?

Kai*_*ido 5

是的,你做错了一些事情。

作为一般规则,您不应将距离增加固定量,而是使用增量时间来确定对象自上一帧以来应移动的距离。

这是因为requestAnimationFrame(rAF) 可能不会定期触发,例如,如果浏览器有很多并行的事情要做,则下一个 rAF 循环可能会延迟。无论如何,您无法确定 rAF 回调将以何种速率触发;这将取决于用户显示器的刷新率。


在这里,您尝试设置 60FPS 的最大帧速率,我认为您认为这将允许您使用固定的增量值,因为此代码应该控制帧速率。

但此代码仅在帧速率是目标 FPS 的倍数(例如 120Hz、240Hz)时才有效。所有其他帧速率都会受到此代码的影响,并且正如我们之前所说,帧速率不应被认为是稳定的,即使是 120Hz 和 240Hz 显示器也会受到此代码的影响。
(请注意,在刷新率低于 60Hz 的显示器上,此代码也无法帮助它们赶上延迟。)

让我们以 75Hz 显示器为例(因为它实际上很常见,而且是一个很好的例子),没有任何东西干扰页面,因此具有“稳定”的帧速率。
每帧的持续时间应为 1s/75 -> ~13.33333ms。在更新对象的位置之前,您的代码会检查帧的持续时间是否高于 1s/60 -> ~16.66666ms。

在这个 75Hz 显示器上,每一帧都会失败,因此位置只会在下一帧更新:

第一帧 第二帧 第三帧 第四帧
时钟时间 13.33333毫秒 26.66666毫秒 39.99999毫秒 53.33332毫秒
最后油漆 0毫秒 0毫秒 26.66666毫秒 26.66666毫秒
距上次绘制的时间 13.33333毫秒 26.66666毫秒 13.33333毫秒 26.66666毫秒
地位 丢弃的 丢弃的
x 位置 0像素 6像素 6像素 12像素

当在具有相同稳定条件的 60Hz 显示器上时,它会是

第一帧 第二帧 第三帧 第四帧
时钟时间 16.66666毫秒 33.33333毫秒 49.99999毫秒 66.66666毫秒
最后油漆 0毫秒 16.66666毫秒 33.33333毫秒 49.99999毫秒
距上次绘制的时间 16.66666毫秒 16.66666毫秒 16.66666毫秒 16.66666毫秒
地位
x 位置 6像素 12像素 18像素 24像素

因此,您可以看到 50 毫秒后,75Hz 设置的x值仍然为 6px,而在最佳条件下它应该已经为 18px,以及我们如何最终仅以 37.5FPS 而不是目标 60FPS 进行绘制。


你可能不是在 75Hz 显示器上,但在我的 macOS Firefox 上,它会从 CPU 计算 rAF 速率,而不是查看显示器的刷新率,我最终会遇到更糟糕的情况,帧需要大约 16.65ms,这意味着遍历画布所花费的时间实际上是没有帧速率限制时的两倍。


为了避免这种情况,请使用增量时间来确定对象的位置。这样,无论两帧之间的延迟如何,无论显示器的刷新率等如何,您的对象都会在正确的位置渲染,即使您丢弃一两帧,您的动画也不会跳跃或卡住。

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.canvas.width  = 700;
ctx.canvas.height = 300;

var x = 0;
const px_per_frame_at_60Hz = 6;
const px_per_second = (px_per_frame_at_60Hz * 60);

var update = function( elapsed_time ) {
  const distance = elapsed_time * px_per_second;
  x = (x + distance) % canvas.width;
}

var draw = function() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillRect(x, 10, 30, 30);
}

let lastRenderTime = 0
const frameRate = 60;

function main(currentTime) {
  const secondsSinceLastRender = (currentTime - lastRenderTime) / 1000
  update( secondsSinceLastRender );
  draw();
  lastRenderTime = currentTime;
  // better keep it at the end in case something throws in this callback,
  // we don't want it to throw every painting frames indefinitely  
  window.requestAnimationFrame(main)
}

window.requestAnimationFrame(main)
Run Code Online (Sandbox Code Playgroud)
<canvas id="canvas"></canvas>
Run Code Online (Sandbox Code Playgroud)