Thursday, November 19, 2015

The Code of Clocks

One of the exercises at the program I work at is reading and drawing clocks. I figured that would be a great programming exercise. Drawing lines in Pygame would be simple and I'd just put it on a clock background. First I downloaded a picture of a clock and called it "clock2.jpg."

The code for loading a background image and making it transparent is:

# Load and set up graphics.
background_image = pygame.image.load("clock2.jpg").convert()
background_image.set_colorkey(WHITE) #make it transparent

You need to write commands for drawing the hands. In Pygame a line is defined by its endpoints. Naturally the center of the screen is width/2, height/2, but the other end of the hand is a math problem! What would be the best coordinate system to use? It wouldn't be easy to point to a specific point in Cartesian (x- and y-) coordinates but it's not hard to figure out how far to rotate the hand in polar coordinates. See my book Hacking Math Class for how to explore polar coordinates using Python. Here's how to find our point using the length of the hands and the angle of rotation:


The point would be (r*cos(theta), r*sin(theta))
The length of the hand is r. I made all the hands the same length for testing. They'd be different colors, though:

# Define some colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED   = (255, 0, 0)
BLUE = (0, 0, 255)

#clock hands:
hour_length = 290
minute_length = 290
second_length = 290

Unlike the figure above, in a clock face the rotation is, well, clockwise from straight up. The easy solution to that is to switch sine and cosine. Now what's theta? All programming languages measure angles in radians, not degrees. No problem: import pi from the math or numpy module and 2*pi is a whole circle.

from math import pi, sin, cos

Each hour is a twelfth of the whole circle, or 2*pi/12. That means if it's 1 o'clock, the rotation is 1 * 2*pi divided by 12, 2 o'clock would be 2 * 2*pi /12 and so on. That makes the code for our hour hand this:

#draw hour hand
h_theta = hour*2*pi/12

and the Pygame syntax for drawing a line:

h_tip = [300+hour_length*sin(h_theta), 300-hour_length*cos(h_theta)]
pygame.draw.line(screen, BLACK, [300,300], h_tip, 2)

The "300" comes from the fact that the center of my clock is (300,300).

So that draws a perfect hour hand. Here's 1 and 2 o'clock:
hour = 1 or hour = 2

 

Let's add a minute hand. Now the circle (2*pi radians) is cut up into 60 slices, so we'll add that to our code:

#draw minute hand
m_theta = minute*2*pi/60
m_tip = [300+minute_length*sin(m_theta), 300- \ minute_length*cos(m_theta)]
pygame.draw.line(screen, BLUE, [300,300], m_tip, 2)

It'll be just as long as the hour hand, but blue instead of black. Now 1:00 looks like this:
That's correct, but when I put in 1:30 I get an incorrect clock:
hour = 1
minute = 30
The hour hand should be halfway between the 1 and the 2. Every hour the hour hand moves 2 * pi/12 radians but it should move 1/60th of that every minute. I'm changing the hour hand code to this:

h_theta = hour*2*pi/12 + minute*2*pi/(12*60)

That should fix the mistake:

We'll add a similar line for our minute hand to take into account the number of seconds, add a seconds hand and we'll have a three-handed clock.

h_theta = hour*2*pi/12 + minute*2*pi/(12*60)
...
m_theta = minute*2*pi/60 + second*2*pi/(60*60)
...
s_theta = second*2*pi/60

Of course, you need the usual Pygame code to get stuff on the screen. I highly recommend Professor Craven's tutorials, and that's where I got a template for Pygame programs.

Here's the whole code:

import pygame
from pygame.locals import *
from math import pi, sin, cos
from random import randint

# Define some colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED   = (255, 0, 0)
BLUE = (0, 0, 255)

#clock hands:
hour_length = 290
minute_length = 290
second_length = 290

#Change these numbers to show the time:
hour = 1
minute = 45
second = 50

# Create an 600x600 sized screen
screen = pygame.display.set_mode([600, 600])

# This sets the name of the window
pygame.display.set_caption('Clocks!')

# Set positions of graphics
background_position = [0, 0]

# Load and set up graphics.
background_image = pygame.image.load("clock2.jpg").convert()
background_image.set_colorkey(WHITE)

# Call this function so the Pygame library can initialize itself
pygame.init()

clock = pygame.time.Clock()

done = False
#main loop:
while not done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
            
    screen.fill(WHITE)
    # Copy image to screen:
    screen.blit(background_image, background_position)

    #draw hour hand
    h_theta = hour*2*pi/12 +minute*2*pi/(12*60)
    h_tip = [300+hour_length*sin(h_theta), 300-hour_length*cos(h_theta)]
    pygame.draw.line(screen, BLACK, [300,300], h_tip, 2)

    #draw minute hand
    m_theta = minute*2*pi/60 + second*2*pi/(60*60)
    m_tip = [300+minute_length*sin(m_theta), 300-minute_length*cos(m_theta)]
    pygame.draw.line(screen, BLUE, [300,300], m_tip, 2)

    #draw seconds hand
    s_theta = second*2*pi/60
    s_tip = [300+second_length*sin(s_theta), 300-second_length*cos(s_theta)]
    pygame.draw.line(screen, RED, [300,300], s_tip, 2)

    pygame.display.flip()

    clock.tick(60)

pygame.quit()

You can use the time module in Python to make a clock read the current time:

>>> import time
>>> the_time = time.time()
>>> dt = time.localtime(the_time)
>>> print(dt.tm_hour,":",dt.tm_min,":",dt.tm_sec)
16 : 41 : 33

In our clocks program just set the 'hour' variable equal to 'dt.tm_hour" and so on. Yes, you have a clock on your computer and your phone already but you made this one!

1 comment: