[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: Use PIL to make an analog clock out of cake slice images in Python

[An analog clock featuring cake slice images built with PIL in Python]

My earlier post Keep an analog clock on top of all other windows in tkinter and Python explained how to make an analog clock with a non-rectangular window that stays above other windows. You can click and drag to move it. Ensure that the window has the focus and then press Alt+F4 to close it.

This post modifies that one to draw the clock face as a collection of scaled and rotated cake slice images. Click and drag any slice to move the clock. (You can also click and drag the clock's hands or outline, although they're harder to hit.) Click Alt+F4 to close the program.

There are two main new pieces to this example. First, it uses PIL to load the slices and then scale, rotate, and position them. Second, the tick method uses PIL to draw the clock's hands every second.

Drawing the Clock Face

When it starts, the program calls the following method to draw the clock face.

def make_clock_face(self): # Make the PIL image to hold the face. size = (self.clock_wid, self.clock_wid) clock_face = Image.new('RGBA', size, color='pink') dr = ImageDraw.Draw(clock_face) bounds = ((0, 0), (self.clock_wid-1, self.clock_wid-1)) dr.ellipse(bounds, fill='pink', outline='black', width=2) # Loop through image files to make the tick marks. slice_wid = self.clock_wid // 4 slice_radius = self.clock_wid / 3 angle = 0 for filename in glob.glob('*.png'): self.draw_slice(clock_face, filename, angle, self.cx, self.cy, slice_wid, slice_radius) angle += 360 / 12 # Save the clock face without clock hands. self.clock_face = clock_face # Display the clock face. photo_image = ImageTk.PhotoImage(clock_face) self.face_label = tk.Label(self.window, image=photo_image, highlightthickness=0, borderwidth=0) self.face_label.pack() self.face_label.image = photo_image # Start the clock. self.window.focus_force() self.tick()

This code creates a new PIL image that has the clock's desired dimensions, setting its background color to pink. The window's transparentcolor property is set to pink so any parts of the window that remain pink are invisible when the clock is displayed. (See the previous post for information about setting the transparency color and removing the window's title bar and borders.)

Next, the code makes an ImageDraw object to draw on the image. It then draws a circle to outline the clock. You can remove this outline if you like.

The code then uses glob.glob to enumerate the PNG files in the current directory. It loops through those files and calls draw_slice for each. (I'll describe draw_slice in the next section.) After calling draw_slice, the code adds 360 / 12 = 30 degrees to angle so the next slice is properly positioned and rotated.

After drawing the slices, this method saves the clock face image in self.clock_face. It then uses the image to make a PhotoImage version that tkinter can understand. It displays that image in a label, forces focus to the window (so the window closes if you immediately press Alt+F4), and calls self.tick, which I'll describe shortly.

draw_slice

The following draw_slice method performs some of the most interesting PIL operations in the program.

def draw_slice(self, clock_face, filename, angle, cx, cy, slice_wid, slice_radius): '''Use this file as a tick mark.''' # Load the image file. image = Image.open(filename) # Resize. slice_hgt = int(slice_wid * image.height / image.width) image = image.resize((slice_wid, slice_hgt)) # Rotate. image = image.rotate(180-angle, expand=True, fillcolor=None) # Draw. radians = math.radians(angle) x0 = int(cx + math.cos(radians) * slice_radius - image.width / 2) y0 = int(cy + math.sin(radians) * slice_radius - image.height / 2) x1 = x0 + image.width y1 = y0 + image.height box = (x0, y0, x1, y1) clock_face.paste(image, box=box, mask=image)

This method first opens the cake slice image file. To make all of the cake slices roughly the same size, it then resizes the slice image to it has the width slice_wid. To do that without distorting the image, it calculates a height to match the width. This calculation makes the original and resized images have the same aspect ratio: slice_wid / slice_hgt = image.width / image.height.

Note that the code converts the resulting height into an integer because PIL only works with integer image sizes. You cannot have an image that is 75.5 pixels tall.

Having calculated the necessary image height, the code uses the image's resize method to resize it.

Next the method rotates the image. It rotates it by 180 - angle degrees to make the image point in the right direction. For example, the angle 0° points to the right. The position of the second cake slice is 30° from there at 4:00. To make the slice point in toward the center of the clock, the program needs to rotate that image by 180 - 30 = 150°.

Note that the code sets expand=True in the call to rotate so that method expands the image if necessary so the rotated result fits. If you rotate a rectangular image by 45°, the corners would stick off of the result if you do not allow the image to expand as needed. Setting expand=True tells PIL to make the result image as large as necessary and center the original picture in the result image.

The code also sets fillcolor=None so any new parts of the rotated image are filled with a transparent color.

Having scaled and rotated the image, the method then draws it onto the clock face. It uses sines and cosines to calculate the piece's position distance slice_radius from the clock's center. Note that the math.sin and math.cos methods take their angle parameters in radians so the code must convert the slice's angle from degrees to radians.

Now that it knows where the slice should be centered, the code uses clock_face.paste to draw the image there. The original cake slice pictures have transparent backgrounds and the call to rotate filled new areas with a transparent color, so only the cake part is non-transparent.

If you omit the paste method's mask parameter, it sets pixels on the result image to transparent where they are transparent in the rotated cake slice. The result is a light gray box surrounding the cake slice, which looks pretty bad. (Remove the mask parameter to see for yourself.)

The mask parameter tells paste to only paste the parts of the image that correspond to non-transparent parts of the mask image. Since the non-cake pixels in the rotated slice are transparent, this means only the cake parts are pasted onto the clock face.

Pasting only non-transparent pixels onto an image is a somewhat non-intuitive process, but it's essential when you're pasting images.

self.tick

The following code shows how the program's tick method draws the clock's hands every second.

def tick(self): '''Update the clock hand positions.''' now = datetime.now() # Copy the clock face. face = self.clock_face.copy() dr = ImageDraw.Draw(face) # Seconds. seconds = now.second second_radians = seconds / 60 * (2 * math.pi) - math.pi / 2 x = self.cx + math.cos(second_radians) * self.second_radius y = self.cy + math.sin(second_radians) * self.second_radius dr.line(((self.cx, self.cy), (x, y)), fill='red', width=2) # Minutes. minutes = now.minute + seconds / 60 minute_radians = minutes / 60 * (2 * math.pi) - math.pi / 2 x = self.cx + math.cos(minute_radians) * self.minute_radius y = self.cy + math.sin(minute_radians) * self.minute_radius dr.line(((self.cx, self.cy), (x, y)), fill='red', width=4) # Hours. hours = now.hour + minutes / 60 hour_radians = hours / 12 * (2 * math.pi) - math.pi / 2 x = self.cx + math.cos(hour_radians) * self.hour_radius y = self.cy + math.sin(hour_radians) * self.hour_radius dr.line(((self.cx, self.cy), (x, y)), fill='red', width=6) # Draw the central dot. self.cx = self.radius self.cy = self.radius r = 5 bounds = ((self.cx - r, self.cy - r), (self.cx + r, self.cy + r)) dr.ellipse(bounds, fill='black') # Display the new image. photo_image = ImageTk.PhotoImage(face) self.face_label.configure(image=photo_image) self.face_label.image = photo_image # Try again in a second. DELAY = 1000 # Update every 1000 milliseconds. self.face_label.after(DELAY, self.tick)

This code first gets the current date and time. It makes a copy of the clock face, which holds the cake slices but no hands. It makes an ImageDraw object to use when drawing on the image copy.

Next, the code gets the current time's seconds number and converts that into an angle on the clock. One end of the seconds hand is at the center of the clock face (self.cx, self.cy). The code uses sines and cosines to find the position of the hand's other end and uses dr.line to draw the hand.

The method uses similar code to draw the minutes and hours hands. It then draws a circle at the clock's center. (It looks a little strange if you omit that circle.)

Having finished drawing the hands, the program converts the clock face copy into a PhotoImage and configures self.face_label to display it. The program saves the PhotoImage in self.face_label.image so it won't be garbage collected and tkinter can use it as needed to display the clock on the screen.

Finally, the method calls self.face_label.after to call the tick method again in 1000 milliseconds or 1 second.

Conclusion

This example produces a rather silly result, but it demonstrates several useful PIL techniques including:
  • Using a transparent background on a window
  • Scaling an image
  • Rotating an image
  • Pasting an image onto another image while using a transparency mask
  • Copying an image and making changes to update it periodically without redrawing everything

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

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