Friday, September 21, 2012

Animating Multiple Elements Simultaneously with jQuery

Consider a group of absolutely positioned divs that are visually styled to look like 50x80 pixel rectangles (I setup a full demo in my sandbox to illustrate):

<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>

We can animate all of them at once using a class selector and calling animate:

$('.box').animate({left: 100, top: 100}, 'slow');

This will move all the boxes to the same location of the page. Unless the elements were all in different places to begin with, this animation will look rather unimpressive.

We can't select all the elements and set different target CSS properties so we need to calculate them and then iterate over all the elements using each() to animate each box individually:

var sp = starting point of divs
var ep = 5 different end points

// Animate all the boxes out to 5 different top/left positions
// All of the boxes start are the same point
$('.box').each(function (idx, box)
var $box = $(box);
$box.animate({left: ep[idx].x, top: ep[idx].y}, 2000, 'easeOutBounce');


// Return back to the starting point
$('.box').animate({left: sp.x, top: sp.y}, 'slow');

So one question comes to mind when looking at this code:

Why did all the animations start at the same time? Shouldn't we expect some slight discrepancy in timing because they can't all be started together?

The answer is that Javascript runs in a single thread. None of the animations can occur if something else is executing. This is both helpful and problematic. Its helpful because we can leverage the single thread to setup all of our animations so they start at the same time. Animate() is just adding to a queue and returning. The animation process can't start until our setup is complete. Its problematic because if some other code is running when the animation is suppose to be running, jQuery will skip ahead giving the appearance of choppy animations.

In the demo, one of the examples has a for loop that iterates 100,000,000 times after each element's animation is queued. By time the code finishes, the animation basically jumps to the end. This is because as soon as you call animate(), the clock starts ticking, jQuery will ensure the animation runs for the amount of time requested. If you say 500ms and your code takes that long to finish running before relinquishing control, the animation will just jump to the end. This means that you have to attempt not interfere with execution because you will starve the animation code from getting time to run.

To avoid crummy animations, we can adopt several strategies:

  1. Ensure you pre-calculate everything the animation will need to do. This way the complexity around the actual animate() calls will be minimal and the least amount of time necessary will be used to queue all the animations. If you use the step callback option in animate(), ensure that this code is using pre-calculated values as well to augment the animation.

  2. Consider disabling event handlers to cut down on unexpected code execution. Anything the user can click, drag, etc. will all steal from the animation execution and potentially affect the smoothness of the animation effect

When I first started this exercise, I was afraid it would be difficult to synchronize the animations of all the elements. I had forgotten about how the single thread would affect the execution. This "feature" of Javascript does simplify getting everything lined up as long as you are also aware of the limitations and work around them accordingly.