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.

trigonometry.js
// 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();
}
📐 Demo tương tác: Vòng tròn đơn vị & sóng sin

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.

atan2_usage.js
// 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.

vec2_class.js
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
📐 Demo tương tác: Vector 2D theo chuột

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_coordinates.js
// 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.

matrix_math.js
// 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

Related Articles

Bài viết liên quan trong series

Lesson 7: Animation Loop: requestAnimationFrame & Delta Time Bài 7: Animation Loop — requestAnimationFrame & Delta Time Lesson 5: Pixel Manipulation: Image Filters & Color Picker Bài 5: Pixel Manipulation: Image Filters & Color Picker Back to Canvas Series Overview Quay lại Lộ trình Canvas Series