The problem

Draw a working clock: 60 tick marks around a dial, plus hour, minute, and second hands that show the real time. Every hand is an angle problem — and screens measure angles differently than math class.

Angles on a screen

Math angles start at +x and go counterclockwise with y up. A clock starts at 12 and goes clockwise with y down. For a hand showing a fraction \(f\) of a full turn (e.g. 15 minutes = 0.25), the clockwise-from-top angle is:

$$ \theta = 2\pi f $$

and the endpoint at length \(L\) from center \((c_x, c_y)\) is:

$$ x = c_x + L\sin\theta, \qquad y = c_y - L\cos\theta $$

Check: at \(f = 0\) (12 o’clock), \((x, y) = (c_x, c_y - L)\) — straight up. At \(f = 0.25\), \((c_x + L, c_y)\) — pointing right. Correct.

Time → fractions

$$ f_{sec} = \frac{s}{60}, \qquad f_{min} = \frac{m + s/60}{60}, \qquad f_{hr} = \frac{(h \bmod 12) + m/60}{12} $$

Including the smaller unit makes hands glide instead of jumping (the minute hand creeps as seconds pass, like a real clock).

Full build

Paste this into the JS Playground in canvas mode:

var cx = W / 2, cy = H / 2, R = 150;

// Dial + 60 ticks
ctx.strokeStyle = '#e2e8f0';
ctx.lineWidth = 4;
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.stroke();
for (var i = 0; i < 60; i++) {
  var a = (i / 60) * Math.PI * 2;
  var big = (i % 5 === 0);
  var r1 = R - (big ? 18 : 9);
  ctx.lineWidth = big ? 4 : 2;
  ctx.beginPath();
  ctx.moveTo(cx + r1 * Math.sin(a), cy - r1 * Math.cos(a));
  ctx.lineTo(cx + R * Math.sin(a), cy - R * Math.cos(a));
  ctx.stroke();
}

function hand(fraction, length, width, color) {
  var a = fraction * Math.PI * 2;
  ctx.strokeStyle = color;
  ctx.lineWidth = width;
  ctx.lineCap = 'round';
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx + length * Math.sin(a), cy - length * Math.cos(a));
  ctx.stroke();
}

loop(function () {
  var now = new Date();
  clear();
  // (redraw dial each frame — wrap the code above in a function in real code)
  hand(((now.getHours() % 12) + now.getMinutes() / 60) / 12, R * 0.55, 7, '#e2e8f0');
  hand((now.getMinutes() + now.getSeconds() / 60) / 60, R * 0.8, 5, '#38bdf8');
  hand(now.getSeconds() / 60, R * 0.9, 2, '#f472b6');
});

The snippet redraws hands each frame on top of a static dial. For a tidy program, wrap the dial drawing in drawDial() and call clear(); drawDial(); inside the loop.

Try it

  1. Add a smooth second hand: use s + ms/1000 over 60 instead of whole seconds.
  2. Add numbers: place 12, 3, 6, 9 with fillText at \(f = 0, 0.25, 0.5, 0.75\).
  3. Break it on purpose: use \(x = c_x + L\cos\theta\) instead of sine. Which direction does 12 o’clock point now, and why?

Next: Project: Projectile Lab — parametric motion and the 45° max-range rule.