Graphics Animating the Clock in Real Time

This lesson brings the clock to life by clearing and redrawing the whole face every frame, comparing setInterval and requestAnimationFrame as the two ways to drive that loop.

Why We Redraw Instead of Erasing

Canvas has no memory of the shapes it drew, once a line is painted it is just colored pixels, not an object you can select and delete. There is no addressable second hand to erase and move, there is only a bitmap. So the standard technique for any canvas animation, clocks included, is to clear the drawing surface completely and redraw everything, face, ticks, and all three hands, from scratch on every frame at the hands' new angles.

  • Clear the canvas (or at least the area the clock occupies)
  • Redraw the face and tick marks
  • Recompute the current hour, minute, and second angles
  • Redraw the hour, minute, and second hands at those angles
  • Repeat on a timer or animation loop

Example

<!DOCTYPE html>
<html>
<head>
</head>
<body>

<canvas id="clock" width="300" height="300"></canvas>

<script>
function drawClock() {
  ctx.save();
  ctx.clearRect(-radius, -radius, canvas.width, canvas.height);

  drawFace(ctx, clockRadius);
  drawTicks(ctx, clockRadius);

  const { hourAngle, minuteAngle, secondAngle } = getHandAngles();
  drawHand(ctx, hourAngle, clockRadius * 0.5, clockRadius * 0.07);
  drawHand(ctx, minuteAngle, clockRadius * 0.8, clockRadius * 0.05);

  ctx.strokeStyle = '#d9534f';
  drawHand(ctx, secondAngle, clockRadius * 0.9, clockRadius * 0.02);

  ctx.restore();
}
</script>

</body>
</html>

clearRect() takes coordinates in the current coordinate system, not raw pixel coordinates. Because the context was translated to the clock's center back when the canvas was first set up, (0, 0) here is the center of the dial, not its top-left corner. That is why the clear uses ctx.clearRect(-radius, -radius, canvas.width, canvas.height) instead of starting at (0, 0), a plain clearRect(0, 0, canvas.width, canvas.height) after a translate would only wipe the bottom-right quarter of the clock, leaving a permanent smear of old hand positions in the rest of the dial.

Example

<!DOCTYPE html>
<html>
<head>
</head>
<body>

<canvas id="clock" width="300" height="300"></canvas>

<script>
const canvas = document.getElementById('clock');
const ctx = canvas.getContext('2d');

const radius = canvas.height / 2;
ctx.translate(radius, radius);
const clockRadius = radius * 0.9;

function drawFace(ctx, radius) {
  ctx.beginPath();
  ctx.arc(0, 0, radius, 0, 2 * Math.PI);
  ctx.fillStyle = '#f5f5f5';
  ctx.fill();
  ctx.lineWidth = radius * 0.04;
  ctx.strokeStyle = '#333333';
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(0, 0, radius * 0.05, 0, 2 * Math.PI);
  ctx.fillStyle = '#333333';
  ctx.fill();
}

function drawTicks(ctx, radius) {
  for (let i = 0; i < 12; i++) {
    const angle = i * (Math.PI / 6); // 360 degrees / 12 hours
    ctx.save();
    ctx.rotate(angle);
    ctx.beginPath();
    ctx.lineWidth = radius * 0.03;
    ctx.moveTo(0, -radius * 0.9);
    ctx.lineTo(0, -radius * 0.85);
    ctx.stroke();
    ctx.restore();
  }
}

function getHandAngles() {
  const now = new Date();
  const hours = now.getHours() % 12;
  const minutes = now.getMinutes();
  const seconds = now.getSeconds();

  const secondAngle = seconds * (Math.PI / 30);
  const minuteAngle = minutes * (Math.PI / 30) + secondAngle / 60;
  const hourAngle = hours * (Math.PI / 6) + minuteAngle / 12;

  return { hourAngle, minuteAngle, secondAngle };
}

function drawHand(ctx, angle, length, width) {
  ctx.save();
  ctx.rotate(angle);
  ctx.beginPath();
  ctx.lineWidth = width;
  ctx.lineCap = 'round';
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -length);
  ctx.stroke();
  ctx.restore();
}

function drawClock() {
  ctx.save();
  ctx.clearRect(-radius, -radius, canvas.width, canvas.height);

  drawFace(ctx, clockRadius);
  drawTicks(ctx, clockRadius);

  const { hourAngle, minuteAngle, secondAngle } = getHandAngles();
  drawHand(ctx, hourAngle, clockRadius * 0.5, clockRadius * 0.07);
  drawHand(ctx, minuteAngle, clockRadius * 0.8, clockRadius * 0.05);

  ctx.strokeStyle = '#d9534f';
  drawHand(ctx, secondAngle, clockRadius * 0.9, clockRadius * 0.02);

  ctx.restore();
}

drawClock();
setInterval(drawClock, 1000);
</script>

</body>
</html>
Note: Call drawClock() once immediately, then start the interval. If you only call setInterval(drawClock, 1000), the canvas stays blank for the first second before the first tick fires.

Smoother Motion with requestAnimationFrame

setInterval(drawClock, 1000) is perfectly fine for a clock whose second hand jumps once per second, and it is easy on the CPU and battery since it only runs 60 times a minute. But if you want the second hand to sweep continuously rather than tick, you need to redraw far more often than once a second, using fractional seconds computed from the current milliseconds. That is a job for requestAnimationFrame, which asks the browser to call your function right before the next repaint, typically 60 times a second.

Example

<!DOCTYPE html>
<html>
<head>
</head>
<body>

<canvas id="clock" width="300" height="300"></canvas>

<script>
const canvas = document.getElementById('clock');
const ctx = canvas.getContext('2d');

const radius = canvas.height / 2;
ctx.translate(radius, radius);
const clockRadius = radius * 0.9;

function drawFace(ctx, radius) {
  ctx.beginPath();
  ctx.arc(0, 0, radius, 0, 2 * Math.PI);
  ctx.fillStyle = '#f5f5f5';
  ctx.fill();
  ctx.lineWidth = radius * 0.04;
  ctx.strokeStyle = '#333333';
  ctx.stroke();

  ctx.beginPath();
  ctx.arc(0, 0, radius * 0.05, 0, 2 * Math.PI);
  ctx.fillStyle = '#333333';
  ctx.fill();
}

function drawTicks(ctx, radius) {
  for (let i = 0; i < 12; i++) {
    const angle = i * (Math.PI / 6); // 360 degrees / 12 hours
    ctx.save();
    ctx.rotate(angle);
    ctx.beginPath();
    ctx.lineWidth = radius * 0.03;
    ctx.moveTo(0, -radius * 0.9);
    ctx.lineTo(0, -radius * 0.85);
    ctx.stroke();
    ctx.restore();
  }
}

function getHandAngles() {
  const now = new Date();
  const hours = now.getHours() % 12;
  const minutes = now.getMinutes();
  const seconds = now.getSeconds();

  const secondAngle = seconds * (Math.PI / 30);
  const minuteAngle = minutes * (Math.PI / 30) + secondAngle / 60;
  const hourAngle = hours * (Math.PI / 6) + minuteAngle / 12;

  return { hourAngle, minuteAngle, secondAngle };
}

function drawHand(ctx, angle, length, width) {
  ctx.save();
  ctx.rotate(angle);
  ctx.beginPath();
  ctx.lineWidth = width;
  ctx.lineCap = 'round';
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -length);
  ctx.stroke();
  ctx.restore();
}

function drawClock() {
  ctx.save();
  ctx.clearRect(-radius, -radius, canvas.width, canvas.height);

  drawFace(ctx, clockRadius);
  drawTicks(ctx, clockRadius);

  const { hourAngle, minuteAngle, secondAngle } = getHandAngles();
  drawHand(ctx, hourAngle, clockRadius * 0.5, clockRadius * 0.07);
  drawHand(ctx, minuteAngle, clockRadius * 0.8, clockRadius * 0.05);

  ctx.strokeStyle = '#d9534f';
  drawHand(ctx, secondAngle, clockRadius * 0.9, clockRadius * 0.02);

  ctx.restore();
}

function getSmoothAngles() {
  const now = new Date();
  const ms = now.getMilliseconds();
  const seconds = now.getSeconds() + ms / 1000;
  const minutes = now.getMinutes() + seconds / 60;
  const hours = (now.getHours() % 12) + minutes / 60;

  return {
    secondAngle: seconds * (Math.PI / 30),
    minuteAngle: minutes * (Math.PI / 30),
    hourAngle: hours * (Math.PI / 6)
  };
}

function animate() {
  drawClock(getSmoothAngles());
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>

</body>
</html>
AspectsetInterval(fn, 1000)requestAnimationFrame(fn)
Fires roughlyOnce per second, on your scheduleOnce per display refresh, about 60 times/sec
Best forA ticking second handA smoothly sweeping second hand
Behavior in background tabsKeeps firing (browsers may throttle it)Automatically pauses, saving battery
CPU/battery costLowHigher, since it redraws far more often
Note: For a normal ticking clock widget, setInterval(drawClock, 1000) is the right choice, it matches how often the display actually needs to change and keeps running even in situations where an animation loop would otherwise be paused. Reach for requestAnimationFrame specifically when you want the visibly smooth sweep, and accept the extra redraw cost that comes with it.

Exercise: Graphics Canvas Clock Project

Why should an hour hand's angle include a small contribution from the current minutes?