I had a go at your problem with a different approach.
The idea is to add objects to the circle one at a time.
Each object defines a collision area around it.
When a new object is added, it is checked for collision with the existing ones.
If a collision occurs, the new objects is added to a "collision group" containing all objects that will require relative position adjustments to avoid overlaping each other.
If no overlap is detected, a new collision group containing only the new object is created.
After accepting a new object, a group will cover a portion of the circle centered around its objects center of gravity and just wide enough to hold all the objects with no overlap.
Each time a collision group grows, it is checked against all the other groups for overlap, and merged with the first intersecting group detected.
The process is repeated with the resulting group until no more merges can be done.
Once all objects have been added, position adjustment are computed within each group to spread the objects evenly.
See a fiddle here
var CollisionResolver = function (radius) { this.radius = radius || 3.5; // degrees this.groups = []; this.collision = {}; this.objects = {}; this.num_obj = 0; } CollisionResolver.prototype = { // add an object to the circle add: function (obj) { function sort_group () { group.elem.sort(function(a,b) {return a.pos>b.pos; }); var middle = 0; for (var i = 0; i != group.elem.length ; i++) middle += group.elem[i].pos; middle /= group.elem.length; var range = this.radius * group.elem.length; group.min = middle-range; group.max = middle+range; // see if the expanded group now overlaps another for (var g = 0 ; g != this.groups.length ; g++) { var group2 = this.groups[g]; if (group2 == group) continue; for (var offset = 0 ; offset <720 ; offset += 360) { if ( (group2.max+offset > group.min) && (group2.min+offset < group.max)) { // merge groups for (var i = 0 ; i != group2.elem.length ; i++) group2.elem[i].pos += offset; group.elem = group.elem.concat(group2.elem); this.groups.splice (g, 1); // try again with the merged group sort_group.call (this,group); return; } } } } // store the object obj.id = this.num_obj; this.objects[this.num_obj++] = obj; // see if the object falls within a collision group for (var g in this.groups) { var group = this.groups[g]; for (var offset = 0 ; offset <720 ; offset += 360) { if ( (obj.pos+offset+this.radius > group.min) && (obj.pos+offset-this.radius < group.max)) { // insert the object into the collision group obj.pos += offset; group.elem.push (obj); sort_group.call (this, group); return; } } } // create a new singleton collision group var group = { elem: [obj], min :obj.pos-this.radius, max :obj.pos+this.radius }; this.groups.push (group); }, resolve: function () { // spread the objects inside each group var groups = this.groups; for (var i = 0 ; i != groups.length ; i++) { var group = groups[i]; for (var o = 0 ; o != group.elem.length ; o++) { group.elem[o].pos = (group.min + (2*o+1) * this.radius + 360) % 360; } } // return the positions var res = []; for (var i = 0 ; i != this.num_obj ; i++) res.push (this.objects[i].pos); return res; } } function spread (positions, radius) { var collider = new CollisionResolver (radius); // initialize object positions for (var i = 0; i != positions.length ; i++) { collider.add ({ pos:positions[i]} ); } // resolve collisions return collider.resolve(); }
It is still a bit rough around the edges (I'm not quite sure the 360°->0 transitions handling is really foolproof), but it seems to do the job in most cases. I'd say that's good enough for a proof of concept.
This algorithm does not enforce your requirement of a maximal (angular) distance between the original and adjusted positions. On the other hand, it guarantees no overlap as long as there is enough room to fit all the objects into the circle.