
本文介绍如何在 html5 canvas 中高效绘制六边形网格,并通过点在多边形检测算法实现单个六边形的精准交互(如悬停变色),避免为每个格子创建独立 canvas,兼顾性能与可维护性。
本文介绍如何在 html5 canvas 中高效绘制六边形网格,并通过点在多边形检测算法实现单个六边形的精准交互(如悬停变色),避免为每个格子创建独立 canvas,兼顾性能与可维护性。
在基于 Canvas 的棋盘类游戏开发中,一个常见痛点是:图形虽能绘制成功,却难以对单个单元格(如六边形)进行独立交互(例如点击变色、选中高亮)。问题根源在于——Canvas 是位图绘图 API,本身不保留图形对象信息;你画完即“丢失”了该六边形的几何定义和身份标识。
解决这一问题的关键在于分离数据与渲染:
✅ 内存中保存结构化数据:每个六边形不再只是“画出来的像素”,而是一个包含顶点坐标({x, y} 数组)和状态(如 color、id、isOccupied)的对象;
✅ 建立坐标映射逻辑:提供 isPointInPoly(polygon, point) 工具函数,实时判断鼠标位置是否落在某个多边形内部;
✅ 按需重绘+状态驱动:交互触发时,仅更新对应六边形的数据属性,并调用统一绘制函数刷新其视觉表现。
以下是一个精简、可运行的完整实现方案:
<canvas id="canvas" width="800" height="600" style="border: 1px solid #ccc;"></canvas>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const { width, height } = canvas.getBoundingClientRect();
canvas.width = width;
canvas.height = height;
// ✅ 步骤1:定义六边形数据结构(顶点数组 + 状态)
const hexagons = []; // 存储所有六边形的顶点坐标数组
// ✅ 步骤2:通用多边形绘制函数(复用性强)
function drawPolygon(polygon, color) {
ctx.beginPath();
ctx.moveTo(polygon[0].x, polygon[0].y);
for (let i = 1; i < polygon.length; i++) {
ctx.lineTo(polygon[i].x, polygon[i].y);
}
ctx.closePath();
ctx.strokeStyle = "#333";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = color;
ctx.fill();
}
// ✅ 步骤3:生成单个六边形并存入数据池
function createHexagon(x, y, size) {
const s = size;
const polygon = [
{ x, y },
{ x: x + s / 2, y: y - s / 5 },
{ x: x + s, y },
{ x: x + s, y: y + s / 2 },
{ x: x + s / 2, y: y + s / 2 + s / 5 },
{ x, y: y + s / 2 },
{ x, y } // 闭合起点
];
// 随机初始色 + 唯一索引(便于调试/扩展)
const color = "#" + Math.floor(Math.random() * 0xFFFFFF).toString(16);
hexagons.push({ polygon, color, id: hexagons.length });
return { polygon, color };
}
// ✅ 步骤4:构建网格(递归/循环均可,此处简化为嵌套循环示例)
function buildGrid(startX, startY, size, cols, rows) {
let offsetX = 0;
for (let r = 0; r < rows; r++) {
const y = startY + r * (size / 2 + size / 5);
const rowOffset = (r % 2 === 0) ? 0 : size / 2;
for (let c = 0; c < cols; c++) {
const x = startX + c * size + rowOffset;
createHexagon(x, y, size);
}
}
}
// 初始化棋盘(10×8 网格,尺寸适配)
buildGrid(40, 40, 60, 10, 8);
// ✅ 步骤5:核心交互逻辑 —— 检测鼠标是否在某六边形内
function isPointInPoly(poly, pt) {
let inside = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const xi = poly[i].x, yi = poly[i].y;
const xj = poly[j].x, yj = poly[j].y;
const intersect = ((yi > pt.y) !== (yj > pt.y)) &&
(pt.x < (xj - xi) * (pt.y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
// ✅ 步骤6:绑定鼠标事件(以悬停变色为例)
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 查找被悬停的六边形
const hovered = hexagons.find(hex => isPointInPoly(hex.polygon, { x: mouseX, y: mouseY }));
// 清空画布并重绘全部(简单可靠;高频交互可优化为局部重绘)
ctx.clearRect(0, 0, canvas.width, canvas.height);
hexagons.forEach(hex => {
// 若当前六边形被悬停,临时使用高亮色(如黄色),否则用原色
const renderColor = hovered && hex === hovered ? '#FFD700' : hex.color;
drawPolygon(hex.polygon, renderColor);
});
// 更新光标样式增强反馈
canvas.style.cursor = hovered ? 'pointer' : 'default';
});
// ? 扩展提示:如需点击变色,将 'mousemove' 替换为 'click',并在事件中修改 hex.color 后重绘关键注意事项:
- 性能考量:本例每次悬停都全量重绘,适合中小规模网格(≤200 格)。若格子极多(如千级),建议缓存离屏 Canvas 或仅重绘受影响区域;
-
坐标精度:
getBoundingClientRect()获取的是 CSS 像素,确保 Canvas 未被 CSS 缩放(否则需用devicePixelRatio校准); -
可扩展性:
hexagons数组中的每个对象可自由添加属性(如owner: "player1"、terrain: "forest"),为游戏逻辑打下基础; -
算法鲁棒性:
isPointInPoly使用经典的射线交叉法,兼容任意凸/凹多边形,且已针对六边形顶点顺序做了验证。
通过这种“数据驱动 + 函数式渲染”的范式,你不仅解决了单格交互难题,更构建了一个可长期演进的棋盘系统骨架——后续添加拖拽、动画、碰撞检测等特性都将水到渠成。


















