4

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++;
    }
}
tdc
  • 5,174
  • 12
  • 53
  • 102
  • 1
    in technical terms, you can reduce a lot of the cartesian distance calculations by simply checking if the two circles x/y positions are within 2 radii of each other in the X or Y directions. e.g. radius 1 and radius 2 circles, if one has an X position of 5 and the other an X of 10, they can't possibly overlap because the distance is greater than their combined radiuses. that's just simple addition and greater/less testing. only do the full cartesian distances if they're within (r1+r2) distance of each other. – Marc B Jun 10 '16 at 18:41
  • 1
    Is there any reason you even mention canvas (and JavaScript... and HTML...)? This question should be **purely** algorithmic. Remove all the irrelevant code blocks and tags and explain what your expected/required behavior is and what you've tried. – Amit Jun 10 '16 at 18:43
  • 1
    @Amit: Languages sometimes matter when doing things like this. Languages like Matlab or libraries like D3 or box2d sometimes have built-in functions that's already optimized for doing this. Because the OP doesn't know the solution he cannot possibly know weather or not the language matter. – slebetman Jun 10 '16 at 18:55
  • @MarcB thank you for your input -- I've updated my original answer with an attempt at your solution, however the browser still seems to lock up. – tdc Jun 10 '16 at 19:07
  • 1
    Why do you continue to count the hits once you've found one? You could break out of the loop immediately. – m69's been on strike for years Jun 10 '16 at 21:17
  • @slebetman - the question is so specific and so logic oriented (it literally asks for an algorithm) there's no reason to concentrate on anything but the algorithm - HTML and it's canvas API are geared towards general purpose visualization and I expect anybody who does anything with canvas to know that. At most, a suitable question would be purely logic oriented and mention as a final remark the technologies where this will be used - justj case someone does know of an existing implementation of such an algorithm. – Amit Jun 11 '16 at 07:56
  • @Amit: Mentioning as a final remark will not attract people who would know of an existing implementation. Tagging it with related technology (in this case "javascript") would attract people who would know existing solution. When you see the list of questions on the front page you only see the title, a bit of the first paragraph and tags. You won't see the final remark. – slebetman Jun 11 '16 at 15:28
  • @slebetman - exactly. that's the point. the question is algorithmic. it's "reusability" (someone looking for the same information the future) is algorithmic. it's most likely solution will be algorithmic. **that's precisely what should be visible in the "front page" summary**. – Amit Jun 11 '16 at 15:34
  • I'm happy to remove the canvas-related bits if that is a point of contention? I was just trying to provide adequate data for my problem. Maybe someone has a canvas lib that does this automatically, I don't know, but I was just trying to provide adequate data. Sorry if it offended? – tdc Jun 11 '16 at 20:26
  • Could you perhaps make a jsfiddle we can play with, or post code that's complete enough to test? – גלעד ברקן Jun 12 '16 at 13:47

2 Answers2

1

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.

btilly
  • 43,296
  • 3
  • 59
  • 88
  • Interesting. I added some filtering so that it would only execute on a collection minus itself but it still appears to hang. Is this because I'm essentially triggering a `while(true)` loop for the 100 item collection? Do you have any tips on how I could re-work my implementation? Thanks so much. :) – tdc Jun 11 '16 at 20:23
  • @Prefix I would be willing to bet money that you still have a bug. Try it with just 4 circles. Have it print out debugging statements so you know where every bubble is. Trace its logic and action through with paper and pencil. Hopefully your remaining bug will become obvious. – btilly Jun 12 '16 at 19:52
  • I've observed it works fine with under ~15 circles, but any more than that and it just totally hangs the browser. It's also dependent on size -- some of the circles are fairly large (300px radius) and those always hang the browser.. If I do small circles it seems to work bettter – tdc Jul 01 '16 at 17:29
0

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.

גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • so this would require the bubbles to be grouped by radii? not really the visual effect I'm looking for, more of a random fill sort type of thing. – tdc Jun 11 '16 at 20:27
  • @Prefix not sure what you mean by "grouped by radii," could you explain? The algorithm works like this: first, the entire space gets partitioned into four random sized rectangles, then each one of those four produces another four, smaller random sized rectangles, and so on according to your specifications. Since I noticed that your code assigns random radii to bubbles, I suggestd making a similar determination by randomly "cutting off" the queuing of the next partition so you'll be assured to get a variety of box sizes. You end up with the entire space randomly partitioned with no overlaps. – גלעד ברקן Jun 12 '16 at 03:38
  • hm. I think im just having a hard time visualizing this conceptually without a code example. I'd love to see how this works and potentially implement it into my code, would you mind mocking up how I'd do this? (psuedo-code is fine too). cheers – tdc Jul 01 '16 at 17:36