Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> JavaScript

Cách vẽ lưới (grid) bằng HTML5 Canvas hoặc SVG


So sánh Canvas vs SVG cho Grid

Đặc điểmCanvasSVG
KiểuRaster (bitmap)Vector (DOM elements)
Hiệu năng grid lớnTố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ợpGame, 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.