So sánh Canvas vs SVG cho Grid
| Đặc điểm | Canvas | SVG |
|---|---|---|
| Kiểu | Raster (bitmap) | Vector (DOM elements) |
| Hiệu năng grid lớn | Tốt (một draw call) | Kém (nhiều DOM nodes) |
| Tương tác (click cell) | Cần tính toán toạ độ | Dễ dàng (event trên element) |
| Zoom không mờ | Không (cần redraw) | Có (vector) |
| Phù hợp | Game, chart, nhiều ô (>1000) | Spreadsheet, UI grid, ít ô |
Canvas Grid - Hiệu năng cao
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Canvas Grid</title>
<style>
body { display: flex; justify-content: center; padding: 2rem; background: #f5f5f5; }
canvas { background: white; box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-radius: 8px; }
.controls { margin-top: 1rem; text-align: center; }
button { margin: 0 0.5rem; padding: 0.5rem 1rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<canvas id="gridCanvas" width="600" height="400"></canvas>
<div class="controls">
<button onclick="redrawGrid(20)">Grid 20px</button>
<button onclick="redrawGrid(50)">Grid 50px</button>
<button onclick="redrawGrid(100)">Grid 100px</button>
<button onclick="toggleSubGrid()">Bật/tắt sub-grid</button>
</div>
<script>
const canvas = document.getElementById('gridCanvas');
const ctx = canvas.getContext('2d');
let gridSize = 50;
let showSubGrid = false;
const subGridSize = 10;
const padding = 20;
function drawGrid() {
const { width, height } = canvas;
// Clear
ctx.clearRect(0, 0, width, height);
// Background
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, width, height);
// Main grid lines
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 1;
// Vertical lines
for (let x = padding; x < width - padding; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x + 0.5, padding); // +0.5 cho line mỏng nét (pixel alignment)
ctx.lineTo(x + 0.5, height - padding);
ctx.stroke();
}
// Horizontal lines
for (let y = padding; y < height - padding; y += gridSize) {
ctx.beginPath();
ctx.moveTo(padding, y + 0.5);
ctx.lineTo(width - padding, y + 0.5);
ctx.stroke();
}
// Border
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
ctx.strokeRect(padding + 0.5, padding + 0.5, width - 2 * padding, height - 2 * padding);
// Sub grid (optional)
if (showSubGrid) {
ctx.strokeStyle = '#eee';
ctx.lineWidth = 0.5;
for (let x = padding; x < width - padding; x += subGridSize) {
if (x % gridSize !== 0) {
ctx.beginPath();
ctx.moveTo(x + 0.5, padding);
ctx.lineTo(x + 0.5, height - padding);
ctx.stroke();
}
}
for (let y = padding; y < height - padding; y += subGridSize) {
if (y % gridSize !== 0) {
ctx.beginPath();
ctx.moveTo(padding, y + 0.5);
ctx.lineTo(width - padding, y + 0.5);
ctx.stroke();
}
}
}
// Coordinates label
ctx.fillStyle = '#999';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (let x = padding; x < width - padding; x += gridSize) {
const col = (x - padding) / gridSize;
ctx.fillText(col.toString(), x, padding - 15);
}
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let y = padding; y < height - padding; y += gridSize) {
const row = (y - padding) / gridSize;
ctx.fillText(row.toString(), padding - 5, y);
}
}
function redrawGrid(size) {
gridSize = size;
drawGrid();
}
function toggleSubGrid() {
showSubGrid = !showSubGrid;
drawGrid();
}
// Highlight cell on hover
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (x >= padding && x < canvas.width - padding && y >= padding && y < canvas.height - padding) {
const col = Math.floor((x - padding) / gridSize);
const row = Math.floor((y - padding) / gridSize);
const cellX = padding + col * gridSize;
const cellY = padding + row * gridSize;
drawGrid(); // Redraw base
// Highlight cell
ctx.fillStyle = 'rgba(52, 152, 219, 0.2)';
ctx.fillRect(cellX, cellY, gridSize, gridSize);
ctx.strokeStyle = '#3498db';
ctx.lineWidth = 2;
ctx.strokeRect(cellX + 0.5, cellY + 0.5, gridSize, gridSize);
// Tooltip
canvas.title = `Cell: [${col}, ${row}]`;
} else {
canvas.title = '';
}
});
canvas.addEventListener('mouseleave', () => {
drawGrid();
canvas.title = '';
});
// Initial draw
drawGrid();
</script>
</body>
</html>
SVG Grid - Dễ tương tác, scale tốt
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>SVG Grid</title>
<style>
body { display: flex; justify-content: center; padding: 2rem; background: #f5f5f5; }
.svg-container { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
svg { display: block; }
.cell:hover { fill: rgba(52, 152, 219, 0.2); stroke: #3498db; stroke-width: 2; cursor: pointer; }
.controls { margin-top: 1rem; text-align: center; }
button { margin: 0 0.5rem; padding: 0.5rem 1rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="svg-container">
<svg id="gridSvg" width="600" height="400"></svg>
</div>
<div class="controls">
<button onclick="recreateGrid(20)">Grid 20px</button>
<button onclick="recreateGrid(50)">Grid 50px</button>
<button onclick="recreateGrid(100)">Grid 100px</button>
</div>
<script>
const svg = document.getElementById('gridSvg');
const NS = 'https://www.w3.org/2000/svg';
let gridSize = 50;
const padding = 20;
const cols = 11; // (600-40)/50 ≈ 11
const rows = 7; // (400-40)/50 ≈ 7
function createGrid(size) {
gridSize = size;
svg.innerHTML = ''; // Clear
// Defs for patterns (optional)
const defs = document.createElementNS(NS, 'defs');
const pattern = document.createElementNS(NS, 'pattern');
pattern.setAttribute('id', 'gridPattern');
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute('width', gridSize);
pattern.setAttribute('height', gridSize);
const line1 = document.createElementNS(NS, 'line');
line1.setAttribute('x1', 0); line1.setAttribute('y1', 0);
line1.setAttribute('x2', gridSize); line1.setAttribute('y2', 0);
line1.setAttribute('stroke', '#ddd'); line1.setAttribute('stroke-width', 1);
const line2 = document.createElementNS(NS, 'line');
line2.setAttribute('x1', 0); line2.setAttribute('y1', 0);
line2.setAttribute('x2', 0); line2.setAttribute('y2', gridSize);
line2.setAttribute('stroke', '#ddd'); line2.setAttribute('stroke-width', 1);
pattern.appendChild(line1);
pattern.appendChild(line2);
defs.appendChild(pattern);
svg.appendChild(defs);
// Background rect with pattern
const bg = document.createElementNS(NS, 'rect');
bg.setAttribute('x', padding);
bg.setAttribute('y', padding);
bg.setAttribute('width', (cols) * gridSize);
bg.setAttribute('height', (rows) * gridSize);
bg.setAttribute('fill', 'url(#gridPattern)');
svg.appendChild(bg);
// Border
const border = document.createElementNS(NS, 'rect');
border.setAttribute('x', padding);
border.setAttribute('y', padding);
border.setAttribute('width', (cols) * gridSize);
border.setAttribute('height', (rows) * gridSize);
border.setAttribute('fill', 'none');
border.setAttribute('stroke', '#999');
border.setAttribute('stroke-width', 2);
svg.appendChild(border);
// Interactive cells (transparent rects on top)
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const cell = document.createElementNS(NS, 'rect');
cell.setAttribute('class', 'cell');
cell.setAttribute('x', padding + col * gridSize);
cell.setAttribute('y', padding + row * gridSize);
cell.setAttribute('width', gridSize);
cell.setAttribute('height', gridSize);
cell.setAttribute('fill', 'transparent');
cell.setAttribute('stroke', 'transparent');
cell.setAttribute('stroke-width', 2);
cell.dataset.col = col;
cell.dataset.row = row;
cell.addEventListener('mouseenter', () => {
cell.setAttribute('fill', 'rgba(52, 152, 219, 0.2)');
cell.setAttribute('stroke', '#3498db');
svg.title = `Cell: [${col}, ${row}]`;
});
cell.addEventListener('mouseleave', () => {
cell.setAttribute('fill', 'transparent');
cell.setAttribute('stroke', 'transparent');
svg.title = '';
});
cell.addEventListener('click', () => {
alert(`Clicked cell [${col}, ${row}]`);
});
svg.appendChild(cell);
}
}
// Coordinate labels
for (let col = 0; col < cols; col++) {
const text = document.createElementNS(NS, 'text');
text.setAttribute('x', padding + col * gridSize + gridSize/2);
text.setAttribute('y', padding - 5);
text.setAttribute('text-anchor', 'middle');
text.setAttribute('font-size', '10');
text.setAttribute('font-family', 'monospace');
text.setAttribute('fill', '#999');
text.textContent = col;
svg.appendChild(text);
}
for (let row = 0; row < rows; row++) {
const text = document.createElementNS(NS, 'text');
text.setAttribute('x', padding - 5);
text.setAttribute('y', padding + row * gridSize + gridSize/2);
text.setAttribute('text-anchor', 'end');
text.setAttribute('dominant-baseline', 'middle');
text.setAttribute('font-size', '10');
text.setAttribute('font-family', 'monospace');
text.setAttribute('fill', '#999');
text.textContent = row;
svg.appendChild(text);
}
}
function recreateGrid(size) {
createGrid(size);
}
createGrid(50);
</script>
</body>
</html>
Khi nào dùng cái nào?
- Canvas: Game board (cờ vua, cờ caro), heatmap, pixel art editor, large grid (>1000 cells), real-time rendering
- SVG: Spreadsheet-like UI, seat map, clickable grid cells, need CSS styling per cell, print quality, zoomable
Kết hợp: Canvas cho render, SVG cho overlay UI
Dùng canvas vẽ grid nền (hiệu năng), overlay SVG elements cho interactive cells.