Animating Many Elements and the Animate Method

by kirupa   |   5 August 2015

So far, we’ve looked at how to use the animate method to animate only a single element. That is a bit limiting. A large class of (totally awesome!) animations involve being able to animate multiple elements. Take the following animation:

Browser Warning

Because of how new Web Animations is, it is only supported in the latest versions of Chrome and can be optionally enabled for Firefox. IE support is "Under Consideration". For broader support, you can either wait a short while or use this handy polyfill!

In this example, you have five circles that just zig and zag all over the screen. Under the covers, this animation is accomplished by just using the animate method and employing some simple JavaScript tricks. In the following sections, we will look at everything that is involved when animating more than a single element.

Onwards!

Setting the Stage

Everything you are about to see makes a whole lot more sense if you follow along with a simple example. This is entirely optional, of course, but if you do want to follow along, we are going to create five circles in HTML that will serve as the things we animate.

Here is what the HTML looks like for them:

<div id="mainContainer">
   <div class="circle"></div>
   <div class="circle"></div>
   <div class="circle"></div>
   <div class="circle"></div>
   <div class="circle"></div>
</div>

Our circles are basically div elements that have a class value of circle associated with it. By default, div elements aren't circular. They are also invisible and really annoying to position. The corresponding CSS fixes all of that for us:

.circle {
	width: 25px;
	height: 25px;
	border-radius: 50%;
	background-color: none;
	border: #FFF 10px solid;
	display: "inline-block";
	position: absolute;
}
#mainContainer {
  background-color: #CC6666;
  width: 550px;
  height: 350px;
}

When you put all of this together, you'll see something that looks as follows:

Looks here are deceiving. Just because you see one circle doesn't mean that there aren't more circles lurking around there. They all just happen to be stacked perfectly on top of each other, and we'll fix that all shortly in the next section when we get our hands messy again with the animate method!

Easing Into Animating Multiple Elements

Animating multiple elements using the animate method is scarily very similar to animating just a single element using the animate method. To cut right to the chase, the multiple element approach simply involves you looping through all of the elements you wish to animate and calling the animate method on each of them.

Here is what that looks like in code form:

var circles = document.querySelectorAll(".circle");

for (var i = 0; i < 5; i++) {
  var circle = circles[i];

  circle.keyframes = [{
    opacity: 0,
    transform: "translate3d(" + 90 * i + "px, 0px, 0px)"
  }, {
    opacity: 1,
    transform: "translate3d(" + 90 * i + "px, 200px, 0px)"
  }, {
    opacity: 0,
    transform: "translate3d(" + 90 * i + "px, 0px, 0px)"
  }];

  circle.animProps = {
    duration: 1000 + 500 * i,
    easing: "ease-out",
    iterations: Infinity
  }

  var animationPlayer = circle.animate(circle.keyframes, circle.animProps);
}

Take a look at what the code is doing. We have a for loop that goes through all the elements returned by the querySelectorAll call:

var circles = document.querySelectorAll(".circle");

for (var i = 0; i < 5; i++) {
   .
   .
   .
}

Inside the loop, we are back to dealing with each element individually:

var circles = document.querySelectorAll(".circle");

for (var i = 0; i < 5; i++) {
  var circle = circles[i];
   .
   .
   .
}

The only major difference is that all of the code that you see gets called once for each element you are applying the animation to. There is one other detail that is different than what you've seen before. Notice how the keyframe and animation properties are defined:

circle.keyframes = [{
  opacity: 0,
  transform: "translate3d(" + 90 * i + "px, 0px, 0px)"
}, {
  opacity: 1,
  transform: "translate3d(" + 90 * i + "px, 200px, 0px)"
}, {
  opacity: 0,
  transform: "translate3d(" + 90 * i + "px, 0px, 0px)"
}];

circle.animProps = {
  duration: 1000 + 500 * i,
  easing: "ease-out",
  iterations: Infinity
}

They are separated out into their own properties that live directly on each of the circle elements we are animating. This makes working with them much easier – both aesthetically and, as you will see in a little bit, functionally. The thing to note is that the end result is still the same as what you've seen before: the animate object gets called on an element with the keyframes and animation properties specified as arguments:

var animationPlayer = circle.animate(circle.keyframes, circle.animProps);

It just happens that the arguments refer to properties defined outside of the animate method call as opposed to being fully defined inline. Anyway, if you were to run this animation, you will see something that looks as follows:

If you want to inspect this example further, you may want to view it in its own page at the following location.

Ok, before we move on to greater and shinier things, let's review what just happened. What we did is simply apply the animate method on multiple elements to animate all of them. Inside each keyframe, we aren't doing anything crazy either. To add some variety with what the animation does on each circle, we are using the i value from the loop to give each circle a slight variation in position and duration:

circle.keyframes = [{
  opacity: 0,
  transform: "translate3d(" + 90 * i + "px, 0px, 0px)"
}, {
  opacity: 1,
  transform: "translate3d(" + 90 * i + "px, 200px, 0px)"
}, {
  opacity: 0,
  transform: "translate3d(" + 90 * i + "px, 0px, 0px)"
}];

Seems simple enough, right?

Now, there is one thing this approach to animating multiple elements won't do. If you are looking for some extra variety where the animation doesn't repeat the same exact thing path on every single iteration, you are going to be disappointed. Changing the values of your keyframes and animation properties in each iteration to create a whole new animation requires some additional tricks, and you're going to learn all about them next!

Adding Some More Variety

In the previous section, the animation you attached to each element never changed. It just retraced its exact same steps in the exact same way every single time. That's boring, and we don't do boring around here. What we want to do is have our animation do something slightly different every time it loops. To do this, there are several things we need to do. We need to...

  1. Detect that the animation has run to completion
  2. On the recently completed animation, update the keyframe details with whole new values to vary things up a bit

That seems kinda obvious, but actually implementing that with the animate method is a whole lotta fun...and pain.

Let's first setup our animation again with a slight twist. Previously, you saw one level of code refactoring where the keyframes and animation properties were stashed away as their own property on the circle element. This time around, let's go one step further and move everything related to animating an element into its own function with the argument being the element to attach the animation to. By modifying our earlier example slightly, this refactoring is going to look like this:

var circles = document.querySelectorAll(".circle");

for (var i = 0; i < 5; i++) {
  var circle = circles[i];
  animateCircles(circle);
}

function animateCircles(circle) {
  var xMax = 500;
  var yMax = 300;

  circle.keyframes = [{
    opacity: 0,
    transform: "translate3d(" + (Math.random() * xMax) + "px, " + (Math.random() * yMax) + "px, 0px)"
  }, {
    opacity: 1,
    transform: "translate3d(" + (Math.random() * xMax) + "px, " + (Math.random() * yMax) + "px, 0px)"
  }, {
    opacity: 0,
    transform: "translate3d(" + (Math.random() * xMax) + "px, " + (Math.random() * yMax) + "px, 0px)"
  }];

  circle.animProps = {
    duration: 1000 + Math.random() * 3000,
    easing: "ease-out",
    iterations: 1
  }

  var animationPlayer = circle.animate(circle.keyframes, circle.animProps);
}

Pay attention to the animateCircles function. This function is now responsible for attaching an animation to an element, and all you have to do is just call it and pass in the element you wish to animate…just like what we do now:

for (var i = 0; i < 5; i++) {
  var circle = circles[i];
  animateCircles(circle);
}

The contents of our for loop couldn't be simpler, for all of the heavy lifting has been shifted over the animateCircles function. That doesn't change how the animation itself works, though. If you test your animation right now, you'll see your circles moving randomly around the screen. They don't run forever, though. They run exactly once, and that is by design given what specified as the value for the iterations property:

circle.animProps = {
  duration: 1000 + Math.random() * 3000,
  easing: "ease-out",
  iterations: 1
}

Our animation running just once is an important detail, and this tees me up to explain more of what we are trying to do. Each time the animation runs to completion, I mentioned earlier that we want to update the keyframe details so that the next iteration of the animation behaves a little differently. The word update is not the most accurate description of what our code is going to do. What we are going to do is actually replace the current keyframes and animation properties with an entirely new instance that contains whole new values. We do that by calling the animate method again at the end of each iteration. That little bombshell is the implementation detail to complement the verbal description of the game plan we saw earlier.

To do this, the first thing we need to do is figure out when an animation has run to completion. That is easily done by listening to the finish event on our Animation object. Before you get too puzzled, I do realize that this is the first time I've brought up both a finish event and an object called Animation. Let's step to the side and take another look at this.

Meet the Animation Object

When you call the animate method, an Animation object is automatically returned for you:

var animationPlayer = circle.animate(circle.keyframes, circle.animProps);

In our example (and many examples earlier), we call the animate method and set it equal to a variable – such as animationPlayer in this case. For this entire time, this variable has been storing a reference to the Animation object that gets returned as a result of calling the animate method.

The Animation object has been running directly under our noses this entire time!

By having the Animation object, listening to events it fires is pretty easy to handle. You can use addEventListener to listen to an event on this object just like you would listen for events on other objects. Unsurprisingly, listening to our finish event looks as follows:

animationPlayer.addEventListener('finish', function(e) {
  // do something
}, false);

This snippet of code totally works and the anonymous function that acts as our event handler will get called when this animation completes. All we need to do is call the animate method on the element whose Animation object just fired the finish event. Seems really simple, right?

As is the case with all questions of this sort, the answer is never going to be positive. From the event handler, there isn't a way to access the element the animation is attached to. At least, there isn't a solution that I was able to find. The event arguments for the finish event don't contain anything that helps with this either. The solution is to enhance our event handling capabilities by including a reference to the animated element in addition to the Animation object and the event arguments.

Like everything else in JavaScript, there are many ways to implement this solution, but the most straightforward way is to do something that looks as follows:

function addFinishHandler(anim, el) {
  anim.addEventListener('finish', function(e) {
    // do something
  }, false);
}

This addFinishHandler function takes two arguments:

  1. The Animation object
  2. The element you are animating

By relying on the magic of closures, the addEventListener definition and the anonymous function that gets called when the finish event is heard will allow you to work with the animated element in addition to the regular event arguments that get passed in by the finish event.

With this approach, we can accomplish our goal of calling the animate method again on the element whose Animation object triggered the finish event:

function addFinishHandler(anim, el) {
  anim.addEventListener('finish', function(e) {
    animateCircles(el);
  }, false);
}

Of course, given the refactoring that we did, the way we call the animate method is by calling animateCircles instead.

All that remains is to actually use this awesome addFinishHandler function. Inside the animateCircles method, just after we call the animate method, add the following highlighted line:

var animationPlayer = circle.animate(circle.keyframes, circle.animProps);
addFinishHandler(animationPlayer, circle);

When you run everything right now, you will find that your circles move from spot to spot and change where they go at each iteration. This is because the keyframe values and animation properties get reset with whole new values each time thanks to us aggressively calling animateCircles every time a finish event gets overheard!

Before we wrap things up, there is just one more thing I'd like for us to do. To ensure we don't lose sight of the subtle and not-so-subtle things our code does, let's take a moment and walk through everything briefly:

  1. Each circle gets attached to an animation via the animateCircles function
  2. We use a lot of Math.random() calls inside the keyframe declaration to provide some level of variation in where a circle moves and how long it runs
  3. Each animation runs exactly once, and we listen to the finish event to deal with each animation once it completes
  4. Thanks to addFinishHandler, the event handler for the finish event contains not only the event arguments passed in but a reference to the element the Animation object was affecting as well
  5. To restart our animation with a whole new set of keyframes and animation property values, we call animateCircles from the finish event handler
  6. Throughout all of this, you got a few milliseconds older - milliseconds you've lost and will never get back

If you can map these steps to the line of code responsible for making that step happen, you should pat yourself on the back for a job well done. There were some deep conceptual and technical tricks we needed to pull, but the end result is something totally cool. At least, I think so!

Conclusion

We are finally at the end. Looking back at what we've accomplished, I am not totally happy with everything we did. First of all, it seems dirty to replace the entire set of keyframes and animation properties at each animation iteration. It also seems odd that there doesn't seem to be a way to access the element the Animation object is attached to via the event arguments for the finish event. Ideally, the event arguments for the finish event should allow you to do that.

I'm hoping all of this is a result of me not fully understanding what is available in the Web Animations spec, and if that is the case, please do comment below (or tweet to @kirupa) and let me know how to properly handle this situation. If everything I've done is according to the book, then I would hope that future revisions of the Web Animations spec take some of these concerns into consideration for the future.

If you have a question about this or any other topic, the easiest thing is to drop by our forums where a bunch of the friendliest people you'll ever run into will be happy to help you out!

THE KIRUPA NEWSLETTER

Get cool tips, tricks, selfies, and more...personally hand-delivered to your inbox!

( View past issues for an idea of what you've been missing out on all this time! )

GOT A QUESTION?

HOT FORUM TOPICS

Serving you freshly baked content since 1998!

Killer hosting by (mt) mediatemple

Facebook Twitter Youtube Pinterest Instagram Github
BACK TO TOP
new books - yay!!!