This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Compositing (cách các pixel mới được "trộn" vào các pixel đã có sẵn trên canvas) là một trong những phần mạnh mẽ nhưng ít được khai thác nhất của Canvas 2D API. Khi nắm vững globalAlpha, globalCompositeOperation, clip() cùng Path2D, bạn có thể tạo ra hiệu ứng glow neon, mặt nạ (masking), tẩy xóa (erase), đèn pha (spotlight), khung avatar bo tròn và viền chuyển động "marching ants" mà không cần bất kỳ thư viện nào. Bài học này đi từ nền tảng pixel blending đến các thủ thuật chuyên nghiệp, kèm 2 demo tương tác trực tiếp ngay trong trang.

1. globalAlpha & Độ trong suốt

Có hai cách hoàn toàn khác nhau để tạo độ trong suốt trong Canvas, và việc nhầm lẫn giữa chúng là nguồn gốc của rất nhiều lỗi vẽ.

  • Alpha cục bộ qua rgba() / hsla(): Độ trong suốt được "nung" trực tiếp vào màu của một thao tác vẽ cụ thể. Ví dụ ctx.fillStyle = 'rgba(225, 29, 72, 0.5)' chỉ ảnh hưởng đến hình được tô bằng màu đó.
  • Alpha toàn cục qua globalAlpha: Là một hệ số (0.0 → 1.0) nhân với độ mờ của mọi thao tác vẽ tiếp theo — kể cả ảnh (drawImage), gradient, hay path. Nó đóng vai trò như một "lớp kính mờ" áp lên toàn bộ context.

Điểm quan trọng: hai cơ chế này nhân chồng lên nhau. Nếu globalAlpha = 0.5 và bạn tô bằng rgba(0,0,0,0.5), độ mờ hiệu dụng cuối cùng là 0.5 × 0.5 = 0.25. Vì globalAlpha là một phần của trạng thái context, nó được lưu/khôi phục bởi save()/restore() — đây là cách an toàn để thay đổi tạm thời độ mờ rồi trả về giá trị cũ.

global_alpha_layering.js
// Xếp lớp ba vòng tròn mờ chồng nhau để tạo cảm giác "kính"
ctx.save();
ctx.globalAlpha = 0.4;          // Lớp kính mờ áp cho mọi hình bên dưới

ctx.fillStyle = '#e11d48';
ctx.beginPath(); ctx.arc(120, 100, 60, 0, Math.PI * 2); ctx.fill();

ctx.fillStyle = '#2563eb';
ctx.beginPath(); ctx.arc(170, 100, 60, 0, Math.PI * 2); ctx.fill();

ctx.restore();                  // globalAlpha tự động trở về 1.0

// Vùng giao nhau sẽ tối/đậm hơn vì hai lớp alpha cộng dồn quang học.

Một ứng dụng kinh điển của globalAlpha là hiệu ứng fade in/out theo thời gian: ta tăng dần giá trị alpha mỗi frame của animation loop, tạo cảm giác đối tượng hiện ra hoặc biến mất mượt mà.

fade_in_animation.js
let opacity = 0;
function fadeIn() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.globalAlpha = Math.min(opacity, 1);   // kẹp giá trị trong [0,1]
  ctx.drawImage(logo, 100, 60);
  ctx.globalAlpha = 1;                       // dọn dẹp để không "rò" sang frame sau

  opacity += 0.02;
  if (opacity < 1) requestAnimationFrame(fadeIn);
}
fadeIn();

2. globalCompositeOperation — 26 chế độ pha trộn

Thuộc tính ctx.globalCompositeOperation quyết định công thức toán học trộn pixel nguồn (source — hình bạn sắp vẽ) với pixel đích (destination — nội dung đã có trên canvas). Mặc định là source-over: hình mới đè lên hình cũ theo alpha thông thường. Canvas hỗ trợ tổng cộng 26 chế độ, chia làm hai họ lớn: nhóm Porter-Duff (quản lý vùng hiển thị dựa trên alpha) và nhóm separable blend modes (trộn màu theo từng kênh, giống Photoshop).

Các chế độ Porter-Duff đặc biệt hữu ích cho maskingerase: destination-out dùng hình nguồn để "khoét lỗ" vào nội dung cũ (cọ tẩy), còn source-in chỉ giữ lại phần nguồn nằm trong vùng đích (cắt ảnh theo hình). Nhóm blend như multiply (nhân, làm tối), screen (làm sáng), overlay (tăng tương phản) và lighter (cộng màu — tạo glow rực rỡ) phục vụ hiệu ứng ánh sáng và phối màu nghệ thuật.

Nhóm Chế độ Tác dụng
Porter-Duff
(vùng/alpha)
source-over (mặc định) Nguồn đè lên đích theo alpha
source-in, source-out, source-atop Giữ nguồn trong/ngoài/chồng vùng đích
destination-over, destination-in, destination-out, destination-atop Đảo vai trò: vẽ sau lưng, tẩy xóa, mặt nạ ngược
copy, xor Thay thế hoàn toàn / loại trừ vùng chồng
Blend
(màu/kênh)
lighter, screen, lighten, color-dodge Làm sáng — lý tưởng cho glow, lửa, ánh sáng
multiply, darken, color-burn Làm tối — bóng đổ, nhuộm màu, kính lọc
overlay, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity Tăng tương phản & pha trộn nghệ thuật theo HSL

Hiệu ứng glow thường dùng lighter: vì chế độ này cộng giá trị màu, các vùng chồng nhau càng nhiều sẽ càng sáng rực, mô phỏng cách photon thực sự cộng dồn năng lượng ánh sáng.

glow_with_lighter.js
ctx.globalCompositeOperation = 'lighter';   // cộng màu -> sáng rực
for (let i = 0; i < 12; i++) {
  const g = ctx.createRadialGradient(300, 150, 0, 300, 150, 80);
  g.addColorStop(0, 'rgba(56, 189, 248, 0.25)');
  g.addColorStop(1, 'rgba(56, 189, 248, 0)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.globalCompositeOperation = 'source-over'; // LUÔN reset sau khi xong

Ngược lại, destination-out biến thao tác vẽ thành một cọ tẩy: pixel nguồn không thêm màu mà trừ alpha của đích, tạo lỗ trong suốt — nền tảng của tính năng "eraser" và hiệu ứng cào thẻ cào (scratch card).

eraser_destination_out.js
// Tô đầy một lớp phủ rồi cho người dùng "cào" lộ ảnh bên dưới
ctx.fillStyle = '#94a3b8';
ctx.fillRect(0, 0, canvas.width, canvas.height);

canvas.addEventListener('pointermove', (e) => {
  ctx.globalCompositeOperation = 'destination-out'; // chế độ tẩy
  ctx.beginPath();
  ctx.arc(e.offsetX, e.offsetY, 24, 0, Math.PI * 2);
  ctx.fill();   // không vẽ màu — chỉ khoét trong suốt
});

Một ứng dụng tinh tế khác là spotlight (đèn pha): tô một lớp tối phủ toàn màn rồi dùng destination-out với gradient hình tròn để "soi" lộ phần nội dung phía dưới — chính là kỹ thuật mà Demo 2 sẽ minh họa.

🎨 Demo tương tác: Trình khám phá Composite Mode

3. clip() & Vùng cắt

Hàm ctx.clip() biến path hiện tại thành một mặt nạ hình học: sau khi gọi, mọi thao tác vẽ tiếp theo chỉ hiển thị trong vùng giới hạn bởi path đó, phần tràn ra ngoài bị cắt bỏ hoàn toàn. Khác với fill() hay stroke(), clip() không vẽ gì cả — nó chỉ thu hẹp "vùng vẽ hợp lệ".

Điểm mấu chốt khi làm việc với clip là không có hàm "unclip". Cách duy nhất để hủy vùng cắt là dùng cặp ctx.save() trước khi clip và ctx.restore() sau khi vẽ xong. Vì clip region là một phần của trạng thái context, restore() sẽ khôi phục lại vùng vẽ rộng ban đầu. Nhiều lần clip lồng nhau sẽ tạo giao (intersection) của các vùng — mỗi clip mới chỉ thu hẹp thêm chứ không bao giờ mở rộng.

circular_avatar.js
// Cắt ảnh đại diện thành hình tròn (avatar bo tròn)
function drawCircularAvatar(img, cx, cy, r) {
  ctx.save();                          // lưu trạng thái không-clip
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.clip();                          // chỉ vẽ trong hình tròn
  ctx.drawImage(img, cx - r, cy - r, r * 2, r * 2);
  ctx.restore();                       // hủy clip, trả lại vùng vẽ đầy đủ

  // viền tròn vẽ SAU restore nên không bị cắt
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.strokeStyle = '#38bdf8';
  ctx.lineWidth = 4;
  ctx.stroke();
}

Từ phiên bản hiện đại, clip() chấp nhận một đối tượng Path2D làm tham số trực tiếp: ctx.clip(myPath2D). Điều này cho phép tách bạch định nghĩa hình dạng khỏi context, tái sử dụng cùng một mặt nạ ở nhiều nơi, và thậm chí truyền quy tắc lấp đầy 'evenodd' để tạo vùng cắt dạng "lỗ thủng".

clip_with_path2d.js
const ring = new Path2D();
ring.arc(150, 150, 120, 0, Math.PI * 2);   // vòng ngoài
ring.arc(150, 150, 60, 0, Math.PI * 2);    // vòng trong (lỗ)

ctx.save();
ctx.clip(ring, 'evenodd');   // chỉ vẽ trong "vành khăn" giữa 2 vòng tròn
ctx.fillStyle = '#e11d48';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();

Một ứng dụng cực kỳ phổ biến là reveal effect (hiệu ứng lộ dần): kết hợp clip() với một hình tròn có bán kính tăng dần theo animation, ta tạo cảm giác nội dung "nở ra" từ một điểm. Cũng có thể clip theo vùng chữ nhật trượt ngang để làm hiệu ứng "rèm kéo" (wipe transition) giữa hai slide.

reveal_clip_animation.js
let r = 0;
function reveal() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.save();
  ctx.beginPath();
  ctx.arc(300, 150, r, 0, Math.PI * 2);
  ctx.clip();
  drawScene();           // toàn cảnh, nhưng chỉ phần trong hình tròn hiện ra
  ctx.restore();
  r += 6;
  if (r < 360) requestAnimationFrame(reveal);
}
reveal();

4. Path2D Object

Trước khi có Path2D, mọi đường vẽ phải được "kể lại" cho context bằng chuỗi lệnh beginPath()moveTo()lineTo()… mỗi frame. Với một biểu đồ có hàng trăm đối tượng lặp lại, điều này tốn rất nhiều lời gọi JavaScript. Path2D giải quyết bằng cách cho phép định nghĩa một hình dạng một lần, lưu vào đối tượng, rồi tái sử dụng qua ctx.fill(path), ctx.stroke(path) hay ctx.clip(path).

Lợi ích hiệu năng đến từ việc trình duyệt có thể cache hình học nội bộ của một Path2D: bạn dựng đường dẫn một lần, sau đó chỉ truyền tham chiếu thay vì phát lại toàn bộ chuỗi lệnh vẽ mỗi khung hình. Ngoài ra, Path2D còn có thể gộp đường dẫn khác qua path.addPath(other, matrix) kèm ma trận biến đổi.

reuse_path2d.js
// Định nghĩa hình ngôi sao MỘT lần, vẽ lại 200 lần
const star = new Path2D();
star.moveTo(0, -10);
for (let i = 1; i < 5; i++) {
  const a = (i * 4 * Math.PI) / 5 - Math.PI / 2;
  star.lineTo(Math.cos(a) * 10, Math.sin(a) * 10);
}
star.closePath();

for (let i = 0; i < 200; i++) {
  ctx.save();
  ctx.translate(Math.random() * canvas.width, Math.random() * canvas.height);
  ctx.fillStyle = '#fbbf24';
  ctx.fill(star);     // tái dùng cùng một đối tượng path
  ctx.restore();
}

Ấn tượng hơn nữa, hàm tạo Path2D chấp nhận trực tiếp cú pháp SVG path data dưới dạng chuỗi. Điều này cho phép bạn lấy thuộc tính d từ một file SVG (ví dụ icon) và render nó nguyên vẹn lên canvas mà không cần dịch tay từng lệnh.

path2d_from_svg.js
// Lấy nguyên chuỗi "d" của một icon SVG và vẽ lên canvas
const heart = new Path2D(
  'M12 21s-7-4.35-9.5-8.5C1 9 3 5 6.5 5 8.5 5 10 6.5 12 9c2-2.5 3.5-4 5.5-4C21 5 23 9 21.5 12.5 19 16.65 12 21 12 21z'
);
ctx.translate(280, 120);
ctx.scale(6, 6);
ctx.fillStyle = '#e11d48';
ctx.fill(heart);   // hỗ trợ đầy đủ M, L, C, Q, A, Z...

5. Line styling nâng cao

Canvas cung cấp nhiều thuộc tính tinh chỉnh cách một đường (stroke) được vẽ. ctx.setLineDash([dài, trống]) định nghĩa mẫu nét đứt: mảng [10, 5] nghĩa là 10px nét rồi 5px khoảng trống, lặp lại. Bằng cách tăng dần ctx.lineDashOffset mỗi frame, ta tạo hiệu ứng kinh điển "marching ants" (đàn kiến diễu hành) — viền chạy liên tục, thường dùng cho vùng chọn (selection) trong các app đồ họa.

Hình dạng đầu mút và góc nối của đường được điều khiển bởi: lineCap ('butt' cắt phẳng, 'round' bo tròn, 'square' vuông kéo dài), và lineJoin ('miter' nhọn, 'round' tròn, 'bevel' vát). Khi dùng 'miter' ở các góc rất nhọn, mũi nhọn có thể vọt ra rất dài — miterLimit đặt ngưỡng tối đa, vượt ngưỡng sẽ tự động chuyển sang 'bevel' để tránh gai nhọn xấu xí.

marching_ants.js
let offset = 0;
function marchingAnts() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.setLineDash([12, 6]);
  ctx.lineDashOffset = -offset;   // dịch âm để nét chạy theo chiều kim đồng hồ
  ctx.lineWidth = 2;
  ctx.strokeStyle = '#38bdf8';
  ctx.strokeRect(80, 60, 240, 140);

  offset = (offset + 1) % 18;     // 18 = tổng chu kỳ dash (12 + 6)
  requestAnimationFrame(marchingAnts);
}
marchingAnts();
cap_join_miter.js
ctx.lineWidth = 18;
ctx.lineCap = 'round';     // đầu mút bo tròn, mềm mại
ctx.lineJoin = 'miter';    // góc nối nhọn
ctx.miterLimit = 4;        // nếu mũi nhọn dài quá 4× lineWidth -> vát bevel

ctx.beginPath();
ctx.moveTo(60, 200);
ctx.lineTo(160, 60);
ctx.lineTo(260, 200);      // góc nhọn ở đỉnh sẽ bị giới hạn bởi miterLimit
ctx.stroke();

ctx.setLineDash([]);       // [] = xóa mẫu nét đứt, trở lại nét liền

6. Shadow & Glow

Canvas có bốn thuộc tính bóng đổ tích hợp sẵn: shadowColor (màu bóng, cần alpha > 0 để hiển thị), shadowBlur (độ mờ nhòe của bóng), cùng shadowOffsetX / shadowOffsetY (độ lệch của bóng so với hình). Khi đặt shadowBlur lớn nhưng cả hai offset bằng 0, bóng tỏa đều quanh hình — đây chính là công thức tạo glow neon.

Để có hiệu ứng neon rực rỡ thực sự, ta thường kết hợp shadow với compositing: vẽ chữ/hình nhiều lần với shadowBlur tăng dần dưới chế độ 'lighter', khiến các lớp glow cộng dồn năng lượng ánh sáng. Lưu ý hiệu năng: shadowBlur rất tốn CPU/GPU, nên hạn chế dùng trong animation loop dày đặc, hoặc render glow một lần ra canvas đệm (offscreen) rồi tái sử dụng.

drop_shadow.js
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 12;
ctx.shadowOffsetX = 6;
ctx.shadowOffsetY = 6;

ctx.fillStyle = '#38bdf8';
ctx.fillRect(80, 80, 160, 100);   // hình hộp có bóng đổ chéo phải-dưới
ctx.restore();                     // tắt bóng cho các thao tác sau
neon_glow.js
ctx.save();
ctx.globalCompositeOperation = 'lighter';   // các lớp glow cộng dồn
ctx.shadowColor = '#f0abfc';
ctx.font = 'bold 64px system-ui';
ctx.fillStyle = '#f0abfc';

[6, 14, 28].forEach(blur => {                // 3 lớp glow chồng nhau
  ctx.shadowBlur = blur;
  ctx.fillText('NEON', 120, 150);
});
ctx.restore();

7. Câu hỏi trắc nghiệm ôn tập

Trắc nghiệm 1: globalAlpha vs rgba()

Nếu globalAlpha = 0.5 và bạn tô bằng rgba(0,0,0,0.5), độ mờ hiệu dụng cuối cùng là bao nhiêu?

Trắc nghiệm 2: Compositing để tẩy xóa

Chế độ globalCompositeOperation nào biến thao tác vẽ thành một "cọ tẩy" khoét lỗ trong suốt vào nội dung đã có?

Trắc nghiệm 3: clip() và save/restore

Làm cách nào để hủy một vùng cắt (clip) và trả lại vùng vẽ đầy đủ cho canvas?

8. Demo: Spotlight Reveal & Marching Ants

Demo dưới đây gộp nhiều kỹ thuật vừa học: một lớp tối phủ toàn cảnh được khoét bằng destination-out với gradient hình tròn để "soi" lộ mẫu hoa văn nhiều màu bên dưới (spotlight). Vị trí đèn pha được điều khiển bằng thanh trượt, và viền vùng sáng là một đường setLineDash chạy liên tục theo kiểu marching ants. Kéo thanh trượt để di chuyển đèn pha.

🎨 Demo tương tác: Spotlight Reveal + Marching Ants

Tải file code thực hành minh họa bài học

File chứa toàn bộ ví dụ về compositing, globalAlpha, clip(), Path2D, line dash và shadow/glow:

Tải về canvas_compositing.js

Related Articles

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

Lesson 5: Pixel Manipulation: Image Filters & Color Picker Bài 5: Pixel Manipulation: Image Filters & Color Picker Lesson 3: Transform & State: translate, rotate, scale & Matrix Bài 3: Transform & State: translate, rotate, scale & Ma Trận Back to Canvas Series Overview Quay lại Lộ trình Canvas Series