If you have ever found yourself reimplementing the same little geometry helpers — the area of a polygon, the distance between two points, "is this click inside that shape?" — over and over across projects, Geometric.js is the library that quietly ends that cycle. It is a small, dependency-free JavaScript toolkit for doing 2D geometry: translating, rotating, and scaling shapes; measuring lengths, areas, and angles; testing point-in-polygon; computing convex hulls; running boolean operations on polygons; and a good deal more.
What makes it pleasant to use is what it deliberately leaves out. There are no custom classes, no wrapper objects, no new Point(). A point is just an [x, y] array, a line is two of those, and a polygon is a list of them. Because everything is plain data, the library composes effortlessly with d3, SVG, Canvas, JSON storage, and any other code that already speaks coordinate arrays. It comes from the data-visualization world, but it is pure math — equally at home in Node, the browser, or any JavaScript runtime.
Why Plain Arrays Change Everything
The defining decision behind geometric is that it never asks you to adopt its data model. Most geometry libraries hand you Point, Segment, and Polygon classes, then expect you to marshal your data in and your results back out. Geometric.js skips all of that:
- A point is
[x, y]. - A line is
[[x0, y0], [x1, y1]]. - A polygon is
[[x0, y0], [x1, y1], [x2, y2], ...]. - An angle is just a number, in degrees.
This pays off immediately. The values you pass in are the same values you can serialize to JSON, draw to a Canvas, feed into a d3 path generator, or hand to another library. Every function takes plain data and returns plain data, with no hidden state and no mutation — so the output of one call flows straight into the next. It is functional and composable by nature, not by convention.
The API reflects this with flat, namespaced function names like polygonScale, lineRotate, and pointInPolygon. You can import the whole namespace or cherry-pick the functions you need, since it ships as tree-shakeable ES modules with TypeScript declarations included.
What's In the Box
The surface area is broad for such a small package. Points support rotation and translation. Lines cover angle, length, midpoint, rotation, translation, and interpolation. Polygons are the richest category — area, bounds, centroid, perimeter, convex hull, regular and random polygon generation, scaling (including scaling by area), reflection, winding-order normalization, and a full set of boolean operations: polygonUnion, polygonIntersection, polygonDifference, and polygonXor.
On top of those, there is a generous set of relationship and hit-testing helpers — lineIntersection, pointInPolygon, pointOnLine, pointLeftofLine, pointToPolygon (the closest point on a perimeter), polygonInPolygon, and more. Many of these accept an optional epsilon tolerance, a thoughtful touch for working with floating-point coordinates where exact equality is unreliable. Rounding out the kit are angle utilities including angleReflect for bouncing-ball and light-ray effects, plus angleToRadians and angleToDegrees converters.
Getting It Installed
Geometric.js has zero runtime dependencies and weighs in at just a few kilobytes.
npm install geometric
yarn add geometric
Then import the whole namespace, or just the functions you want:
import * as geometric from "geometric";
// or cherry-pick for smaller bundles
import { polygonArea, pointInPolygon } from "geometric";
There is also a UMD build available over a CDN, which exposes a global geometric via a script tag — handy for quick prototypes or Observable notebooks.
First Steps With Shapes
Measuring and Moving Points
The point functions are the smallest building blocks, and they read exactly as they describe. pointTranslate moves a point a given distance along a given angle, and pointRotate spins one around an origin.
import { pointTranslate, pointRotate } from "geometric";
// Move a point 100px at a 45° angle from the origin
pointTranslate([0, 0], 45, 100);
// → [70.71..., 70.71...]
// Rotate a point 90° around [0, 0]
pointRotate([10, 0], 90, [0, 0]);
// → [0, 10] (approximately)
Angles are always in degrees, which tends to match how people think about rotation when laying out a UI or a chart. If you need radians for trigonometry elsewhere, angleToRadians and angleToDegrees bridge the gap.
Working With Polygons
Polygons are where the library earns its keep. Here are the everyday measurements you reach for constantly in data visualization — area, centroid, and a containment test — each a single call.
import { polygonArea, polygonCentroid, pointInPolygon } from "geometric";
const square = [
[0, 0],
[0, 10],
[10, 10],
[10, 0],
];
polygonArea(square); // → 100
polygonCentroid(square); // → [5, 5]
pointInPolygon([5, 5], square); // → true
The centroid is the true, area-weighted centroid — perfect for dropping a text label in the visual center of an irregular shape. If you only need the cheaper arithmetic mean of the vertices, polygonMean is there too, and it is worth knowing the two are not the same for non-symmetric shapes.
Generating Shapes on the Fly
You do not always have coordinates to start from. polygonRegular builds a regular polygon from a side count, area, and center, while polygonRandom produces a random convex polygon — both useful for procedural art, placeholder geometry, or test fixtures.
import { polygonRegular, polygonHull } from "geometric";
// A pentagon with area 200, centered at [50, 50]
const pentagon = polygonRegular(5, 200, [50, 50]);
// Wrap a scatter of points in their convex hull
const hull = polygonHull([
[0, 0],
[5, 5],
[10, 0],
[5, 2],
[3, 8],
]);
polygonHull is the classic "shrink-wrap these points" operation, ideal for outlining a cluster of data points or building a clickable region around a group of markers.
Going Further
Interpolators That Return Functions
One of the more d3-flavored touches is that lineInterpolate and polygonInterpolate do not return a point — they return a function that maps a parameter t in [0, 1] to a position along the path. This makes animating a marker along a route, or distributing items evenly around a perimeter, almost trivial.
import { lineInterpolate, polygonInterpolate } from "geometric";
const along = lineInterpolate([
[0, 0],
[100, 0],
]);
along(0); // → [0, 0]
along(0.5); // → [50, 0]
along(1); // → [100, 0]
// Place 12 evenly spaced points around a polygon's perimeter
const aroundEdge = polygonInterpolate(pentagon);
const dots = Array.from({ length: 12 }, (_, i) =>
aroundEdge(i / 12)
);
Because the interpolator is a plain closure, you can hand t straight from a d3 transition, a requestAnimationFrame clock, or a scroll-progress value, and animate geometry without any framework glue.
Boolean Operations and Hit-Testing
For interactive maps, games, and editors, the relationship functions are the stars. You can combine two polygons, find where a line crosses a shape, or snap a cursor to the nearest point on a boundary.
import {
polygonUnion,
polygonIntersection,
pointToPolygon,
angleReflect,
} from "geometric";
const a = [[0, 0], [0, 10], [10, 10], [10, 0]];
const b = [[5, 5], [5, 15], [15, 15], [15, 5]];
polygonUnion(a, b); // the merged outline of both squares
polygonIntersection(a, b); // just the overlapping region
// Snap a stray point onto the closest spot on a polygon's edge
pointToPolygon(a, [12, 3]); // → nearest point on the perimeter
// Bounce an incoming ray off a surface (incidence angle, surface angle)
angleReflect(45, 0); // → reflection angle, great for bouncing balls or light
pointInPolygon paired with polygonIntersection covers a surprising amount of collision detection, while angleReflect handles the ricochet math for bouncing-ball physics or simple ray reflections without any trigonometry on your part.
Putting It Together: A Clickable SVG Region
Because everything is just arrays, wiring Geometric.js into a render layer is almost no work. Here a polygon is drawn to SVG and a click is tested against it — the same array feeds both the rendering and the math.
import { pointInPolygon, polygonCentroid } from "geometric";
const region = [
[40, 20],
[120, 40],
[100, 120],
[30, 90],
];
const points = region.map((p) => p.join(",")).join(" ");
const svg = `<polygon points="${points}" fill="#88c" />`;
function handleClick(event: MouseEvent) {
const click: [number, number] = [event.offsetX, event.offsetY];
if (pointInPolygon(click, region)) {
const [cx, cy] = polygonCentroid(region);
console.log(`Hit! Label this region near ${cx}, ${cy}.`);
}
}
No conversion step, no adapter, no class instances — the coordinates you draw are the coordinates you test.
Where It Fits Among the Alternatives
It helps to know when geometric is the right reach and when something else fits better. If you are working with real-world latitude and longitude on a sphere, Turf.js (@turf/turf) is the heavyweight choice — it operates on GeoJSON and does geodesic math, at the cost of being far larger and more ceremonious. Geometric.js is for plane geometry on bare arrays: lighter, simpler, and free of GeoJSON overhead.
d3's own d3-polygon shares the exact same plain-array philosophy but offers a much smaller function set; Geometric.js is essentially a superset that adds points, lines, angles, transforms, booleans, and interpolators. For rigorous, edge-case-hardened boolean clipping specifically, dedicated libraries like polygon-clipping go deeper. And if you actively want a full object model with Point and Segment classes and affine transforms, @flatten-js/core is the opposite design philosophy — pick it when an object model is what you are after.
The Takeaway
Geometric.js is the "geometry, but keep it simple" option. It is mature and stable, zero-dependency, a few kilobytes, and ships its own TypeScript types. By choosing plain coordinate arrays over a class hierarchy, it stays out of your way: shapes are just data you can serialize, draw, store, and compose. For d3 and Canvas data visualization, screen-space games, generative art, and lightweight 2D mapping where a full GIS stack would be overkill, it hits a sweet spot that is genuinely hard to beat. The next time you catch yourself writing a point-in-polygon test by hand, reach for this instead.