Count to numbers above a billion in a few seconds without browser freezing - javascript

I am trying to make a counter that counts to big numbers such as 6 billion (like google's random number generator) but the browser freezes.
var counter = document.querySelector("#blahblahblah");
var nb = 0;
var ne = 6_000_000_000;
for (;nb<=ne;nb++) {
counter.innerHTML = nb;
};

The best thing you can do is use requestAnimationFrame. This represents the fastest rate that you can update the DOM.
Use the callback to update your element text to the number proportional to the time allowed; 3 seconds (a few) from your question title
const runCounter = (num, timeout, el) => {
let start;
const step = (ts) => {
if (!start) {
start = ts; // record the first iteration timestamp
}
const progress = (ts - start) / timeout; // how far into the time span
if (progress < 1) {
el.textContent = Math.floor(num * progress);
requestAnimationFrame(step); // request the next frame
} else {
el.textContent = num; // finish
}
};
requestAnimationFrame(step);
};
runCounter(6_000_000_000, 3000, document.getElementById("blahblahblah"));
<output id="blahblahblah">0</output>

Related

How to do a task in a time interval in javascript

inside JavaScript, how can I tell something to be done in a certain period of time? For example, for 5 seconds, the value of a variable will increase and it will stop after 5 seconds.
Of course, there are setinterval and settimeout, but they only do that work after that period of time is over. I want that work to be done during that time.
setInterval
You can use setInteveral, but it's mostly trash for everything I've ever attempted to use it for. In the code below, the counter will only increment to around 35 in 1 second; about 28 milliseconds per interval -
// initialize a counter
let counter = 0
// increment the counter on interval
let myprocess = setInterval(_ => counter++, 0)
// after 5 seconds, stop the process and log the counter value
setTimeout(_ => { clearInterval(myprocess); console.log(counter) }, 1000)
A more appropriate utility would be setImmediate. It is supported in Node but not in browsers yet. Polyfills are available if you plan to use this in the browser.
async generators
Above we see setInterval is quite slow. Another approach is to use an asynchronous generator. In the code below, the counter can reach over 300,000 in just 1 second; About 3.3 microseconds per interval.
async function *count(until) {
let counter = 0
while (Date.now() < until) {
yield counter++
}
}
async function main() {
for await (const n of count(Date.now() + 1000))
document.write(`${n}, `)
return "done"
}
main().then(console.log)
If the goal is to run the process as fast as possible for the fixed duration, we will skip expensive operations like document.write. In the example below the counter can reach over 1M in just 1 second; just under 1 microsecond per interval -
async function *count(until) {
let counter = 0
while (Date.now() < until) {
yield counter++
}
}
async function run(process, duration) {
let result
for await (const n of process(Date.now() + duration))
result = n
return result
}
run(count, 1000).then(console.log)
// 1045592
blocking while loop
If the loop itself is the main part of your program and you want simply want to run the counter as fast as possible for a fixed duration, use a while loop. Note this will block all other code from running for the entire duration of the loop -
function count(until) {
let n = 0
while (Date.now() < until)
n++
return n
}
setTimeout(console.log, 0, "I was blocked")
console.log(count(Date.now() + 1000))
// 36618673
// "I was blocked"
You will need to check inside some sort of loop if the time has passed, make sure the loop is async in nature or the JS main thread will have issues.
I have modified PI calculator from here Javascript: PI (π) Calculator to run for 5 seconds instead of infinite
, it's using requestAnimationFrame to keep things async->
function * generateDigitsOfPi() {
let q = 1n;
let r = 180n;
let t = 60n;
let i = 2n;
while (true) {
let digit = ((i * 27n - 12n) * q + r * 5n) / (t * 5n);
yield Number(digit);
let u = i * 3n;
u = (u + 1n) * 3n * (u + 2n);
r = u * 10n * (q * (i * 5n - 2n) + r - t * digit);
q *= 10n * i * (i++ * 2n - 1n);
t *= u;
}
}
// Demo
let iter = generateDigitsOfPi();
const tmStart = Date.now();//
let output = document.querySelector("div");
(function displayNextDigit() {
output.insertAdjacentHTML("beforeend", iter.next().value);
scrollTo(0, document.body.scrollHeight);
if (Date.now() - tmStart < 5000) requestAnimationFrame(displayNextDigit);
})();
div { word-wrap:break-word; font-family: monospace }
<div></div>
const setRunInSeconds = (callback, ms = 1000, delay = 1) => {
const intervalId = setInterval(callback, delay)
setInterval(() => {
clearInterval(intervalId)
}, ms)
}
let num = 0
setRunInSeconds(() => {
console.log('interval: ' + num)
num += 1
}, 5000, 1)

How am I able to add timing to javascript that alters text every second?

I would like this code to count up from 0 to 940 (very fast) and alter the text every time it updates
Here's my code (inside my head tag):
<script type="text/javascript">
function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
function onLoad(){
var x = document.getElementById("numberID");
var n = 940;
var text = "";
for(i = 0;i < n + 1;i++){
text = i;
x.innerHTML = text;
sleep(1);
}
}
</script>
At the moment, it just waits a second then displays '940' on screen and doesn't display it counting up.
Any help would be appreciated, thanks!
Here's the code I recently put in, still doesn't work:
const x = document.getElementById("numberID");
function newFrame(duration, start = performance.now()) {
requestAnimationFrame((now) => {
const elapsed = now - start;
x.innerText = Math.max(0, Math.min(duration,
Math.round(elapsed)));
if(elapsed < duration)
newFrame(duration, start);
})
}
}
newFrame(940);
Using a while loop to "sleep" is going to block the page's thread and nothing else can happen in the meantime. This is considered bad practice.
setTimeout guarantees that at least the defined time has passed, but can take (much) longer. This is imprecise, and especially bad for shorter intervals. Same with setInterval. They're also not recommended for callbacks that involve updating the DOM.
What you need to do is use a requestAnimationFrame.
function newFrame(duration, start = performance.now()) {
requestAnimationFrame((now) => {
const elapsed = now - start
console.log(`time passed: ${elapsed} ms`)
if(elapsed < duration)
newFrame(duration, start)
})
}
newFrame(940)
In your specific case, I'd replace the console.log statement put there for didactic purposes, with something along the lines of:
x.innerText = Math.max(0, Math.min(duration, Math.round(elapsed)))
Here's what that would look like:
const x = document.getElementById("numberID")
function newFrame(duration, start = performance.now()) {
requestAnimationFrame((now) => {
const elapsed = now - start
x.innerText = Math.max(0, Math.min(duration, Math.round(elapsed)))
if(elapsed < duration)
newFrame(duration, start)
})
}
newFrame(940)
<span id="numberID"></span>
The sleep function is not doing anything, what you need is a setTimeout to display the text at every x milliseconds.
Something like the below will work.
let x = null;
let timeout = null;
const changeText = (text) => {
x.innerHTML = text;
clearTimeout(timeout);
}
function onLoad() {
x = document.getElementById("numberID");
const n = 940;
const t = .01; // in seconds
for( let i = 0; i <= n; i++) {
timeout = setTimeout( () => changeText((i+1).toString()), (t * i) * 1000);
}
}
onLoad();
<span id="numberID"></span>

How to fix setInterval issue when the clamped minimum is slower than the one second for inactive tab? [duplicate]

I need to create a simple but accurate timer.
This is my code:
var seconds = 0;
setInterval(function() {
timer.innerHTML = seconds++;
}, 1000);
After exactly 3600 seconds, it prints about 3500 seconds.
Why is it not accurate?
How can I create an accurate timer?
Why is it not accurate?
Because you are using setTimeout() or setInterval(). They cannot be trusted, there are no accuracy guarantees for them. They are allowed to lag arbitrarily, and they do not keep a constant pace but tend to drift (as you have observed).
How can I create an accurate timer?
Use the Date object instead to get the (millisecond-)accurate, current time. Then base your logic on the current time value, instead of counting how often your callback has been executed.
For a simple timer or clock, keep track of the time difference explicitly:
var start = Date.now();
setInterval(function() {
var delta = Date.now() - start; // milliseconds elapsed since start
…
output(Math.floor(delta / 1000)); // in seconds
// alternatively just show wall clock time:
output(new Date().toUTCString());
}, 1000); // update about every second
Now, that has the problem of possibly jumping values. When the interval lags a bit and executes your callback after 990, 1993, 2996, 3999, 5002 milliseconds, you will see the second count 0, 1, 2, 3, 5 (!). So it would be advisable to update more often, like about every 100ms, to avoid such jumps.
However, sometimes you really need a steady interval executing your callbacks without drifting. This requires a bit more advanced strategy (and code), though it pays out well (and registers less timeouts). Those are known as self-adjusting timers. Here the exact delay for each of the repeated timeouts is adapted to the actually elapsed time, compared to the expected intervals:
var interval = 1000; // ms
var expected = Date.now() + interval;
setTimeout(step, interval);
function step() {
var dt = Date.now() - expected; // the drift (positive for overshooting)
if (dt > interval) {
// something really bad happened. Maybe the browser (tab) was inactive?
// possibly special handling to avoid futile "catch up" run
}
… // do what is to be done
expected += interval;
setTimeout(step, Math.max(0, interval - dt)); // take into account drift
}
I'ma just build on Bergi's answer (specifically the second part) a little bit because I really liked the way it was done, but I want the option to stop the timer once it starts (like clearInterval() almost). Sooo... I've wrapped it up into a constructor function so we can do 'objecty' things with it.
1. Constructor
Alright, so you copy/paste that...
/**
* Self-adjusting interval to account for drifting
*
* #param {function} workFunc Callback containing the work to be done
* for each interval
* #param {int} interval Interval speed (in milliseconds)
* #param {function} errorFunc (Optional) Callback to run if the drift
* exceeds interval
*/
function AdjustingInterval(workFunc, interval, errorFunc) {
var that = this;
var expected, timeout;
this.interval = interval;
this.start = function() {
expected = Date.now() + this.interval;
timeout = setTimeout(step, this.interval);
}
this.stop = function() {
clearTimeout(timeout);
}
function step() {
var drift = Date.now() - expected;
if (drift > that.interval) {
// You could have some default stuff here too...
if (errorFunc) errorFunc();
}
workFunc();
expected += that.interval;
timeout = setTimeout(step, Math.max(0, that.interval-drift));
}
}
2. Instantiate
Tell it what to do and all that...
// For testing purposes, we'll just increment
// this and send it out to the console.
var justSomeNumber = 0;
// Define the work to be done
var doWork = function() {
console.log(++justSomeNumber);
};
// Define what to do if something goes wrong
var doError = function() {
console.warn('The drift exceeded the interval.');
};
// (The third argument is optional)
var ticker = new AdjustingInterval(doWork, 1000, doError);
3. Then do... stuff
// You can start or stop your timer at will
ticker.start();
ticker.stop();
// You can also change the interval while it's in progress
ticker.interval = 99;
I mean, it works for me anyway. If there's a better way, lemme know.
Bergi's answer pinpoints exactly why the timer from the question is not accurate. Here's my take on a simple JS timer with start, stop, reset and getTime methods:
class Timer {
constructor () {
this.isRunning = false;
this.startTime = 0;
this.overallTime = 0;
}
_getTimeElapsedSinceLastStart () {
if (!this.startTime) {
return 0;
}
return Date.now() - this.startTime;
}
start () {
if (this.isRunning) {
return console.error('Timer is already running');
}
this.isRunning = true;
this.startTime = Date.now();
}
stop () {
if (!this.isRunning) {
return console.error('Timer is already stopped');
}
this.isRunning = false;
this.overallTime = this.overallTime + this._getTimeElapsedSinceLastStart();
}
reset () {
this.overallTime = 0;
if (this.isRunning) {
this.startTime = Date.now();
return;
}
this.startTime = 0;
}
getTime () {
if (!this.startTime) {
return 0;
}
if (this.isRunning) {
return this.overallTime + this._getTimeElapsedSinceLastStart();
}
return this.overallTime;
}
}
const timer = new Timer();
timer.start();
setInterval(() => {
const timeInSeconds = Math.round(timer.getTime() / 1000);
document.getElementById('time').innerText = timeInSeconds;
}, 100)
<p>Elapsed time: <span id="time">0</span>s</p>
The snippet also includes a solution for your problem. So instead of incrementing seconds variable every 1000ms interval, we just start the timer and then every 100ms* we just read elapsed time from the timer and update the view accordingly.
* - makes it more accurate than 1000ms
To make your timer more accurate, you would have to round
Most of the timers in the answers here will linger behind the expected time because they set the "expected" value to the ideal and only account for the delay that the browser introduced before that point. This is fine if you just need accurate intervals, but if you are timing relative to other events then you will (nearly) always have this delay.
To correct it, you can keep track of the drift history and use it to predict future drift. By adding a secondary adjustment with this preemptive correction, the variance in the drift centers around the target time. For example, if you're always getting a drift of 20 to 40ms, this adjustment would shift it to -10 to +10ms around the target time.
Building on Bergi's answer, I've used a rolling median for my prediction algorithm. Taking just 10 samples with this method makes a reasonable difference.
var interval = 200; // ms
var expected = Date.now() + interval;
var drift_history = [];
var drift_history_samples = 10;
var drift_correction = 0;
function calc_drift(arr){
// Calculate drift correction.
/*
In this example I've used a simple median.
You can use other methods, but it's important not to use an average.
If the user switches tabs and back, an average would put far too much
weight on the outlier.
*/
var values = arr.concat(); // copy array so it isn't mutated
values.sort(function(a,b){
return a-b;
});
if(values.length ===0) return 0;
var half = Math.floor(values.length / 2);
if (values.length % 2) return values[half];
var median = (values[half - 1] + values[half]) / 2.0;
return median;
}
setTimeout(step, interval);
function step() {
var dt = Date.now() - expected; // the drift (positive for overshooting)
if (dt > interval) {
// something really bad happened. Maybe the browser (tab) was inactive?
// possibly special handling to avoid futile "catch up" run
}
// do what is to be done
// don't update the history for exceptionally large values
if (dt <= interval) {
// sample drift amount to history after removing current correction
// (add to remove because the correction is applied by subtraction)
drift_history.push(dt + drift_correction);
// predict new drift correction
drift_correction = calc_drift(drift_history);
// cap and refresh samples
if (drift_history.length >= drift_history_samples) {
drift_history.shift();
}
}
expected += interval;
// take into account drift with prediction
setTimeout(step, Math.max(0, interval - dt - drift_correction));
}
I agree with Bergi on using Date, but his solution was a bit of overkill for my use. I simply wanted my animated clock (digital and analog SVGs) to update on the second and not overrun or under run creating obvious jumps in the clock updates. Here is the snippet of code I put in my clock update functions:
var milliseconds = now.getMilliseconds();
var newTimeout = 1000 - milliseconds;
this.timeoutVariable = setTimeout((function(thisObj) { return function() { thisObj.update(); } })(this), newTimeout);
It simply calculates the delta time to the next even second, and sets the timeout to that delta. This syncs all of my clock objects to the second. Hope this is helpful.
Here's a solution that pauses when the window is hidden, and can be cancelled with an abort controller.
function animationInterval(ms, signal, callback) {
const start = document.timeline.currentTime;
function frame(time) {
if (signal.aborted) return;
callback(time);
scheduleFrame(time);
}
function scheduleFrame(time) {
const elapsed = time - start;
const roundedElapsed = Math.round(elapsed / ms) * ms;
const targetNext = start + roundedElapsed + ms;
const delay = targetNext - performance.now();
setTimeout(() => requestAnimationFrame(frame), delay);
}
scheduleFrame(start);
}
Usage:
const controller = new AbortController();
// Create an animation callback every second:
animationInterval(1000, controller.signal, time => {
console.log('tick!', time);
});
// And stop it sometime later:
controller.abort();
Modern, Fully Programmable Timer
This timer takes a frequency in Hertz, and a callback that can take up to four arguments, the current frame index, the current time, the time that the current frame would have ideally occurred at, and a reference to the timer instance (so the caller and callback can both access its methods).
Note: All times are based on performance.now, and are relative to the moment that the page loaded.
Timer instances have three API methods:
stop: Takes no args. Kills the timer immediately (and permanently).
Returns the frame index for the next frame (the cancelled frame).
adapt: Takes a frequency in Hertz and adapts the timer to it, beginning
from the next frame. Returns the implied interval in milliseconds.
redefine: Takes a new callback function. Swaps it with the current
callback. Effects the next frame. Returns undefined.
Note: The tick method passes this around explicitly (as self) to work around the problem of this referencing window when the tick method is invoked via setTimeout.
class ProgrammableTimer {
constructor(hertz, callback) {
this.target = performance.now(); // target time for the next frame
this.interval = 1 / hertz * 1000; // the milliseconds between ticks
this.callback = callback;
this.stopped = false;
this.frame = 0;
this.tick(this);
}
tick(self) {
if (self.stopped) return;
const currentTime = performance.now();
const currentTarget = self.target;
const currentInterval = (self.target += self.interval) - currentTime;
setTimeout(self.tick, currentInterval, self);
self.callback(self.frame++, currentTime, currentTarget, self);
}
stop() { this.stopped = true; return this.frame }
adapt(hertz) { return this.interval = 1 / hertz * 1000 }
redefine(replacement) { this.callback = replacement }
}
Doesn't get much more accurate than this.
var seconds = new Date().getTime(), last = seconds,
intrvl = setInterval(function() {
var now = new Date().getTime();
if(now - last > 5){
if(confirm("Delay registered, terminate?")){
clearInterval(intrvl);
return;
}
}
last = now;
timer.innerHTML = now - seconds;
}, 333);
As to why it is not accurate, I would guess that the machine is busy doing other things, slowing down a little on each iteration adds up, as you see.
This is an old question but figured I'd share some code I use sometimes:
function Timer(func, delay, repeat, runAtStart)
{
this.func = func;
this.delay = delay;
this.repeat = repeat || 0;
this.runAtStart = runAtStart;
this.count = 0;
this.startTime = performance.now();
if (this.runAtStart)
this.tick();
else
{
var _this = this;
this.timeout = window.setTimeout( function(){ _this.tick(); }, this.delay);
}
}
Timer.prototype.tick = function()
{
this.func();
this.count++;
if (this.repeat === -1 || (this.repeat > 0 && this.count < this.repeat) )
{
var adjustedDelay = Math.max( 1, this.startTime + ( (this.count+(this.runAtStart ? 2 : 1)) * this.delay ) - performance.now() );
var _this = this;
this.timeout = window.setTimeout( function(){ _this.tick(); }, adjustedDelay);
}
}
Timer.prototype.stop = function()
{
window.clearTimeout(this.timeout);
}
Example:
time = 0;
this.gameTimer = new Timer( function() { time++; }, 1000, -1);
Self-corrects the setTimeout, can run it X number of times (-1 for infinite), can start running instantaneously, and has a counter if you ever need to see how many times the func() has been run. Comes in handy.
Edit: Note, this doesn't do any input checking (like if delay and repeat are the correct type. And you'd probably want to add some kind of get/set function if you wanted to get the count or change the repeat value.
One of my simplest implementations is down below. It can even survive page reloads. :-
Code pen: https://codepen.io/shivabhusal/pen/abvmgaV
$(function() {
var TTimer = {
startedTime: new Date(),
restoredFromSession: false,
started: false,
minutes: 0,
seconds: 0,
tick: function tick() {
// Since setInterval is not reliable in inactive windows/tabs we are using date diff.
var diffInSeconds = Math.floor((new Date() - this.startedTime) / 1000);
this.minutes = Math.floor(diffInSeconds / 60);
this.seconds = diffInSeconds - this.minutes * 60;
this.render();
this.updateSession();
},
utilities: {
pad: function pad(number) {
return number < 10 ? '0' + number : number;
}
},
container: function container() {
return $(document);
},
render: function render() {
this.container().find('#timer-minutes').text(this.utilities.pad(this.minutes));
this.container().find('#timer-seconds').text(this.utilities.pad(this.seconds));
},
updateSession: function updateSession() {
sessionStorage.setItem('timerStartedTime', this.startedTime);
},
clearSession: function clearSession() {
sessionStorage.removeItem('timerStartedTime');
},
restoreFromSession: function restoreFromSession() {
// Using sessionsStorage to make the timer persistent
if (typeof Storage == "undefined") {
console.log('No sessionStorage Support');
return;
}
if (sessionStorage.getItem('timerStartedTime') !== null) {
this.restoredFromSession = true;
this.startedTime = new Date(sessionStorage.getItem('timerStartedTime'));
}
},
start: function start() {
this.restoreFromSession();
this.stop();
this.started = true;
this.tick();
this.timerId = setInterval(this.tick.bind(this), 1000);
},
stop: function stop() {
this.started = false;
clearInterval(this.timerId);
this.render();
}
};
TTimer.start();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<h1>
<span id="timer-minutes">00</span> :
<span id="timer-seconds">00</span>
</h1>
Inspired by Bergi's answer I created the following complete non drifting timer. What I wanted was a way to set a timer, stop it, and do this simply.
var perfectTimer = { // Set of functions designed to create nearly perfect timers that do not drift
timers: {}, // An object of timers by ID
nextID: 0, // Next available timer reference ID
set: (callback, interval) => { // Set a timer
var expected = Date.now() + interval; // Expected currect time when timeout fires
var ID = perfectTimer.nextID++; // Create reference to timer
function step() { // Adjusts the timeout to account for any drift since last timeout
callback(); // Call the callback
var dt = Date.now() - expected; // The drift (ms) (positive for overshooting) comparing the expected time to the current time
expected += interval; // Set the next expected currect time when timeout fires
perfectTimer.timers[ID] = setTimeout(step, Math.max(0, interval - dt)); // Take into account drift
}
perfectTimer.timers[ID] = setTimeout(step, interval); // Return reference to timer
return ID;
},
clear: (ID) => { // Clear & delete a timer by ID reference
if (perfectTimer.timers[ID] != undefined) { // Preventing errors when trying to clear a timer that no longer exists
console.log('clear timer:', ID);
console.log('timers before:', perfectTimer.timers);
clearTimeout(perfectTimer.timers[ID]); // Clear timer
delete perfectTimer.timers[ID]; // Delete timer reference
console.log('timers after:', perfectTimer.timers);
}
}
}
// Below are some tests
var timerOne = perfectTimer.set(() => {
console.log(new Date().toString(), Date.now(), 'timerOne', timerOne);
}, 1000);
console.log(timerOne);
setTimeout(() => {
perfectTimer.clear(timerOne);
}, 5000)
var timerTwo = perfectTimer.set(() => {
console.log(new Date().toString(), Date.now(), 'timerTwo', timerTwo);
}, 1000);
console.log(timerTwo);
setTimeout(() => {
perfectTimer.clear(timerTwo);
}, 8000)
driftless is a drop-in replacement for setInterval that mitigates drift. Makes life easy, import the npm package, then use it like setInterval / setTimeout:
setDriftlessInterval(() => {
this.counter--;
}, 1000);
setDriftlessInterval(() => {
this.refreshBounds();
}, 20000);
you can use a function called setTimeout that we can use to set the countdown.
Firstly, create a javascript snippet and add it to your page as follows;
var remainingTime = 30;
var elem = document.getElementById('countdown_div');
var timer = setInterval(countdown, 1000); //set the countdown to every second
function countdown() {
if (remainingTime == -1) {
clearTimeout(timer);
doSomething();
} else {
elem.innerHTML = remainingTime + ' left';
remainingTime--; //we subtract the second each iteration
}
}
Source + more details -> https://www.growthsnippets.com/30-second-countdown-timer-javascript/
Many of these answers here are great, but they typically their code examples are pages and pages of code (the good ones even have instructions on the best way to copy/paste it all). I just wanted to understand this problem with a very simple example.
Working Demo
var lastpause = 0;
var totaltime = 0;
function goFunction(e) {
if(this.innerText == 'Off - Timer Not Going') {
this.innerText = 'On - Timer Going';
} else {
totaltime += Date.now() - lastpause;
this.innerText = 'Off - Timer Not Going';
}
lastpause = Date.now();
document.getElementById('count').innerText = totaltime + ' milliseconds.';
}
document.getElementById('button').addEventListener('click', goFunction);
<button id="button">Off - Timer Not Going</button> <br>
Seconds: <span id="count">0 milliseconds.</span>
Explanation of Demo
totaltime — This is the total time calculated.
lastpause — This is the only real temporary variable we have. Whenever someone hits pause, we set lastpause to Date.now(). When someone unpauses, and re-pauses again, we calculate the time diff of Date.now() subtracted from the last pause.
We only need those two variables: Our total and the last time we stopped the timer. The other answers seem to use this approach, but I wanted a compact explanation.
I gave up building my own and ended up using this neat library.

Increment from zero to number in a set time

I am trying to increment from 0 to a number (can be any number from 2000 to 12345600000) within a certain duration (1000 ms, 5000 ms, etc). I have created the following:
http://jsfiddle.net/fmpeyton/c9u2sky8/
var counterElement = $(".lg-number");
var counterTotal = parseInt(counterElement.text()/*.replace(/,/g, "")*/);
var duration = 1000;
var animationInterval = duration/counterTotal;
counterElement.text("0");
var numberIncrementer = setInterval(function(){
var currentCounterNumber = parseInt(counterElement.text()/*.replace(/,/g, "")*/);
if (currentCounterNumber < counterTotal){
currentCounterNumber += Math.ceil(counterTotal/duration);
// convert number back to comma format
// currentCounterNumber = addCommas(currentCounterNumber);
counterElement.text(currentCounterNumber);
} else {
counterElement.text(counterTotal);
clearInterval(numberIncrementer);
}
console.log("run incrementer");
}, animationInterval);
function addCommas(number){
for (var i = number.length - 3; i > 0; i -= 3)
number = number.slice(0, i) + ',' + number.slice(i);
return number;
}
And this somewhat works, but it does not respect the duration. I.e. if you increase the number from 1000 to 1000000000, they both take different amounts of time to reach the destination number.
How can I increment from zero to a number in a specific time frame?
As #Mouser pointed out, the issue is that the animationInterval can't be too small (the actual minimum threshold will vary based on the browser and platform). Instead of varying the interval, vary the increment to the counter:
var counterElement = $(".lg-number");
var counterTotal = parseInt(counterElement.text()/*.replace(/,/g, "")*/);
var duration = 1000;
var animationInterval = 10;
var startTime = Date.now();
counterElement.text("0");
var numberIncrementer = setInterval(function(){
var elapsed = Date.now() - startTime;
var currentCounterNumber = Math.ceil(elapsed / duration * counterTotal);
if (currentCounterNumber < counterTotal){
counterElement.text(currentCounterNumber);
} else {
counterElement.text(counterTotal);
clearInterval(numberIncrementer);
}
console.log("run incrementer");
}, animationInterval);
I played around with your fiddle and found that the delay needs to be higher. At 8ms or 16ms, it is accurate enough to handle a second, but not accurate enough to handle half a second. From experimenting, it seems like a delay of 64ms is small enough to seem like it's incrementing smoothly, but big enough to have an accurate effect.
The difference is that the current number is calculated based on the process rather than directly manipulated.
var counterElement = $(".lg-number");
var counterTotal = parseInt(counterElement.data('total'));
var interval = 0;
var duration = parseInt(counterElement.data('duration'));;
var delay = 64
var numberIncrementer = setInterval(function(){
var currentCounterNumber = 0;
interval += delay;
if (interval <= duration){
var progress = interval / duration;
currentCounterNumber = Math.round(progress * counterTotal);
} else {
currentCounterNumber = counterTotal
clearInterval(numberIncrementer);
}
counterElement.text(currentCounterNumber);
}, delay);
http://jsfiddle.net/c9u2sky8/5/
Also: Javascript timers are not perfectly accurate. But this should be accurate enough for UI use cases.

Controlling fps with requestAnimationFrame?

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.
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
}
}
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.
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.
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).
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
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();
var time = 0;
var time_framerate = 1000; //in milliseconds
function animate(timestamp) {
if(timestamp > time + time_framerate) {
time = timestamp;
//your code
}
window.requestAnimationFrame(animate);
}
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.
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.
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)
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);
}
Here is an idea to reach desired fps:
detect browser's animationFrameRate (typically 60fps)
build a bitSet, according to animationFrameRate and your disiredFrameRate (say 24fps)
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>
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.
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.
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).

Categories

Resources