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.
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
- Represent moving objects in polar coordinates and update angle/distance per frame.
- Implement a spawn-and-retire object lifecycle: new objects appear at the edge; old ones disappear in the centre.
- Render each object using polar-to-Cartesian conversion with
cos(θ) * r, sin(θ) * r. - Compose an “infinite” animation by ensuring the steady-state object count stays bounded.
Quick start — the infinite blossom
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)
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.
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:
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.
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.
Run the infinite blossom
Run infinite_blossom.py. The script generates a 180-frame loop with ~50 petals on screen at any moment.
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?
Answers
Three-fold symmetry — spawning in groups of 3 produces visible 3-fold rotational symmetry at every distance. Single-petal spawns would feel random; 6 or 12 would feel busy. Three is the visual sweet spot for “ordered chaos” — enough structure to read as composed, sparse enough to feel organic.
Steady-state petal count — spawn rate doubles (every 6 frames instead of 12); lifetime stays at 213 frames; petal count doubles to ~106. The animation looks denser and may feel cluttered.
Colour cue for motion — petals at the edge are dimmer (more orange); petals near the centre are saturated red. The eye reads warmer/brighter as “approaching” and cooler/dimmer as “receding.” Reversing the gradient would make the petals feel like they were emerging from the centre rather than arriving at it.
Sweep blossom parameters
Three variants.
Goals
- Slower spiral. Reduce rotation per frame from 1.2° to 0.3°. The petals will feel more radial than spiral.
- Five-fold symmetry. Spawn 5 petals 72° apart instead of 3 at 120°. The composition now has 5-fold rotational symmetry.
- 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”.
Goal 1 — what to expect
The petals move almost straight inward instead of spiraling. The composition becomes more radial than vortical — closer to a centripetal flow than a vortex.
Goal 2 — what to expect
Visible 5-fold rotational symmetry. Five is an odd number, so the symmetry is more difficult for the eye to track than 3 — the composition reads as more organic.
Goal 3 — what to expect
The reverse animation: petals emerge from the centre and grow outward, exiting the visible canvas. Use the inner canvas trick in reverse — retire when dist > INNER // 2. Visually it’s the opposite mood — explosion outward instead of gravitational inward.
Build a multi-shape emitter
Extend the system to support multiple object shapes — circles, squares, and the existing triangles — chosen randomly at spawn time.
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. Hint 1 — dispatch on shape
def draw(self, draw, centre):
r = math.radians(self.angle)
x = int(math.cos(r) * self.dist) + centre[0]
y = int(math.sin(r) * self.dist) + centre[1]
if self.shape == 'circle':
draw.ellipse([x - 8, y - 8, x + 8, y + 8], fill=self.color)
elif self.shape == 'square':
draw.rectangle([x - 7, y - 7, x + 7, y + 7], fill=self.color)
elif self.shape == 'tri':
# triangle pointing outward
verts = [(x, y - 10), (x - 8, y + 6), (x + 8, y + 6)]
draw.polygon(verts, fill=self.color)A small primitive per branch — circles read as soft, squares as crisp, triangles as directional.
Hint 2 — random colour per spawn
hue = rng.uniform(0, 360)
# HSV → RGB (cheap implementation)
h = hue / 60
c = 200
x = c * (1 - abs((h % 2) - 1))
if h < 1: r, g, b = c, x, 0
elif h < 2: r, g, b = x, c, 0
elif h < 3: r, g, b = 0, c, x
elif h < 4: r, g, b = 0, x, c
elif h < 5: r, g, b = x, 0, c
else: r, g, b = c, 0, x
color = (int(r + 55), int(g + 55), int(b + 55))Uniform-hue colours look more varied than uniform-RGB.
Complete solution
The combined system supports any number of shapes — extend with stars, lines, or even small images. The lifecycle code (update + retire) is unchanged; only the draw dispatch and the spawn config grow. This is the structure every particle system in every game engine ships.
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.5withdist -= 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 implementationSummary
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] 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] 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] Pearson, M. (2011). Generative Art: A Practical Guide Using Processing. Manning. ISBN 978-1-935182-62-5.
- [4] Shiffman, D. (2012). The Nature of Code: Simulating Natural Systems with Processing. natureofcode.com
- [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] 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] Unity Technologies. (2024). Particle System Module Reference. docs.unity3d.com