请求动画帧会周期性地加快/减慢速度。

13

据我理解,requestAnimationFrame应该尽可能接近于浏览器的帧率,大约为60fps。为确保这确实发生了,我已经记录了每个requestAnimationFrame调用的时间戳,如下:

function animate(now){
    console.log(now);
    window.requestAnimationFrame(animate);
}
window.requestAnimationFrame(animate);

Console.log数据显示,调用之间的时间间隔一直约为0.016674毫秒,因此表明帧率约为60fps(确切地说是59.9736116108912fps)。

控制台日志(样本数据):

Timestamp   FPS               (Current - previous) timestamp
------------------------------------------------------------
100.226     59.97361161       0.016674
116.9       59.97361161       0.016674
133.574     59.97361161       0.016674
   .             .               .
   .             .               .
150.248     59.97361161       0.016674
166.922     59.97361161       0.016674
183.596     59.97361161       0.016674
200.27      59.97361161       0.016674

到目前为止,requestAnimationFrame的调用在一致的时间间隔内发生,并且没有任何调用滞后/执行过快。

然而,修改animate()函数的内容以执行相对更复杂的函数会导致requestAnimationFrame调用不再那么一致。

控制台日志(样本数据):

Timestamp       FPS               (Current - previous) timestamp
------------------------------------------------------------
7042.73         59.97361161       0.016674
7066.278        42.4664515        0.023548
7082.952        59.97361161       0.016674
7099.626        59.97361161       0.016674
   .                 .                .
   .                 .                .
17104.026       59.97361161       0.016674
17112.84        113.4558657       0.008814
17129.514       59.97361161       0.016674
17146.188       59.97361161       0.016674

从上述数据样本可以看出,时间戳差异和帧率不再稳定,并且有时会发生过早/过晚的情况,导致帧速率不一致。如果 requestAnimationFrame 被稳定地推迟调用,我会认为由于 JavaScript 的单线程特性,驻留在 animate() 函数中的复杂代码执行时间过长,从而导致延迟的 requestAnimationFrame 调用。然而,由于 requestAnimationFrame 偶尔被过早调用,这似乎并非问题所在。

我的代码(框架):

for (var counter = 0; counter < elements.length; counter ++) //10 elements
{
  //some other code

  animate(element);
}

function animate(element)
{
   // function invocation => performs some complex calculations and animates the element passed in as a parameter

   window.requestAnimationFrame(function() { animate(element) } );
}

从上面的代码片段中可以看出,在初始的for循环内,对于每个canvas元素都会多次调用requestAnimationFrame。同时,也打算为每个canvas元素无限制地调用requestAnimationFrame。因为要执行的动画时间非常重要(在这种情况下,动画时间非常重要,应该尽可能准确),所以必须确保requestAnimationFrame调用在整个过程中始终以一致的时间间隔发生。这些时间间隔最好尽可能接近0.016674(≈ 60fps)。

关于要执行的动画的一些详细信息(对于每个canvas元素):

我有一个非常特殊的情况,需要尽可能准确地绘制闪烁/闪光动画,即canvas颜色必须以一致的速度改变,在指定的时间间隔内。例如,canvas颜色需要保持红色正好0.025秒,然后再保持蓝色0.025秒,随后又是0.025秒的红色canvas等等(动画应该无限制地进行,对于每个元素)。我的当前方法涉及跟踪动画循环中已经过去的帧数(因此,每个requestAnimationFrame调用对应于单个帧)。

由于在60Hz显示器上不能实现精确的0.025秒帧长度,每个红/蓝canvas周期都应该是“近似”的。因此,在考虑到60Hz显示器的情况下,创建一个完整的周期,其中canvas最初为红色,然后为蓝色,需要3帧(1秒/60 = 0.01666667秒* 3帧= 0.05秒=>单个完整的红/蓝周期所需的持续时间)。将0.05秒除以2即可得到所需的帧长度(即0.025秒),但由于在60Hz显示器上无法实现这一点,因此通过呈现2个红色canvas帧,再加上一个单独的蓝色帧(从而形成整个3帧周期)来近似循环。不幸的是,即使考虑了显示器的刷新率,时间也倾向于波动,导致不良的不准确性。

最后几个问题:

  1. 您能否澄清是什么原因导致了这种不一致的requestAnimationFrame行为?

  2. 是否可以应用任何优化以确保requestAnimationFrame调用在执行时具有一致的时间间隔?

  3. 如果我使用其他类型的功能(例如与setInterval()函数结合使用的Web Workers),是否可以实现更好的时间精度?


你应该在 setTimeout 中编写逻辑,在 requestAnimationFrame 中进行视觉更新。 - Get Off My Lawn
1
@GetOffMyLawn 不过,setTimeout不是非常不准吗?从进行的测试以及许多在线来源来看,似乎是这样。setInterval也是如此。 - Questionnaire
1
你用什么浏览器生成了这个 113FPS 的帧?我无法重现。否则,1 如果有任何东西阻塞了浏览器,它将无法保持一致的帧率。2 取决于你正在做什么/是什么导致瓶颈。一些操作可以转移到WebWorker,但不是全部。3 取决于你要做什么。现在,你不应该假设任何帧率,浏览器可能(实际上应该)适应显示器的帧率。因此,使用两个不同的监视器(即使是相同的浏览器+计算机),两个用户可能会得到完全不同的结果。 - Kaiido
我的理解是,你可以在开发者工具中查看渲染时间过长的帧,并了解其原因。 - Nickolay
1
当你在帧率方面遇到这样的短暂下降,而不知道原因时,垃圾回收总是一个好的猜测。 - Thomas
显示剩余10条评论
1个回答

5

requestAnimationFrame会尽其所能以“一致”的帧率运行。这并不能保证60fps,只是说明它会尽可能地快速执行动画。

这种方法可以让你在下一个可用的屏幕重绘时执行代码,消除了与用户浏览器和硬件准备就绪同步以进行屏幕更改的猜测工作。

我们输入包含要运行的代码的回调函数,并请求requestAnimationFrame() 在屏幕准备好接受下一个屏幕重绘时运行它

为了使您的动画保持恒定,您必须在回调中计算数据,并根据实际增量重新计算值,而不是假设一个恒定的FPS。

例如:

function moveit(timestamp, el, dist, duration){
    //if browser doesn't support requestAnimationFrame, generate our own timestamp using Date:
    var timestamp = timestamp || new Date().getTime()
    var runtime = timestamp - starttime
    var progress = runtime / duration
    progress = Math.min(progress, 1)
    el.style.left = (dist * progress).toFixed(2) + 'px'
    if (runtime < duration){ // if duration not met yet
        requestAnimationFrame(function(timestamp){ // call requestAnimationFrame again with parameters
            moveit(timestamp, el, dist, duration)
        })
    }
}

requestAnimationFrame(function(timestamp){
    starttime = timestamp || new Date().getTime() //if browser doesn't support requestAnimationFrame, generate our own timestamp using Date
    moveit(timestamp, adiv, 400, 2000) // 400px over 1 second
})

嗨,谢谢您的回复,对于您在示例中提供的特定情况,这确实是有意义的。然而,我有一个非常具体的情况,需要尽可能准确地绘制闪烁动画,即画布颜色必须以一致的速度改变,持续指定的时间间隔。因此,例如,画布颜色需要保持红色正好0.025秒,然后另外0.025秒画布颜色设置为蓝色,接着再0.025秒画布又是红色,如此循环(无限循环)。 - Questionnaire
所以,针对这种特定情况,我目前不知道如何调整您的示例以满足这些要求。 - Questionnaire
@问卷调查 可能有一种方法,使画布的像素缓冲区在大约您想要的时间(+-几微秒)内是您想要的颜色,但它不可能以这个速率精确地绘制。显示器的屏幕刷新率最终会与您的代码请求发生冲突,这是无法避免的。如果我们采用常见的60Hz刷新率,则您的画布只能绘制0.016ms或0.032ms,中间没有任何东西。 - Kaiido
1
正如@Kaiido所说,您可以跳过帧,延迟帧渲染,但是在不考虑CPU和刷新率量子上发生的情况下,在完美的时间闪烁是不可能的。 - Mosè Raguzzini
我同意,刷新率肯定会对此造成干扰。不过,在执行计算时,我打算将其考虑在内。抱歉在之前的评论/问题中没有提到这一点。更多细节请参见下面的评论。 - Questionnaire
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接