I'm trying to recreate this image using Python and PIL.

This is the code I come upped with:
from PIL import Image, ImageDraw
def draw_lines(draw, points):
new_points = []
for idx, point in enumerate(points):
x, y = point
if idx != len(points) - 1:
if idx == 0:
x = x + 25
elif idx == 1:
y = y + 25
elif idx == 2:
x = x - 25
elif idx == 3:
y = y - 25
else:
x = x + 25
new_points.append((x, y))
draw.line(new_points, fill="black", width=1)
return new_points
def main():
im = Image.new('RGB', (501, 501), color=(255, 255, 255))
draw = ImageDraw.Draw(im)
points = [
(0, 0),
(500, 0),
(500, 500),
(0, 500),
(0, 0),
]
draw.line(points, fill="black", width=1)
for i in range(80):
points = draw_lines(draw, points)
im.save("out.png")
if __name__ == '__main__':
main()
and this is the output:

and also how can I fill those formed triangles with color?
Update:
By modifying the answer here Rotating a square in PIL, I was able to do this.

Code:
import math
from PIL import Image, ImageDraw
def distance(ax, ay, bx, by):
return math.sqrt((by - ay) ** 2 + (bx - ax) ** 2)
def rotated_about(ax, ay, bx, by, angle):
radius = distance(ax, ay, bx, by)
angle += math.atan2(ay - by, ax - bx)
return (
round(bx + radius * math.cos(angle)),
round(by + radius * math.sin(angle))
)
image = Image.new('RGB', (510, 510), color=(255, 255, 255))
draw = ImageDraw.Draw(image)
def draw_sqr(pos, sqlen, rota):
square_center = pos
square_length = sqlen
square_vertices = (
(square_center[0] + square_length / 2, square_center[1] + square_length / 2),
(square_center[0] + square_length / 2, square_center[1] - square_length / 2),
(square_center[0] - square_length / 2, square_center[1] - square_length / 2),
(square_center[0] - square_length / 2, square_center[1] + square_length / 2)
)
square_vertices = [rotated_about(x, y, square_center[0], square_center[1], math.radians(rota)) for x, y in
square_vertices]
draw.polygon(square_vertices, outline="black")
def draw_rot_sqr(pos):
scale = 500
rot = 0
n = 1.1575
for i in range(10):
draw_sqr(pos, scale, rot)
rot = rot * n + 10
scale = scale / n - 10
draw_rot_sqr((255, 255))
image.show()
Now, how can I properly scale and rotate the squares where all points intersect with the sides at any size?
Edit, drawing triangles
Vertices for drawing triangles:
def draw_sqr(pos, p_len, rota):
x, y = pos
altitude = p_len * math.sqrt(3) / 2
apothem = altitude / 3
x_top = x
y_top = y - apothem * 2
x_base_1 = x + p_len / 2
x_base_2 = x - p_len / 2
y_base = y + apothem
vertices = (
(x_top, y_top),
(x_base_1, y_base),
(x_base_2, y_base)
)
vertices = [rotated_about(x, y, pos[0], pos[1], rota) for x, y in
vertices]
draw.polygon(vertices, outline="black")
Outputs:

It's a cute math problem.

Given the above diagram, in which
is the length of the sides of the starting square, and
is the length for the new square, we must find
such that, when rotating the new square by it, all corners touch the sides of the previous square.
can be defined as
, in which
is the scaling factor. For example, if the scaling factor is 0.9, each new square's sides will be 90% of the length of the sides for the previous one.
With some basic trigonometry,
can be found to be:

For a generic polygon, it is defined as

in which
is the internal angle value for the polygon (90° for the square, so it falls back to the previous equation).
It should be noted that
is lower-bounded by
, given the square root in the formula.
Geometrically, it makes sense. For a square, for example, the diagonal of the new square
should be no smaller than the sides of the previous one, which translates to
.
Working it out with
, we find that

With a scaling factor over 1, the new squares will be larger, but the principle of touching the corners still applies.
As for the plus-minus in the formula, the minus corresponds to a clockwise rotation, the plus being for counter-clockwise.
Finally,
can be calculated with sine rule

With this in mind, you can produce the following output.
Obs.: The code contemplates only squares, that is, it considers
to be equal to 90°, though it can be easily generalized (refer to
and
equations).

import math
from PIL import Image, ImageDraw
def calc_a(L, f):
return L/2.0*(1-(1-2*(1-f**2))**.5)
def calc_theta(L, f, direction='cw'):
a = calc_a(L, f)
if direction == 'cw':
d = 1
elif direction == 'ccw':
d = -1
return d*math.asin(a/(f*L))
def distance(ax, ay, bx, by):
return math.sqrt((by - ay) ** 2 + (bx - ax) ** 2)
def rotated_about(ax, ay, bx, by, angle):
radius = distance(ax, ay, bx, by)
angle += math.atan2(ay - by, ax - bx)
return (
round(bx + radius * math.cos(angle)),
round(by + radius * math.sin(angle))
)
image = Image.new('RGB', (510, 510), color=(255, 255, 255))
draw = ImageDraw.Draw(image)
def draw_sqr(pos, sqlen, rota):
square_center = pos
square_length = sqlen
square_vertices = (
(square_center[0] + square_length / 2, square_center[1] + square_length / 2),
(square_center[0] + square_length / 2, square_center[1] - square_length / 2),
(square_center[0] - square_length / 2, square_center[1] - square_length / 2),
(square_center[0] - square_length / 2, square_center[1] + square_length / 2)
)
square_vertices = [rotated_about(x, y, square_center[0], square_center[1], rota) for x, y in
square_vertices]
draw.polygon(square_vertices, outline="black")
def draw_rot_sqr(pos):
side = 500 # starting square side length
f = 0.9 # should be bigger than 1/sqrt(2), for math reasons
base_theta = calc_theta(side, f, direction='cw')
theta = 0 # first square has no rotation
for i in range(10):
draw_sqr(pos, side, theta)
# theta is relative to previous square, so we should accumulate it
theta += base_theta
side *= f
draw_rot_sqr((255, 255))
image.show()
Using the generic implementation that considers that
can be different than 90°, it is possible to do this with any polygon shape. Here's an example applying it to a triangle:

Outputs: 1000 iterations with a 0.98 scaling factor; and
scaling factor.

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With