SVG logo animation with Anime.js

Many a library exist for animation on the web these days. Anime is my favorite. It’s approachable,[^1] comes with decent documentation and examples, and packs phenomenal cosmic power in an itty bitty living space (9.15 KB minified).

Anime lets you run animations on just about anything — CSS, SVG, DOM attributes — but what I love most about it is its ability to run animations on individual CSS transform properties. With Anime, you can write something like this:

1anime({
2 // specfiy a string selector, a DOM node, or an array of those things
3 targets: '.a-dead-body',
4 // we can animate on a specific transform property
5 translateX: 100,
6 // as many of these as you want
7 rotate: 180,
8 // doesn’t have to be a transform, anything numerically-based will do
9 color: 'rgb(255,0,0)',
10 // how long?
11 duration: 1000,
12 // cool built-in easing curves
13 easing: 'easeOutElastic',
14 // do something afterward
15 complete: function() {
16 console.log('It’s alive!');
17 }
18});

Which could produce something like this:

💀 Click me!

See?

Of course, that’s a nearly useless animation. Let me show you a real-world example — the logo animation on the home page of this very website.

My logo (or personal mark, as I like to call it) is an SVG that resembles the initials “bw” stylized to look like a futuristic “w”. Take a look below and, when you are ready, click the animate button to see how we want our final animation to look.

In order to achieve this effect, we need to break the logo into three distinct pieces:

First we need to get our logo into its pre-animation state — the starting point. To do that, we need to do the following:

  1. Make sure the whole logo starts off transparent
  2. Explicitly make the middle piece transparent
  3. Bump the entire logo down by half of its height
  4. Swap the left and right pieces

Update 10–23–2017: Safari 11 requires that transform-box: fill-box; be set on each piece in order to translate each piece by a percentage of its own height or width.

1$('#ex1-logo').css({
2 'transform': 'translateY(50%)',
3 'opacity': 0
4});
5$('#ex1-logo-middle').css('opacity', 0);
6$('#ex1-logo-left').css('transform', 'translateX(100%)');
7$('#ex1-logo-right').css('transform', 'translateX(-100%)');

Good. If you could see it at this point, it would resemble an arrow pointing upward. Now we can bring in Anime.

First, we want to move the whole logo group back up to its original position while simultaneously fading it in.

1anime({
2 targets: '#ex1-logo',
3 translateY: ['50%', '0%'], // start value, end value
4 opacity: 1,
5 easing: 'easeOutSine',
6});

Sweet. As soon as that animation finishes, we want to do two things simultaneously:

We can latch on to Anime’s complete callback in order to run code after the first animation finishes.

1anime({
2 targets: '#ex1-logo',
3 translateY: ['50%', '0%'], // start value, end value
4 opacity: 1,
5 easing: 'easeOutSine',
6 complete: function() {
7 // MORE ANIME HERE!
8 }
9});

I’m going to fire two different Anime animations inside that callback. First, the hard one — swapping the left and right pieces back to their original position.

1anime({
2 targets: '#ex1-logo',
3 translateY: ['50%', '0%'], // start value, end value
4 opacity: 1,
5 easing: 'easeOutSine',
6 complete: function() {
7 anime({
8 targets: ['#ex1-logo-left', '#ex1-logo-right'],
9 translateX: function(el, index) {
10 if($(el).attr('id') === 'ex1-logo-left') {
11 return ['100%', '0%'];
12 } else {
13 return ['-100%', '0%'];
14 }
15 },
16 easing: 'easeInOutQuad',
17 duration: 500
18 });
19 }
20});

What?! We have two targets? And what is this translateX voodoo?

A very cool feature of Anime is its ability to accept a function instead of a static value. The function will run for each target specified, and automatically recieves the current DOM element (el) and the zero-based index of the current target. Very similar to a jQuery .each() function, if you are familiar with that.

For each target, we can now do some logic to return back the value we want. In our case, we want to give the left piece different start and end points from the right piece. That’s done with an if statement that checks the element’s id.

On to the next part. We’re just going to run another Anime animation at (roughly) the same time by calling it right after this one.

This is the final state of our animation code, so here it is all together now:

1$('#ex1-logo').css({
2 'transform': 'translateY(50%)',
3 'opacity': 0
4});
5$('#ex1-logo-middle').css('opacity', 0);
6$('#ex1-logo-left').css('transform', 'translateX(100%)');
7$('#ex1-logo-right').css('transform', 'translateX(-100%)');
8 
9anime({
10 targets: '#ex1-logo',
11 translateY: ['50%', '0%'], // start value, end value
12 opacity: 1,
13 easing: 'easeOutSine',
14 complete: function() {
15 anime({
16 targets: ['#ex1-logo-left', '#ex1-logo-right'],
17 translateX: function(el, index) {
18 if($(el).attr('id') === 'ex1-logo-left') {
19 return ['100%', '0%'];
20 } else {
21 return ['-100%', '0%'];
22 }
23 },
24 easing: 'easeInOutQuad',
25 duration: 500
26 });
27 
28 anime({
29 targets: '#ex1-logo-middle',
30 opacity: 1,
31 easing: 'easeInOutExpo',
32 });
33 }
34});

Lastly, we fade in the middle piece, and we even use a fancy smancy easing curve to do it.

One more time, with feeling

The final animation looks something like this, as we saw earlier.

If that seemed like a lot, don’t worry — it kind of is. It might seem a bit odd at first, but it’s just a matter of thinking through what you want to do. Think about it in terms of a starting point and an ending point. Then write out in words what you want to accomplish. If you get stuck, the docs are your friend.

Want to see more tutorials? Get in touch and let me know what you would like to see.