求助:如何用Canvas绘制行星点的分组区域?需了解数学原理
行星点区域划分(Voronoi图)实现指南
先明确术语:Voronoi图(Voronoi Diagram)
你要做的这种按行星点划分专属区域的效果,专业名叫Voronoi图,也常被称为泰森多边形。每个行星对应一个封闭区域,区域内任意点到该行星的距离,都比到其他行星更近,那些红蓝线条就是不同区域的边界线。
背后的数学逻辑
- 任意两条区域边界线,都是对应两个行星点的垂直平分线:边界上的每一点到这两个行星的距离完全相等
- 整个图的所有边界,就是所有行星点两两之间垂直平分线的交集,最终围成一个个只属于单个行星的区域
在Canvas/P5.js里的实现方式
方式1:自己写核心逻辑画边界
适合想搞懂底层原理的情况,步骤很清晰:
- 遍历每一对行星点,计算它们的垂直平分线
- 把这条平分线延长到画布边缘,得到能显示在画布内的线段
- 用Canvas或P5.js的画线API绘制这些线段
给你个P5.js的示例代码:
let planets = []; function setup() { createCanvas(800, 600); // 生成10个随机行星点 for (let i = 0; i < 10; i++) { planets.push(createVector(random(width), random(height))); } } function draw() { background(255); // 先画行星点 fill(0); planets.forEach(p => ellipse(p.x, p.y, 8, 8)); // 遍历所有行星对,画垂直平分线(Voronoi边界) strokeWeight(2); for (let i = 0; i < planets.length; i++) { for (let j = i + 1; j < planets.length; j++) { const p1 = planets[i]; const p2 = planets[j]; // 计算两点中点 const mid = p5.Vector.lerp(p1, p2, 0.5); // 计算垂直平分线的方向向量(原向量旋转90度) const dir = p5.Vector.sub(p2, p1).rotate(HALF_PI).normalize(); // 延长线到画布边缘,得到线段的两个端点 const lineStart = getCanvasEdgeIntersection(mid, dir, -1000); const lineEnd = getCanvasEdgeIntersection(mid, dir, 1000); // 随机红蓝颜色 stroke(random() > 0.5 ? 'red' : 'blue'); line(lineStart.x, lineStart.y, lineEnd.x, lineEnd.y); } } } // 辅助函数:计算延长线与画布边缘的交点 function getCanvasEdgeIntersection(start, dir, distance) { let end = p5.Vector.add(start, p5.Vector.mult(dir, distance)); let x = end.x; let y = end.y; // 检查并修正超出画布的坐标 if (x < 0) { y = start.y + (0 - start.x) * dir.y / dir.x; x = 0; } else if (x > width) { y = start.y + (width - start.x) * dir.y / dir.x; x = width; } if (y < 0) { x = start.x + (0 - start.y) * dir.x / dir.y; y = 0; } else if (y > height) { x = start.x + (height - start.y) * dir.x / dir.y; y = height; } return createVector(x, y); }
方式2:用现成算法库简化开发
如果不想折腾底层逻辑,直接用P5.js的社区扩展(比如p5.voronoi),这类库已经帮你处理了边界裁剪、区域闭合等复杂问题,直接调用API就能生成完整的Voronoi图。示例代码:
let voronoi; let planets = []; function setup() { createCanvas(800, 600); voronoi = new Voronoi(); // 生成行星点 for (let i = 0; i < 10; i++) { planets.push({x: random(width), y: random(height)}); } } function draw() { background(255); // 生成Voronoi图数据 const diagram = voronoi.compute(planets, {x:0, y:0, width:width, height:height}); // 绘制行星点 fill(0); planets.forEach(p => ellipse(p.x, p.y, 8, 8)); // 绘制区域边界 strokeWeight(2); diagram.edges.forEach(edge => { stroke(random() > 0.5 ? 'red' : 'blue'); line(edge.va.x, edge.va.y, edge.vb.x, edge.vb.y); }); }
注意:用这类库时,直接把库文件下载到本地项目里引入就行,不用外链。
进阶小技巧
- 如果需要给每个行星区域填充颜色,可以先获取每个区域的顶点,用
beginPath()连接所有顶点,再调用fill()上色 - 行星数量多的时候,自己写的双重循环效率会下降,建议用分治法这类高效算法优化,或者直接用成熟库
内容的提问来源于stack exchange,提问作者Baptiste Bauer
相关产品推荐
相关产品推荐

