javascripthtml5-canvasgridisometric

Handle mouse hovering image inside of canvas isometric grid


I got a isometric grid in html canvas.

I am trying to handle the mouse hover the buildings.

Some buildings will have different heights.

As you can see in the image below I am hovering a tile, the mouse pointer is inside the blueish tile.

The problem is when the mouse pointer is off the ground tile, or in the middle of the building image, the highlighted tile goes off.

Need a way to click on each individual building, how can this be resolved?

Main basic functions:

  let applied_map = ref([]); // tileMap
  let tile_images = ref([]); // this will contain loaded IMAGES for canvas to consume from
  let tile_height = ref(50);
  let tile_width = ref(100);



  const renderTiles = (x, y) => {
  let tileWidth = tile_width.value;
  let tileHeight = tile_height.value;
  let tile_half_width = tileWidth / 2;
  let tile_half_height = tileHeight / 2;
 
  for (let tileX = 0; tileX < gridSize.value; ++tileX) {
    for (let tileY = 0; tileY < gridSize.value; ++tileY) {
      let renderX = x + (tileX - tileY) * tile_half_width;
      let renderY = y + (tileX + tileY) * tile_half_height;
      let tile = applied_map.value[tileY * gridSize.value + tileX];
      renderTileBackground(renderX, renderY + 50, tileWidth, tileHeight);
      if (tile !== -1) {
        if (tile_images.value.length) {
          renderTexturedTile(
            tile_images.value[tile].img,
            renderX,
            renderY + 40,
            tileHeight
          );
        }
      }
    }
  }

  if (
    hoverTileX.value >= 0 &&
    hoverTileY.value >= 0 &&
    hoverTileX.value < gridSize.value &&
    hoverTileY.value < gridSize.value
  ) {
    let renderX = x + (hoverTileX.value - hoverTileY.value) * tile_half_width;
    let renderY = y + (hoverTileX.value + hoverTileY.value) * tile_half_height;

    renderTileHover(renderX, renderY + 50, tileWidth, tileHeight);
  }
};

const renderTileBackground = (x, y, width, height) => {
  ctx.value.beginPath();
  ctx.value.setLineDash([5, 5]);
  ctx.value.strokeStyle = "black";
  ctx.value.fillStyle = "rgba(25,34, 44,0.2)";
  ctx.value.lineWidth = 1;
  ctx.value.moveTo(x, y);
  ctx.value.lineTo(x + width / 2, y - height / 2);
  ctx.value.lineTo(x + width, y);
  ctx.value.lineTo(x + width / 2, y + height / 2);
  ctx.value.lineTo(x, y);
  ctx.value.stroke();
  ctx.value.fill();
};

const renderTexturedTile = (imgSrc, x, y, tileHeight) => {
  let offsetY = tileHeight - imgSrc.height;

  ctx.value.drawImage(imgSrc, x, y + offsetY);
};

const renderTileHover = (x, y, width, height) => {
  ctx.value.beginPath();
  ctx.value.setLineDash([]);
  ctx.value.strokeStyle = "rgba(161, 153, 255, 0.8)";
  ctx.value.fillStyle = "rgba(161, 153, 255, 0.4)";
  ctx.value.lineWidth = 2;
  ctx.value.moveTo(x, y);
  ctx.value.lineTo(x + width / 2, y - height / 2);
  ctx.value.lineTo(x + width, y);
  ctx.value.lineTo(x + width / 2, y + height / 2);
  ctx.value.lineTo(x, y);
  ctx.value.stroke();
  ctx.value.fill();
};

enter image description here

Updates after answer below

Based on Helder Sepulveda answer I created a function drawCube.

And added to my click function and to the renderTiles. So on click and frame update it creates a cube with 3 faces,and its placed on same position as the building and stores the Path on a global variable, the cube follows the isometric position. In the drawCube, there is a condition where i need to hide the right face from the cube. Hide if there's a building on the next tile. So if you hover the building it wont trigger the last building on.

enter image description here enter image description here

      //...some code click function
      //...
      if (tile_images.value[tileIndex] !== undefined) {
        drawCube(
          hoverTileX.value + tile_height.value,
          hoverTileY.value +
            Number(tile_images.value[tileIndex].img.height / 2) -
            10,
          tile_height.value, // grow X pos to left
          tile_height.value, // grow X pos to right,
          Number(tile_images.value[tileIndex].img.height / 2), // height,
          ctx.value,
          {
            tile_index: tileIndex - 1 < 0 ? 0 : tileIndex - 1,
          }
        );
      }

This is the drawCube

const drawCube = (x, y, wx, wy, h, the_ctx, options = {}) => {
  // https://codepen.io/AshKyd/pen/JYXEpL
  let path = new Path2D();
  let hide_options = {
    left_face: false,
    right_face: false,
    top_face: false,
  };
  if (options.hasOwnProperty("hide")) {
    hide_options = Object.assign(hide_options, options.hide);
  }
  // left face
  if (!hide_options.left_face) {
    path.moveTo(x, y);
    path.lineTo(x - wx, y - wx * 0.5);
    path.lineTo(x - wx, y - h - wx * 0.5);
    path.lineTo(x, y - h * 1);
  }

  // right;
  if (
    !hide_options.right_face &&
    !coliders.value[options.tile_index].hide_right_face
  ) {
    path.moveTo(x, y);
    path.lineTo(x + wy, y - wy * 0.5);
    path.lineTo(x + wy, y - h - wy * 0.5);
    path.lineTo(x, y - h * 1);
  }
  //top
  if (!hide_options.right_face) {
    path.moveTo(x, y - h);
    path.lineTo(x - wx, y - h - wx * 0.5);
    path.lineTo(x - wx + wy, y - h - (wx * 0.5 + wy * 0.5));
    path.lineTo(x + wy, y - h - wy * 0.5);
  }
  // the_ctx.beginPath();
  let isONHover = the_ctx.isPointInPath(
    path,
    mousePosition.x - 10,
    mousePosition.y - 10
  );
  the_ctx.fillStyle = null;
  if (isONHover) {
    // let indx = options.tile_pos.y * gridSize.value + options.tile_pos.x;
    //this is the click on object event
    if (isMouseDown.value) {
      //Trigger
      if (buildozer.value === true) {
        coliders.value[options.tile_index] = -1;
        applied_map.value[options.tile_index] = -1;
      }
      isMouseDown.value = false;
    }
    the_ctx.fillStyle = "green";
  }

  the_ctx.fill(path);
  if (
    coliders.value[options.tile_index] == -1 &&
    applied_map.value[options.tile_index]
  ) {
    coliders.value[options.tile_index] = path;
  }
};

Solution

  • In a nutshell you need to be able to detect mouseover on more complex shapes ...

    I recommend you to use Path2d:
    https://developer.mozilla.org/en-US/docs/Web/API/Path2D
    That way you can build any shape you like and then we have access to isPointInPath to detect if the mouse is over our shape.
    https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath

    Here is a small example:

    class Shape {
      constructor(x, y, width, height) {
        this.path = new Path2D()
        this.path.arc(x, y, 12, 0, 2 * Math.PI)
        this.path.arc(x, y - 9, 8, 0, 1.5 * Math.PI)
        this.path.lineTo(x + width / 2, y)
        this.path.lineTo(x, y + height / 2)
        this.path.lineTo(x - width / 2, y)
        this.path.lineTo(x, y - height / 2)
        this.path.lineTo(x + width / 2, y)
      }
    
      draw(ctx, pos) {
        ctx.beginPath()
        ctx.fillStyle = ctx.isPointInPath(this.path, pos.x, pos.y) ? "red" : "green"
        ctx.fill(this.path)
      }
    }
    
    function getMousePos(canvas, evt) {
      var rect = canvas.getBoundingClientRect()
      return {
        x: evt.clientX - rect.left,
        y: evt.clientY - rect.top
      }
    }
    
    var canvas = document.getElementById("canvas")
    var ctx = canvas.getContext("2d")
    
    shapes = []
    for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 4; j++) {
        shapes.push(new Shape(50 + i * 40, 40 + j * 40, 40, 20))
      }
    }
    
    canvas.addEventListener("mousemove", function(evt) {
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        var mousePos = getMousePos(canvas, evt)
        shapes.forEach((s) => {s.draw(ctx, mousePos)})
      },
      false
    )
    shapes.forEach((s) => {
      s.draw(ctx, {x: 0, y: 0})
    })
    <canvas id="canvas" width="200" height="200"></canvas>

    This example draws a "complex" shape (two arcs and a few lines) and the shape changes color to red when the mouse is hovering the shape