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

求助:如何用Canvas绘制行星点的分组区域?需了解数学原理

行星点区域划分(Voronoi图)实现指南

先明确术语:Voronoi图(Voronoi Diagram)

你要做的这种按行星点划分专属区域的效果,专业名叫Voronoi图,也常被称为泰森多边形。每个行星对应一个封闭区域,区域内任意点到该行星的距离,都比到其他行星更近,那些红蓝线条就是不同区域的边界线。

背后的数学逻辑

  • 任意两条区域边界线,都是对应两个行星点的垂直平分线:边界上的每一点到这两个行星的距离完全相等
  • 整个图的所有边界,就是所有行星点两两之间垂直平分线的交集,最终围成一个个只属于单个行星的区域

在Canvas/P5.js里的实现方式

方式1:自己写核心逻辑画边界

适合想搞懂底层原理的情况,步骤很清晰:

  1. 遍历每一对行星点,计算它们的垂直平分线
  2. 把这条平分线延长到画布边缘,得到能显示在画布内的线段
  3. 用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 14:10:16