Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RequestAnimationFrame speeds up/slows down periodically

From my understanding, requestAnimationFrame should run as close as possible to the browser's frame rate, which is approximately 60fps. To ensure that this is indeed taking place, I have been logging timestamps for each requestAnimationFrame invocation like so:

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

Console.log data shows that invocations are consistently taking place approximately 0.016674 milliseconds apart, thus indicating that the frame rate is ≈ 60fps (59.9736116108912fps to be exact).

Console.logs (sample data):

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

Up to this point, requestAnimationFrame invocations are occurring at consistent time intervals, and no invocation lags behind/executes too fast.

However, modifying the contents of the animate() function to execute a relatively more complex function, results in requestAnimationFrame invocations which are not as consistent.

Console.logs (sample data):

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

As can be seen in the above data sample, timestamp differences and frame rates are no longer steady, and sometimes occur too soon/too late, resulting in inconsistent frame rates. Had requestAnimationFrame been consistently invoked late, I would have assumed that due to JavaScript's single-threaded nature, complex code residing in the animate() function is taking too long to execute, and thus results in a delayed requestAnimationFrame invocation. However, since requestAnimationFrame is occasionally being invoked too early, this does not seem to be the case.

My code (skeleton):

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) } );
}

As can be seen in the above code snippet, requestAnimationFrame is being invoked multiple times for each element, within the initial for loop. RequestAnimationFrame invocations are also intended to go on infinitely, for each of the elements. Since the animation to be performed is highly time-critical (animation timing is very important in this scenario and should be as accurate as possible), it is essential that requestAnimationFrame invocations occur at consistent time intervals throughout. These time intervals should ideally be as close as possible to 0.016674 (≈ 60fps).

Some detail regarding animation to be performed (on each canvas element):

I have a very specific situation, for which I am required to draw a blinking/flashing animation as accurately as possible with respect to time, i.e. canvas colour will have to change at a consistent rate, for the specified time interval. Therefore, for instance, canvas colour needs to stay red for exactly 0.025 seconds, followed by another 0.025 seconds where canvas colour is set to blue, which are then followed by another 0.025s where canvas is red and so on...(animation should go on infinitely, for each of the elements). My current approach involves keeping track of the number of frames which have elapsed within the animation loop itself (thus, each requestAnimationFrame invocation corresponds to a single frame).

Since on a 60Hz monitor an exact frame length of 0.025 seconds cannot be achieved, each red/blue canvas cycle should be "approximated". So, taking into consideration a 60Hz monitor, creating a complete cycle, where the canvas is initially red, followed by blue, a total of 3 frames would be required (1 sec/60 = 0.01666667 seconds * 3 frames = 0.05 seconds => the desired duration for a single, complete red/blue cycle). Dividing 0.05 seconds by 2 would give the desired frame length (which is 0.025 seconds), however since this cannot be achieved on a 60Hz monitor, the cycle is approximated by presenting 2 red canvas frames, followed by a single blue frame (thus forming the entire 3-frame cycle). Unfortunately, even when taking the monitor's refresh rate into consideration, the timing tends to fluctuate, resulting in undesirable inaccuracies.

Final questions:

  1. Would you be able to clarify what is causing this inconsistent requestAnimationFrame behaviour?

  2. Can any optimisations be applied to ensure that requestAnimationFrame invocations are executed at consistent time intervals?

  3. Can better timing accuracy be achieved if I use some other kind of functionality (say, web workers in combination with the setInterval() function)?

like image 886
Questionnaire Avatar asked Aug 27 '19 20:08

Questionnaire


People also ask

How often does requestAnimationFrame run?

The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation. requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe> s in order to improve performance and battery life.

Why is requestAnimationFrame better than setInterval?

Another reason requestAnimationFrame is so useful is the fact that it gives you a time variable which you can use to calculate the amount of time between frames. This is not something that setInterval will do and since setInterval is not 100% accurate it is very possible your animation will get messed up over time.

Should I use requestAnimationFrame?

To optimize system and browser resources, it is recommended to use requestAnimationFrame , which requests the browser to execute the code during the next repaint cycle. This allows the system to optimize resources and frame-rate to reduce unnecessary reflow/repaint calls.

How do I stop Windows requestAnimationFrame?

When you start an animation using requestAnimationFrame you will get a reference to the animation. You can anytime use the cancelAnimationFrame global function by passing the reference as parameter to stop the animation.


1 Answers

requestAnimationFrame will "do its best" to run at a "consistent" frame rate. That doesn't guarantee a 60fps; it just states that it will animate as fast as it can.

The method in a nutshell allows you to execute code on the next available screen repaint, taking the guess work out of getting in sync with the user's browser and hardware readiness to make changes to the screen.

We enter a callback function containing the code we wish to run, and requestAnimationFrame() will run it when the screen is ready to accept the next screen repaint.

In order to keep constant your animation you have to calculate the data in the callback recomputing values against the actual delta, not presuming a constant FPS.

Eg:

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
})
like image 84
Mosè Raguzzini Avatar answered Oct 08 '22 22:10

Mosè Raguzzini