Reputation: 1574
I am trying to build an analog clock with the second hand rotating every second, min hand rotating 6deg every minute and hour hand rotating 6deg every 12 minutes.
Here's the codesandbox: https://codesandbox.io/s/react-example-d7mes?file=/Clock.js
Whenever the min hand's angle is either of these [72, 144, 216, 288, 360]
(12 minute degrees), I rotate the hour hand by 6 degrees once.
This is what I'm doing:
let twelveMinDegrees = [72, 144, 216, 288, 360];
setInterval(() => {
this.setState(prev => ({
sec: prev.sec == 360 ? 6 : prev.sec + 6, //degrees
min: prev.sec == 354 ? (prev.min == 360 ? 6 : prev.min + 6) : prev.min, //degrees
hrs: (function(){ //degrees
const indx = twelveMinDegrees.findIndex(el => el == prev.min)
if(!minChanged && indx >=0){ //only change once for every 12min
minChanged = true;
let incHrs = prev.hrs + (6*indx);
console.log(incHrs);
return incHrs;
}else{
if(!twelveMinDegrees.includes(prev.min)){
minChanged = false;
}
return prev.hrs;
}
})()
}))
}, 1000)
But the hour hand does not change and is set back to the previous value in the else part the second time and the value returned incHrs
is neglected because before the state is updated, the else
is called the next second which returns prev.hrs
which is still the old value(not the value returned in the if(!minChanged && indx >=0)
)
How do I fix this?
Upvotes: 3
Views: 207
Reputation: 56875
There's a fundamental design problem here which is that setInterval
is the wrong tool for keeping time. setInterval
only guarantees that the callback won't run for at least 1000 milliseconds, not that it will run exactly in 1000 milliseconds, so you're going to wind up with drift and skipped times.
I recommend using requestAnimationFrame
and the Date
library or performance.now
to determine when ticks have occurred.
With this set up, you can scale the hour hand proportional to the number of minutes left in the hour with:
(hours + minutes / 60) * 30 + 180
If you want a rougher granularity to the hour hand adjustments, truncate the minutes into 6 distinct chunks:
(hours + floor(minutes / 10) * 10 / 60) * 30 + 180
Doing this mathematically is much less messy than looking up the increment points in a hardcoded array.
Here's a minimal example which you could use to keep accurate time (I'll leave styling to you):
.hand {
width: 2px;
height: 40%;
background-color: black;
transform-origin: top center;
position: absolute;
border-radius: 3px;
top: 50%;
left: 50%;
}
.analog-clock {
position: relative;
border-radius: 50%;
border: 1px solid #aaa;
height: 120px;
width: 120px;
}
<script type="text/babel" defer>
const {Fragment, useEffect, useState, useRef} = React;
const Clock = () => {
const [date, setDate] = useState(new Date());
const requestRef = useRef();
let prevDate = null;
const tick = () => {
const now = new Date();
if (prevDate && now.getSeconds() !== prevDate.getSeconds()) {
setDate(now);
}
prevDate = now;
requestRef.current = requestAnimationFrame(tick);
};
useEffect(() => {
requestRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(requestRef.current);
}, []);
const pad = n => n.toString().padStart(2, 0);
const computeHourDeg = date =>
(date.getHours() + ~~(date.getMinutes() / 10) * 10 / 60) * 30 + 180
;
return (
<Fragment>
<div className="analog-clock">
<div
className="hand"
style={{transform: `rotate(${6 * date.getSeconds() + 180}deg)`}}
></div>
<div
className="hand"
style={{transform: `rotate(${6 * date.getMinutes() + 180}deg)`}}
></div>
<div
className="hand"
style={{background: "red",
height: "30%",
transform: `rotate(${computeHourDeg(date)}deg)`}}
></div>
</div>
<h3>
{pad(date.getHours())}:
{pad(date.getMinutes())}:
{pad(date.getSeconds())}
</h3>
</Fragment>
);
};
ReactDOM.render(<Clock />, document.body);
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Here's a sped-up version with a mocked Date
object to illustrate that it's working correctly:
.hand {
width: 2px;
height: 40%;
background-color: black;
transform-origin: top center;
position: absolute;
border-radius: 3px;
top: 50%;
left: 50%;
}
.analog-clock {
position: relative;
border-radius: 50%;
border: 1px solid #aaa;
height: 120px;
width: 120px;
}
<script type="text/babel" defer>
const {Fragment, useEffect, useState, useRef} = React;
const speedMS = 5;
class MockDate {
static second = 0;
static minute = 0;
static hour = 0;
constructor() {
this.second = MockDate.second;
this.minute = MockDate.minute;
this.hour = MockDate.hour;
}
getSeconds() {
return this.second;
}
getMinutes() {
return this.minute;
}
getHours() {
return this.hour || 12;
}
}
setInterval(() => {
if (++MockDate.second === 60) {
MockDate.second = 0;
if (++MockDate.minute === 60) {
MockDate.minute = 0;
MockDate.hour = (MockDate.hour + 1) % 12;
}
}
}, speedMS);
const Clock = () => {
const [date, setDate] = useState(new MockDate());
const requestRef = useRef();
let prevDate = null;
const tick = () => {
const now = new MockDate();
if (prevDate && now.getSeconds() !== prevDate.getSeconds()) {
setDate(now);
}
prevDate = now;
requestRef.current = requestAnimationFrame(tick);
};
useEffect(() => {
requestRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(requestRef.current);
}, []);
const pad = n => n.toString().padStart(2, 0);
const computeHourDeg = date =>
(date.getHours() + ~~(date.getMinutes() / 10) * 10 / 60) * 30 + 180
;
return (
<Fragment>
<div className="analog-clock">
<div
className="hand"
style={{transform: `rotate(${6 * date.getSeconds() + 180}deg)`}}
></div>
<div
className="hand"
style={{transform: `rotate(${6 * date.getMinutes() + 180}deg)`}}
></div>
<div
className="hand"
style={{background: "red",
height: "30%",
transform: `rotate(${computeHourDeg(date)}deg)`}}
></div>
</div>
<h3>
{pad(date.getHours())}:
{pad(date.getMinutes())}:
{pad(date.getSeconds())}
</h3>
</Fragment>
);
};
ReactDOM.render(<Clock />, document.body);
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
Upvotes: 1