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;
}
};
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.
var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0;_x000D_
ctx.font = "20px sans-serif";_x000D_
_x000D_
// update canvas with some information and animation_x000D_
var fps = new FpsCtrl(12, function(e) {_x000D_
ctx.clearRect(0, 0, c.width, c.height);_x000D_
ctx.fillText("FPS: " + fps.frameRate() + _x000D_
" Frame: " + e.frame + _x000D_
" Time: " + (e.time - pTime).toFixed(1), 4, 30);_x000D_
pTime = e.time;_x000D_
var x = (pTime - mTime) * 0.1;_x000D_
if (x > c.width) mTime = pTime;_x000D_
ctx.fillRect(x, 50, 10, 10)_x000D_
})_x000D_
_x000D_
// start the loop_x000D_
fps.start();_x000D_
_x000D_
// UI_x000D_
bState.onclick = function() {_x000D_
fps.isPlaying ? fps.pause() : fps.start();_x000D_
};_x000D_
_x000D_
sFPS.onchange = function() {_x000D_
fps.frameRate(+this.value)_x000D_
};_x000D_
_x000D_
function FpsCtrl(fps, callback) {_x000D_
_x000D_
var delay = 1000 / fps,_x000D_
time = null,_x000D_
frame = -1,_x000D_
tref;_x000D_
_x000D_
function loop(timestamp) {_x000D_
if (time === null) time = timestamp;_x000D_
var seg = Math.floor((timestamp - time) / delay);_x000D_
if (seg > frame) {_x000D_
frame = seg;_x000D_
callback({_x000D_
time: timestamp,_x000D_
frame: frame_x000D_
})_x000D_
}_x000D_
tref = requestAnimationFrame(loop)_x000D_
}_x000D_
_x000D_
this.isPlaying = false;_x000D_
_x000D_
this.frameRate = function(newfps) {_x000D_
if (!arguments.length) return fps;_x000D_
fps = newfps;_x000D_
delay = 1000 / fps;_x000D_
frame = -1;_x000D_
time = null;_x000D_
};_x000D_
_x000D_
this.start = function() {_x000D_
if (!this.isPlaying) {_x000D_
this.isPlaying = true;_x000D_
tref = requestAnimationFrame(loop);_x000D_
}_x000D_
};_x000D_
_x000D_
this.pause = function() {_x000D_
if (this.isPlaying) {_x000D_
cancelAnimationFrame(tref);_x000D_
this.isPlaying = false;_x000D_
time = null;_x000D_
frame = -1;_x000D_
}_x000D_
};_x000D_
}
_x000D_
body {font:16px sans-serif}
_x000D_
<label>Framerate: <select id=sFPS>_x000D_
<option>12</option>_x000D_
<option>15</option>_x000D_
<option>24</option>_x000D_
<option>25</option>_x000D_
<option>29.97</option>_x000D_
<option>30</option>_x000D_
<option>60</option>_x000D_
</select></label><br>_x000D_
<canvas id=c height=60></canvas><br>_x000D_
<button id=bState>Start/Stop</button>
_x000D_
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.