Wednesday, October 3, 2012

Managing Multiple Animation Sequences in jQuery

The animate function in jQuery provides a quick and convenient method of animating an element's styles. A problem you might run into when working with animate() is how to setup sequences of animations to run based on certain events or other animations completing. The challenge is coding the animation sequences so you can read the code and understand the flow of the animation. If its just one element (or a group that need to do the same thing), then you can just chain multiple animate calls together:



// Starting at 0,0
$('#myDiv')
.animate({left: 50}, 1000)
.delay(1000)
.animate({top: 50}, 1000);



This simply moves a div left 50px over 1 second, waits 1 second, and then moves the div down 50px over 1 second. So far, nothing we can't understand. We could get a little more fancy and perform the animation based on a click event:



$('goDiv').click(function (e) {

$('#myDiv')
.css({left: e.pageX, top: e.pageY})
.show()
.animate({left: 0}, 1000)
.delay(1000)
.animate({top: 0}, {
duration: 1000,
complete: function () {
$(this).hide();
}
});

});


Now we're animating from the mouse coordinates of the click to the top left of the screen. Additionally, the element is only made visible during the animation and then hidden upon finishing via the complete callback.

Again, so far everything is pretty readable and not overly complex. But what if we want to coordinate the animation of two divs based on the click event and stagger their animations:



$('.box').css('display', 'none');

$('#goDiv').click(function (e) {

var pos = $(this).offset();

$('#myDiv1')
.stop(true)
.css({left: e.pageX-pos.left, top: e.pageY-pos.top})
.show(250)
.animate({left: 0}, 1000)
.delay(1000)
.animate({top: 0}, {
duration: 1000,
complete: function () {
$(this).hide(250);
}
});

$('#myDiv2')
.stop(true)
.css({left: e.pageX-pos.left, top: e.pageY-pos.top})
.delay(1000)
.show(250)
.animate({top: 0}, 1000)
.delay(1000)
.animate({left: 0}, {
duration: 1000,
complete: function () {
$(this).hide(250);
}
});

});



Its not readily obvious how these animations are timed and even work together. Here's a demo of the functionality the above code implements. Its easier to see it than visualize it based on the code. You could try to place the myDiv2 animation in the complete callback of myDiv1's first animation. This would make it more obvious that it starts after myDiv1 moves to the left:



$('.box').css('display', 'none');

$('#goDiv').click(function (e) {

var pos = $(this).offset();

$('#myDiv1')
.stop(true)
.css({left: e.pageX-pos.left, top: e.pageY-pos.top})
.show(250)
.animate({left: 0}, {
duration: 1000,
complete: function () {
$('#myDiv2')
.stop(true)
.css({left: e.pageX-pos.left, top: e.pageY-pos.top})
.show(250)
.animate({top: 0}, 1000)
.delay(1000)
.animate({left: 0}, {
duration: 1000,
complete: function () {
$(this).hide(250);
}
});
}
})
.delay(1000)
.animate({top: 0}, {
duration: 1000,
complete: function () {
$(this).hide(250);
}
});

});



I can't say this is really anymore elegant or readable. And if more elements were involved with any more complex timings, the code would be virtually impossible to understand. This is the problem I encountered when working on a SVG-based animation that simulates a pulse wave when the mouse is clicked. As I built the sequences of animations, it became apparent that I was going to need to organize my code in a way that would make it easier to understand what was happening. Otherwise, in a month, when I wanted to do something with it, I would have no idea what was happening. I also intended to reuse parts of the effect with some other projects and wanted something that was reasonably modular enough that I could plug it in without much effort. The problem was further compounded by the fact that I needed to use the step callback to manually manage the animated properties of the SVG elements.

My solution, while not perfect, was to move the sequencing code out of the control code by creating functions that could be used in the step and complete callbacks of the animate function. This enabled me to keep the event handler code as short as possible and organize the animation logic in an order that approximated the sequencing of the actual animation.

Using that same approach with the example I developed in this post:



$('.box').css('display', 'none');

function div1Show()
{
$('#myDiv1').show(250, div1Left);
}

function div1Left()
{
$('#myDiv1').animate({left: 0}, {
duration: 1000,
complete: function ()
{
div2Show();
div1Top();
}
});
}

function div2Show()
{
$('#myDiv2').show(250, div2Top);
}

function div2Top()
{
$('#myDiv2').animate({top: 0}, {
duration: 1000,
complete: div2Left
});
}

function div1Top()
{
$('#myDiv1')
.delay(1000)
.animate({top: 0}, {
duration: 1000,
complete: div1Hide
});
}

function div1Hide()
{
$('#myDiv1').hide(250);
}

function div2Left()
{
$('#myDiv2')
.delay(1000)
.animate({left: 0}, {
duration: 1000,
complete: div2Hide
});
}

function div2Hide()
{
$('#myDiv2').hide(250);
}


$('#goDiv').click(function (e) {

var pos = $(this).offset();

$('#myDiv1')
.stop(true)
.css({left: e.pageX-pos.left, top: e.pageY-pos.top});

$('#myDiv2')
.stop(true)
.css({left: e.pageX-pos.left, top: e.pageY-pos.top});

div1Show();

});


Now, the click event only handles setting the initial position and kicking off the animation sequence. Each part of the animation is organized in a different function in approximately the order it will run. However, without even looking at the code in the functions, you can get an idea of the flow. The function names provide a reasonable outline of what's happening. The other advantage is now the animation is modular - I can use any part of it, repeat a section, skip parts, etc. The demo is running on this last version and I left all the other variations in the demo's source for reference.

There's certainly no requirement to organize the code in this fashion. However, if you are building complex animation sequences, it may be advantageous to consider a similar approach to constructing your animations. Ultimately, it will provide flexible and readable code that you will probably appreciate in the future when you need to understand what you wrote and why you wrote it.