Pixels2GenAI
Path i Foundations
M 02 · 2.2.2 · hands-on

2.2.2 Archimedean Spiral

Convert polar coordinates (angle, radius) to Cartesian pixels and trace an Archimedean spiral with `r = a + bθ`, then tune the growth rate and colour-interpolate along the curve.

Duration18–22 min
Levelbeginner
Load3 core concepts
Prereqs2.1.1 (line drawing), 2.2.1 (linspace), basic trigonometry

Overview

The Archimedean spiral has one equation: $r = a + b\theta$. The radius grows linearly with the angle, so consecutive turns sit at a constant distance from each other [1]. Translate that single line of polar-coordinate arithmetic into pixels — convert each $(r, \theta)$ to Cartesian, draw a line to the previous point — and you have the spiral. The same template extends to logarithmic spirals (replace + with *), star-like rose curves (next module), and the entire family of polar-coordinate art that comes after.

Learning objectives

  1. Convert between polar (r, θ) and Cartesian (x, y) coordinates using cos and sin.
  2. Implement the Archimedean spiral equation r = a + b·θ and trace it pixel-by-pixel.
  3. Use a Python generator to model an open-ended sequence of spiral points without storing them all.
  4. Linearly interpolate two colours along the spiral’s progress for a gradient stroke.

Quick start — one Archimedean spiral

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

def draw_line(canvas, x0, y0, x1, y1, color):
    n = max(abs(x1 - x0), abs(y1 - y0)) + 1
    xs = np.linspace(x0, x1, n).round().astype(int)
    ys = np.linspace(y0, y1, n).round().astype(int)
    h, w = canvas.shape[:2]
    inside = (xs >= 0) & (xs < w) & (ys >= 0) & (ys < h)
    canvas[ys[inside], xs[inside]] = color

WIDTH, HEIGHT = 512, 512
cx, cy = WIDTH // 2, HEIGHT // 2
canvas = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)

def spiral_points(start_radius=10, growth_rate=4.0, num_points=500):
    for i in range(num_points):
        theta = i * 0.1
        r = start_radius + growth_rate * theta
        x = int(cx + r * np.cos(theta))
        y = int(cy + r * np.sin(theta))
        yield x, y

points = spiral_points()
prev_x, prev_y = next(points)
for x, y in points:
    draw_line(canvas, prev_x, prev_y, x, y, [255, 255, 255])
    prev_x, prev_y = x, y

Image.fromarray(canvas, mode='RGB').save('simple_spiral.png')
A white Archimedean spiral on a black 512 by 512 canvas, expanding from the centre outward with roughly eight visible turns
Fig. 1 An Archimedean spiral — 500 points generated in polar, converted to Cartesian, connected with line segments.

Core concepts

Concept 1 — Polar coordinates and the conversion

A point can be addressed two ways. In Cartesian coordinates you give horizontal and vertical offsets (x, y) from an origin. In polar coordinates you give an angle θ and a distance r, and the same point comes out:

$$x = r \cos\theta, \qquad y = r \sin\theta$$

For anything with rotational structure — circles, spirals, rose curves, sunbursts — polar is the natural language. The conversion is one line of code per coordinate [2].

Diagram showing the polar coordinate system: an angle theta measured from the positive x-axis and a radius r measured from the origin, with the resulting Cartesian point marked
Fig. 2 Polar coordinates `(r, θ)` — one rotation around the origin, then one radial step out.

Concept 2 — The Archimedean spiral

Archimedes’ spiral is the simplest polar curve where both r and θ vary:

$$r = a + b,\theta$$

with a the starting radius and b the growth rate. Each full turn of θ (one increment) adds 2πb to the radius — a constant gap between successive turns. This separates it from the logarithmic spiral seen in nautilus shells, where the radius multiplies rather than adds, and the gap grows with each turn [3].

python · archimedean_demo.py
# Trace 500 points along the spiral
start_radius = 5     # 'a' — the inner radius
growth_rate = 0.5    # 'b' — radius added per radian
angle_step = 0.1     # how far to advance per point

for i in range(500):
    theta = i * angle_step
    r = start_radius + growth_rate * theta
    # convert and plot

The two parameters that decide what the spiral looks like are growth_rate (how fast it opens) and angle_step (how smoothly the curve traces — small steps give clean curves, large steps give a polygonal staircase).

Four spiral variations in a two-by-two grid: a tight spiral with many turns, a loose spiral with few turns, a dense smooth spiral, and a sparse angular spiral
Fig. 3 Four `(growth_rate, angle_step)` combinations. Tight vs. loose is parameter `b`; smooth vs. angular is the step size.

Concept 3 — Python generators for open-ended sequences

A spiral is theoretically infinite — θ can keep growing forever. A Python generator function models that naturally: instead of return-ing a list, it yields one value at a time, and the caller pulls more as needed [4]:

python · generator_demo.py
def spiral_points(start_radius, growth_rate, num_points):
    for i in range(num_points):
        theta = i * 0.1
        r = start_radius + growth_rate * theta
        x = int(cx + r * np.cos(theta))
        y = int(cy + r * np.sin(theta))
        yield x, y      # pause here, send (x, y) back to the caller

# Consume the generator one point at a time
for x, y in spiral_points(5, 0.5, 1000):
    ...

Three reasons generators fit this kind of work: memory (no list of 1000 tuples sitting around), composability (a spiral generator chains naturally into a draw-line consumer), and a conceptual match (a spiral is an iterative process, not a fixed-size dataset).

Exercises

Three exercises in Execute → Modify → Create order: run the basic spiral, tune its parameters, then add a colour gradient along the path.

EXECUTE I.

Run the Archimedean spiral

Run simple_spiral.py and observe the output.

python · exercise1_execute.py
import numpy as np
from PIL import Image

def draw_line(canvas, x0, y0, x1, y1, color):
    n = max(abs(x1 - x0), abs(y1 - y0)) + 1
    xs = np.linspace(x0, x1, n).round().astype(int)
    ys = np.linspace(y0, y1, n).round().astype(int)
    h, w = canvas.shape[:2]
    inside = (xs >= 0) & (xs < w) & (ys >= 0) & (ys < h)
    canvas[ys[inside], xs[inside]] = color

WIDTH, HEIGHT = 512, 512
cx, cy = WIDTH // 2, HEIGHT // 2
canvas = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)

def spiral_points(start_radius=10, growth_rate=4.0, num_points=500):
    for i in range(num_points):
        theta = i * 0.1
        r = start_radius + growth_rate * theta
        x = int(cx + r * np.cos(theta))
        y = int(cy + r * np.sin(theta))
        yield x, y

points = spiral_points()
prev_x, prev_y = next(points)
for x, y in points:
    draw_line(canvas, prev_x, prev_y, x, y, [255, 255, 255])
    prev_x, prev_y = x, y

Image.fromarray(canvas, mode='RGB').save('simple_spiral.png')

Reflection questions

  • Where does the spiral start drawing — at the centre or the outer edge?
  • Roughly how many full turns are visible, given num_points=500 and angle_step=0.1?
  • What would change visually if you swapped np.cos and np.sin for the x/y assignment?
MODIFY II.

Tune the spiral parameters

Edit exercise1_execute.py to produce these three pictures.

Goals

  1. Tighter — more turns packed into the same canvas (smaller growth_rate).
  2. Looser — fewer turns, expanding faster (larger growth_rate).
  3. Inward — starts at the outer edge and spirals into the centre.
CREATE III.

Colour-gradient spiral

Build a spiral whose stroke transitions from red at the centre to blue at the outer edge. Reuse the generator pattern; add a per-point progress value (0 at the start, 1 at the end) and interpolate between two RGB triples.

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

# ... draw_line and canvas setup as before ...

start_color = np.array([255, 50, 50])    # red
end_color   = np.array([50, 50, 255])    # blue

def spiral_with_progress(start_radius, growth_rate, num_points):
    for i in range(num_points):
        # TODO 1: compute theta, r, and (x, y)
        # TODO 2: compute progress = i / (num_points - 1)
        # TODO 3: yield (x, y, progress)
        pass

def interpolate_color(c0, c1, t):
    # TODO 4: clamp t to [0, 1] and return (1-t)*c0 + t*c1 as uint8
    pass

# TODO 5: walk the generator; for each segment, compute its colour with
# interpolate_color(progress) and pass it to draw_line.

Image.fromarray(canvas, mode='RGB').save('color_spiral.png')

Make it your own

  • Logarithmic spiral. Replace r = a + b*θ with r = a * exp(b*θ). Pick a small b (around 0.1) — exponential growth ramps fast.
  • Double spiral. Run two generators with theta shifted by π (half a turn) and draw both onto the same canvas in different colours — the result reads like a galaxy’s arms.
  • Rainbow. Cycle through HSV hues with progress, then convert back to RGB with colorsys.hsv_to_rgb for a six-colour rainbow.

Downloads

simple_spiral.py — quick start spiral_variations.py — parameter sweep color_spiral_solution.py — gradient stroke

Summary

Common pitfalls to avoid

  • Passing degrees into np.cos/np.sin — they expect radians. Use np.radians(deg) if you start in degrees.
  • Forgetting to add cx, cy to the converted coordinates — the spiral renders in the top-left corner instead of centred.
  • Picking a growth_rate so large that the second turn already exits the canvas.
  • Casting floats to int with .astype(int) after broadcasting and getting truncation gaps — use np.round().astype(int) if a gap is visible.

References

  1. [1] Heath, T. L. (Ed.). (1897). The Works of Archimedes. Cambridge University Press. English edition of Archimedes’ On Spirals (c. 225 BCE).
  2. [2] Coolidge, J. L. (1952). The origin of polar coordinates. The American Mathematical Monthly, 59(2), 78–85. doi:10.2307/2307104
  3. [3] Thompson, D. W. (1917). On Growth and Form. Cambridge University Press.
  4. [4] Python Software Foundation. (2024). Functional Programming HOWTO — Generators. Python Documentation. docs.python.org
  5. [5] NumPy Community. (2024). Trigonometric functions. NumPy Documentation. numpy.org
  6. [6] Pearson, M. (2011). Generative Art: A Practical Guide Using Processing. Manning.
  7. [7] Foley, J. D., van Dam, A., Feiner, S. K., & Hughes, J. F. (1990). Computer Graphics: Principles and Practice (2nd ed.). Addison-Wesley.