This post was originally posted on The Algorithmic Cookbook under Making Word Clock, A Clock Which Tells The Time With Words
This post is also on YouTube.
I saw a TikTok where this guy was flexing a Qlocktwo W, a watch which tells the time with words. This inspired me to make a web version of it, which I did in one evening-ish (and fixed here and there, which is more like when I started writing this post and uncovered a lot of problems with it). Here’s how.
Prior Art
Wowed by the watch, I tried to find it online. My first search term was for ‘word clock’, I came across an online word clock by timeanddate.com. It not only shows the time in words, it does so for multiple cities,and in different languages. Outclassed before I’ve even started.
Looking around a bit more, it turns out the clock in the TikTok is called a QlockTwo W and it costs at least $800.
Making The Grid
The first challenge was making a grid. The best way would be to generate it algorithmically, but I didn’t (and still don’t) know how to. Instead, I did it manually. At first I was going to rip one off online, but I decided to try and do it myself.
After much experimentation, I managed to make this layout. Note that the underscores are for random characters.
# /_includes/js/grid.txt
a I T _ I S _ _ T W O N E _ _
b _ T H R E E L E V E N I N E
c _ _ T W E L V E I G H T E N
d _ F I V E _ S I X _ F O U R
e S E V E N _ T W E N T Y _ _
f T H I R T Y O H F O U R T Y
g _ F I F T Y _ _ T W O N E _
h _ F O U R T E E N _ F I V E
i T H R E E _ _ F I F T E E N
j _ S E V E N T E E N _ T E N
k _ _ E I G H T E E N _ _ _ _
l _ _ _ N I N E T E E N _ _ _
m _ T W E L V E L E V E N _ _
n S I X T E E N _ O C L O C K
o T H I R T E E N _ _ _ _ _ _
Turning The Grid Into Code
To build the website, I decided to use Eleventy because of how extendable it is through JavaScript. The way I’ve set it up is a big mess, but it works.
To define the grid, I created a JS file with an object containing the grid as I’ve defined it and an order in which to read the rows (eclipses for brevity).
module.exports = {
order: ['a', 'b', ..., 'o'],
a: ['I', 'T', ...rc(1), 'I', 'S', ...rc(2), 'T', 'W', 'O', 'N', 'E', ...rc(2)],
b: [...rc(1), 'T', 'H', 'R', 'E', 'E', 'L', 'E', 'V', 'E', 'N', 'I', 'N', 'E'],
...,
o: ['T', 'H', 'I', 'R', 'T', 'E', 'E', 'N', ...rc(6)]
}
You may notice various function calls like ...rc(n)
. What this does is generates n random characters and places them into the property’s array through the spread operator. So ...rc(2)
will generate two random characters. Here’s the code which generates the random characters.
function randChars (num) {
const chars = []
for (let i = 0; i < num; i++) {
const randCharCode = 65 + Math.floor(Math.random() * 25)
chars.push(String.fromCharCode(randCharCode))
}
return chars
}
const rc = randChars
For num
items, it generates a random number from 65 to 90 (which corresponds to the ASCII codes for the letters A-Z) and pushes the corresponding ASCII character to a chars
array, which is then returned. Not sure why I added the line const rc = randChars
instead of renaming randChars
to rc
since I only used it once.
This whole thing seems terribly inefficient, but it shouldn’t matter too much since it’s for building the site. It still builds pretty quick. Then again, the really slow stuff comes later.
To show this grid on the webpage, I created a data file in the _data/
directory which imports the script I defined the grid in. As I was writing this, I realized that I could have just defined it there to begin with. Either way, it had to go on the webpage, which I did with a Nunjucks template.
<div id="clock" aria-hidden="true">
{%- for row in grid.order %}
<div class="row" data-row="{{ row }}">
{%- for col in grid[row] %}
<span data-cell="{{ row }}{{ loop.index }}" data-lit=false>{{ col }}</span>
{%- endfor %}
</div>
{%- endfor %}
</div>
This code:
- loops through the rows of the grid in the order I defined in the
order
property, - creates a
div
with thedata-row
attribute set to the current grid, - in it, it then loops through each element on that row and
- puts it in a span with the
data-cell
attribute set to the row and item’s index, adata-lit
to false (more in a bit) and the contents being the letter.
Along with that, I needed to specify how the grid is defined to update the time. I did this with a massive, 201 line long object specifying where all the words are on the grid. I specified the locations for the (control???) words 'IT'
, 'IS'
. 'OH'
and 'OCLOCK'
, with the locations of the minutes and hours being defined in their own object. Thanks to the way my grid is designed, I didn’t have to list out all the locations for each of the 59 minutes as I put all the -TY numbers before the -TEENS and SUB-TEENS.
const words = {
IT: {
row: 'a',
start: 1,
end: 2
},
...,
hours: {
1: {
row: 'a',
start: 10,
end: 12
},
...
},
minutes: {
1: {
row: 'g',
start: 11,
end: 13
},
...
}
That means that the grid is defined in two places, one place for the markup, and another for the time controlling. With that set, it’s time to show the, uh… time?
Showing The Time
The site’s code is in _includes/js/main.js and is initialized in the init
function.
function init() {
const now = new Date()
const nextMinIn = (60 - now.getSeconds()) * 1000 + now.getMilliseconds()
updateTime(now)
setTimeout(() => {
updateTime(new Date())
setInterval(() => {
updateTime(new Date())
}, 60000)
}, nextMinIn)
}
What this code does is that it:
- shows the current time,
- calculate the time to the next minute (
nextMinIn
) in milliseconds, - sets a timeout to run after
nextMinIn
milliseconds which:- updates the time and
- sets an interval to update the time every minute.
All the fun stuff starts in. updateTime
, which takes a time time
.
As for what updateTime
actually does,
function updateTime(time, currLitElems) {
lightTime(time, currLitElems)
const timeElem = document.getElementById('accessTime')
let prettyNow = time.toLocaleTimeString([], {hour12: true, hour: '2-digit', minute: '2-digit', })
timeElem.innerHTML = `It is <time datetime="${prettyNow}">${prettyNow}</time>`
}
It updates the time on both the word clock and the timeElem
I made to provide an accessible version for the current time in the HTML in _includes/main.njk
.
<p id="accessTime" class="sr-only" aria-live="polite" aria-atomic="true"></p>
Back to updateTime
, there’s the lightTime
function which takes the time
and shows it on the UI.
function lightTime(time) {
dimLitWords()
const hour = time.getHours() % 12 === 0 ? 12 : time.getHours() % 12
const hourCells = words.hours[hour]
const minutes = time.getMinutes()
const litElems = [words["IT"], words["IS"], hourCells]
switch(true) {
case minutes === 0:
litElems.push(words['OCLOCK'])
break;
case minutes < 10:
litElems.push(words['OH'])
case minutes < 20:
litElems.push(words.minutes[minutes])
break
default:
const remainder = minutes % 10
const tenny = minutes - remainder
litElems.push(words.minutes[tenny])
if (remainder !== 0) {
litElems.push(words.minutes[remainder])
}
}
lightWords(litElems)
}
It finds all the lit items which matched the query [data-lit="true"]
and turns them off by setting data-lit
to false
. Controlling the data-lit
attribute is how I show certain times (or not).
After that, I parse the time’s hour and minutes, and initiate an array for the cells I want to light with “IT”, “IS” and the cell location corresponding to the hour.
As for minutes, I’m doing this in a switch statement:
- If it’s 0, I’ll add ‘OCLOCK’ to get lit.
- If it’s less than 10, I push “OH”.
- If it’s less than 20, I push the cell location for the
minutes
. Notice how there isn’t abreak
in the previous statement? That’s because I wanted to show an ‘OH’ if the number is under 10 as well as the number itself, which is covered in the < 20 case. This is probably the first time I’ve done that. - Otherwise (for numbers over 20), I push the -TY portion and the remainder if it’s greater than 0.
To light the cells, the code calls lightWords
which calls lightWord
that iterates through the range specified by a cell (in row
from start
to end
).
Other Things
For this site, I decided to use 11ty since it has a lot of JavaScript integration and is pretty fast. It’s structured like this:
word-clock/
_data/
grid.js
grid.txt
_includes/
css/
main.njk
main.css
normalize.css
js/
main.js
main.njk
partials/
head.njk
index.njk
.eleventy.js
index.md
css.md
js.md
As previosuly mentioned, _data
holds the grid that is rendered by _includes/index.njk
. index.njk
has the web page template, with the grid. I put the site’s head
in partials/head.njk
, which itself contains a description tag for SEO. Surely SEO is more complicated than that. There’s also a meta generator with the Eleventy version I used.
The css.md
and js.md
files are there so I can concat all the css and js files into a css and js file respectivley. They need a better asset pipeline like Hugo’s or maybe I need to learn how to use it better. I should go through Andy Bell’s Eleventy Course.
I’ve gone through the JS, but the CSS is nothing special. There’s normalize.css as my reset and the CSS I declared (ellipses for brevity).
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 100%;
--bg: rgb(16, 16, 16);
--fg: white;
--dimed: rgb(200, 200, 200);
--lit: orange;
}
.sr-only{...}
a {color: orangered;}
a:hover, a:active {color: orange; outline: 2px dashed white;}
a:visited {color: goldenrod;}
/* Clock Row */
.row {
display: flex;
justify-content: center;
font-family: monospace;
}
/* Clock Cell */
.row > span {
font-size: 1.25rem;
padding: 0.75vmin 1vmin;
font-weight: bold;
}
[data-lit=false] {
color: var(--dimed)
}
[data-lit=true] {
color: var(--lit);
}
It’s the first time I’ve used CSS variables. These ones are mostly for the background and foreground colors, and the colors for the links and the states for data-lit
. The sr-only
is to make content visible only for screen readers, which is the access-time
I mentioned earlier.
The clock rows use flexbox to spread things about and each cell has some padding and bold monospace text. Interestingly, it’s rather responsive without me specifying any max-width
s and the like. There’s the margin: x auto
trick to center things, but that’s the extent of my work on making the site responsive.
Last thing is that at some point I used the app play.js to run this website, and it worked. The trick is to hook into the eleventy package, and serve the site yourself. I wish I could show it to you in action, but something changed betwen Eleventy versions meaning that it doesn’t work anymore. The most I have is this picture showing the package.json
file, Eleventy’s server log and the webpage, all on my iPhone 7 Plus.
While it app itself has a long way to go (dependency resolution is rather recent and git operations are rather weak), I’m hoping that the play.js team keep improving the app.
Conclusion and What’s Next
With all my work, here’s what Word Clock looks like now:
And that’s how I made Word Clock in an evening-ish… and a bit more since I discovered a lot of bugs while making it. Interestingly, writing about how I made Word Clock helped me find bugs I didn’t notice before. I should do this more often.
As for what I want to do next, there’s a few things. It wouldn’t hurt to make this site prettier and include a few animations here and there. Also, it would be nice to support additional locales or languages as well as to make a grid generator to make telling the time more dynamic with algorithms.
You can find Work Clock online and it’s source code on GitHub.
Thanks for reading! If you’ve enjoyed this post, you can support me by
- sharing this post,
- becoming a Patron,
- send me some money on either Paypal, Ko-fi or Buy Me A Coffee or
- Get a domain on using my Namecheap affiliate link. Note that I’ll earn a comission on anything you buy with that link.
Top comments (1)
This is highly impressive and creative! I am sure there are better ways to do it but I love that you just did it your way and made it work. That’s awesome. I am a big proponent of “Do it however you know how!” And like you mentioned, you learned a lot just writing about it. And you learned even more just throwing yourself into it and trying. My favorite way to learn!
If you ever get around to trying it a different way I can’t wait to see what you come up with.
Well done! This is awesome work and you should be proud!