Chapter 54: Canvas Clock
A live analog clock using Canvas.
I’m going to teach this the way a patient, enthusiastic teacher would — slowly, clearly, step by step, explaining why we do every single thing, with complete runnable code, visual thinking, and comments so you understand every line.
We will build a classic analog clock that:
- Shows hour, minute, second hands
- Updates every second
- Looks nice (with shadows, gradients, numbers)
- Is fully self-contained in one HTML file
Canvas Analog Clock – Complete Step-by-Step Tutorial
Step 1: Basic HTML + Canvas setup
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Canvas Analog Clock</title> <style> body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; background: #111; font-family: Arial, sans-serif; } canvas { border: 2px solid #444; border-radius: 50%; box-shadow: 0 0 40px rgba(0,0,0,0.8); } </style> </head> <body> <canvas id="clock" width="400" height="400"></canvas> <script> // All JavaScript will go here </script> </body> </html> |
Why this setup?
- Square canvas (400×400) → perfect for round clock
- Dark background + shadow → clock looks like it’s floating
- border-radius: 50% → makes canvas look circular even though it’s square
Step 2: Get context & helper functions
Add this inside <script>:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
const canvas = document.getElementById('clock'); const ctx = canvas.getContext('2d'); // Center helpers (used everywhere) const centerX = canvas.width / 2; const centerY = canvas.height / 2; const radius = canvas.width / 2 - 20; // leave space for border & shadow |
Step 3: Draw the clock face (once – not in loop)
We only need to draw numbers, ticks, and background circle once (or when resizing), not every second.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
function drawClockFace() { // Outer circle (metallic look) ctx.beginPath(); ctx.arc(centerX, centerY, radius + 15, 0, Math.PI * 2); ctx.fillStyle = '#222'; ctx.fill(); ctx.strokeStyle = '#888'; ctx.lineWidth = 10; ctx.stroke(); // Inner background (soft gradient) const grad = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); grad.addColorStop(0, '#444'); grad.addColorStop(1, '#111'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.fill(); // Hour ticks (bigger & thicker) for (let i = 0; i < 12; i++) { const angle = (i * Math.PI / 6) - Math.PI / 2; // 0 = top const x1 = centerX + Math.cos(angle) * (radius - 30); const y1 = centerY + Math.sin(angle) * (radius - 30); const x2 = centerX + Math.cos(angle) * radius; const y2 = centerY + Math.sin(angle) * radius; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 8; ctx.stroke(); } // Minute ticks (thinner) for (let i = 0; i < 60; i++) { if (i % 5 === 0) continue; // skip hour ticks const angle = (i * Math.PI / 30) - Math.PI / 2; const x1 = centerX + Math.cos(angle) * (radius - 15); const y1 = centerY + Math.sin(angle) * (radius - 15); const x2 = centerX + Math.cos(angle) * radius; const y2 = centerY + Math.sin(angle) * radius; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#aaa'; ctx.lineWidth = 3; ctx.stroke(); } // Numbers (12, 3, 6, 9 only for simplicity) ctx.fillStyle = '#fff'; ctx.font = 'bold 36px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const numbers = [12, 3, 6, 9]; numbers.forEach((num, i) => { const angle = (i * Math.PI / 2) - Math.PI / 2; const x = centerX + Math.cos(angle) * (radius - 50); const y = centerY + Math.sin(angle) * (radius - 50); ctx.fillText(num, x, y); }); } // Draw face once at start drawClockFace(); |
Step 4: Draw hands (updated every second)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
function drawHands() { const now = new Date(); const seconds = now.getSeconds(); const minutes = now.getMinutes(); const hours = now.getHours() % 12; // Second hand ctx.save(); ctx.translate(centerX, centerY); ctx.rotate((seconds * Math.PI / 30) - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(0, -radius + 40); ctx.strokeStyle = '#F44336'; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Minute hand ctx.save(); ctx.translate(centerX, centerY); ctx.rotate((minutes * Math.PI / 30 + seconds * Math.PI / 1800) - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(0, -radius + 80); ctx.strokeStyle = '#fff'; ctx.lineWidth = 8; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Hour hand ctx.save(); ctx.translate(centerX, centerY); ctx.rotate((hours * Math.PI / 6 + minutes * Math.PI / 360) - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(0, -radius + 140); ctx.strokeStyle = '#fff'; ctx.lineWidth = 14; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Center dot ctx.beginPath(); ctx.arc(centerX, centerY, 10, 0, Math.PI*2); ctx.fillStyle = '#fff'; ctx.fill(); } |
Step 5: Animation loop (update every second)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function updateClock() { // We don't clear the whole canvas — we only redraw hands // Face is already drawn once drawHands(); } // Update every second setInterval(updateClock, 1000); // First draw updateClock(); |
Full final code (put everything together)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Canvas Analog Clock</title> <style> body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; background: #111; } canvas { border: 2px solid #444; border-radius: 50%; box-shadow: 0 0 40px rgba(0,0,0,0.8); } </style> </head> <body> <canvas id="clock" width="400" height="400"></canvas> <script> const canvas = document.getElementById('clock'); const ctx = canvas.getContext('2d'); const cx = canvas.width / 2; const cy = canvas.height / 2; const radius = canvas.width / 2 - 20; function drawClockFace() { // Outer ring ctx.beginPath(); ctx.arc(cx, cy, radius + 15, 0, Math.PI * 2); ctx.fillStyle = '#222'; ctx.fill(); ctx.strokeStyle = '#888'; ctx.lineWidth = 10; ctx.stroke(); // Inner gradient const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius); grad.addColorStop(0, '#444'); grad.addColorStop(1, '#111'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.fill(); // Hour ticks for (let i = 0; i < 12; i++) { const angle = (i * Math.PI / 6) - Math.PI / 2; const x1 = cx + Math.cos(angle) * (radius - 30); const y1 = cy + Math.sin(angle) * (radius - 30); const x2 = cx + Math.cos(angle) * radius; const y2 = cy + Math.sin(angle) * radius; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 8; ctx.stroke(); } // Minute ticks for (let i = 0; i < 60; i++) { if (i % 5 === 0) continue; const angle = (i * Math.PI / 30) - Math.PI / 2; const x1 = cx + Math.cos(angle) * (radius - 15); const y1 = cy + Math.sin(angle) * (radius - 15); const x2 = cx + Math.cos(angle) * radius; const y2 = cy + Math.sin(angle) * radius; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#aaa'; ctx.lineWidth = 3; ctx.stroke(); } // Numbers ctx.fillStyle = '#fff'; ctx.font = 'bold 36px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; [12,3,6,9].forEach((n, i) => { const angle = (i * Math.PI / 2) - Math.PI / 2; const x = cx + Math.cos(angle) * (radius - 50); const y = cy + Math.sin(angle) * (radius - 50); ctx.fillText(n, x, y); }); } function drawHands() { const now = new Date(); const sec = now.getSeconds(); const min = now.getMinutes(); const hr = now.getHours() % 12; // Second hand ctx.save(); ctx.translate(cx, cy); ctx.rotate((sec * Math.PI / 30) - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(0, -radius + 40); ctx.strokeStyle = '#F44336'; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Minute hand ctx.save(); ctx.translate(cx, cy); ctx.rotate((min * Math.PI / 30 + sec * Math.PI / 1800) - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(0, -radius + 80); ctx.strokeStyle = '#fff'; ctx.lineWidth = 8; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Hour hand ctx.save(); ctx.translate(cx, cy); ctx.rotate((hr * Math.PI / 6 + min * Math.PI / 360) - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(0, -radius + 140); ctx.strokeStyle = '#fff'; ctx.lineWidth = 14; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Center dot ctx.beginPath(); ctx.arc(cx, cy, 10, 0, Math.PI*2); ctx.fillStyle = '#fff'; ctx.fill(); } // Draw static face once drawClockFace(); // Update hands every second setInterval(drawHands, 1000); // First draw drawHands(); </script> </body> </html> |
Summary – What we learned / reused in this project
- Full canvas setup with centered clock
- arc() for face & hands
- save() / restore() + translate + rotate for hand rotation around center
- strokeStyle, lineWidth, lineCap for nice hand look
- setInterval for time update (simple & reliable)
- Static face drawn once → hands redrawn every second (efficient)
Your mini homework (try tonight)
- Add small second ticks (thin lines every 6°) around the clock face
- Make the hour numbers bigger & bolder for 12, 3, 6, 9
- Add a date display below the clock using fillText (e.g. “March 03, 2026”)
Paste your modified code here if you want feedback — I’ll review it like we’re pair-programming together 😄
Any part confusing?
- Rotating hands around center?
- Why we don’t clear the whole canvas every second?
- Angles in radians?
- Something else?
Tell me — we’ll zoom in until the clock feels perfect to you.
You just built a real, live analog clock in pure Canvas — that’s huge! 🚀
