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>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>Exercise: Graphics Canvas Clock Project
Why should an hour hand's angle include a small contribution from the current minutes?