Clock
3 Nov 2019 · 📖 in 9 minutes Clocks, clocks everywhereTL;DR Like clocks? I built a clock made of clocks, click here to play with it 👇
Or read on for a more in depth look at building clocks out of clocks (with some React).
Inspiration from strange places
Have you something like this before?
Me too, it's cool huh! I really enjoy the way the time seems to gradually appear and take shape.
Actully, the more I looked at it the more I appreciated the way it was put together, they are not indiviudal clocks in the traditional sense as one arm is not simply following the other. Both arms of the clock are moving freely allowing for the various interesting shapes and movements and, of course, the time.
I bet that wouldn't be too difficult to put together, a RaspberryPi to control the "clocks" with a mechanism to specify an angle for the first and second hands...
Creativity intensifies
Ok, so I don't have the motors to build the physical clocks and I'm not sure what I need to buy or how to wire them into a RaspberryPi. However, what I can do to prototype the idea is build a web app.
The quickest way to get started is to use create-react-app
, this provides the basic
setup for a React app.
The first thing I wanted to do was to create a simple clock, requirements were
simple; two hands that can move independantly of each other and a face.
Throwing together some div
s, a bit of CSS and voila, I had the makings of a clock, I can specify
the angle in degrees, of each hand using the a CSS transform rotate
and there's a white
face.
Making things move
From here I wanted to see what the best way to animating things could be. I spent a bit of time looking into the various ways I could animate components in React. Then I thought, nah, why not just see what I can I achieve without a library, surely I should be able to prototype something just using React and a bit of CSS knowhow.
Using create-react-app
meant that I get a modern version of React out of the
box, which means hooks! I've been looking for an excuse to try out hooks and
this seemed a good a time as any to try.
I imagined a basic rendering loop like this:
- inital state sets start / end positions
- render clock hands at initial positon
setTimeout
orrequestAnimationFrame
to increment and set new position- render clock hands at new position
Keeping the hand positions in state meant I could "animate" the hands by updating the state, incrementally, and causing a re-render which would update the hands to their new position.
const [angle, setAngle] = useState({
hour: getRandomStartingAngle(),
minute: getRandomStartingAngle()
});
In the new world of React hooks there's a hook perfect for the job of
triggering the increment: useEffect
which, amongst other things, is run after every render (for more details
check out the docs).
To increment I needed to create an effect with would trigger at
a reasonable rate, for this I used requestAnimationFrame
which is a suitable
API to schedule an update on as it's usually called 60 times per second
(generally considered the threshold for smooth animation).
useEffect(()=> {
requestAnimationFrame(()=>{
const { hour, minute } = angle;
setAngle({
hour: hour + 1,
minute: minute + 1
});
})
}, [angle]);
Putting it all together and I had a clock that animated around and around, and around, and around, and never stop.
It seemed to work pretty well. However, I made a couple of mistakes which won't become apparent until I start trying to create numbers from the clocks.
Drawing numbers, with clocks
Next was to put a bunch of these little clocks onto the screen and see how they animate, using a small bit of flexbox
to define rows / columns and create 2x3 grids for a single number.
So it's starting to look a lot more like it could resemble a number. In order to animate to the number I needed to work out all the various positions that could go into a clock number and then tell the smaller clocks to animate to those positions.
The approach to draw a number was to pass a targetAngle
to each of the
smaller clocks. The basic idea was that for a given target angle the clock
would continue increment the postion of the hands until they'd reached it, then
stop.
function getIncrementValue(angle, targetAngle) {
if(angle === targetAngle){
return angle;
} else {
return resetAngle(angle + 1);
}
}
Incrementing by 1 each time means the target angle would eventually be achieved, however the first mistake I made in the sub clock logic rears its head.
As the hands increment around they can reach an angle above 360deg
which breaks for situations where the hand has to travel around the whole clock
to reach the target angle. To solve this I added a resetAngle
function
which keeps the numbers between 0 < 359
allowing the taget angle to always be
reached.
Next was the job of actually figuring out these angles. The approach intially was to write, by hand, each angle, for each number, for each clock in the 2x3 grid... I quickly grew tired of this. Instead it's easier to specify a a number of set positions which are the building blocks of the number.
const createAngleSet = (hour, minute) => ({hour, minute});
const bothLeft = createAngleSet(270, 90);
const bothRight = createAngleSet(90, 270);
const bothTop = createAngleSet(0, 180);
const bothBottom = createAngleSet(180, 0);
const topAndBottom = createAngleSet(0, 0);
const rightAndLeft = createAngleSet(90, 90);
const topLeftCorner = createAngleSet(90, 0);
const topRightCorner = createAngleSet(270, 0);
const bottomLeftCorner = createAngleSet(0, 270);
const bottomRightCorner = createAngleSet(0, 90);
const emptySpace = createAngleSet(225, 45);
Above is the list of all the positions needed to "draw" the numbers 0-9 and they're used in a number config that looks something like this:
TWO: {
a1: { ...bothRight },
a2: { ...topLeftCorner },
a3: { ...bottomLeftCorner },
b1: { ...topRightCorner },
b2: { ...bottomRightCorner },
b3: { ...bothLeft }
}
The result of all this work was the realisation of the numbers. The effect was captured almost exactly as I wanted it, with the number appearing out of the randomness of the single clock faces.
The full NumberGrid is available for preview and illustrates the whole number set that is used in building the clock.
Animations, animations, animations
Earlier I mentioned I had made a mistake when creating the mini-clock, did you catch it?
Well, my
first go with useEffect
was more on feeling than careful study of the
documentation. As a result when I first attmepted to draw the numbers 0-9, at
the same time, there was pretty terrible performance.
Turns out useEffect
is expected to be triggered and then torn down, as a reuslt it's
supposed to provide a cleanup
function to do any bits of cleanup that are
needed if an in progress effect needs to be cancelled. This caused a subtle issue
as everything seems to animate smoothly however it slowed way down as I scaled up from the 1 mini-clock to the 54 that I needed in order to display the full 0-9 numbers.
useEffect(()=> {
const increment = requestAnimationFrame(()=>{
const { hour, minute } = angle;
const { hour: targetHour, minute: targetMinute } = targetAngle;
setAngle({
hour: getIncrementValue(hour, targetHour, speed),
minute: getIncrementValue(minute, targetMinute, speed)
});
})
return () => cancelAnimationFrame(increment);
}, [angle, targetAngle, speed]);
I corrected this within my effect with cancelAnimationFrame
and added a speed value, via useContext
to give me some
control over the mini-clock animations (necessary for building a stopwatch).
Clock
I now have all the pieces to build the clock. Using the Date
object and updating the
target time every time the hours or seconds changed. The DigitalClock
would
then work out the individual parts of the time string and pass them to the
ClockNumbers
which would, in turn, pass the individual parts to each mini
clock.
I am so happy with the result 🕑 😬
Thanks for reading and check out the clock below 👇
First appeared on Trusty Interior, last update 3 Nov 2024