Search code examples
javascriptsvgsnap.svgsvg-animate

how to optimize snap svg animations


I have a snap svg animation which animates a bunch of circles, and draws a line between them if they are within a certain proximity of each other. However, I realize that there is a lot of optimizing I can do, but I'm not exactly sure how to do it. I feel like it would be useful to

  1. have a good example of proximity detection in snap
  2. have some more information on optimizing animations in snap svg. It hasn't been easy to find.

Here is a working example of the animation:

http://jsfiddle.net/heaversm/sbj4W/1/

and here are the things I believe can be optimized:


Each circle calls its own animation function - the circles have all been added to a group, and I'm guessing there is a way to apply random motion to all members of a group that is more performant than call a function for each and every element within the group.

for (var i=0; i<this.drawingConfig.circles.amount;i++){
  ...
  this.animateSingle(circleShape);
}

The proximity function is awkward - for each circle, for each update cycle, I have to loop through an array of all the other circles and find out if the X and Y coordinates are close enough to draw a line to. Plus, that means you're getting duplicate lines, because each circle will draw a line to its neighbors, instead of having a single shared line between the two.

for (var i=0;i<circles.length;i++){
  var nextCircle = circles[i].node;
  var nextCircleX = nextCircle.cx.baseVal.value;
  var distance = Math.abs(nextCircleX-thisCircleX);
  var proximity = mainModule.drawingConfig.circles.proximity;
  if (distance < proximity){

    var nextCircleY = nextCircle.cy.baseVal.value;
    var thisCircleY = shape.node.cy.baseVal.value;
    var distanceY = Math.abs(nextCircleY - thisCircleY);
    if (distanceY < proximity){

      var line = mainModule.s.line(thisCircleX, thisCircleY, nextCircleX, nextCircleY).attr({stroke: '#a6a8ab', strokeWidth: '1px'});
      mainModule.drawingConfig.circles.circleGroup.add(line);
    }
  }
}

Correspondingly, I each circle's animation function clears all the lines on the screen. Ideally all the circles would be sharing one update function, and in that function, you'd clear the lines.

Snap.animate(startX, animX, function (val) {

  var lines = Snap.selectAll('line');
  lines.remove();

  ...

}, mainModule.drawingConfig.circles.animTime);

Right now, I can tell the renderer can't keep up with all of the various animations / loops. Any help optimizing the above things (or anything else you can see that I'm doing weird, would be greatly appreciated.


Solution

  • I cleaned this up by running only one animation loop, on a timer every 10ms, and animated the position of the circles by just giving them a slope and, each update, continuing them further along that slope. You can see an updated fiddle here:

    http://jsfiddle.net/heaversm/fJ6fj/

        var mainModule = {
      s: Snap("#svg"),
      drawingConfig: {
        circles: {
          amount: 20,
          sizeMin: 10,
          sizeMax: 20,
          proximity: 100,
          circleGroup: null,
          circleArray: [],
          animTime: 2000
        },
        canvas: {
          width: 800,
          height: 600
        }
      },
    
      init: function(){
        //this.sizeCanvas();
        this.makeCircles();
      },
    
      sizeCanvas: function(){
        $('#svg').width(800).height(600);
      },
    
      makeCircles: function(){
        this.drawingConfig.circles.circleGroup = this.s.g();
    
        for (var i=0; i<this.drawingConfig.circles.amount;i++){
          var circleX = this.randomNumber(0, this.drawingConfig.canvas.width);
          var circleY = this.randomNumber(0, this.drawingConfig.canvas.height);
          var circleRadius = this.randomNumber(this.drawingConfig.circles.sizeMin,this.drawingConfig.circles.sizeMax);
          var circleFill = '#'+Math.floor(Math.random()*16777215).toString(16);
          var circleShape = this.s.circle(circleX, circleY, circleRadius);
          circleShape.attr({
            fill: circleFill
          });
          this.drawingConfig.circles.circleGroup.add(circleShape);
    
          var circleIncline = this.setIncline();
          var circleObj = { incline: circleIncline, shape: circleShape };
    
          this.drawingConfig.circles.circleArray.push(circleObj);
    
        }
    
        this.update();
    
    
      },
    
      setIncline: function(){
        return { incX: this.randomNumber(-5,5), incY: this.randomNumber(-5,5) }
      },
    
      update: function(){
    
        var lines = Snap.selectAll('line');
        lines.remove();
    
        for (var i=0; i<this.drawingConfig.circles.amount; i++){
          var circle = this.drawingConfig.circles.circleArray[i];
          var circleX = circle.shape.node.cx.animVal.value;
          var circleY = circle.shape.node.cy.animVal.value;
          this.move(circle,circleX,circleY);
    
          for (var j=0;j<i;j++){
            if (i != j){
              var circle2 = this.drawingConfig.circles.circleArray[j];
              var circle2X = circle2.shape.node.cx.animVal.value;
              var circle2Y = circle2.shape.node.cy.animVal.value;
              var dist = mainModule.distance(circleX,circleY,circle2X,circle2Y);
              if (dist <= mainModule.drawingConfig.circles.proximity){ //
                var lineWeight = 10/dist;
                var line = mainModule.s.line(circleX, circleY, circle2X, circle2Y).attr({stroke: '#a6a8ab', strokeWidth: '1px'});
              }
    
              if (dist <= 10) { //collision
                circle.incline = mainModule.setIncline();
                circle2.incline = mainModule.setIncline();
              }
    
            }
          }
    
        }
    
        setTimeout(function(){ mainModule.update(); },10);
    
      },
    
      distance: function(circleX,circleY,circle2X,circle2Y){
        var distX = circle2X - circleX;
        var distY = circle2Y - circleY;
        distX = distX*distX;
        distY = distY*distY;
        return Math.sqrt(distX + distY);
      },
    
      move: function(circle,curX,curY){
        if (curX > this.drawingConfig.canvas.width || curX < 0) {
          circle.incline.incX = -circle.incline.incX;
        }
        if (curY > this.drawingConfig.canvas.height || curY < 0) {
          circle.incline.incY = -circle.incline.incY;
        }
        curX = curX + circle.incline.incX;
        curY = curY + circle.incline.incY;
    
        if (curX > this.drawingConfig.canvas.width) {
          curX = this.drawingConfig.canvas.width;
          circle.incline = this.setIncline();
        } else if (curX < 0) {
          curX = 0;
          circle.incline = this.setIncline();
        }
    
        if (curY > this.drawingConfig.canvas.height) {
          curY = this.drawingConfig.canvas.height;
          circle.incline = this.setIncline();
        } else if (curY < 0) {
          curY = 0;
          circle.incline = this.setIncline();
        }
    
        circle.shape.attr({ cx: curX, cy: curY });
    
      },
    
      randomNumber: function(min,max){
        return Math.floor(Math.random()*(max-min+1)+min);
      },
    
      getBounds: function(shape){
        shapeBox = shape.node.getBoundingClientRect();
      }
    
    }
    
    mainModule.init();