- Ý tưởng & quy trình vẽ
- Mẹo hiệu năng khi dùng Canvas
- Demo trực tiếp
- Tuỳ chỉnh
- Đồ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
- 2. Công thức góc cho kim
- 3. Vẽ vạch (ticks)
- 4. Vẽ kim
- 5. Vòng lặp animation với requestAnimationFrame
- 6. “Sweep second hand”
- 7. Chống mờ khi thay đổi kích thước
- 8. Quản lý state vẽ
- 9. Tỉ lệ theo bán kính
- 10. Checklist lỗi thường gặp
- TL;DR (công thức cốt lõi)
- Kết luận
Ý tưởng & quy trình vẽ
- Khung vẽ: dùng
<canvas>, scale theodevicePixelRatiođể 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
Đồ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 = 2π. 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à
translatevề 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
cancelAnimationFrametrước khi init lại. - Nhấp nháy: thiếu
clearRecthoặc quênsave/restorestate.
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