Js has 3 microtask queues, these are(were) setTimeout/Interval/Immediate
(some people call these macrotask, etc whatever), requestAnimationFrame
(rAF) and the new child Promises
. Promises resolve asap, setTimeouts have 4ms min difference between successive invocations if they are nested (& more than 5 layers deep), rAF will execute around 60 frames per second.
Amongst these rAF is aware of document.hidden state and roughly executes every ~17ms (16.67 theoretically). If your desired intervals are larger than this value, settle with rAF.
The problem with rAF is, since it executes every ~17ms, if I would want to execute something with 100 ms intervals,then after 5 ticks I would be at ~85ms, at the sixth tick I'd be at 102ms. I can execute at 102ms, but then I need to drop down 2ms from the next invocation time. This will prevent accidental 'phasing out' of the callback with respect to the frames you specify. You can roughly design a function that accepts an options object:
function wait(t,options){
if(!options){
options = t;
window.requestAnimationFrame(function(t){
wait(t,options);
});
return options;
}
if(!options._set) {
options.startTime = options.startTime || t;
options.relativeStartTime = options.startTime;
options.interval = options.interval || 50;
options.frame = options.frame || 0;
options.callback = options.callback || function(){};
options.surplus = options.surplus || 0;
options._set = true;
}
options.cancelFrame = window.requestAnimationFrame(function(t){
wait(t,options);
});
options.elapsed = t - options.relativeStartTime + options.surplus;
if (options.elapsed >= options.interval) {
options.surplus = options.elapsed % options.interval;
options.lastInvoked = t;
options.callback.call(options);
options.frame++;
options.relativeStartTime = t;
}
return options;
}
The object gets recycled and updated at every invocation. Copy paste to your console the above and try:
var x = wait({interval:190,callback:function(){console.log(this.lastInvoked - this.relativeStartTime)}})
The callback executes with this
pointing to the options object. The returned x is the options object itself. To cancel it from running:
window.cancelAnimationFrame(x.cancelFrame);
This doesn't always have to act like interval, you can also use it like setTimeout. Let's say you have variable frames with multiples of 32 as you said,in that case extend the options object:
var x = wait({frameList:[32,32,64,128,256,512,1024,2048,32,128],interval:96,callback:function(){
console.log(this.lastInvoked - this.relativeStartTime);
window.cancelAnimationFrame(this.cancelFrame);
this.interval = this.frameList[this.frame];
if(this.interval){
wait(this);
}
}})
I added a frameList key to the options object. Here are some time values that we want to execute the callback. We start with 96, then go inside the frameList array, 32,32, 64 etc. If you run the above you'll get:
99.9660000000149
33.32199999992736
33.32199999992736
66.64400000008754
133.28799999994226
249.91499999980442
517.7960000000894
1016.5649999999441
2049.7950000001583
33.330000000074506
133.31999999983236
So these are my thoughts about what I'd do in your situation.
it runs as close as possible to the specified interval. If you put very close intervals such as 28,30,32 you will not be able to inspect the difference by eye. perhaps try console logging the 'surplus' values like this:
var x = wait({interval:28,callback:function(){
console.log(this.surplus);
}})
You will see slightly different numbers for different intervals, and these numbers will shift in time, because we are preventing 'phasing out'. The ultimate test would be to look at the average time takes for certain amount of 'frames':
var x = wait({interval:28,callback:function(){
if(this.frame === 99){
console.log(this.lastInvoked - this.startTime);
window.cancelAnimationFrame(this.cancelFrame);
}
}}) //logs something around 2800
if you change the interval to 32 for instance, it will log something around 3200ms etc. In conclusion, the function we design should not depend on what the real time is, it should get from the js engine which frame we are currently at, and should base its pace on that.