3

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

enter image description here

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:

enter image description here

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.
enter image description here
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:
enter image description here

conquistador
  • 673
  • 3
  • 11
  • 35

1 Answers1

5

It's a cute math problem.

Cute squares diagram

Given the above diagram, in which L is the length of the sides of the starting square, and L line is the length for the new square, we must find theta such that, when rotating the new square by it, all corners touch the sides of the previous square.

L line can be defined as Calculating L line, in which f 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, a can be found to be:

Calculating a

For a generic polygon, it is defined as

Calculating generic a

in which alpha 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 f is lower-bounded by f generic inequality, given the square root in the formula.

Geometrically, it makes sense. For a square, for example, the diagonal of the new square Square root of 2 times L line should be no smaller than the sides of the previous one, which translates to L' L inequality.

Working it out with Calculating L line, we find that

f lower bound

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, theta can be calculated with sine rule

Calculate theta

With this in mind, you can produce the following output.

Obs.: The code contemplates only squares, that is, it considers alpha to be equal to 90°, though it can be easily generalized (refer to a and theta equations).

Cute squares

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 alpha can be different than 90°, it is possible to do this with any polygon shape. Here's an example applying it to a triangle:

Spiral triangles


Bonus Memes

Outputs: 1000 iterations with a 0.98 scaling factor; and square root of one over two scaling factor.

Crazy squares Symmetric squares

Gabriel Jablonski
  • 854
  • 1
  • 6
  • 17
  • Thank you!. Just one thing, would this also work on triangle vertices? – conquistador Nov 27 '19 at 15:03
  • I think it's rotating concentric triangle. – conquistador Nov 27 '19 at 15:33
  • 1
    @conquistador I've updated the answer to include the generic equation for any polygon. I haven't included the code, but you should be able to figure out what to change. The main thing is to draw the triangles the same way you're drawing the squares, starting at the center and calculating the vertices. – Gabriel Jablonski Nov 27 '19 at 18:09
  • I'm using this to calculate vertices: ` (square_center[0] + square_length / 2, square_center[1] + square_length / 2 / 1.5), (square_length / 2, square_center[1] - square_length / 2), (square_center[0] - square_length / 2, square_center[1] + square_length / 2 / 1.5)` It draws the 1st triangle fine but after that, it's an equilateral anymore. – conquistador Nov 28 '19 at 06:28
  • 1
    @conquistador You almost got it. All vertices should be calculated in relation to the center, so for example, the `x` for the second one will always be misplaced. And you should probably use the triangle's height somewhere, not just the side length. – Gabriel Jablonski Nov 28 '19 at 10:14
  • I did my reading and honestly I'm not great at maths. I changed my approach on drawing the triangles (check the updated question.). My problem is the triangles doesn't scale properly. – conquistador Nov 28 '19 at 12:28
  • @conquistador I think it's a problem with using arc tangent to calculate theta, like I suggested originally. Have you tried with arc sine? Check my updated code. And have you included alpha in the calculations? – Gabriel Jablonski Nov 28 '19 at 13:43