Controlling fps with requestAnimationFrame?
Asked Answered
M

15

228

It seems like requestAnimationFrame is the de facto way to animate things now. It worked pretty well for me for the most part, but right now I'm trying to do some canvas animations and I was wondering: Is there any way to make sure it runs at a certain fps? I understand that the purpose of rAF is for consistently smooth animations, and I might run the risk of making my animation choppy, but right now it seems to run at drastically different speeds pretty arbitrarily, and I'm wondering if there's a way to combat that somehow.

I'd use setInterval but I want the optimizations that rAF offers (especially automatically stopping when the tab is in focus).

In case someone wants to look at my code, it's pretty much:

animateFlash: function() {
    ctx_fg.clearRect(0,0,canvasWidth,canvasHeight);
    ctx_fg.fillStyle = 'rgba(177,39,116,1)';
    ctx_fg.strokeStyle = 'none';
    ctx_fg.beginPath();
    for(var i in nodes) {
        nodes[i].drawFlash();
    }
    ctx_fg.fill();
    ctx_fg.closePath();
    var instance = this;
    var rafID = requestAnimationFrame(function(){
        instance.animateFlash();
    })

    var unfinishedNodes = nodes.filter(function(elem){
        return elem.timer < timerMax;
    });

    if(unfinishedNodes.length === 0) {
        console.log("done");
        cancelAnimationFrame(rafID);
        instance.animate();
    }
}

Where Node.drawFlash() is just some code that determines radius based off a counter variable and then draws a circle.

Mesquite answered 4/11, 2013 at 8:31 Comment(3)
Does your animation lag? I think the biggest advantage of requestAnimationFrame is (as the name kind of suggests) to request an animation frame only when it is needed. Let's say you show a static black canvas, you should get 0 fps because no new frame is needed. But if you're displaying an animation that requires 60fps, you should get that too. rAF just allows to "skip" useless frames and then save CPU.Ause
setInterval do not work in inactive tab too.Elisha
This code runs differently on 90hz display vs 60hz display vs 144hz display.Octavia
M
268

How to throttle requestAnimationFrame to a specific frame rate

Demo throttling at 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

This method works by testing the elapsed time since executing the last frame loop.

Your drawing code executes only when your specified FPS interval has elapsed.

The first part of the code sets some variables used to calculate elapsed time.

var stop = false;
var frameCount = 0;
var $results = $("#results");
var fps, fpsInterval, startTime, now, then, elapsed;


// initialize the timer variables and start the animation

function startAnimating(fps) {
    fpsInterval = 1000 / fps;
    then = Date.now();
    startTime = then;
    animate();
}

And this code is the actual requestAnimationFrame loop which draws at your specified FPS.

// the animation loop calculates time elapsed since the last loop
// and only draws if your specified fps interval is achieved

function animate() {

    // request another frame

    requestAnimationFrame(animate);

    // calc elapsed time since last loop

    now = Date.now();
    elapsed = now - then;

    // if enough time has elapsed, draw the next frame

    if (elapsed > fpsInterval) {

        // Get ready for next frame by setting then=now, but also adjust for your
        // specified fpsInterval not being a multiple of RAF's interval (16.7ms)
        then = now - (elapsed % fpsInterval);

        // Put your drawing code here

    }
}
Marvamarve answered 4/11, 2013 at 16:25 Comment(19)
Nice demo - it should be accepted. Here, forked your fiddle, to demonstrate using window.performance.now() instead of Date.now(). This goes nicely with the high-res timestamp that rAF already recieves, so there's no need to call Date.now() inside the callback: jsfiddle.net/chicagogrooves/nRpVD/2Bravado
Thanks for the updated link using the new rAF timestamp feature. The new rAF timestamp adds useful infrastruction and it's also more precise than Date.now.Marvamarve
This is a really nice demo, which inspired me to make my own (JSFiddle). The main differences are using rAF (like Dean's demo) instead of Date, adding controls to dynamically adjust target framerate, sampling framerate on a separate interval from the animation, and adding a graph of historical framerates.Orel
This is an ingenious solution-- the only problem is it creates additional overhead in the RAF, and can hurt the actual frame-rate due to all the data-manipulation going on inside the RAF. To circumvent this, keep the data manipulation in a seperate setInterval, if possible in a web-worker so it has it's own thread. Ideally the RAF should only update graphics, and read js objects containing current data. Data manipulation should be done outside the RAF and the new data placed in objects for your RAF callback to read.Largent
Can someone explain me the elapsed % fpsInterval part ? Why we need to "also adjust for your specified fpsInterval not being a multiple of RAF's interval (16.7ms)" ?Interlocution
All you can control is when you're going to skip a frame. A 60 fps monitor always draws at 16ms intervals. For example if you want your game to run at 50fps, you want to skip every 6th frame. You check if 20ms (1000/50) has elapsed, and it hasn't (only 16ms has elapsed) so you skip a frame, then the next frame 32ms has elapsed since you drew, so you draw and reset. But then you'll skip half the frames and run at 30fps. So when you reset you remember you waited 12ms too long last time. So next frame another 16ms passes but you count it as 16+12=28ms so you draw again and you waited 8ms too longDaffodil
This answer does not work for many fps values I tried, 5, 10, 20, 30...Dabchick
shouldn't requestAnimationFrame(animate); be after the if (elapsed > fpsInterval) {} block ?Tropicalize
@Tropicalize no, that doesn't really matter. RAF only tells the browser to call animate() before it draws the next frame. The only difference in putting it afterwards is that you could miss a RAF call because your code inbetween took longer than the browsers (monitors) frame rate. see also: #29181753Assume
The above solution/answer by @Marvamarve has a problem, leave the browser open at 60FPS, let it run. After about 500secs, frame rate drops to 35, then 20's, then down to 13 fps after an hour or so. What is the problem? How can it be fixed?Contraposition
This is broken. There is no accumulation of the remaining time across frames. This render loop is subject to time drift. To fix it, you need to keep track of the time remaining after the modulus, and carry it across to the next frame.Octavia
holy shit, elapsed > fpsInterval is much slower than elapsed > parseInt(fpsInterval)Handbreadth
As @Octavia mentioned this may cause drifting. Another fix would be to just increment an "expected" time by the known interval and then wait for it. Also, the first (and only) callback parameter to requestAnimationFrame is a DOMHighResTimeStamp (ms but float value, may be more precise in certain builds/circumstances, but regardless cuts the need to call Date,(i.e., function animate(animationTime) { ... })Headgear
@DeanRadcliffe what is newtime? I didn't understand the source of itContaminant
In response to @Octavia and Synexis comments this may be a good example: cleverti.com/blog/…Gramercy
One caveat though, it doesn't display current FPS, it displays average fps since the start. If you set a higher fps limit, it will start slowing down the more time passes.Supper
Just wondering why you're not making use of the timestamp that gets passed into the animate function by the request animation function, surely this negates the need to call the date function? Unless there's something I've missed is there a reason this way is preferable?Reannareap
What could be the reason for never getting higher than around 31 fps with requestAnimationFrame on my system?Alduino
Notice that the adjustment may be detrimental to animation's fluidity. It may theoretically produce the given fps but will be jittery as it will be dropping frames irregularly. If you only reset the timer without the modulus adjustment it will lock fps to an integer division of the monitor's max fps without any jitter or jerkiness. E.g. for 144hz -> 72, 48 and 36 work great. For 60hz -> 30, 20, 15.Aureliaaurelian
C
78

I suggest wrapping your call to requestAnimationFrame in a setTimeout:

const fps = 25;
function animate() {
  // perform some animation task here

  setTimeout(() => {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
animate();

You need to call requestAnimationFrame from within setTimeout, rather than the other way around, because requestAnimationFrame schedules your function to run right before the next repaint, and if you delay your update further using setTimeout you will have missed that time window. However, doing the reverse is sound, since you’re simply waiting a period of time before making the request.

Catlin answered 25/8, 2016 at 2:9 Comment(7)
This actually seems to work in keeping the framerate down and so not cooking my CPU. And it's so simple. Cheers!Internment
This is a nice, simple way to do it for lightweight animations. It does get a little out of sync though, at least on some devices. I used this technique on one of my former engines. It worked good till things got complex. Biggest problem was when hooked up to orientation sensors, it would either lag behind or get jumpy. Later I found using a seperate setInterval and communicating updates between sensors, setInterval frames, and RAF frames via object properties allowed the sensors and RAF to go real-time, while animation time could be controlled via property updates from setInterval.Largent
My monitor is 60 FPS, if I set var fps=60, I only get about 50 FPS using this code. I want to slow it to 60 because some people have 120 FPS monitors, but I don't want to affect everyone else. This is surprisingly difficult.Daffodil
The reason why you get lower FPS than expected is because setTimeout can execute the callback after more than the specified delay. There is a number of possible reason for this. And every loop it takes the time to set a new timer and execute some code before setting the new timeout. You have no way to be accurate with this, you should always consider a slower than expected result, but as long as you don't know how much slower it will be, trying to lower the delay would be inaccurate as well. JS in browsers is not meant to be so accurate.Cloots
This causes significant stutter and shouldn't be used in any production gamesTouchline
Please see simpler answer, after my original answer.Largent
@Touchline I agree. I don't see why so many likes on this. Very inefficient. Why I worked hard for a better solution that actually does what it claims. Nothing wrong with trying. Just... I was blessed enough to be endowed with a truely working solution to this problem.Largent
B
66

Update 2016/6

The problem throttling the frame rate is that the screen has a constant update rate, typically 60 FPS.

If we want 24 FPS we will never get the true 24 fps on the screen, we can time it as such but not show it as the monitor can only show synced frames at 15 fps, 30 fps or 60 fps (some monitors also 120 fps).

However, for timing purposes we can calculate and update when possible.

You can build all the logic for controlling the frame-rate by encapsulating calculations and callbacks into an object:

function FpsCtrl(fps, callback) {

    var delay = 1000 / fps,                               // calc. time per frame
        time = null,                                      // start time
        frame = -1,                                       // frame count
        tref;                                             // rAF time reference

    function loop(timestamp) {
        if (time === null) time = timestamp;              // init start time
        var seg = Math.floor((timestamp - time) / delay); // calc frame no.
        if (seg > frame) {                                // moved to next frame?
            frame = seg;                                  // update
            callback({                                    // callback function
                time: timestamp,
                frame: frame
            })
        }
        tref = requestAnimationFrame(loop)
    }
}

Then add some controller and configuration code:

// play status
this.isPlaying = false;

// set frame-rate
this.frameRate = function(newfps) {
    if (!arguments.length) return fps;
    fps = newfps;
    delay = 1000 / fps;
    frame = -1;
    time = null;
};

// enable starting/pausing of the object
this.start = function() {
    if (!this.isPlaying) {
        this.isPlaying = true;
        tref = requestAnimationFrame(loop);
    }
};

this.pause = function() {
    if (this.isPlaying) {
        cancelAnimationFrame(tref);
        this.isPlaying = false;
        time = null;
        frame = -1;
    }
};

Usage

It becomes very simple - now, all that we have to do is to create an instance by setting callback function and desired frame rate just like this:

var fc = new FpsCtrl(24, function(e) {
     // render each frame here
  });

Then start (which could be the default behavior if desired):

fc.start();

That's it, all the logic is handled internally.

Demo

var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0;
ctx.font = "20px sans-serif";

// update canvas with some information and animation
var fps = new FpsCtrl(12, function(e) {
	ctx.clearRect(0, 0, c.width, c.height);
	ctx.fillText("FPS: " + fps.frameRate() + 
                 " Frame: " + e.frame + 
                 " Time: " + (e.time - pTime).toFixed(1), 4, 30);
	pTime = e.time;
	var x = (pTime - mTime) * 0.1;
	if (x > c.width) mTime = pTime;
	ctx.fillRect(x, 50, 10, 10)
})

// start the loop
fps.start();

// UI
bState.onclick = function() {
	fps.isPlaying ? fps.pause() : fps.start();
};

sFPS.onchange = function() {
	fps.frameRate(+this.value)
};

function FpsCtrl(fps, callback) {

	var	delay = 1000 / fps,
		time = null,
		frame = -1,
		tref;

	function loop(timestamp) {
		if (time === null) time = timestamp;
		var seg = Math.floor((timestamp - time) / delay);
		if (seg > frame) {
			frame = seg;
			callback({
				time: timestamp,
				frame: frame
			})
		}
		tref = requestAnimationFrame(loop)
	}

	this.isPlaying = false;
	
	this.frameRate = function(newfps) {
		if (!arguments.length) return fps;
		fps = newfps;
		delay = 1000 / fps;
		frame = -1;
		time = null;
	};
	
	this.start = function() {
		if (!this.isPlaying) {
			this.isPlaying = true;
			tref = requestAnimationFrame(loop);
		}
	};
	
	this.pause = function() {
		if (this.isPlaying) {
			cancelAnimationFrame(tref);
			this.isPlaying = false;
			time = null;
			frame = -1;
		}
	};
}
body {font:16px sans-serif}
<label>Framerate: <select id=sFPS>
	<option>12</option>
	<option>15</option>
	<option>24</option>
	<option>25</option>
	<option>29.97</option>
	<option>30</option>
	<option>60</option>
</select></label><br>
<canvas id=c height=60></canvas><br>
<button id=bState>Start/Stop</button>

Old answer

The main purpose of requestAnimationFrame is to sync updates to the monitor's refresh rate. This will require you to animate at the FPS of the monitor or a factor of it (ie. 60, 30, 15 FPS for a typical refresh rate @ 60 Hz).

If you want a more arbitrary FPS then there is no point using rAF as the frame rate will never match the monitor's update frequency anyways (just a frame here and there) which simply cannot give you a smooth animation (as with all frame re-timings) and you can might as well use setTimeout or setInterval instead.

This is also a well known problem in the professional video industry when you want to playback a video at a different FPS then the device showing it refresh at. Many techniques has been used such as frame blending and complex re-timing re-building intermediate frames based on motion vectors, but with canvas these techniques are not available and the result will always be jerky video.

var FPS = 24;  /// "silver screen"
var isPlaying = true;

function loop() {
    if (isPlaying) setTimeout(loop, 1000 / FPS);

    ... code for frame here
}

The reason why we place setTimeout first (and why some place rAF first when a poly-fill is used) is that this will be more accurate as the setTimeout will queue an event immediately when the loop starts so that no matter how much time the remaining code will use (provided it doesn't exceed the timeout interval) the next call will be at the interval it represents (for pure rAF this is not essential as rAF will try to jump onto the next frame in any case).

Also worth to note that placing it first will also risk calls stacking up as with setInterval. setInterval may be slightly more accurate for this use.

And you can use setInterval instead outside the loop to do the same.

var FPS = 29.97;   /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);

function loop() {

    ... code for frame here
}

And to stop the loop:

clearInterval(rememberMe);

In order to reduce frame rate when the tab gets blurred you can add a factor like this:

var isFocus = 1;
var FPS = 25;

function loop() {
    setTimeout(loop, 1000 / (isFocus * FPS)); /// note the change here

    ... code for frame here
}

window.onblur = function() {
    isFocus = 0.5; /// reduce FPS to half   
}

window.onfocus = function() {
    isFocus = 1; /// full FPS
}

This way you can reduce the FPS to 1/4 etc.

Benenson answered 4/11, 2013 at 17:31 Comment(5)
In some instances you are not trying to match the monitors frame rate but rather, in image sequences for example, drop frames. Excellent explanation btwMancini
One of the biggest reasons to throttle with requestAnimationFrame would be to line up execution of some code with the browsers's animation frame. Things end up running a lot smoother, especially if you're running some logic on data every frame, like with music visualizers for example.Katheryn
This is bad because the main use of requestAnimationFrame is to synchronize DOM operations (read/write) so not using it will hurt performance when accessing the DOM, since operations will not be queued to be performed together and will force layout repaint needlessly.Bonne
There is no risk of "calls stacking up", as JavaScript runs single threaded, and no timeout event is triggered while the your code is running. So if the function takes longer than the timeout, it just runs almost any time as fast as it can, while the browser would still do redraws and trigger other timeouts inbetween the calls.Pitarys
I know that you state the page refresh cannot be updated faster than the fps limit on the display. However, is it possible to refresh faster by triggering a page reflow? Conversely, is it possible to not notice multiple page reflows if they are done faster than the native fps rate?Alyssa
L
32

These are all good ideas in theory, until you go deep. The problem is you can't throttle an RAF without de-synchronizing it, defeating it's very purpose for existing. So you let it run at full-speed, and update your data in a separate loop, or even a separate thread!

Yes, I said it. You can do multi-threaded JavaScript in the browser!

There are two methods I know that work extremely well without jank, using far less juice and creating less heat. Accurate human-scale timing and machine efficiency are the net result.

Apologies if this is a little wordy, but here goes...


Method 1: Update data via setInterval, and graphics via RAF.

Use a separate setInterval for updating translation and rotation values, physics, collisions, etc. Keep those values in an object for each animated element. Assign the transform string to a variable in the object each setInterval 'frame'. Keep these objects in an array. Set your interval to your desired fps in ms: ms=(1000/fps). This keeps a steady clock that allows the same fps on any device, regardless of RAF speed. Do not assign the transforms to the elements here!

In a requestAnimationFrame loop, iterate through your array with an old-school for loop-- do not use the newer forms here, they are slow!

for(var i=0; i<sprite.length-1; i++){  rafUpdate(sprite[i]);  }

In your rafUpdate function, get the transform string from your js object in the array, and its elements id. You should already have your 'sprite' elements attached to a variable or easily accessible through other means so you don't lose time 'get'-ing them in the RAF. Keeping them in an object named after their html id's works pretty good. Set that part up before it even goes into your SI or RAF.

Use the RAF to update your transforms only, use only 3D transforms (even for 2d), and set css "will-change: transform;" on elements that will change. This keeps your transforms synced to the native refresh rate as much as possible, kicks in the GPU, and tells the browser where to concentrate most.

So you should have something like this pseudocode...

// refs to elements to be transformed, kept in an array
var element = [
   mario: document.getElementById('mario'),
   luigi: document.getElementById('luigi')
   //...etc.
]

var sprite = [  // read/write this with SI.  read-only from RAF
   mario: { id: mario  ....physics data, id, and updated transform string (from SI) here  },
   luigi: {  id: luigi  .....same  }
   //...and so forth
] // also kept in an array (for efficient iteration)

//update one sprite js object
//data manipulation, CPU tasks for each sprite object
//(physics, collisions, and transform-string updates here.)
//pass the object (by reference).
var SIupdate = function(object){
  // get pos/rot and update with movement
  object.pos.x += object.mov.pos.x;  // example, motion along x axis
  // and so on for y and z movement
  // and xyz rotational motion, scripted scaling etc

  // build transform string ie
  object.transform =
   'translate3d('+
     object.pos.x+','+
     object.pos.y+','+
     object.pos.z+
   ') '+

   // assign rotations, order depends on purpose and set-up. 
   'rotationZ('+object.rot.z+') '+
   'rotationY('+object.rot.y+') '+
   'rotationX('+object.rot.x+') '+

   'scale3d('.... if desired
  ;  //...etc.  include 
}


var fps = 30; //desired controlled frame-rate


// CPU TASKS - SI psuedo-frame data manipulation
setInterval(function(){
  // update each objects data
  for(var i=0; i<sprite.length-1; i++){  SIupdate(sprite[i]);  }
},1000/fps); //  note ms = 1000/fps


// GPU TASKS - RAF callback, real frame graphics updates only
var rAf = function(){
  // update each objects graphics
  for(var i=0; i<sprite.length-1; i++){  rAF.update(sprite[i])  }
  window.requestAnimationFrame(rAF); // loop
}

// assign new transform to sprite's element, only if it's transform has changed.
rAF.update = function(object){     
  if(object.old_transform !== object.transform){
    element[object.id].style.transform = transform;
    object.old_transform = object.transform;
  }
} 

window.requestAnimationFrame(rAF); // begin RAF

This keeps your updates to the data objects and transform strings synced to desired 'frame' rate in the SI, and the actual transform assignments in the RAF synced to GPU refresh rate. So the actual graphics updates are only in the RAF, but the changes to the data, and building the transform string are in the SI, thus no jankies but 'time' flows at desired frame-rate.


Flow:

[setup js sprite objects and html element object references]

[setup RAF and SI single-object update functions]

[start SI at percieved/ideal frame-rate]
  [iterate through js objects, update data transform string for each]
  [loop back to SI]

[start RAF loop]
  [iterate through js objects, read object's transform string and assign it to it's html element]
  [loop back to RAF]

Method 2. Put the SI in a web-worker. This one is FAAAST and smooth!

Same as method 1, but put the SI in web-worker. It'll run on a totally separate thread then, leaving the page to deal only with the RAF and UI. Pass the sprite array back and forth as a 'transferable object'. This is buko fast. It does not take time to clone or serialize, but it's not like passing by reference in that the reference from the other side is destroyed, so you will need to have both sides pass to the other side, and only update them when present, sort of like passing a note back and forth with your girlfriend in high-school.

Only one can read and write at a time. This is fine so long as they check if it's not undefined to avoid an error. The RAF is FAST and will kick it back immediately, then go through a bunch of GPU frames just checking if it's been sent back yet. The SI in the web-worker will have the sprite array most of the time, and will update positional, movement and physics data, as well as creating the new transform string, then pass it back to the RAF in the page.

This is the fastest way I know to animate elements via script. The two functions will be running as two separate programs, on two separate threads, taking advantage of multi-core CPU's in a way that a single js script does not. Multi-threaded javascript animation.

And it will do so smoothly without jank, but at the actual specified frame-rate, with very little divergence.


Result:

Either of these two methods will ensure your script will run at the same speed on any PC, phone, tablet, etc (within the capabilities of the device and the browser, of course).

Largent answered 23/1, 2018 at 23:44 Comment(17)
As a side note-- in Method 1, if there is too much activity in your setInterval it may slow down your RAF due to single-threaded async. You can mitigate this breaking up that activity over more than on SI frame, so async will pass control back to RAF quicker. Remember, RAF goes at max frame-rate, but syncs graphical changes with the display, so it's ok to skip a few RAF frames-- as long as you don't skip more than SI frames it won't jank.Largent
Method 2 is more robust, as it is actually multi-tasking the two loops, not switching back and forth through async, but you still want to avoid your SI frame taking longer than your desired frame-rate, so splitting SI activity may still be desirable if it has a lot of data-manipulation going on that would take more than one SI frame to complete.Largent
I thought it worth mentioning, as a note of interest, that running paired loops like this actually registers in Chromes DevTools that the GPU is running at the frame-rate specified in the setInterval loop! It appears only RAF frames in which graphical changes occur are counted as frames by the FPS meter. So RAF frames in which only non-graphical work, or even just blank loops, don't count as far as the GPU is concerned. I find this interesting as a starting point for further research.Largent
I believe this solution has the problem that it keeps running when rAF gets suspended, e.g. because the user switched to another tab.Assume
P.S. I did some reading and it seems most browsers limit timed events to once per second in background tabs anyway (which should probably also be handled in some way). If you still want to address the issue and completely pause when not visible, there seems to be the visibilitychange event.Assume
Nice! Yes, they do limit tabs in BG for good reasons! RAF basically just stops. Did not know about visibilitychange event. That would make it much simpler to pause and/or calculate where things should be once the tab is active again!Largent
@Largent Maybe I'm missing something. I thought you can't change the DOM through a web worker.Spit
You don't. You do calculations in the web-worker and message the results. Other than that you're still running your RAF the same. You could similarly run another thread via an iframe. The messaging works basically the same. I haven't tried the iframe idea. Either way it would place the calculations in a separate thread than the parts running the RAF and the interval frames.Largent
for(var i=0; i<sprite.length-1; i++) Why are you using "less-than" with "length-1"? You will never update the last element this way.Preachy
I used Method1 and it really helped. Can be tailored to multiple situations and refactoring under this paradigm helped in my situation where having it all in a single loop of the RAF calls did not workUndertake
@R.Navega Probably a typo, or to avoid an error in my implementation. Do what works.Largent
@Undertake Very glad this helped you. Would love to see what you have done with it. Nobody ever shows their stuff!Largent
NOTE: I think Method 2 is no longer viable. Stick with Method 1.Largent
@Assume True, but not the purpose here. Doesn't really matter. There are ways you can choose to stop your code when in background. Point here is what happens when in the forefront.Largent
@Largent why is method 2 no longer viable?Aricaarick
@Largent how does this approach compare to the kind of thing described in gafferongames.com/post/fix_your_timestep ? My hunch is that your approach is easier to implement, but still suffers from the timing issues of setInterval.Aricaarick
@Aricaarick Not sure about first question; will have to check it out. Wrote this years ago. No, does not have same issue. If your setInterval is outside of the RAF, and sticks to making updates to variables/objects to store the style changes, with the RAF only doing the work up applying the whole set of changes each frame-- Chromium translates this into only making he changes when something actually changes, hence DevTools will show the framerate of your setInterval. In this method, the RAF skips doing any work except when something changes. Frames that change, all he same-- so no jank.Largent
L
15

How to easily throttle to a specific FPS:

// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
    maxFPS = 30,
    timestep = 1000 / maxFPS; // ms for each frame

function main(timestamp) {
    window.requestAnimationFrame(main);

    // skip if timestep ms hasn't passed since last frame
    if (timestamp - lastTimestamp < timestep) return;

    lastTimestamp = timestamp;

    // draw frame here
}

window.requestAnimationFrame(main);

Source: A Detailed Explanation of JavaScript Game Loops and Timing by Isaac Sukin

Lvov answered 6/1, 2018 at 6:51 Comment(4)
If my monitor runs at 60 FPS and I want my game to run at 58 FPS I set maxFPS=58, this will make it run at 30 FPS because it will skip every 2nd frame.Daffodil
Yes, I tried this one as well. I choose not to actually throttle the RAF itself-- only the changes are updated by the setTimeout. In Chrome at least, this causes the effective fps to run at the setTimeouts pace, according to readings in DevTools. Of course it can only update real video frames at the speed of the video card and monitor refresh rate, but this method appears to operate with the least jankies, so smoothest "apparent" fps control, which is what I'm going for.Largent
Since I keep track of all motion in JS objects separately from the RAF, this keeps the animation logic, collision detection or whatever you need, running at a perceptually consistent rate, regardless of the RAF or the setTimeout, with a little extra math.Largent
It's important to mention in the answer that requestAnimationFrame callback automatically gets an argument "similar to the one returned by performance.now(), indicating the point in time when requestAnimationFrame() starts to execute callback functions" MDNBonne
P
8

The simplest way

note: It might behave differently on different screens with different frame rate.


const FPS = 30;
let lastTimestamp = 0;


function update(timestamp) {
  requestAnimationFrame(update);
  if (timestamp - lastTimestamp < 1000 / FPS) return;
  
  
   /* <<< PUT YOUR CODE HERE >>>  */

 
  lastTimestamp = timestamp;
}


update();

Prefigure answered 21/1, 2021 at 14:10 Comment(3)
is it even stable?Brigitta
I set the FPS to 65 (not 60) to reach stable 60 fps on 120 HZ mobile screen as well as 60 HZ desktopAnnetteannex
Thiis is how I did it, but when the frame generation takes longer than a period, they start stacking up; I got recursive rAF calls and the browser would hang and freeze. I'll use some other method on this page.Hinrichs
T
6
var time = 0;
var time_framerate = 1000; //in milliseconds

function animate(timestamp) {
  if(timestamp > time + time_framerate) {
    time = timestamp;    

    //your code
  }

  window.requestAnimationFrame(animate);
}
Toleration answered 31/3, 2020 at 19:16 Comment(1)
Please add a few sentences to explain what your code is doing, so you can get more upvotes for your answer.Rivy
O
6

A simple solution to this problem is to return from the render loop if the frame is not required to render:

const FPS = 60;
let prevTick = 0;    

function render() 
{
    requestAnimationFrame(render);

    // clamp to fixed framerate
    let now = Math.round(FPS * Date.now() / 1000);
    if (now == prevTick) return;
    prevTick = now;

    // otherwise, do your stuff ...
}

It's important to know that requestAnimationFrame depends on the users monitor refresh rate (vsync). So, relying on requestAnimationFrame for game speed for example will make it unplayable on 200Hz monitors if you're not using a separate timer mechanism in your simulation.

Octillion answered 10/9, 2020 at 14:28 Comment(1)
This is the only solution that worked for me on three.js v106Strain
L
4

Simplified explanation of earlier answer. At least if you want real-time, accurate throttling without the janks, or dropping frames like bombs. GPU and CPU friendly.

setInterval and setTimeout are both CPU-oriented, not GPU.

requestAnimationFrame is purely GPU-oriented.

Run them separately. It's simple and not janky. In your setInterval, update your math and create a little CSS script in a string. With your RAF loop, only use that script to update the new coordinates of your elements. Don't do anything else in the RAF loop.

The RAF is tied inherently to the GPU. Whenever the script does not change (i.e. because the SI is running a gazillion times slower), Chromium-based browsers know they do not need to do anything, because there are no changes. So the on-the-fly script created each "frame", say 60 times per second, is still the same for say 1000 RAF GPU frames, but it knows nothing has changed, and the net result is it wastes no energy on this. If you check in DevTools, you will see your GPU frame-rate registers at the rate delineated by the setInterval.

Truely, it is just that simple. Separate them, and they will cooperate.

No jankies.

Largent answered 9/7, 2022 at 9:49 Comment(0)
P
3

Here is an idea to reach desired fps:

  1. detect browser's animationFrameRate (typically 60fps)
  2. build a bitSet, according to animationFrameRate and your disiredFrameRate (say 24fps)
  3. lookup bitSet and conditionally "continue" the animation frame loop

It uses requestAnimationFrame so the actual frame rate won't be greater than animationFrameRate. you may adjust disiredFrameRate according to animationFrameRate.

I wrote a mini lib, and a canvas animation demo.

function filterNums(nums, jitter = 0.2, downJitter = 1 - 1 / (1 + jitter)) {
  let len = nums.length;
  let mid = Math.floor(len % 2 === 0 ? len / 2 : (len - 1) / 2), low = mid, high = mid;
  let lower = true, higher = true;
  let sum = nums[mid], count = 1;
  for (let i = 1, j, num; i <= mid; i += 1) {
    if (higher) {
      j = mid + i;
      if (j === len)
        break;
      num = nums[j];
      if (num < (sum / count) * (1 + jitter)) {
        sum += num;
        count += 1;
        high = j;
      } else {
        higher = false;
      }
    }
    if (lower) {
      j = mid - i;
      num = nums[j];
      if (num > (sum / count) * (1 - downJitter)) {
        sum += num;
        count += 1;
        low = j;
      } else {
        lower = false;
      }
    }
  }
  return nums.slice(low, high + 1);
}

function snapToOrRound(n, values, distance = 3) {
  for (let i = 0, v; i < values.length; i += 1) {
    v = values[i];
    if (n >= v - distance && n <= v + distance) {
      return v;
    }
  }
  return Math.round(n);
}

function detectAnimationFrameRate(numIntervals = 6) {
  if (typeof numIntervals !== 'number' || !isFinite(numIntervals) || numIntervals < 2) {
    throw new RangeError('Argument numIntervals should be a number not less than 2');
  }
  return new Promise((resolve) => {
    let num = Math.floor(numIntervals);
    let numFrames = num + 1;
    let last;
    let intervals = [];
    let i = 0;
    let tick = () => {
      let now = performance.now();
      i += 1;
      if (i < numFrames) {
        requestAnimationFrame(tick);
      }
      if (i === 1) {
        last = now;
      } else {
        intervals.push(now - last);
        last = now;
        if (i === numFrames) {
          let compareFn = (a, b) => a < b ? -1 : a > b ? 1 : 0;
          let sortedIntervals = intervals.slice().sort(compareFn);
          let selectedIntervals = filterNums(sortedIntervals, 0.2, 0.1);
          let selectedDuration = selectedIntervals.reduce((s, n) => s + n, 0);
          let seletedFrameRate = 1000 / (selectedDuration / selectedIntervals.length);
          let finalFrameRate = snapToOrRound(seletedFrameRate, [60, 120, 90, 30], 5);
          resolve(finalFrameRate);
        }
      }
    };
    requestAnimationFrame(() => {
      requestAnimationFrame(tick);
    });
  });
}
function buildFrameBitSet(animationFrameRate, desiredFrameRate){
  let bitSet = new Uint8Array(animationFrameRate);
  let ratio = desiredFrameRate / animationFrameRate;
  if(ratio >= 1)
    return bitSet.fill(1);
  for(let i = 0, prev = -1, curr; i < animationFrameRate; i += 1, prev = curr){
    curr = Math.floor(i * ratio);
    bitSet[i] = (curr !== prev) ? 1 : 0;
  }
  return bitSet;
}



let $ = (s, c = document) => c.querySelector(s);
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));

async function main(){
  let canvas = $('#digitalClock');
  let context2d = canvas.getContext('2d');
  await new Promise((resolve) => {
    if(window.requestIdleCallback){
      requestIdleCallback(resolve, {timeout:3000});
    }else{
      setTimeout(resolve, 0, {didTimeout: false});
    }
  });
  let animationFrameRate = await detectAnimationFrameRate(10); // 1. detect animation frame rate
  let desiredFrameRate = 24;
  let frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); // 2. build a bit set
  let handle;
  let i = 0;
  let count = 0, then, actualFrameRate = $('#actualFrameRate'); // debug-only

  let draw = () => {
    if(++i >= animationFrameRate){ // shoud use === if frameBits don't change dynamically
      i = 0;
      /* debug-only */
      let now = performance.now();
      let deltaT = now - then;
      let fps = 1000 / (deltaT / count);
      actualFrameRate.textContent = fps;
      then = now;
      count = 0;
    }
    if(frameBits[i] === 0){ // 3. lookup the bit set
      handle = requestAnimationFrame(draw);
      return;
    }
    count += 1; // debug-only
    let d = new Date();
    let text = d.getHours().toString().padStart(2, '0') + ':' +
        d.getMinutes().toString().padStart(2, '0') + ':' +
        d.getSeconds().toString().padStart(2, '0') + '.' +
        (d.getMilliseconds() / 10).toFixed(0).padStart(2, '0');
    context2d.fillStyle = '#000000';
    context2d.fillRect(0, 0, canvas.width, canvas.height);
    context2d.font = '36px monospace';
    context2d.fillStyle = '#ffffff';
    context2d.fillText(text, 0, 36);
    handle = requestAnimationFrame(draw);
  };
  handle = requestAnimationFrame(() => {
    then = performance.now();
    handle = requestAnimationFrame(draw);
  });

  /* debug-only */
  $('#animationFrameRate').textContent = animationFrameRate;
  let frameRateInput = $('#frameRateInput');
  let frameRateOutput = $('#frameRateOutput');
  frameRateInput.addEventListener('input', (e) => {
    frameRateOutput.value = e.target.value;
  });
  frameRateInput.max = animationFrameRate;
  frameRateOutput.value = frameRateOutput.value = desiredFrameRate;
  frameRateInput.addEventListener('change', (e) => {
    desiredFrameRate = +e.target.value;
    frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate);
  });
}

document.addEventListener('DOMContentLoaded', main);
<div>
  Animation Frame Rate: <span id="animationFrameRate">--</span>
</div>
<div>
  Desired Frame Rate: <input id="frameRateInput" type="range" min="1" max="60" step="1" list="frameRates" />
  <output id="frameRateOutput"></output>
  <datalist id="frameRates">
    <option>15</option>
    <option>24</option>
    <option>30</option>
    <option>48</option>
    <option>60</option>
  </datalist>
</div>
<div>
  Actual Frame Rate: <span id="actualFrameRate">--</span>
</div>

<canvas id="digitalClock" width="240" height="48"></canvas>
Parnassian answered 24/3, 2022 at 6:19 Comment(3)
While you will have FPS frames per seconds, each frame won't have the expected duration. When we say we want a 24FPS animation, we actually want each frame to last ~41.7ms, not that we have 23 1ms frames and one 977ms frame (to make things obviously extreme). That's basically what your code is doing, some frames will last 50ms (3 * 16.7), some 33ms (2 * 16.7) but none will have the expected 42ms duration. jsfiddle.net/qtj7mze0Assimilative
(also regarding the refresh-rate detector, monitors come with funny refresh-rates and any parallel task will get it off. In Chromium you could use a Worker to try to avoid that since rAF is available there, but even then it's rather luck based).Assimilative
Yes! each frame won't have the expected duration. until your GPU/monitor support Vriable Refresh Rate / Pro Motion / Dynamic Refresh Rate, and VRR-related API comes to browsers, then we can ensure each frame lasts the expected duration..Parnassian
E
2

Skipping requestAnimationFrame cause not smooth(desired) animation at custom fps.

// Input/output DOM elements
var $results = $("#results");
var $fps = $("#fps");
var $period = $("#period");

// Array of FPS samples for graphing

// Animation state/parameters
var fpsInterval, lastDrawTime, frameCount_timed, frameCount, lastSampleTime, 
		currentFps=0, currentFps_timed=0;
var intervalID, requestID;

// Setup canvas being animated
var canvas = document.getElementById("c");
var canvas_timed = document.getElementById("c2");
canvas_timed.width = canvas.width = 300;
canvas_timed.height = canvas.height = 300;
var ctx = canvas.getContext("2d");
var ctx2 = canvas_timed.getContext("2d");


// Setup input event handlers

$fps.on('click change keyup', function() {
    if (this.value > 0) {
        fpsInterval = 1000 / +this.value;
    }
});

$period.on('click change keyup', function() {
    if (this.value > 0) {
        if (intervalID) {
            clearInterval(intervalID);
        }
        intervalID = setInterval(sampleFps, +this.value);
    }
});


function startAnimating(fps, sampleFreq) {

    ctx.fillStyle = ctx2.fillStyle = "#000";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx2.fillRect(0, 0, canvas.width, canvas.height);
    ctx2.font = ctx.font = "32px sans";
    
    fpsInterval = 1000 / fps;
    lastDrawTime = performance.now();
    lastSampleTime = lastDrawTime;
    frameCount = 0;
    frameCount_timed = 0;
    animate();
    
    intervalID = setInterval(sampleFps, sampleFreq);
		animate_timed()
}

function sampleFps() {
    // sample FPS
    var now = performance.now();
    if (frameCount > 0) {
        currentFps =
            (frameCount / (now - lastSampleTime) * 1000).toFixed(2);
        currentFps_timed =
            (frameCount_timed / (now - lastSampleTime) * 1000).toFixed(2);
        $results.text(currentFps + " | " + currentFps_timed);
        
        frameCount = 0;
        frameCount_timed = 0;
    }
    lastSampleTime = now;
}

function drawNextFrame(now, canvas, ctx, fpsCount) {
    // Just draw an oscillating seconds-hand
    
    var length = Math.min(canvas.width, canvas.height) / 2.1;
    var step = 15000;
    var theta = (now % step) / step * 2 * Math.PI;

    var xCenter = canvas.width / 2;
    var yCenter = canvas.height / 2;
    
    var x = xCenter + length * Math.cos(theta);
    var y = yCenter + length * Math.sin(theta);
    
    ctx.beginPath();
    ctx.moveTo(xCenter, yCenter);
    ctx.lineTo(x, y);
  	ctx.fillStyle = ctx.strokeStyle = 'white';
    ctx.stroke();
    
    var theta2 = theta + 3.14/6;
    
    ctx.beginPath();
    ctx.moveTo(xCenter, yCenter);
    ctx.lineTo(x, y);
    ctx.arc(xCenter, yCenter, length*2, theta, theta2);

    ctx.fillStyle = "rgba(0,0,0,.1)"
    ctx.fill();
    
    ctx.fillStyle = "#000";
    ctx.fillRect(0,0,100,30);
    
    ctx.fillStyle = "#080";
    ctx.fillText(fpsCount,10,30);
}

// redraw second canvas each fpsInterval (1000/fps)
function animate_timed() {
    frameCount_timed++;
    drawNextFrame( performance.now(), canvas_timed, ctx2, currentFps_timed);
    
    setTimeout(animate_timed, fpsInterval);
}

function animate(now) {
    // request another frame
    requestAnimationFrame(animate);
    
    // calc elapsed time since last loop
    var elapsed = now - lastDrawTime;

    // if enough time has elapsed, draw the next frame
    if (elapsed > fpsInterval) {
        // Get ready for next frame by setting lastDrawTime=now, but...
        // Also, adjust for fpsInterval not being multiple of 16.67
        lastDrawTime = now - (elapsed % fpsInterval);

        frameCount++;
    		drawNextFrame(now, canvas, ctx, currentFps);
    }
}
startAnimating(+$fps.val(), +$period.val());
input{
  width:100px;
}
#tvs{
  color:red;
  padding:0px 25px;
}
H3{
  font-weight:400;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3>requestAnimationFrame skipping <span id="tvs">vs.</span> setTimeout() redraw</h3>
<div>
    <input id="fps" type="number" value="33"/> FPS:
    <span id="results"></span>
</div>
<div>
    <input id="period" type="number" value="1000"/> Sample period (fps, ms)
</div>
<canvas id="c"></canvas><canvas id="c2"></canvas>

Original code by @tavnab.

Erose answered 16/5, 2016 at 10:5 Comment(0)
T
1

I always do it this very simple way without messing with timestamps:

let fps, eachNthFrame, frameCount;

fps = 30;

//This variable specifies how many frames should be skipped.
//If it is 1 then no frames are skipped. If it is 2, one frame 
//is skipped so "eachSecondFrame" is renderd.
eachNthFrame = Math.round((1000 / fps) / 16.66);

//This variable is the number of the current frame. It is set to eachNthFrame so that the 
//first frame will be renderd.
frameCount = eachNthFrame;

requestAnimationFrame(frame);

//I think the rest is self-explanatory
function frame() {
  if (frameCount === eachNthFrame) {
    frameCount = 0;
    animate();
  }
  frameCount++;
  requestAnimationFrame(frame);
}
Toname answered 26/8, 2018 at 6:13 Comment(2)
This will run too fast if your monitor is 120 fps.Daffodil
This wont help you with a consistent framerateAstounding
A
1

For throttling FPS to any value, pls see jdmayfields answer. However, for a very quick and easy solution to halve your frame rate, you can simply do your computations only every 2nd frame by:

requestAnimationFrame(render);
function render() {
  // ... computations ...
  requestAnimationFrame(skipFrame);
}
function skipFrame() { requestAnimationFrame(render); }

Similarly you could always call render but use a variable to control whether you do computations this time or not, allowing you to also cut FPS to a third or fourth (in my case, for a schematic webgl-animation 20fps is still enough while considerably lowering computational load on the clients)

Assume answered 15/1, 2021 at 13:44 Comment(0)
H
1

I tried multiple solutions provided on this question. Even though the solutions work as expected, they result in not so professional output.

Based on my personal experience, I would highly recommend not to control FPS on the browser side, especially using requestAnimationFrame. Because, when you do that, it'll make the frame rendering experience very choppy, users will clearly see the frames jumping and finally, it won't look real or professional at all.

So, my advice would be to control the FPS from the server side at the time of sending itself and simply render the frames as soon as you receive them on the browser side.

Note: if you still want to control on the client-side, try avoiding usage of setTimeout or Date object in your logic of controlling fps. Because, when the FPS is high, these will introduce their own delay in terms of event loops or object creations.

Hearts answered 30/11, 2021 at 3:28 Comment(0)
U
-2

Here's a good explanation I found: CreativeJS.com, to wrap a setTimeou) call inside the function passed to requestAnimationFrame. My concern with a "plain" requestionAnimationFrame would be, "what if I only want it to animate three times a second?" Even with requestAnimationFrame (as opposed to setTimeout) is that it still wastes (some) amount of "energy" (meaning that the Browser code is doing something, and possibly slowing the system down) 60 or 120 or however many times a second, as opposed to only two or three times a second (as you might want).

Most of the time I run my browsers with JavaScript intentially off for just this reason. But, I'm using Yosemite 10.10.3, and I think there's some kind of timer problem with it - at least on my old system (relatively old - meaning 2011).

Unmoving answered 2/4, 2015 at 23:24 Comment(1)
setTimeout causes significant stutter and shouldn't be used in production games.Touchline

© 2022 - 2024 — McMap. All rights reserved.