Drawing a clock with Python and the Cairo graphics library

The Hour Numerals

In the last section, I warned that drawing the numerals on the clock would be complex since it involves text handling. On the one hand, drawing text can get very complicated: Some languages draw their characters differently depending on the order in which the characters appear in the text (Arabic letters often connect together much like cursive in Western languages); text might consist of words from a language written from left-to-right mixed with words from a language written from right-to-left; and some Asian languages are written vertically. For such serious layout of text, it is a good idea to use a library such as Pango [6] to ensure all such complications are dealt with properly.

However, the numerals on a clock face are much simpler to draw: They are language-neutral (all languages print Indo-Arabic numerals 1 through 12 the same way). Therefore, I can get away with using Cairo's built-in text handling capabilities [7], which are simplistic enough that they are marked as a "toy API."

The first step is to define a list (actually a Python tuple) of strings, each corresponding to one of the 12 hour markings on the clock face. The 12-hour mark is conventionally placed at the top of the clock, so that mark will appear first on the list; the rest of the list is in increasing numerical order starting with 1:

NUMERAL_STRINGS = ('12', '1', '2', '3', '4', '5', '6', '7', '8', '9','10', '11')

Of course, feel free to use any text strings you'd like, as long as there are 12 of them in the list and they are not too long; you might, for example, prefer Roman numerals over the Indo-Arabic ones I've chosen to use here. Figure 7 shows the clock with Indo-Arabic numerals.

Figure 7: The completed clock face, showing the numerals denoting the hours.

Again, some distances need to be calculated relative to the size of the entire clock, so that all parts of the clock will scale properly. For a 300 pixel clock, I find a 10-pixel gap between the indicator mark lines and the numerals to be reasonable – the same distance I chose for the gap between the indicator marks and the clock bezel in the previous section. The numerals should be fairly large – they need to be easily readable – so I chose 30 pixels, or a fifth of the clock's radius, for the numerals:

marks_to_numeral_gap = clock_radius/15
numeral_size = clock_radius/5

The distance from the center of the clock to each numeral can be roughly calculated at this point, but the final position of each numeral has to be determined on a numeral-by-numeral basis, as the text for each hour marking may vary in height (some fonts display digits and many letters with descenders, among other inconsistencies):

numeral_distance = mark_end_distance - marks_to_numeral_gap

Next, Cairo needs to know the font and font size in which to subsequently draw text. Many clocks I have seen have the numerals drawn in a serif font; most serif fonts I've seen are quite thin, so I chose bold text to make the display a bit clearer:

context.select_font_face ('serif', cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
context.set_font_size (numeral_size)

You may replace serif with sans-serif if you prefer, or even another generic font name such as monospace or cursive. Specific font names, such as Times New Roman, also work here, though a fallback font will be used instead if the named font you choose is not installed. Also, you may change cairo.FontSlant.NORMAL to a different setting, such as cairo.FontSlant.ITALIC, but in practice, I've never seen a clock with such styled hour markings.

The next part of the program operates in a loop to encircle the clock, much like in the last section when I drew the indicator mark lines. The main difference in this part is that this loop only cycles 12 times, since there are only 12 hour marks, instead of 60 second marks. But like in the previous section, the angle at which each hour mark is to be placed is calculated for each iteration of the loop:

angle = 2*math.pi/12 * i
x_multiplier = math.sin (angle)
y_multiplier = -math.cos (angle)

For efficiency, I have precalculated the sine and cosine of the angle and stored them in temporary variables; they will be used multiple times in the following calculations.

Now I will use Cairo to find out how wide and tall the text comprising the hour marking will be when it is drawn on the image – without actually drawing the text anywhere yet. This is because I need to know exactly where to position the text, and that depends on the final dimensions of the text:

extents = context.text_extents (NUMERAL_STRINGS[i])

Calculating where to position the text relative to the center of the clock certainly looks complicated, but it really isn't; it just involves a fair amount of arithmetic:

text_x = \
 numeral_distance * x_multiplier + \
 extents.x_bearing * x_multiplier - \
 extents.width / 2 * x_multiplier - \
 extents.width / 2
text_y = \
 numeral_distance * y_multiplier + \
 extents.y_bearing * y_multiplier + \
 extents.height / 2 * y_multiplier + \
 extents.height / 2

numeral_distance positions the text in the ballpark of where it needs to go. However, the text will vary in size from one numeral to another – 12 is definitely at least wider than a 1, and some fonts draw a 6 with a rounded part on the bottom, which they don't draw on a 2, for instance, so even the heights of the text strings are likely to vary. To properly center the text, the coordinates at which to place the text are adjusted by half the text's width (extents.width / 2) and half the text's height ( extents.height / 2). Sines and cosines range between negative 1 and positive 1, and Cairo needs to know where to position the top-left corner of the text, so I have to further subtract half the width (and height) from the coordinates. Finally, when Cairo draws the "extents" (dimensions) of the text, it tends to expect the coordinates to be relative to the bottom left corner of the text; the x_bearing and y_bearing contain adjustments to be made to the coordinates in order to obtain coordinates relative to where Cairo expects them to be.

Finally, draw the text at the proper position in the image:

context.move_to (text_x, text_y)
context.show_text (NUMERAL_STRINGS[i])
context.fill ()

Using context.stroke () in lieu of context.fill () would produce an outline or "silhouette" of the characters in the text, instead of normal "solid" characters.

The Hour, Minute, and Second Hands

I have now walked you through the steps and code required to draw all of the components of a clock face – from the bezel and white backdrop to the markings for each second and the numerals denoting each hour. However, a clock is not complete without a set of hands to indicate the current time. The hands consist of nothing more than a few lines and an arc (a semi-circle to be exact). In this section, I will at last complete the clock program by adding the code to draw the hour, minute, and second hands according to the current time.

To find the current time in Python, I use the localtime () function, which requires importing the time module built-in to Python:

import time
current_time = time.localtime ()

localtime () returns a Python object which, among other members, contains the current hour (tm_hour, ranging from 0 to 23, where 0 is midnight); the current minute (tm_min, ranging from 0 to 59); and the current second (tm_sec, usually ranging from 0 to 59, though very rarely it can reach 60 due to leap seconds). With these three values and some mathematics, I can calculate the coordinates of each component – line and arc – of each hand on the clock.

As with the other parts of the clock, I want the dimensions of the hands to scale along with the rest of the clock. For a 300 pixel clock, I find that hands with a width of about 8 pixels look good. I also like hour, minute, and second hands which, respectively, radiate 50, 75, and about 95 pixels out from the center of the clock. These dimensions are 1/20, 1/3, 1/2, and 5/8 of the clock's radius:

hand_width = clock_radius/20
hour_hand_length = clock_radius/3
minute_hand_length = clock_radius/2
second_hand_length = clock_radius*5/8

Once I know the current hour, minute and second, I need to calculate the angle at which to draw each of the hands. I convert the 24-hour time returned by localtime () to 12-hour time using a modulus of 12 (divide by 12 and find the remainder). From there, in theory, it should be simply a matter of converting the ranges of each of the values – 0 to 60 seconds, 0 to 60 minutes, and 0 to 12 hours – to fractions of 2*pi:

second_hand_angle = 2*math.pi / 60 * current_time.tm_sec
minute_hand_angle = 2*math.pi / 60 * current_time.tm_min
hour_hand_angle = 2*math.pi / 12 * (current_time.tm_hour % 12)

However, a real analog clock usually connects all three hands to the same rotor, gearing down each hand in such a way that the hands move at different rates. This means that the hour hand only moves 1/12 of the circumference of the clock when the minute hand makes a full circle; but the hour hand moves continuously, albeit barely noticeably, every time the minute hand – and, for that matter, the second hand – moves. I'll do the same with this clock:

second_hand_angle = 2*math.pi / 60 * current_time.tm_sec
minute_hand_angle = 2*math.pi / 60 * current_time.tm_min +second_hand_angle / 60
hour_hand_angle = 2*math.pi / 12 * (current_time.tm_hour % 12) +minute_hand_angle / 60

The code for drawing the hands is the same for each of the three hands – the only part that varies is the length of each hand (the hour hand is conventionally shorter than the other two hands at least). For this reason, I implemented the code for drawing a hand as a function, whose two parameters are the angle at which the hand should be drawn and the desired length of the hand:

def draw_hand (hand_angle, hand_length):

Inside the function, I calculate the coordinates for the points making up the hand. Each hand starts with a line as long as the desired hand length; the line is drawn half the hand's width from the center of the clock. The tip of the hand is drawn as an arc – a semi-circle to be exact – centered between the top of the first line of the hand and a second, mirror-image line to finish the hand:

x1 = hand_width/2 * -math.cos (hand_angle)
y1 = hand_width/2 * -math.sin (hand_angle)
x2 = x1 + hand_length * math.sin (hand_angle)
y2 = y1 - hand_length * math.cos (hand_angle)
xc = hand_length * math.sin (hand_angle)
yc = hand_length * -math.cos (hand_angle)

Now it's time to actually draw the hand:

context.move_to (x1, y1)
context.line_to (x2, y2)
context.arc (xc, yc, hand_width/2, hand_angle - math.pi, hand_angle)
context.line_to (-x1, -y1)
context.close_path ()
context.fill ()

Returning to the main part of the program, I call the function for each hand to draw – first the hour, then the minute, and finally the second hand:

draw_hand (hour_hand_angle, hour_hand_length)
draw_hand (minute_hand_angle, minute_hand_length)
draw_hand (second_hand_angle, second_hand_length)

Finally, I draw a circle over the center of the clock, with a diameter equal to the width of each hand:

context.arc (0, 0, hand_width/2, 0, 2*math.pi)
context.fill ()

Without the extra circle, the base of each hand would be drawn with flat lines, giving a less-than-eye-appealing "sharp" look to the hands (Figure 8). Compare Figure 8 to Figure 1, which does include the circle capping the rotor.

Figure 8: The clock, with the hands drawn without a circular "rotor cap" to conceal the sharp lines comprising the bases of the hands.

Listing 3 contains the complete code for the clock program. As already shown, Figure 1 illustrates the final result of the completed program. Obviously, the exact appearance of the clock will vary depending on the current time and your choice of fonts (for the numerals).

Listing 3

clock.py

001 #!/usr/bin/env python3
002
003 import math
004 import time
005 import cairo
006
007 def draw_hand (hand_angle, hand_length):
008         x1 = hand_width/2 * \
009              -math.cos (hand_angle)
010         y1 = hand_width/2 * \
011              -math.sin (hand_angle)
012         x2 = x1 + hand_length * \
013              math.sin (hand_angle)
014         y2 = y1 - hand_length * \
015              math.cos (hand_angle)
016         xc = hand_length * \
017              math.sin (hand_angle)
018         yc = hand_length * \
019              -math.cos (hand_angle)
020
021         context.move_to (x1, y1)
022         context.line_to (x2, y2)
023         context.arc (xc, yc,
024                      hand_width/2,
025                      hand_angle-math.pi,
026                      hand_angle)
027         context.line_to (-x1, -y1)
028         context.close_path ()
029         context.fill ()
030
031 # Replace the following with values of
032 # your choosing:
033 IMAGE_WIDTH  = 300
034 IMAGE_HEIGHT = 300
035
036 MARK_WIDTH       = 1
037 MAJOR_MARK_WIDTH = 3
038
039 # You may change this to the
040 # corresponding Roman numerals ('XII',
041 # 'I', 'II', etc.) if you wish
042 NUMERAL_STRINGS = ('12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11')
043
044 clock_diameter = min (IMAGE_WIDTH,
045                       IMAGE_HEIGHT)
046 clock_radius = clock_diameter/2
047
048 surface = cairo.ImageSurface (cairo.Format.ARGB32, IMAGE_WIDTH, IMAGE_HEIGHT)
049 context = cairo.Context (surface)
050
051 context.translate ((IMAGE_WIDTH-clock_diameter)/2, (IMAGE_HEIGHT-clock_diameter)/2)
052
053 # Draw the background of the clock face
054 context.set_source_rgb (1., 1., 1.)
055 context.arc (clock_radius, clock_radius,
056              clock_radius, 0, 2*math.pi)
057 context.fill ()
058
059 # Draw the clock bezel
060 # (300 / 12 = 25 pixels)
061 bezel_width = clock_diameter/12
062 bezel_radius = clock_radius - bezel_width/2
063
064 context.set_source_rgb (0., 0., 0.)
065 context.set_line_width (bezel_width)
066 context.arc (clock_radius, clock_radius,
067              bezel_radius, 0, 2*math.pi)
068 context.stroke ()
069
070 # Draw the circles enclosing the
071 # indicator marking lines
072 # (150 / 15 = 10 pixels)
073 bezel_to_marks_gap = clock_radius/15
074 # (150 / 10 = 15 pixels)
075 mark_length = clock_radius/10
076 mark_start_distance = clock_radius - bezel_width - bezel_to_marks_gap
077 mark_end_distance = mark_start_distance - mark_length
078
079 # From now on, make Cairo interpret all
080 # coordinates we pass to it as relative
081 # to the center of the clock
082 context.translate (clock_radius,
083                    clock_radius)
084
085 context.set_line_width (MARK_WIDTH)
086 context.arc (0, 0, mark_start_distance,
087              0, 2*math.pi)
088 context.stroke ()
089 context.arc (0, 0, mark_end_distance,
090              0, 2*math.pi)
091 context.stroke ()
092
093 # Draw the indicator marks
094 for i in range (60):
095         angle = 2*math.pi/60 * i
096
097         if i % 5 == 0:
098                 context.set_line_width (MAJOR_MARK_WIDTH)
099         else:
100                 context.set_line_width (MARK_WIDTH)
101
102         context.move_to (mark_start_distance * math.sin (angle), mark_start_distance * -math.cos (angle))
103         context.line_to (mark_end_distance * math.sin (angle), mark_end_distance * -math.cos (angle))
104         context.stroke ()
105
106 # Draw the numerals denoting each hour
107 # (150 / 15 = 10 pixels)
108 marks_to_numeral_gap = clock_radius/15
109 # (150 / 5 = 30 pixels)
110 numeral_size = clock_radius/5
111 numeral_distance = mark_end_distance - marks_to_numeral_gap
112
113 context.select_font_face ('serif', cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
114 context.set_font_size (numeral_size)
115
116 for i in range (12):
117         angle = 2*math.pi/12 * i
118         x_multiplier = math.sin (angle)
119         y_multiplier = -math.cos (angle)
120
121         extents = context.text_extents (NUMERAL_STRINGS[i])
122
123         text_x = numeral_distance * x_multiplier + extents.x_bearing * x_multiplier - \
124          extents.width / 2 * x_multiplier - extents.width / 2
125
126         text_y = numeral_distance * y_multiplier + extents.y_bearing * y_multiplier + \
127          extents.height / 2 * y_multiplier + extents.height / 2
128
129         context.move_to (text_x, text_y)
130         context.show_text (NUMERAL_STRINGS[i])
131         context.fill ()
132
133 # Draw the hour, minute and second hands
134  # (150 / 20 = 7.5 pixels)
135 hand_width = clock_radius/20
136
137  # (150 / 3 = 50 pixels)
138 hour_hand_length = clock_radius/3
139  # (150 / 2 = 75 pixels)
140 minute_hand_length = clock_radius/2
141  # (150 * 5/8 = 93.75 pixels)
142 second_hand_length = clock_radius*5/8
143
144 current_time = time.localtime ()
145
146 second_hand_angle = 2*math.pi / 60 * \
147                     current_time.tm_sec
148 minute_hand_angle = 2*math.pi / 60 * \
149                     current_time.tm_min + \
150                     second_hand_angle / 60
151 hour_hand_angle = 2*math.pi / 12 * \
152                   (current_time.tm_hour % 12) + \
153                   minute_hand_angle / 60
154
155 draw_hand (hour_hand_angle,
156            hour_hand_length)
157 draw_hand (minute_hand_angle,
158            minute_hand_length)
159 draw_hand (second_hand_angle,
160            second_hand_length)
161
162 # Draw the rotor cap in front of the
163 # base of the hands, to conceal the
164 # sharp edges of the lines which
165 # comprise the clock hands
166 context.arc (0, 0, hand_width/2,
167              0, 2*math.pi)
168 context.fill ()
169
170 # Write the result to a PNG file
171 surface.flush ()
172 surface.write_to_png ('clock.png')

Conclusion

In this article, I have shown you how to use the Cairo graphics library to draw computer graphics. I have demonstrated its use by walking you through all the steps required to draw an analog clock displaying the current time. This article only scratches the surface of what Cairo can do. For instance, you can draw Bezier curves, which are far more flexible in scope than arcs. Cairo can also draw shapes in colored gradients and other patterns more complex than solid colors. You also can save to other file formats in addition to PNG. I could have written the program to draw to SVG files instead. If you want to send output to a printer, look into Cairo's capability for producing output as PDF or PostScript. In a subsequent article, I will integrate the clock into an interactive graphical user interface program using the GTK [2] user interface library.

Infos

  1. The Cairo graphics library: https://cairographics.org/
  2. The GTK graphical user interface toolkit: https://www.gtk.org/
  3. A list of official Cairo documentation: https://cairographics.org/documentation/
  4. The official Cairo programmer's reference documentation: https://cairographics.org/manual/
  5. The X Window System: https://www.x.org/
  6. The Pango text layout and rendering library: https://www.pango.org/
  7. Documentation for Cairo's basic text rendering capabilities: https://cairographics.org/manual/cairo-text.html

The Author

Michael Williams is a freelance programmer and avid user of Debian-based Linux distributions. He mainly enjoys working with the GTK graphical toolkit and is a developer on the MATE Desktop project (https://mate-desktop.org). Find his various public contributions on GitHub (https://github.com/thesquash) and the Ubuntu MATE Community forum (https://ubuntu-mate.community/u/gordon/summary)

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Graphics in Python with Cairo and GTK

    Add graphics that automatically update. We show you how to build an analog clock widget with the Cairo and GTK libraries.

  • Blender

    With a little help from Blender you can create your own 3D models – including animations. This article shows you how to assemble a partially automated virtual watch model with Blender and Python.

  • DIY Alarm Clock

    A few electronic components, some code, and a hand-made wooden case make a fine retro-style bedside clock.

  • Bang! Ding! Crash!

    To create an action-packed game with LÖVE, these are a few last things you should learn how to do – overlay fancy images to "physical" objects, detect collisions, and get input from the keyboard or mouse.

  • Programming Snapshot – Go Racing Game

    The fastest way through a curve on a racetrack is along the racing line. Instead of heading for Indianapolis, Mike Schilli trains his reflexes with a desktop application written in Go, just to be on the safe side.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News