You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Canvas等距网格中建筑鼠标悬停与点击交互问题求解

等距网格建筑的鼠标悬停与点击交互问题

我在HTML Canvas中实现了一个等距网格,正尝试处理建筑的鼠标悬停交互,部分建筑存在不同高度。当前遇到两个问题:

  • 鼠标指针离开地面瓦片或位于建筑图像中间时,高亮的瓦片会消失
  • 需要实现单个建筑的点击功能

核心基础函数

let applied_map = ref([]); // tileMap
let tile_images = ref([]); // 存储Canvas可用的已加载图片
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();
};

等距网格示例

后续更新

基于Helder Sepulveda的思路,我编写了drawCube函数,并将其整合到点击函数和renderTiles中。点击时和帧更新时,会在建筑所在位置生成一个带三个面的立方体,并将Path存储在全局变量中,立方体位置与等距网格对齐。

在drawCube中添加了逻辑:若相邻瓦片存在建筑,则隐藏立方体的右侧面,避免悬停时误触发其他建筑。

优化后示例1
优化后示例2

部分点击函数代码

//...部分点击函数代码
//...
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, // 向左延伸的X长度
    tile_height.value, // 向右延伸的X长度
    Number(tile_images.value[tileIndex].img.height / 2), // 高度
    ctx.value,
    {
      tile_index: tileIndex - 1 < 0 ? 0 : tileIndex - 1,
    }
  );
}

drawCube函数代码

const drawCube = (x, y, wx, wy, h, the_ctx, options = {}) => {
  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);
  }
  // 绘制左侧面
  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);
  }

  // 绘制右侧面
  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);
  }
  // 绘制顶面
  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);
  }
  
  let isONHover = the_ctx.isPointInPath(
    path,
    mousePosition.x - 10,
    mousePosition.y - 10
  );
  the_ctx.fillStyle = null;
  if (isONHover) {
    // 处理点击事件
    if (isMouseDown.value) {
      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;
  }
};

内容的提问来源于stack exchange,提问作者James Allan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 04:15:34