Animating svg with dart's universal tween engine

Some time ago, I ported the java universal tween engine made by Aurelien Ribon to dart. It doesn't seem to be used that much so far, but regardless, from time to time some issues arise at the github repo.

Issue #8 asks for the tween engine to implement an svg animation mechanism. While I can see how that would be extremely useful, I would like to keep the library from depending on the dart:svg pub package.

There's nothing the universal tween engine can't interpolate on, and SVG is no exception to this.

Here's a little example on how an animation can be done on an svg
rectangle:

Setting up the project

  1. Start by creating a new web project(I'm going to name it svg_dart_animation
  2. In your pubspec.yaml add reference to the tween engine package:

    dependencies:
        browser: any
        tweenengine: any
    
  3. Open the svg_dart_animation.dart file. Make sure you have these imports:

    import 'dart:html';
    import 'dart:svg';
    import 'package:tweenengine/tweenengine.dart' as tween;
    
  4. Delete all code on the file, and just leave an empty void main() method

Getting the code ready

Get your html ready

Now, add an svg element to your html, which contains a rect child, here's mine:

<!DOCTYPE html>

<html>  
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Svg tween example</title>

    <script async type="application/dart" src="svg_tween_example.dart"></script>
    <script async src="packages/browser/dart.js"></script>

    <link rel="stylesheet" href="svg_tween_example.css">
  </head>
  <body>
    <h1>Svg tween example</h1>

    <svg id="svg" viewBox="0 0 500 400" preserveAspectRatio="xMinYMin meet" width="100%" height="100%">
      <rect id="my_animated_box" x="100" y="100" rx="20" ry="20" width="150" height="150"
  style="fill:red;stroke:black;stroke-width:5;opacity:0.5" />
    </svg>
  </body>
</html>

Creating the accessor

Whenever you want to interpolate numeric data on a class using the tween engine, you need to implement the interface TweenAccessor. Since this example is only about animating svg's rectangle objects, we will only create one accessor, which will allow us to animate all rectangles attributes

class RectAccessor implements tween.TweenAccessor<RectElement>{  
  static const int XY = 1;
  static const int RXRY = 2;
  static const int W = 3;
  static const int H = 4;
  static const int WH = 5;

  int getValues(RectElement target, int tweenType, List<num> returnValues){}

  void setValues(RectElement target, int tweenType, List<num> newValues){}
}

What the code above is doing:

  • define the type of tweens / interpolations we are going to have for our RectElement objects ( which come from dart:svg package)
  • implement the TweenAccessor interface.

So, now we actually need to tell to the tween engine where it's going to retrieve the values from, for that, the TweenAccessor provides the engine with the getValues method, where the engine provides you the instance it is currently interpolating on(target), the kind of tween that it is processing now (tweenType, which will be delimited by the constants defined in our RectAccessor interface) and a List<num>(returnValues), which will allow us to feed the values into the engine. This method is meant to return how many attributes were modified by this getValues call.
Since we are just going to interpolate only on our x, y, rx, ry, width and height properties, the method below suffices:

int getValues(RectElement target, int tweenType, List<num> returnValues){  
    switch (tweenType){
      case XY:
        returnValues.setRange(0, 2, [target.x.baseVal.value, target.y.baseVal.value]);
        //returnValues[0] = target.x.baseVal.value;
        //returnValues[1] = target.y.baseVal.value;
        return 2;
      case RXRY:
        returnValues.setRange(0, 2, [target.rx.baseVal.value, target.ry.baseVal.value]);
        //returnValues[0] = target.rx.baseVal.value;
        //returnValues[1] = target.ry.baseVal.value;
        return 2;
      case W:
        returnValues[0] = target.width.baseVal.value;
        return 1;
      case H:
        returnValues[0] = target.height.baseVal.value;
        return 1;
      case WH:
        returnValues.setRange(0, 2, [target.width.baseVal.value, target.height.baseVal.value]);
        //returnValues[0] = target.width.baseVal.value;
        //returnValues[1] = target.height.baseVal.value;
        return 2;
    }
    return 0;
  }

The code above, basically is just fetching appropiate properties according to the tween type, and dumping them into the returnValues list.
The lines commented out are just to show that you can put the values one by one, in case you are not familiar with dart's List.setRange

Now on to the setValues implementation:

void setValues(RectElement target, int tweenType, List<num> newValues){  
    switch (tweenType){
      case XY:
        target.x.baseVal.value = newValues[0];
        target.y.baseVal.value = newValues[1];
        break;
      case RXRY:
        target.rx.baseVal.value = newValues[0];
        target.ry.baseVal.value = newValues[1];
        break;
      case W:
        target.width.baseVal.value = newValues[0];
        break;
      case H:
        target.height.baseVal.value = newValues[0];
        break;
      case WH:
        target.width.baseVal.value = newValues[0];
        target.height.baseVal.value = newValues[1];
        break;
    }
  }

The above code is doing the inverse operation, setting the object's properties by taking them from the provided newValues List.

This is enough to tell the engine how to interpolate all the values we want inside a RectElement.

The main method

First let's configure our Tweens.

void main(){  
    tween.Tween.combinedAttributesLimit = 2;
    tween.Tween.registerAccessor(RectElement, new RectAccessor());
}

First line, is basically setting the maximum number of attributes that we can change at once, meaning, this is going to be the length of the returnValues and newValues Lists in the accesor above.
Second line is saying that whenever we ask to animate a RectElement, the engine should use our RectAccessor to interpolate it.

Next, declare a TweenManager outside of the main() method so we can access it from everywhere, and initialize it inside your main

tween.TweenManager _tweenManager;  
void main(){  
    ...
    _tweenManager = new TweenManager();
    ...
}

Since we are going to animate the rect element, we need to set some kind of keyFrames for it.
Here are mine:

  //these are the keyframes...they could be loaded from some other svg 
  RectElement kf1= new RectElement()
      ..attributes = {
         'x': '50', 
         'y': '100',
         'rx': '20',
         'ry': '20',
         'width': '250',
         'height': '150',
      };

  RectElement kf2= new RectElement()
        ..attributes = {
           'x': '50', 
           'y': '100',
           'rx': '20',
           'ry': '20',
           'width': '250',
           'height': '150',
        };

They really don't need to RectElement instances, but that way it's easier to read.

Next, we need the actual tween target. Since we already have that in our html, lets just get it:

void main(){  
    ...
    SvgSvgElement svg_box = querySelector('#svg');
    RectElement rectangle = svg_box.children.first as RectElement;  
    ...
}

That's it! We are all setup.

Creating an animation for svg

I am not planning to go into too much detail of how the tween engine works, but here's how we are going to define the animation by nesting Timelines:

void main(){  
tween.Timeline.createSequence()  
    ..beginParallel()
      ..push( tween.Tween.to(rectangle, RectAccessor.XY, 0.5)..targetValues = [kf1.x.baseVal.value, kf1.y.baseVal.value]  )
      ..push( tween.Tween.to(rectangle, RectAccessor.RXRY, 0.5)..targetValues = [kf1.rx.baseVal.value, kf1.ry.baseVal.value] )
      ..push( tween.Tween.to(rectangle, RectAccessor.WH, 0.5)..targetValues = [kf1.width.baseVal.value, kf1.height.baseVal.value] )
    ..end()
    ..beginParallel()
      ..push( tween.Tween.to(rectangle, RectAccessor.XY, 0.5)..targetValues = [kf2.x.baseVal.value, kf2.y.baseVal.value]  )
      ..push( tween.Tween.to(rectangle, RectAccessor.RXRY, 0.5)..targetValues = [kf2.rx.baseVal.value, kf2.ry.baseVal.value] )
      ..push( tween.Tween.to(rectangle, RectAccessor.WH, 0.5)..targetValues = [kf2.width.baseVal.value, kf2.height.baseVal.value] )
    ..end()
    ..repeatYoyo(tween.Tween.INFINITY, 0)
    ..start(_tweenManager);

}

Running the animation

Now that we have all built up, lets just do one more thing, create our loop that will be playing the animation:

void main(){  
    ....
    window.animation.then(update);
}

num lastUpdate = 0;  
void update(num delta){  
  num deltaTime = (delta - lastUpdate) / 1000;
  lastUpdate = delta;

  _tweenManager.update(deltaTime);
  window.animationFrame.then(update);
}

And that's it! Run the example on your dartium, or compile to javascript and run it on any other browser, and you should see see the animation.

how it looks

I created a gist with the above code for a much cleaner look.

It was not much work to animate a rectangle object in this example, however, creating a full animation suite for svg would take a lot more work, since every property of the svg specification should be considered. This however, could be a good start to create such an animation suite

comments powered by Disqus