Smashing Newsletter

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.
This project began as a simple thought experiment: How far can we push SVG animation? At the time, designer Chris Halaska 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.

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

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 Halaska 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 Collection,” 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:

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

In The Illusion of Life, 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

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).

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

Timing

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.

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

Easing

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.

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

Preparing the Assets

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

Figure 5. A breakdown of the elements that make up the final animation.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 groups, explained later on.

<g id="zipper">
<path fill="#272C40" d="…"/>
</g>

Figure 7. The SVG export settings used. We unchecked “responsive” because the animation units are pixel-based.

Handling Masks

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.

Figure 8. The use of clipPath to hide the belt straps before animation.

Prototype, Prototype, Prototype

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

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 SVGs, while Internet Explorer’s support for SVG CSS animation is completely non-existent. 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.js. Because the animation engine also uses CSS transformations, the Firefox and Internet Explorer issues remained unresolved.

GSAP

GSAP 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 Gannon let us know that TimelineMax is included in TweenMax and combined is only 37kb, a misunderstanding on our end.

Snap.svg

In our final attempt, we used Snap.svg, the successor to Raphael. 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

Since writing this article, three very promising SVG animation libraries have been gaining traction in the community: Mo.js, Anime and the Web Animations API. 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

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 functions for later use.

Figure 9. The final folder structure of our project. The “Hello world” scaffold begins with just the highlighted files.

Hello World

“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 function to indicate the external SVG source and an anonymous callback that will append the nodes to the DOM tree.

Making A Simple Plugin

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.

Objects as Parameters

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.

Merging Objects

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.

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.

Hardcoded Prototype

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.

A Snap.svg Animation

We’re finally ready to animate our first element. Snap.svg provides two functions to do this: transform and animate. 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 documentation 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.

Calculating Origins

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 getBBox, 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.

Animation in Practice

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.

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.

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.

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.

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.

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.

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.

svgTween: Rotation And Scale

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.

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.

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.

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.

JSON Config

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.

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.).

The details of how to load a JSON file 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.

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.

A Note On Performance

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.

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.

What’s Next?

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.

Mobile Devices

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

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

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

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