What is generative art and why am I interested in it?
In general generative art is defined as art that has been (at least in some part) created by using an autonomous system. I think of it as art that is created by some system, but the artist (not the system although some may argue that the system is the true artist) doesn't know exactly how it will turn out. While the artist can tweak some parts of the system (usually the inputs) they do not have full control of the generation of the artwork.
I recently discovered generative art after reading an excellent article by Anders Hoff, titled On Generative Algorithms. I found it to be a very interesting read, well worth your time, and it inspired me to make my own generative algorithms. In particular the first example, Hyphae, interested me. I thought, hey I bet I can do that in a couple lines of python!
Enter Circlepusher
The goal is to push some circles around on a canvas that don't intersect with each other and randomly split into smaller circles to be pushed. Let's start by making those circles, we need something to store a location, a radius, and some random angle
import random class Circle: def __init__(self, radius, location): """ :param radius: radius of circle in pixels :param location: two tuple of x,y """ self.radius = radius self.location = location self.angle = random.randint(0, 360) @property def x(self): return self.location[0] @property def y(self): return self.location[1]
What good's a circle if you can't see it (and is it even really a circle at all)? There's probably other ways to draw circles, but I'm going to use PIL(low) because it was pretty simple. Also I'm going to use some globals because I don't care if this code is pretty, I want it to be easy to hack.
import random from PIL import Image, ImageDraw image = Image.new('L', (500, 500), 'white') image_draw = ImageDraw.Draw(image) class Circle: def __init__(self, radius, location): """ :param radius: radius of circle in pixels :param location: two tuple of x,y """ self.radius = radius self.location = location self.angle = random.randint(0, 360) def draw(self): image_draw.ellipse((self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius), fill='black') circle = Circle(10, (250, 250)) circle.draw() image.show()
Now we have a nice big black circle in the middle of our canvas, here's some docs on PIL.Image.new (the L we specify tells Pillow that this will be a grayscale image, see PIL Modes if you want to learn more).
Now it's time to push it (push it real good)!
import random from PIL import Image, ImageDraw ... Class Circle: ... def push(self): # Let's step by 1/4 of the radius each time step = self.radius / 4 rad_angle = math.radians(self.angle) self.location = (self.x + step*math.cos(rad_angle), self.y + step*math.sin(rad_angle)) circle = Circle(10, (250, 250)) for _ in range(100): circle.draw() circle.push() image.show()

Woo! We have a single clock hand, not too impressive huh? However if we continue to increase the number of iterations we'll soon run off the edge of the canvas, let's prevent that by adding a check to the push function
import random import math from PIL import Image, ImageDraw image_bounds = (500, 500) image = Image.new('L', image_bounds, 'white') image_draw = ImageDraw.Draw(image) class Circle: def __init__(self, radius, location): """ :param radius: radius of circle in pixels :param location: two tuple of x,y """ self.radius = radius self.location = location self.angle = random.randint(0, 360) def draw(self): image_draw.ellipse((self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius), fill='black') def push(self): # Let's step by 1/4 of the radius each time step = self.radius / 4 rad_angle = math.radians(self.angle) next_step = (self.x + step*math.cos(rad_angle), self.y + step*math.sin(rad_angle)) if self.within_bounds(next_step): self.location = next_step @staticmethod def within_bounds(location): if location[0] < 0 or location[0] > image_bounds[0] or location[1] < 0 or location[1] > image_bounds[1]: return False return True ...
This doesn't take into account the radius of the circle being pushed, it only checks it's center. Why should we limit ourselves to 2 circles? Let's make 10!
for _ in range(10): circle = Circle(10, (250, 250)) for __ in range(100): circle.draw() circle.push()

Well... it is a thing, but not a very good thing. Let's make it so that they don't step on each other. I'll start by adding the concept of deactivating a circle, if it bumps into another circle, let's not keep trying to push it. Note that the method of detecting "collision" is pretty "stupid" since I'm just checking a single pixel ahead of us, we could still collide at any of the other pixels we're about to paint. Next let's make sure we don't step over the edge of the canvas. Finally let's add a little spin to the movement of our circles, a curve which will be added to the angle at each step.
import random import math from PIL import Image, ImageDraw image_bounds = (500, 500) image = Image.new('L', image_bounds, 'white') image_draw = ImageDraw.Draw(image) class Circle: def __init__(self, radius, location): """ :param radius: radius of circle in pixels :param location: two tuple of x,y """ self.radius = radius self.location = location self.angle = random.randint(0, 360) self.curve = random.randint(-45, 45)/100 self.active = True @property def x(self): return self.location[0] @property def y(self): return self.location[1] def draw(self): if not self.active: return image_draw.ellipse((self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius), fill='black') def push(self): if not self.active: return # Let's step by 1/4 of the radius each time step = self.radius / 4 rad_angle = math.radians(self.angle) next_step = (self.x + step*math.cos(rad_angle), self.y + step*math.sin(rad_angle)) # Stepping by 1/4 of the radius will put us still inside our current radius, so let's look a bit further ahead big_step = (self.x + self.radius*2*math.cos(rad_angle), self.y + self.radius*2*math.sin(rad_angle)) if self.within_bounds(next_step) and self.free_spot(big_step): self.location = next_step else: self.active = False if self in circles: circles.remove(self) self.angle = (self.angle + self.curve) % 360 @staticmethod def within_bounds(location): if location[0] < 0 or location[0] > image_bounds[0] or location[1] < 0 or location[1] > image_bounds[1]: return False return True def free_spot(self, spot): # Simply check the canvas to see if the passed spot is white return self.within_bounds(spot) and image.getpixel(spot) == 255 for _ in range(10): circle = Circle(10, (250, 250)) for __ in range(1000): circle.draw() circle.push() image.show()
Now we have lines that curve beautifully, don't intersect with each other and stop without going over the edge of the canvas. Finally we have something worth tinkering with.

Making Babies
Time to mutate! Let's create some smaller versions of our tentacles that randomly come off of our main "branches". Babies should probably be smaller then their parent(s?) so let's make their radius 75% of their parent. While we're at it let's randomize the starting location of our circles.
import random import math from PIL import Image, ImageDraw image_bounds = (500, 500) image = Image.new('L', image_bounds, 'white') image_draw = ImageDraw.Draw(image) circles = [] ... class Circle: ... def push(self): if not self.active: return if self.should_make_baby(): circles.append(self.make_baby()) # Let's step by 1/4 of the radius each time step = self.radius / 4 rad_angle = math.radians(self.angle) next_step = (self.x + step*math.cos(rad_angle), self.y + step*math.sin(rad_angle)) # Stepping by 1/4 of the radius will put us still inside our current radius, so let's look a bit further ahead big_step = (self.x + self.radius*2*math.cos(rad_angle), self.y + self.radius*2*math.sin(rad_angle)) if self.within_bounds(next_step) and self.free_spot(big_step): self.location = next_step else: self.active = False if self in circles: circles.remove(self) self.angle = (self.angle + self.curve) % 360 ... @staticmethod def should_make_baby(): # 1/50 chance to make a baby return not random.randint(0, 20) def make_baby(self): return Circle(self.radius*.75, self.location) for _ in range(10): circles.append(Circle(10, (random.randint(0, image_bounds[0]), random.randint(0, image_bounds[1])))) for _ in range(1000): for circle in circles: circle.draw() circle.push() image.show()
Not the prettiest thing, but nice and noisey.

Comments !