Chapter 55: Clock Intro
A clock with Canvas from the very beginning.
I’m going to teach this like your favorite patient teacher who wants you to really understand — not just copy-paste and move on. We’ll go slowly, explain why every line exists, show you the thinking process, and build a beautiful, live, analog clock together — step by step.
By the end of this lesson you’ll have:
- A complete, elegant analog clock that shows real time
- Clear understanding of how Canvas animation, rotation, arcs, text, and time math work together
- Code you can immediately run, modify, and be proud of
Canvas Clock – Complete Introduction & Build-Along Tutorial
Step 0: Why build a clock in Canvas?
A clock is the perfect first real project because it forces you to combine almost everything you’ve learned:
- Drawing circles & arcs (face, hands)
- Rotating things around a center point
- Updating every second (animation/time loop)
- Text positioning & alignment (numbers)
- Math with angles (hours → degrees)
- Keeping state clean (save/restore)
- Redrawing only what changes (hands) while keeping static parts
And the result looks impressive and useful — something you can actually show people.
Step 1: Basic HTML + Canvas structure
|
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 – Intro</title> <style> body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; background: #0d1117; /* dark background like GitHub */ font-family: Arial, sans-serif; } canvas { border: 4px solid #30363d; border-radius: 50%; box-shadow: 0 0 60px rgba(0,0,0,0.9); } </style> </head> <body> <canvas id="clock" width="420" height="420"></canvas> <script> // All JavaScript will go here </script> </body> </html> |
Why this structure?
- Square canvas (420×420) → perfect for round clock
- Dark theme + thick border + shadow → looks premium & modern
- border-radius: 50% → makes square canvas look circular
Step 2: Get context & define helpers
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
const canvas = document.getElementById('clock'); const ctx = canvas.getContext('2d'); // Center & radius helpers (used everywhere – good habit) const cx = canvas.width / 2; // 210 const cy = canvas.height / 2; // 210 const radius = canvas.width / 2 - 30; // leave space for thick border |
Step 3: Draw the static clock face (only once!)
We draw numbers, ticks, and background circle once — 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 75 76 77 78 |
function drawClockFace() { // Outer ring (metallic/dark) ctx.beginPath(); ctx.arc(cx, cy, radius + 25, 0, Math.PI * 2); ctx.fillStyle = '#161b22'; // dark gray ctx.fill(); ctx.strokeStyle = '#8b949e'; ctx.lineWidth = 12; ctx.stroke(); // Main face gradient (subtle 3D look) const faceGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius); faceGrad.addColorStop(0, '#22272e'); faceGrad.addColorStop(1, '#0d1117'); ctx.fillStyle = faceGrad; ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.fill(); // Hour ticks – thicker & longer for (let i = 0; i < 12; i++) { const angle = (i * Math.PI / 6) - Math.PI / 2; // 0° at top const inner = radius - 35; const outer = radius - 5; const x1 = cx + Math.cos(angle) * inner; const y1 = cy + Math.sin(angle) * inner; const x2 = cx + Math.cos(angle) * outer; const y2 = cy + Math.sin(angle) * outer; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#c9d1d9'; ctx.lineWidth = 8; ctx.lineCap = 'round'; ctx.stroke(); } // Minute ticks – thinner for (let i = 0; i < 60; i++) { if (i % 5 === 0) continue; // skip hour marks const angle = (i * Math.PI / 30) - Math.PI / 2; const inner = radius - 18; const outer = radius - 5; const x1 = cx + Math.cos(angle) * inner; const y1 = cy + Math.sin(angle) * inner; const x2 = cx + Math.cos(angle) * outer; const y2 = cy + Math.sin(angle) * outer; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = '#8b949e'; ctx.lineWidth = 3; ctx.stroke(); } // Numbers (12, 3, 6, 9 only – clean look) ctx.fillStyle = '#c9d1d9'; ctx.font = 'bold 40px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; [12, 3, 6, 9].forEach((num, i) => { const angle = (i * Math.PI / 2) - Math.PI / 2; const x = cx + Math.cos(angle) * (radius - 60); const y = cy + Math.sin(angle) * (radius - 60); ctx.fillText(num, x, y); }); } // Draw face once at start drawClockFace(); |
Step 4: Draw moving 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 57 |
function drawHands() { const now = new Date(); const seconds = now.getSeconds(); const minutes = now.getMinutes(); const hours = now.getHours() % 12; // Second hand (thin & red) ctx.save(); ctx.translate(cx, cy); ctx.rotate(seconds * Math.PI / 30 - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -15); ctx.lineTo(0, -radius + 50); ctx.strokeStyle = '#F44336'; ctx.lineWidth = 5; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Minute hand ctx.save(); ctx.translate(cx, cy); ctx.rotate((minutes + seconds/60) * Math.PI / 30 - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -15); ctx.lineTo(0, -radius + 80); ctx.strokeStyle = '#c9d1d9'; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Hour hand (thickest) ctx.save(); ctx.translate(cx, cy); ctx.rotate((hours + minutes/60) * Math.PI / 6 - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -15); ctx.lineTo(0, -radius + 140); ctx.strokeStyle = '#c9d1d9'; ctx.lineWidth = 16; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Center bolt / dot ctx.beginPath(); ctx.arc(cx, cy, 12, 0, Math.PI*2); ctx.fillStyle = '#8b949e'; ctx.fill(); } |
Step 5: Make it live (update every second)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Draw static face once drawClockFace(); // Update hands every second setInterval(drawHands, 1000); // First draw drawHands(); |
Full final code (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 165 |
<!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: #0d1117; } canvas { border: 4px solid #30363d; border-radius: 50%; box-shadow: 0 0 60px rgba(0,0,0,0.9); } </style> </head> <body> <canvas id="clock" width="420" height="420"></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 - 30; function drawClockFace() { // Outer ring ctx.beginPath(); ctx.arc(cx, cy, radius + 25, 0, Math.PI * 2); ctx.fillStyle = '#161b22'; ctx.fill(); ctx.strokeStyle = '#8b949e'; ctx.lineWidth = 12; ctx.stroke(); // Face gradient const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius); grad.addColorStop(0, '#22272e'); grad.addColorStop(1, '#0d1117'); 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 - 35); const y1 = cy + Math.sin(angle) * (radius - 35); 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 = '#c9d1d9'; ctx.lineWidth = 8; ctx.lineCap = 'round'; 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 - 18); const y1 = cy + Math.sin(angle) * (radius - 18); 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 = '#8b949e'; ctx.lineWidth = 3; ctx.stroke(); } // Numbers ctx.fillStyle = '#c9d1d9'; 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 - 60); const y = cy + Math.sin(angle) * (radius - 60); 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, -15); ctx.lineTo(0, -radius + 50); ctx.strokeStyle = '#F44336'; ctx.lineWidth = 5; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Minute hand ctx.save(); ctx.translate(cx, cy); ctx.rotate((min + sec/60) * Math.PI / 30 - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -15); ctx.lineTo(0, -radius + 80); ctx.strokeStyle = '#c9d1d9'; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Hour hand ctx.save(); ctx.translate(cx, cy); ctx.rotate((hr + min/60) * Math.PI / 6 - Math.PI / 2); ctx.beginPath(); ctx.moveTo(0, -15); ctx.lineTo(0, -radius + 140); ctx.strokeStyle = '#c9d1d9'; ctx.lineWidth = 16; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); // Center dot ctx.beginPath(); ctx.arc(cx, cy, 12, 0, Math.PI*2); ctx.fillStyle = '#c9d1d9'; ctx.fill(); } // Draw static face once drawClockFace(); // Update hands every second setInterval(drawHands, 1000); // Initial draw drawHands(); </script> </body> </html> |
Summary – What we learned / reused
- arc() for face & hands
- save() / restore() + translate + rotate for correct hand rotation
- Static face drawn once → hands redrawn every second (efficient)
- lineCap = ’round’ for smooth hand ends
- Real-time Date object + angle math
- Nice styling (gradients, shadows, numbers)
Your mini homework (try tonight)
- Add small second ticks (thin lines every 6°)
- Make 12, 3, 6, 9 numbers bigger & bolder
- Show current date below the clock (use fillText with new Date().toLocaleDateString())
Paste your modified version here if you want feedback — I’ll review it like we’re sitting together improving it.
Any part confusing?
- Rotating hands around center?
- Why we don’t clear the whole canvas every second?
- Angle math (why -Math.PI/2)?
- 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 something to be proud of! 🚀
