棋盘类界面里,给每个格子建一个 canvas 是最容易想到的做法,代价是节点数暴涨、事件要逐个绑定。单个 canvas 承载整盘,靠坐标换算定位格子,节点只剩一个。

布局计算
const R = 24; // 外接圆半径
const W = Math.sqrt(3) * R; // 横向间距
const H = 2 * R; // 格高
const rowGap = H * 0.75; // 纵向间距
function center(col, row) {
const x = W * (col + (row % 2 ? 0.5 : 0));
const y = rowGap * row;
return { x, y };
}
尖顶六边形按行交错排布,奇数行整体右移半个格宽。绘制时按中心点依次连出六个顶点即可。
命中检测
function pointInPolygon(px, py, pts) {
let inside = false;
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
const [xi, yi] = pts[i];
const [xj, yj] = pts[j];
if ((yi > py) !== (yj > py) &&
px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) {
inside = !inside;
}
}
return inside;
}
射线法对任意多边形都成立。棋盘规模大时先按行列粗筛,只对候选格子做多边形判定,判定次数大幅下降。
三个要点
- 坐标换算要减去画布偏移。事件坐标减去
getBoundingClientRect()的结果,得到画布内坐标。 - 按 devicePixelRatio 放大位图。CSS 尺寸与位图尺寸分开设置,再用 scale 校正,否则边缘发虚。
- 重绘只清一次画布。状态变更后整体重绘,增量擦除带来的残影比全量重绘更难处理。
交互层
click 处理选中与着色,mousemove 处理悬停高亮,两者复用同一套命中逻辑。触屏环境补充 touchstart 的坐标读取,换算方式一致。
