My requestAnimationFrame
animation appears to lose frames while drawing time code to a javascript canvas. The time code shows a sequence of 31 graphic states each lasting 24 seconds and I am trying to run it at 24 times real-time so the entire sequence can be previewed in 31 seconds.
Instead of setTimeout
, I decided to use requestAnimationFrame
, as recommended here and here. The animation runs smoothly through the 31 states, once per second, with rapidly changing minutes and seconds displayed during each state.
In order to create the illusion of speeded up time code I decided to derive time values from timestamps, i.e. the frame period between successive timestamps measured in milliseconds. From this I derived seconds
and a smaller arbitrary time interval using a variable called subSeconds
.
However when the animation stops after 31 seconds, the final time value displayed is always short of the expected final value, namely, 12:24 (or 744 seconds).
With subSeconds
set to a value of 10 milliseconds, the actual final value is typically anything between 12:00 and 12:09. With an even coarser value of subSeconds
, e.g. 50 milliseconds, the actual final value can be as low as 11:25.
This excerpt from the console log of framePeriod
(below) reveals some variation between successive timestamps as well as occasional lines where framePeriod
exceeds 1 second.
Console log excerpt
34 67 100 134 167 200 234 267 344 345 367 400 434 468 501 534 568 601 635 668 710 734 771 803 834 867 901 935 968 1001
33 66 99 133 167 199 232 267 299 334 367 399 434 467 499 534 566 600 633 667 700 734 766 800 832 868 900 933 967 1000
34 67 100 132 166 200 233 267 300 333 367 400 433 467 500 533 567 600 633 667 699 733 823 825 833 866 900 933 966 999 1032
34 67 101 134 167 201 234 267 301 334 367 400 434 467 500 534 568 600 634 667 700 734 767 801 834 867 901 934 968 1000
34 68 100 135 167 200 234 267 300 334 367 401 435 467 500 534 567 601 634 667 700 734 767 801 834 867 901 934 967 1000
34 68 100 134 168 200 234 268 301 334 367 400 435 467 501 535 567 601 634 668 701 734 767 800 834 867 901 934 967 1000
Here’s a 31-second demonstration of the problem without the graphic imagery.
function animate31States() {
const cnv = document.getElementById('draw');
const ctx = cnv.getContext('2d');
ctx.translate(cnv.width/2, cnv.height/2);
let currentTime = performance.now();
let state = 0;
let timeUnits = 0;
let totalSeconds = 0;
const subSecond = 10;
const second = 1000;
// Animation loop
function draw(timestamp) {
if (state >= 31) {
return;
}
const framePeriod = timestamp - currentTime;
console.log(framePeriod);
if (framePeriod >= subSecond) {
timeUnits++;
totalSeconds = timeUnits * (24/31);
}
if (framePeriod >= second) {
state++;
currentTime = timestamp;
}
const secondsStr = clockify(totalSeconds);
clearCanvas();
if (state < 31) {
drawText(`${state + 1}`, 0, -385, 24);
} else {
drawText('should end at 12:24', 0, -385, 16);
}
drawText(secondsStr, 0, -355, 16);
requestAnimationFrame(draw);
}
function drawText(text, x, y, size) {
ctx.textAlign = 'center';
ctx.font = `${size}px Helvetica Neue, Helvetica, Arial, sans-serif`;
ctx.fillText(text, x, y);
}
function clockify(time) {
const m = mins(time);
const s = secs(time);
return m + s;
}
function mins(time) {
const m = Math.floor(time / 60);
return `${m.toString().padStart(2, '0')}:`;
}
function secs(time) {
const s = Math.floor(time % 60);
return `${s.toString().padStart(2, '0')} `;
}
function clearCanvas() {
ctx.clearRect(-cnv.width/2, -cnv.height/2, cnv.width, cnv.height);
}
requestAnimationFrame(draw); // Start the animation loop
}
animate31States();
<canvas id="draw" width=390 height=844></canvas>
I can’t see any other way to calculate time code from ‘framePeriod’. Can anyone tell me how to achieve what I am trying to do. Thanks.
2
Answers
RequestAnimationFrame isn’t that accurate. JavaScript works on a single event loop and other task can get in the way.
All RequestAnimationFrame does is place a call at the back of the event loop after each frame has rendered. If another task is still in the call stack when this happens, then it will get called first and your animation event will have to wait it’s turn to get called.
Just compute your desired state based on the current wallclock time and the time the animation started. The
Math.min()
in thestate
expression makes sure the last state shown will be 31 (even if frames continue to be rendered – which you can of course opt out of).Sure, this might skip a frame, but given browsers’ limitations, you can’t really do much better.