<div id="app"></div>
* {
font-family: monospace;
box-sizing: border-box;
}
:root {
--clock-size: 100px;
--seconds-width: 1px;
--seconds-length: 45px;
--minutes-width: 2px;
--minutes-length: 40px;
--hours-width: 4px;
--hours-length: 30px;
}
main {
border: 1px solid black;
margin: 16px auto;
width: 450px;
padding: 0 16px 16px;
}
label {
align-items: center;
display: flex;
}
.clocks-container {
display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
justify-content: space-between;
}
h2 {
text-align: center;
}
.clock {
width: 100px;
height: 100px;
margin: 0 auto;
border: 1px solid black;
border-radius: 50px;
position: relative;
box-sizing: content-box;
}
.clock-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
border-radius: 4px;
background: black;
}
.clockface {
position: relative;
height: 100%;
top: 50%;
left: 50%;
}
.num {
position: absolute;
font-size: 12px;
}
.hand {
position: absolute;
transition: transform 0.01s;
border-radius: 2px 2px 10px 10px;
}
.seconds {
width: var(--seconds-width);
height: var(--seconds-length);
background: red;
transform-origin: calc(var(--seconds-width) / 2) var(--seconds-length);
left: calc(50px - var(--seconds-width) / 2);
top: calc(var(--clock-size) / 2 - var(--seconds-length));
}
.minutes {
width: var(--minutes-width);
height: var(--minutes-length);
background: blue;
transform-origin: calc(var(--minutes-width) / 2) var(--minutes-length);
left: calc(50px - var(--minutes-width) / 2);
top: calc(var(--clock-size) / 2 - var(--minutes-length));
}
.hours {
width: var(--hours-width);
height: var(--hours-length);
background: black;
transform-origin: calc(var(--hours-width) / 2) var(--hours-length);
left: calc(50px - var(--hours-width) / 2);
top: calc(var(--clock-size) / 2 - var(--hours-length));
}
import React, { FC, useEffect, useRef, useState } from "https://cdn.skypack.dev/react@17.0.1";
import * as ReactDOM from "https://cdn.skypack.dev/react-dom@17.0.1";
const useAnimationFrame = (callback: () => void) => {
const savedCallback = useRef<() => void>()
useEffect(() => {
savedCallback.current = callback
})
useEffect(() => {
let rafId: number
const loop = () => {
if (savedCallback.current) {
savedCallback.current()
}
rafId = requestAnimationFrame(loop)
}
loop()
return () => cancelAnimationFrame(rafId)
}, [savedCallback])
}
const identity = <T, >(x: T) => x
const Clockface = () => {
const numbers = Array(12).fill(0).map((_, i) => i)
return <div className="clockface">
{numbers.map(num => {
const angle = Math.PI - num * (2 * Math.PI) / 12
const style = {
top: `${Math.cos(angle) * 42}%`,
left: `${Math.sin(angle) * 42}%`,
transform: `translate(-50%, -50%)`,
}
return <div key={num} className="num" style={style}>{num === 0 ? 12 : num}</div>
})}
</div>
}
type ClockProps = {
shouldQuantize: boolean,
timeZone: string | 'local',
}
const ClockHands: FC<ClockProps> = ({ shouldQuantize, timeZone }) => {
const degrees = 360
const [handsAngles, setHandsAngles] = useState({
hours: 0,
minutes: 0,
seconds: 0,
})
const getTimeZoneOffsetMs = (timeZone: string | 'local') => {
const now = new Date()
const utcDate = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }))
const tzDate = new Date(now.toLocaleString('en-US', { timeZone: timeZone === 'local' ? undefined : timeZone }))
return (tzDate.getTime() - utcDate.getTime())
}
useAnimationFrame(() => {
const nowMs = Date.now() + getTimeZoneOffsetMs(timeZone)
const hours = nowMs % (1000 * 60 * 60 * 12) / 1000 / 60 / 60
const minutes = hours % 1 * 60
const seconds = minutes % 1 * 60
const quantize = shouldQuantize ? Math.floor : identity
setHandsAngles({
hours: (quantize(hours) / 12) * degrees,
minutes: (quantize(minutes) / 60) * degrees,
seconds: (quantize(seconds) / 60) * degrees,
})
})
return <>
<div className="hand hours" style={{ transform: `rotate(${handsAngles.hours}deg)` }}/>
<div className="hand minutes" style={{ transform: `rotate(${handsAngles.minutes}deg)` }}/>
<div className="hand seconds" style={{ transform: `rotate(${handsAngles.seconds}deg)` }}/>
</>
}
const Clock: FC<ClockProps> = ({ shouldQuantize, timeZone }) => {
return <div>
<h2>{timeZone}</h2>
<div className="clock">
<Clockface/>
<ClockHands shouldQuantize={shouldQuantize} timeZone={timeZone}/>
<div className='clock-center' />
</div>
</div>
}
const AnalogClock = () => {
const [shouldQuantize, setShouldQuantize] = useState(false)
return <main>
<h1>Analog clock</h1>
<label>
<span>Quantize time:</span>
<input type="checkbox" checked={shouldQuantize} onChange={() => setShouldQuantize(!shouldQuantize)}/>
</label>
<div className="clocks-container">
<Clock shouldQuantize={shouldQuantize} timeZone="local"/>
<Clock shouldQuantize={shouldQuantize} timeZone="Europe/Berlin"/>
<Clock shouldQuantize={shouldQuantize} timeZone="America/New_York"/>
<Clock shouldQuantize={shouldQuantize} timeZone="Australia/Eucla"/>
</div>
</main>
}
ReactDOM.render(<AnalogClock />, document.getElementById('app'));
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.