This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Toán học là nền tảng không thể thiếu khi làm việc với Canvas — từ animation, physics đến game development. Bài này trang bị cho bạn kiến thức lượng giác, vector 2D và ma trận biến đổi cần thiết cho toàn bộ series.
1. Lượng giác cơ bản
JavaScript sử dụng radians, không phải degrees. Lượng giác là chìa khóa cho chuyển động tròn, sóng và xoay.
// Degrees ↔ Radians
const toRadians = (deg) => deg * Math.PI / 180;
const toDegrees = (rad) => rad * 180 / Math.PI;
// Chuyển động tròn: x = cx + r * cos(angle), y = cy + r * sin(angle)
function drawCircularMotion(ctx, cx, cy, radius, angle) {
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.fillStyle = '#e11d48';
ctx.fill();
}
// Vẽ sóng sine
function drawSineWave(ctx, offsetY, amplitude, frequency, phase) {
ctx.beginPath();
for (let x = 0; x < canvas.width; x++) {
const y = offsetY + amplitude * Math.sin(frequency * x + phase);
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
ctx.stroke();
}
// Vẽ spiral (xoắn ốc)
function drawSpiral(ctx, cx, cy, maxRadius, turns) {
ctx.beginPath();
for (let angle = 0; angle < turns * Math.PI * 2; angle += 0.05) {
const r = (angle / (turns * Math.PI * 2)) * maxRadius;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
if (angle === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = '#22c55e';
ctx.lineWidth = 2;
ctx.stroke();
}
2. Math.atan2 — Góc giữa 2 điểm
atan2(dy, dx) trả về góc (radians) từ trục x dương đến điểm (dx, dy). Rất hữu ích để xoay object hướng về phía target.
// Tính góc từ point A đến point B
function angleBetween(ax, ay, bx, by) {
return Math.atan2(by - ay, bx - ax);
}
// Khoảng cách Euclidean
function distance(ax, ay, bx, by) {
return Math.hypot(bx - ax, by - ay);
}
// Ứng dụng: Mũi tên xoay hướng về chuột
let mouseX = 0, mouseY = 0;
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
function drawArrow(ctx) {
const cx = 300, cy = 200; // vị trí mũi tên
const angle = Math.atan2(mouseY - cy, mouseX - cx);
const dist = Math.hypot(mouseX - cx, mouseY - cy);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
// Vẽ mũi tên
ctx.beginPath();
ctx.moveTo(30, 0);
ctx.lineTo(-15, -12);
ctx.lineTo(-5, 0);
ctx.lineTo(-15, 12);
ctx.closePath();
ctx.fillStyle = '#e11d48';
ctx.fill();
ctx.restore();
// Hiển thị thông tin
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.fillText(`Angle: ${toDegrees(angle).toFixed(1)}°`, 10, 20);
ctx.fillText(`Distance: ${dist.toFixed(1)}px`, 10, 40);
}
3. Vector 2D
Vector là khái niệm cốt lõi trong game/animation — biểu diễn hướng, vận tốc, lực. Tạo class Vec2 với các phép toán cơ bản.
class Vec2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
// Phép cộng
add(v) { return new Vec2(this.x + v.x, this.y + v.y); }
// Phép trừ
sub(v) { return new Vec2(this.x - v.x, this.y - v.y); }
// Nhân scalar
mul(s) { return new Vec2(this.x * s, this.y * s); }
// Độ dài (magnitude)
mag() { return Math.hypot(this.x, this.y); }
// Normalize (vector đơn vị, độ dài = 1)
normalize() {
const m = this.mag();
return m > 0 ? new Vec2(this.x / m, this.y / m) : new Vec2();
}
// Dot product — hữu ích cho projection, angle
dot(v) { return this.x * v.x + this.y * v.y; }
// Cross product 2D — trả về scalar (z component)
cross(v) { return this.x * v.y - this.y * v.x; }
// Giới hạn độ dài
limit(max) {
if (this.mag() > max) return this.normalize().mul(max);
return new Vec2(this.x, this.y);
}
// Góc của vector
angle() { return Math.atan2(this.y, this.x); }
// Tạo vector từ góc
static fromAngle(angle, length = 1) {
return new Vec2(Math.cos(angle) * length, Math.sin(angle) * length);
}
// Khoảng cách đến vector khác
dist(v) { return this.sub(v).mag(); }
}
// Sử dụng
const pos = new Vec2(100, 200);
const vel = new Vec2(3, -2);
const newPos = pos.add(vel); // Vec2(103, 198)
const direction = vel.normalize(); // vector đơn vị cùng hướng
const speed = vel.mag(); // 3.606
4. Hệ tọa độ Polar vs Cartesian
Hệ tọa độ Polar (r, θ) rất hữu ích khi vẽ hình tròn, hoa hồng, spirograph. Chuyển đổi qua lại với Cartesian (x, y) dễ dàng.
// Polar → Cartesian
function polarToCart(r, theta) {
return { x: r * Math.cos(theta), y: r * Math.sin(theta) };
}
// Cartesian → Polar
function cartToPolar(x, y) {
return { r: Math.hypot(x, y), theta: Math.atan2(y, x) };
}
// Vẽ hoa hồng (Rose curve): r = a * cos(k * θ)
function drawRose(ctx, cx, cy, a, k) {
ctx.beginPath();
for (let theta = 0; theta < Math.PI * 2 * (k % 1 === 0 ? 1 : 2); theta += 0.01) {
const r = a * Math.cos(k * theta);
const x = cx + r * Math.cos(theta);
const y = cy + r * Math.sin(theta);
if (theta === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.strokeStyle = '#e11d48';
ctx.lineWidth = 2;
ctx.stroke();
}
drawRose(ctx, 200, 200, 150, 5); // hoa 5 cánh
// Spirograph: x = R*cos(t) + r*cos(t*R/r), y = R*sin(t) + r*sin(t*R/r)
function drawSpirograph(ctx, cx, cy, R, r, d) {
ctx.beginPath();
const diff = R - r;
for (let t = 0; t < Math.PI * 2 * r / gcd(R, r); t += 0.01) {
const x = cx + diff * Math.cos(t) + d * Math.cos(diff * t / r);
const y = cy + diff * Math.sin(t) - d * Math.sin(diff * t / r);
if (t === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = '#8b5cf6';
ctx.lineWidth = 1.5;
ctx.stroke();
}
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
drawSpirograph(ctx, 400, 200, 120, 45, 80);
5. Ma trận biến đổi 2D
Ma trận 2×2 (hoặc 3×3 cho affine) cho phép biểu diễn rotation, scale, skew trong một phép nhân. Đây là nền tảng toán học đằng sau setTransform() của Canvas.
// Rotation matrix:
// | cos θ -sin θ | | x | | x*cosθ - y*sinθ |
// | sin θ cos θ | × | y | = | x*sinθ + y*cosθ |
function rotatePoint(x, y, angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return {
x: x * cos - y * sin,
y: x * sin + y * cos
};
}
// Scale matrix:
// | sx 0 | | x | | x*sx |
// | 0 sy | × | y | = | y*sy |
function scalePoint(x, y, sx, sy) {
return { x: x * sx, y: y * sy };
}
// Nhân 2 matrix 3x3 (cho affine transforms)
function multiplyMatrix(a, b) {
return [
a[0]*b[0] + a[1]*b[3] + a[2]*b[6],
a[0]*b[1] + a[1]*b[4] + a[2]*b[7],
a[0]*b[2] + a[1]*b[5] + a[2]*b[8],
a[3]*b[0] + a[4]*b[3] + a[5]*b[6],
a[3]*b[1] + a[4]*b[4] + a[5]*b[7],
a[3]*b[2] + a[4]*b[5] + a[5]*b[8],
a[6]*b[0] + a[7]*b[3] + a[8]*b[6],
a[6]*b[1] + a[7]*b[4] + a[8]*b[7],
a[6]*b[2] + a[7]*b[5] + a[8]*b[8]
];
}
// Liên hệ với Canvas setTransform(a, b, c, d, e, f):
// | a c e | a = scaleX, b = skewY
// | b d f | c = skewX, d = scaleY
// | 0 0 1 | e = translateX, f = translateY
// Ví dụ: xoay hình vuông quanh tâm bằng matrix thủ công
function drawRotatedRect(ctx, cx, cy, w, h, angle) {
const hw = w / 2, hh = h / 2;
const corners = [
[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]
];
ctx.beginPath();
corners.forEach(([x, y], i) => {
const rotated = rotatePoint(x, y, angle);
const px = cx + rotated.x;
const py = cy + rotated.y;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
});
ctx.closePath();
ctx.fillStyle = '#f59e0b';
ctx.fill();
}
// Tham khảo:
// - The Nature of Code (natureofcode.com) — Daniel Shiffman
// - Math for Game Developers (YouTube playlist)
6. Câu hỏi trắc nghiệm ôn tập
Trắc nghiệm 1: Radians conversion
90 degrees bằng bao nhiêu radians?
Trắc nghiệm 2: atan2
Math.atan2(0, -1) trả về giá trị nào?
Trắc nghiệm 3: Vector normalize
Vector (3, 4) sau khi normalize có độ dài bao nhiêu?
Tải file code thực hành minh họa bài học
File tổng hợp các pattern lượng giác, vector 2D và ma trận biến đổi:
Tải về canvas_math.js
Comments
Bình luận