#!/usr/bin/env python3 import cairo import math import random import threading import time import socket import colorsys from utils import circle_fill from utils import circle_clip_fill from utils import random_color import numpy WIDTH, HEIGHT = 1024, 1024 ANGLE_RANDOM_MIN = -0.6 ANGLE_RANDOM_MAX = 0.6 # how much to shrink each consecutive circle SHRINK = 0.00004 start_r = 0.01 NODE_ARRAY = None start_hue = 0 # should be ~ 1 px MIN_RADIUS = ((1/WIDTH) + (1/HEIGHT)) / 2 def chunker(seq, size): return (seq[pos:pos + size] for pos in range(0, len(seq), size)) class Branch(): def __init__(self, idx, ctx, x, y, r, ang, mother=None): #ctx.set_source_rgb(255, 0, 0) self.nodes = [Node(ctx, x, y, r, ang, dry=True)] #ctx.set_source_rgb(0,0, 0) self.idx = idx self.ctx = ctx self.ended = False self.ignores = [] self.first = True self.mother = mother def _last_node(self): return self.nodes[-1] def set_ignores(self, ignores): self.ignores = ignores def place_next(self, branches): if not self.ended: last = self._last_node() next_x = last.r * math.sin(last.ang) + last.x next_y = last.r * math.cos(last.ang) + last.y next_r = last.r - SHRINK # too small? if next_r < MIN_RADIUS: self.ended = True return False # did we hit canvas edge? # if next_x + next_r > 1 or next_x - next_r < 0: if (math.pow(next_x - 0.5, 2) + math.pow(next_y - 0.5, 2)) > math.pow(0.4, 2): self.ended = True return False if next_y + next_r > 1 or next_y - next_r < 0: self.ended = True return False last_nodes = self.nodes[-2:] # did we hit another circle? # search radius is radius of biggest possible circle + our radius search_radius = next_r + start_r search_box_x1 = next_x - search_radius search_box_x2 = next_x + search_radius search_box_y1 = next_y - search_radius search_box_y2 = next_y + search_radius filtered = NODE_ARRAY[NODE_ARRAY['x'] > search_box_x1] filtered = filtered[filtered['x'] < search_box_x2] filtered = filtered[filtered['y'] > search_box_y1] filtered = filtered[filtered['y'] < search_box_y2] # remove previous nodes for ln in last_nodes: filtered = filtered[ (filtered['x'] != ln.x) & (filtered['y'] != ln.y) ] for possible_hit in filtered: if circles_intersect(*possible_hit, next_x, next_y, next_r): self.ended = True return False next_ang = last.ang + (random.uniform(ANGLE_RANDOM_MIN, ANGLE_RANDOM_MAX) * (1 - 40*last.r)) self.nodes.append(Node(self.ctx, next_x, next_y, next_r, next_ang)) if self.first: self.first = False first = self.nodes[0] if self.mother: self.ctx.set_operator(cairo.Operator.DEST_OVER) circle_fill(self.ctx, first.x, first.y, first.r) self.ctx.set_operator(cairo.Operator.OVER) else: circle_fill(self.ctx, first.x, first.y, first.r) return True def circles_intersect(x1, y1, r1, x2, y2, r2): distance = math.sqrt(math.pow(abs(x2 - x1), 2) + math.pow(abs(y2 - y1), 2)) combined_r = r1 + r2 return distance < combined_r class Node(): def __init__(self, ctx, x, y, r, ang, dry=False): global NODE_ARRAY self.x = x self.y = y self.r = r self.ang = ang if not dry: NODE_ARRAY = numpy.append(NODE_ARRAY, numpy.array([(x, y, r)], dtype=[('x', 'float'), ('y', 'float'), ('r', 'float')]) ) circle_fill(ctx, x, y, r) def __ne__(self, other): return self.x != other.x or self.y != other.y or self.r != other.r or self.ang != other.ang def grow_sub(ctx, branch, branches, new_subs): # create a sub branch based on length sub_branches = len(branch.nodes) // 8 * 2 if sub_branches == 0: return #sub_branches = 4 #print(f'creating {sub_branches} subs') for i in range(sub_branches): for n in range(20): # attempts at growing branches found_ang = False start_node = random.choice(branch.nodes) for a_try in range(10): # start perpendicular to our last angle start_angle = start_node.ang + random.choice([90, -90]) + random.uniform(-30, 30) start_x = (start_node.r * 1.2) * math.sin(start_angle) + start_node.x start_y = (start_node.r * 1.2) * math.cos(start_angle) + start_node.y start_r = start_node.r * 0.8 new_branch = Branch(0, ctx, start_x, start_y, start_r, start_angle, mother=start_node) if new_branch.place_next(branches): found_ang = True break if found_ang: break grow_branch_until_ended(new_branch, branches) new_branch.set_ignores(branch.nodes) branches.append(new_branch) new_subs.append(new_branch) def grow_branch_until_ended(branch, branches): while not branch.ended: branch.place_next([b for b in branches if b != branch]) def grow_subs(ctx, subs, branches): source = ctx.get_source() new_subs = [] for branch in subs: grow_sub(ctx, branch, branches, new_subs) #for branch in new_subs: # grow_branch_until_ended(branch, branches) return new_subs def main(): global start_hue global NODE_ARRAY for run in range(32): random.seed() start_hue = random.uniform(0, 1) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, WIDTH, HEIGHT) ctx = cairo.Context(surface) ctx.scale(WIDTH, HEIGHT) # Normalizing the canvas # place seeds branches = [] r, g, b = colorsys.hsv_to_rgb(start_hue, 1.0, 1.0) #r, g, b = random_color() ctx.set_source_rgb(r, g, b) branches.append(Branch(0, ctx, 0.5, 0.4, start_r, 180)) branches.append(Branch(1, ctx, 0.4, 0.5, start_r, 270)) branches.append(Branch(2, ctx, 0.6, 0.5, start_r, 90)) branches.append(Branch(3, ctx, 0.5, 0.6, start_r, 0)) NODE_ARRAY = numpy.array([ (0.5, 0.4, start_r), (0.4, 0.5, start_r), (0.6, 0.5, start_r), (0.5, 0.6, start_r) ], dtype = [('x', 'float'), ('y', 'float'), ('r', 'float')]) # grow initial branches print('growing initial branches') for branch in branches: grow_branch_until_ended(branch, branches) subs = branches rarity = 3 + random.randint(0, 5) print(f'rarity: {rarity}') try: for x in range(rarity): if subs == None: return r, g, b = colorsys.hsv_to_rgb(start_hue + x * 0.05, 1.0, 1.0) ctx.set_source_rgb(r, g, b) subs = grow_subs(ctx, subs, branches) print(f'iteration {x} done') #surface.write_to_png("out/hyphae.png") # Output to PNG r, g, b = colorsys.hsv_to_rgb(start_hue - 0.05, 1.0, 0.2) ctx.set_source_rgb(r, g, b) ctx.set_operator(cairo.Operator.DEST_OVER) circle_fill(ctx, 0.5, 0.5, 0.4) finally: surface.write_to_png(f"out/hyphae_{run}_{rarity}.png") # Output to PNG print(f'run {run} complete') if __name__ == '__main__': main()