Canvas 正在以不一致的速度绘制 (requestAnimationFrame)
Canvas is drawing with inconsistent speed (requestAnimationFrame)
我有最简单直接的动画用canvas:
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)
只是一个从左向右移动的矩形。
然而,即使在我强大的 PC 上 它也是 运行 不一致的(你可以看到它对于 60 fps 来说不够流畅,而且速度也在变化)。
我是不是做错了什么,或者这就是 canvas 的工作原理?
是的,你做错了几件事。
作为一般规则,您不应将距离增加固定量,而应使用 delta-time 来确定对象自上一帧以来应移动的距离。
这是因为 requestAnimationFrame
(rAF) 可能不会定期触发,例如,如果浏览器有很多并行的事情要做,下一个 rAF 循环可能会延迟。
无论如何,您无法确定 rAF 回调将以何种速率触发;这将取决于用户显示器的刷新率。
此处您尝试设置 60FPS 的最大帧速率,我假设您认为这将允许您使用固定的增量值,因为这段代码应该控制帧速率。
但是此代码仅适用于帧速率是目标 FPS 的倍数(例如 120Hz、240Hz)的情况。其他所有帧率都会受到此代码的影响,因为正如我们之前所说,帧率不应被认为是稳定的,即使是 120Hz 和 240Hz 显示器也会受到影响。
(请注意,在刷新率低于 60Hz 的显示器上,此代码也无法帮助他们赶上延迟。)
让我们以 75Hz 显示器为例(因为它实际上很常见,因为它是一个很好的例子),没有任何干扰页面,因此帧速率“稳定”。
每帧的持续时间应为 1s/75 -> ~13.33333ms。
在更新对象的位置之前,您的代码会检查帧的持续时间是否超过 1s/60 -> ~16.66666ms。
在这个 75Hz 监视器上,每一帧都不会满足此条件,因此位置只会在下一帧更新:
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
当在具有相同稳定条件的 60Hz 显示器上时,它会是
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
所以你可以看到在 50 毫秒后,75Hz 设置的 x
值仍然是 6px,而在最佳条件下它应该已经是 18px,以及我们如何最终只以 37.5FPS 而不是目标 60FPS。
您可能不在 75Hz 显示器上,但在我的 macOS Firefox 上,它确实从 CPU 计算 rAF 的速率而不是查看显示器的刷新率,我最终的情况更糟,其中帧大约需要 16.65 毫秒,这意味着遍历 canvas 所花费的时间实际上是没有帧速率限制时所花费时间的两倍。
为了避免这种情况,请使用增量时间来确定对象的位置。这样,无论两帧之间的延迟如何,无论显示器的刷新率等如何,您的对象都会在正确的位置渲染,即使您丢掉一两帧,您的动画也不会跳跃或卡住。
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>
我有最简单直接的动画用canvas:
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)
只是一个从左向右移动的矩形。
然而,即使在我强大的 PC 上 它也是 运行 不一致的(你可以看到它对于 60 fps 来说不够流畅,而且速度也在变化)。
我是不是做错了什么,或者这就是 canvas 的工作原理?
是的,你做错了几件事。
作为一般规则,您不应将距离增加固定量,而应使用 delta-time 来确定对象自上一帧以来应移动的距离。
这是因为 requestAnimationFrame
(rAF) 可能不会定期触发,例如,如果浏览器有很多并行的事情要做,下一个 rAF 循环可能会延迟。
无论如何,您无法确定 rAF 回调将以何种速率触发;这将取决于用户显示器的刷新率。
此处您尝试设置 60FPS 的最大帧速率,我假设您认为这将允许您使用固定的增量值,因为这段代码应该控制帧速率。
但是此代码仅适用于帧速率是目标 FPS 的倍数(例如 120Hz、240Hz)的情况。其他所有帧率都会受到此代码的影响,因为正如我们之前所说,帧率不应被认为是稳定的,即使是 120Hz 和 240Hz 显示器也会受到影响。
(请注意,在刷新率低于 60Hz 的显示器上,此代码也无法帮助他们赶上延迟。)
让我们以 75Hz 显示器为例(因为它实际上很常见,因为它是一个很好的例子),没有任何干扰页面,因此帧速率“稳定”。
每帧的持续时间应为 1s/75 -> ~13.33333ms。
在更新对象的位置之前,您的代码会检查帧的持续时间是否超过 1s/60 -> ~16.66666ms。
在这个 75Hz 监视器上,每一帧都不会满足此条件,因此位置只会在下一帧更新:
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 |
当在具有相同稳定条件的 60Hz 显示器上时,它会是
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 |
所以你可以看到在 50 毫秒后,75Hz 设置的 x
值仍然是 6px,而在最佳条件下它应该已经是 18px,以及我们如何最终只以 37.5FPS 而不是目标 60FPS。
您可能不在 75Hz 显示器上,但在我的 macOS Firefox 上,它确实从 CPU 计算 rAF 的速率而不是查看显示器的刷新率,我最终的情况更糟,其中帧大约需要 16.65 毫秒,这意味着遍历 canvas 所花费的时间实际上是没有帧速率限制时所花费时间的两倍。
为了避免这种情况,请使用增量时间来确定对象的位置。这样,无论两帧之间的延迟如何,无论显示器的刷新率等如何,您的对象都会在正确的位置渲染,即使您丢掉一两帧,您的动画也不会跳跃或卡住。
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>