Nguyên lý zoom canvas tại vị trí con trỏ
Mặc định canvas scale từ gốc tọa độ (0,0). Để zoom tại vị trí chuột, cần dịch chuyển gốc tọa độ (translate) đến vị trí chuột, scale, sau đó dịch ngược lại.
Công thức toán học
Thứ tự biến đổi quan trọng: Translate → Scale → Translate ngược
ctx.translate(mouseX, mouseY); // Dịch gốc về vị trí chuột ctx.scale(scaleFactor, scaleFactor); // Phóng to/thu nhỏ ctx.translate(-mouseX, -mouseY); // Dịch ngược về vị trí ban đầu
Ví dụ hoàn chỉnh
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="800" height="600" style="border:1px solid #ccc;"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
let scale = 1;
const minScale = 0.1;
const maxScale = 10;
const scaleStep = 0.1;
// Vẽ nội dung mẫu
function drawContent() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(offsetX, offsetY);
ctx.scale(scale, scale);
// Vẽ lưới tham chiếu
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
for(let i = 0; i < 100; i++) {
ctx.beginPath();
ctx.moveTo(i * 50, 0);
ctx.lineTo(i * 50, 5000);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * 50);
ctx.lineTo(5000, i * 50);
ctx.stroke();
}
// Vẽ hình mẫu
ctx.fillStyle = '#3498db';
ctx.fillRect(100, 100, 200, 150);
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.arc(500, 300, 80, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
let offsetX = 0, offsetY = 0;
let isPanning = false;
let lastX = 0, lastY = 0;
// Xử lý zoom bằng cuộn chuột
canvas.addEventListener('wheel', function(e) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Tính toán offset mới để giữ vị trí chuột cố định
const newScale = Math.max(minScale, Math.min(maxScale, scale - e.deltaY * 0.001 * scale));
const scaleRatio = newScale / scale;
offsetX = mouseX - (mouseX - offsetX) * scaleRatio;
offsetY = mouseY - (mouseY - offsetY) * scaleRatio;
scale = newScale;
drawContent();
}, { passive: false });
// Xử lý pan (kéo thả)
canvas.addEventListener('mousedown', function(e) {
isPanning = true;
lastX = e.clientX;
lastY = e.clientY;
canvas.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', function(e) {
if(!isPanning) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
offsetX += dx;
offsetY += dy;
lastX = e.clientX;
lastY = e.clientY;
drawContent();
});
window.addEventListener('mouseup', function() {
isPanning = false;
canvas.style.cursor = 'grab';
});
// Khởi tạo
canvas.style.cursor = 'grab';
drawContent();
</script>
</body>
</html>
Giải thích kỹ thuật
Ma trận biến đổi (Transformation Matrix)
Mỗi lần gọi translate() và scale() sẽ nhân ma trận biến đổi hiện tại. Thứ tự thực hiện ảnh hưởng trực tiếp đến kết quả.
Cách tính offset khi zoom
Để giữ điểm dưới con trỏ chuột không di chuyển trên màn hình:
newOffset = mousePos - (mousePos - oldOffset) * (newScale / oldScale)
Mở rộng: Sử dụng thư viện
Để dự án phức tạp, cân nhắc các thư viện:
- Fabric.js - Hỗ trợ zoom/pan built-in
- Konva.js - Effectiveness cao cho canvas 2D
- PanZoom - Thư viện nhẹ chuyên biệt cho zoom/pan