Last active
December 6, 2022 23:19
-
-
Save drhodes/cb0e4a5fa9c50bc40cff8b624a309d5d to your computer and use it in GitHub Desktop.
This file contains 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
import math | |
from PIL import Image | |
from PIL import ImageDraw | |
''' | |
to run, need python3 | |
$ python3 -m venv venv | |
$ source ./venv/bin/activate | |
$ pip install pillow | |
$ python main.py | |
''' | |
def equal(x, y): | |
return math.isclose(x, y, rel_tol=1e-10) | |
class Point: | |
def __init__(self, x, y): | |
self.x = x | |
self.y = y | |
def as_tuple(self): | |
return self.x, self.y | |
def __repr__(self): | |
return str((self.x, self.y)) | |
def __eq__(self, other): | |
return equal(self.x, other.x) and equal(self.y, other.y) | |
class Vector: | |
def __init__(self, p): | |
self.x = p.x | |
self.y = p.y | |
@staticmethod | |
def from_to(p1, p2): | |
x = p2.x - p1.x | |
y = p2.y - p1.y | |
return Vector(Point(x, y)) | |
def dot(self, other): | |
return self.x*other.x + self.y*other.y | |
def mag(self): | |
return math.sqrt(self.x**2 + self.y**2) | |
def angle_to(self, other): | |
return math.acos(self.dot(other)/(self.mag() * other.mag())) | |
def right_angle_with(self, other): | |
return equal(self.angle_to(other), math.pi/2) | |
def normalize(self): | |
return Vector(Point(self.x/self.mag(), self.y/self.mag())) | |
def scale(self, s): | |
return Vector(Point(s * self.x, s * self.y)) | |
def resize(self, s): | |
return self.normalize().scale(s) | |
class RightTriangle: | |
def __init__(self, p1, p2, p3): | |
self.p1 = p1 | |
self.p2 = p2 | |
self.p3 = p3 | |
self.rp = self.right_corner() | |
if not self.rp: | |
raise Exception("this is not a right triangle") | |
def right_corner(self): | |
'''the point at the right angle''' | |
# at p1? | |
if Vector.from_to(self.p1, self.p2).right_angle_with(Vector.from_to(self.p1, self.p3)): | |
return self.p1 | |
elif Vector.from_to(self.p2, self.p1).right_angle_with(Vector.from_to(self.p2, self.p3)): | |
return self.p2 | |
elif Vector.from_to(self.p3, self.p1).right_angle_with(Vector.from_to(self.p3, self.p2)): | |
return self.p3 | |
return None | |
def acute_corners(self): | |
# get the points at the acute corners | |
rc = self.right_corner() | |
return [p for p in [self.p1, self.p2, self.p3] if p is not rc] | |
def draw(self, ctx, depth): | |
ctx.polygon([ | |
self.p1.as_tuple(), | |
self.p2.as_tuple(), | |
self.p3.as_tuple(), | |
], width=int(depth/2)) | |
def altitude(self): | |
# https://en.wikipedia.org/wiki/Right_triangle | |
# find the altitude point on hypotenuse | |
p1, p2 = self.acute_corners() | |
rc = self.right_corner() | |
hypv = Vector.from_to(p1, p2) | |
legv = Vector.from_to(p1, rc) | |
theta = legv.angle_to(hypv) | |
newmag = legv.mag() * math.cos(theta) | |
altv = hypv.resize(newmag) | |
return Point(p1.x + altv.x, p1.y+ altv.y) | |
def split(self): | |
# return two smaller triangles by splitting on the altitude | |
p1, p2 = self.acute_corners() | |
rc = self.right_corner() | |
pa = self.altitude() | |
return (RightTriangle(p2, rc, pa), | |
RightTriangle(p1, rc, pa)) | |
def recurse(self, ctx, depth): | |
self.draw(ctx, depth) | |
if depth == 0: return | |
t1, t2 = self.split() | |
t1.recurse(ctx, depth-1) | |
t2.recurse(ctx, depth-1) | |
def main(): | |
im = Image.new("RGB", (1000, 1000)) | |
ctx = ImageDraw.Draw(im) | |
ctx.rectangle([(0,0),im.size], fill = (80,80,80)) | |
rt = RightTriangle(Point(100, 100), Point(900, 700), Point(900,100)) | |
rt.recurse(ctx, 12) | |
im.save("recursive_triangle.png") | |
if __name__ == "__main__": | |
main() | |
Author
drhodes
commented
Dec 1, 2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment