[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: Recursively draw rings of nested circles in Python

[A sequence of rings of nested tangent circles drawn recursively in Python]

This example combines techniques from the following posts:

Make a list of color shades in Python
Functions that let you make a list of shades of color
Make a ring of tangent circles inside another ring of tangent circles in Python
Rings of circles inside other rings of circles
Make a ring of small, tangent circles inside a larger circle in Python
The outermost ring of circles
Use a rainbow gradient to select numbers in Python
Functions to convert between RGB and HSL color models
The program uses those This post describes two of the program's key pieces of code: the part that draws the circles and the part that deals with colors at a high level.

Drawing Circles

The following draw_nested_circles function orchestrates the circle drawing.

def draw_nested_circles(dr, max_depth, outline, colors, draw_outer, n, cx, cy, R): '''Draw a circle containing a sequence of nested circle rings.''' # Draw the outer circle. if draw_outer: rect = (cx - R, cy - R, cx + R, cy + R) dr.ellipse(rect, outline=outline, fill=colors[0]) # Color 0. # Draw the first ring of inside circles. centers = get_inner_circles(n, cx, cy, R) for i, (x, y, r) in enumerate(centers): rect = (x-r, y-r, x+r, y+r) dr.ellipse(rect, outline=outline, fill=colors[i]) # Color 1. # Draw the remaining rings of circles. G = math.dist((cx, cy), (x, y)) g = r color1 = 1 draw_nested_levels(dr, max_depth, 2, colors, True, # Color 2. color1, outline, n, cx, cy, G, g)

This function draws the outer circle if desired. It then calls get_inner_circles (described in my post Make a ring of small, tangent circles inside a larger circle in Python) to get the first ring of inner circles and draws it.

The code then sets up the values it needs to draw the next level of circles and calls the following draw_nested_levels function to draw the remaining circles.

def draw_nested_levels(dr, max_depth, depth, colors, is_offset, outline, n, cx, cy, G, g): '''Recursively draw more levels.''' # See if we're done. if depth >= max_depth: return # Get next level. centers = get_nested_circles(is_offset, n, cx, cy, G, g) color = colors[depth % len(colors)] for x, y, r in centers: rect = (x-r, y-r, x+r, y+r) dr.ellipse(rect, outline=outline, fill=color) # Recurse. is_offset = not is_offset G = math.dist((cx, cy), (x, y)) g = r draw_nested_levels(dr, max_depth, depth + 1, colors, is_offset, outline, n, cx, cy, G, g)

The function first checks whether it has drawn enough nested levels of circles and returns if it has.

Next, the code calls get_nested_circles (described in Make a ring of tangent circles inside another ring of tangent circles in Python) to get the next level of nested circles. I've modified that function slightly to take a new is_offset parameter that indicates whether the first circle should be drawn with theta = -math.pi or whether it should be offset by half of the angle between the circles' centers. That lets the circles snuggle together so each level's circles are tangent to the two closest circles in the previous level.

After it draws this ring of circles, the function sets G to the distance from the center of the whole arrangement (cx, cy) to the center of the last circle it drew (x, y). It sets g to the radius of the circles it just drew and recursively calls itself to draw the next ring of circles.

The recursive calls continue until the program has drawn the desired number of nested rings.

Using Colors

My first version of this program just drew the circles, but I decided it might be more interesting if it drew the levels in different shades of color.

The following code shows how the program starts the drawing process with the code dealing with color highlighted in blue.

def draw(self, *args): '''Draw a big circle with three smaller circles inside it.''' # Create a PIL image to hold the drawing. wid = hgt = 400 transparent = (0, 0, 0, 0) image = Image.new('RGBA', (wid, hgt), transparent) dr = ImageDraw.Draw(image) n = int(self.num_circles_var.get()) margin = 10 R = min(wid, hgt) / 2 - margin cx = wid / 2 cy = hgt / 2 # Make a palette of shades. r, g, b, a = hex_to_rgb(self.color) h, s, l, a = rgb_to_hsl(r, g, b, a) num_shades = n hsl_shades = hsl_make_shades(h, s, 0.9, 0, num_shades) colors = hsl_list_to_tkinter_list(hsl_shades) # Set the outline color. if self.outline_var.get(): outline = hsl_to_tkinter((h, s, 0.5, 255)) else: outline = None # Draw the circles. max_depth = int(self.num_rings_var.get()) draw_nested_circles(dr, max_depth, outline, colors, False, n, cx, cy, R) # Display the image. self.canvas.config(width=wid, height=hgt) self.photo_image = ImageTk.PhotoImage(image) self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image) # Save the image. image.save('circles.png')

[A sequence of rings of nested tangent circles drawn recursively in Python] The code starts by creating a PIL image as before. It then converts the currently selected color self.color from a tkinter hex string to an RGB color and then converts that into an HSL color. The code calls hsl_make_shades (described in Make a list of color shades in Python) to create shades of the basic color. It creates one shade for each level of circles, starting with brightness 0.9 (light) and ending with brightness 0 (black). (You could modify the program to let the user change those values.)

The code uses the hsl_list_to_tkinter_list function (also described in Make a list of color shades in Python) to convert the shades from HSL to hex strings that tkinter can understand.

After it finishes setting up the colors, the method calls the draw_nested_circles function described earlier to do the drawing.

Conclusion

I was hoping this example would create a more interesting result. It's not as wonderful as I had hoped, but it's not too bad and it provided some challenges in making the circles line up properly. In my next post, I'll try a different coloring method.

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

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