<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
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.