This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Canvas cung cấp API mạnh mẽ để render text và vẽ hình ảnh. Bài này sẽ hướng dẫn bạn từ việc hiển thị text cơ bản, tự viết word wrap, đến các kỹ thuật drawImage nâng cao như crop, sprite sheet animation và export canvas.
1. Text trên Canvas
Không giống HTML/CSS, Canvas không có text layout engine. Bạn phải tự quản lý vị trí, font và alignment.
const ctx = canvas.getContext('2d');
// Font — sử dụng CSS font string
ctx.font = '32px Arial';
ctx.font = 'bold 24px "Segoe UI", sans-serif';
ctx.font = 'italic 18px Georgia';
// Fill text (tô đặc)
ctx.fillStyle = '#e11d48';
ctx.fillText('Hello Canvas!', 50, 50);
// Stroke text (viền)
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
ctx.strokeText('Outlined Text', 50, 100);
// Text alignment — canh chỉnh theo trục x
ctx.textAlign = 'left'; // default — x là điểm bắt đầu bên trái
ctx.textAlign = 'center'; // x là điểm giữa text
ctx.textAlign = 'right'; // x là điểm kết thúc bên phải
// Text baseline — canh chỉnh theo trục y
ctx.textBaseline = 'top'; // y là đỉnh text
ctx.textBaseline = 'middle'; // y là giữa text
ctx.textBaseline = 'alphabetic'; // default — y là baseline chữ cái
// Measure text width
ctx.font = '20px Arial';
const metrics = ctx.measureText('Hello Canvas');
console.log(metrics.width); // width in pixels
2. Multiline Text & Word Wrap
Canvas không hỗ trợ xuống dòng tự động. Bạn phải tự viết hàm word wrap đo từng từ và xuống dòng khi vượt quá maxWidth.
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split(' ');
let line = '';
const lines = [];
for (const word of words) {
const testLine = line + (line ? ' ' : '') + word;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && line) {
lines.push(line);
line = word;
} else {
line = testLine;
}
}
lines.push(line);
// Render từng dòng
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], x, y + i * lineHeight);
}
return lines.length; // trả về số dòng đã render
}
// Sử dụng
ctx.font = '16px Arial';
ctx.fillStyle = '#fff';
const longText = 'Canvas không hỗ trợ multiline text nên chúng ta phải tự viết hàm word wrap để xuống dòng tự động khi text quá dài.';
wrapText(ctx, longText, 20, 50, 300, 24);
3. Vẽ Image: drawImage()
drawImage() có 3 dạng overload: vẽ nguyên ảnh, vẽ với resize, và vẽ với crop từ source.
const img = new Image();
img.onload = () => {
// Dạng 1: Vẽ nguyên ảnh tại vị trí (dx, dy)
ctx.drawImage(img, 10, 10);
// Dạng 2: Vẽ + resize về (dWidth, dHeight)
ctx.drawImage(img, 200, 10, 150, 100);
// Dạng 3: Crop từ source + vẽ tại destination
// drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
ctx.drawImage(img, 50, 50, 200, 200, 400, 10, 100, 100);
};
img.src = 'photo.jpg';
// Load multiple images với Promise
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
// Load tất cả rồi mới vẽ
Promise.all([
loadImage('bg.png'),
loadImage('player.png'),
loadImage('enemy.png')
]).then(([bg, player, enemy]) => {
ctx.drawImage(bg, 0, 0);
ctx.drawImage(player, 100, 200);
ctx.drawImage(enemy, 400, 200);
});
4. Sprite Sheets
Sprite sheet là một ảnh lớn chứa nhiều frame animation. Dùng drawImage dạng 3 (crop) để cắt từng frame.
class SpriteAnimator {
constructor(image, frameWidth, frameHeight, frameCount) {
this.image = image;
this.frameWidth = frameWidth;
this.frameHeight = frameHeight;
this.frameCount = frameCount;
this.currentFrame = 0;
this.frameTimer = 0;
this.frameInterval = 100; // ms per frame
}
update(deltaTime) {
this.frameTimer += deltaTime;
if (this.frameTimer >= this.frameInterval) {
this.currentFrame = (this.currentFrame + 1) % this.frameCount;
this.frameTimer = 0;
}
}
draw(ctx, x, y) {
const sx = this.currentFrame * this.frameWidth;
ctx.drawImage(
this.image,
sx, 0, // source x, y
this.frameWidth, this.frameHeight, // source size
x, y, // destination x, y
this.frameWidth, this.frameHeight // destination size
);
}
}
// Sử dụng
const spriteSheet = new Image();
spriteSheet.onload = () => {
const sprite = new SpriteAnimator(spriteSheet, 64, 64, 8);
// Trong game loop: sprite.update(dt); sprite.draw(ctx, 100, 100);
};
spriteSheet.src = 'character_walk.png';
5. Canvas as Image Source
Canvas có thể export thành ảnh hoặc dùng làm source cho canvas khác — rất hữu ích cho pre-rendering và save.
// Export canvas thành Data URL
const dataURL = canvas.toDataURL('image/png');
// Kết quả: "data:image/png;base64,iVBOR..."
// Tạo link download
const link = document.createElement('a');
link.download = 'my-drawing.png';
link.href = dataURL;
link.click();
// Export thành Blob (hiệu quả hơn cho file lớn)
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
// Dùng url cho download hoặc upload
URL.revokeObjectURL(url); // giải phóng memory sau khi dùng
}, 'image/png');
// Canvas-to-Canvas: vẽ canvas này lên canvas khác
const offscreen = document.createElement('canvas');
offscreen.width = 200;
offscreen.height = 200;
const offCtx = offscreen.getContext('2d');
// Pre-render phức tạp trên offscreen
offCtx.fillStyle = '#e11d48';
offCtx.beginPath();
offCtx.arc(100, 100, 80, 0, Math.PI * 2);
offCtx.fill();
// Vẽ offscreen canvas lên main canvas (rất nhanh!)
ctx.drawImage(offscreen, 50, 50);
6. Câu hỏi trắc nghiệm ôn tập
Trắc nghiệm 1: measureText
ctx.measureText('Hello') trả về gì?
Trắc nghiệm 2: drawImage overloads
drawImage có bao nhiêu dạng overload?
Trắc nghiệm 3: Image loading
Tại sao phải đặt code vẽ image trong callback onload?
Tải file code thực hành minh họa bài học
File script tổng hợp các ví dụ về text rendering, drawImage và canvas export:
Tải về canvas_text_images.js
Comments
Bình luận