Search code examples
javascripthtmlcanvashtml5-canvas

Update canvas on mouse move


The basic idea comes from the map of a game. According to the my code review, the map is a full-page canvas. I have no problem with drawing images on canvas. My question is how to detect map houses and update the canvas or even add click ability to it. I have attached a GIF and HTML code from the original game to better understand my request.

enter image description here

<div id="canvasBorder"><canvas id="canvasMap"></canvas></div>

Okay, This is my code. It's simple. I have drawn the map houses according to the main image which is large on the canvas.

function onClick2() {
  const imagePath = '/lobby/map.png';

  //Image Positions and Width/Height
  const array = [
    { x: 1764, y: 1104, w: 126, h: 84 },
    { x: 0, y: 1188, w: 126, h: 84 },
    { x: 126, y: 1188, w: 126, h: 84 },
    { x: 2090, y: 340, w: 126, h: 68 },
    { x: 126, y: 1188, w: 126, h: 84 },
  ];

  if (canvasRef?.current) {
    let x = canvasRef?.current.getContext('2d');

    let img = new Image();
    img.src = path;

    //Draw Map Blocks
    //Here I deleted the extra codes, I just wanted to show that it was done this way.
    if (x) {
      x.drawImage(
        img,
        array[3].x,
        array[3].y,
        array[3].w,
        array[3].h,
        0,
        0,
        array[3].w,
        array[3].h
      );
    }
  }
}

This is my result: enter image description here

Here I need your guidance to understand the implementation trick. Here we need to recognize the mouse movement on the image or we need a number of squares that are rotated and have the image and work with the isPointInPath function. If we proceed with the second way that I mentioned, to draw the squares, we need rotate(-0.25 * Math.PI);


Solution

  • You need to track the cursor position with mousemove and then do a simple change of basis to translate screen coordinates to game grid coordinates:

    • The cursor's position v_C = (x, y) is in the canonical basis, C.

    • You have an alternate basis B (your 2.5D grid), and its alternate basis vectors in basis C are:

      columnVector_C = (cellHalfSizeLong, cellHalfSizeShort)
      rowsVector_C = (-cellHalfSizeLong, cellHalfSizeShort)
      
    • You put them in columns on a matrix M_BC:

      const M_BC = [
        [cellHalfSizeLong, -cellHalfSizeLong],
        [cellHalfSizeShort, cellHalfSizeShort],
      ];
      

      This matrix helps you translate vectors from basis B to C

    • Invert that matrix to get M_CB. This matrix helps you translate vectors from basis C to B.

    • v_B = M_CB * v_C

    • Lastly, floor the coordinates. This tells you what cell to select/highlight in the 2.5D grid.

    Still:

    Here's a working example:

    // Some basic utils to work with matrices and vectors:
    
    function matrixVectorMultiply(matrix, vector) {
      let result = [];
    
      for (let i = 0; i < matrix.length; i++) {
        let sum = 0;
    
        for (let j = 0; j < vector.length; j++) {
          sum += matrix[i][j] * vector[j];
        }
    
        result.push(sum);
      }
    
      return result;
    }
    
    function invertMatrix(matrix) {
      const n = matrix.length;
    
      let identity = [];
    
      for (let i = 0; i < n; i++) {
        identity.push([]);
    
        for (let j = 0; j < n; j++) {
          identity[i].push(i === j ? 1 : 0);
        }
      }
    
      // Apply Gauss-Jordan elimination:
    
      for (let i = 0; i < n; i++) {
        let pivot = matrix[i][i];
    
        for (let j = 0; j < n; j++) {
          matrix[i][j] /= pivot;
          identity[i][j] /= pivot;
        }
    
        for (let k = 0; k < n; k++) {
          if (k !== i) {
            let factor = matrix[k][i];
    
            for (let j = 0; j < n; j++) {
              matrix[k][j] -= factor * matrix[i][j];
              identity[k][j] -= factor * identity[i][j];
            }
          }
        }
      }
    
      return identity;
    }
    
    // Define the grid data (colors of each cell):
    
    const gridData = [
      ['#008800', '#00FF00', '#008800', '#00FF00', '#008800', '#00FF00'],
      ['#00FF00', '#008800', '#00FF00', '#008800', '#00FF00', '#008800'],
      ['#008800', '#00FF00', '#000088', '#00FF00', '#008800', '#00FF00'],
      ['#00FF00', '#008800', '#00FF00', '#008800', '#00FF00', '#000088'],
      ['#008800', '#00FF00', '#008800', '#00FF00', '#000088', '#0000FF'],
      ['#00FF00', '#008800', '#00FF00', '#000088', '#0000FF', '#000088'],
    ];
    
    // This is just for the demo. In a real application, the grid data matrix would
    // probably contain all the information on each cell objects (array items):
    
    const gridColorToType = {
      '#008800': 'Grass',
      '#00FF00': 'Grass',
      '#000088': 'Water',
      '#0000FF': 'Water',
    };
    
    const selectedCellBolor = '#000000';
    
    // Get the UI elements:
    
    const positionLabelElement = document.getElementById('positionLabel');
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    
    positionLabelElement.textContent = ' ';
    
    // Adjust the canvas to the window:
    
    const width = canvas.width = window.innerWidth;
    const height = canvas.height = window.innerHeight;
    
    // Grid sizing params:
    
    const cellSizeLong = 100;
    const cellHalfSizeLong = cellSizeLong / 2;
    const cellSizeShort = cellSizeLong / 3;
    const cellHalfSizeShort = cellSizeShort / 2;
    
    // Keep track of the selected/highlighted cell:
    
    let currentRow = 0;
    let currentCol = 0;
    
    // Drawing functions:
    
    function drawCell(ctx, color, row, col) {
      ctx.fillStyle = color;
    
      // Calculate the position of the cell
      const x = (col - row) * cellHalfSizeLong + width / 2;
      const y = (col + row) * cellHalfSizeShort;
    
      // Fill:
      
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(x + cellHalfSizeLong, y + cellHalfSizeShort);
      ctx.lineTo(x, y + cellSizeShort);
      ctx.lineTo(x - cellHalfSizeLong, y + cellHalfSizeShort);
      ctx.closePath();
      ctx.fill();
    
      // Border:
      ctx.strokeStyle = '#000000';
      ctx.stroke();
    }
    
    function drawBoard() {
      ctx.clearRect(0, 0, width, height);
          
      const numRows = gridData.length;
      const numCols = gridData[0].length;
    
      // Draw all the cells in their respective color:
      
      for (let row = 0; row < numRows; ++row) {
        for (let col = 0; col < numCols; ++col) {      
          drawCell(ctx, gridData[row][col], row, col);
        }
      }
      
      // And re-draw the selected one on top (you might want to do this differently):
      drawCell(ctx, selectedCellBolor, currentRow, currentCol);
    }
    
    canvas.addEventListener('mousemove', () => {
        const x_C = width / 2 - event.clientX;
        const y_C = event.clientY;
        
        // First column is the columns vector in the 2.5D grid.
        // Second column is the rows vector in the 2.5 grid.
        const M_BC = [
          [cellHalfSizeLong, -cellHalfSizeLong],
          [cellHalfSizeShort, cellHalfSizeShort],
        ];
        
        // We need the inverse of that matrix to translate canonical basis
        // coordinates to coordinates in the 2.5D space's base:
        const M_CB = invertMatrix(M_BC);
        
        const [x_B, y_B] = matrixVectorMultiply(M_CB, [x_C, y_C]);
        const int_x_B = Math.floor(x_B);
        const int_y_B = Math.floor(y_B);
        
        currentRow = int_x_B;
        currentCol = int_y_B;
        
        const cellType = gridColorToType[gridData[currentRow]?.[currentCol]] || 'Void';
        
        positionLabelElement.textContent = `(${
          (x_C | 0).toFixed().padStart(4, ' ')
        }, ${
          (y_C | 0).toFixed().padStart(4, ' ')
        }) => (${
          x_B.toFixed(2).padStart(5, ' ')
        }, ${
          y_B.toFixed(2).padStart(5, ' ')
        }) => (${
          int_x_B.toFixed().padStart(2, ' ')
        }, ${
          int_y_B.toFixed().padStart(2, ' ')
        }) => ${ cellType }`;    
        
        requestAnimationFrame(() => {
          drawBoard();
        });
    });
    
    drawBoard();
    body {
      background: #777;
    }
    
    #canvas {
      position: fixed;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
    }
    
    #positionLabel {
      position: fixed;
      bottom: 0;
      left: 0;
      background: rgba(255, 255, 255, .5); 
      padding: 8px;
      border-radius: 0 4px 0 0;
      font-family: monospace;
      font-weight: bold;
      white-space: pre;
      backdrop-filter: blur(8px);
      pointer-events: none;
    }
    <canvas id="canvas"></canvas>
    
    <div id="positionLabel"> <div>

    If you don't care about pixel-perfect accuracy (e.g. you don't care whether if a cell has a tree that overflows a bit, covering the one behind, the mouse is not able to recognize being on top of the tree), then you don't need isPointInPath, and you also gain a lot of performance.