Thursday, 14 January 2016

Chaining animations with Snap.svg

I was messing around with SVG animations using Snap.svg today. According to the tin, "SVG is an excellent way to create interactive, resolution-independent vector graphics that will look great on any size screen. And the Snap.svg JavaScript library makes working with your SVG assets as easy as jQuery makes working with the DOM."

It's the first time I've used this library to create SVG images. I had some challenges trying to build an animation that would render items (svg drawings) in a sequential order.

I ended out with some code I wanted to share here. There might be better approaches, but it does the trick, and it's easily be customisable to suit a variety of needs!

The code is pretty simple, but it illustrates how to chain animations together.

To re-draw the animation, click here.



As I said, there's probably many ways to achieve this. I went about it by creating an object with a method for rendering any "svg" items in it's internal array. The internal array can be populated, and calling the render() method creates the all of the SVG elements (in the internal array) in order, animating them as it goes.

Here's the commented code:

<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>
<script type="text/javascript">
//Create an object that is going to be responsible for storing an array of svg items to animate
//The array will have an addGraphic method for adding items to the array
//and a method for rendering the svg items - obj.render()
var animation = function(paper){
 //internal array of svg items
 this.$a = [];
 //create the Snap "canvas"
 this.$s = Snap(paper);
 //The renderIndex property will be used to keep track of which items have been animated
 this.renderIndex = 0;
}
animation.prototype.constructor = animation;
animation.prototype.addGraphic = function (o) {
 //Add a new svg item to the array
 this.$a.push(o); 
}
animation.prototype.render = function () {   
 //Render (animate) all of the svg items in the array
 if(this.renderIndex === 0){   
  //If the index is at zero, clear the current svg items in the canvase, before re-drawing them
  this.$s.clear();
 }
 //If the renderIndex is greater than the number of items in the animation array, 
 //then we've animated everything! Reset the renderIndex to 0, and return.
 if(this.renderIndex > this.$a.length){this.renderIndex = 0; return;}
 //Get the next item in the animation array
 var e = this.$a[this.renderIndex];
 //increment the renderIndex. This method gets called recursively, and the renderIndex is 
 //used to keep track of the next item in the animation array that needs to be created.
 this.renderIndex = this.renderIndex + 1;
 if(!e){this.renderIndex = 0;return;}
 //This is a demo... so this method only processes circles, lines and text
 if(e.type === 'circle'){
  //Create the circle, but make the radius 0
  var b = this.$s.circle(e.x,e.y,0).attr({fill: "#fff"});    
  //Now change the radius to the final value, and animate the transition
  //Notice that the last argument in the animate() function is a callback 
  //function. The callback function points back to this function,
  //which will draw the next item in the array
  b.animate({
   stroke: "#000",
   strokeWidth: 2,
   r: e.r,   
   fill: e.cs === true ? '#bada55' : '#b37c45'
  },250, mina.easein, this.render.bind(this));
 }
 if(e.type === 'line'){ 
  //Does the same as per the circle, except for a line
  var b = this.$s.line(e.x1,e.y1,e.x1,e.y1).attr({fill: "#fff"})
  b.animate({
   stroke: "#000",
   strokeWidth: 2,
   x2: e.x2,
   y2: e.y2
  },250, mina.easein, this.render.bind(this));
 }
 if(e.type === 'text'){  
  //Does the same as per the circle, except for some text
  var b = this.$s.text(e.x,e.y,e.text).animate({},50, mina.easein, this.render.bind(this));
 }
}
//Create a new instance of the animation object
var an = new animation('#an');
//Populate the animation object with a bunch of svg items that will be rendered in order when the 
//animation.render() method is called.
an.addGraphic({type:'circle',cs:false,x:30,y:30,r:30});
an.addGraphic({type:'line',cs:false,x1:60,y1:30,x2:90,y2:30});
an.addGraphic({type:'circle',cs:false,x:120,y:30,r:30});
an.addGraphic({type:'line',cs:false,x1:150,y1:30,x2:180,y2:30});
an.addGraphic({type:'circle',cs:false,x:210,y:30,r:30});
an.addGraphic({type:'line',cs:false,x1:240,y1:30,x2:270,y2:30});
an.addGraphic({type:'circle',cs:true,x:300,y:30,r:30});
an.addGraphic({type:'text',cs:true,x:294,y:36,text:'Y'});
an.addGraphic({type:'line',cs:false,x1:330,y1:30,x2:360,y2:30});
an.addGraphic({type:'circle',cs:false,x:390,y:30,r:30});
//Call the render() method
an.render();
</script>

Happy days!