Работа с геометрией в браузере с помощью Geometric.js
Geometric.js добавляет в браузерные графики и карты пересечение полигонов, bounds, point-in-polygon и интерполяцию путей для SVG и Canvas.
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при менее чем трёх вершинах — этого достаточно, чтобы разместить подсказку вокруг любой фигуры.- Булевы операции, возвращающие геометрию, —
polygonIntersection,polygonUnion,polygonDifferenceиpolygonXor— требуют geometric v3 или выше; предикатpolygonIntersectsPolygonдоступен начиная с v2. - Начиная с версии 3.x, geometric поставляется с собственными объявлениями TypeScript, поэтому достаточно установить
geometricи не нужно устанавливать@types/geometric.
Примитивы — это обычные массивы, и в этом вся суть
Geometric.js удобен в использовании именно потому, что не вводит никаких структур данных. Точка — это массив [x, y], прямая — массив из двух точек, а полигон — массив вершин. Никаких пользовательских классов для инстанцирования и никаких специальных структур данных для изучения — это означает, что передаваемые значения можно сериализовать, инспектировать и отрисовывать напрямую. Поскольку результат является массивом, он без каких-либо адаптеров передаётся в D3, атрибут SVG points или цикл ctx.lineTo на Canvas:
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 под «простотой геометрии».
Установка и импорт
Discover how at OpenReplay.com.
Установите geometric из npm, pnpm или yarn; пакет поставляется с приоритетом ESM, поддержкой CommonJS и встроенными объявлениями TypeScript. Последняя версия на npm — 3.0.9 (июль 2026 года).
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+)
Полный набор булевых операций: polygonIntersection, polygonUnion, polygonDifference и polygonXor — каждая возвращает массив точек. Все четыре добавлены в v3. Для проверки вложенности, а не перекрытия, polygonInPolygon(polygonA, polygonB) возвращает булево значение, указывающее, находится ли первый полигон целиком внутри второго, при этом граничные точки считаются вложенными.
Как получить ограничивающий прямоугольник полигона?
polygonBounds(polygon) возвращает [topLeft, bottomRight] в виде двух точек [x, y] или null при менее чем трёх вершинах — этого достаточно для размещения подсказки или метки вокруг любой фигуры за один вызов. Функция игнорирует точки с недопустимыми значениями (null, undefined, NaN, Infinity), поэтому случайный пропуск в данных не испортит прямоугольник.
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
// разместить <rect> или подсказку в точке topLeft с данными шириной и высотой
Это типичная задача наложения в визуализации данных: для произвольной области на карте или графике разместить рамку метки вокруг неё, не вычисляя вручную минимум и максимум по каждой вершине.
Интерполяция: анимация вдоль пути или периметра
lineInterpolate(line) и polygonInterpolate(polygon) возвращают функцию-интерполятор, которую вызывают с параметром t из диапазона [0, 1] — именно то, что нужно для анимации маркера вдоль пути или по периметру. В текущей сборке 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]
Управляйте t из requestAnimationFrame или D3-перехода — и маркер будет плавно скользить вдоль линии. 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
У проверки принадлежности внутренней области есть одна предсказуемая слабость: клики, попадающие точно на ребро. Записи сессий при перетаскивании для выделения и выборе областей на карте нередко выявляют этот сбой: ошибка «почему моё выделение не сработало?», которую юнит-тесты пропускают, но которая очевидна при просмотре взаимодействия. Грамотное решение — pointOnPolygon(point, polygon, epsilon), который проверяет границу с необязательным допуском epsilon, например 1e-6, для устранения неоднозначности попадания на линию, которую обычная проверка внутренней области игнорирует.
| Возможность | Функция | Возвращает | Мин. версия |
|---|---|---|---|
| Проверка принадлежности точки области | pointInPolygon | boolean (трассировка лучей) | v2 |
| Проверка границы | pointOnPolygon | boolean (epsilon) | v2 |
| Ограничивающий прямоугольник | polygonBounds | [topLeft, bottomRight] или null | v2 |
| Фигура перекрытия | polygonIntersection | polygon | v3 |
| Предикат перекрытия | polygonIntersectsPolygon | boolean | v2 |
| Интерполяция пути | lineInterpolate | function (t) => [x, y] | v2 |
Обращайтесь к Geometric.js, как только в графике, карте или canvas-интерфейсе потребуется более одной геометрической операции: модель «массив на входе — массив на выходе» означает, что каждый результат напрямую передаётся в следующий вызов или в рендерер. Установите geometric, импортируйте две-три функции, необходимые для вашей задачи, и удалите тригонометрию со Stack Overflow. В справочнике API перечислен полный набор функций на случай, если понадобится четвёртая операция.
Часто задаваемые вопросы
В чём разница между polygonIntersection и polygonIntersectsPolygon в Geometric.js?
polygonIntersectsPolygon(a, b) возвращает булево значение, указывающее, перекрываются ли два полигона, тогда как polygonIntersection(a, b) возвращает саму общую область в виде нового массива полигона, который можно отрисовать. Используйте предикат для быстрой проверки «да/нет» и операцию, возвращающую геометрию, когда нужна реальная фигура перекрытия. Предикат доступен начиная с v2, но polygonIntersection требует geometric v3 или выше.
Нужно ли устанавливать @types/geometric для использования Geometric.js с TypeScript?
Нет. Начиная с версии 3.x, geometric поставляется с собственными объявлениями TypeScript, сгенерированными из исходного кода и опубликованными вместе с пакетом, поэтому установка одного лишь geometric обеспечивает автодополнение в редакторе и проверку типов. Отдельный пакет DefinitelyTyped @types/geometric заморожен на версии 2.5.3 от ноября 2023 года и избыточен при использовании v3; его установка может заменить точные встроенные типы устаревшими.
Что возвращает Geometric.js, когда точка попадает точно на ребро полигона?
pointInPolygon использует трассировку лучей для проверки принадлежности внутренней области, и точки, попадающие точно на ребро, считаются неоднозначными и могут возвращать false, что проявляется как клики, не регистрирующиеся в интерфейсах перетаскивания для выделения или выбора областей на карте. Используйте pointOnPolygon(point, polygon, epsilon) для граничных случаев; необязательный допуск epsilon, например 1e-6, определяет, насколько близко к линии считается попаданием на неё.
Можно ли напрямую отрисовывать результаты Geometric.js в SVG или Canvas без преобразования?
Да. Поскольку каждый примитив является обычным массивом JavaScript, а не экземпляром класса, массивы, возвращаемые такими функциями, как polygonRegular или polygonIntersection, напрямую передаются в атрибут SVG points или цикл Canvas ctx.lineTo без какого-либо адаптерного кода. Тот же массив, с которым вы работаете в вычислениях, — это значение, которое вы сериализуете в JSON, передаёте в D3 или отрисовываете, что и является ключевой идеей дизайна библиотеки.
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