Pixels2GenAI
Path ii Continuum
M 08 · 8.2.2 · hands-on

8.2.2 Infinite Blossom

Petals spawn at the edge in polar coordinates, spiral inward as they rotate, and disappear at the centre. New ones spawn forever — no start, no end.

Duration20–25 min
Levelintermediate
Load
Prereqs8.2.1 (per-object animation), 2.2.2 (polar coordinates)

Overview

The infinite blossom is the opposite of the flower assembly (8.2.1): instead of N pre-placed tiles converging to a finished image, new objects spawn continuously at the canvas edge, each one with its own lifetime. There is no end state. The animation runs forever, and yet the canvas stays bounded because every object eventually disappears at the centre.

Mechanically the trick is to keep objects in polar coordinates — angle and distance from a fixed centre — and update both per frame: rotate the angle slightly (causing visual spiraling) and decrease the distance (causing inward motion). When distance == 0, retire the object. Spawn a fresh batch every N frames, and the pipeline becomes a steady flow. The same pattern drives every screensaver, every particle-effect emitter in game engines, and every “infinite spiral” video that fascinates an Instagram feed.

Learning objectives

  1. Represent moving objects in polar coordinates and update angle/distance per frame.
  2. Implement a spawn-and-retire object lifecycle: new objects appear at the edge; old ones disappear in the centre.
  3. Render each object using polar-to-Cartesian conversion with cos(θ) * r, sin(θ) * r.
  4. Compose an “infinite” animation by ensuring the steady-state object count stays bounded.

Quick start — the infinite blossom

python · quick_start.py
import math
import numpy as np
from PIL import Image, ImageDraw

INNER, CANVAS, SPAWN_R, FRAMES = 800, 600, 320, 180

class Petal:
    def __init__(self, angle_deg, dist):
        self.angle = angle_deg
        self.dist = dist
    def update(self):
        self.angle = (self.angle + 1.2) % 360
        self.dist -= 1.5
    def polar(self, ang, d):
        r = math.radians(ang)
        return int(math.cos(r) * d), int(math.sin(r) * d)
    def draw(self, draw):
        m = 1.2 + self.dist / 280
        v = [self.polar(self.angle, self.dist),
             self.polar(self.angle - 30, self.dist * m),
             self.polar(self.angle + 30, self.dist * m)]
        ox, oy = INNER // 2, INNER // 2
        col = (255, int(60 + 110 * self.dist / SPAWN_R), int(40 + 60 * self.dist / SPAWN_R))
        draw.polygon([(x + ox, y + oy) for x, y in v], fill=col)
An animated GIF showing red and orange triangular petals spiraling inward from the edges of a canvas toward the centre, where they vanish. New petals continuously appear at the outer rim, creating a hypnotic looping pattern with three-fold rotational symmetry
Fig. 1 180-frame loop. Petals spawn in triplets every 12 frames at the rim, spiral inward with 1.2°/frame rotation and 1.5 px/frame inward motion. They vanish at the centre.

Core concepts

Concept 1 — Polar coordinates as the natural state

For motion that’s rotational + radial, polar coordinates (angle, distance) are vastly simpler than Cartesian (x, y). Rotating an object becomes “add to the angle”; moving it inward becomes “subtract from the distance.” Per-frame update is a single addition per axis.

python · update.py
def update(self):
    self.angle = (self.angle + 1.2) % 360   # rotate 1.2° per frame
    self.dist -= 1.5                         # 1.5 px inward per frame

The trade-off: rendering needs a polar-to-Cartesian conversion at every draw call. For a thousand objects per frame the cost is negligible — the conversion is two cos/sin calls. Same trick the spiral lesson (2.2.2) and the harmonograph lesson (2.3.3) used; same trick every 2D physics engine uses when objects orbit a centre.

Concept 2 — The spawn-and-retire lifecycle

For the animation to last forever without unbounded memory, objects must retire when they finish their lifecycle. We keep a list of active petals, update them, then filter out the ones with dist <= 0:

python · lifecycle.py
for f in range(FRAMES):
    if f % 12 == 0:                                 # spawn cadence
        petals += create_three(SPAWN_RADIUS,
                                rng.uniform(0, 360))
    for p in petals:
        p.update()
        p.draw(draw)
    petals = [p for p in petals if p.dist > 0]      # retire arrived

The steady-state count is determined by spawn rate × lifetime. With petals spawning in groups of 3 every 12 frames and a lifetime of SPAWN_RADIUS / 1.5 ≈ 213 frames, you can expect about 3 * 213 / 12 ≈ 53 petals on screen at any moment.

Concept 3 — Rendering: oversize canvas + centre crop

When new petals spawn at the edge, the visible window would show them appearing out of nowhere — a discontinuity. The fix: render on a canvas larger than the visible viewport, then crop the centre.

python · crop.py
INNER, CANVAS = 800, 600
crop_box = ((INNER - CANVAS) // 2, (INNER - CANVAS) // 2,
            (INNER - CANVAS) // 2 + CANVAS, (INNER - CANVAS) // 2 + CANVAS)
frame.crop(crop_box)

The 100 px margin on each side hides the spawn moment: by the time petals enter the visible 600×600 viewport, they have already travelled enough that their motion looks continuous. Every infinite-emitter animation needs some version of this trick — render bigger, crop the centre, hide the boundary.

Exercises

Three exercises in Execute → Modify → Create order: render the loop, sweep parameters, then build a multi-direction emitter.

EXECUTE I.

Run the infinite blossom

Run infinite_blossom.py. The script generates a 180-frame loop with ~50 petals on screen at any moment.

infinite_blossom.py — reference implementation

Reflection questions

  • The petals spawn in groups of 3 spaced 120° apart. Why?
  • What is the steady-state petal count if the spawn cadence is every 6 frames instead of 12?
  • The colour gets warmer as petals move toward the centre. How does this contribute to the perception of inward motion?
MODIFY II.

Sweep blossom parameters

Three variants.

Goals

  1. Slower spiral. Reduce rotation per frame from 1.2° to 0.3°. The petals will feel more radial than spiral.
  2. Five-fold symmetry. Spawn 5 petals 72° apart instead of 3 at 120°. The composition now has 5-fold rotational symmetry.
  3. Reversed motion. Negate the distance decrement: petals spawn at the centre and spiral outward. You’ll need to swap “retire when dist == 0” with “retire when dist > MAX_R”.
CREATE III.

Build a multi-shape emitter

Extend the system to support multiple object shapes — circles, squares, and the existing triangles — chosen randomly at spawn time.

python · exercise3_starter.py
import math, numpy as np
from PIL import Image, ImageDraw

class Particle:
    def __init__(self, angle_deg, dist, shape, color):
        self.angle = angle_deg
        self.dist = dist
        self.shape = shape       # 'tri', 'circle', or 'square'
        self.color = color

    def update(self):
        # TODO 1: same as before — rotate angle, decrease distance
        pass

    def draw(self, draw, centre):
        # TODO 2: dispatch on self.shape, draw the appropriate primitive
        pass

# Spawn loop: pick a random shape from ['tri', 'circle', 'square']
# and a random colour for each new particle.

Make it your own

  • Wind field. Add a horizontal velocity that varies sinusoidally with vertical position. Petals get blown by virtual wind as they fall.
  • Gravity well. Replace dist -= 1.5 with dist -= 1.5 * (1 + 200 / max(dist, 50)). Petals accelerate inward, mimicking falling into a gravity well.
  • Audio-reactive spawn rate. Sample a microphone (pyaudio) and spawn petals proportional to loudness. The blossom becomes a music visualiser.

Downloads

infinite_blossom.py — reference implementation

Summary

Common pitfalls to avoid

  • Cartesian per-frame update. Working in (x, y) for rotation requires trig at every update; polar makes the update one addition. Always pick the coordinate system that makes the update cheap.
  • Spawn-inside-viewport. Visible “pop” when objects materialise on screen. Spawn beyond the crop window.
  • Unbounded object list. Forgetting to retire arrived objects leaks memory and slows down the animation over time.
  • Frame-loop count desynchronised with spawn cadence. If FRAMES % SPAWN_PERIOD != 0, the GIF loop point may show a visible jump. Either match the periods or use a long enough total to make the discontinuity sub-frame.

References

  1. [1] Reeves, W. T. (1983). Particle systems — a technique for modeling a class of fuzzy objects. ACM Transactions on Graphics, 2(2), 91–108. doi:10.1145/357318.357320
  2. [2] Akenine-Möller, T., Haines, E., Hoffman, N., et al. (2018). Real-Time Rendering (4th ed.). CRC Press. ISBN 978-1-138-62700-0.
  3. [3] Pearson, M. (2011). Generative Art: A Practical Guide Using Processing. Manning. ISBN 978-1-935182-62-5.
  4. [4] Shiffman, D. (2012). The Nature of Code: Simulating Natural Systems with Processing. natureofcode.com
  5. [5] Lasseter, J. (1987). Principles of traditional animation applied to 3D computer animation. Proceedings of SIGGRAPH ‘87, 35–44. doi:10.1145/37402.37407
  6. [6] Foley, J. D., van Dam, A., Feiner, S. K., & Hughes, J. F. (1990). Computer Graphics: Principles and Practice (2nd ed.). Addison-Wesley. ISBN 978-0-201-12110-0.
  7. [7] Unity Technologies. (2024). Particle System Module Reference. docs.unity3d.com