DEV Community

Cover image for Making a Neon Clock using React Hooks
Ganesh Prasad
Ganesh Prasad

Posted on • Updated on

Making a Neon Clock using React Hooks

A little bit of backstory

TL;DR; some rather dark humour on what motivated me to make the clock

About 2 years ago, back in September of 2017, when I was a Software Engineer in Zomato in Delhi NCR, I contracted a severe case of Viral Hepatitis-E. Then I had to take leave from the job and go back to my parental home in the small, little known, coastal town in Odisha (my hometown) to take rest and recover. Recovering from an illness like Hepatitis-E is a rather lengthy and painful process, it took me 20 days. Back then, the network coverage in my locality was not very good and the internet speed was frustratingly low (a little better than 2G) and there were only a handful of channels available on the TV (most of them being local news channels). So, for 20 long days, I stayed home virtually cut off from the world outside, not having a lot of things to worry about than taking rest and recovering. Of course, I had some good books (novel mostly) in my room, but there are only so many times a man can read and re-read a certain book. All in all, life was as far removed as possible from the hustle of a fast growing start-up in a metro city.

I spent the 20 days, reading, looking at the clock, reading again, looking at the clock again, checking if it was time to take medicines, read again, look at the clock again and so on... There is a saying that time goes slow when you want it to pass faster, it was one of those times.

Eventually, a couple of days in to my recovery / isolation, I figured if I had to spend half of my life looking at clocks and telling myself it was so-and-so o' clock of the day, why not code a little clock for a change ? I could write that in good old HTML, CSS and Vanilla JS without having to access the internet and pull half of everything out there with npm. And I did.

2 years later, that is in September of 2019, I have revisited that little clock of mine and rewritten it using React Hooks. So let's jump into it and look at the making of the neon clock.

The Clock Making

Here is how it looks like (the clock that we will be building in this article)

The Neon Clock

The Requirements

  1. It should sync with the system clock and tick every second.
  2. It should convert the current time to an object specifying how to read it out in standard english.
  3. It should highlight the relevant phrases from a list of words that would combine to read out the current time.
  4. It should speak the what time it is, every 15 minutes.
  5. The clock should be animated.

Scaffolding the page in Pug

Because we will be using React to render our clock, we do not really need to write a lot of HTML just now; rather we will just link our JS libraries and stylesheets and create a container div with id root where React will render our application. Let's write that up quickly in Pug.

We will be using the Julius Sans One font from Google fonts, because that's cool.

html
  head
    title Neon Clock (Using React Hooks)
    meta(name='viewport', content='initial-scale=2.0')
    link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Julius+Sans+One')
    link(rel='styleheet', href='/style.css')

  body
    #root
    script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js')
    script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js')
Enter fullscreen mode Exit fullscreen mode

Writing the application in Javascript

Getting the time and reading it out

Let's start with the phrases, that we will need to read out the time. Here are a few examples where the time is given in 24 hours format along with the way these are read:

  • 00:00 - It's midnight
  • 00:05 - It's five past midnight
  • 00:10 - It's ten past midnight
  • 01:00 - It's one O'clock in the morning
  • 01:15 - It's quarter past one in the morning
  • 13:30 - It's half past one in the afternoon
  • 11:45 - It's quarter to noon
  • 17:55 - It's five to six in the afternoon
  • 20:25 - It's twenty five past eight in the evening
  • 20:26 - It's about twenty five past eight in the evening
  • 20:24 - It's nearly twenty five past eight in the evening
  • ... and so on

If we look at all the possible strings that follow this format, it becomes apparent that they can be constructed from the following list of phrases in order:

const phrases = [
  'IT IS',
  'ABOUT',
  'NEARLY',
  'TEN',
  'QUARTER',
  'TWENTY',
  'FIVE',
  'HALF',
  'PAST',
  'TO',
  'ONE',
  'TWO',
  'THREE',
  'FOUR',
  'FIVE',
  'SIX',
  'SEVEN',
  'EIGHT',
  'NINE',
  'TEN',
  'ELEVEN',
  'NOON',
  'MIDNIGHT',
  'O\' CLOCK',
  'IN THE',
  'MORNING',
  'AFTERNOON',
  'EVENING',
];
Enter fullscreen mode Exit fullscreen mode

Notice that, five and ten appear twice in the list. This is because these phrases can appear twice in a time read out (once in the minute part and once in the hour part, consider 17:25 or 04:55 or 10:10 etc)

Now let's write up a function that will get the current time and extract hour, minute and second values, as well as the locale string describing the current date and current time.

function getNow () {
  const now = new Date(Date.now());
  const hour = now.getHours();
  const minute = now.getMinutes();
  const second = now.getSeconds();
  const display = now.toLocaleString();

  return {
    hour,
    minute,
    second,
    display,
  };
}
Enter fullscreen mode Exit fullscreen mode

Now that we have a function to get the current time as a simple object, let's write a function to analyze that current time object and figure out how to read it out.

We can do that in 2 steps:

  1. Create a configuration object that describes certain aspects of the reading out process for any given time.
  2. Generate the actual time read out string.

For step-1 let's consider a few questions we need to answer before we can decide how to read out a give time value.

  1. Do we need the seconds value ? (This is a definite NO)
  2. Is the minute value an exact multiple of 5 ? In other words, is the minute hand pointing directly to a number on the dial of the clock ?
  3. Is the minute value slightly less than a multiple of 5 ? In other words, is the minute hand slightly before a number on the dial of the clock ?
  4. Is the minute value slightly more than a multiple of 5 ? In other words, is the minute hand slightly after a number on the dial of the clock ?
  5. What is the nearest multiple of five value from the minute hand ?
  6. Is it an exact hour ? Is it something O'clock or noon or midnight ?
  7. Is it a some minutes past a certain hour ?
  8. Is it less than 30 minutes before a certain hour ?
  9. What is the nearest value to the hour hand on the dial of the clock ?
  10. Is it morning or afternoon or evening ?

We can now write a function that takes a simple time object containing hour and minute values and answers these questions for us.

function getReadoutConfig ({ hour, minute }) {
  const lastMinuteMark = Math.floor(minute / 5) * 5;
  const distFromLast = minute - lastMinuteMark;
  const isExact = distFromLast === 0;
  const isNearly = !isExact && distFromLast > 2;
  const isAbout = !isExact && !isNearly;
  const nearestMinuteMark = isNearly
    ? (lastMinuteMark + 5) % 60
    : lastMinuteMark;
  const isOClock = nearestMinuteMark === 0;
  const isPast = !isOClock && nearestMinuteMark <= 30;
  const isTo = !isOClock && !isPast;
  const minuteMark = (isPast || isOClock)
    ? nearestMinuteMark
    : 60 - nearestMinuteMark;

  const nearestHour = (isTo || (isOClock && isNearly)) ? (hour + 1) % 24 : hour;
  const nearestHour12 = nearestHour > 12
    ? nearestHour - 12
    : nearestHour;
  const isNoon = nearestHour === 12;
  const isMidnight = nearestHour === 0;
  const isMorning = !isMidnight && nearestHour < 12;
  const isAfternoon = nearestHour > 12 && nearestHour <= 18;
  const isEvening = nearestHour > 18;

  return {
    isExact,
    isAbout,
    isNearly,

    minute: minuteMark,
    isOClock: isOClock && !isNoon && !isMidnight,
    isPast,
    isTo,

    hour: nearestHour12,
    isNoon,
    isMidnight,
    isMorning,
    isAfternoon,
    isEvening,
  };
}
Enter fullscreen mode Exit fullscreen mode

In step-2, we take the configuration object returned from the function above and check which phrases needs to be highlighted to read out the given time. We will simply return an array of boolean values (true or false) indicating whether a phrase in the phrases array is to be highlighted or not.

function getHighlights (config) {
  return [
    true, // IT IS
    config.isAbout, // ABOUT
    config.isNearly, // NEARLY
    config.minute === 10, // TEN
    config.minute === 15, // QUARTER
    config.minute === 20 || config.minute === 25, // TWENTY
    config.minute === 5 || config.minute === 25, // FIVE
    config.minute === 30, // HALF
    config.isPast, // PAST
    config.isTo, // TO
    config.hour === 1, // ONE
    config.hour === 2, // TWO
    config.hour === 3, // THREE
    config.hour === 4, // FOUR
    config.hour === 5, // FIVE
    config.hour === 6, // SIX
    config.hour === 7, // SEVEN
    config.hour === 8, // EIGHT
    config.hour === 9, // NINE
    config.hour === 10, // TEN
    config.hour === 11, // ELEVEN
    config.isNoon, // NOON
    config.isMidnight, // MIDNIGHT
    config.isOClock, // O' CLOCK
    config.isMorning || config.isAfternoon || config.isEvening, // IN THE
    config.isMorning, // MORNING
    config.isAfternoon, // AFTERNOON
    config.isEvening, // EVENING
  ];
}
Enter fullscreen mode Exit fullscreen mode

Now we can get the actual time readout string by concatenating highlighted phrases from the phrases array:

const readoutConfig = getReadoutConfig(time);
const highlighted = getHighlights(readoutConfig);
const readoutString = phrases.filter((phrase, index) => highlighted[index]).join(' ');
Enter fullscreen mode Exit fullscreen mode

The useClock hook

Now that we have functions to get the current time and to read it out, we need some way to make sure that these functions get used in sync with the system clock every second. We can do that by

  1. check the time now
  2. decide when the next second starts
  3. register a 1000ms (1s) interval when the next second starts.
  4. everytime the interval ticks, update the current time in our app.

Let's write a React Hook for that and call it useClock. Firstly, we need a state value called time that will keep track of the current time. And we need another state value called timer that will keep track of whether we have set an interval or not.

Our hook will check if the timer or interval has been set and if not, it will set the interval. This bit of logic can be written using useEffect, that runs once when the application renders for the first time. This effect does not need to run on every subsequent render unless we clear the interval and set the timer to null.

Each time the interval ticks, we will set the state time to the current time.

Because the users of the useClock hook are not supposed to set the time value by themselves, and can only read it, we will return only time from the useClock hook.

function useClock () {
  const [timer, setTimer] = React.useState(null);
  const [time, setTime] = React.useState(getNow());

  // this effect will run when our app renders for the first time
  React.useEffect(() => {
    // When this effect runs, initialize the timer / interval
    if (!timer) initTimer();

    // This returned function will clear the interval when our app unmounts
    return (() => (timer && window.clearInterval(timer) && setTimer(null));

  }, [timer]); // This hook will run only when the value of timer is set or unset

  function initTimer () {
    const now = Date.now();
    const nextSec = (Math.floor(now / 1000) + 1) * 1000;
    const timeLeft = nextSec - now;

    // Register an interval beginning next second
    window.setTimeout(() => {
      // on each second update the state time
      const interval = window.setInterval(() => setTime(getNow()), 1000);

      // now our timer / interval is set
      setTimer(interval);
    }, timeLeft);
  }

  return time;
}
Enter fullscreen mode Exit fullscreen mode

Rendering the Clock and Readout components

Now that we have almost everything in place, let's write some components to render our app. First we need an app component that will render inside the root div we created in our Pug file. It will contain a standard analog clock component and a time readout component.

function NeonClock () {
  const time = useClock();
  return (
    <div className='clock'>
      <StandardClock time={time} />
      <TimeReadout time={time} />
    </div>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<NeonClock />, root);
Enter fullscreen mode Exit fullscreen mode

Let's build the StandardClock component first. It will look like an analog clock and will be animated. To look like an analog clock, it will have a dial, which will have 12 Roman numerals and 60 small line segments. Each 5th line segment out of these 60 small line segments needs to be slightly longer. Let's call these small line segments ticks for simplicity. The clock will have 3 hands of course, which will rotate at their own speeds.

As it can be seen the only moving parts of this clock are the 3 hands. We can set their rotational motion by setting the CSS transform: rotate(xx.x deg).

function StandardClock ({ time }) {
  const clockMarks = [ 'XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI' ];

  // Calculate the angles of rotation of each hand (in degrees)
  const hourAngle = ((time.hour % 12) * 60 + time.minute) / 2;
  const minuteAngle = (time.minute * 60 + time.second) / 10;
  const secondAngle = time.second * 6;

  return (
    <div className='standard-clock'>
      <div>
        { clockMarks.map(mark => <span className='mark'>{mark}</span>) }
      </div>
      <div>
        { Array(60).fill(1).map(tick => <span className='tick' />) }
      </div>
      <div className='inner-circle' />
      <div className='inner-circle-2' />
      <div className='hour-hand' style={{ transform: `rotate(${hourAngle}deg)` }} />
      <div className='minute-hand' style={{ transform: `rotate(${minuteAngle}deg)` }} />
      <div className='second-hand' style={{ transform: `rotate(${secondAngle}deg)` }} />
      <div className='center' />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, let's build the time readout component. This will of course have the phrases, some of which will be highlighted. This will also have a speaker component which will use the window.speechSynthesis API to speak out the time every 15 minutes.

To display the readout text in a cool way, we will display all the phrases in a muted manner and add a glow class to the phrases that should be highlighted.

function TimeReadout ({ time }) {
  // get the highlighted phrases and the readout string
  const readoutConfig = getReadoutConfig(time);
  const highlighted = getHighlights(readoutConfig);
  const timeText = phrases.filter((phrase, index) => highlighted[index]).join(' ') + '.';

  // speak out the time only on the first second of each 15 minutes
  const shouldSpeak = time.second === 0 && time.minute % 15 === 0;

  return (
    <div className='readout'>
      <p className='phrases'>
        { phrases.map((phrase, index) => (
          <span className={highlighted[index] ? 'glow' : ''}>
            {phrase}
          </span>
        ))}
      </p>
      <p className='timer'>{time.display}</p>
      <Speaker active={shouldSpeak} text={timeText} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

With that done, let's build our Speaker component. First we need a function that will speak out any given text in a proper British accent (Because the Brits speak English as it should be spoken, which is with humour. And apparently they invented the English language in the first place, bless them !)

To speak the text, first we need to create an utterance object for the text and set the rate (how fast should it speak), pitch (of the voice), volume and the voice template (we will use the first voice that speaks en-GB). Then we can pass this utterance object to the speechSynthesis.speak function to actually get it spoken out.

function speak (text) {
  const synth = window.speechSynthesis;
  const rate = 0.7;
  const pitch = 0.6;
  const voice = synth.getVoices().filter(v => v.lang === 'en-GB')[0];
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.voice = voice;
  utterance.pitch = pitch;
  utterance.rate = rate;
  synth.speak(utterance);
}
Enter fullscreen mode Exit fullscreen mode

Now we can create a Speaker component, that will render nothing, but use an useEffect hook to call the speak function when the prop active is set to true.

function Speaker ({ active, text }) {
  React.useEffect (() => {
    if (active) speak(text);
  });
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Styling our components

With all the components and logic in place, let's style our components using LESS. In this section I will briefly mention some major / important points in the styling, please refer to the pen for this article for the specifics.

The muted and glowing phrases

The muted text effect is created by using a muted and darker shade of red and a 2px blur on the text. The glow effect is created by using a brighter (almost white) shade of red and a red coloured text-shadow with a 20px spread. Moreover the font-weight of the glowing text is set to bold to give it a bolder and brighter look.

span {
  color: @muted-red;
  margin: 0 10px;
  transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
  vertical-align: middle;
  filter: blur(2px);

  &.glow {
    color: @glowing-red;
    text-shadow: 0 0 20px @shadow-red;
    font-weight: bold;
    filter: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

The Roman Numerals on the dial

The dial of the clock (the circle) is of the dimension 300px * 300px. Each of the Roman numerals is placed with absolute positioning, 10px inside the outer circle and horizontally centered with respect to the outer circle. The transform-origin of the spans containing the numerals is set to coincide with the center of the circle.

.mark {
  position: absolute;
  display: inline-block;
  top: 10px;
  left: 115px;
  width: 50px;
  height: 30px;
  font-size: 30px;
  text-align: center;
  color: @glowing-red;
  filter: none;
  transform-origin: 25px 140px;
}
Enter fullscreen mode Exit fullscreen mode

Then each of these spans containing the numerals is rotated with increments of 30 degrees. We have used a LESS recursive mixin to generate the rotations and apply them to the spans with nth-child selectors.

.generate-mark-rotation (@n) when (@n = 13) {}
.generate-mark-rotation (@n) when (@n < 13) {
  span.mark:nth-child(@{n}) {
    transform: rotate((@n - 1) * 30deg);
  }
  .generate-mark-rotation(@n + 1);
}
.generate-mark-rotation(2);
Enter fullscreen mode Exit fullscreen mode

Same method is used to put the 60 line-segments on the dial in place.

Placing and rotating the hands

The hands are first placed at the 00:00:00 position, using absolute positioning with the bottom of each hand coinciding with the center of the circle. Then the transform-origin of the hands is set to coincide with the center of the circle.

When the transform:rotate(xx.x deg) is set by the React component on the hand divs they rotate with respect to the center of the clock.

Making it responsive

For simplicity, we have set the upper bound for small screen devices to be 960px. For smaller screens we use smaller font sizes and smaller dimensions for the clock components. That makes it reasonably responsive across all devices.

Here is the pen containing everything described in this article

Hoping you enjoyed reading about this little project and learned a few things from it.
You can find more about me at gnsp.in.

Thanks for reading !

Top comments (5)

Collapse
 
remibruguier profile image
Rémi BRUGUIER

That is neat, I will give it a go. Thanks for sharing.

Collapse
 
chrisachard profile image
Chris Achard

Neat clock! Thanks for the walkthrough :)

Collapse
 
idkjs profile image
Alain

github.com/idkjs/reason-neon-clock ported to #ReasonML. Thanks for sharing this.

Collapse
 
gnsp profile image
Ganesh Prasad

Wow ! This is awesome. Nice work !

Collapse
 
apurvvv profile image
APURV SINGH

How can it be done on React Native? Ganesh, you are a fabulous developer.