TimerLib

TimerLib offers a simple high resolution animation timer object and drop-in replacements for the WindowOrWorkerGlobalScope.setTimeout() and WindowOrWorkerGlobalScope.setInterval() timers. If you look at the documentation for those functions you will begin to realise quite how many problems and limitations they have. In addition, CSS Animation has limitations and quirks and the Web Animation API is hideously complicated for simple (or what should be simple) animations and is, as yet, poorly supported across browsers.

While I was developing BoxNav I needed an easier, more flexible way of animating and I developed this object library to address that need. I found it so easy to understand and use that I have made it available as an open source module, and here it is.

This library uses the window.performance API and window.getAnimationFrame() to provide highly accurate timers that are performative and kind to smartphone and laptop batteries. It makes web animation simple.

It will be helpful to understand what window.getAnimationFrame() does, and really it is very simple. getAnimationFrame() calls a callback function whenever the browser is about to do a screen update, passing it a DOMHighResTimeStamp. That is a little like saying to your wife (or your mum, if you are a proper geek), "Give me a nudge next time you're heading down the shops so I can..." So every time the browser is about to paint it calls your animation function, allowing you to update positions or fades or colours etc and the TimerLib timer object keeps track of how far through the animation we are by converting the timestamp returned by getAnimationFrame() into an elapsed time as a percentage of the animation duration; expressed as a floating point number between 0.0 and 1.0

What does that mean? Well it means you can multiply any (numeric) property value by the timer's progress value to scale it, i.e. change it by the amount required for each point in the animation timeline. This is called tweening, calculating intermediate positions between a start and end value.

The timer object can also convert that progress value to one adjusted by an easing function. This allows you to apply easeIn, easeOut etc to any animated property.

TimerLib is implemented as both an object-oriented library or module. To use it as a library add <script src="timerlib.js"></script> in your html. To import the objects and functions as modules use eg. import {Timer} from timerlib.module.js in your javascript. Modules are great but, sadly, you can run into CORS difficulties when running your app from local files.

It offers the following objects:

Timer

The Timer object provides a progress percentage as a number between 0 and 1 that indicates the progress through your animation. Since it is a number between 0 and 1 it can be used as a multiplier to scale a distance, rotation, fade etc.

Call with const myTimer = new Timer(id[, duration]);

Generally you would use it in conjunction with requestAnimationFrame(), which calls a callback, passing it a high resolution timestamp. For example:

const myTimer = new Timer(requestAnimationFrame('animate'), 500);

That statement creates a timer object using the timestamp returned from requestAnimationFrame() as its id. requestAnimationFrame() calls the animate callback almost immediately (usually 60 times every second, or whatever the refresh rate is for your monitor) and the new timer object stores the animation duration. At this point the timer has not actually started timing.

Inside your animation callback you call myTimer.progress(), which returns the percentage of the animation duration that has elapsed. The first time myTimer.progress() is called it starts the timer, using the performance API to get a timestamp, in milliseconds accurate to the nearest 5 microseconds, and stores that as the start time. It then returns 0 (the beginning of the animation) to your calling animation function.

The next time your animation calls myTimer.progress() it returns the elapsed time as a percentage of total animation duration.

Perhaps it is easiest to get to grips with how the timer is used by showing some example code:

import {Timer} from './timerlib.module.js';	// import timer from the module
const myAnimation = () => {
	const tick = () => {
		// percentage of time elapsed as float between 0.0 and 1.0
		// the first time this is called the timer starts timing
		const progress = myTimer.progress();
		// use various easing functions for different transforms
		const ease1 = myTimer.ease('easeOut');
		const ease2 = myTimer.ease('parametric');
		const ease3 = myTimer.ease('parabolic');
		// multiply distances by progress value or eased value
		const rot = ease1 * 90;		// rotation from 0 to 90 degrees
		const newX = ease2 * 300;	// value for x transform, from 0 to 300px
		const newZ = ease3 * 400;	// value for z transform, from 0 to 400px
		const size = 1 - ease1;		// value for scale transform, from 1 to 0
		const opacity = 1 - progress;	// opacity value, linear (no ease) from 1 to 0
		// animate the element by applying the transforms and fade
		myThing.style.transform = 
			`translate3d(${newX}px, 0, ${-newZ}px)
				rotateX(${rot}deg) scale(${size})`;
		myThing.style.opacity = `${opacity}`;
		// recursively call the tick() callback
		// until our timer tells us the duration has expired
		if (!myTimer.done) requestAnimationFrame(tick);
		else {
			// reset or do anything necessary when animation ends
			resetAnim();
			// we can also emulate CSS Animation and the Web Animation API
			// by emitting an animationend event
			document.despatchEvent(new Event('animationend')):
		}
	}
	// reset the animation element once the animation ends
	const resetAnim = () => {
		myThing.style.top = '1rem';
		myThing.style.left = '.5rem';
		myThing.style.transform = '';
		myThing.style.opacity = 1;
	}
	// prepare anything necessary before starting the animation
	const myThing = document.querySelector('#myThing');
	// kick off the animation
	const myTimer = new Timer(requestAnimationFrame(tick), 5000);
}
		

The above code animates an html element so that it rotates around the x-axis, slides to the right (along the x-axis), disappears into the distance (z-axis), shrinking and fading as it goes, all over a period of five seconds (5000ms). Click the 'run' button to see what it does.

Schedule

The Schedule object is a drop-in replacement for WindowOrWorkerGlobalScope.setTimeout(), again employing the Performance API and window.getAnimationFrame() for performance, compatibility and battery saving.

schedule is also far more capable than setTimeout() since it is possible to pause and resume or reset the timeout as well as to cancel it before it fires. setTimeout() can only be canceled.

You use schedule as follows:

const mySchedule = new Schedule(delay, callback[, ...args]);

delay is given in milliseconds and specifies how long to wait before calling the callback.

callback is the name of the callback function to call.

...args is zero or one or more arguments to pass to the callback function.

This is very similar to the setTimeout() signature except that setTimeout() requires the callback as the first parameter and delay as the second. Personally I think it is more logical to give the delay first so as not to separate the callback from its arguments.

You pause, reset, cancel or resume the timeout by calling schedule's methods:

mySchedule.pause();

mySchedule.play();

mySchedule.reset();

mySchedule.cancel();

pause() pauses the timeout at its current elapsed time until play(), reset() or cancel() are called.

play() continues the countdown from wherever the delay was paused, or from 0 if reset() was called while the schedule was paused.

reset() resets the delay back to its original value and you are free to call reset() while the delay is running or paused.

cancel() does what it says on the tin: your callback never gets called.

You are even free to swap callbacks or modify parameters mid-flight by assigning new values to the schedule's callback and args properties. eg:

mySchedule.callback = 'aDifferentFunction';

or

mySchedule.args[2] = 200;

You can also freely change the delay time while the countdown is running by assigning to the schedule's delay property, eg:

mySchedule.delay = 500;

If you just want a one-shot timer callback and don't need to pause, reset or cancel the callback then you can instead use the library's delayCall() function, which has the same calling signature but is far more succinct in its implimentation, saving resources by loading less module code. Use it like this:

delayCall(delay, callback[, ...args]);

Schedules

The Schedules object is a drop-in replacement for WindowOrWorkerGlobalScope.setInterval(). Like schedule it employs the Performance API and window.getAnimationFrame() for performance, compatibility and battery saving.

Schedules is actually a subclass of the schedule class and differs in that it calls its callback repeatedly at a specified interval rather than just once after a set delay.

It also refers to the delay parameter as interval rather than delay. Other than that it has a similar call signature and functionality to schedule.

You call it as follows:

const mySchedules = new Schedules(interval, callback[, ...args]);

The methods and properties are the same as for the schedule object above except for the naming of the delay property.

You can change the shedules interval in a similar way, eg:

mySchedules.interval = 500;

Code

So here's the library code. You can copy and paste it into a file or download it as a zip file:

//	Timer library using  the performance API and requestAnimationFrame()
// by Prajna Pranab
// version 0.1.1
//
// durations, delays and intervals are given in milliseconds to +/- 5 microsecond
// accuracy (though browsers that don't support HiDef timestamps may round to the
// nearest millisecond) so you can use, eg, interval = new schedule(0.5, myCallback)
// to call myCallback every 500 microseconds
//
// for more info see the
// documentation

// general purpose timer object ------------------------------------------------
// default duration = 200ms
// either set the duration when instanting or use, eg, myTimer.duration = 2000;
// call with const myTimer = new timer(requestAnimationFrame(callback));
// timer doesn't start until myTimer.progress() or myTimer.ease() is called
class Timer {
	constructor(id, duration=200) {
		this.id = id;
		this.duration = duration;
		this.done = false;
	}
	// update the timer entry
	track() {
		const [entry] = performance.getEntriesByName(this.id);
		if (!entry) {
			// create a new entry if there isn't one yet
			performance.mark(this.id);
			return 0;
		}
		// return the elapsed time
		return performance.now() - entry.startTime;
	}
	// get elapsed time from the tracker, convert to 0.0 < progress < 1.0
	progress() {
		const percent = Math.min(this.track() / this.duration, 1);
		// when it reaches 1 we're done
		if (percent == 1) {
			// remove the entry
			performance.clearMarks(this.id);
			this.done = true;
		}
		return percent;
	};
	// easing functions convert progress to eased progress
	ease(fn) {
		const p = this.progress();
		switch(fn) {
			case 'easeIn': return p * p;
			case 'easeOut': return p * (2 - p);
			case 'bezier': return p * p * (3 - 2 * p);
			case 'parametric': return (p * p) / (2 * (p * p - p) + 1);
			case 'parabola': return -p + (.5 * p * p);
		}
	}
}

// schedue a callback function to be called after a delay ----------------------
// replaces setTimeout()
// call with mySchedule = new schedule(delay, callback[, ...args]);
// then you can mySchedule.pause() or mySchedule.cancel()
// use delayCall() instead if you don't need to cancel etc
// timer starts when assigned
class Schedule {
	constructor(delay, callback, ...args) {
		this._delay = delay;
		this.originalDelay = delay;
		this.callback = callback;
		this.args = args;
		this.canceled = false;
		this.paused = false;
		this.start = performance.now();
		requestAnimationFrame(this.loop.bind(this));
	}
	get delay() { return this._delay; }
	set delay(period) {
		this._delay = period;
		this.originalDelay = period;
	}
	loop() {
		if (this.canceled || this.paused) return;
		if (performance.now() - this.start < this._delay)
			requestAnimationFrame(this.loop.bind(this));
		else this.callback.call(this, ...this.args);
	}
	cancel() {
		this.canceled = true;
	}
	pause() {
		this._delay = performance.now() - this.start;
		this.paused = true;
	}
	play() {
		this.start = performance.now();
		this.paused = false;
		requestAnimationFrame(this.loop.bind(this));
	}
	reset() {
		this._delay = this.originalDelay;
		this.canceled = false;
		this.start = performance.now();
	}
}

// schedule a callback function to repeat --------------------------------------
// replaces setInterval()
// call with mySchedule = new schedules(interval, callback[, ...args]);
// timer starts when assigned
// call mySchedule.cancel() to stop
class Schedules  extends schedule {
	constructor(period, callback, ...args) {
		super(period, callback, ...args);
	}
	get interval() { return super.delay; }
	set interval(period) { super.delay = period; }
	loop() {
		if (this.canceled || this.paused) return;
		if (performance.now() - this.start < this._delay)
			requestAnimationFrame(this.loop.bind(this));
		else {
			this.callback.call(this, ...this.args);
			this.reset();
			requestAnimationFrame(this.loop.bind(this));
		}
	}
}

// minimal replacement for setTimeout() ----------------------------------------
// more accurate and kinder to batteries, automatically paused when minimised
// call with delayCall(delay, callback[, ...args])
const delayCall = (delay, callback, ...args) => {
	const loop = () => {
		if (performance.now() - start < delay)
			requestAnimationFrame(loop);
		else callback.call(this, ...args);
	};
	const start = performance.now();
	requestAnimationFrame(loop);
}