Simple Clock

Alex Carney

28 Dec 2019 1920 x 1080 11 revisions 67 sloc v0.0.6

I thought it would be interesting to experiment with creating an image that changed each time it was drawn. Now this isn't the most exciting realisation of that idea, but it does also touch on an idea I want to look at developing further as part of arlunio itself. Applying the concept of interfaces to arlunio images I think could be very powerful - imagine having some generic "human" that knew how to walk and all you had to do was tell it how to draw one...

Anyway back to this image, as you've probably noticed by now it's a clock - not a very interesting clock but it tells the time, in fact it's frozen at the time that it was when the image was rendered.

import arlunio as ar
import numpy as np

from datetime import datetime

from arlunio.image import Image, fill 
from arlunio.mask import all_
from arlunio.math import R, T
from arlunio.shape import Circle

As the clock is made up from a collection of distinct shapes you need to ensure that each component is defined with a consistent scale otherwise it won't look right. I found it useful to say that the radius of the clockface is 1 and then define everything else relative to that. However since the default scale of the grid an image is defined on is also 1 the clock would look a bit cramped as it would run right up againt the edge.

Rather than work to a more awkard scale I'm defining a scale paramaeter that I'm going to pass to each shape. This changes the size of the underlying cartesian grid that is mapped onto the image when drawn. By enlarging this grid each shape will appear smaller and fit better into the final image

scale = 1.1

As you've probably noticed the hands of the clock are just rectangles, but instead of using the built in Rectangle shape in the standard library I've decided to define a custom ClockHand shape. This is because the standard Rectangle shape is hard to control in this scenario (perhaps this should be improved 🤔...) You will also see that I'm using polar coordinates instead of the conventional \(x\) and \(y\)

By defining the ClockHand shape with polar coordinates adding the ability to set the rotation becomes easy since all we have to do is take an offset t0 and subtract it from the angle parameter t. Something to note is that when arlunio maps polar coordinates onto an image it follows the mathematical convention of having the line t = 0 be at 3 o'clock, so in order to make positioning the hands easier later on I also shift this coordinate by \(\frac{\pi}{2}\) (90 degrees) so when t0 = 0 the hand will be pointing at 12 o'clock

Unfortunately while nice for expressing rotations, polar coordinates are certainly not the best at expressing a rectangle. Thankfully the conversion from polar to standard \(x\) and \(y\) coordinates is quite straightforward

\begin{align} x &= r\cos{(t)} \\ y &= r\sin{(t)} \end{align}

Once we have our \(x\) and \(y\) coordinates it's easy enough to define a rectangle as (ar.)all the \(x\) values between 0 and the length and all the \(y\) values between -size and size.

@ar.definition
def ClockHand(r: R, t: T, *, length=1, size=0.025):
    t -= np.pi/2
    
    x = r * np.cos(t)
    y = r * np.sin(t)
      
    return all_(
        x > 0, x < length,
        np.abs(y) < size
    )

Next we look at the shape I've called Numeral, since it's used to indicate the hours on the clockface having the ability to rotate it easily is important so it is also defined with respect to polar coordinates. In fact it's definition is almost identical to the ClockHand shape with one small difference. To try and make the clock a little more interesting to look at the sides of a numeral are defined to be those underneath a small slope, instead of having flat sides like a rectangle.

@ar.definition
def Numeral(r: R, t: T):
    t -= np.pi/2
    
    x = r * np.cos(t)
    y = r * np.sin(t)
    
    return all_(
        x > 0.8, x < 0.95,
        np.abs(y) < (0.1 * x) - 0.07
    )

With all the shapes that we need defined it's time to focus on how we're going to position the hands in order to tell the time. This is where we get into that "interfaces" idea I was talking about earlier, as you can see there's nothing formal yet instead we're just taking advantage of Python's duck typing. This function only requires that the shapes you give it all have a t0 property that controls its angle - which is all you need to implement a clock poser(?), positioner(?), director(?)... 🤔 we'll have to think of a name later!

Calculating the angle itself is quite straightforward - assuming you're familiar with radians that is. The make_clock function is written with the assumption that t0 = 0 is going to correpsond with the hand pointing at 12 o'clock, which we ensured earlier when shifting the t parameter in the ClockHand shape by \(\frac{\pi}{2}\) radians (90 degrees).

From there it's case of unpacking our Python datetime object and calculating the percentage each respective hand has travelled around the clockface by dividing it by the maximum value each hand can represent (60 minutes in an hour, 12 hours on a clock etc). Once we have the percentage we can multiply it by \(2\pi\)) (360 degrees) to get the angle. Finally we add each of the shapes representing the hands together so that we return a single object representing the clock.

def make_clock(dt, width, height, image, hour_hand, minute_hand, second_hand):
    hour = dt.hour if dt.hour <= 12 else dt.hour - 12
    minutes = dt.minute
    seconds = dt.second
    
    second_hand.t0 = -2 * np.pi * (seconds / 60) 
    minute_hand.t0 = -2 * np.pi * (minutes / 60)
    hour_hand.t0 = -2 * np.pi * ((hour / 12) + (minutes / 600))

    image = fill(second_hand(width=width, height=height), foreground="#f00", image=image)
    image = fill(minute_hand(width=width, height=height), image=image)
    image = fill(hour_hand(width=width, height=height), image=image)

    center = Circle(r=0.2)
    image = fill(center(width=width, height=height), image=image)

    return image

A few other points of interest to make about the above implementation

  • datetime objects use 24hr time so it feels neater to convert it to 12hr time even if it's not strictly necessary
  • When calculating the angle for the hour hand we also add a small contribution from the minute hand so that we replicate the effect of the hour hand moving between numbers throughout the hour. - However it probably requires further thought in order to produce a more accurate result...

Last but not least we come to the final assembly, I've gone on long enough as it is so here is a few highlights

  • We create three instances of the ClockHand, hours, minutes and seconds
  • Using a for loop we "spin" an instance of the Numeral shape around the clockface one for each hour.
  • Since our make_clock function was written to take any datetime instance, having the clock frozen at the time it was drawn is a simple as passing the result of datetime.now() to the function.
@ar.definition
def Clock(width: int, height: int) -> Image:

    clock = Circle(r=1, pt=0.01, scale=scale)
    image = fill(clock(width=width, height=height), background="white")

    for i in range(12):
        t0 = i * (np.pi / 6)
        numeral = Numeral(t0=t0, scale=scale)
        image = fill(numeral(width=width, height=height), image=image)

    hour_hand = ClockHand(size=0.02, length=0.5, scale=scale)
    minute_hand = ClockHand(size=0.02, length=0.8, scale=scale)
    second_hand = ClockHand(size=0.01, length=.95, scale=scale)

    return make_clock(datetime.now(), width, height, image, hour_hand, minute_hand, second_hand)
clock = Clock()
image = clock(width=1920, height=1080)
image