Making a Neon Clock using React Hooks
Ganesh Prasad
Posted on September 7, 2019
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 Requirements
- It should sync with the system clock and tick every second.
- It should convert the current time to an object specifying how to read it out in standard english.
- It should highlight the relevant phrases from a list of words that would combine to read out the current time.
- It should speak the what time it is, every 15 minutes.
- 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')
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',
];
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,
};
}
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:
- Create a configuration object that describes certain aspects of the reading out process for any given time.
- 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.
- Do we need the seconds value ? (This is a definite NO)
- 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 ?
- 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 ?
- 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 ?
- What is the nearest multiple of five value from the minute hand ?
- Is it an exact hour ? Is it something O'clock or noon or midnight ?
- Is it a some minutes past a certain hour ?
- Is it less than 30 minutes before a certain hour ?
- What is the nearest value to the hour hand on the dial of the clock ?
- 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,
};
}
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
];
}
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(' ');
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
- check the time now
- decide when the next second starts
- register a 1000ms (1s) interval when the next second starts.
- 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;
}
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);
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>
);
}
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>
);
}
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);
}
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;
}
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;
}
}
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;
}
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 span
s 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);
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 div
s 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 !
Posted on September 7, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 21, 2023