如何用JS/HTML5 Canvas检测六边形内像素以划分陆水网格?
嘿,我来帮你搞定这个六边形网格的像素检测问题!你已经搞定了六边形的绘制,接下来核心就是判断哪些像素属于六边形内部,然后统计水域占比对吧?咱们一步步来:
第一步:改造六边形绘制函数,保存顶点信息
你的原hex函数只负责绘制,我们需要把六边形的顶点坐标存下来,这样后面才能判断点是否在内部。修改后的函数会返回顶点数组,同时别忘了闭合路径:
function hex(x = 0, y = 0, size = 10) { const vertices = []; ctx.beginPath(); for (let side = 0; side < 6; side++) { const angle = side * 2 * Math.PI / 6; const px = x + size * Math.cos(angle); const py = y + size * Math.sin(angle); vertices.push({x: px, y: py}); side === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py); } ctx.closePath(); // 闭合路径,确保填充完整 return vertices; }
第二步:判断单个像素是否在六边形内
因为六边形是凸多边形,用叉积法判断效率很高——只要点在所有边的同一侧(内部方向),就属于六边形内部。先写个叉积计算的辅助函数:
// 计算向量AB与AC的叉积,用于判断点C相对于AB的位置 function crossProduct(a, b, c) { return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); }
然后实现点在六边形内的判断函数:
function isPointInHex(point, hexVertices) { let sign = null; for (let i = 0; i < hexVertices.length; i++) { const a = hexVertices[i]; const b = hexVertices[(i + 1) % 6]; // 循环取下一个顶点 const cross = crossProduct(a, b, point); // 点刚好在边上,直接算内部 if (cross === 0) return true; // 记录第一个非零叉积的符号 if (sign === null) { sign = cross > 0 ? 1 : -1; } else if ((cross > 0 ? 1 : -1) !== sign) { // 符号不一致,说明点在外部 return false; } } return true; }
第三步:确定需要检测的像素范围
没必要遍历整个画布,只需要遍历六边形的外接矩形内的像素就行,能大幅减少计算量。写个函数获取外接矩形的边界:
function getHexBoundingBox(hexVertices) { let minX = Infinity, maxX = -Infinity; let minY = Infinity, maxY = -Infinity; hexVertices.forEach(v => { minX = Math.min(minX, v.x); maxX = Math.max(maxX, v.x); minY = Math.min(minY, v.y); maxY = Math.max(maxY, v.y); }); // 转换成整数像素坐标(像素是离散的) return { left: Math.floor(minX), right: Math.ceil(maxX), top: Math.floor(minY), bottom: Math.ceil(maxY) }; }
第四步:像素颜色检测与统计
首先要获取地图的像素数据,然后判断每个六边形内的像素是否属于水域,最后计算占比:
1. 获取地图像素数据
假设你的地图在另一个Canvas上(比如mapCanvas),先一次性获取所有像素数据:
const mapCtx = mapCanvas.getContext('2d'); const mapImageData = mapCtx.getImageData(0, 0, mapCanvas.width, mapCanvas.height); const pixelData = mapImageData.data; // 每个像素占4位:r, g, b, a
2. 获取单个像素的颜色
function getPixelColor(x, y, imageData) { const index = (y * imageData.width + x) * 4; return { r: pixelData[index], g: pixelData[index + 1], b: pixelData[index + 2], a: pixelData[index + 3] }; }
3. 判断是否为水域颜色
你需要根据自己地图的实际蓝色调调整阈值,这里给个参考:
function isWaterColor(color) { // 示例阈值:偏蓝的颜色,可根据你的地图采样调整 return color.r < 100 && color.g < 150 && color.b > 150; }
4. 统计六边形内的水域占比并着色
function processHex(x, y, size, waterThreshold = 0.5) { const hexVertices = hex(x, y, size); const bbox = getHexBoundingBox(hexVertices); let waterCount = 0; let totalCount = 0; // 遍历外接矩形内的所有像素 for (let px = bbox.left; px <= bbox.right; px++) { for (let py = bbox.top; py <= bbox.bottom; py++) { // 跳过地图外的像素 if (px < 0 || px >= mapImageData.width || py < 0 || py >= mapImageData.height) continue; // 判断像素是否在六边形内 if (isPointInHex({x: px, y: py}, hexVertices)) { totalCount++; const color = getPixelColor(px, py, mapImageData); if (isWaterColor(color)) waterCount++; } } } // 计算占比并设置颜色 const waterRatio = totalCount === 0 ? 0 : waterCount / totalCount; ctx.fillStyle = waterRatio >= waterThreshold ? '#4a90e2' : '#2ecc71'; ctx.fill(); ctx.strokeStyle = '#000'; ctx.stroke(); // 描边方便看网格 return waterRatio >= waterThreshold; }
第五步:生成整个六边形网格
六边形网格的排列有规律:奇数行的六边形会比偶数行偏移半个六边形宽度。我们来实现网格生成:
function generateHexGrid(mapWidth, mapHeight, hexSize) { const hexWidth = hexSize * Math.sqrt(3); // 六边形的宽度(两个对边的距离) const hexHeight = hexSize * 2; // 六边形的高度(两个对顶点的距离) // 遍历所有行,行间距是高度的3/4(六边形紧密排列的间距) for (let y = hexSize; y < mapHeight + hexSize; y += hexHeight * 0.75) { // 奇数行偏移半个宽度 const xOffset = Math.floor(y / (hexHeight * 0.75)) % 2 === 1 ? hexWidth / 2 : 0; // 遍历该行的所有六边形 for (let x = xOffset + hexWidth/2; x < mapWidth + hexWidth; x += hexWidth) { processHex(x, y, hexSize); } } }
一些优化小建议
- 提前获取ImageData:不要每次调用processHex都重新获取,提前获取一次能大幅提升性能。
- 调整颜色阈值:用取色器工具采样你地图里的水域颜色,精确调整isWaterColor的参数。
- 性能优化:如果网格很大,考虑用Web Worker处理像素统计,避免阻塞主线程导致页面卡顿。
- 边界处理:地图边缘的六边形可能部分超出范围,统计时记得跳过这些像素。
内容的提问来源于stack exchange,提问作者Durakken
相关产品推荐
相关产品推荐

