Collision detection in SVG

Using Snap.svg to detect proximity and collisions and change object trajectory.

svg,animation,snap.svg,javascript

06/05/2014

I was recently looking in to Adobe's snap.svg in preparation for a heavily svg focused web project, and trying to rig up a simple prototype where a number of balls move around the screen, changing trajectory when they collide and and drawing lines between each other when they pass a certain proximity. I was using snaps' animate functionality, but with so many animation loops running currently, the prototype chugged, which is when I realized I was going about it all wrong. Someone on Stack Overflow suggested just running a single loop, and not having it run in such rapid succession, so I rewrote the code, based on an old processing sketch I remember seeing.

Here's a demo (click the "result" tab):

Here's a walkthrough of how it's set up.

First, initiate snap:


s: Snap("#svg"),

where #svg is a reference to an empty svg element with ID #svg.

Then initiate a config object that will store all your static variables.


  drawingConfig: {
    circles: {
      amount: 20,
      sizeMin: 10, //min radius of circle
      sizeMax: 20, //max radius of circle
      proximity: 100, //distance at which a line will be drawn between neighboring circles
      circleArray: [] //stores all the circles on screen
    },
    canvas: {
      width: 800,
      height: 600
    }
  },

I made a couple helper functions that I knew I'd be calling often:


setIncline: function(){
  return { incX: this.randomNumber(-5,5), incY: this.randomNumber(-5,5) }
},

randomNumber: function(min,max){
  return Math.floor(Math.random()*(max-min+1)+min);
},

getBounds: function(shape){
  shapeBox = shape.node.getBoundingClientRect();
}

These simple set the slope at which a ball will travel (its trajectory), generate a random number between X and Y, and get the bounding box of each circle to determine distances / collisions / etc.

Then, you need to draw the circles to the screen:


makeCircles: function(){

  for (var i=0; i<this.drawingConfig.circles.amount;i++){
    var circleX = this.randomNumber(0, this.drawingConfig.canvas.width); //random coordinates for each circle
    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); //generates a random hex value
    var circleShape = this.s.circle(circleX, circleY, circleRadius);
    circleShape.attr({
      fill: circleFill
    });
    this.drawingConfig.circles.circleGroup.add(circleShape);

    var circleIncline = this.setIncline(); //set the trajectory of the ball
    var circleObj = { incline: circleIncline, shape: circleShape }; //create an object which will hold both the svg shape itself, and keep track of its incline
    this.drawingConfig.circles.circleArray.push(circleObj); //store each circle in an array so we can reference it later
  }

  this.update();
},

So once we place all the circles on the screen, we call our update function, which will run in a loop to animate all the circles, draw all the lines, and detect collisions:



update: function(){

  var lines = Snap.selectAll('line');
  lines.remove(); //remove all the lines that are currently on screen

  for (var i=0; i<this.drawingConfig.circles.amount; i++){ //for each circle on screen...
    var circle = this.drawingConfig.circles.circleArray[i];
    var circleX = circle.shape.node.cx.animVal.value; //get its position...
    var circleY = circle.shape.node.cy.animVal.value;
    this.move(circle,circleX,circleY);

    for (var j=0;j<i;j++){ //and compare it to the position of all the other circles...
      if (i != j){ //this makes sure we're not comparing the circle to itself...
        var circle2 = this.drawingConfig.circles.circleArray[j];
        var circle2X = circle2.shape.node.cx.animVal.value; //get the circles x and y position
        var circle2Y = circle2.shape.node.cy.animVal.value;
        var dist = mainModule.distance(circleX,circleY,circle2X,circle2Y); //calls a function that will measure the distance between the two circles
        if (dist <= mainModule.drawingConfig.circles.proximity){ //if the circles are closer than the range we specified in our configuration object...
          var line = mainModule.s.line(circleX, circleY, circle2X, circle2Y).attr({stroke: '#a6a8ab', strokeWidth: '1px'}); //draw a line between them
        }

        if (dist <= 10) { //collision //if the circles are touching, we have a collision, so we should change the circles' trajectories
          circle.incline = mainModule.setIncline();
          circle2.incline = mainModule.setIncline();
        }

      }
    }

  }

  setTimeout(function(){ mainModule.update(); },10); //run this loop every 10ms

},
So here, there's a few things going on. We're taking each circle, comparing it against each other circle's position, finding if it's close enough to draw a line between, and finding if it is colliding with another circle, at which point we'll change its trajectory. The distance function is explained below, but I should note a few things here that are not optimal and could be done better. If you have suggestions, let me know! One thing is that we probably don't need to compare all circles on screen - if they're not remotely close to each other, there's no point in measuring the distance. If this were a more complex prototype, I could implement the concept of a quad tree, which divides the screen into quadrants, and only compares objects against each other if they are in the same quadrant. Two - I'm pretty sure snap has a better way to get the x and y coordinates of something (or at least I'd hope so). I tried using
.attr('cx')
- but it returns it as a string, which I'd have to parse as an integer, and that seemed like it would slow things down. Three is that this could probably be running using
requestAnimationFrame
. Anyway, here's the distance function:

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);
},

Again - this could be optimized, but we're basically taking the distance between the two x coordinates and squaring it, as well as the y, and then returning the square root of those two distances added together - pythogorean theorem - a2+b2 = c2. Lastly, we just need to look at the function responsible for moving the circles:


move: function(circle,curX,curY){
  if (curX > this.drawingConfig.canvas.width || curX < 0) { //if we're outside of the boundaries of our svg canvas...
    circle.incline.incX = -circle.incline.incX; //reverse the slope so it heads the opposite direction
  }
  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 }); //set the position

},

So here, I could do a much more realistic collision equation, rather than just reversing direction, but this will work for now. Also, there are instances where the circles got stuck on the side of the screen - I probably need some padding in there when, if the circle goes beyond the screen boundaries, I set it equal to a distance within the boundaries (not the boundary itself) so it has somewhere else to animate to on the next cycle. But again, this will work for demonstration. And that's collision. If you can suggest improvements, I'd really appreciate it. Here's a demo of the whole thing working, with complete code .