from vector import Vector from math import pi from PIL import Image, ImageDraw, ImageColor import random WIDTH = 1448 # Final image will be twice this, resolution low HEIGHT = 1024 # for performance reasons (this also leaves some artifacts) SS = 8 # Supersampling, for anti-aliasing THICKNESS = 1 # Line thickness RECURSION_LIMIT = 100 # Just for safety START_LINES = 100 # How many lines to start with. MARGINS = 25 # Pillow will leave margins around the image MIN_LENGTH = 3 # Line has to be N pixels long before it checks for collisions MIN_SPLIT = 12 # Minimum length of line for it to spawn new lines GRADIENT_MODE = True # Gradient mode, uses colors from GRAD_COLOR instead random.seed("empatien") # Enter your seed here or comment out COLORS = ["#3B5284", "#5Ba8A0", "#CBE54E", "#94B447", "#5D6E1E"] # random.shuffle(COLORS) BG_COLOR = ImageColor.getrgb("#fff") GRAD_COLOR1 = ImageColor.getrgb("#e56900") GRAD_COLOR2 = ImageColor.getrgb("#bc0000") ANGLE = pi / 4 q = pi / 2 ANGLES = [ANGLE, q + ANGLE, q * 2 + ANGLE, q * 3 + ANGLE] LINES = [] def interpolate(f_co, t_co, interval): det_co =[(t - f) / interval for f , t in zip(f_co, t_co)] for i in range(interval): yield [round(f + det * i) for f, det in zip(f_co, det_co)] def get_color(rec): if rec >= len(COLORS): rec = rec % len(COLORS) return ImageColor.getrgb(COLORS[rec]) def ccw(a,b,c): return (c.y-a.y) * (b.x-a.x) > (b.y-a.y) * (c.x-a.x) def intersect(l1, l2): a, b = l1.start, l1.end c, d = l2.start, l2.end return ccw(a,c,d) != ccw(b,c,d) and ccw(a,b,c) != ccw(a,b,d) def check_bounds(v): return (v.x > 0 and v.x < WIDTH) and (v.y > 0 and v.y < HEIGHT) class Line(object): def __init__(self, start, angle, rec=0): self.rec = rec self.start = start self.end = Vector(start.x, start.y) a = Vector(1, 0) a.rotate(angle) self.angle_rads = angle self.angle = a self.checking = False self.start_finished = False self.end_finished = False self.finished = False def grow(self): if not self.checking: if self.length() > MIN_LENGTH: self.checking = True if not self.end_finished: self.end += self.angle if not check_bounds(self.end): self.end_finished = True elif self.checking: temp_line = Line(self.end, 0) temp_line.end = self.end - self.angle for l in LINES: if l == self: continue if intersect(l, temp_line): self.end_finished = True break elif not self.start_finished: self.start -= self.angle if not check_bounds(self.start): self.start_finished = True elif self.checking: temp_line = Line(self.start, 0) temp_line.end = self.start + self.angle for l in LINES: if l == self: continue if intersect(temp_line, l): self.start_finished = True break def spawn(self): normal1 = self.angle_rads + pi / 2 normal2 = self.angle_rads - pi / 2 l1 = Line(self.midpoint(), normal1 - ANGLE, rec=self.rec + 1) l2 = Line(self.midpoint(), normal2 + ANGLE, rec=self.rec + 1) l1.end_finished = True l2.end_finished = True LINES.append(l1) LINES.append(l2) def length(self): return (self.end - self.start).getLength() def midpoint(self): return Vector((self.start.x + self.end.x) / 2, (self.start.y + self.end.y) / 2) def update(self): if not self.end_finished or not self.start_finished: self.grow() elif not self.finished: if self.rec < RECURSION_LIMIT and self.length() > MIN_SPLIT: self.spawn() self.finished = True def __repr__(self): return "Line[s: {0}, e: {1}]".format(self.start, self.end) if __name__ == "__main__": # Spawn initial lines at random locations for i in range(START_LINES): x = random.gauss(WIDTH / 2, WIDTH - MARGINS * 4) y = random.gauss(HEIGHT / 2, HEIGHT - MARGINS * 4) LINES.append(Line(Vector(x, y), random.choice(ANGLES))) # Grows until 50000 iterations or no lines left to grow. # Prints out progress info not_finished = True i = 0 while i < 50000 and not_finished: i = 0 unfinished = 0 max_rec = 0 for l in LINES: if l.rec > max_rec: max_rec = l.rec if not l.finished: unfinished += 1 l.update() if not unfinished: not_finished = False print("Left: {0} Total: {1} Progress: {2:.2f} Recursion: {3}".format(unfinished, len(LINES), (1.0 - unfinished / len(LINES)) * 100, max_rec)) bg_im = Image.new("RGBA", (int((WIDTH + MARGINS * 2) * SS), int((HEIGHT + MARGINS * 2) * SS)), BG_COLOR) if GRADIENT_MODE: # Gradient can be tweaked here grad_im = Image.new("RGBA", (int((WIDTH + MARGINS * 2) * SS), int((HEIGHT + MARGINS * 2) * SS)), 0) draw_grad = ImageDraw.Draw(grad_im) for i, color in enumerate(interpolate(GRAD_COLOR1, GRAD_COLOR2, grad_im.height)): draw_grad.line([(0, i), (grad_im.width, i)], tuple(color), width=1) im = Image.new("RGBA", (int((WIDTH + MARGINS * 2) * SS), int((HEIGHT + MARGINS * 2) * SS)), (255, 255, 255, 0)) draw = ImageDraw.Draw(im) for l in reversed(LINES): sx = MARGINS + l.start.x sy = MARGINS + l.start.y ex = MARGINS + l.end.x ey = MARGINS + l.end.y if GRADIENT_MODE: c = (0, 0, 0, 255 - l.rec * 3) # Lines get weaker the deeper in recursion they are draw.line((sx * SS, sy * SS, ex * SS, ey * SS), fill=c, width=THICKNESS * SS) else: draw.line((sx * SS, sy * SS, ex * SS, ey * SS), fill=get_color(l.rec), width=THICKNESS * SS) if GRADIENT_MODE: im_blended = Image.composite(grad_im, bg_im, im) else: bg_im.paste(im, (0, 0), im) im_blended = bg_im im = im_blended.resize((WIDTH * 2, HEIGHT * 2), resample=Image.ANTIALIAS) im.show()