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

JavaScript Canvas瓦片鼠标悬停跟随式弹窗实现技术问询

嘿,这个需求我刚好折腾过,给你理清楚实现逻辑,附上完整可运行的代码,直接抄就能用~

实现Canvas瓦片悬浮跟随弹窗的完整方案

核心思路拆解

要实现这个效果,核心要解决两个问题:精准判断鼠标悬停在哪个瓦片上,以及让弹窗跟随鼠标实时更新位置和内容。

1. 先搞定「鼠标-瓦片」的坐标映射

Canvas的鼠标坐标是基于视口的,我们需要把它转换成瓦片网格的行列号。比如每个瓦片是32px,那用鼠标在Canvas内的坐标除以瓦片尺寸取整,就能得到对应的行列。

2. 打造跟随式弹窗

用一个固定定位的div做弹窗,默认隐藏。当检测到鼠标在有效瓦片上时,显示弹窗并同步更新内容和位置;鼠标离开瓦片或Canvas时隐藏弹窗。

完整可运行代码

HTML部分

<!-- Canvas容器 -->
<canvas id="tileCanvas" width="320" height="320"></canvas>
<!-- 悬浮弹窗 -->
<div id="tileTooltip" class="tooltip"></div>

CSS部分

.tooltip {
  position: fixed;
  background: #ffffff;
  border: 1px solid #cccccc;
  padding: 8px 12px;
  border-radius: 4px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  display: none;
  pointer-events: none; /* 关键:防止弹窗干扰鼠标事件 */
  z-index: 1000;
  font-size: 14px;
  line-height: 1.4;
}

JavaScript部分

const canvas = document.getElementById('tileCanvas');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tileTooltip');

// 基础配置
const tileSize = 32;       // 单个瓦片的尺寸(像素)
const gridSize = 10;       // 瓦片网格大小(10×10)
let canvasRect = canvas.getBoundingClientRect(); // Canvas的视口位置

// 模拟瓦片数据:每个瓦片存储独立信息(你可以替换成自己的业务数据)
const tileData = Array.from({ length: gridSize }, (_, row) => 
  Array.from({ length: gridSize }, (_, col) => ({
    row: row + 1,
    col: col + 1,
    terrain: Math.random() > 0.5 ? '平原' : '山地',
    resources: Math.floor(Math.random() * 100),
    description: `这是第${row+1}行第${col+1}列的瓦片`
  })
));

// 绘制瓦片(这里用纯色模拟,实际换成new Image()加载的图片即可)
function drawTiles() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  tileData.forEach((row, rowIdx) => {
    row.forEach((tile, colIdx) => {
      // 绘制瓦片底色(模拟不同地形)
      ctx.fillStyle = tile.terrain === '平原' ? '#f0f9e8' : '#d9d2c5';
      ctx.fillRect(colIdx * tileSize, rowIdx * tileSize, tileSize, tileSize);
      // 绘制瓦片编号(可选,方便调试)
      ctx.fillStyle = '#333333';
      ctx.font = '12px Arial';
      ctx.textAlign = 'center';
      ctx.fillText(`${tile.row},${tile.col}`, colIdx*tileSize + tileSize/2, rowIdx*tileSize + tileSize/2 + 4);
    });
  });
}

// 根据鼠标位置获取对应的瓦片
function getHoveredTile(clientX, clientY) {
  // 转换为Canvas内部坐标(修正Canvas的视口偏移)
  const x = clientX - canvasRect.left;
  const y = clientY - canvasRect.top;
  // 计算瓦片行列
  const col = Math.floor(x / tileSize);
  const row = Math.floor(y / tileSize);
  // 检查是否在有效网格范围内
  if (row >= 0 && row < gridSize && col >= 0 && col < gridSize) {
    return tileData[row][col];
  }
  return null;
}

// 更新弹窗的内容和位置
function updateTooltip(tile, mouseX, mouseY) {
  if (!tile) {
    tooltip.style.display = 'none';
    return;
  }
  // 组装弹窗内容(可根据你的需求自定义)
  tooltip.innerHTML = `
    <strong>瓦片 ${tile.row}-${tile.col}</strong><br>
    地形:${tile.terrain}<br>
    资源量:${tile.resources}<br>
    ${tile.description}
  `;
  // 显示弹窗,位置偏移鼠标避免遮挡
  tooltip.style.display = 'block';
  tooltip.style.left = `${mouseX + 12}px`;
  tooltip.style.top = `${mouseY + 12}px`;
}

// 绑定鼠标事件
canvas.addEventListener('mousemove', (e) => {
  // 实时更新Canvas的视口位置(防止页面滚动后坐标偏移)
  canvasRect = canvas.getBoundingClientRect();
  const hoveredTile = getHoveredTile(e.clientX, e.clientY);
  updateTooltip(hoveredTile, e.clientX, e.clientY);
});

// 鼠标离开Canvas时隐藏弹窗
canvas.addEventListener('mouseleave', () => {
  tooltip.style.display = 'none';
});

// 页面加载完成后绘制瓦片
window.addEventListener('load', drawTiles);

额外优化建议

  • 瓦片图片加载:如果是用new Image()加载外部瓦片,记得在图片onload事件触发后再调用drawTiles,避免绘制空白。
  • 弹窗边界处理:如果鼠标靠近屏幕边缘,弹窗可能会超出视口,可以加个判断,当弹窗右侧/底部超出视口时,调整位置到鼠标左侧/上方。
  • 性能优化:如果瓦片数量很大,可以考虑只在鼠标移动时计算一次瓦片,避免频繁重复计算。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 08:25:44