Menu Search
Jump to the content X X
Smashing Conf Barcelona

You know, we use ad-blockers as well. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. our upcoming SmashingConf Barcelona, dedicated to smart front-end techniques and design patterns.

The Illusion Of Life: An SVG Animation Case Study

With flat design becoming the ever visible trend of 2016, it’s clear why there’s been a resurgence in SVG usage. The benefits are many: resolution-independence, cross-browser compatibility and accessible DOM nodes. In this article, we’ll take a look at how we can use SVGs to create seemingly complex animations from simple illustrations.

The Brief Link

Figure 1. What are we creating? Seemingly complex animations from simple SVG illustrations.

This project began as a simple thought experiment: How far can we push SVG animation?

At the time, designer Chris Halaska694 and I were colleagues working on an illustration-heavy campaign website. While aesthetically pleasing, the designs lacked the required “oomph” that all creatives search for. We found the answer in “The Camera Collection5,” a motion graphics animation that had just gone viral. We could use animations to bring illustrations to life, and SVGs were the perfect medium to do this.

Further Reading on SmashingMag: Link

The problem we were facing, which still exists very much today, is that SVG animation is either delegated to a front-end developer who is attempting art direction or to a designer who is attempting JavaScript. Of course, neither of these scenarios is inherently wrong, but with animation being visual in nature and with very few applications tackling the problem, we wanted to bridge the gap between code and design.

Our idea was to create a data-driven process that enables designers to quickly prototype animations from static illustrations.

The Rules Of Animation Link

In The Illusion of Life10, Disney outlines 12 basic principles to add character to animation. Squash and stretch, anticipation, slow in and slow out, timing, and exaggeration all serve to bring any object, inanimate or otherwise, to life. We wanted to follow these principles in our project, moving away from the rigidness of the DOM to something more fluid and natural. By creating a system around transformations, timing and easing, we were able to create animations that were stylistically uniform, but each with a character of its own.

Transformations Link

The flat design trend lends itself so well to SVG usage because of the simplicity of the illustrations. We mimicked this characteristic in animation, pairing geometric shapes with simple geometric movements. We had a single rule: use basic transformations (translate, rotate, scale) with basic origins (left, right, top, bottom and center).

transformations11
Figure 2. The nine possible animation origins, using a combination of left, right, center, top and bottom.

Timing Link

To maintain a similar cadence and rhythm, we constrained ourselves to very specific increments of time. Animations lasted 2 seconds and comprised 10 individual steps. A tween, an animation of just a single transformation (translate, rotate and scale) had to begin and end on one of these steps, which we defined as keyframes.

timing12
Figure 3. An example animation in which each step increments by 200 milliseconds with three overlapping tweens.

Easing Link

While transformations and timing are enough to create the visual perception of motion, easing brings everything to life. We found that three easing formulas provided more than enough variation to add character to the movements: easeOutBack, easeInOutBack and easeOutQuint.

easing13
Figure 4. A visual comparison of animations with and without easing. Note that using any variation of easingBack will influence the transformations to some extent.

Let’s Get Started Link

Preparing the Assets Link

Though the illustration app landscape has matured in recent years, with Sketch14 and Inkscape15 becoming increasingly popular, we opted to create our SVGs in Adobe Illustrator.

breakdown16
Figure 5. A breakdown of the elements that make up the final animation.
illustrator layers17
Figure 6. Illustrator automatically creates IDs from layer names when exporting to SVG.

Before you export to SVG, group and label every layer. Illustrator will automatically creates IDs from the layer names during the exporting process. For every animated element, the output should look similar to the XML shown below. Note that even if an element doesn’t have any children, it still needs to be grouped under a g tag. This is in preparation for adding SVG transform groups18, explained later on.

<g id="zipper">
<path fill="#272C40" d="…"/>
</g>
illustrator exporting19
Figure 7. The SVG export settings used. We unchecked “responsive” because the animation units are pixel-based.

Handling Masks Link

You may have noticed the <Clip Group> layer in figure 6. These are essentially clipping masks created in Illustrator. When exported to SVG, they automatically become predefined clipPaths that can be used to mask elements in the exact same way.

<g>
<defs>
  <rect id="SVGID_1_" x="235" y="-106.3" width="500" height="309"/>
</defs>

<clipPath id="SVGID_2_">
  <use xlink:href="#SVGID_1_"  overflow="visible"/>
</clipPath>

<g id="strap-right" clip-path="url(#SVGID_2_)">
  <path fill="#93481F" stroke="#000000" stroke-width="1.5" stroke-miterlimit="10" d="…"
    />
</g>
</g>
clipPath20
Figure 8. The use of clipPath to hide the belt straps before animation.

Prototype, Prototype, Prototype Link

With the assets prepared, we were ready to build. We began an iterative process of creating prototypes and testing various technologies to find a solution. Here, we’ll briefly outline each of our attempts, the pros and cons, and why we pivoted from one solution to the next.

CSS and Velocity.js Link

Our initial attempts at using CSS to create animations were promising. We believed that with hardware-accelerated transformations, the animations would run smoothly and the implementation would be straightforward, without the need for external libraries. While we were able to create a functioning version in Chrome, the solution failed in all other browsers.

Firefox would not respect the transform-origin property of SVGs21, while Internet Explorer’s support for SVG CSS animation is completely non-existent22. Lastly, with CSS and JavaScript being so tightly coupled, we found ourselves jumping back and forth between too many files for the solution to be considered elegant.

In a similar vein, we ran into the same problems with Velocity.js23. Because the animation engine also uses CSS transformations, the Firefox and Internet Explorer issues remained unresolved.

GSAP Link

GSAP24 has been an industry standard since its Flash days, and its popularity has risen even more so since being ported to JavaScript. With its chainable syntax, extensive SVG support and unparalleled performance, GSAP was an obvious contender — save for one issue: It was overkill. Importing TweenMax and TimelineMax immediately doubled the size of our project and proved to be excessive. Chris Gannon25 let us know that TimelineMax is included in TweenMax and combined is only 37kb, a misunderstanding on our end.

Snap.svg Link

In our final attempt, we used Snap.svg26, the successor to Raphael27. Snap offers extensive functionality in DOM manipulation but the bare minimum in animation support. Though we recognized this as a setback, the deficiencies led us to roll our own JavaScript to fill in the gaps. This resulted in a lightweight solution that was still capable of achieving the fidelity of animations we were striving for.

Mo.js, Anime and Web Animations API Link

Since writing this article, three very promising SVG animation libraries have been gaining traction in the community: Mo.js28, Anime29 and the Web Animations API30. If we get the chance to revisit the problem, these alternatives would definitely be taken into consideration. Nonetheless, the concepts behind this article should be transferable to any animation library you wish to use.

The Scaffold Link

We’ll begin by importing a basic style sheet and the Snap.svg library into our project. We’ll also include a port of Robert Penner’s easing functions31 for later use.

folder structure32
Figure 9. The final folder structure of our project. The “Hello world” scaffold begins with just the highlighted files.
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>The Illusion of Life: An SVG Animation Case Study</title>

  <!-- Styles -->
  <link rel="stylesheet" type="text/css" href="css/style.css" />

  <!-- Libraries -->
  <script src="js/libs/snap.svg.min.js"></script>
  <script src="js/libs/snap.svg.easing.min.js"></script></html>
</head>
</html>
/* Full screen */
html, body {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
  background-color: #E6E6E6;
  font-family: sans-serif;
}

/* Centered canvas */
#canvas {
  position: absolute;
  top: 50%;
  left: 50%;
  -webkit-transform: translateX(-50%) translateY(-50%);
  -ms-transform: translateX(-50%) translateY(-50%);
  transform: translateX(-50%) translateY(-50%);
  overflow: hidden;
}

Hello World Link

“Hello world” — a small, simple win. For us, that just meant getting something on the screen. We first instantiated a new Snap object, with a DOM ID representing our canvas. We use the Snap.load function34 to indicate the external SVG source and an anonymous callback that will append the nodes to the DOM tree.

<body>
<div id="canvas"></div>

<script>
  (function() {
    var s = Snap('#canvas');

    Snap.load("svg/backpack.svg", function (data) {
      s.append(data);
    });
  })();
</script>
</body>

Making A Simple Plugin Link

To create a reusable component for multiple animations, we create a “plugin” using the prototype pattern. Using an immediately invoked function expression (IIFE) ensures data encapsulation, while still adding SVGAnimation to the global namespace. If we place the code we have so far into a separate init function, we will have the basis for SVGAnimation.

; (function(window) {
'use strict';

var svgAnimation = function () {
  var self = this;
  self.init();
};

svgAnimation.prototype = {
  constructor: svgAnimation,

  init: function() {
    var s = Snap('#canvas');

    Snap.load("svg/backpack.svg", function (data) {
      s.append(data);
    });
  }
};

// Add to global namespace
window.svgAnimation = svgAnimation;
})(window);

See the Pen Simple Plugin36 by Michael Ngo (@hellomichael6358545041372) on CodePen6459555142383.

Adding Options Link

Dissecting Snap.load, we can see two potential parameters that can be passed in as options, a canvas and an external SVG source. Let’s create a separate loadSVG function to handle just that.

/*
Loads the SVG into the DOM
@param {Object}   canvas
@param {String}   svg
*/
loadSVG: function(canvas, data) {
Snap.load(svg, function(data) {
  canvas.append(svg);
});
}

Objects as Parameters Link

Now we need a way to pass these options into SVGAnimation. There are several ways to do this, the standard way being to pass individual parameters.

var backpack = new svgAnimation(Snap('#canvas'), 'svg/backpack.svg');

But there’s a better solution. By passing in objects instead, the code becomes not only more readable, but also more flexible. We no longer need to keep track of the order; we can make parameters optional; and we can also reuse the object later. So, let’s rewrite the previous snippet, passing in an options object instead.

var backpack = new svgAnimation({
canvas:       new Snap('#canvas'),
svg:          'svg/backpack.svg'
});

Merging Objects Link

Now that we have the options object, we need to make the values accessible to the rest of the plugin. But before we do this, let’s merge the passed-in object with our own defaults. Even though we’ve chosen to set both values to null, we’ll still include them as a reference for the type of values we expect to receive.

svgAnimation.prototype = {
constructor: svgAnimation,

options: {
  canvas:     null,
  svg:        null
}
};

With the defaults now set, we’ll use an extend function to merge both objects. Essentially, the function will loop through all of the properties of one object and copy them over to another.

/*
Merges two objects
@param  {Object}  a
@param  {Object}  b
@return {Object}  sum
http://stackoverflow.com/questions/11197247/javascript-equivalent-of-jquerys-extend-method
*/
function extend(a, b) {
for (var key in b) {
  if (b.hasOwnProperty(key)) {
    a[key] = b[key];
  }
}

return a;
}

With the extend function defined, let’s amend the SVGAnimation constructor. One thing you’ll notice is that self is set to this. We’ll cache the original this to ensure that inner scopes have access to the current object’s data and methods.

var svgAnimation = function (options) {
var self = this;
self.options = extend({}, self.options);
extend(self.options, options);
self.init();
}

Lastly, we’ll update init to call loadSVG, passing in the canvas and svg reference we set during instantiation.

init: function() {
var self = this;

self.loadSVG(self.options.canvas, self.options.svg);
}

See the Pen Adding Options40 by Michael Ngo (@hellomichael6358545041372) on CodePen6459555142383.

Hardcoded Prototype Link

Adding SVG Transformation Groups

As mentioned earlier, Snap.svg’s animation engine is quite primitive and, just like CSS, only supports transform strings as a single request. This means that if you’re looking to animate more than one type of transformation, it must happen either sequentially or all at once (sharing duration and easing). Though not the most elegant solution, adding extra nodes to the DOM tree solves this problem. With a separate grouped element for each translate, rotate and scale transformation, we can now independently control each tween. The example that best illustrates this use case is the zipper, which also serves as our initial prototype.

We begin by passing the zipper element to the createTransformGroup function, which we then go on to define.

var $zipper = canvas.select("#zipper");
self.createTransformGroup($zipper);

After we’ve selected all child nodes, we can use the Snap.g function44 to nest the contents within each respective transform group.

/*
Create scale, rotate and transform groups around an SVG DOM node
@param {object} Snap element
*/

createTransformGroup: function(element) {
if (element.node) {
  var childNodes = element.selectAll('*');

  element.g().attr('class', 'translate')
    .g().attr('class', 'rotate')
    .g().attr('class', 'scale')
    .append(childNodes);
}
}

This results in the creation of independent transformation groups, which we can target in our animations.

<!-- Old node -->
<g id="zipper">
<path fill="#272C40" d="…"/>
</g>

<!-- New node -->
<g id="zipper">
<g class="translate">
  <g class="rotate">
    <g class="scale">
      <path fill="#272C40" d="…"></path>
    </g>
  </g>
</g>
</g>

A Snap.svg Animation Link

We’re finally ready to animate our first element. Snap.svg provides two functions to do this: transform45 and animate46. We’ll use transform to place the animation at the first keyframe, and then use animate to get us to the last.

Snap.svg supports standard SVG transform notation, but we’ve opted to use transform strings as a means to set parameters instead. Explanations are sparse on the official website, but legacy documentation47 can be found on Raphael’s. The initial uppercase letter is an abbreviation of the transformation. The parameters x, y and angle represent the values we are animating to, with cx and cy being the center of origin.

// Scale
Snap.animate({transform: 'S x y cx cy'}, duration, easing, callback);

// Rotation
Snap.animate({transform: 'R angle cx cy'}, duration, callback);

// Translate
Snap.animate({transform: 'T x y'}, duration, callback);

Calculating Origins Link

We ran into an interesting problem with defining origins, however. In Snap.svg, the animate and transform functions only accept parameters as pixel values, making it extremely difficult to measure. Ideally, as our brief outlined, we wanted to define the origin as a combination of top, right, bottom, left and center.

Fortunately, Snap.svg provides getBBox48, which measures the bounding box of any given element, returning a multitude of descriptors, including the values we’re searching for. We’ll write two functions, getOriginX and getOriginY, that accept a bBox object and a direction string as parameters, returning pixel values as needed.

/*
Translates the horizontal origin from a string to pixel value
@param {Object}     Snap bBox
@param {String}     "left", "right", "center"
@return {Object}    pixel value
*/

getOriginX: function (bBox, direction) {
if (direction === 'left') {
  return bBox.x;
}

else if (direction === 'center') {
  return bBox.cx;
}

else if (direction === 'right') {
  return bBox.x2;
}
},

/*
Translates the vertical origin from a string to pixel value
@param {Object}     Snap bBox
@param {String}     "top", "bottom", "center"
@return {Object}    pixel value
*/

getOriginY: function (bBox, direction) {
if (direction === 'top') {
  return bBox.y;
}

else if (direction === 'center') {
  return bBox.cy;
}

else if (direction === 'bottom') {
  return bBox.y2;
}
}

Animation in Practice Link

Let’s see this all in practice with a scaling animation. We first select the corresponding transform group using its class name, scale it down until it’s hidden, and then animate it back to its original size. You’ll notice that we are scaling from the top of the zipper, with a duration of 400 milliseconds, and setting the custom easing to easeOutBack.

// Scale Tween
var $scaleElement = $zipper.select('.scale');
var scaleBBox = $scaleElement.getBBox();
$scaleElement.transform('S' + 0 + ' ' + 0 + ' ' + self.getOriginX(scaleBBox, 'center') + ' ' + self.getOriginY(scaleBBox, 'top'));
$scaleElement.animate({transform: 'S' + 1 + ' ' + 1 + ' ' + self.getOriginX(scaleBBox, 'center') + ' ' + self.getOriginY(scaleBBox, 'top')}, 400, mina['easeOutBack']);

Rotation follows the same pattern, with a few complexities. In this case, we have three tweens that play consecutively. When each animation is finished, we use its callback function to play the next animation in queue.

// Rotate Tween
var $rotateElement = $zipper.select('.rotate');
var rotateBBox = $rotateElement.getBBox();
$rotateElement.transform('R' + 45 + ' ' + rotateBBox.cx + ' ' + rotateBBox.cy);

$rotateElement.animate({ transform: 'R' + -60 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeOutBack'], function() {
$rotateElement.animate({ transform: 'R' + 30 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeOutBack'], function() {
  $rotateElement.animate({ transform: 'R' + 0 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeInOutBack']);
});
});

The translate tween mimics both scale and rotation, with one key difference. Because the translate animation doesn’t begin immediately, we use setTimeout to delay the starting time by 400 milliseconds.

// Translate Tween
var $translateElement = $zipper.select('.translate');
$translateElement.transform('T' + 110 + ' ' + 0);

setTimeout(function() {
$translateElement.animate({ transform: 'T' + 0 + ' ' + 0 }, 600, mina['easeOutQuint']);
}, 400);

See the Pen Hard-coded Prototype49 by Michael Ngo (@hellomichael6358545041372) on CodePen6459555142383.

Keyframes Are Key Link

At this point, you might be wondering, “Well, that was fairly complex for such a simple animation.” We wouldn’t disagree.

Our goal was to create a data-driven process to rapidly prototype animations. By creating a separate tween class and introducing the concept of keyframes, we can go from code like this…

// Translate Tween
var $translateElement = $zipper.select('.translate');
$translateElement.transform('T' + 110 + ' ' + 0);

setTimeout(function() {
$translateElement.animate({ transform: 'T' + 0 + ' ' + 0 }, 600, mina['easeOutQuint']);
}, 400);

… to code like this:

// Translate Tween
new svgTween({
element: $zipper.select('.translate'),
keyframes: [
  {
    "step": 2,
    "x": 110,
    "y": 0
  },

  {
    "step": 5,
    "x": 0,
    "y": 0,
    "easing": "easeOutQuint"
  }
],
duration: 2000/10
});

As we subdivide each animation into individual steps, we begin to see how this format might make prototyping easier. Let’s break down the parameters of this translate tween and explain where these numbers come from.

In our original code, you might have noticed that the durations and delays were all divisible by a factor of 200 milliseconds. That wasn’t a coincidence. If the entirety of an animation lasts 2000 milliseconds and consists of 10 steps, we simply need to divide the former by the latter to calculate the duration of a single step. We can now use the same logic to determine why the keyframes start at step 2 and end at step 5. The setTimeout, which lasts 400 milliseconds, corresponds to two steps, the initial delay. Furthermore, the duration of the animation is 600 milliseconds, which is calculated to be three steps, the difference between steps 2 and 5.

svgTween: Translate Link

With the output of our black box defined, let’s write the functionality for the SVGTween class. Using the same pattern as SVGAnimation, we’re able to quickly flesh out a basic scaffold.

/*
svgTween.js v1.0.0
Licensed under the MIT license.
http://www.opensource.org/licenses/mit-license.php

Copyright 2015, Smashing Magazine
http://www.smashingmagazine.com/
http://www.hellomichael.com/
*/

; (function(window) {
'use strict';

var svgTween = function (options) {
  var self = this;
  self.options = extend({}, self.options);
  extend(self.options, options);
  self.init();
};

svgTween.prototype = {
  constructor: svgTween,

  options: {
    element:    null,
    keyframes:  null,
    duration:   null
  },

  init: function () {
    var self = this;
  }
};

/*
  Merges two objects
  @param {Object} a
  @param {Object} b
  @return {Object} sum
  http://stackoverflow.com/questions/11197247/javascript-equivalent-of-jquerys-extend-method
*/
function extend(a, b) {
  for (var key in b) {
    if (b.hasOwnProperty(key)) {
      a[key] = b[key];
    }
  }

  return a;
}

// Add to namespace
window.svgTween = svgTween;
})(window);

Using the same algorithm as before, we’ll set the animation to the first initial hidden state, then animate from there. Instead of using Snap.svg’s transform and animate functions, we’ll rewrite them as resetTween and playTween to handle keyframes instead.

resetTween will accept an element and the keyframes array. The only difference is that, instead of directly setting the values in the transform string, we’ll use the values in the first keyframe.

/*
Resets the animation to the first keyframe

@param {Object} element
@param {Array}  keyframes
*/
resetTween: function (element, keyframes) {
var self = this;

var translateX = keyframes[0].x;
var translateY = keyframes[0].y;

element.transform('T' + translateX + ',' + translateY);
}

Because Snap.svg doesn’t provide chainable animation methods, we’ll have to use callbacks for consecutive animations.

Snap.animation(attr, duration, [easing], [callback]);

However, this instantly becomes unruly if we have more than two keyframes, essentially sending us into a form of callback hell. To handle this problem, we’ll implement playTween as a recursive function, allowing us to loop through animations without necessarily having to nest them.

Let’s start by defining the parameters of our animation. Just as with resetTween, we’ll set the values in our transform string to the keyframe values. Easing is done very much in the same way. Duration is either set to the pause leading up to the first animation or calculated as the span of time between steps.

/*
Recursively loop through keyframes to create pauses or tweens

@param {Object} element
@param {Array}  keyframes
@param {Int}    duration
@param {Int}    index
*/
playTween: function(element, keyframes, duration, index) {
var self = this;

// Set keyframes we’re transitioning to
var translateX = keyframes[index].x;
var translateY = keyframes[index].y;

// Set easing parameter
var easing = mina[keyframes[index].easing];

// Set duration as an initial pause or the difference of steps between keyframes
var newDuration = index ? ((keyframes[index].step - keyframes[(index-1)].step) * duration) : (keyframes[index].step * duration);
}

With the parameters prepared, let’s write conditional statements that pause, play or kill an animation. Our first conditional statement checks whether the animation begins immediately at step 0. If it does, we’ll move on, because the transform function already handles this first keyframe. If we tried to animate to the same values as resetTween, we would sometimes see a brief flicker, a bug that took us ages to find. The next two conditional statements check whether we should delay the animation or begin playing tweens. The one thing to note is the use of nested conditional statements that check whether the recursive function should fire again. Without them, playTween could run indefinitely.

// Play first tween immediately if starts on step 0
if (index === 0 && keyframes[index].step === 0) {
self.playTween(element, keyframes, duration, (index + 1));
}

// Or pause tween if initial keyframe
else if (index === 0 && keyframes[index].step !== 0) {
setTimeout(function() {
  if (index !== (keyframes.length - 1)) {
    self.playTween(element, keyframes, duration, (index + 1));
  }
}, newDuration);
}

// Or animate tweens if keyframes exist
else {
element.animate({
  transform: 'T' + translateX + ' ' + translateY
}, newDuration, easing, function() {
  if (index !== (keyframes.length - 1)) {
    self.playTween(element, keyframes, duration, (index + 1));
  }
});
}

The last step is to update our init function to call resetTween and playTween.

init: function () {
var self = this;

self.resetTween(self.options.element, self.options.keyframes);
self.playTween(self.options.element, self.options.keyframes, self.options.duration, 0);
}

See the Pen svgTween – Translate53 by Michael Ngo (@hellomichael6358545041372) on CodePen6459555142383.

svgTween: Rotation And Scale Link

With our zipper now moving from right to left, it’s time to add rotation and scale to the mix. Let’s amend our options to include type, originX and originY. Because svgTween will now handle all transformations, we’ll include a type variable to specify which one. We’ll also track originX and originY to set the correct transform-origins for scale and rotation. Translation is never affected by transform-origin, so it is always set to center center by default.

options: {
element:    null,
type:       null,
keyframes:  null,
duration:   null,
originX:    null,
originY:    null
}

Let’s update resetTween and playTween to handle these new values. We’ll first check the type and then construct the respective transform strings. We’ll create separate translateX, translateY, rotationAngle, scaleX and scaleY variables, so that it is visually identifiable how our transform strings are generated.

/*
Resets the animation to the first keyframe

@param {Object} element
@param {String} type - "scale", "rotate", "translate"
@param {Array}  keyframes
@param {String} originX - "left", "right", "center"
@param {String} originY - "top", "bottom", "center"
*/
resetTween: function (element, type, keyframes, originX, originY) {
var transform, translateX, translateY, rotationAngle, scaleX, scaleY;

if (type === 'translate') {
  translateX = keyframes[0].x;
  translateY = keyframes[0].y;
  transform = 'T' + translateX + ' ' + translateY;
}

else if (type === 'rotate') {
  rotationAngle = keyframes[0].angle;
  transform = 'R' + rotationAngle + ' ' + originX + ' ' + originY;
}

else if (type === 'scale') {
  scaleX = keyframes[0].x;
  scaleY = keyframes[0].y;
  transform = 'S' + scaleX + ' ' + scaleY + ' ' + originX + ' ' + originY;
}

element.transform(transform);

We’ll mimic the same pattern in playTween, replacing the relevant index from the recursive function. We’ll also update the function calls with the new type, originX and originY parameters.

/*
Recursively loop through keyframes to create pauses or tweens

@param {Object} element
@param {String} type - "scale", "rotate", "translate"
@param {Array}  keyframes
@param {String} originX - "left", "right", "center"
@param {String} originY - "top", "bottom", "center"
@param {Int}    duration
@param {Int}    index
*/
playTween: function(element, type, keyframes, originX, originY, duration, index) {
var self = this;

// Set keyframes we're transitioning to
var transform, translateX, translateY, rotationAngle, scaleX, scaleY;

if (type === 'translate') {
  translateX = keyframes[index].x;
  translateY = keyframes[index].y;
  transform = 'T' + translateX + ' ' + translateY;
}

else if (type === 'rotate') {
  rotationAngle = keyframes[index].angle;
  transform = 'R' + rotationAngle + ' ' + originX + ' ' + originY;
}

else if (type === 'scale') {
  scaleX = keyframes[index].x;
  scaleY = keyframes[index].y;
  transform = 'S' + scaleX + ' ' + scaleY + ' ' + originX + ' ' + originY;
}

// Set easing parameter
var easing = mina[keyframes[index].easing];

// Set duration as an initial pause or the difference of steps between keyframes
var newDuration = index ? ((keyframes[index].step - keyframes[(index-1)].step) * duration) : (keyframes[index].step * duration);

// Skip first tween if animation immediately starts on step 0
if (index === 0 && keyframes[index].step === 0) {
  self.playTween(element, type, keyframes, originX, originY, duration, (index + 1));
}

// Or pause tween if initial keyframe
else if (index === 0 && keyframes[index].step !== 0) {
  setTimeout(function() {
    if (index !== (keyframes.length - 1)) {
      self.playTween(element, type, keyframes, originX, originY, duration, (index + 1));
    }
  }, newDuration);
}

// Or animate tweens if keyframes exist
else {
  element.animate({
    transform: transform
  }, newDuration, easing, function() {
    if (index !== (keyframes.length - 1)) {
      self.playTween(element, type, keyframes, originX, originY, duration, (index + 1));
    }
  });
}
}

Lastly, we’ll update our init function to set type, originX and originY, before calling resetTween and playTween. We can set type simply by adopting the class of the passed-in element. At this point, we can transfer over getOriginX and getOriginY from SVGAnimation. We then use a ternary operator to set our origin, defaulting to center if the values are undefined.

init: function () {
var self = this;

// Set type
self.options.type = self.options.element.node.getAttributeNode('class').value;

// Set bbox to specific transform element (.translate, .scale, .rotate)
var bBox = self.options.element.getBBox();

// Set origin as specified or default to center
self.options.originX = self.options.keyframes[0].cx ? self.getOriginX(bBox, self.options.keyframes[0].cx) : self.getOriginX(bBox, 'center');
self.options.originY = self.options.keyframes[0].cy ? self.getOriginY(bBox, self.options.keyframes[0].cy) : self.getOriginY(bBox, 'center');

// Reset and play tween
self.resetTween(self.options.element, self.options.type, self.options.keyframes, self.options.originX, self.options.originY);
self.playTween(self.options.element, self.options.type, self.options.keyframes, self.options.originX, self.options.originY, self.options.duration, 0);
}

Let’s finalize our zipper animation by instantiating new tweens for both rotation and scale. As with translate, we can calculate the keyframes and duration by the number of steps and overall length of the animation. In reality, we defined all of these parameters much more organically: by viewing the animations as they progressed and constantly fine-tuning the numbers.

// Rotate tween
new svgTween({
element: $zipper.select('.rotate'),
keyframes: [
  {
    "step": 0,
    "angle": 45,
    "cy": "top"
  },

  {
    "step": 2,
    "angle": -60,
    "easing": "easeOutBack"
  },

  {
    "step": 4,
    "angle": 30,
    "easing": "easeOutQuint"
  },

  {
    "step": 6,
    "angle": 0,
    "easing": "easeOutBack"
  }
],
duration: duration
});
// Scale tween
new svgTween({
element: $zipper.select('.scale'),
keyframes: [
  {
    "step": 0,
    "x": 0,
    "y": 0,
    "cy": "top"
  },

  {
    "step": 2,
    "x": 1,
    "y": 1,
    "easing": "easeOutBack"
  }
],
duration: duration
});

See the Pen svgTween: Rotation and Scale57 by Michael Ngo (@hellomichael6358545041372) on CodePen6459555142383.

JSON Config Link

The very last step of our build is to extract the hardcoded values from SVGAnimation and add them to our constructor instead. Let’s add the keyframes, duration and number of steps in the instantiation.

(function() {
var backpack = new svgAnimation({
  canvas:       new Snap('#canvas'),
  svg:          'svg/backpack.svg',
  data:         'json/backpack.json',
  duration:     2000,
  steps:        10
});
})();

By passing in a JSON file to define keyframes, a designer can immediately create a prototype without having to dive into documentation. In fact, this concept could be completely library-agnostic if you replace Snap.svg with GSAP, Mo.js or the Web Animations API.

The JSON file is formatted into separate tweens, consisting of element IDs and keyframes. We include the zipper animation as an example, but the backpack.json file includes arrays for all of the elements (zipper, pockets, logo, etc.).

{
"animations": [
  {
    "id": "#zipper",
    "keyframes": {
      "translateKeyframes": [
        {
          "step": 6,
          "x": 110,
          "y": 0
        },

        {
          "step": 9,
          "x": 0,
          "y": 0,
          "easing": "easeOutQuint"
        }
      ],

      "rotateKeyframes": [
        {
          "step": 4,
          "angle": 45,
          "cy": "top"
        },

        {
          "step": 6,
          "angle": -60,
          "easing": "easeOutBack"
        },

        {
          "step": 8,
          "angle": 30,
          "easing": "easeOutQuint"
        },

        {
          "step": 10,
          "angle": 0,
          "easing": "easeOutBack"
        }
      ],

      "scaleKeyframes": [
        {
          "step": 4,
          "x": 0,
          "y": 0,
          "cy": "top"
        },

        {
          "step": 6,
          "x": 1,
          "y": 1,
          "easing": "easeOutBack"
        }
      ]
    }
  }
]
}
options: {
data:                 null,
canvas:               null,
svg:                  null,
duration:             null,
steps:                null
}

The details of how to load a JSON file61 are beyond the scope of this article, but what’s significant is the use of a callback function to return the JSON data for future use — in our case, passing the animations array to loadSVG.

/*
Get JSON data and populate options
@param {Object}   data
@param {Function} callback
*/
loadJSON: function(data, callback) {
var self = this;

// XML request
var xobj = new XMLHttpRequest();
xobj.open('GET', data, true);

xobj.onreadystatechange = function() {
  // Success
  if (xobj.readyState === 4 && xobj.status === 200) {
    var json = JSON.parse(xobj.responseText);

    if (callback && typeof(callback) === "function") {
      callback(json);
    }
  }
};

xobj.send(null);
}

We’re now able to update loadSVG to loop through our animations array, creating svgTweens dynamically. If any of translateKeyframes, rotateKeyframes or scaleKeyframes are defined, we instantiate a new svgTween, passing in the keyframes and duration from our options file.

loadSVG: function(canvas, svg, animations, duration) {
var self = this;

Snap.load(svg, function(data) {
  // Placed SVG into the DOM
  canvas.append(data);

  // Create tweens for each animation
  animations.forEach(function(animation) {
    var element = canvas.select(animation.id);

    // Create scale, rotate and transform groups around an SVG node
    self.createTransformGroup(element);

    // Create tween based on keyframes
    if (animation.keyframes.translateKeyframes) {
      self.options.tweens.push(new svgTween({
        element: element.select('.translate'),
        keyframes: animation.keyframes.translateKeyframes,
        duration: duration
      }));
    }

    if (animation.keyframes.rotateKeyframes) {
      self.options.tweens.push(new svgTween({
        element: element.select('.rotate'),
        keyframes: animation.keyframes.rotateKeyframes,
        duration: duration
      }));
    }

    if (animation.keyframes.scaleKeyframes) {
      self.options.tweens.push(new svgTween({
        element: element.select('.scale'),
        keyframes: animation.keyframes.scaleKeyframes,
        duration: duration
      }));
    }
  });
});
}

Finally, we update our init function to call loadJSON, which in turn calls loadSVG, finishing our tutorial for good.

init: function() {
var self = this;

self.loadJSON(self.options.data, function (data) {
  self.loadSVG(self.options.canvas, self.options.svg, data.animations, (self.options.duration/self.options.steps));
});
}

See the Pen JSON Config62 by Michael Ngo (@hellomichael6358545041372) on CodePen6459555142383.

A Note On Performance Link

Our goal was to see how far we could push SVG animation; so, we favored animation fidelity over performance. We stand by this because it enabled us to push our animations much further than anticipated. However, we didn’t ignore performance completely.

Looking at the Chrome DevTools timeline, we see that the animation plays at a steady 60 frames per second, with a few hiccups in between. If we break down the backpack animation, there are 19 elements with 3 possible transforms. That means, at worst, there are 57 possible tweens happening at once. Fortunately, this isn’t the case because the tweens are staggered over the lifetime of the animation. We can visually see this in the CPU graph, as its usage steadily ramps up, peaks where the animations overlap the most, and then diminishes as each tween ends. Visually, Firefox and Internet Explorer were able to play the animations with no discernible differences in performance.

desktop performance65
Figure 10. The Chrome DevTools timeline, showing CPU usage and the frame rate for desktop.

As expected, mobile devices took a performance hit. Using remote debugging on an old Android device, our frame rate dropped from 60 per second, hovering between 30 and 60. Though not perfect, we felt this was more than satisfactory for our needs. There is a silver lining, though, because our latest tests on an iPhone 5 and iPhone 6 performed flawlessly.

mobile performance66
Figure 11. Remote debugging on Android, showing weaker performance on mobile.

What’s Next? Link

Unfortunately, the campaign was dropped before completion, so we never had a chance to dive deeper into the project. As is, the source code provided isn’t quite production-ready; we would have liked to have addressed a few key issues.

Event-Driven Link

Our Codepen embed provides a “rerun” button, but our implementation isn’t event-driven. Ideally, the animations wouldn’t immediately play back until initiated via some type of interaction (mouse click, waypoint, etc.).

Mobile Devices Link

While these animations do run on mobile devices, as mentioned, they are processor-heavy. So, consider their importance in the overall design of your project. Performance and file size could be saved significantly by excluding them. If they’re an absolute necessity, consider further how they could be made responsive for mobile viewports.

Fallbacks Link

The solution for our animations works in all modern browsers and has been tested in Internet Explorer 9+, Firefox and Chrome. This is primarily due to Snap.svg support. If your project requires the use of older browsers, you could try using Snap.svg’s predecessor, Raphael. The more accessible approach is progressive enhancement, serving a static SVG initially and then adding animation for those with capable browsers.

Signing Off Link

Well, there you have it, from simple illustration to complex animation. You can download the entire code base on GitHub67.

Backpack Full Animation68

Last but not least, a big thank you to Rey Bango of the Smashing Magazine team, Chris Halaska694 for the amazing illustrations, Matt Harwood70 for the code review, and Rhiana Chan for the much-needed editing.

(rb, ml, al, il)

Footnotes Link

  1. 1 http://codepen.io/hellomichael/pen/QNgZdx/
  2. 2 http://codepen.io/hellomichael
  3. 3 http://codepen.io
  4. 4 http://www.chrishalaska.com/
  5. 5 https://vimeo.com/41336551
  6. 6 https://www.smashingmagazine.com/2014/03/rethinking-responsive-svg/
  7. 7 https://www.smashingmagazine.com/2015/03/different-ways-to-use-svg-sprites-in-animation/
  8. 8 https://www.smashingmagazine.com/2015/09/creating-cel-animations-with-svg/
  9. 9 https://www.smashingmagazine.com/2011/09/the-guide-to-css-animation-principles-and-examples/
  10. 10 https://en.wikipedia.org/wiki/Disney_Animation:_The_Illusion_of_Life
  11. 11 https://www.smashingmagazine.com/wp-content/uploads/2016/07/02-transformations-opt.jpg
  12. 12 https://www.smashingmagazine.com/wp-content/uploads/2016/07/03-timing-opt.jpg
  13. 13 https://www.smashingmagazine.com/wp-content/uploads/2016/07/04-easing-opt.gif
  14. 14 https://www.sketchapp.com/
  15. 15 https://inkscape.org/en/
  16. 16 https://www.smashingmagazine.com/wp-content/uploads/2016/07/05-breakdown-opt.jpg
  17. 17 https://www.smashingmagazine.com/wp-content/uploads/2016/07/06-illustrator-layers-opt.jpg
  18. 18 #svg-transform-groups
  19. 19 https://www.smashingmagazine.com/wp-content/uploads/2016/07/07-illustrator-exporting-opt.jpg
  20. 20 https://www.smashingmagazine.com/wp-content/uploads/2016/07/08-clipPath-opt.gif
  21. 21 https://bugzilla.mozilla.org/show_bug.cgi?id=923193
  22. 22 http://stackoverflow.com/questions/24302615/css3-animation-is-not-working
  23. 23 http://julian.com/research/velocity/
  24. 24 http://greensock.com/gsap
  25. 25 https://twitter.com/ChrisGannon/status/757997386035789824
  26. 26 http://snapsvg.io/
  27. 27 http://raphaeljs.com/
  28. 28 http://mojs.io/
  29. 29 http://anime-js.com
  30. 30 https://github.com/web-animations/web-animations-js
  31. 31 https://github.com/overjase/snap-easing/blob/master/README
  32. 32 https://www.smashingmagazine.com/wp-content/uploads/2016/07/09-folder-structure-opt.jpg
  33. 33 https://github.com/hellomichael/svgAnimation/tree/hello-world
  34. 34 http://snapsvg.io/docs/#Snap.load
  35. 35 https://github.com/hellomichael/svgAnimation/tree/plugin
  36. 36 http://codepen.io/hellomichael/pen/aZJMmr/
  37. 37 http://codepen.io/hellomichael
  38. 38 http://codepen.io
  39. 39 https://github.com/hellomichael/svgAnimation/tree/options
  40. 40 http://codepen.io/hellomichael/pen/oLZVYg/
  41. 41 http://codepen.io/hellomichael
  42. 42 http://codepen.io
  43. 43 https://github.com/hellomichael/svgAnimation/tree/hard-coded
  44. 44 http://snapsvg.io/docs/#Paper.g
  45. 45 http://snapsvg.io/docs/#Element.transform
  46. 46 http://snapsvg.io/docs/#Snap.animate
  47. 47 http://raphaeljs.com/reference.html#Element.transform
  48. 48 http://snapsvg.io/docs/#Element.getBBox
  49. 49 http://codepen.io/hellomichael/pen/KMWENR/
  50. 50 http://codepen.io/hellomichael
  51. 51 http://codepen.io
  52. 52 https://github.com/hellomichael/svgAnimation/tree/tween-translate
  53. 53 http://codepen.io/hellomichael/pen/jrBJVz/
  54. 54 http://codepen.io/hellomichael
  55. 55 http://codepen.io
  56. 56 https://github.com/hellomichael/svgAnimation/tree/tween-rotate-scale
  57. 57 http://codepen.io/hellomichael/pen/ezvXBQ/
  58. 58 http://codepen.io/hellomichael
  59. 59 http://codepen.io
  60. 60 https://github.com/hellomichael/svgAnimation/tree/json
  61. 61 https://codepen.io/KryptoniteDove/post/load-json-file-locally-using-pure-javascript
  62. 62 http://codepen.io/hellomichael/pen/xOqBRo/
  63. 63 http://codepen.io/hellomichael
  64. 64 http://codepen.io
  65. 65 https://www.smashingmagazine.com/wp-content/uploads/2016/07/10-desktop-performance-opt.jpg
  66. 66 https://www.smashingmagazine.com/wp-content/uploads/2016/07/11-mobile-performance-opt.jpg
  67. 67 https://github.com/hellomichael/SVGAnimation
  68. 68 https://www.smashingmagazine.com/wp-content/uploads/2016/07/backpack-animation.gif
  69. 69 http://www.chrishalaska.com/
  70. 70 http://www.morningharwood.com/

↑ Back to top Tweet itShare on Facebook

Michael Ngo is an award winning front-end developer working across Sydney and Vancouver. He's been nominated for a Webby, an AGDA design award, and his portfolio has been featured on Awwwards as the "Site of the Day". His only goal, to make the web more beautiful, one pixel at a time.

  1. 1

    Timothy Robb

    July 26, 2016 12:59 pm

    Thanks for this article. I have been involved in the development of solutions for the conversion of Flash-based interactive/animated e-learning into HTML5 and have been looking for ways to gracefully convert some of the more complex interactive material. Most content can be screen captured into MP4 but interactive content needs an approach that allows for animation, scalability, and touch/mouse events.

    Most interactive animated content is being converted using Adobe Animate CC, but I have some content that I hope to make a simpler conversion process.

    This article enlightened me to some features that might help with the conversion process of interactive drag and drop questions. SVG just might save a huge amount of time and simplify some of the challenges that I have come across.

    1
    • 2

      Use Google Swiffy to convert Flash animations to HTML5.

      -1
    • 4

      Michael Ngo

      July 27, 2016 8:11 am

      Thanks for read Timothy. I’ve been excited about SVG for awhile, even more so after this talk by Dmitry Baranovskiy. He makes a great point that without designers/developers actually trying to make cool things with SVG, it’s adoption would stagnate. It’s definitely awesome to see the opposite happen as of late though.

      https://www.youtube.com/watch?v=SeLOt_BRAqc&t=35m45s

      2
    • 5

      You might want to have a look at Spine. Our game designer uses this for converting old Flash games animations into interactive HTML5.

      0
  2. 6

    Trevor Goodchild

    July 26, 2016 3:17 pm

    Love the presentation. Funny how we’ve come full circle and all the Flash haters are now forced to admit that the real reason they hated Flash is because they have no imagination. Now watch all the wise usability experts start producing thin excuses for not using animation because they can’t use “its not open source” anymore.

    2
  3. 7

    Way too much JS coding. It would be much easier to define animations declaratively, using SVG Animation or Vivus (https://github.com/maxwellito/vivus). Or animate HTML elements over a timeline in Google Web Designer.

    0
    • 8

      Michael Ngo

      July 27, 2016 7:45 am

      Hey Adam,

      Wouldn’t disagree that there was a lot of JS, but we were hoping that it would reduce the amount of time needed to create the animations.

      I’m not sure how Vivus or Google Web Designer could be used for this type of animation though, care to elaborate? Our recommendation would definitely be GSAP moving forward, they’ve just got everything covered in terms of SVG animation.

      1
      • 9

        Correct. Way too many people in this comment section suggesting things without any follow up as to why they’re better suited to these types of animations. GSAP is an excellent solution.

        1
  4. 10

    I noticed you have drawn twice the same strap and its individual elements. Was there any reason why you would not use

    An animated case

    This way you can draw generic elements (collected in defs) and position them at the place of your choice.

    0
  5. 11

    Ok let’s put this on codepen. :)

    position of svg element

    0
    • 12

      Michael Ngo

      July 27, 2016 7:30 am

      Hey Karl,

      We never had a chance to further optimise the animations, but your idea to reuse elements in the SVG would definitely save a few kb.

      0
  6. 13

    “GSAP was an obvious contender — save for one issue: It was overkill. Importing TweenMax and TimelineMax immediately doubled the size of our project and proved to be excessive.”

    Just for information :
    snap.svg-min.js : 77 kb

    TweenMax : 36kb
    TimelineMax : 6kb

    4
    • 14

      Yeah, not to mention this seems way more complicated then it would be with gsap. It’s a shame the author dismissed it because of a misunderstanding on their part.

      0
  7. 15

    There is another way to generate SVG animations not covered in this article, that requires no coding knowledge and doesn’t rely on javascript.

    Check out the Flash2Svg tool:
    http://www.tbyrne.org/viber-animated-svgs

    -3
  8. 17

    This is a project I made by SVG animation: http://five.sutunam.dev5.sutunam.com

    It’s still in the development phase. SVG seems to be a great topic these days.

    3
  9. 19

    Hi Michael,
    It is amazing, thanks for sharing!
    but I noticed that there is a typo in this article:
    Adding Options

    
    /*
    Loads the SVG into the DOM
    @param {Object}   canvas
    @param {String}   svg
    */
    loadSVG: function(canvas, data) {
    Snap.load(svg, function(data) {
      canvas.append(svg);
    });
    }
    

    The loadSVG arguments seem should be canvas and svg.

    0
  10. 20

    Thanks so much guys – love it. Definitely agree, our designers struggle to code and our developers struggle to art direct!

    We’ve just set up a basic animation using one of the icons from our site – liquid.com.au – and it’s already proving fruitful!

    Here’s the demo: http://codepen.io/liquid/full/grjyqz/

    Thanks again!

    1
  11. 22

    There seems to be a typo in the loadSVG function right before the last codepen demo.

    self.options.tweens.push(new svgTween({
            element: element.select('.translate'),
            keyframes: animation.keyframes.translateKeyframes,
            duration: duration
          }));

    The options object doesn’t have a tween array to push new tweens to just new svgTween(...) works though. Thanks again writing this.

    0
  12. 23

    Hi Michael Ngo!

    Awesome post, really useful! Have you heard about Rawpixel? You’ve probably seen our images all over the place.

    ddWe’re the leading stock photo contributor in the world. We’ve just launched our website where we give away the best free design resources out there. We’d be stoked if you could add us to this list. Check us out on http://www.rawpixel.com. Looking forward to you joining our community of creatives. BE INSPIRED. BE RAW. Nica

    -2
  13. 24

    Can you share how much time did you spend on getting the animation done?

    0
    • 25

      Hey Laura, setting up an initial animation only takes about 10 minutes. You can get away with hiding all elements and staggering the animations to get a pretty decent effect. However, this animation took a couple hours to finesse. I would recommend putting some thought into that one element of an animation that really defines that particular illustration (e.g. the zipper in the backpack).

      0
  14. 26

    You might also enjoy some of these RxJS/SVG graphics/animation code samples for frivolous fun:
    https://github.com/ocampesato/rxjs-svg-graphics

    0
  15. 27

    hi! any simple way to make the animations loop rather than play once? thanks.

    1

↑ Back to top