Canvas API: Vẽ đồng hồ kim động bằng HTML5 Canvas

Canvas API cho phép bạn vẽ đồ họa 2D ngay trong trình duyệt, từ hình cơ bản, biểu đồ, đến game mini. Bài này mình sẽ dựng một đồng hồ kim analog chạy thời gian thật bằng Canvas, có tuỳ chọn “kim giây mượt” và đổi kích thước trực tiếp.

Canvas API: Vẽ đồng hồ kim động bằng HTML5 Canvas

Ý tưởng & quy trình vẽ

  • Khung vẽ: dùng <canvas>, scale theo devicePixelRatio để nét căng trên màn hình Retina.
  • Mặt đồng hồ: vẽ vòng tròn, vạch phút/giờ, số 1–12 (tuỳ chọn).
  • Kim: tính góc kim giờ/phút/giây từ thời gian hiện tại; “kim giây mượt” dùng mili-giây để nội suy.
  • Animation: lặp vẽ bằng requestAnimationFrame để đạt 60FPS, nhẹ & mượt.
  • Responsive: đổi size bằng slider; canvas tự scale theo DPR để không bị mờ.

Mẹo hiệu năng khi dùng Canvas

  • Tránh style/layout reflow giữa mỗi frame; chỉ vẽ lên context 2D.
  • Gom các thao tác vẽ: dùng save()/restore(), translate() 1 lần để đơn giản hoá toạ độ.
  • Luôn scale theo devicePixelRatio để chữ & nét không “bể” trên các màn hình DPI cao.

Demo trực tiếp

Tuỳ chỉnh

300px

Canvas tự scale theodevicePixelRatiođể nét căng trên màn hình Retina.

Đồng hồ Canvas

JS Deep-Dive: hiểu và làm chủ Canvas Clock

1. Tọa độ, tỉ lệ DPI & vì sao phải scale theo devicePixelRatio

Canvas có kích thước hiển thị (CSS px) và kích thước vẽ thật (logical px). Màn hình Retina có devicePixelRatio > 1, nếu không scale thì nét sẽ mờ. Quy ước thực hành:

const dpr = Math.max(1, window.devicePixelRatio || 1);
canvas.style.width = cssSize + 'px';
canvas.style.height = cssSize + 'px';
canvas.width  = Math.round(cssSize * dpr);
canvas.height = Math.round(cssSize * dpr);

const ctx = canvas.getContext('2d');
ctx.setTransform(1,0,0,1,0,0);   // reset transform
ctx.scale(dpr, dpr);             // scale cho DPI cao
ctx.translate(cssSize/2, cssSize/2); // gốc tọa độ vào tâm

Đưa gốc vào tâm giúp vẽ tròn/kim tự nhiên hơn (quay quanh (0,0)).

2. Công thức góc cho kim

1 vòng tròn = . Kim giây/phút: chia 60; kim giờ: chia 12. Dùng mili-giây để làm “kim giây mượt”.

const ms = now.getMilliseconds();
const s  = now.getSeconds() + (smooth ? ms/1000 : 0);
const m  = now.getMinutes() + s/60;
const h  = (now.getHours() % 12) + m/60;

const angSec  = s * Math.PI/30;  // 60 division
const angMin  = m * Math.PI/30;
const angHour = h * Math.PI/6;   // 12 division

3. Vẽ vạch (ticks)

Cứ 5 vạch là vạch giờ (đậm & dài hơn). Xoay hệ trục để vẽ, đỡ phải tính tọa độ từng điểm.

function drawTicks(ctx, radius){
  for(let i=0; i<60; i++){
    const ang = i * Math.PI/30;
    ctx.rotate(ang);
    ctx.beginPath();
    const len = (i % 5 === 0) ? radius*0.12 : radius*0.06;
    ctx.moveTo(0, -radius);
    ctx.lineTo(0, -radius + len);
    ctx.lineWidth  = (i % 5 === 0) ? Math.max(2, radius*0.015)
                                   : Math.max(1, radius*0.006);
    ctx.strokeStyle = '#333';
    ctx.stroke();
    ctx.rotate(-ang);
  }
}

4. Vẽ kim

Quy tắc: rotate rồi vẽ một đoạn thẳng. Thứ tự vẽ: giờ → phút → giây.

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

5. Vòng lặp animation với requestAnimationFrame

Mỗi frame: clear → vẽ mặt số/vạch/số → tính thời gian → vẽ kim.

let animId;
function loop(){
  const now = new Date();
  drawFrame(now);           // chứa các lệnh vẽ 1 frame
  animId = requestAnimationFrame(loop);
}
loop();

// Khi đổi kích thước: cancelAnimationFrame(animId), resize, rồi loop() lại.

6. “Sweep second hand”

Muốn kim giây nhảy từng nấc: bỏ cộng ms/1000. Muốn mượt (sweep): giữ ms/1000. Tốn CPU không đáng kể với Canvas 2D.

7. Chống mờ khi thay đổi kích thước

Khi đổi slider size: tính lại DPR, canvas.width/height, translate, radius. Không chỉ phóng to bằng CSS.

8. Quản lý state vẽ

Dùng ctx.save()/ctx.restore() để tránh “vương” transform/style. Tránh thao tác DOM trong mỗi frame; gom UI ngoài vòng vẽ.

9. Tỉ lệ theo bán kính

Giữ tỷ lệ kích thước theo radius để đồng hồ đẹp ở mọi size.

const rim  = Math.max(2, radius*0.02);   // viền
const hW   = Math.max(5, radius*0.05);   // dày kim giờ
const font = Math.max(12, radius*0.16);  // cỡ chữ số

10. Checklist lỗi thường gặp

  • Mờ/nhòe: quên scale theo DPR và translate về tâm.
  • Kim lệch/đảo chiều: tính góc đúng nhưng trục chưa đúng; nhớ vẽ theo trục y âm sau khi dịch tâm.
  • Lag khi resize: quên cancelAnimationFrame trước khi init lại.
  • Nhấp nháy: thiếu clearRect hoặc quên save/restore state.

TL;DR (công thức cốt lõi)

// Góc kim
angSec  = (sec + (smooth ? ms/1000 : 0)) * Math.PI/30;
angMin  = (min + sec/60)                 * Math.PI/30;
angHour = ((hour % 12) + min/60)         * Math.PI/6;

// Trình tự vẽ 1 frame
clear();
drawFace(); drawTicks(); drawNumerals();
drawHand(angHour, r*0.50, r*0.05,  '#111');
drawHand(angMin,  r*0.73, r*0.035, '#111');
drawHand(angSec,  r*0.80, r*0.015, '#e53935');

Kết luận

Canvas rất hợp để vẽ những thành phần động, mượt và tùy biến cao như đồng hồ, biểu đồ realtime hay mini-games. Khi bạn nắm vững toạ độ, góc, và vòng đời vẽ với requestAnimationFrame, phần còn lại chỉ là sáng tạo.

Bình luận


  • Không có bình luận.

Init Toolbox

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...