Title: Draw Archimedes spirals in Python
An Archimedes spiral is defined by the polar coordinate equation r = A * θ. It's as simple as that, and this would be a significantly shorter post except for one main question: how big do you need to make θ to make the spiral fill the drawing area?
Planning Theta
You can't simply continue drawing the spiral until it leaves the drawing area because it may come back. For example, in the picture at the top of this post, the blue spiral leaves through the left side of the drawing area, cuts back into it near the upper right corner, leaves again out of the right side, comes back in near the bottom right corner, leaves again from the bottom, and then grows too large to intersect the rectangle after that.
The distance from a point on the spiral to the spiral's center is simply r. In the Archimedes spiral, r increases as θ increases, so once r is too big to intersect the drawing area, it never goes back, and that gives us the test we need.
Find the corner of the drawing area that is farthest from the spiral's center. When r is greater than the distance to that corner, the spiral has gone far enough.
Generating Spiral Points
The following get_spiral_points function generates points for a spiral centered at (cx, cy) and within the drawing area rect.
def get_spiral_points(rect, cx, cy, A, start_angle=-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
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 r > max_r:
break
theta += dtheta
return points
The code first calculates the distances from the spiral's center to each of the drawing area's corners and then finds the minimum of those distances max_r.
It then enters a loop that runs while theta is less than max_r. Inside the loop, it calculates r, calls the polar_to_cartesian function to convert the (r, θ) from polar coordinates to Cartesian coordinates, and adds the result to the points list.
If theta is greater than max_r, the code breaks out of its loop. If theta is not greater than max_r, the code adds d_theta to theta and continues the loop to generate the next point on the spiral.
Converting Polar Coordinates
The following code shows the polar_to_cartesian helper function.
def polar_to_cartesian(cx, cy, r, theta):
'''Convert from polar to Cartesian coordinates.'''
# theta is in radians.
return cx + r * math.cos(theta), cy + r * math.sin(theta)
This code simply uses the rules for polar coordinate conversion to convert (r, θ) to (x, y).
Finding Many Spirals
The following get_spirals function gets a list of evenly spaced spirals with the same center.
def get_spirals(rect, cx, cy, num_spirals, A, start_angle=-math.pi):
'''Return points that define a group of spirals.'''
d_start = 2 * math.pi / num_spirals
# Generate the spirals.
spirals = []
for i in range(num_spirals):
spirals.append(
get_spiral_points(rect, cx, cy, A, start_angle))
start_angle += d_start
return spirals
This function first sets d_start equal to 2π / num_spirals. That gives the difference in spacing for each spiral's starting angle so they are evenly spaced around the center point.
Next, the code uses a loop to generate each of the spirals. Inside the loop, it calls get_spiral_points to get one spiral's points and adds them to the spirals list.
Using the Points
The following method uses the get_spirals method to draw the spirals and drawing area rectangle.
def draw_uncentered_spirals(self):
'''Draw the spirals.'''
# Get parameters.
self.canvas.update()
wid = self.canvas.winfo_width()
hgt = self.canvas.winfo_height()
cx = wid * 0.4
cy = hgt * 0.4
A = self.a_var.get()
rect = (wid * 0.2, hgt * 0.2, wid * 0.8, hgt * 0.8)
num_spirals = self.num_spirals_var.get()
start_angle = 0
# Define some colors.
colors = ['red', 'green', 'blue', 'orange', 'black']
# Draw the spirals.
self.canvas.delete(tk.ALL)
spirals = get_spirals(rect, cx, cy, num_spirals, A, start_angle)
for i, points in enumerate(spirals):
self.canvas.create_line(points, fill=colors[i % len(colors)])
# Draw the drawing area rectangle.
self.canvas.create_rectangle(rect, outline='yellow')
self.canvas.create_rectangle(rect, outline='black', dash=(4, 4))
This code updates the program's Canvas widget and gets its size. It uses multiples of the width and height to set the spiral's center and the drawing area's rectangle. The code then creates a list of colors before it gets into the serious spiral-drawing code.
The program calls get_spirals to get a list of spirals and then enumerates that list. For each point list, the code draws the points.
After it draws the spirals, the code draws the drawing area so you can see it.
Conclusion
The program also includes code that lets you draw spirals centered on the canvas and without showing the drawing area box. Look at the draw_spirals method to see how to switch drawings.
In addition to drawing Archimedes spirals (obviously), this example 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.
|