Wednesday, February 6, 2013

Icon collision detection/resolution in javascript

Let's say you have an application that has several icons, like a desktop.  Since this application is web based, you get the coordinates of where to draw these icons from the backend through an API request.  Sometimes you can have coordinates that are in the same spot.  This can confuse your user because to them, they might think there is only one icon, when two or more are on top of each other.  This blog attempts to show possible solutions to this.

One approach to this problem is to try to draw colliding objects in a ramdom location around the original collision.  For example, if you had two objects drawn at (50, 50), you could choose a random radius between 0 and 1 (or some max radius), and a random angle to place between 0 and 2π.  Then you would take the angle and get the sin/cos values multiplied by the radius.  The code would look something like this.

// I'm sure there are point libraries out there but this is just a quick mockup.
var Point = function(x,y){ this.x = x, this.y = y; };
Point.prototype = {
 add: function(other) {
  return new Point(this.x + other.x, this.y + other.y);
 }
};

var MAX_RADIUS = 50;
function getNextPosition(point) {
 var theta = Math.random() * 2 * Math.PI;
 var radius = Math.random() * MAX_RADIUS;
 var x = Math.cos(theta) * radius;
 var y = Math.sin(theta) * radius;
 var offset = new Point(x, y);
 return point.add(offset);
};


Here is an example of running this code on 20 points.  I highlighted the original point red.
This is an ok attempt, but we can improve upon this.  What if we wanted the points to be drawn around the initial point in a circle.  Then when that circle is exhausted, we move to the next outer circle.  After filling out 2-3 circles, we fallback to the random point algorithm above.  Also when we go to the next circle, we double the number of points in the next circle since there is more space.

The mechanism we use to determine what part of the circle we are on is to use an iterator.  The iterator is calculated based on the number of times you have collided with this point.  We take this iterator and map it to the good old unit circle.



If we were walking around the unit circle, you could do what the above diagram does.  Ignoring the values divided by 3 or 6, you could pretend each point is walking around the unit circle in 8ths.  If you start at iterator 0, you want the value of angle 0, which yields cos(0) -> 1, sin(0) -> 0, or Point (1,0).  Then you move to iterator 2, which puts you at the angle, π/4, and yields the Point (√2/2, √2/2).  Iterator 3 yields the angle, π/2, Point (0, 1), etc.

So if you wanted to draw 4 points around the initial point using an iterator, the basic calculation looks like this:

var points_per_circle = 4;
var radius = 10; // the size of your icon
var current_circle = 1;
var angle = (iterator / points_per_circle) * (2 * Math.PI);
var x = Math.cos(angle) * radius * current_circle;
var y = Math.sin(angle) * radius * current_circle;

Given this calculation, next we need a way to determine if we have made a collision.  For this, I just use a hash and my key is the toString function on my Point object.  It needs to be defined otherwise you just get the string "[object]" when you call toString.

The data we store at the collision is our iterator.  We increment this number each time we have a collision.  This helps us remember the last iteration spot we used to calculate our point and will help us determine the next point.


var collision_hash = {};
var MAX_CIRCLES = 3;
var ELEMENT_RADIUS = 10;
Point.prototype.toString = function() { return '(' + this.x + ',' + this.y + ')'; };

function getNextPosition(point) {
 // increment that we have visited this point.
 collision_hash[point] = collision_hash[point] == undefined ? 1 : collision_hash[point] + 1;
 var iterator = collision_hash[point] - 1;

 if ( iterator == 0){
  return point;
 }
 
 var radius = ELEMENT_RADIUS; // the size of the element
 var current_circle = 1; // to extend the radius out to the next circle
 var points_per_circle = 4;
 while (iterator > points_per_circle && current_circle < MAX_CIRCLES) {
  iterator = iterator - points_per_circle;
  // goto the next circle and double the number of points
  current_circle++; 
  points_per_circle = points_per_circle * 2; 
 }

 // calculate the angle by slicing a circle up using the iterator
 var angle = (iterator / points_per_circle) * (2 * Math.PI);

 // if we have exhuasted 3 circles
 if (iterator > points_per_circle) {
  // randomize
  angle = Math.random() * 2 * Math.PI;
  radius = Math.random() * ELEMENT_RADIUS;
 }
 var x = Math.cos(angle) * radius * current_circle;
 var y = Math.sin(angle) * radius * current_circle;
 var offset = new Point(x, y);
 return point.add(offset);
};

Here are a few examples of after 4, then 12, then 28, then 40 collisions




This algorithm is fine, but it was brought to my attention that we don't have to have the wasted space in between.  It turns out that the better fit of things in a circle around an object is a hexagonal grid.  Also this algorithm doesn't keep track of the collision points as future collision points.  For example, if I collide with a point at (50,50) and return a point of (51, 50), I need to remember that I have visited (51,50) as well so that if I visit a different point that is (51,50) I draw a circle around that.

Drawing a hexagonal circle around a point is easy.  Just use π/3 to iterate around the circle.