Transforming SVG Declarative Animation into Javascript

Bob Hopgood

16 October 2016

Introduction

SVG declarative animation was initially available through an efficiently compiled Adobe plugin, ASV, in 1999. Later, most browsers provided support with the notable exception of Microsoft's various browser offerings. IE eventually supported SVG but without SVG's declarative animation.

In the last few years, CSS has provided support for styling transitions. Instead of just providing styling control for layout and text properties, it also became possible to make changes in such properties over time to create transition effects. The major difference is that SVG declarative animation is aimed at generating animation content while CSS is aiming to brighten up the styling of a web page.

Both SVG and CSS require a timing model to define how properties of styling and attributes of content change over time. The original reference model for the web defined separate recommendations for each feature. XML defined markup, (X)HTML defined document markup, SVG defined scalable vector graphics and CSS defined Cascading Stylesheets that allowed both the page author and the page reader to defined their prefered styling for a page's content. The fourth timing dimension was defined by the SMIL recommendation (Synchronized Multimedia Integration Language) that was used by the 1998 multimedia players (for example, RealPlayer and QuickTime). SVG also defined its declarative animation using a subset of the SMIL model that was then extended for the needs of declarative animation.

Companies such as Microsoft and Google face the need to provide timing control in their browsers for both SVG declarative animation and CSS styling transitions. The current proposed solution is to drop declarative animation support from SVG and provide timing control of CSS properties just through CSS. As most of SVG's declarative animation is provided via animating path attributes with no equivalent CSS properties this does present problems that need solving.

Javascript is available on most browsers as an alternative way of modifying both attributes and properties over time. In the past this has not been particularly attractive as there were significant performance issues. While the Adobe plugin gave excellent performance for demanding SVG declarative animations, the equivalent animation using uncompiled Javascript was significantly worse with jerky frame-dropping animation. In the period from 2005 onwards, most browsers became reasonably competent at providing SVG declarative animation support of similar quality to the Adobe plugin.

Since 2012, the efficiency of Javascript support in browsers has improved sufficiently that is is now possible to consider using Javascript as an alternative to SVG declarative animation support in the browsers. Partly this is due to the decline in SVG declarative animation support by the browsers. An animation that ran smoothly in Chrome, for example, two years ago no longer performs anywhere near as well. This is also true for Firefox.

We currently have about 50 animations on the web with the largest being about 15 Mbytes in length, containing about 15000 elements defined using SVG declarative animation. The times of the animations run from a few minutes to 15 minutes for the longest.

A Javascript solution is needed that is:

GSAP

Greensock's Animation Platform GSAP seems the Javascript Library that most closely fits our needs. Careful coding and making full use of Javascript compilation, GSAP uses Request Animation Frame to achieve a smooth updating of the animation at 60 frames/sec or as close to that as is possible.

GSAP has, therefore, demonstrated that it is efficient and can implement most of the SVG declative animation facilities required:

Suppose we have an SVG element:

<circle id="crcl" cx="20" cy="50" r="5" style="opacity:0.5"/>
<set xlink:href="crcl" attributeName="opacity" attributeType="CSS" to="1" begin="2s" />
<animate id="first" xlink:href="crcl" attributeName="opacity" attributeType="CSS" to="0" begin="4s" dur="3s" fill="freeze"/>
<set xlink:href="crcl" attributeName="cx"  to="3" begin="first.end" />
<animate xlink:href="crcl" attributeName="cx"  to="6" begin="first.end+2s" dur="3s" fill="freeze"/>

Some GSAP code could be:

var crcl = document.getElementById("crcl");
TweenMax.set(photo,   {delay:2, opacity:1});
TweenMax.to(photo, 3, {delay:4, opacity:0});

The first command sets the CSS opacity property to 1 for the SVG element with id defined as crcl two seconds from the animation start. The second command at 4 seconds from the animation start causes the opacity to change from 1 to 0 over the next 3 seconds.

Similarly:

var crcl = document.getElementById("crcl");
TweenMax.set(photo,   {delay:7, atr:{cx:3});
TweenMax.to(photo, 3, {delay:9, atr:{cx:6});

would make similar changes to the cx attribute.

Animating transformations in SVG are of the form:

<path id="pth" transform="translate(300,300)" style="fill:blue" d="M0,0h60l-30,40l-30,-40z"/>

<animateTransform xlink:href="#pth" attributeName="transform"  type="translate"  
    begin="1s"    values="300,300;500,400;400,300;600,400" 
    keyTimes="0;0.2;0.8;1"   dur="4s" fill="freeze" />

TweenMax.set(pth, {delay:1, attr:{transform: 'translate(300,300)'}});
TweenMax.to(pth, 0.8,{ease:Linear.easeNone, delay: 1, attr: {transform: 'translate(500,400)'}});
TweenMax.to(pth, 2.4,{ease:Linear.easeNone, delay: 1.8, attr: {transform: 'translate(400,300)'}});
TweenMax.to(pth, 0.8,{ease:Linear.easeNone, delay: 4.2, attr: {transform: 'translate(600,400)'}});

In this case, the problem is a bit more complicated. First, the transform has to state the type of transform. The values attribute gives a set of values. Finally, the timing intervals, defined by keyTimes, are not straightforward. However, the transformation is essentially the same as before.

Repeats are organised as follow:

<animate xlink:href="#crcl" attributeName="cx" begin="3s" 
values="400;600;400" repeatCount="5" dur="2s"   fill="freeze"/>

var tlgsanid19= new TimelineMax({delay:3, repeat:5});
tlgsanid19.set(crcl, {attr: {cx: '400'}})
tlgsanid19.to(crcl, 1,{ease:Linear.easeNone,  attr: {cx: '600'}})
tlgsanid19.to(crcl, 1,{ease:Linear.easeNone,  attr: {cx: '400'}})

It is necessary to put the repeat on the sequence of tweens so a new timeline has to be created and given a name and a starting time. The format of the tween commands is a set with no separators followed by the last one ending in semicolon. Each transformation is applied in turn from the end of the previous one.

Finally, we have motion along a path:

<animateMotion xlink:href="#crcle" begin="2s"  
    path="M400,200c0,100,200,100,200,0c0,-100,200,-100, 200,0" 
    rotate="auto" dur="3s" fill="freeze"/> 

TweenMax.to(crcle,3,{delay:2, bezier: 
    {values:MorphSVGPlugin.pathDataToBezier("
    M400,200c0,100,200,100,200,0c0,-100,200,-100, 200,0"), type:"cubic"}});

GSAP has a plugin to transform path data to a canonical form that can be used to move an object along a path.

Some of the minor problems encountered are:

All the problems are relatively minor so an attempt has been made to define an XSLT transformation that takes an exisiting animation and generate a new document that can implement either the current SVG declarative animation or the GSAP equivalent. This then gives a good comparison of the performance of the two options.

XSLT Transformations

For large animations, the first problem was to avoid the Saxon version of XSLT blowing up under the weight of the intermediate XSLT variables used in the conversion. The solution is fairly straightforward. The original monolithic XSLT transformation has been divided into a sequence of transformations where the intermediate XML files are large but in general can be transformed sequentially. These are handled without any problem by Saxon.

The overall flow diagram is shown below.

00_template.xml 00_add_ids.xsl Adds id attributes to parents of animation commands that have no xlink:href 00_template2.xml 01_extract_anims.xsl Extract all the animation commands converting attributes to elements Add ids to animate commands with repeat 01_anims.xml 02_timeline.xsl Establishes the absolute timeline for the ids used in the animation elements. Recursive and can take a while 02_timeline.xml 03_timeline_list.xsl Lists the absolute times set up by the animation element ids 03_timeline.htm 04_update_anims.xsl Updates animation commands Now have absolute time values 04_updated_anims.xml 05_timeline_list.xsl Lists the absolute times of all the animation elements 05_anim_list.htm 06_vars.xsl 06_vars.xml var id = document. getElementById('id')... var w04grb1 = document.getElementById('w04grb1'); 07_translate.xsl 07_translate.xml TweenMax.to(...); ... 08_update.xsl Generates new animation file containing both SVG and GSAP versions 08_result.xml

We will use a simple SVG file as an example of how the system works. The initial splash screen shows the two options (SVG declarative animation and GSAP Javascript animation). Hitting the SVG animated arrow removes the splash screen and runs the animation where the timing is mainly controlled by a set of chained ids.

Hitting the GSAP Scripted button causes nothing to happen so far;-)

The file 00_template.xml is then transformed by the following set of transformatiions:

00_add_ids.xsl
GSAP needs an id to indicate the object of the transformation. Something like:

<circle cx="600" cy="500" r="20" style="fill:cyan;stroke:none">"
<animate attributeName="cx" to="900" begin="start12.begin" dur="2s" fill="freeze" />
</circle>

is a problem as the circle element has no associated id and the animate element no xlink:href.

All this transformation does is add an id attribute (id="gasp_id66" in this case) to the circle element if it does not already have one.

Later animate commands without an xlink:href attribute will have an attribute added linking the child animate command to its parent.

01_extractanims.xsl
The set of animate commands in the SVG document are extracted and transformed into an XML structure:
<cmnd>
  <name>animate</name>
  <id>start2</id>
  <begin>
   <full>start.begin+1s</full>
   <first>start.begin+1s</first>
   <origin>start</origin>
   <type>begin</type>
   <incr>1</incr>
   </begin>
   <href,atname,attype,dur,values,to,from,repeatcount,fill,calcmode,keytime,keysplines,path,rotate,pthorigin,trtype..
</cmnd>

Most of the potential attributes are converted into XML elements. Some have been omitted mainly either because they are infrequently used or by GSAP not having an obvious transformation. (by, min, max, restart, repeatDur, keySplines, keyPoints, additive, accumulate).

An initial attempt at parsing the begin attribute has been made. Lists separated by semicolons in the begin attribute have not been handled but could be.

02_timeline.xsl
It starts with a set of commands where most are dependent on some other time (eg begin="start2.end+5s"). At least one must be an absolute value to start the process off. Each recursion examines the list of unresolved commands and resolves those it can handle on this iteration. At each iteration the number of unresolved commands is decreased until zero is eventually reached. If a large number of animations are dependent on, say, the one before, the recursion depth can be quite large.
03_timelinelist.xsl
Really just a debug aid giving all the starting values of commands with ids that are used in defining another animation's start point.
04_update_anims.xsl
The main function is to define the starting time of all the animate commands.
05_timelinelist.xsl
Really just a debug aid giving all the starting values of all commands.
06_vars.xsl
The form of the GSAP script is something like:
<script>
function animated() {
var crcl = document.getElementById('crcl');
...........
TweenMax.to(kiwisvg,1,{ delay:1, fill: 'red'});
TweenMax.set(crcl, {delay:3, attr:{x: '450'}});
TweenMax.to(crcle,2,{delay:2, bezier: {values:MorphSVGPlugin.pathDataToBezier("M400,200c0,100,200,100,200,0c0,-100,200,-100, 200,0"), type:"cubic"}});
....

The object to be animated have to be defined and the equivalent of from and to are the GSAP commands Tweenmax.set and Tweenmax.to. delay defines the starting time and the second paranmeter of Tweenmax.to defines the duration.

This transformation just generates the var declarations for all the ids that either appear in objects that are animated or appear in the animate commands.

07_translate.xsl
The main transformation now that the preparatory work has been done. There is a need to expand any values element into a TweenMax.set followed by several Tweenmax.to starting at different times and with potentially different durations (if keyTimes is specified).
The standard interpolation in GSAP is to ease in and out. For multiple values, it is more likely that linear interpolation is required (just a guess!). In this case, {delay: is replaced by {ease:Linear.easeNone, delay:.
08_update.xsl
A simple transformation that takes the original file and adds to it the variable declarations and GSAP commands.

Inplementing repeatCount is not straightforward with a values parameter. In GSAP, it is possible to create a separate timeline but this has to run to completion so if you try and do the repeat for the individual values in the values list you land up with multiple timeline trying to work on the same object. The solution is to define a new timeline and add the repeat to that.

Suppose we have:

<animate xlink:href="#crclq" attributeName="cx" begin="start4.begin+1s" 
values="400;600;400;200;400" 
repeatCount="5" dur="4s"   fill="freeze"/>

The code generated is something like:

var tlgsanid19= new TimelineMax({delay:5, repeat:5});
tlgsanid19.set(crclq,                         {attr: {cx: '400'}})
tlgsanid19.to(crclq, 1,{ease:Linear.easeNone,  attr: {cx: '600'}})
tlgsanid19.to(crclq, 1,{ease:Linear.easeNone,  attr: {cx: '400'}})
tlgsanid19.to(crclq, 1,{ease:Linear.easeNone,  attr: {cx: '200'}})
tlgsanid19.to(crclq, 1,{ease:Linear.easeNone,  attr: {cx: '400'}});

The timeline has to be given a unique name. The delay appears on the timeline definition as does the repeat. The individual animations are done in sequence with no delay between them. This is achieved by not having a semicolon following each one.

The result is a document similar to the original one but now the Javascript animation and the declarative SVG one both work.

A few things remain to be implemented (things that I never use:

At the moment, the IW3C2 logos have been transformed. First the intial 20 and secondly the last four. Both are significant examples. The first 20 is a 9 Mbyte file that generates 4 Mbytes of Javascript. The last four is more demanding with a 15 Mbyte file generating 12 Mbytes of Javascript.

The XSLT transformations are quite fast taking about 15 seconds to do each complete transformation.