diff --git a/hyphae_nft.py b/hyphae_nft.py new file mode 100644 index 0000000..349b886 --- /dev/null +++ b/hyphae_nft.py @@ -0,0 +1,211 @@ +#!/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) // 7 * 3 + 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 + + 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 + finally: + surface.write_to_png(f"out/hyphae_{run}_{rarity}.png") # Output to PNG + print(f'run {run} complete') + +if __name__ == '__main__': + main()