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 necessaryLast but not least we come to the final assembly, I've gone on long enough as it is so here is a few highlights
ClockHand
, hours, minutes and secondsNumeral
shape around the clockface one for each
hour.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