Chapter 59: Canvas Clock Start
Lesson: Canvas Clock – Starting from Absolute Zero
Step 1 – The absolute bare-minimum canvas clock face (just a circle)
Create this file and open it in the browser:
|
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 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Canvas Clock – Step 1</title> <style> body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; background: #0d1117; } canvas { border: 8px solid #30363d; border-radius: 50%; box-shadow: 0 0 60px rgba(0,0,0,0.8); } </style> </head> <body> <canvas id="clock" width="420" height="420"></canvas> <script> const canvas = document.getElementById('clock'); const ctx = canvas.getContext('2d'); // Center helpers (you will use these in every clock) const cx = canvas.width / 2; // 210 const cy = canvas.height / 2; // 210 const radius = canvas.width / 2 - 40; // leave space for thick border // Draw the simplest possible clock face: just one big circle ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.fillStyle = '#161b22'; // very dark gray face ctx.fill(); ctx.strokeStyle = '#8b949e'; // silver/gray border ctx.lineWidth = 16; ctx.stroke(); // Temporary label so we know it's working ctx.fillStyle = '#c9d1d9'; ctx.font = 'bold 28px Arial'; ctx.textAlign = 'center'; ctx.fillText('Clock Face – Step 1', cx, cy + radius + 50); </script> </body> </html> |
What we have right now:
- A dark round clock face with a thick metallic-looking border
- Nothing else yet — no ticks, no numbers, no hands
- This is the foundation — everything else will be drawn on top of this circle
Why we do it this way:
- Square canvas + border-radius: 50% → looks perfectly circular
- Dark background + light border → modern premium watch style
- radius = width/2 – 40 → leaves space for the thick border so it doesn’t get cut off
Step 2 – Add hour & minute ticks (the skeleton)
Add this function right after the previous code and call it:
|
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 |
function addTicks() { // Hour ticks – longer & thicker (every 30°) for (let i = 0; i < 12; i++) { const angle = (i * Math.PI / 6) - Math.PI / 2; // 0° at top (12 o'clock) const inner = radius - 40; const outer = radius - 8; 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'; // light gray-white ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.stroke(); } // Minute ticks – shorter & thinner (every 6°) for (let i = 0; i < 60; i++) { if (i % 5 === 0) continue; // skip the hour ticks we already drew const angle = (i * Math.PI / 30) - Math.PI / 2; const inner = radius - 20; const outer = radius - 8; 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 = 4; ctx.stroke(); } } // Call it after drawing the face addTicks(); |
Why this works:
- Math.PI / 6 = 30° per hour
- Math.PI / 30 = 6° per minute
- – Math.PI / 2 shifts 0° from right → top (12 o’clock position)
- lineCap = ’round’ makes ticks look smooth and professional
Step 4 – Add numbers (only 12, 3, 6, 9 – modern & clean)
Add this right after addTicks():
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
ctx.fillStyle = '#c9d1d9'; ctx.font = 'bold 48px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const positions = [12, 3, 6, 9]; positions.forEach((num, i) => { const angle = (i * Math.PI / 2) - Math.PI / 2; const x = cx + Math.cos(angle) * (radius - 70); const y = cy + Math.sin(angle) * (radius - 70); ctx.fillText(num, x, y); }); |
Why only 12, 3, 6, 9? Modern watches and minimalist designs often do this — it looks cleaner and less crowded.
Step 5 – Center dot (preparation for hands)
|
0 1 2 3 4 5 6 7 8 9 |
ctx.beginPath(); ctx.arc(cx, cy, 14, 0, Math.PI * 2); ctx.fillStyle = '#c9d1d9'; ctx.fill(); |
This will become the center bolt that holds the hands.
Full code so far – just the face (no hands yet)
|
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 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Canvas Clock Face</title> <style> body { margin:0; height:100vh; display:flex; justify-content:center; align-items:center; background:#0d1117; } canvas { border:6px solid #30363d; border-radius:50%; box-shadow:0 0 80px 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 - 40; // 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 – only 12, 3, 6, 9 ctx.fillStyle = '#c9d1d9'; ctx.font = 'bold 48px 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 - 70); const y = cy + Math.sin(angle) * (radius - 70); ctx.fillText(n, x, y); }); // Center dot (will hold hands later) ctx.beginPath(); ctx.arc(cx, cy, 14, 0, Math.PI*2); ctx.fillStyle = '#c9d1d9'; ctx.fill(); </script> </body> </html> |
Homework / next-step challenge
- Make the numbers bigger for 12, 3, 6, 9 (try 64px font)
- Add small dots instead of minute lines (use arc with tiny radius)
- Change the face to a lighter silver/gray theme
- (Bonus) Try Roman numerals (XII, III, VI, IX) instead of Arabic numbers
Paste your version here when you’re done — I’ll give feedback like we’re sitting together improving it.
Any part confusing?
- Why subtract Math.PI / 2 from the angle?
- Why radius – 70 for number position?
- Why textBaseline = ‘middle’ for numbers?
- Something else?
Tell me — we’ll zoom in until the clock face feels 100% clear to you.
Next lesson we’ll add the moving hands — it’s going to look amazing! 🚀
