Why isn't my throttled requestAnimationFrame running 60FPS on my 144hz monitor? [duplicate] - javascript

This question already has an answer here:
Canvas is drawing with inconsistent speed (requestAnimationFrame)
(1 answer)
Closed 11 months ago.
I want to make my requestAnimationFrame run 60FPS regardless of monitors refresh rate so the game would run at the same speed on every monitor.
Although it seems to work, when I try using chrome DevTools performance tab I can see the FPS being like -> 48, 72, 48, 72....
Here is my throttled version of rAF
let start;
let elapsed;
function startAnimating(timestamp) {
const fpsInterval = 1000 / 60;
if (start === undefined) {
start = timestamp
}
elapsed = timestamp - start;
if (elapsed >= fpsInterval) {
start = timestamp - (elapsed % fpsInterval);
move();
}
requestAnimationFrame(startAnimating);
}

The TL;DR is, requestAnimationFrame() runs whenever it feels like. You can't really throttle it. If you want to try to do that, you should use setTimeout() or setInternal() instead. They'll be called closer to your throttled rate, usually +/- 4ms.
Or you can do what you're doing, which is throttle when you take action, if you want. You'll see in DevTools whatever requestAnimationFrame() is being called at, but your function would only do the move() at 60FPS, or whatever rate best lines up with it. Though, this can create some clunkiness, since you may end up with much slower effective FPSthan what you want, if the intervals between each call by requestAnimationFrame() don't line up well with your target FPS.
Usually a better approach is instead scale all of your changes, such as movement, by the delta of your time since the last call:
let lastTime = Date.now();
function startAnimating() {
const now = Date.now();
const delta = now - lastTime;
lastTime = now;
move(delta); // delta is the ms since the last call
requestAnimationFrame(delta);
}
function move(delta) {
character.moveBy(speedInPxPerSecond * (delta / 1000));
}
startAnimating();
So, if you want your character to move 100px a second, if you multiply that 100 by delta / 1000, you end up with a character that moves exactly (or very, very nearly exactly) 100px a second, regardless if your FPS is 10, 30, 60, or 1000.

...so the game would run at the same speed on every monitor.
With respect, that's not the best way to do that. (It's not just you, people make this mistake all the time. :-) ) Your game should run at the same speed regardless of whether your rAF callbacks are done at 30Hz, 60Hz, 144Hz, or whatever. The way animation and time-based game logic should be written is to look at the current time as of the callback and figure out what to render at that moment. The rAF callbacks should not be what drives the clock of the game. That should be based on an actual clock (for instance, Date.now() or performance.now()).
For example, here's a simple animation done incorrectly (based on calls to rAF). Aside from the fact that it will run at the wrong speed if your refresh rate is not 60Hz, the browser gets busy for a minute and can't update the screen:
// Make the block go left to right in five seconds
// 5sec = 5,000ms. 5sec at 60Hz is 300 callbacks
// 100% / 300 = 0.333333334% per callback.
const block = document.getElementById("block");
let start = Date.now();
updateBlock();
let timerHandle = 0;
busyBrowser();
function busyBrowser() {
timerHandle = setTimeout(() => {
const stop = Date.now() + 100;
while (Date.now() < stop); // NEVER DO THIS FOR REAL
busyBrowser();
}, 230);
}
function updateBlock() {
let left = parseFloat(block.style.left || "0");
left = Math.min(100, left + 0.333333334);
block.style.left = left + "%";
if (left === 100) {
console.log(`Done after ${(Date.now() - start) / 1000} seconds`);
clearInterval(timerHandle);
} else {
requestAnimationFrame(updateBlock);
}
}
#channel {
position: relative;
height: 1rem;
}
#block {
position: absolute;
left: 0;
top: 0;
height: 1rem;
}
Should take five seconds to go left to right.
<div id="channel">
<div id="block">X</div>
</div>
On my system with a 100Hz refresh rate, that takes four seconds, because it's wrong in two ways:
My refresh rate is 100Hz, not 60Hz, but the code assumes it's 60Hz.
There were times when the browser was busy doing "other things" (my busy loop) and couldn't call rAF.
The only reason it's four seconds is the delays (#2); without them, it's three seconds.
Instead, the code in rAF should look at what the time is, and render based on where things should be at that time:
// Make the block go left to right in five seconds
// 5sec = 5,000ms. 5sec at 60Hz is 300 callbacks
// 100% / 300 = 0.333333334% per callback.
const block = document.getElementById("block");
let start = Date.now();
let stop = start + 5000;
updateBlock();
let timerHandle = 0;
busyBrowser();
function busyBrowser() {
timerHandle = setTimeout(() => {
const stop = Date.now() + 100;
while (Date.now() < stop); // NEVER DO THIS FOR REAL
busyBrowser();
}, 230);
}
function updateBlock() {
const elapsed = Date.now() - start;
let left = Math.min(
100,
elapsed * 0.02 // 100 / 5000 = 0.2% per ms
);
block.style.left = left + "%";
if (left === 100) {
console.log(`Done after ${elapsed / 1000} seconds`);
clearInterval(timerHandle);
} else {
requestAnimationFrame(updateBlock);
}
}
#channel {
position: relative;
height: 1rem;
}
#block {
position: absolute;
left: 0;
top: 0;
height: 1rem;
}
Should take five seconds to go left to right.
<div id="channel">
<div id="block">X</div>
</div>
That finishes in five seconds on my machine, despite my faster refresh rate and the browser being intermittently busy.

Related

JavaScript stopwatch being slower than expected

In my quest to become a JavaScript developer and as an F1 fan, I had to make a stopwatch, to track reaction time. The problem I stumbled upon, and it seems to be the case in many tutorials I've seen, is that you can't really be millisecond exact.
What I mean is, when I try to set an Interval for every 10 milliseconds, it works perfectly, but if I try to set an Interval for 1 millisecond, 1 second on my page is like 5 seconds in real life.
let clock = document.querySelector('.timer')
let interval = null;
let milliseconds = 0;
let seconds =0;
window.addEventListener('click', () => {
interval = setInterval(startClock,10)
})
document.addEventListener('keyup', event => {
if (event.code === 'Space') {
clearInterval(interval);
setTimeout(()=>{
alert(`${seconds}:${milliseconds}`);
},50)
}
})
function startClock(){
milliseconds += 10;
if(milliseconds==1000){
milliseconds = 0;
seconds++;
}
let sec = seconds.toString();
let ms = milliseconds.toString();
if(seconds<10){
sec = `0${seconds}`;
}
if(milliseconds<100){
ms = `0${milliseconds}`;
}
clock.innerHTML =`${sec}:${ms}`;
}
p{
color: rgb(25, 25, 25);
font-size: 170px;
height: 120px;
font-weight: bold;
}
<p class="timer">00:000</p>
for every 10 milliseconds, it works perfect
Even then it's not reliable. It can drift over time, it can be delayed by anything blocking the UI even for the briefest of moments, etc.
Taking a step back... Do you really need your UI clock to display every individual millisecond? Are you observing the results 1,000 times per second? Motion pictures update a couple dozen times per second and we perceive them as fluid continuous motion. What human needs to see 1,000 distinct numbers per second?
Instead, have your clock update the UI at regular intervals (if every 10ms is working for you then that's reasonable, every 50ms would probably be reasonable too) to show the current time. You don't need to create a stopwatch to measure time, the computer is already measuring time and you can query that measurement whenever you like, as often as you like.
All your "stopwatch" needs to do is know when it started and it can always calculate the elapsed milliseconds since then.
You can perhaps try to mimic the more random look and feel of a quickly-ticking millisecond timer by making the intervals an odd number, like 27ms or 41ms.
For example:
let clock = document.querySelector('.timer')
let interval = null;
let startTime = null;
window.addEventListener('click', () => {
startTime = new Date();
interval = setInterval(clockTick, 42)
})
function clockTick(){
let diff = new Date().getTime() - startTime.getTime();
clock.innerHTML =`${Math.floor(diff / 1000)}:${`${diff % 1000}`.padStart(3, "0")}`;
}
p{
color: rgb(25, 25, 25);
font-size: 170px;
height: 120px;
font-weight: bold;
}
<p class="timer">00:000</p>
Since you can't guarantee how long the code inside the interval will take to run, it's better to use the Date api. Instead of relying on the interval, you could run a loop that constantly calculates the ms between now and the start.
Here's an example of this concept:
// assume this runs when the stopwatch starts
let startTime = new Date().getTime()
// this should stop when the user stops the timer, but I'm using a for loop
// for the example
for(let i = 0; i < 100; i++) {
let now = new Date().getTime()
console.log("ms elapsed: " + (now - startTime))
}
Now it doesn't matter how fast the code is; it should always be accurate.
Javascript is single-threaded, which means it can only run one task at a time, blocking everything else.
That's why the browser stops working if you do while(true) {}. You can't scroll, click, and all animations stop while the loop runs.
Instead, I would suggest to use requestForAnimationFrame, so the timer only updates when the screen does a repaint (60, 120, 144 or whatever Hz your monitor has).
My timer stops when you click a second time.
const timerEl = document.getElementById('timer');
var startTime = 0, animationId = 0;
const toggleTimer = () => {
if (startTime == 0) {
startTime = new Date().getTime();
startTimer();
} else {
stopTimer();
updateTimerElement(new Date().getTime());
startTime = 0;
}
}
const startTimer = () => {
updateTimerElement(new Date().getTime());
animationId = requestAnimationFrame(startTimer);
}
const stopTimer = () => {
cancelAnimationFrame(animationId);
}
const updateTimerElement = (currentTime) => {
let passedTime = currentTime - startTime;
const MINUTE = 1000 * 60;
const SECOND = 1000;
let min = Math.floor(passedTime / MINUTE);
passedTime = passedTime - min * MINUTE;
let sec = Math.floor(passedTime / SECOND);
let ms = passedTime - sec * SECOND;
timerEl.innerText = `${addPadding(min)}:${addPadding(sec)}:${addPadding(ms, 3)}`;
}
const addPadding = (number, pad = 2) => {
return String(number).padStart(pad, '0');
}
document.addEventListener('mouseup', () => { toggleTimer() })
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 0;
}
#timer {
font-size: 40vh;
font-weight: bold;
}
<div id="timer">00:00:000</div>

When using requestAnimationFrame, how to make part of animation update faster

I am using requestAnimationFrame to make a game (game like a snake), and the frame update speed of the game is initially updated each second one time
It needs to update the "requestAnimationFrame" of this snake from a second each time to 0.5 second each time. Because of many snakes’ design and while a snake touches any item, it will get a speed-up situation for 10 seconds.
My question is how to maintain the main "requestAnimationFrame" (updated every second), and there is still another "requestAnimationFrame" (updated every 0.5 second)?
main requestAnimationFrame code
let speed = 1;
let lastRenderTime = 0;
const doAnimation = function (currentTime) {
window.requestAnimationFrame(doAnimation);
const secondRender = (currentTime - lastRenderTime) / 1000; // secondRender is used to control the update per second
if (secondRender < 1 / speed) {
return;
}
lastRenderTime = currentTime
}
window.requestAnimationFrame(doAnimation);
requestAnimationFrame fire rate is usually around 60Hz. That is 60 calls per second, giving you a maximum theoretical precision of ~16ms (0.016s).
This means that inside you loop you can make things update at any rate above that. But why would you like to truncate precision?
The whole point of requestAnimationFrame is to know exactly when a repaint will happen and to pass information about your animation at the right time. As an example: if your snake has to move 1000px every second, why would you notify the browser about the update every second? Ideally you should update your view on every frame. So in this example, a 16px variation every 16ms.
Please see the following snippet and note that there are no conditionals, anywhere. But just continuous update.
Obviously final implementation would depend on your use case, but this is only the working principle.
const boxA = document.getElementById('boxA'); // DEMO
const boxB = document.getElementById('boxB'); // DEMO
let xA = 0; // DEMO
let xB = 0; // DEMO
const speedA = 80; // [px/s]
const speedB = 160; // [px/s]
let then = 0;
const animate = function (now) {
window.requestAnimationFrame(animate);
const delta = (now - then) / 1000;
// A
const a = speedA * delta;
boxA.style.transform = `translateX(${xA += a}px)`; // DEMO
// B
const b = speedB * delta;
boxB.style.transform = `translateX(${xB += b}px)`; // DEMO
then = now
}
window.requestAnimationFrame(animate);
.container {
display: flex;
flex-direction: column;
}
#boxA,
#boxB {
display: inline-block;
height: 50px;
width: 50px;
transform: translateX(0);
}
#boxA {
background-color: #ff0000;
}
#boxB {
background-color: #0000ff;
}
<div class='container'>
<span id='boxA'></span>
<span id='boxB'></span>
</div>
let then, then2;
(function loop(delta) {
then = then || delta;
then2 = then2 || delta;
let time = delta - then;
let time2 = delta - then2;
if (time > 1000) {
then = delta;
// in here every second
}
if (time2 > 500) {
then2 = delta;
// in here every 0.5 seconds
}
document.body.innerHTML = time.toFixed(2) + '<br>' + time2.toFixed(2);
requestAnimationFrame(loop);
})();

timestamp of requestAnimationFrame is not reliable

I think the timestamp argument passed by requestAnimationFrame is computed wrongly (tested in Chrome and Firefox).
In the snippet below, I have a loop which takes approx. 300ms (you may have to tweak the number of loop iterations).
The calculated delta should always be larger than the printed 'duration' of the loop.
The weird thing is, sometimes it is slower sometimes not. Why?
let timeElapsed = 0;
let animationID;
const loop = timestamp => {
const delta = timestamp - timeElapsed;
timeElapsed = timestamp;
console.log('delta', delta);
// some heavy load for the frame
const start = performance.now();
let sum = 0;
for (let i = 0; i < 10000000; i++) {
sum += i ** i;
}
console.warn('duration', performance.now() - start);
animationID = requestAnimationFrame(loop)
}
animationID = requestAnimationFrame(loop);
setTimeout(() => {
cancelAnimationFrame(animationID);
}, 2000);
jsFiddle: https://jsfiddle.net/Kritten/ohd1ysmg/53/
Please not that the snippet stops after two second.
At least in Blink and Gecko, the timestamp passed to rAF callback is the one of the last VSync pulse.
In the snippet, the CPU and the event-loop are locked for about 300ms, but the monitor still does emit its VSync pulse at the same rate, in parallel.
When the browser is done doing this 300ms computation, it has to schedule a new animation frame.
At the next event-loop iteration it will check if the monitor has sent a new VSync pulse and since it did (about 18 times on a 60Hz), it will execute the new rAF callbacks almost instantly.
The timestamp passed to rAF callback may thus indeed be the one of a time prior to when your last callback ended, because the event-loop got freed after the last VSync pulse.
One way to force this is to make your computation last just a bit more than a frame's duration, for instance on a 60Hz monitor VSync pulses will happen every 16.67ms, so if we lock the event-loop for 16.7ms we are quite sure to have a timestamp delta lesser than the actual computation time:
let stopped = false;
let perf_elapsed = performance.now();
let timestamp_elapsed = 0;
let computation_time = 0;
let raf_id;
const loop = timestamp => {
const perf_now = performance.now();
const timestamp_delta = +(timestamp - timestamp_elapsed).toFixed(2);
timestamp_elapsed = timestamp;
const perf_delta = +(perf_now - perf_elapsed).toFixed(2);
perf_elapsed = perf_now;
const ERROR = timestamp_delta < computation_time;
if (computation_time) {
console.log({
computation_time,
timestamp_delta,
perf_delta,
ERROR
});
}
// some heavy load for the frame
const computation_start = performance.now();
const frame_duration = 1000 / frequency.value;
const computation_duration = (Math.ceil(frame_duration * 10) + 1) / 10; // add 0.1 ms
while (performance.now() - computation_start < computation_duration) {}
computation_time = performance.now() - computation_start;
raf_id = requestAnimationFrame(loop)
}
frequency.oninput = evt => {
cancelAnimationFrame( raf_id );
console.clear();
raf_id = requestAnimationFrame(loop);
setTimeout(() => {
cancelAnimationFrame( raf_id );
}, 2000);
};
frequency.oninput();
In case your monitor has a different frame-rate than th common 60Hz, you can insert it here:
<input type="number" id="frequency" value="60" steps="0.1">
So what to use between this timestamp and performance.now() is your call I guess, the timestamp tells you when the frame began, performance.now() will tell you when your code executes, you could use both if needed. Even without such a big computation spanning over frames, you can very well have an other task scheduled before yours that took a few ms to complete or even a big CSS composition that should get performed after, and you have no real way to know.

How to time requestAnimationFrame() independent of refreshrates

I have a situation where I want to animate the width of a 600px wide div to 0px in 1 second. I could use requestAnimationFrame() for this. But I wouldn't really be a 100% sure if the animation will take 1 second.
It would look something like this:
let width = 600;
let animation;
function animateWidth(){
if(width <= 0){
window.cancelAnimationFrame();
}else{
width -= 10; // (600px/60fps)
}
document.getElementById('id1').setAttribute('width', width);
animation = window.requestAnimationFrame(animateWidth);
}
animation = window.requestAnimationFrame(animateWidth);
The thing is, when a device has a different fps it will affect the duration of the animation (at 30fps it will take 2 seconds and at 60 fps it will take one). I want to make sure this duration of the animation is always one second. If the fps-rate is different I would want to change the new values of the width based on the duration (so animating at 30 fps we would change the width by 20 each step(600px/30fps)).
Is there any way I can achieve this while using requestAnimationFrame? If I could get the average interval between frames or the fps that would work I think.
Am I perhaps worrying about something that isn't really a big issue?
What fps can I expect on different devices (mobile, pc, tablet, etc.)?
documentation: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
requestAnimationFrame passes the time since the page loaded to the callback so for example
const startValue = 600;
const endValue = 0;
// return a value between start and end as l goes from 0 to 1
function lerp(start, end, l) {
return start + (end - start) * l;
}
const startTime = performance.now();
const durationInSeconds = 1;
const element = document.getElementById('id1');
function animateWidth(now) {
const timeSinceStartInSeconds = (now - startTime) * 0.001;
// l goes from 0 to 1 over durationInSeconds;
const l = Math.min(timeSinceStartInSeconds / durationInSeconds, 1);
element.setAttribute('width', lerp(startValue, endValue, l));
// if we haven't finished request another animation frame
if (l < 1) {
requestAnimationFrame(animateWidth);
}
}
requestAnimationFrame(animateWidth);
#id1 { background: red; }
<canvas id="id1"></canvas>
If it was me I'd probably try to make it a function
// return a value between start and end as l goes from 0 to 1
function lerp(start, end, l) {
return start + (end - start) * l;
}
function animateAttribute(element, attribute, startValue, endValue, duration) {
const startTime = performance.now();
function animate(now) {
const timeSinceStart = (now - startTime) * 0.001;
// l goes from 0 to 1 over durationInSeconds;
const l = Math.min(timeSinceStart / duration, 1);
element.setAttribute(attribute, lerp(startValue, endValue, l));
// if we haven't finished request another animation frame
if (l < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
const element = document.getElementById('id1');
const attribute = 'width';
const start = 600;
const end= 0;
const duration = 1;
animateAttribute(element, attribute, start, end, duration);
#id1 { background: red; }
<canvas id="id1"></canvas>
Also by passing l to any of these functions and passing the result to lerp you can change how the animation happens to match any of these styles of motion.
As for your other questions
Is there any way I can achieve this while using requestAnimationFrame? If I could get the average interval between frames or the fps that would work I think.
You can compute the interval between frames. Example:
let then = performance.now();
function animate(now) {
deltaTimeInMilliseconds = (now - then);
then = now;
...
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Am I perhaps worrying about something that isn't really a big issue?
Only you can answer that question
What fps can I expect on different devices (mobile, pc, tablet, etc.)?
Most devices are 60fps but there are gamer monitors that run at 120fps or 240fps as well as VR (and WebVR) which generally runs at 90fps or more and you can use browsers inside VR. Whether or not you care about any of those situations is up to you.
Measuring time since the animation start and using it as a reference in width calculation would be a common practice here.
Using this code the animation will last one second no matter what FPS the device has.
const el = document.getElementById('id1');
const start = Date.now();
function animateWidth() {
const t = (Date.now() - start) / 1000; // 1000 - one second
if(t >= 1) {
el.setAttribute('width', 0);
}
else {
el.setAttribute('width', 600 * (1 - t));
window.requestAnimationFrame(animateWidth);
}
}
animateWidth();

Using while(Date.now() < interval) {} with requestAnimationFrame()

We all know how difficult it is to make a proper update algorithm if certain fps is important or something like this.
Anyway, I just came up with this infinite-ish while cycle hack, which just freezes the program until the next frame, and it seems to work flawlessly.
var then = Date.now()
var fps = 40;
var interval = 1000 / fps;
function mainloop() {
while (Date.now() - then < interval) {} // freezes program until next frame
requestAnimationFrame(mainloop);
then = Date.now();
// update logic goes here
}
mainloop();
I haven't seen this solution anywhere, so I wanted to ask whether it is clean and correct. I know it is bad freezing the program just to wait for something and that piece of code looks terrible, but it seems to work. Is there a cleaner solution that would work similarly to my code?
You can use setTimeout to wait for a certain time but that will not be very precise. However by changing interval all the time you can get the average delay precise enough.
var startTime = Date.now();
var fps = 40;
var frames = 0;
var interval = 1000 / fps;
function mainloop() {
frames++;
var timeElapsed = Date.now() - startTime,
averageFps = 1000 * frames / timeElapsed;
if(averageFps < fps && interval > 0) interval -= 0.1;
if(averageFps > fps) interval += 0.1;
setTimeout(mainloop, interval);
// update logic goes here
}
setTimeout(mainloop, interval);
But there is still the risk that the computer isn't able to meet the requested fps if it's too slow.
Using a while loop to waste time is a bad idea. It just wastes processor time that could be doing something else.
Using SetTimeout as suggested by jishi, is one possible solution. However, that only controls when your code runs. You have no real control over when the browser actually paints. Bascially, the browser will paint the last frame that your code updated.
Thus, another possible solution is to use requestAnimation. In your drawing code, determine the last frame that would have occurred at your preferred rate. Draw that frame. For example...
var start = null;
var fps = 40;
var interval = 1000 / fps;
function mainloop(timeStamp) {
if (!start) {
start = timeStamp;
}
var n = (timeStamp - start) / interval;
// update logic goes here to paint nth frame
requestAnimationFrame(mainloop);
}
mainloop();
In your specific scenario, it would probably be better to delay the mainloop execution using setTimeout, like this:
var nextExecution = Date.now() - then + interval;
if (nextExecution < 0) nextExecution = 0;
setTimeout(mainloop, nextExecution);
This will allow it to do other stuff while waiting for the next frame rendering.

Categories

Resources