Circular Arrangement of Circles

@anon1712571 posted an interesting visual on Twitter that seemed like it would be cool to create in JavaScript! After a few hours of fiddling with random stuff, here is the JS version:

This isn’t a perfect replica, but with more effort it can be made to come very close. Like @nobody mentions, the circles shouldn’t overlap. They do in my example. The exact color space is different from the original image, so the colors are different. Lastly, the spacing of the circles is a bit haphazard. This is more noticeable as we go further out. In the original image, there are predefined peaks of circles every 30 or so degrees at the outside.

You can see the code and see a live version at the following pen:
https://codepen.io/kirupa/pen/WKKbpR?editors=1000#0

Hope you all find this interesting :slight_smile:

Cheers,
Kirupa

3 Likes

Hmm, I vaguely remember tackling the nonoverlapping aspect in a random placement grid one day a long time ago (>8 years?!). Found someone linking to it here: https://forums.envato.com/t/random-distribution-of-objects-in-space-with-no-overlap/75215/22

http://reclipse.net/kirupa/randomPlacementGrid.as

You could probably do the same thing on a polar coordinate grid instead.

Looks neat!

3 Likes

Cool!

1 Like

If “nobody” mentioned the issue about overlapping, why worry about it? But I do expect “more effort” next time. :wink:

PS: Uncle Sam would be proud of your new avatar.

1 Like

Looks good @Kirupa! I took a stab at recreating this as well, wanted to see if one could try to get a good head start on the initial positions by treating it as a phyllotaxis without any randomness associated to it.

Then with some simple particle simulation, we could try to get the disturbance by having the particles move towards the origin, collide with an invisible circle in the center (and with each other).

output

I bet with some tweaking of parameters one could recreate this. I just sketched this out in Processing-python mode.

def setup():
    global n,c,colorOptions,particles
    size(600,600)
    background(255)
    noStroke()
    particles = []
    
    #Colorpicked hex values from image
    colorOptions = ['#836871',
                    '#6d1a34',
                    '#af2944',
                    '#e94442',
                    '#e8af58',
                    '#ac7ead',
                    '#888f9f',
                    '#388c8c',
                    '#836871'][::-1]
    
    #Setup initial phyllotaxis positions
    c = 4
    angle=137.5
    for n in range(1000,3500):
        a = n * radians(angle)
        r = c * sqrt(n)
    

        newPoint = PVector(r*cos(a),
                           r*sin(a))
        newColor = lerpColors((a%TWO_PI)/TWO_PI,colorOptions) 
        particles.append(particle(newPoint,newColor))

def draw():
    background(255)
    
    #Move origin to center
    translate(width/2,height/2)
    
    for p in particles:
        
        #Check for collisions (inefficent)
        for other in particles:
            if(p!=other):
                d = PVector.dist(p.pos,other.pos)
                if(d <= p.r + other.r + 4):
                    p.moveAwayFrom(other)
        
        #Display and move particles
        p.display()
        p.move()
    
def lerpColors(amt, colorOptions):
    """Lerp function that takes an amount between 0 and 1, 
    and a list of colors and returns the appropriate
    interpolation"""
    
    if (len(colorOptions)==1): return colorOptions[0]
    spacing = 1.0/(len(colorOptions)-1)
    lhs = floor(amt/spacing)
    rhs = ceil(amt/spacing)
    
    try:
        return lerpColor(colorOptions[lhs], 
                         colorOptions[rhs], 
                         amt%spacing/spacing)
    except:
        return lerpColor(colorOptions[constrain(lhs, 0, len(colorOptions)-2)], 
                         colorOptions[constrain(rhs, 1, len(colorOptions)-1)], 
                         amt);
class particle(object):
    def __init__(self,pos,someColor):
        self.pos = pos
        self.someColor = someColor
        self.r = 2.5
    def move(self):
        """Move towards origin"""
        d = PVector.dist(self.pos,PVector(0,0))
        centerRadius = 100
        if(d <= self.r + centerRadius):
            direction = PVector.sub(PVector(0,0,0),self.pos).normalize().mult(-0.5)
        else:
            direction = PVector.sub(PVector(0,0,0),self.pos).normalize().mult(0.5)
        
        
        self.pos.add(direction)
    def moveAwayFrom(self,other):
        direction = PVector.sub(other.pos,self.pos).normalize().mult(-1)
        self.pos.add(direction)
    def display(self):
        noStroke()
        fill(self.someColor)
        ellipse(self.pos.x,self.pos.y,self.r*2,self.r*2)

Code has n**2 comparisons from collision detection, hence low fps. It would be better to test with a more optimized collision detection ds or library that implements it well.

4 Likes

This is fantastic! :slight_smile:

1 Like