Created
February 16, 2020 15:16
-
-
Save NiclasEriksen/ef6882d05fcceb2565953a4f8678fa5c to your computer and use it in GitHub Desktop.
Recursive line geometry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf8 -*- | |
"""vector.py: A simple little Vector class. Enabling basic vector math. """ | |
__author__ = "Sven Hecht" | |
__license__ = "GPL" | |
__version__ = "1.0.1" | |
__maintainer__ = "Sven Hecht" | |
__email__ = "[email protected]" | |
__status__ = "Production" | |
from random import * | |
from math import * | |
class Vector: | |
def __init__(self, x=0, y=0): | |
self.x = 0 | |
self.y = 0 | |
if isinstance(x, tuple) or isinstance(x, list): | |
y = x[1] | |
x = x[0] | |
elif isinstance(x, Vector): | |
y = x.y | |
x = x.x | |
self.set(x,y) | |
@staticmethod | |
def random(size=1): | |
sizex = size | |
sizey = size | |
if isinstance(size, tuple) or isinstance(size, list): | |
sizex = size[0] | |
sizey = size[1] | |
elif isinstance(size, Vector): | |
sizex = size.x | |
sizey = size.y | |
return Vector(random() * sizex, random() * sizey) | |
@staticmethod | |
def randomUnitCircle(): | |
d = random()*pi | |
return Vector(cos(d)*choice([1,-1]), sin(d)*choice([1,-1])) | |
@staticmethod | |
def distance(a, b): | |
return (a - b).getLength() | |
@staticmethod | |
def angle(v1, v2): | |
return acos(v1.dotproduct(v2) / (v1.getLength() * v2.getLength())) | |
@staticmethod | |
def angleDeg(v1, v2): | |
return Vector.angle(v1,v2) * 180.0 / pi | |
def rotate(self, rads): | |
ca = cos(rads) | |
sa = sin(rads) | |
x = ca * self.x - sa * self.y | |
y = sa * self.x + ca * self.y | |
self.set(x, y) | |
def set(self, x,y): | |
self.x = x | |
self.y = y | |
def toArr(self): return [self.x, self.y] | |
def toInt(self): return Vector(int(self.x), int(self.y)) | |
def toIntArr(self): return self.toInt().toArr() | |
def getNormalized(self): | |
if self.getLength() != 0: | |
return self / self.getLength() | |
else: return Vector(0,0) | |
def dotproduct(self, other): | |
if isinstance(other, Vector): | |
return self.x * other.x + self.y * other.y | |
elif isinstance(other, tuple) or isinstance(other, list): | |
return self.x * other[0] + self.y * other[1] | |
else: | |
return NotImplemented | |
def __add__(self, other): | |
if isinstance(other, Vector): | |
return Vector(self.x + other.x, self.y + other.y) | |
elif isinstance(other, tuple) or isinstance(other, list): | |
return Vector(self.x + other[0], self.y + other[1]) | |
elif isinstance(other, int) or isinstance(other, float): | |
return Vector(self.x + other, self.y + other) | |
else: | |
return NotImplemented | |
def __sub__(self, other): | |
if isinstance(other, Vector): | |
return Vector(self.x - other.x, self.y - other.y) | |
if isinstance(other, tuple) or isinstance(other, list): | |
return Vector(self.x - other[0], self.y - other[1]) | |
elif isinstance(other, int) or isinstance(other, float): | |
return Vector(self.x - other, self.y - other) | |
else: | |
return NotImplemented | |
def __rsub__(self, other): | |
if isinstance(other, Vector): | |
return Vector(other.x - self.x, other.y - self.y) | |
elif isinstance(other, tuple) or isinstance(other, list): | |
return Vector(other[0] - self.x, other[1] - self.y) | |
elif isinstance(other, int) or isinstance(other, float): | |
return Vector(other - self.x, other - self.y) | |
else: | |
return NotImplemented | |
def __mul__(self, other): | |
if isinstance(other, Vector): | |
return Vector(self.x * other.x, self.y * other.y) | |
elif isinstance(other, tuple) or isinstance(other, list): | |
return Vector(self.x * other[0], self.y * other[1]) | |
elif isinstance(other, int) or isinstance(other, float): | |
return Vector(self.x * other, self.y * other) | |
else: | |
return NotImplemented | |
def __div__(self, other): | |
if isinstance(other, Vector): | |
return Vector(self.x / other.x, self.y / other.y) | |
elif isinstance(other, tuple) or isinstance(other, list): | |
return Vector(self.x / other[0], self.y / other[1]) | |
elif isinstance(other, int) or isinstance(other, float): | |
return Vector(self.x / other, self.y / other) | |
else: | |
return NotImplemented | |
def __rdiv__(self, other): | |
if isinstance(other, Vector): | |
return Vector(other.x / self.x, other.y / self.y) | |
elif isinstance(other, tuple) or isinstance(other, list): | |
return Vector(other[0] / self.x, other[1] / self.y) | |
elif isinstance(other, int) or isinstance(other, float): | |
return Vector(other / self.x, other / self.y) | |
else: | |
return NotImplemented | |
def __pow__(self, other): | |
if isinstance(other, int) or isinstance(other, float): | |
return Vector(self.x ** other, self.y ** other) | |
else: | |
return NotImplemented | |
def __iadd__(self, other): | |
if isinstance(other, Vector): | |
self.x += other.x | |
self.y += other.y | |
return self | |
elif isinstance(other, tuple) or isinstance(other, list): | |
self.x += other[0] | |
self.y += other[1] | |
return self | |
elif isinstance(other, int) or isinstance(other, float): | |
self.x += other | |
self.y += other | |
return self | |
else: | |
return NotImplemented | |
def __isub__(self, other): | |
if isinstance(other, Vector): | |
self.x -= other.x | |
self.y -= other.y | |
return self | |
elif isinstance(other, tuple) or isinstance(other, list): | |
self.x -= other[0] | |
self.y -= other[1] | |
return self | |
elif isinstance(other, int) or isinstance(other, float): | |
self.x -= other | |
self.y -= other | |
return self | |
else: | |
return NotImplemented | |
def __imul__(self, other): | |
if isinstance(other, Vector): | |
self.x *= other.x | |
self.y *= other.y | |
return self | |
elif isinstance(other, tuple) or isinstance(other, list): | |
self.x *= other[0] | |
self.y *= other[1] | |
return self | |
elif isinstance(other, int) or isinstance(other, float): | |
self.x *= other | |
self.y *= other | |
return self | |
else: | |
return NotImplemented | |
def __idiv__(self, other): | |
if isinstance(other, Vector): | |
self.x /= other.x | |
self.y /= other.y | |
return self | |
elif isinstance(other, tuple) or isinstance(other, list): | |
self.x /= other[0] | |
self.y /= other[1] | |
return self | |
elif isinstance(other, int) or isinstance(other, float): | |
self.x /= other | |
self.y /= other | |
return self | |
else: | |
return NotImplemented | |
def __ipow__(self, other): | |
if isinstance(other, int) or isinstance(other, float): | |
self.x **= other | |
self.y **= other | |
return self | |
else: | |
return NotImplemented | |
def __eq__(self, other): | |
if isinstance(other, Vector): | |
return self.x == other.x and self.y == other.y | |
else: | |
return NotImplemented | |
def __ne__(self, other): | |
if isinstance(other, Vector): | |
return self.x != other.x or self.y != other.y | |
else: | |
return NotImplemented | |
def __gt__(self, other): | |
if isinstance(other, Vector): | |
return self.getLength() > other.getLength() | |
else: | |
return NotImplemented | |
def __ge__(self, other): | |
if isinstance(other, Vector): | |
return self.getLength() >= other.getLength() | |
else: | |
return NotImplemented | |
def __lt__(self, other): | |
if isinstance(other, Vector): | |
return self.getLength() < other.getLength() | |
else: | |
return NotImplemented | |
def __le__(self, other): | |
if isinstance(other, Vector): | |
return self.getLength() <= other.getLength() | |
else: | |
return NotImplemented | |
def __eq__(self, other): | |
if isinstance(other, Vector): | |
return self.x == other.x and self.y == other.y | |
else: | |
return NotImplemented | |
def __len__(self): | |
return int(sqrt(self.x**2 + self.y**2)) | |
def getLength(self): | |
return sqrt(self.x**2 + self.y**2) | |
def __getitem__(self, key): | |
if key == "x" or key == "X" or key == 0 or key == "0": | |
return self.x | |
elif key == "y" or key == "Y" or key == 1 or key == "1": | |
return self.y | |
def __str__(self): return "[x: %(x)f, y: %(y)f]" % self | |
def __repr__(self): return "{'x': %(x)f, 'y': %(y)f}" % self | |
def __neg__(self): return Vector(-self.x, -self.y) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment