2
$\begingroup$

enter image description here

I created the image back in 2018, way before I learned programming. Now I am trying to write a function that creates images like this programmatically.

Basically, you start with a regular triangle (all three angles are 60°) whose side length is 1, you construct a square with side length 1 from each of its sides, then in the next iteration, you construct a unit pentagon from each of the three squares, then in the next iteration you construct a unit hexagon from each of the three pentagons from the previous iteration from the sides that will make the thing grow as depicted, and so on, and so forth.

In each iterations you construct three regular polygons with number of sides equal to one plus the number of sides of polygons in the previous iteration, from the sides that will make the shape grow as depicted, until a given number of iterations is reached.

I know how to compute the vertices of the shapes programmatically, I just don't know how to determine from which side I need to construct the next polygon. I tried to Google search this but I don't know what the proper term for it is, and image searching yields no results. Can anyone help me?


Update

I have done it, I have already wrote a Python function that generates images like this, here is an example output:

enter image description here Here is the code if anyone is interested:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import PolyCollection
from PIL import Image

def sin(d: float): return np.sin(np.radians(d))
def cos(d: float): return np.cos(np.radians(d))
def tan(d: float): return np.tan(np.radians(d))
def atan2(x, y): return np.rad2deg(np.arctan2(y, x))

def rotate(pos, angle, center=(0, 0)):
    cx, cy = center
    px, py = pos
    diff_x, diff_y = (px - cx), (py - cy)
    cosa, sina = cos(angle), sin(angle)
    px1 = cosa * diff_x - sina * diff_y + cx
    py1 = sina * diff_x + cosa * diff_y + cy
    return (px1, py1)

def spectrum_position(n, string=False):
    if not isinstance(n, int):
        raise TypeError('`n` should be an integer')
    if n < 0:
        raise ValueError('`n` must be non-negative')
    n %= 1530
    if 0 <= n < 255:
        return (255, n, 0) if not string else f'ff{n:02x}00'
    elif 255 <= n < 510:
        return (510-n, 255, 0) if not string else f'{510-n:02x}ff00'
    elif 510 <= n < 765:
        return (0, 255, n-510) if not string else f'00ff{n-510:02x}'
    elif 765 <= n < 1020:
        return (0, 1020-n, 255) if not string else f'00{1020-n:02x}ff'
    elif 1020 <= n < 1275:
        return (n-1020, 0, 255) if not string else f'{n-1020:02x}00ff'
    elif 1275 <= n < 1530:
        return (255, 0, 1530-n) if not string else f'ff00{1530-n:02x}'
    
def increment_rotate(pos1, pos2, rotation):
    x1, y1 = pos1
    x2, y2 = pos2
    side = ((x2-x1)**2+(y2-y1)**2)**.5
    angle = atan2((x2 - x1), (y2 - y1))
    new_x, new_y = x2+side*cos(angle), y2+side*sin(angle)
    new_x, new_y = rotate((new_x, new_y), rotation, (x2, y2))
    return new_x, new_y

def make_polygon(pos1, pos2, sides):
    assert sides >= 3
    unit_rotation = 360/sides
    x1, y1 = pos1
    x2, y2 = pos2
    side = ((x2-x1)**2+(y2-y1)**2)**.5
    positions = [pos1]
    prev_pos = pos2
    cur_pos = pos1
    for i in range(sides-2):
        new_pos = increment_rotate(prev_pos, cur_pos, unit_rotation)
        positions.append(new_pos)
        prev_pos = cur_pos
        cur_pos = new_pos
    
    positions.append(pos2)
    return positions                                                                         

def polygon_spiral(unit, iterations, num_colors=12):
    step = 1530/num_colors
    palette = ['#'+spectrum_position(round(step*i), 1) for i in range(num_colors)]
    colors = [palette[0]]
    radius = unit*cos(30)/1.5
    y1 = -radius/2
    x1 = -unit/2
    x2 = unit/2
    points = [(0, radius), (x2, y1), (x1, y1)]
    polygons = [points]
    side = 4
    left_start, left_end = points[0], points[2]
    down_start, down_end = points[2], points[1]
    right_start, right_end = points[1], points[0]
    for i in range(iterations):
        left = make_polygon(left_start, left_end, side)
        down = make_polygon(down_start, down_end, side)
        right = make_polygon(right_start, right_end, side)
        polygons.append(left)
        polygons.append(down)
        polygons.append(right)
        colors.extend([palette[(i+1)%num_colors]]*3)
        half = (side/2).__ceil__()
        left_start, left_end = left[half-1:half+1]
        down_start, down_end = down[half-1:half+1]
        right_start, right_end = right[half-1:half+1]
        side += 1
    
    return {'polygons': polygons, 'colors': colors}

def plot_polygon_spiral(iterations, unit=1, width=1920, height=1080, lw=2, alpha=1, num_colors=12, show=True):
    polygons, colors = polygon_spiral(unit, iterations, num_colors).values()
    fig = plt.figure(figsize=(width/100, height/100),
                 dpi=100, facecolor='black')
    ax = fig.add_subplot(111)
    ax.set_axis_off()
    collection = PolyCollection(polygons, facecolors=colors, lw=lw, alpha=alpha, edgecolor='w')
    ax.add_collection(collection)
    plt.axis('scaled')
    fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
    fig.canvas.draw()
    image = Image.frombytes(
        'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
    if not show:
        plt.close(fig)
    else:
        plt.show()
    return image
$\endgroup$

1 Answer 1

2
$\begingroup$

You can obtain a function from whatever labelling system the software uses to convert to/from any other system of your choosing.

Choose the system which labels the "original" side (the one in or closest to the original triangle, by distance travelled through the existing polygons) as side zero, and side one as the next in whichever direction you want the spiral to go, i.e. a clockwise numbering leads to a clockwise spiral.

Then the "construction" side (where the next polygon shares) will be given by $\lceil\frac{n}{2}\rceil$ for $n$ the number of sides in the polygon.

For example, in a square, this causes the next pentagon to be built on side two, which is the one opposite side zero, while in the pentagon, the next hexagon is built on side $\lceil 2.5\rceil=3$ which is the side "after" the vertex opposite side zero.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .