简体   繁体   中英

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

在此处输入图像描述

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.

在此处输入图像描述 在此处输入图像描述

      //...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;
  }
};

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM