且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

画布以不一致的速度绘制(requestAnimationFrame)

更新时间:2022-10-18 10:28:35

是的,您做错了几件事.

通常,您不应将距离增加固定量,而应使用 delta-时间来确定您的对象自上一帧以来应移动了多少.

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


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

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

让我们以一个75Hz的显示器为例(因为它实际上很常见,并且因为它是一个很好的例子),而没有任何干扰页面的情况,因此没有稳定"的干扰.帧速率.
每个帧的持续时间应为1s/75->〜13.33333毫秒.在更新对象的位置之前,您的代码将检查帧的持续时间是否大于1s/60->.〜16.66666ms.

在此75Hz监视器上,每帧都将失败此条件,因此位置将仅在下一帧更新:

第一帧 第二帧 第三帧 第4帧
时钟时间 13.33333ms 26.66666ms 39.99999ms 53.33332ms
最后油漆 0ms 0ms 26.66666ms 26.66666ms
上次油漆时间 13.33333ms 26.66666ms 13.33333ms 26.66666ms
状态 丢弃 彩绘 丢弃 彩绘
x位置 0px 6px 6px 12px

在具有相同稳定条件的60Hz显示器上,本来应该是

第一帧 第二帧 第三帧 第4帧
时钟时间 16.66666ms 33.33333ms 49.99999ms 66.66666ms
最后油漆 0ms 16.66666ms 33.33333ms 49.99999ms
上次油漆时间 16.66666ms 16.66666ms 16.66666ms 16.66666ms
状态 彩绘 彩绘 彩绘 彩绘
x位置 6px 12px 18px 24px

因此,您可以看到在50ms之后,在***条件下75Hz设置的 x 值仍应为18px时,其 x 值仍为6px,以及如何最终仅以37.5FPS进行绘制60FPS的目标.


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


为了避免这种情况,请使用增量时间来确定对象的位置.这样,无论两个帧之间的延迟如何,无论监视器的刷新率如何,您的对象都将被渲染到正确的位置,即使您丢下一两个帧,动画也不会跳动或卡住./p>

  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 =经过时间* px_per_second;x =(x +距离)%canvas.width;}var draw = function(){ctx.clearRect(0,0,canvas.width,canvas.height);ctx.fillRect(x,10,30,30);}让lastRenderTime = 0const frameRate = 60;函数main(currentTime){const secondsSinceLastRender =(currentTime-lastRenderTime)/1000update(secondsSinceLastRender);画();lastRenderTime = currentTime;//***将其保留在最后,以防在此回调中引发异常,//我们不希望它无限期地抛出每个画框window.requestAnimationFrame(主要)}window.requestAnimationFrame(main) 

 < canvas id ="canvas"></canvas>  

I have the most simple and straightforward animation with canvas:

jsFiddle link

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)

It's just a rectangle moving from left to right.

However, even on my powerful PC it's running inconsistently (you can see it's not smooth enough for 60 fps and also the speed is varying).

Is there something I'm doing wrong or is this just how canvas works?

Yes you are doing a few things wrong.

As a general rule, you should not increment the distance by a fixed amount, instead use a delta-time to determine by how much your object should have moved since the last frame.

This is because requestAnimationFrame(rAF) may not fire at regular intervals, for instance if the browser has a lot of things to do in parallel the next rAF loop may get delayed. And anyway, you can't be sure at which rate rAF callbacks will fire; this will depend on the user's monitor's refresh-rate.


Here you are trying to set up a maximum frame rate of 60FPS, which I assume you thought would allow you to use a fixed increment value, since this code is supposed to control the frame-rate.

But this code would work only where the frame-rate is a multiple of the target FPS (e.g 120Hz, 240Hz). Every other frame rate will suffer from this code, and since as we said before the frame-rate should not be thought as being stable, even 120Hz and 240Hz monitors would suffer from it.
(note that on monitors where the refresh rate is lower than 60Hz, this code won't help them catch up on their delay either.)

Let's take a 75Hz monitor as an example (because it's actually quite common and because it makes for a good example), without anything interfering with the page and thus a "stable" frame-rate.
Every frame should have a duration of 1s/75 -> ~13.33333ms. Before updating the object's position, your code checks if the duration of the frame is above 1s/60 -> ~16.66666ms.

On this 75Hz monitor every single frame will fail this condition, and thus the position will get updated only at the next frame:

1st frame 2nd frame 3rd frame 4th frame
clock time 13.33333ms 26.66666ms 39.99999ms 53.33332ms
last-paint 0ms 0ms 26.66666ms 26.66666ms
time-from-last-paint 13.33333ms 26.66666ms 13.33333ms 26.66666ms
status discarded painted discarded painted
x position 0px 6px 6px 12px

When on a 60Hz monitor with same stable conditions it would have been

1st frame 2nd frame 3rd frame 4th frame
clock time 16.66666ms 33.33333ms 49.99999ms 66.66666ms
last-paint 0ms 16.66666ms 33.33333ms 49.99999ms
time-from-last-paint 16.66666ms 16.66666ms 16.66666ms 16.66666ms
status painted painted painted painted
x position 6px 12px 18px 24px

So you can see how after 50ms, the 75Hz setup has its x value still at 6px when it should already be at 18px in optimal conditions, and how we end up painting only at 37.5FPS instead of the targeted 60FPS.


You may not be on 75Hz monitor, but on my macOS Firefox, which does calculate rAF's rate from the CPU instead of looking at the monitor's refresh-rate, I end up in a situation even worse, where frames takes about 16.65ms, meaning that to traverse the canvas it takes literally twice the time it would take without your frame-rate restriction.


In order to avoid that, use a delta-time to determine the position of your object. This way, no matter the delay between two frames, no matter the monitor's refresh-rate etc. your object will get rendered at the correct position and even if you drop a frame or two, your animation won't jump or get stuck.

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)

<canvas id="canvas"></canvas>