Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw lines inside a square using PIL

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

like image 657
conquistador Avatar asked Jun 03 '26 00:06

conquistador


1 Answers

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

like image 147
Gabriel Jablonski Avatar answered Jun 05 '26 00:06

Gabriel Jablonski