javascriptcanvasgame-developmentisometric

Mouse coordinates in a canvas to 30 degree isometric coordinates on a grid


I have an isometric grid I'm drawing on a canvas. It uses a 30 degree angle offset and I use some script to draw a basic grid. For this grid I am projecting a flat grid with 40x40 tile sizes.

gridRows = 10;
gridCols = 10;
tileSize = 40;

gridWidth = gridCols * tileSize;
gridHeight = gridRows * tileSize;

canvasWidth = tileSize * (gridCols + gridRows) * Math.sqrt(3) / 2;
canvasHeight = tileSize * (gridRows + gridCols) / 2;

canvasOffset = tileSize * gridRows * Math.sqrt(3) / 2;

function carToIso(x, y) {
      // Convert cartesian (x, y) to isometric coordinates
      return [
        Math.round((x - y) * Math.sqrt(3) / 2 + canvasOffset),
        Math.round((x + y) / 2)
      ];
}

function drawGrid() {
            let canvas = $("#canvas");
      canvas.attr('width', canvasWidth);
      canvas.attr('height', canvasHeight);

            let ctx = canvas.get(0).getContext('2d');

      // Background
      ctx.fillStyle = "white";
      ctx.fillRect(0, 0, canvasWidth, canvasHeight);

      // Draw lines
      ctx.beginPath();
      ctx.lineWidth = 1;

      // Rows
      for(let i = 0; i <= gridRows; ++i) {
        ctx.moveTo(...carToIso(0, i * tileSize));
        ctx.lineTo(...carToIso(gridWidth, i * tileSize));
      }

      // Columns
      for(let i = 0; i <= gridCols; ++i) {
        ctx.moveTo(...carToIso(i * tileSize, 0));
        ctx.lineTo(...carToIso(i * tileSize, gridHeight));
      }

      ctx.stroke();
}

drawGrid();

This works perfectly fine and gets me the grid I want. A fiddle with this working is here; https://jsfiddle.net/fgw10sev/2/

For the next step, I need to figure out what tile someone is hovering over. I initially had a 2:1 grid and was able to convert mouse coordinates within the canvas to grid coordinates in the original un-projected grid like so;

function mouseToIso(x, y) {
      // Convert mouse (x, y in canvas) to grid coordinates
      return [
        Math.round((2 * y + x - canvasOffset) / 2),
        Math.round((2 * y - x + canvasOffset) / 2)
      ];
    }

However, with the addition of the sqrt(3) factor, this no longer works and I cannot - for the life of me - figure out how to fix this. The original mouseToIso function was already a hackjob of trial and error and I've tried injecting the sqrt(3) factor in various places, but I just can't get it to work.

Can anyone help me out here? Or tell me I need to use a completely different approach entirely?

Note; I need this sqrt(3) factor in there because of the images I'm using. They're all at 30 degree angles and without that factor, the grid does no line up properly with the images.

Note 2; just for completenes - sqrt(3)/2 == cos(deg2rad(30)) and the 2 can be used interchangeably.


Solution

  • I am not myself very well versed in algebra so there may be an even simpler solution but anyway...

    What your function needs to do is to invert the transformation that occurred in the previous step in order to find back the original x and y values.

    a = Math.round((x - y) * Math.sqrt(3) / 2 + canvasOffset);
    b = Math.round((x + y) / 2);
    

    From this we can retrieve the values (x - y) and (x + y) as being

    (x - y) = (a - canvasOffset) / Math.sqrt(3) * 2;
    (x + y) = b * 2;
    

    Now that we have both (x - y) and (x + y) we can find x by adding up these two values and dividing by 2, the y value cancels itself.
    And now it's easy to find y, since it's just (x + y) - x.

    const pre = document.querySelector("pre");
    const log = (txt) => pre.textContent = JSON.stringify(txt);
    
    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    const gridRows = 10;
    const gridCols = 10;
    const tileSize = 40;
    
    const gridWidth = gridCols * tileSize;
    const gridHeight = gridRows * tileSize;
    
    const canvasWidth = tileSize * (gridCols + gridRows) * Math.sqrt(3) / 2;
    const canvasHeight = tileSize * (gridRows + gridCols) / 2;
    
    const canvasOffset = tileSize * gridRows * Math.sqrt(3) / 2;
    
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    
    function carToIso(x, y) {
      // Convert cartesian (x, y) to isometric coordinates
      // I moved the scaling part here, it makes lees noise in the rest of the code
      // But feel free to change it back
      x *= tileSize;
      y *= tileSize;
      return [
        Math.round((x - y) * Math.sqrt(3) / 2 + canvasOffset),
        Math.round((x + y) / 2)
      ];
    }
    
    function isoToCar(a, b) {
      // Convert isometric (a, b) to cartesian coordinates
      const xMinusY = (a - canvasOffset) / Math.sqrt(3) * 2;
      const xPlusY = b * 2;
      const x = (xMinusY + xPlusY) / 2;
      const y = xPlusY - x;
      return [
        Math.floor(x / tileSize), // scaling is here too
        Math.floor(y / tileSize)
      ];
    }
    
    function drawGrid() {
      // Draw lines
      ctx.beginPath();
      ctx.lineWidth = 1;
    
      ctx.fillStyle = "red";
    
      // Rows
      for (let i = 0; i <= gridRows; ++i) {
        const [x1, y1] = carToIso(0, i);
        const [x2, y2] = carToIso(gridCols, i);
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.fillText("r" + i, (x2 + x1) / 2, (y2 + y1) / 2);
      }
    
      // Columns
      for (let i = 0; i <= gridCols; ++i) {
        const [x1, y1] = carToIso(i, 0);
        const [x2, y2] = carToIso(i, gridRows);
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.fillText("c" + i, (x2 + x1) / 2, (y2 + y1) / 2);
      }
    
      ctx.stroke();
    }
    
    function fillCell(x, y, color) {
      ctx.beginPath();
      ctx.moveTo(...carToIso(x, y));
      ctx.lineTo(...carToIso((x + 1), y));
      ctx.lineTo(...carToIso((x + 1), (y + 1)));
      ctx.lineTo(...carToIso(x, (y + 1)));
      ctx.fillStyle = "green";
      ctx.fill();
    }
    
    onmousemove = (evt) => {
      const {top, left} = canvas.getBoundingClientRect();
      const mouseX = evt.clientX - left;
      const mouseY = evt.clientY - top;
      const [gridX, gridY] = isoToCar(mouseX, mouseY);
      log({
        mouseX,
        mouseY,
        gridX,
        gridY
      });
    
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      fillCell(gridX, gridY, "green");
      drawGrid();
    };
    
    drawGrid();
    pre { position: absolute }
    <pre></pre>
    <canvas id="canvas"></canvas>