Stefan Baumgartner

Web ops, performance and front-end

Basic SVG path tweening with SMIL

16 July 2013 by @ddprrt | Posted in: SVG

Sorry, your browser does not support SVG animations with SMIL.

I'm working on a tribute to one of my childhood heroes, the Caped Crusader, the Dark Knight, the world's greatest detective: Batman. And when I say childhood hero, I do mean a hero to this day. Anyhow, inspired by an EPS file i got over on DeviantArt, I wanted to create a history of his emblems from the very first to the very last, spanning all 73 years, much like this now infamous video did.

First I had the idea of just fading over the logos, but that's actually kinda boring, so I went back to a rad idea I used once back then when Macromedia Flash 4 was still in its early days: Tweening! (well, just like in the video, no?)

After a little research, I stumbled upon two ways to do it: Animating SVG with RaphaëlJS, a JavaScript library for cross-browser SVG, or using the very powerful SMIL for SVG animations.

All right! To the Batcave, Robins!

A short thought on RaphaëlJS

We already have some experience with RaphaëlJS in our company. We used the library to create parts of Adidas Customize to achieve recolorable, complex formed widgets on IE7 and IE8.

The library also allows to animate between paths, and does it in a very interesting, jQuery-like way: Instead of using SMIL, RaphaëlJS interpolates path points between the start and ending state and constantly updates the path inside your SVG. I was stunned by the complexity of this rather powerful algorithm, but looking at it from a performance point of view ... nah, you get the same issues you love to hate from jQuery.

RaphaëlJS is good if you don't want to delve to deeply into drawing programs or SVG source code, and I used it mainly to apply certain transformations on exiting SVG paths, and then copying the new SVG result. But for my tribute page I dropped it completely and just used it as a fallback for IE, because SMIL is still not implemented and looking at the current preview of IE11, will not be landing there for quite some while.

But the main reason for me to use SMIL was a rather clear one: Why using an 80kb JavaScript library if I can do everything with native means?

The first animation

My work is based on a great animation done by Tavmjong Bah. In his blog post he give additional information on how he actually implemented it. Some important parts were: The SVG paths you want to transform have to be in the same pattern, otherwise you don't get any animation at all:

Not even those funky effects we know from Flash back then, which is one of the main advantages of RaphaëlJS: The algorithm interpolating between two paths might lead to quirky results, but is nonetheless bloody good!

Check out this Pen!

Anyhow, I did want to stick to SMIL, but even by using Tavmjongs data I wasn't able to recreate one transition between two bats. It took me some time to realize how Tavmjong was implementing his animation. Mostly because I didn't take a good look at the values. The <animate>-element is pretty straightforward, but the values do need some explanation: To create an animation from path A to B, the values inside the element have to feature both paths, separated by a semicolon. So if you want a transition from Figure A to B, you first have to include the path in your <path>-element, and then again as the first value tuple in your animation:

<!-- The 'd' in path is the first bat -->
<path
  d="M 256,213 C 245,181 206,187  ..."
  fill="#000000">
  <!-- The 'values' include the first
      as well as the second bat -->
  <animate  dur="2s"
    repeatCount="indefinite"
    attributeName="d"
    values="M 256,213 C 245,181 206,187 ... Z;
            M 212,220 C 197,171 156,153 ... Z;"/>
</path>

Result

Actually, I lied a little bit. You don't need to include the path inside the "d" attribute of the <path>-element. The animation will work fine even without it. But: if you include the path data directly you can do some more, event-based stuff with your SVG. But more on that later. First, check on some of the attributes of the <animate> element.

Parameters

Some parameters are already visible in the example above:

We are not done with that. One thing you might realize is that the animation always jumps back to it's initial frame (which is why we also need to define the original path in the parent <path> element). To make sure that the ending state is preserved, we add another attribute called fill and set its value to freeze. In other elements, fill is used to define the filling color, in animation it's the state at the end.

<animate
  dur="2s"

  fill="freeze"

  repeatCount="1"
  attributeName="d"
  values="..." />

Result

Trigger the animation by clicking or tapping on it.

Triggers

As you've seen, animations can be triggered on certain actions. Use the begin attribute to define the interaction or property which starts the animation, as well as end to define the interaction which should stop it.

And this is where this stuff becomes really good, as you can add at least some control to your animation. You can either use DOM events for that, like click (as shown in the example above) or mouseover, but you also can use time constraints to apply a certain delay:

<!-- Triggers the animation after 1s -->
<animate
  dur="2s" repeatCount="indefinite"
  attributeName="d"

  begin="1s"

  values="..."
/>


<!-- Triggers the animation when clicking
  on the element -->
<animate
  dur="2s" repeatCount="indefinite"
  attributeName="d"

  begin="click"

  values="..."
/>

<!-- Triggers the animation on mouseover,
  stops it on mouseout -->
<animate
  dur="2s" repeatCount="indefinite"
  attributeName="d"

  begin="mouseover"
  end="mouseout"

  values="..."
/>

<!-- Triggers the animation on click,
  stops it also on click -->
<animate
  dur="2s" repeatCount="indefinite"
  attributeName="d"

  begin="click"
  end="click"

  values="..."
/>

These parameters take almost any input based DOM event, but with one very special constraint: The SVG data has to be embedded in the DOM. If you have your SVG in a file and are referencing it in an image tag or whatever, the DOM events won't trigger.

Trigger events can be expanded further by not only using the DOM event of an element itself, but also by referencing to an event by another element. For instance, begin="button.click" allows us to trigger the animation once a certain element with the id of button has been clicked.

This gives us a multitude of possibilities. Look at that the following example:

<svg>
  <path d="...">
    <animate
      dur="2s" fill="freeze"
      begin="click" id="anim1"
      values="..." />
  </path>
</svg>

<svg>
  <path d="...">
    <animate
      dur="2s" fill="freeze"
      begin="anim1.begin"
      values="..." />
  </path>
</svg>

Here we start the second animation once the first one has already started.

Result

Click on the left bat to see the magic happen.

Events

Sorry, your browser does not support SMIL events

SMIL supports some events to add additional control with JavaScript to your animation needs. Unfortunately, at the moment animation events are just implemented by Firefox (and pre-Blink Opera ...). I wanted to use this method not only to show some elements once the animation is done, but also to keep the state ready for the next animation, by removing the <animate>-element and changing the original path.

I dropped this idea due to browser constraints, tough it would've been easy: Simply add the callback into your markup, or use addEventListener to achieve the same.

<animate
  dur="2s" fill="freeze" begin="click"
  repeatCount="1"
  attributeName="d"

  onend="cry()"

  values="..." >
function cry() {
  alert("I'M BATMAN");
}

Other events include onbegin which is obviously triggered when the animation starts, and onrepeat, which counts the number of interations the animation has run and fires every time one iteration is complete.

Result

Start the animation by clicking or tapping the bat!

Again, this will work only if the SVG is included directly in your DOM.

Feature test

As you all know, we just should feature detect to check if we're able to use SMIL. However, it might be that you get a false positive in IE9, according to this (somewhat old) Modernizr issue, so be aware!

With that one you can detect SMIL:

return !!document.createElementNS('http://www.w3.org/2000/svg', 'animate');

Place it in a try-catch block! To check for event callbacks, this is how it should work.

var el = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
return !!el.onend;

However, not even Firefox provides interfaces in their DOM API. This is a workaround: Add an almost empty SVG markup to your DOM and have it call a function once it starts. In this function, set your bools or classes, or whatever you like or need.

function smilEventsSupported() {
  //set classes or whatever

}

//can be any element

document.body.innerHTML += '<svg width="0" height="0">'
  + '<path d="m 1,1 1,1 0,0">'
  + '<animate values="m 0,0 0,0 0,0"'
  + 'attributeName="d" begin="0s" repeatCount="0"'
  + 'onbegin="smilEventsSupported()"/>'
  + '</path></svg>'

Bottom line

This blog entry is based on about a weekend of research on that topic, fooling and playing around with values and constantly checking the specification. And I'm sure that I just scratched the surface! I stumpled upon parameters such as keytimes, keyspines or calcMode, which I didn't read in depth. Also the possibility of an animationPath is available, where I've no bloody clue how they can be created whatsoever. SVG animations are a beast of their own, and very powerful once you understand them.

Use is limited, tough. Microsoft has no plans of including SMIL in their current versions of IE. The spec has been out there for a while, but even the preview version of IE11 shows no sign of implementation. Furthermore, if you want to use that stuff on mobile browsers, be aware that performance might be below your expectations. Even iOS Safari, which is still one of the most performant mobile browsers out there, has a clear performance issue when it comes to basic tweening. It's not as bad as animating with RaphaëlJS, tough, because there you just won't get any animation at all.

Comments? Shoot me a tweet!