[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky] [Facebook]
[Build Your Own Python Action Arcade!]

[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Draw filled Archimedes spirals in Python

[Three filled Archimedes spirals drawn in Python]

My post Draw Archimedes spirals in Python explains how to draw Archimedes spirals. This post explains how to fill the area between two spirals. The idea is simple: connect the points on the first and second spirals to create a polygon and the fill that polygon. That approach works but has three problems.

Making a First Attempt

[Reverse the points in the second spiral and join them to the first] The program uses the following code to append one spiral's points to the next spiral's points.

test = 1 if test == 1: # TEST 1: Just connect the two spirals' points. polygons.append(spiral_i + spiral_j)

The picture on the right shows the result. The polygon's edges start at the center C. The first spiral's points move out in the direction of the blue arrow. When that spiral reaches its end at point A, the polygon continues along the second spiral. It jumps to the start of that spiral back at point C and heads out along the red arrow until reaching point B.

The result isn't what we want because the points jump from A back to C and then, when the point list is finished, the polygon closes by connecting the final point B to the first point A.

Reversing Points

The first step in fixing this problem is to reverse the points in the second spiral. That way the polygon's points move out along the first spiral and then back along the second. Here's the code the program uses to test this approach.

elif test == 2: # TEST 2: Reverse the second spiral. polygons.append(spiral_i + list(reversed(spiral_j)))

That's an improvement but you won't see any difference because points A and B lie on opposite sides of the spirals' center. This time the points leave the center C along the blue arrow until they reach point A. Then they jump to point B and trace the second spiral backward until reaching point C again. The points no longer jump from point A to point C, but they do jump from point A to point B so the result looks the same.

Finding the Closest Approach

The general approach is correct: go out along the first spiral and return along the second. The problem is that the second spiral ends on the other side of the drawing area. What we need to do is find the point on the second spiral that is closest to point A and jump to that spiral at that closest point. The following code demonstrates that approach.

else: # Find the point on the second spiral that is # closest to the end point of the first spiral. point_i = spiral_i[-1] distances = [math.dist(point_i, point_j) for point_j in spiral_j] k = distances.index(min(distances)) # Slice out the part of the second spiral up to that point. snippet = spiral_j[0:k] # Add the first spiral to the slice. polygons.append(spiral_i + list(reversed(snippet)))

[This polygon connects the first spiral to the closest point on the second spiral] This code sets point_i to the last point in the first spiral. It then uses a list comprehension to make a list of the distances from point_i to the points in the second spiral. It uses min to get the smallest distance and then uses index to find the index of the point that gives that closest distance.

The program then slices out the second spiral's points from its beginning to the closest point. It reverses that slice and adds it to the first spiral's points.

You can see the result on the right. The polygon's points start at C and move along the first spiral following the blue arrow. When it reaches the end of the first spiral at point A, the program finds the closest point on the second spiral at point B. It then follows that spiral backward along the yellow arrow until it returns to the center point C.

(You may have noticed that previous picture only showed a single spiral: the green one. With that approach, the program actually drew red and green spirals but the green one covered the red one. Now that the polygon isn't jumping from point A to B, the polygons don't overlap so you can see them both.)

This is a huge step forward, but if you look closely at the latest picture you'll see one more problem: the spirals don't fill the entire drawing area. There's an uncovered area on the right side.

Extending the Spirals

The latest problem is that the green spiral doesn't continue far enough to cover the whole drawing area. When it draws that spiral, the program finds the closest point on the red spiral and the result doesn't include enough of the spirals to cover the entire drawing area.

One solution is to extend the spirals a bit more. In this example, we need to continue the spirals until the closest point on the red spiral is far enough around to fill the drawing area.

We originally extended every spiral until it had gone far enough to leave the drawing area forever, so that is far enough for each spiral. The problem occurs when we find the closest point on the next spiral. That point is not at the end of its spiral so it is not guaranteed far enough to have permanently left the drawing area.

If we extend the spirals another half circle (π radians) around the center, then the closest point on the red spiral will be at the original spiral's end point and we know the spiral has permanently left the drawing area at that point.

To do that, the get_spiral_points now takes an optional extra_theta parameter. After the function finishes generating points as before, it continues creating new points until it has gone an extra extra_theta radians around the circle.

Here's the new version of get_spiral_points.

def get_spiral_points(rect, cx, cy, A, start_angle=-math.pi, extra_theta=math.pi): '''Return points that define a spiral.''' # The rect parameter gives the drawing area as xmin, ymin, xmax, ymax. # Find max_r. center = (cx, cy) dists = [ math.dist(center, (rect[0], rect[1])), math.dist(center, (rect[0], rect[3])), math.dist(center, (rect[2], rect[1])), math.dist(center, (rect[2], rect[3])), ] max_r = max(dists) # Get the points. points = [] dtheta = 5 * math.pi / 180 # Five degrees. theta = 0 max_theta = -1 while True: # Calculate r. r = A * theta # Convert to Cartesian coordinates. x, y = polar_to_cartesian(cx, cy, r, theta + start_angle) # Save the point. points.append((x, y)) # If we have gone far enough, stop. if max_theta > 0: if theta > max_theta: break elif r > max_r: max_theta = theta + extra_theta theta += dtheta return points

This function calculates max_r and starts generating points as before. Initially it sets max_theta to -1.

Inside the loop, if r > max_r, the code sets max_theta to the current theta value plus extra_theta to make the loop continue to add more points to the result.

If max_theta > 0, the code checks theta and breaks out of the loop when it exceeds max_theta.

The functions that call get_spiral_points directly or indirectly (get_spirals and get_spiral_polygons) get the benefit of extra_theta so the spirals are extended as needed.

Conclusion

[A Moiré pattern created by Archimedes spirals with A = 1] This program draws filled Archimedes spirals. It uses a couple of tricks to orient the spirals correctly so their points don't jump all over the place and to extend the spirals far enough to cover the drawing area. They're a bit confusing, but the result is still pretty fast.

Like the previous example, this one can produce some interesting Moiré patterns like the one shown on the right if you set A = 1 and then vary the number of spirals.

Download the example to experiment with it and to see additional details.

© 2025 - 2026 Rocky Mountain Computer Consulting, Inc. All rights reserved.