skip to Main Content

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);

2

Answers


  1. I suggest looking into JavaScript events.

    The HTML canvas doesn’t have any fancy input handlers, it’s just for drawing. But we can relatively easily listen for mousemove and click events to interact with the canvas.

    const canvasMap = document.getElementById("canvasMap");
    
    // First method
    canvasMap.onmousemove = function(event) {
        console.log(event);
    }
    
    // Second method
    canvasMap.addEventListener("mousemove", function(event) {
        console.log(event);
        // If collision with tile, draw white border around it.
    });
    
    // For click events
    canvasMap.addEventListener("click", function(event) {
        console.log(event);
    });
    

    Then you can take the clientX and clientY properties and use them to calculate which tile on the canvas the mouse intersects with.

    If the canvas is not positioned at the top left, we can get the bounding client rect and subtract the clientLeft and clientTop properties of the canvas from the mouse coordinates to get the mouse position relative to the canvas.

    If we have the coordinates relative to the canvas it should be as simple as a collision detection, followed by performing a visual change in the canvas.

    For the collision detection you can refer to this stackoverflow post and do some research.

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search