Search code examples
javascriptcanvasmousenoiseisometric

mouse position to isometric tile including height


Struggeling translating the position of the mouse to the location of the tiles in my grid. When it's all flat, the math looks like this:

this.position.x = Math.floor(((pos.y - 240) / 24) + ((pos.x - 320) / 48));
this.position.y = Math.floor(((pos.y - 240) / 24) - ((pos.x - 320) / 48));

where pos.x and pos.y are the position of the mouse, 240 and 320 are the offset, 24 and 48 the size of the tile. Position then contains the grid coordinate of the tile I'm hovering over. This works reasonably well on a flat surface.

https://i.sstatic.net/gp7qU.png

Now I'm adding height, which the math does not take into account.

https://i.sstatic.net/jWGMf.png

This grid is a 2D grid containing noise, that's being translated to height and tile type. Height is really just an adjustment to the 'Y' position of the tile, so it's possible for two tiles to be drawn in the same spot.

I don't know how to determine which tile I'm hovering over.

edit:

Made some headway... Before, I was depending on the mouseover event to calculate grid position. I just changed this to do the calculation in the draw loop itself, and check if the coordinates are within the limits of the tile currently being drawn. creates some overhead tho, not sure if I'm super happy with it but I'll confirm if it works.

edit 2018:

I have no answer, but since this ha[sd] an open bounty, help yourself to some code and a demo

The grid itself is, simplified;

let grid = [[10,15],[12,23]];

which leads to a drawing like:

for (var i = 0; i < grid.length; i++) {
    for (var j = 0; j < grid[0].length; j++) {
        let x = (j - i) * resourceWidth;
        let y = ((i + j) * resourceHeight) + (grid[i][j] * -resourceHeight); 
        // the "+" bit is the adjustment for height according to perlin noise values
    }
}

edit post-bounty:

See GIF. The accepted answer works. The delay is my fault, the screen doesn't update on mousemove (yet) and the frame rate is low-ish. It's clearly bringing back the right tile.

enter image description here

Source


Solution

  • Intresting task.

    Lets try to simplify it - lets resolve this concrete case

    Solution

    Working version is here: https://github.com/amuzalevskiy/perlin-landscape (changes https://github.com/jorgt/perlin-landscape/pull/1 )

    Explanation

    First what came into mind is:

    Step by step

    Just two steps:

    • find an vertical column, which matches some set of tiles
    • iterate tiles in set from bottom to top, checking if cursor is placed lower than top line

    Step 1

    We need two functions here:

    Detects column:

    function getColumn(mouseX, firstTileXShiftAtScreen, columnWidth) {
      return (mouseX - firstTileXShiftAtScreen) / columnWidth;
    }
    

    Function which extracts an array of tiles which correspond to this column.

    Rotate image 45 deg in mind. The red numbers are columnNo. 3 column is highlighted. X axis is horizontal

    enter image description here

    function tileExists(x, y, width, height) {
      return x >= 0 & y >= 0 & x < width & y < height; 
    }
    
    function getTilesInColumn(columnNo, width, height) {
      let startTileX = 0, startTileY = 0;
      let xShift = true;
      for (let i = 0; i < columnNo; i++) {
        if (tileExists(startTileX + 1, startTileY, width, height)) {
          startTileX++;
        } else {
          if (xShift) {
            xShift = false;
          } else {
            startTileY++;
          }
        }
      }
      let tilesInColumn = [];
      while(tileExists(startTileX, startTileY, width, height)) {
        tilesInColumn.push({x: startTileX, y: startTileY, isLeft: xShift});
        if (xShift) {
          startTileX--;
        } else {
          startTileY++;
        }
        xShift = !xShift;
      }
      return tilesInColumn;
    }
    

    Step 2

    A list of tiles to check is ready. Now for each tile we need to find a top line. Also we have two types of tiles: left and right. We already stored this info during building matching tiles set.

    enter image description here

    function getTileYIncrementByTileZ(tileZ) {
        // implement here
        return 0;
    }
    
    function findExactTile(mouseX, mouseY, tilesInColumn, tiles2d,
                           firstTileXShiftAtScreen, firstTileYShiftAtScreenAt0Height,
                           tileWidth, tileHeight) {
        // we built a set of tiles where bottom ones come first
        // iterate tiles from bottom to top
        for(var i = 0; i < tilesInColumn; i++) {
            let tileInfo = tilesInColumn[i];
            let lineAB = findABForTopLineOfTile(tileInfo.x, tileInfo.y, tiles2d[tileInfo.x][tileInfo.y], 
                                                tileInfo.isLeft, tileWidth, tileHeight);
            if ((mouseY - firstTileYShiftAtScreenAt0Height) >
                (mouseX - firstTileXShiftAtScreen)*lineAB.a + lineAB.b) {
                // WOHOO !!!
                return tileInfo;
            }
        }
    }
    
    function findABForTopLineOfTile(tileX, tileY, tileZ, isLeftTopLine, tileWidth, tileHeight) {
        // find a top line ~~~ a,b
        // y = a * x + b;
        let a = tileWidth / tileHeight; 
        if (isLeftTopLine) {
          a = -a;
        }
        let b = isLeftTopLine ? 
           tileY * 2 * tileHeight :
           - (tileX + 1) * 2 * tileHeight;
        b -= getTileYIncrementByTileZ(tileZ);
        return {a: a, b: b};
    }