8.2.2 - Infinite Blossom#

image0

How does it work#

The petals rotate around the center while getting closer. Therefore, they are represented by polar coordinates: an angle and a distance. The polar coordinates make the updating of petal positions very easy. Of course, some trigonometric functions need to be applied to convert polar coordinates to cartesian (x/y) coordinates.

The animation happens on a canvas that is a lot bigger than the actual image. This is because new petals appear regularly at the borders of the image, but that part is cropped off for better aesthetics. The rest is optimizing the intervals in which the petals are places.

For drawing filled polygons I originally wanted to write a numpy function from scratch. ChatGPT-3 told me there is a function np.fill_poly() and gave me a quite sophisticated code example. It turned out it was hallucinating and no such function exists. However, OpenCV has a fast enough function for drawing polygons.

Prerequisites#

This script requires two libraries: OpenCV for displaying the live animation and imageio for exporting animated GIFs.

pip install opencv-python
pip install imagio

The Script#


import math
import numpy as np
import cv2
import time
import imageio

MAXX, MAXY = 3000, 3000
SIZE = 5


class Petal:
    """
    Position of a petal in polar coordinates
    """
    def __init__(self, angle, dist):
        self.angle = angle
        self.dist = dist

    @property
    def moving(self):
        return self.dist > 0

    def update(self):
        self.angle += 1
        self.dist -= 1
    
    def polar_to_cartesian(self, angle, dist):
        rad = math.pi * angle / 180
        x = math.cos(rad) * dist
        y = math.sin(rad) * dist
        return int(y), int(x)

    def draw(self, frame):
        multiplier = 1.2 + self.dist / 200
        y1, x1 = self.polar_to_cartesian(self.angle, self.dist)
        y2, x2 = self.polar_to_cartesian(self.angle - 30, self.dist * multiplier)
        y3, x3 = self.polar_to_cartesian(self.angle + 30, self.dist * multiplier)

        col = (0, 64 - (64 * self.dist) // 360, 255 - (255 * self.dist)//360)

        offset = np.array([MAXY // 2, MAXX // 2])
        vertices = np.array([[y1, x1], [y2, x2], [y3, x3]]) + offset
        cv2.fillPoly(frame, [vertices], col)
        

def create_petals(dist, angle_offset=0):
    return [
        Petal(angle_offset + 0, dist),
        Petal(angle_offset + 120, dist),
        Petal(angle_offset + 240, dist),
    ]


background = np.zeros((MAXY, MAXX, 3), np.uint8)
petals = []
angle = 60
for dist in range(40, 400, 40):
    petals += create_petals(dist, angle)
    angle = 60 - angle


frames = []
angle_ofs = 0

for i in range(240):
    frame = background.copy()
    # more all petals
    for p in petals:
        p.update()
        p.draw(frame)
    petals = [p for p in petals if p.moving]  # remove finished petals

    cropped = frame[1200:-1200,1200:-1200]  # cut off border where new petals appear
    cv2.imshow('frame', cropped)
    rgb = cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB)
    frames.append(rgb)

    if i % 40 == 0:
        petals += create_petals(400, angle_ofs + i)
        angle_ofs = 60 - angle_ofs

    key = chr(cv2.waitKey(1) & 0xFF)
    if key == 'q':
        break

    time.sleep(0.03)

cv2.destroyAllWindows()

imageio.mimsave('infinite_blossom.gif', frames[::2], fps=20)