js多边形算法:获取多边形中心点,且必定在多边形内部

前言

已有《js多边形算法:多边形缩放、获取中心、获取重心/质心、判断是否在多边形内、判断点排序是否顺时针等》,可以参考;

现有的获取多边形中心点,中心点不一定在多边形内部,例如凹多边形;

现在需要获取中心点,如果中心点不在多边形内部,则返回一个尽可能在中心点附近且在多边形内部点;

思路

1、获取多边形中心点,如果在多边形内部则返回中心点;

2、否则,获取多边形包围盒中心点,如果在多边形内部则返回包围盒中心点;

3、否则,从包围盒中心点按"包围盒中心指向多边形中心、垂直向上、垂直向下、水平向左、水平向右"顺序发射射线,获取射线和多边形的所有交点,穷尽所有交点组成的线段,若线段中心点在多边形内部则返回中心点;

例子

如上图,红色块为多边形,绿色边框为包围盒,红色的多边形中心点不在多边形内部,包围盒中心点也不在多边形内部,这时候从包围盒中心点垂直向下发射射线,和多边形相交2个点,再取2个交点的中心点作为多边形内部中心点!

页面效果

源码

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Polygon Center Point</title>
    <style>
      body {
        text-align: center;
      }
      canvas {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <h2>获取多边形中心点,必定在多边形内部</h2>
    <h3>-大话主席 SuperSlide2.com</h3>
    <canvas id="canvas" width="500" height="500"></canvas>
    <div>操作:画布上点击鼠标绘制多边形</div>
    <div>
      <span style="color: red">红色为多边形中心点</span>,<span
        style="color: green"
        >绿色为包围盒中心点</span
      >,<span style="color: blue">蓝色为在多边形内部中心点</span>
    </div>
    <div><button id="clearButton">重新绘制</button></div>
    <div id="curPoint"></div>

    <script>
      const canvas = document.getElementById("canvas");
      const ctx = canvas.getContext("2d");
      const clearButton = document.getElementById("clearButton");

      let polygon = []; // 多边形的点

      // 绘制点
      function drawPoint(center, color = "blue", radius = 5) {
        ctx.beginPath();
        ctx.arc(center.x, center.y, radius, 0, Math.PI * 2);
        ctx.fillStyle = color;
        ctx.fill();
      }

      // 绘制多边形
      function drawPolygon(
        polygon,
        color = "red",
        fillColor = "rgba(255, 0, 0, 0.2)"
      ) {
        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 = color;
        ctx.stroke();

        if (fillColor) {
          ctx.fillStyle = fillColor;
          ctx.fill();
        }
      }

      // 绘制多边形顶点
      function drawPolygonPoints(polygon) {
        for (let i = 0; i < polygon.length; i++) {
          drawPoint(polygon[i], "red", 3);
        }
      }

      // 绘制包围盒
      function drawBoundingBox(polygon) {
        const box = getBoundingBox(polygon);
        drawPolygon(box, "green");
      }

      // 计算多边形的中心点(质心)
      function getPolygonCenter(points) {
        if (!Array.isArray(points) || points.length < 3) {
          console.error("多边形坐标集合不能少于3个");
          return;
        }
        const result = { x: 0, y: 0 };
        let area = 0;
        for (let i = 1; i <= points.length; i++) {
          const curX = points[i % points.length].x;
          const curY = points[i % points.length].y;
          const nextX = points[i - 1].x;
          const nextY = points[i - 1].y;
          const temp = (curX * nextY - curY * nextX) / 2;
          area += temp;
          result.x += (temp * (curX + nextX)) / 3;
          result.y += (temp * (curY + nextY)) / 3;
        }
        result.x /= area;
        result.y /= area;
        return result;
      }

      // 判断点是否在多边形内
      function isPointInPolygon(point, polygon) {
        let inside = false;
        for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
          const xi = polygon[i].x,
            yi = polygon[i].y;
          const xj = polygon[j].x,
            yj = polygon[j].y;
          const intersect =
            yi > point.y !== yj > point.y &&
            point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
          if (intersect) inside = !inside;
        }
        return inside;
      }

      /**
       * 获取多边形的包围盒中心点
       * @param {Array} polygon 多边形点集合
       * @return {Object} 包围盒中心点
       */
      function getBoundingBoxCenter(polygon) {
        const box = getBoundingBox(polygon);
        return {
          x: (box[0].x + box[2].x) / 2,
          y: (box[0].y + box[2].y) / 2,
        };
      }

      /**
       * 获取多边形包围盒坐标集合,返回[左上, 右上, 右下, 左下]
       * @param {Array<Point>} polygon 多边形点数组
       * @returns {Array<Point>} 包围盒点数组
       */
      function getBoundingBox(polygon) {
        let minX = Infinity,
          maxX = -Infinity,
          minY = Infinity,
          maxY = -Infinity;

        polygon.forEach((p) => {
          minX = Math.min(minX, p.x);
          maxX = Math.max(maxX, p.x);
          minY = Math.min(minY, p.y);
          maxY = Math.max(maxY, p.y);
        });

        return [
          { x: minX, y: minY },
          { x: maxX, y: minY },
          { x: maxX, y: maxY },
          { x: minX, y: maxY },
        ];
      }

      /**
       * 获取多边形中心点,必定在多边形内部
       * 包围盒中心点按顺序从垂直向上下左右发射射线,取射线和多边形交点成线段的中心点坐标
       * @param {Array<Point>} polygon 多边形顶点数组
       * @returns {Point} 中心点
       */
      function getPolygonCenterInPolygon(polygon) {
        const center = getPolygonCenter(polygon);
        if (isPointInPolygon(center, polygon)) {
          console.log("质心在多边形内,返回质心");
          return center;
        }

        const boxCenter = getBoundingBoxCenter(polygon);
        if (isPointInPolygon(boxCenter, polygon)) {
          console.log("包围盒中心在多边形内,返回包围盒中心");
          return boxCenter;
        }

        const directions = [
          { x: center.x - boxCenter.x, y: center.y - boxCenter.y }, // 质心指向包围盒中心方向
          { x: 0, y: -1 }, // 垂直向上
          { x: 0, y: 1 }, // 垂直向下
          { x: -1, y: 0 }, // 水平向左
          { x: 1, y: 0 }, // 水平向右
        ];

        const intersections = [];
        for (let d = 0; d < directions.length; d++) {
          const direction = directions[d];
          const rayStart = boxCenter;
          const rayEnd = {
            x: rayStart.x + direction.x * 9999,
            y: rayStart.y + direction.y * 9999,
          };

          const interPoints = getLinePolygonIntersections(polygon, [
            rayStart,
            rayEnd,
          ]);

          if (interPoints.length < 2) continue;

          // 遍历所有交点组成的线段,取线段中心点,若中心点在多边形内则返回中心点
          for (let i = 0; i < interPoints.length; i++) {
            for (let j = i + 1; j < interPoints.length; j++) {
              // 取两个交点作为线段
              const p1 = interPoints[i];
              const p2 = interPoints[j];

              const midpoint = {
                x: (p1.x + p2.x) / 2,
                y: (p1.y + p2.y) / 2,
              };

              if (isPointInPolygon(midpoint, polygon)) {
                console.log("返回线段交点的中点", midpoint);
                return midpoint;
              }
            }
          }
        }
        return null;
      }

      /**
       * 获取线段和多边形的交点集合
       * @param {Array<Point>} polygon 多边形顶点数组
       * @param {Array<Point>} line 线段,[开始点,结束点]
       * @returns {Array<Point>} 交点数组
       */
      function getLinePolygonIntersections(polygon, line) {
        const intersections = [];

        const lineStart = line[0];
        const lineEnd = line[1];

        const n = polygon.length;

        for (let i = 0; i < n; i++) {
          const p1 = polygon[i];
          const p2 = polygon[(i + 1) % n];
          const intersection = get2LinesIntersection(
            p1,
            p2,
            lineStart,
            lineEnd
          );
          if (intersection) {
            intersections.push(intersection);
          }
        }
        // 去重
        return uniqueArray(intersections);
      }

      /** 数组去重,高性能
       * @param {Array} arr 数组,支持数字、对象、字符串
       */
      function uniqueArray(arr) {
        return Array.from(new Set(arr.map((item) => JSON.stringify(item)))).map(
          (item) => JSON.parse(item)
        );
      }

      /**
       * 获取两条线段交点
       * @param {Point} P1 线段1的起点
       * @param {Point} P2 线段1的终点
       * @param {Point} P3 线段2的起点
       * @param {Point} P4 线段2的终点
       * @return {Point | null} 交点
       */
      function get2LinesIntersection(P1, P2, P3, P4) {
        const denom =
          (P2.x - P1.x) * (P4.y - P3.y) - (P2.y - P1.y) * (P4.x - P3.x);

        if (denom === 0) return null; // 两线段平行或重合

        const t =
          ((P3.x - P1.x) * (P4.y - P3.y) - (P3.y - P1.y) * (P4.x - P3.x)) /
          denom;
        const s =
          ((P3.x - P1.x) * (P2.y - P1.y) - (P3.y - P1.y) * (P2.x - P1.x)) /
          denom;

        if (t >= 0 && t <= 1 && s >= 0 && s <= 1) {
          return { x: P1.x + t * (P2.x - P1.x), y: P1.y + t * (P2.y - P1.y) };
        }

        return null;
      }

      // 清空画布
      function clearCanvas() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        polygon = [];
      }

      // 处理点击事件
      canvas.addEventListener("click", (e) => {
        const x = e.offsetX;
        const y = e.offsetY;
        polygon.push({ x, y });
        drawAll();
      });

      // 处理点击事件
      canvas.addEventListener("mousemove", (e) => {
        const x = e.offsetX;
        const y = e.offsetY;
        document.getElementById("curPoint").innerText = `${x},${y}`;
      });

      function drawAll() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawPolygonPoints(polygon);
        if (polygon.length < 3) return;
        drawPolygon(polygon);

        // 默认中心点
        const polygonCenter = getPolygonCenter(polygon);
        drawPoint(polygonCenter, "red");

        // 包围盒中心点
        const boxCenter = getBoundingBoxCenter(polygon);
        drawPoint(boxCenter, "green");

        // 包围盒
        drawPolygon(getBoundingBox(polygon), "green", null);

        // 必在多边形内部的中心点
        const intersectionCenter = getPolygonCenterInPolygon(polygon);
        drawPoint(intersectionCenter, "blue");
      }

      // 清除画布按钮
      clearButton.addEventListener("click", () => {
        clearCanvas();
      });
    </script>
  </body>
</html>

五、点赞

如果帮到您,点个赞再走!

相关推荐
前端Hardy16 分钟前
10 分钟搞定婚礼小程序?我用 DeepSeek 把同学的作业卷成了范本!
前端·javascript·微信小程序
Tminihu18 分钟前
前端大文件上传的时候,采用切片上传的方式,如果断网了,应该如何处理
前端·javascript
颜酱20 分钟前
理解vue3中的compiler-core
前端·javascript·vue.js
果粒chenl31 分钟前
06-原型和原型链
前端·javascript·原型模式
Entropy-Lee32 分钟前
JavaScript语法、关键字和变量
开发语言·javascript·ecmascript
谢尔登33 分钟前
【JavaScript】手写 Object.prototype.toString()
前端·javascript·原型模式
一块plus40 分钟前
1,000 万 DOT 奖励,JAM Prize 邀你共建 Polkadot 下一代基础设施!
javascript·后端·github
爱吃芒果的蘑菇42 分钟前
Python读取获取波形图波谷/波峰
python·算法
李大玄43 分钟前
一个轻量级、无依赖的 Loading 插件 —— @lijixuan/loading
前端·javascript·vue.js
只与明月听44 分钟前
深入Chrome DevTools Memory面板:Web内存分析
javascript·面试·html