All timers in JavaScript are based on the native plain old school JavaScript function setInterval()
or setTimeout()
. Even jQuery uses this internally.
The trick to synchronize timers, is making sure there is only one setInterval()
is invoked, so build something ourselves.
An animation can be designed with:
- have 10 steps, each with an interval of 30ms.
- The total animation takes 300ms.
At each step, the current progress/percentage can be calculated:
var percentage = ( currentStep / totalSteps );
Now, each time your function is called by setInterval()
, you can set all DOM elements at once to the correct positions. To find out where an element should be at each animation frame, you use the:
var diff = ( to - from );
var stepValue = from + diff * percentage;
The jQuery easing functions can be called directly, and the final statement becomes:
var stepValue = jQuery.easing[ easingMethod ]( percentage, 0, from, diff );
I've turned this into a class:
/**
* Animation timeline, with a callback.
*/
function AnimationTimeline( params, onstep )
{
// Copy values.
// Required:
this.from = params.from || 0; // e.g. 0%
this.to = params.to || 1; // e.g. 100%
this.onstep = onstep || params.onstep; // pass the callback.
// Optional
this.steps = params.steps || 10;
this.duration = params.duration || 300;
this.easing = params.easing || "linear";
// Internal
this._diff = 0;
this._step = 1;
this._timer = 0;
}
jQuery.extend( AnimationTimeline.prototype, {
start: function()
{
if( this.from == this.to )
return;
if( this._timer > 0 )
{
self.console && console.error("DOUBLE START!");
return;
}
var myself = this;
this._diff = ( this.to - this.from );
this._timer = setInterval( function() { myself.doStep() }, this.duration / this.steps );
}
, stop: function()
{
clearInterval( this._timer );
this._timer = -1;
this._queue = [];
}
, doStep: function()
{
// jQuery version of: stepValue = from + diff * percentage;
var percentage = ( this._step / this.steps );
var stepValue = jQuery.easing[ this.easing ]( percentage, 0, this.from, this._diff );
// Next step
var props = { animationId: this._timer + 10
, percentage: percentage
, from: this.from, to: this.to
, step: this._step, steps: this.steps
};
if( ++this._step > this.steps )
{
stepValue = this.to; // avoid rounding errors.
this.stop();
}
// Callback
if( this.onstep( stepValue, props ) === false ) {
this.stop();
}
}
});
And now you can use:
var el1 = $("#element1");
var el2 = $("#element2");
var animation = new AnimationTimeline( {
easing: "swing"
, onstep: function( animprops )
{
// This is called for every animation frame. Set the elements:
el1.css( { left: ..., top: ... } );
el2.css( { left: ..., top: ... } );
}
});
// And start it.
animation.start();
Adding a pause/resume is an exercise for the reader.