12k
All articles

在浏览器中使用 Geometric.js 进行真实几何运算

Geometric.js 为浏览器图表和地图提供多边形相交、边界框、点在多边形内测试和路径插值,适用于 SVG 与 Canvas。

OpenReplay Team
OpenReplay Team
在浏览器中使用 Geometric.js 进行真实几何运算

Geometric.js 是一个二维几何库,其中每个基本图元都是普通的 JavaScript 数组:点表示为 [x, y],线段表示为 [[x, y], [x, y]],多边形则是一个点的数组——因此,用于计算的值与序列化为 JSON 或绘制到 SVG、Canvas 的值完全相同。

任何构建过交互式图表或地图的开发者都曾感受过这样的摩擦:每次只需要一个小小的几何运算,却不得不从头重写数学逻辑。如果你曾经手写射线投射法来进行点在多边形内的判断,或者在 D3 图表中反复粘贴包围盒计算代码,Geometric.js 可以用一组命名清晰、经过测试的函数来替代这些代码片段,并且这些函数直接操作你已有的数组。本文将介绍三项核心功能(多边形求交、包围盒和插值),以及两个使用该库优于自行编写三角函数的实际场景。

核心要点

  • Geometric.js 将点表示为 [x, y],将线段表示为两个点,将多边形表示为点的数组——无需实例化类,也无需在绘制到 SVG 或 Canvas 之前进行任何适配。
  • pointInPolygon(point, polygon) 使用射线投射法返回布尔值,让你无需任何外部几何数学即可完成一次 Canvas 命中测试。
  • polygonBounds(polygon) 返回 [topLeft, bottomRight](两个 [x, y] 点),若顶点数少于三个则返回 null——足以为任意形状定位提示框。
  • 返回几何图形的布尔运算——polygonIntersectionpolygonUnionpolygonDifferencepolygonXor——需要 geometric v3 或更高版本;polygonIntersectsPolygon 谓词从 v2 起已可使用。
  • 自 3.x 版本起,geometric 内置了 TypeScript 类型声明,因此安装 geometric 后无需再安装 @types/geometric

基本图元就是普通数组,这就是它的全部卖点

Geometric.js 之所以用起来令人愉快,是因为它不引入任何自定义数据结构。点是 [x, y] 数组,线段是包含两个点的数组,多边形是顶点数组。无需实例化自定义类,也无需学习特殊的数据结构,这意味着传入的值与可以序列化、检查和绘制的值完全相同。由于输出是数组,它可以直接传入 D3、SVG 的 points 属性或 Canvas 的 ctx.lineTo 循环,无需任何适配代码:

import { polygonRegular } from "geometric";

const pentagon = polygonRegular(5, 10000, [150, 150]);
// pentagon 是 [[x, y], [x, y], ...] — 可直接用于绘制:

// SVG
polygon.setAttribute("points", pentagon.map(p => p.join(",")).join(" "));

// Canvas
ctx.beginPath();
pentagon.forEach(([x, y], i) => (i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)));
ctx.closePath();

这种完整的流程(计算、序列化、渲染,全部基于同一个数组)正是 README 中所说的”保持几何运算简单”的含义。

安装与导入

通过 npm、pnpm 或 yarn 安装 geometric;该包以 ESM 为主,同时支持 CommonJS,并内置 TypeScript 类型声明。npm 上的最新版本为 3.0.9(2026 年 7 月)。

npm i geometric
# 或:pnpm add geometric
# 或:yarn add geometric
import { pointInPolygon, polygonBounds } from "geometric"; // ESM
const geometric = require("geometric");                     // CommonJS

关于配置有两点说明。第一,TypeScript 类型声明由源码生成并随包一同发布,因此不应安装 @types/geometric。该 DefinitelyTyped 包已冻结在 2.5.3 版本,在 v3 中是多余的。第二,下文介绍的返回几何图形的布尔运算需要 geometric@^3;在 v2 中它们返回 undefined

多边形求交与布尔运算

对于重叠检测,polygonIntersection(a, b) 将共享区域作为新多边形返回,而 polygonIntersectsPolygon(a, b) 返回布尔值。谓词函数用于低成本的检测;返回几何图形的操作则给出可供渲染的实际形状。

import { polygonIntersection, polygonIntersectsPolygon } from "geometric";

const a = [[0, 0], [4, 0], [4, 4], [0, 4]];
const b = [[2, 2], [6, 2], [6, 6], [2, 6]];

polygonIntersectsPolygon(a, b); // true — 布尔谓词(v2+)
polygonIntersection(a, b);      // 重叠区域作为新多边形返回(v3+)

完整的布尔运算集包括 polygonIntersectionpolygonUnionpolygonDifferencepolygonXor,每个函数均返回点数组。这四个函数均为 v3 新增功能。对于包含关系而非重叠关系的判断,polygonInPolygon(polygonA, polygonB) 返回布尔值,表示第一个多边形是否完全位于第二个多边形内部,边界上的点被视为已包含。

如何获取多边形的包围盒?

polygonBounds(polygon) 返回 [topLeft, bottomRight](两个 [x, y] 点),若顶点数少于三个则返回 null,只需一次调用即可为任意形状定位提示框或标签框。该函数会忽略包含无效值(nullundefinedNaNInfinity)的点,因此数据中偶发的缺口不会破坏包围盒的计算结果。

import { polygonBounds } from "geometric";

const region = [[12, 8], [40, 20], [30, 44], [6, 30]];
const [topLeft, bottomRight] = polygonBounds(region);
// topLeft = [6, 8], bottomRight = [40, 44]

const width = bottomRight[0] - topLeft[0];   // 34
const height = bottomRight[1] - topLeft[1];  // 36
// 以 topLeft 为起点,使用此宽高定位 <rect> 或提示框

这是数据可视化中常见的叠加层任务:给定地图或图表上的任意区域,无需手动遍历每个顶点计算最小/最大值,即可在其周围放置标签框。

插值:沿路径或周长进行动画

lineInterpolate(line)polygonInterpolate(polygon) 返回一个插值函数,调用时传入 [0, 1] 范围内的 t 值,这正是沿路径或周长为标记添加动画所需的形式。在当前 3.x 版本中,clamp 默认为 true,将输出限制在线段范围内;显式传入该参数可使意图更加清晰。

import { lineInterpolate } from "geometric";

const path = [[0, 0], [100, 50]];
const at = lineInterpolate(path, true); // clamp = true

at(0);   // [0, 0]
at(0.5); // [50, 25]
at(1);   // [100, 50]

通过 requestAnimationFrame 或 D3 过渡驱动 t 值,即可实现标记沿线段平滑滑动的效果。polygonInterpolate 对闭合周长执行相同操作,适用于描绘轮廓或让点沿形状边缘移动。

如何判断点是否在多边形内部?

要判断点击位置是否落在某个区域内,pointInPolygon(point, polygon) 使用射线投射法返回布尔值,无需任何外部几何数学。这是经典的 Canvas 命中测试,将十几行手写的边缘穿越逻辑压缩为一次经过测试的函数调用。

import { pointInPolygon, pointOnPolygon } from "geometric";

const region = [[0, 0], [100, 0], [100, 100], [0, 100]];

pointInPolygon([50, 50], region);  // true
pointInPolygon([150, 50], region); // false

内部测试有一个可预见的弱点:点击恰好落在边上的情况。在拖拽选择和地图区域选择器的会话回放中,这种失败模式频繁出现:一个”为什么我的选择没有生效?“的 bug,单元测试无法发现,但观看交互过程时一目了然。精细的解决方案是 pointOnPolygon(point, polygon, epsilon),它通过可选的 epsilon 容差(例如 1e-6)来测试边界,处理纯内部检测会忽略的边界模糊情况。

功能函数返回值最低版本
点在区域内的判断pointInPolygon布尔值(射线投射法)v2
边界测试pointOnPolygon布尔值(epsilon 容差)v2
包围盒polygonBounds[topLeft, bottomRight]nullv2
重叠形状polygonIntersection多边形v3
重叠谓词polygonIntersectsPolygon布尔值v2
路径插值lineInterpolate函数 (t) => [x, y]v2

当图表、地图或 Canvas UI 需要多个几何运算时,就应该考虑使用 Geometric.js:数组输入、数组输出的模型意味着每个结果都可以直接传入下一次调用或渲染器。安装 geometric,导入功能所需的两三个函数,删掉那些从 Stack Overflow 复制来的三角函数代码。当需要第四个运算时,API 参考文档列出了完整的函数集。

常见问题

Geometric.js 中 polygonIntersection 和 polygonIntersectsPolygon 有什么区别?

polygonIntersectsPolygon(a, b) 返回布尔值,表示两个多边形是否重叠;而 polygonIntersection(a, b) 返回共享区域本身,作为可供渲染的新多边形数组。需要低成本的是/否命中检测时使用谓词函数,需要实际重叠形状时使用返回几何图形的操作。谓词函数从 v2 起已可使用,但 polygonIntersection 需要 geometric v3 或更高版本。

在 TypeScript 中使用 Geometric.js 是否需要安装 @types/geometric?

不需要。自 3.x 版本起,geometric 内置了由源码生成并随包发布的 TypeScript 类型声明,因此仅安装 geometric 即可获得编辑器自动补全和类型检查。独立的 DefinitelyTyped 包 @types/geometric 已冻结在 2023 年 11 月的 2.5.3 版本,在 v3 中是多余的;安装它可能会用过时的类型声明覆盖准确的内置类型。

当点恰好落在多边形边上时,Geometric.js 会返回什么?

pointInPolygon 使用射线投射法测试内部,恰好落在边上的点被视为模糊情况,可能返回 false,这在拖拽选择或地图区域 UI 中会表现为点击无法注册的问题。对于边界情况,请使用 pointOnPolygon(point, polygon, epsilon);可选的 epsilon 容差(例如 1e-6)控制距离线段多近算作“在线上”。

Geometric.js 的输出是否可以直接绘制到 SVG 或 Canvas,无需转换?

可以。由于每个基本图元都是普通的 JavaScript 数组而非类实例,polygonRegular 或 polygonIntersection 等函数返回的数组可以直接传入 SVG 的 points 属性或 Canvas 的 ctx.lineTo 循环,无需任何适配代码。用于计算的数组与序列化为 JSON、传入 D3 或用于渲染的值完全相同,这正是该库的核心设计理念。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.