DEV Community

Farai Gandiya
Farai Gandiya

Posted on • Originally published at agckb.xyz on

Making Word Clock, A Clock Which Tells The Time With Words

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.

timeanddate.com showing the current time in Harare as “IT IS HALF PAST SEVEN”

Looking around a bit more, it turns out the clock in the TikTok is called a QlockTwo W and it costs at least $800.

5 QLockTwo W watches lined up in various colours next to a label saying ‘design award 2019’

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.

Papers on a table showing sample grids

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 _ _ _ _ _ _

Enter fullscreen mode Exit fullscreen mode

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)]   
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

This code:

  • loops through the rows of the grid in the order I defined in the order property,
  • creates a div with the data-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, a data-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
    },
    ...
  }

Enter fullscreen mode Exit fullscreen mode

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)
}

Enter fullscreen mode Exit fullscreen mode

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>`
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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)
}

Enter fullscreen mode Exit fullscreen mode

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 a break 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

Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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-widths 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.

package.json, server logs and the webpage on an iphone 7 plus screenshot

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:

The header with the words word clock and a description &ldquo;a clock which displays the time&hellip; in words&rdquo;, a letter grid saying &ldquo;it is five oh two&rdquo; in orange and the site footer

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

Latest comments (1)

Collapse
 
jamesncox profile image
James Cox

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!