how to do performant overlap/collision detection for hundreds of canvas circles?

I am drawing 100 circles of varying sizes to a canvas, and they must not overlap. these circles will also be animating from right to left (looping back around to the right edge of the canvas when they go off the screen), and will also have some vertical "bob" to them, which also cannot overlap any other circles.

Below is what I am currently attempting, which seems to be locking up the browser. I loop through the collection of circles and execute a detectOverlap() function, passing it the collection of circles.

The detectOverlap() function then loops through the circles, performing the following check:

detectOverlap: function (bubblesArr) {
    while (true) {
        var hit = 0;
        for (var i=0; i<bubblesArr.length; i++) {
            var circle = bubblesArr[i];
            var dx = this._x - circle._x;
            var dy = this._y - circle._y;
            var rr = this._radius + circle._radius;
            if (dx * dx + dy * dy < rr * rr) {
                hit++;
            }
        }
        if (hit == 0) {
            break; // didn't overlap, break out of while loop
        }
        // if we didn't break then there was an overlap somewhere. calc again.
        this._x = Math.round(Math.random() * this.stage.getWidth());
        this._y = Math.round(Math.random() * this.stage.getHeight());
    }
},

if hit == 0, the loop breaks and we assume there are no overlaps. Otherwise, we randomly calculate a new X/Y position and restart the process.

this seems inefficient. Any performant tips for doing this?

canvas class (entry point): this class is the "stage", which builds the bubble objects and then adds them to the canvas.

var $container;
var listData;
var bubbles = [];

function init(l, c) {
    $container = c;
    listData = l;

    // this just draws the canvas. full-width + 500px tall.
    var stage = new Konva.Stage({
      container: $container.selector,
      width: window.innerWidth,
      height: 500
    });

    // this creates the drawing layer where the bubbles will live
    layer = new Konva.Layer();

    // create an instance of the Bubble class for each element in the list.
    for (var i=0; i<listData.length; i++) {
        bubbles[i] = new celebApp.Bubble.Bubble(listData[i], stage);
    }

    /** TODO:::: FIGURE OUT COLLISION DETECTION */
    for (var i=0; i<bubbles.length; i++) {
        bubbles[i].detectOverlap(bubbles);
    }

    // create the Konva representation for our generated objects
    for (var i=0; i<bubbles.length; i++) {
        var b = bubbles[i];
        layer.add(new Konva.Circle({
            x: b._x,
            y: b._y,
            radius: b._radius,
            fill: b._fill,
            stroke: b._stroke,
            strokeWidth: b._strokeWidth,
        }));
    }

    // add the layer to the stage
    stage.add(layer);
}

Bubble class: This is the class which represents the data drawn to the screen. we need to ensure that none of these objects overlap one another.

var Bubble = function (listData, stage) {
    this.stage = stage;
    this._x = Math.round(Math.random() * stage.getWidth()),
    this._y = Math.round(Math.random() * stage.getHeight()),
    this._radius = Math.round(Math.random() * 80);
    this._fill = 'red';
    this._stroke = 'black';
    this._strokeWidth = 4;
    this._speed = 3;
};
Bubble.prototype = {
    detectOverlap: function (bubblesArr) {
        while (true) {
            var hit = 0;
            for (var i=0; i<bubblesArr.length; i++) {
                var circle = bubblesArr[i];
                var dx = this._x - circle._x;
                var dy = this._y - circle._y;
                var rr = this._radius + circle._radius;
                if (dx * dx + dy * dy < rr * rr) {
                    hit++;
                }
            }
            if (hit == 0) {
                break; // didn't overlap
            }
            this._x = Math.round(Math.random() * this.stage.getWidth());
            this._y = Math.round(Math.random() * this.stage.getHeight());
        }
    },
};

EDIT: just tried this based on the comment from @MarcB -- however, the browser still seems to lock up. Is the performance bottleneck being caused but 100 items all running their own while() loop?

for (var i=0; i<bubblesArr.length; i++) {
    var circle = bubblesArr[i];
    var combinedRadius = Math.abs(circle._radius + this._radius);
    var distance = Math.abs(this._x - circle._x);
    if (distance <= combinedRadius) {
        hit++;
    }
}

Answers:

Answer

This looks like a simple bug. You initialize a list of circles. Then for each circle in the list you count how many circles in the list overlap it. If you find an overlap you move the circle and try again.

But each circle will find itself in the list and find that it overlaps itself. You move it, and the same thing happens. This is an endless loop that never ends.

You need to have each circle look for circles other than itself that it overlaps with.

Algorithmically you can improve this overlap detection with a clever data structure like quad trees. That will let you immediately find all circles whose centers are within a smallish box of your circle, and let you find overlaps that way.

However if performance is a problem, there is no need to work that hard. Instead sort the circles by x-coordinate, draw vertical bands that are, say, 5 apart, then put each circle into all bands that it intersects. Now for each circle you can just search all bands that it intersects.

The next step up in efficiency would be to sort each band by y-coordinate so that you can do a binary search in that band to find all circles that intersect the band close enough to possibly intersect your circle. But these bands should generally be close to empty, so this is not much of a win.

Answer

If bubbles are only staged after the whole list is created (although you could also simulate random appearance by staging from the existing list in random order), why not avoid collision detection altogether by creating a random spacing?

There are probably more interesting and organic-like procedures but one simple method is to subdivide the space in queued order, maintaining a random cutoff for the current box size to account for the different radii. Something like this:

Describe the space as a tuple consisting of a middle point and a top left point.

  (middle,top_left)

Choose a random point on the top border of the box:

-----x-----------

Choose one random point on the left border of the box and one on the right:

-----x-----------
|    |          |
|    |          |
x-----          |
|    |          |
|    |----------x
|    |          |
-----------------

You now have four new middle and top left points, easily calculated.

Add the four new tuples to the queue and remove the tuple 
representing the parent box. Customize the function to get appropriately 
sized results.

We end up with a list of tuples representing different sized non-overlapping boxes, with given middle points. All that's left is to pick some of the boxes at random (the list could be hashed to avoid collisions) and place bubbles in them. (This assumes bubbles in the same horizontal space are moving at equal speed.)

Something like this (might need some tweaking):

var MIN_WIDTH = MIN_HEIGHT = 20;

function f(top_left,bottom_right){
  var w = bottom_right.x - top_left.x - 2 * MIN_WIDTH,
      h = bottom_right.y - top_left.y - 2 * MIN_HEIGHT,

      random_top = top_left.x + MIN_WIDTH 
                 + Math.ceil(Math.random() * w),

      random_left = top_left.y + MIN_HEIGHT
                  + Math.ceil(Math.random() * h),

      random_right = top_left.y + MIN_HEIGHT
                   + Math.ceil(Math.random() * h);

  var rectangle_1 = [top_left
                    ,{x: random_top, y: random_left}],

      rectangle_2 = [{x: top_left.x, y: random_left}
                    ,{x: random_top, y: bottom_right.y}],

      rectangle_3 = [{x: random_top, y: top_left.y}
                    ,{x: bottom_right.x, y: random_right}],

      rectangle_4 = [{x: random_top, y: random_right}
                    ,bottom_right];

  return [rectangle_1, rectangle_2, rectangle_3, rectangle_4];
}

console.log(JSON.stringify(f({x: 0, y: 0}, {x: 200, y: 200})))

Feed the whole canvas size into f the first time. Then you feed each of the four resultant rectangles into f so each gets subdivided again, and so on. Add a random stop to the recursion so that some rectangles will be larger than others (since those won't be subdivided). Place your bubbles inside the rectangled spaces. They won't collide, but the resulting arrangement might miss a more organic feel - maybe it could be tweaked by randomly "nudging" them a little.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.