Snap Animation States

Avatar of Briant Diehl
Briant Diehl on

There are many ways to make icons for a website. Inline SVG is scalable, easy to modify with CSS, and can even be animated. If you’re interested in learning more about the merits of using inline SVG, I recommend reading Inline SVG vs Icon Fonts. With ever increasing browser support, there’s never been a better time to start working with SVGs. Snap Animation States is a JavaScript plugin built around Snap.svg to help create and extend icon libraries with scaleable, editable SVG icons. Snap Animation States makes it easy to load and animate those SVGs with a simple schema.

Getting Started

Lets start with a basic SVG hamburger menu. This one was made using Affinity Designer, but there are many other free (Inkscape) and paid for (Adobe Illustrator) options available for making vector images.

<svg width="100%" height="100%" viewBox="0 0 65 60" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:square;stroke-miterlimit:1.5;"  fill="none" stroke="#000" stroke-width="10">
   <g>
      <path class="hamburger-top" d="m 5,10 55,0" />
      <path class="hamburger-middle" d="m 5,30 55,0" />
   </g>
   <path class="hamburger-bottom" d="m 5,50 55,0" />
</svg>

Although this is a pretty basic SVG, it still takes up multiple lines in an HTML document. This can be a pain to write if you want to use the SVG in multiple locations across several different web pages. And what if you have to modify your SVG? Then you’re scrambling to remember all the places you used the SVG so you can update it. It’s not very clean or reusable. This is what Snap Animation States is all about solving.

Let’s continue to use the same SVG, but this time we’ll use the plugin to load it to the DOM. The schema for the plugin requires at least two properties: selector: ".some-css-selector", and svg: "svg string". Check out the following demo:

See the Pen Lydgoo by Briant Diehl (@bkdiehl) on CodePen.

You’ll notice in the Pen above that I call icon-hamburger just like I would call a font icon. Just remember that the selector property needs a CSS selector as its value.

There’s more we can do since this plugin is an extension of Snap.svg, a JavaScript library used to create and animate SVGs. So let’s see what’s needed to give this hamburger icon some basic animation.

When I created my SVG, I added classes to the elements I knew I would be animating.

<g>
   <path class="hamburger-top" d="m 5,10 55,0" />
   <path class="hamburger-middle" d="m 5,30 55,0" />
</g>
<path class="hamburger-bottom" d="m 5,50 55,0" />

In my schema, I can start to include the properties needed for animation, and I start by giving it transitionTime: 250. The transition time is applied to each step in the transform chain and can be overridden later by an individual transform.

Now it’s time to include my animation states. I start by setting the property states:{}. The property names for this object should correlate to the state the animation will lead to. In this case I’m going to name my properties open and closed. The property values for this object are arrays of transform objects. So far, the additions to the schema should look like this:

transitionTime: 250,
states: {
  open:[],
  closed: []
}

Next, we need to include the transform objects that define how the SVG elements are to be transformed.

open:[
  { id: "top-lower", element: ".hamburger-top", y:20 },
  { id: "bottom-raise", element: ".hamburger-bottom", y:-20 },
  { waitFor: "top-lower", element: "g", r:45 },
  { waitFor: "bottom-raise", element: ".hamburger-bottom", r:-45},
]

Each transform object has either an id, a waitFor, or a combination of the two. Each id needs to be unique. Objects with an id represent a link in a chain of animations. waitFor always needs to reference a link id that precedes it. In this case there’s an object with id:"top-lower" and an object with waitFor:"top-lower". When the animation starts, id:top-lower will be the first link in the chain, and it will run for 250ms. When it has finished, waitFor:"top-lower" will run for 250ms.

Every transform object must reference an element. The element value can be either a css selector or a direct element reference. For example, one element property has the value of "g" referencing the <g> element in the SVG, while another has the value of ".hamburger-bottom", referencing the class I added to the <path> element.

Now that we know the order of animation and the elements that need to be transformed, we just need to define the transform objects. For those of you unfamiliar with how SVG transforms work, you could start with Transforms on SVG Elements. Otherwise, simply put, imagine that the SVG element you are manipulating starts at the points [0, 0] on an x/y axis. Something else to remember is that x goes left to right, while y goes from top to bottom. In the example above, we see:

{ id: "top-lower", element: ".hamburger-top", y:20 },

This transform object is referring to the top line of the hamburger menu. y: 20 is telling the plugin that, starting at the element’s point of origin [0, 0], I want to move the top line down 20px. The reverse is true for:

{ id: "bottom-raise", element: ".hamburger-bottom", y:-20 },

Here I’m telling the plugin to move my element up by 20px. The same principle applies to the rotations:

{ waitFor: "top-lower", element: "g", r:45 },
{ waitFor: "bottom-raise", element: ".hamburger-bottom", r:-45}

The element that’s being rotated starts at a rotation of 0 degrees. r: 45 is telling the plugin to rotate from 0 degrees to 45 degrees and vice versa for r: -45.

Our second state in the states object looks like this:

closed: [
  { id: "top-angle", element: "g", r: 0 },
  { id: "bottom-angle", element: ".hamburger-bottom", r: 0 },                   
  { waitFor: "top-angle", element: ".hamburger-top", y: 0 },
  { waitFor: "bottom-angle", element: ".hamburger-bottom", y: 0 }
]

You’ll notice that for all of the elements being transformed, their y and r values are set to 0. This is because this state’s purpose is to return the SVG elements to their original state. Since 0 was the point of origin, we’re just performing a transform on each of the elements that will return them to their origin.

We’re almost done. Now that the animation states are defined, I have to decide what’s going to initiate these animations. This requires one more property on the schema: events. events takes an array of objects, since you may want to initiate your animation with more than one event. For the hamburger icon, it’s going to look like this:

events: [
  { event: "click", state: ["open", "closed"] }
]

The object in the array may include the following properties:

  1. event: designed to listen for javascript events. The hamburger icon listens for a ‘click’ event on <i class="icon-hamburger"</i> since that’s what the selector in the schema references.
  2. state: takes either a string or an array. If the state here is "open", then when <i class="icon-hamburger"></i> is clicked, only the “open” animation would run on a click event. Since the value of state is an array, the click event will actually toggle between the “open” and “closed” animations. The array is only designed to take two values and enable toggling.
  3. The last property is optional: selector. By default this value is your schema selector + “animate”. In this case it would be icon-hamburger-animate. You can change the selector if you want by declaring it here. It’s purpose is to enable the javascript events to be tied to either a parent or sibling element of your SVG. For example, if I had an SVG that I wanted to animate inside of a button when the button was clicked, I would need to do this:
<button class="icon-hamburger-animate">
  <i class="icon-hamburger"></i>
</button>

Whew, we made it. Now it’s time to see the final product.

See the Pen bWwQJZ by Briant Diehl (@bkdiehl) on CodePen.

Was it worth it? You may be thinking, that’s a lot of work to just have a single icon. And I would agree with you. Which is why I created a Gulp plugin to help with the heavy lifting.

Gulp Animation States

So far, we have a single icon that we can use wherever I’ve included the schema. Ideally, the schema for icon-hamburger would be saved to a js file that gets bundled up and included site-wide, which means that I could call icon-hamburger wherever I want. What if this js file was autogenerated and contained the schema and plugin call for as many SVG icons as you had access to? You could have easy access library of SVG icons! That’s the purpose of Gulp Animation States. Make sure to check out the documentation here.

Let’s begin with the file structure. Say I went to IcoMoon and generated all the SVG files I needed for my new project. I would want to drop all these newly generated files into a folder in my project. Let’s call that folder `svg`. My file structure would look something like this:

svg
|-- icon-folder.svg
|-- icon-hamburger.svg
|-- icon-mic.svg
|-- icon-wall.svg
|-- icon-wrench.svg

Using Gulp Animation States I can combine all the SVG files in my `svg` folder into a single js file with the selector for each icon set according to the file name of the SVG. The file contents would look something like this:

var iconFolder = {"selector": ".icon-folder","svg": "<svg>Content</svg>"};
SnapStates(iconFolder);
var iconHamburger= {"selector": ".icon-hamburger","svg": "<svg>Content</svg>"};
SnapStates(iconHamburger);
var iconMic= {"selector": ".icon-mic","svg": "<svg>Content</svg>"};
SnapStates(iconMic);
var iconWall= {"selector": ".icon-wall","svg": "<svg>Content</svg>"};
SnapStates(iconWall);
var iconWrench= {"selector": ".icon-wrench","svg": "<svg>Content</svg>"};
SnapStates(iconWrench);

This file could be bundled up with the rest of a website’s key JavaScript, enabling SVG icon usage wherever it’s wanted. But what about the animations? How do they get included in this JavaScript file?

We already have the animation for the hamburger icon, so we’ll use that. In the `svg` folder, we need to create a new file called `icon-hamburger.js`. Note that it has the same name as it’s corresponding SVG file. Here is the new file structure:

svg
|-- icon-folder.svg
|-- icon-hamburger.svg
|-- icon-hamburger.js
|-- icon-mic.svg
|-- icon-wall.svg
|-- icon-wrench.svg

And the contents of `icon-hamburger.js` would be:

{
  transitionTime: 250,
  states: {
    open:[
      { id: "top-lower", element: ".hamburger-top", y:20 },
      { id: "bottom-raise", element: ".hamburger-bottom", y:-20 },
      { waitFor: "top-lower", element: "g", r:45 },
      { waitFor: "top-lower", element: ".hamburger-bottom", r:-45},
    ],
    closed: [
      { id: "top-angle", element: "g", r: 0 },
      { id: "bottom-angle", element: ".hamburger-bottom", r: 0 },                   
      { waitFor: "top-angle", element: ".hamburger-top", y: 0 },
      { waitFor: "bottom-angle", element: ".hamburger-bottom", y: 0 },
    ]
  },
  events: [
    { event: "click", state: ["open", "closed"] }
  ]
}

The Gulp plugin will look for js files with the same name as the SVG file it’s creating a schema for. Demonstrating the output again with the animation states:

var iconFolder = {"selector": ".icon-folder","svg": "<svg>Content</svg>"};
SnapStates(iconFolder);
var iconHamburger= {"selector": ".icon-hamburger","svg": "<svg>Content</svg>", "transitionTime":250,"states":{"open":[{"id":"top-lower","element":".hamburger-top","y":20},{"id":"bottom-raise","element":".hamburger-bottom","y":-20},{"waitFor":"top-lower","element":"g","r":45},{"waitFor":"top-lower","element":".hamburger-bottom","r":-45}],"closed":[{"id":"top-angle","element":"g","r":0},{"id":"bottom-angle","element":".hamburger-bottom","r":0},{"waitFor":"top-angle","element":".hamburger-top","y":0},{"waitFor":"bottom-angle","element":".hamburger-bottom","y":0}]},"events":[{"event":"click","state":["open","closed"]}};
SnapStates(iconHamburger);
var iconMic= {"selector": ".icon-mic","svg": "<svg>Content</svg>"};
SnapStates(iconMic);
var iconWall= {"selector": ".icon-wall","svg": "<svg>Content</svg>"};
SnapStates(iconWall);
var iconWrench= {"selector": ".icon-wrench","svg": "<svg>Content</svg>"};
SnapStates(iconWrench);

Using Gulp Animation States, you manage to retain smaller, bite-sized files that you can easily edit when you need to change something. Those bite-sized pieces compile nicely into a single file that can be bundled with other key components of your site, allowing quick and easy calls to include an SVG in your HTML document.

Further Examples

The hamburger icon was fairly simple, so let’s look at a few more complex icons. We’ll start with a speaker icon.

See the Pen WjoOoy by Briant Diehl (@bkdiehl) on CodePen.

You’ll notice that, overall, the schema is mostly the same. You’ll notice the property easing is new. easing has a default value of easeinout. Besides that, the only changes worth noticing are in my transform objects.

{ id: "waveline1", element: ".wave-line-1", x:-10, s:0.1, attr:{ opacity:.8 }, transitionTime: 250 },
{ id: "waveline2", element: ".wave-line-2", x:-16, s:0.1, attr:{ opacity:.8 }, transitionTime: 300 },
{ id: "waveline3", element: ".wave-line-3", x:-22, s:0.1, attr:{ opacity:.8 }, transitionTime: 350 }

s is for scale, and just like in css, an object’s scale always starts at 1. The attr property allows you to modify any attribute on an SVG element, in this case, the opacity. Finally, remember in the beginning of the article how I mentioned that transitionTime can be overridden by an individual transform? Well, here is how it’s done. I didn’t even declare transitionTime in the main schema. That’s because I wanted each transform to have a unique transition time.

Next, let’s look at a line drawing animation.

See the Pen OmbxVV by Briant Diehl (@bkdiehl) on CodePen.

The first major difference I want you to see is that I’m not declaring svg in the schema. The SVG is inside the <i class="icon-new-document"></i>. This is mostly for demo purposes, so as not to bloat the schema that I want you to be viewing. However, the plugin does allow for this functionality. The use case is for those users who only have a few SVG icons that they need in their document and don’t want to use the gulp plugin.

The transform objects are what I really want to focus on here. There’s a lot of new stuff going on here.

{ id: 'line1-init', element: ".new-document-line1", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },        
{ id: 'line2-init', element: ".new-document-line2", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ id: 'line3-init', element: ".new-document-line3", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ id: 'line4-init', element: ".new-document-line4", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ id: 'line5-init', element: ".new-document-line5", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ waitFor: 'line1-init', element: ".new-document-line1", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line2-init', element: ".new-document-line2", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line3-init', element: ".new-document-line3", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line4-init', element: ".new-document-line4", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line5-init', element: ".new-document-line5", drawPath: 100, transitionTime: { min: 500, max: 1000 } },

If you’ve looked at the Pen, you’ll have noticed that hovering over the new document icon causes the lines to shrink and grow. Each of those lines is a path, and a path can be drawn. The first transform object above includes drawPath. drawpath takes either a number or an object with properties min and max. The number represents a percentage. Let’s say that the transform object had drawPath: 0. That would mean that I want the current path to be drawn to 0% of its length. The transform object really has drawPath: { min: 25, max: 75 }. When the value of drawpath is an object, I’m telling my plugin that I want the path to be drawn to a random percentage between the min and the max. In this case it would be a random number between 25 and 75. If you hover over the icon again, you can see that the line length changes each time the animation occurs. The same principle of setting a random number with min and max applies to transitionTime.

The last newcomer to this animation schema is repeat. repeat takes an object with four valid properties.

  1. loop: takes a Boolean value. If set to true, the animation and all of the transforms further down the chain will repeat until told otherwise. In order to break out of a loop you must either set a loopDuration or change to another animation state.
  2. loopDuration: takes an integer. If I set loop to true and loopDuration to 5000, then the animation chain will repeat itself for 5000ms. If the duration of the animation loop isn’t exactly 5000ms, then the loop will continue its final animation past the set time.
  3. times: takes an integer. If I set times to 2, then my animation will run a total of 3 times. Once because the animation always runs at least once and then 2 more times.
  4. delay: takes an integer. Represents the amount of time you want between the end of the animation and the beginning of the repeat loop.

Next, I want to illustrate a longer animation chain.

See the Pen KmNXdW by Briant Diehl (@bkdiehl) on CodePen.

Take a look at the shake state. The first and last transform objects have either an id or a waitFor property. Every other transform object has both an id and a waitFor property.

shake: [
  { id: "shake-right", element: '.wrench', r: 10 },
  { id: "shake-left", waitFor: 'shake-right', element: '.wrench', r: -10 },
  { id: "back-to-right", waitFor: 'shake-left', element: '.wrench', r: 10 },
  { id: "back-to-left", waitFor: 'back-to-right', element: '.wrench', r: -10 },
  { waitFor: 'back-to-left', element: '.wrench', r: 0 }
]

Each of the three middle transform objects is referencing the id of the preceding transform object with their waitFor. The first animation starts a chain that leads to the reset value r:0 at the very end.

Finally, I want to demonstrate how we would draw lines by setting stroke-dashoffset and stroke-dasharray.

See the Pen rmWzyW by Briant Diehl (@bkdiehl) on CodePen.

First, I want you to notice that on many of my path elements that I include stroke-dashoffset:1000; stroke-dasharray:1000, 1000;

<path class="right-upper-branch" d="M45.998,21.196C43.207,23.292 44.195,27.857 47.629,28.59C48.006,28.671 48.399,28.699 48.784,28.672C49.659,28.611 50.276,28.34 50.994,27.849C51.413,27.563 51.839,27.05 52.092,26.616C53.906,23.507 50.981,19.611 47.489,20.486C46.946,20.622 46.446,20.86 45.998,21.196L41.015,14.571" style="fill:none;stroke:#fff;stroke-width:1.7px;stroke-dashoffset:1000; stroke-dasharray:1000, 1000;"/>

For a more detailed explanation of stroke-dasharray, view stroke-dasharray. For my purposes, let’s say that stroke-dasharray is basically setting the length of the path that I don’t want to show. Now, my path certainly isn’t 1000px long. It’s a bit of an exaggeration, but it’s an exaggeration that makes sure that no portion of my path is shown prematurely. The path is drawn to completion in the following transform.

{ id:["right-upper-branch", 600], element: ".right-upper-branch", drawPath:100  },

When I set drawPath back to 0 it will adjust the stroke-dasharray and stroke-dashoffset accordingly. The last thing I want to point out about this line is the id. It’s an array instead of a string. The first value in the array is the name of the id. The second value will always be an integer representing a timeout. If I only used this transform object in the animation I would only see a path being drawn 600ms after the mouseover event.

For more examples and further documentation you can check out my demo page.

Conclusion

There may be many of you that are still on the fence about whether switching your icon system to something new is a good thing. There are pros and cons to the different icon systems currently available. I’ve tried to create a simple way for you to make the move to SVG icons. I hope you find it useful.