Skip to content

Instantly share code, notes, and snippets.

@NiclasEriksen
Created February 16, 2020 15:16
Show Gist options
  • Save NiclasEriksen/ef6882d05fcceb2565953a4f8678fa5c to your computer and use it in GitHub Desktop.
Save NiclasEriksen/ef6882d05fcceb2565953a4f8678fa5c to your computer and use it in GitHub Desktop.
Recursive line geometry
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()
# -*- 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__ = "info@shdev.de"
__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