Advanced SVG Animation Techniques

By Antoine Quint

In a previous article, Charles McCathieNevile demonstrated how to do time-based (as opposed to frame-based) animation in SVG, using SMIL. But we can do even more. Today's web applications, thanks to the advent of AJAX clearing JavaScript's bad name, are very interactive and heavy on script-based logic.

So far, you can tell animations to start on a user interface event, and the syntax gives you fine-grained control of how the animation is performed. You need more, however, if you want to run an animation, or even create an animation on-the-fly based on particular conditions. Luckily, SVG caters for these needs, too.

As SVG is based on XML. the "glue" between SVG and the JavaScript programming runtime could be expected to be the DOM ... and it is! Animation elements in SVG are just like any other elements in SVG's grammar, and they can be created and controlled from JavaScript using the DOM interfaces. Let's get practical and try to write a handy little function that would fade out any element that's passed as a parameter. So, our fade() function will need to create the following piece of code on the fly:

<animate attributeName="opacity" to="0" dur="0.25" fill="freeze" />

Easy! We know that Document::createElementNS() will let us generate an animate element, and that all attributes will be set using the Element::setAttributeNS() method. Then, after generating the element, all that remains is to append it to the target element passed as a parameter to our fade() function. Thus, the code to generate the aforementioned element should look like this:

function fade (target) {
      // create the <animation> element
      var animation = document.createElementNS(
          'http://www.w3.org/2000/svg', 'animate');
      // set its attributes
      animation.setAttributeNS(null, 'attributeName', 'opacity');
      animation.setAttributeNS(null, 'to', 0);
      animation.setAttributeNS(null, 'dur', 0.25);
      animation.setAttributeNS(null, 'fill', 'freeze');
      // link the animation to the target
      target.appendChild(animation);
    }

This should be enough to have the animation work out. Actually, on second thought, probably not. Indeed, all animations should have a begin attribute defined to specify when it starts. Omitting to specify what begin is means that the time offset to start the animation is 0 seconds, that is, when the document has loaded. So, unless we call this method right when the document is loaded, the animation will not be triggered as we want it to.

SVG has specific facilities to trigger animations from script at any given time. Indeed, a special indefinite keyword can be used in both the begin and end attributes. While this keyword doesn't look really helpful here, since it essentially seems to say that we don't specify when the animation starts or ends, it is in fact really powerful because it means that you can use the methods defined on the ElementTimeControl interface from the SMIL DOM, for instance the beginElement() and endElement() methods. These methods are well named for what they do: begin or start an animation.

This means that if you ever have an animation you want to start from script, following any kind of code you need to execute to check that the animation should run, you can simply use begin="indefinite" and you're golden. So, let's add this piece of beauty to our fade() function (added code highlighted):

function  fade (target) {
    // create the fade animation
    var animation = document.createElementNS(
                         'http://www.w3.org/2000/svg', 'animate');
    animation.setAttributeNS(null, 'attributeName', 'fill-opacity');
    animation.setAttributeNS(null, 'begin', 'indefinite');
    animation.setAttributeNS(null, 'to', 0);
    animation.setAttributeNS(null, 'dur', 0.25);
    animation.setAttributeNS(null, 'fill', 'freeze');
    // link the animation to the target
    target.appendChild(animation);
    // start the animation
    animation.beginElement();
}

Now, trying this out gives the expected result, we did good. The amount of code we wrote here is alright, but if we were to write a more complex effect, things are likely to get very tedious. Being able to reuse a piece of SVG animation code would be much easier. We can't really make use of the built-in markup-based SVG use referencing mechanism here as it's meant to be used for things that get rendered only. We can, however, make use of the DOM to sort things out again. This time around, for illustration, we'll crank up our effect a notch by making our object scale down and slightly rotate as it fades out, for no particular reason besides being able to—and it's all visuals, so that's actually a solid argument! Our template animation should be as follows, wrapped in a element">defs element to clearly show that it's an asset we'll reuse, somehow:

<defs id="defs">
      <animateTransform begin="indefinite" attributeName="transform" 
          type="translate" dur="0.5" additive="sum" />    
      <animateTransform begin="indefinite" attributeName="transform" 
          type="scale" to="0" dur="0.5" additive="sum" />
      <animateTransform begin="indefinite" attributeName="transform" 
          type="rotate" to="30" dur="0.5" additive="sum" />
      <animateTransform begin="indefinite" attributeName="transform" 
          type="translate" dur="0.5" additive="sum" />
      <animate begin="indefinite" attributeName="opacity" to="0" dur="0.5" 
          fill="freeze" />
</defs>

In order for this to work, some blank values must be filled in via script. To be precise, we'll need to know what the size of the object we're fading is in order to slide it to its center, apply scaling and rotation, and then back to its origin, so that the transforms can be applied with the object's center as a reference point (this is what the first and last animateTransform elements do here). Also, a cloned copy of all of these elements is necessary in order to use them for the target object independently of any other fading animation that might have been triggered prior to that. Finally, all these animations also have to be triggered via script.

We'll start by figuring out the bounding box of the objects using the SVG DOM SVGLocatableElement::getBBox() method. Then, cloning the elements is done by iterating through the animation elements and cloning them one after the other using the DOM Node::cloneNode() method. While cloning, we'll also attach the cloned animation elements as children of the target element for the fading animation. Once we have the elements cloned, we'll use the metrics collected from calling getBBox() to set the from and to attributes of the translation animateTransform elements. Finally, we'll use beginElement() to start the animation we've created. So, our final code should look a little something like this:

function fade (target) {
      // get the target's bounding box
      var bounds = target.getBBox();
      var t_x = bounds.width / 2 + bounds.x;
      var t_y = bounds.height / 2 + bounds.y;
      // get a pointer to the template animations
      var template_animations = document.getElementById('defs').getElementsByTagNameNS('http://www.w3.org/2000/svg', '*');
      // clone and append the animations
      var animations = new Array();
      for (var i = 0; i < template_animations.length; i++) {
        var animation = template_animations.item(i).cloneNode(false);
        animations.push(target.appendChild(animation));
      }
      // customize translations
      animations[0].setAttributeNS(null, 'from', t_x + ',' + t_y);
      animations[0].setAttributeNS(null, 'to', t_x + ',' + t_y);
      animations[3].setAttributeNS(null, 'from', (-t_x) + ',' + (-t_y));
      animations[3].setAttributeNS(null, 'to', (-t_x) + ',' + (-t_y));
      // launch animations
      for (var i = 0; i < animations.length; i++) {
        animations[i].beginElement();
      }
    }

And here you can try the final code.

Conclusions

The key take-away from this article should be that SVG's animation elements are like any other elements and can be created and manipulated via the DOM. Of course, special elements such as these have special needs, and the ElementTimeControl interface and its methods cater for those. Now you can have fun mixing animations with script code, the possibilities are endless. Our next article will show you more of this with animation events, smart animation chaining, and more!

This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.

Comments

The forum archive of this article is still available on My Opera.

No new comments accepted.