A collection of functions for working with points in a 2D coordinate system, along with additional utility functions.
- What's new
- How to use — install, imports, develops, publishes
- Examples
- Point class
- Vector class
- Function reference
- Changelog
- New
vectorsdomain —magnitude,normalize,dotProduct,crossProduct,add,subtract,multiply,project,reflect,perpendicular,fromAngle, plus a chainableVectorclass. See Vector operations.
Upgrading from 2.3.x? Version 2.4.0 changed angle's behavior for any point1 not at the origin (it was returning incorrect results), and pointWithoutCollision now returns Point | null instead of Point | string — see CHANGELOG.
See CHANGELOG.md for full notes.
Use Node 18+. The repo pins Node 20 for development (.nvmrc — nvm use recommended).
npm:
npm install pointscapepnpm / Yarn:
pnpm add pointscape
# or
yarn add pointscapeMost functions work with plain { x, y } objects — no class allocation needed.
import { collision, collisionInArray } from "pointscape";
const player = { x: 0, y: 0 };
const enemy = { x: 3, y: 4 }; // exactly 5 units away
collision(player, enemy, 5); // true — within radius
collision(player, enemy, 4); // false — outside radius
// Optional callback fires on hit
collision(player, enemy, 5, () => console.log("hit!"));
// Batch: all targets within radius 3 of origin
const targets = [
{ x: 1, y: 0 },
{ x: 5, y: 0 },
{ x: 3, y: 0 },
];
collisionInArray({ x: 0, y: 0 }, targets, 3);
// [{ x: 1, y: 0 }, { x: 3, y: 0 }]import { nearest, farthest } from "pointscape";
const origin = { x: 0, y: 0 };
const pts = [
{ x: 5, y: 0 },
{ x: 1, y: 0 },
{ x: 10, y: 0 },
];
nearest(origin, pts); // { x: 1, y: 0 }
farthest(origin, pts); // { x: 10, y: 0 }
nearest(origin, []); // nullimport { move, lerp } from "pointscape";
move({ x: 5, y: 5 }, 3, -2);
// { x: 8, y: 3 }
// 25 % of the way from A to B — useful for smooth animation
lerp({ x: 0, y: 0 }, { x: 20, y: 40 }, 0.25);
// { x: 5, y: 10 }
// Midpoint (equivalent to middle())
lerp({ x: 0, y: 0 }, { x: 20, y: 40 }, 0.5);
// { x: 10, y: 20 }import { distance, middle, angle, center, perimeter } from "pointscape";
const a = { x: 0, y: 0 };
const b = { x: 10, y: 10 };
distance(a, b); // 14.142135623730951
middle(a, b); // { x: 5, y: 5 }
angle(a, b); // 0.7853981633974483 (π/4 radians)
const points = [a, b, { x: 0, y: 10 }, { x: 10, y: 0 }];
center(points); // { x: 5, y: 5 }
perimeter(points); // 48.2842712474619Argument order changed in 2.3.0 — points array is always first.
import { rotate, scale, degreesToRadians } from "pointscape";
const box = [
{ x: 10, y: 0 },
{ x: 10, y: 10 },
{ x: 0, y: 10 },
{ x: 0, y: 0 },
];
// Rotate 90 ° around the origin
rotate(box, { x: 0, y: 0 }, degreesToRadians(90));
// Each vertex rotated — e.g. { x: 10, y: 0 } becomes { x: 0, y: 10 }
// Double width, halve height
scale(box, 2, 0.5);
// [{ x: 20, y: 0 }, { x: 20, y: 5 }, { x: 0, y: 5 }, { x: 0, y: 0 }]import { area, triangleArea, circleArea } from "pointscape";
// Polygon area — rectangle 4 × 3
area([
{ x: 0, y: 0 },
{ x: 4, y: 0 },
{ x: 4, y: 3 },
{ x: 0, y: 3 },
]);
// 12
triangleArea({ x: 0, y: 0 }, { x: 6, y: 0 }, { x: 3, y: 4 });
// 12
circleArea(5);
// 78.53981633974483import { square, rectangle, triangle, pentagon } from "pointscape";
// 4 vertices of a 10-unit square (clockwise from origin)
square({ x: 0, y: 0 }, 10);
// [{ x:0,y:0 }, { x:10,y:0 }, { x:10,y:-10 }, { x:0,y:-10 }]
// Rectangle 10 wide × 5 tall
rectangle({ x: 0, y: 0 }, 10, 5);
// [{ x:0,y:0 }, { x:10,y:0 }, { x:10,y:-5 }, { x:0,y:-5 }]
// Triangle vertices, size 10, default direction "left"
triangle({ x: 0, y: 0 }, 10);
// Pentagon centred at (50, 50) with radius 30
pentagon({ x: 50, y: 50 }, 30); // 5 evenly-spaced verticesThe Point class wraps any { x, y } and exposes all geometric operations as chainable methods. Every method that returns a point returns a Point instance, so calls can be chained.
import { Point } from "pointscape";
const a = new Point(10, 20);
const b = new Point(50, 80);
// Basic methods
a.distanceTo(b); // 72.11...
a.angleTo(b); // number in radians
a.equals(b); // false
a.clone(); // Point(10, 20)
a.toString(); // "Point(10, 20)"
// Factory — convert a plain { x, y } to a Point instance
const c = Point.from({ x: 0, y: 0 });
// Chainable — move() and lerp() return Point instances
a.move(5, -5) // Point(15, 15)
.lerp(b, 0.5) // Point(32.5, 47.5)
.distanceTo(b); // number
// Rotate a point around a center
a.rotateAround({ x: 0, y: 0 }, Math.PI); // Point(-10, -20)
// Array-based methods — return Point or Point | null
a.nearestFromPoints([b, c]); // Point | null
a.farthestFromPoints([b, c]); // Point | nullPlain { x: number; y: number } objects work for every function — you don't need to allocate a Point instance unless you want the method API.
Point and Vector share the same { x, y } shape, but Vector represents a direction + magnitude (velocity, displacement, a surface normal) rather than a position — that's why it gets its own arithmetic.
import {
magnitude,
normalize,
dotProduct,
crossProduct,
add,
subtract,
multiply,
project,
reflect,
perpendicular,
fromAngle,
} from "pointscape";
const velocity = { x: 3, y: 4 };
magnitude(velocity); // 5
normalize(velocity); // { x: 0.6, y: 0.8 }
add({ x: 1, y: 2 }, { x: 3, y: 4 }); // { x: 4, y: 6 }
subtract({ x: 5, y: 7 }, { x: 2, y: 3 }); // { x: 3, y: 4 }
multiply(velocity, 2); // { x: 6, y: 8 }
dotProduct({ x: 1, y: 0 }, { x: 0, y: 1 }); // 0 — perpendicular vectors
crossProduct({ x: 2, y: 3 }, { x: 4, y: 5 }); // -2
// The displacement between two points is just a subtraction
const target = { x: 10, y: 10 };
const origin = { x: 0, y: 0 };
const heading = subtract(target, origin); // { x: 10, y: 10 }
// Bounce a velocity off a surface with the given unit normal
reflect({ x: 1, y: -1 }, { x: 0, y: 1 }); // { x: 1, y: 1 }
// Build a velocity vector from a heading angle + speed
fromAngle(Math.PI / 2, 5); // { x: ~0, y: 5 }
perpendicular({ x: 1, y: 0 }); // { x: 0, y: 1 } — 90° counter-clockwise
project({ x: 3, y: 4 }, { x: 1, y: 0 }); // { x: 3, y: 0 }The Vector class mirrors Point, but exposes arithmetic operations as chainable methods.
import { Vector } from "pointscape";
const a = new Vector(3, 4);
const b = new Vector(1, 0);
a.magnitude(); // 5
a.normalize(); // Vector(0.6, 0.8)
a.dot(b); // 3
a.cross(b); // -4
a.equals(b); // false
// Chainable — arithmetic methods return Vector instances
a.add(b).multiply(2).magnitude(); // number
// Factories
Vector.from({ x: 0, y: 0 });
Vector.fromAngle(Math.PI / 2, 5); // Vector(~0, 5)import { inLine, cross, positionInCircle } from "pointscape";
// Is (5, 5) on the line from (0, 0) to (10, 10)?
inLine({ x: 5, y: 5 }, { start: { x: 0, y: 0 }, end: { x: 10, y: 10 } });
// true
// Do two diagonals intersect?
cross(
{ start: { x: 0, y: 0 }, end: { x: 10, y: 10 } },
{ start: { x: 0, y: 10 }, end: { x: 10, y: 0 } }
);
// true
// Point on a circle of radius 10 at angle 0 (rightmost)
positionInCircle({ x: 0, y: 0 }, 10, 0);
// { x: 10, y: 0 }import { sort } from "pointscape";
const pts = [
{ x: 3, y: 1 },
{ x: 1, y: 3 },
{ x: 2, y: 2 },
];
sort([...pts]); // by x → [{ x:1,y:3 }, { x:2,y:2 }, { x:3,y:1 }]
sort([...pts], "y"); // by y → [{ x:3,y:1 }, { x:2,y:2 }, { x:1,y:3 }]
// Note: sort mutates the array in place.import {
degreesToRadians,
radiansToDegrees,
roundToPrecision,
average,
inRange,
} from "pointscape";
degreesToRadians(180); // 3.141592653589793
radiansToDegrees(Math.PI); // 180
roundToPrecision(3.14159, 2); // 3.14
roundToPrecision(3.14159, 0); // 3
average([10, 20, 30, 40]); // 25
inRange(5, 0, 10); // true
inRange(15, 0, 10); // falseimport {
intersection,
difference,
removeDuplicates,
chunk,
sample,
} from "pointscape";
intersection([1, 2, 3, 4], [3, 4, 5, 6]); // [3, 4]
difference([1, 2, 3, 4], [3, 4, 5, 6]); // [1, 2]
removeDuplicates([1, 1, 2, 3, 3, 4]); // [1, 2, 3, 4]
chunk([1, 2, 3, 4, 5, 6], 2); // [[1, 2], [3, 4], [5, 6]]
sample([10, 20, 30, 40, 50]); // one random element, e.g. 30
sample([10, 20, 30, 40, 50], 3); // 3 consecutive elements, e.g. [20, 30, 40]import { randomNumber, randomBoolean, uniqueId } from "pointscape";
randomNumber(1, 10); // random integer in [1, 10], e.g. 7
randomBoolean(); // true or false
uniqueId(); // e.g. "110e8400-e29b-41d4-a716-446655440000"
uniqueId(["id-1", "id-2"]); // UUID guaranteed not to collide with the provided ids- distance
interface Point {
x: number;
y: number;
}
(point1: Point, point2: Point) => number;Returns the distance beetween two points, each point is an object with x and y properties.
- area
(points: Point[]) => number;Returns the area enclosed by the given points. Takes an array of points as argument, where each point is an object with x and y properties.
- triangleArea
(point1: Point, point2: Point, point3: Point) => number;Returns the area of the triangle formed by three points.
- collision
(
point1: Point,
point2: Point,
collisionDistance: number,
[callback]?: () => void
) => boolean;Returns true if the Euclidean distance between the two points is less than or equal to collisionDistance (treat collisionDistance as a non-negative radius for a circle around each position). Optionally invokes callback when a collision is detected.
- collisionInArray
(initialPoint: Point, points: Point[], radius: number) => Point[]Returns every point in points whose distance to initialPoint is within radius, using the same circular rule as collision.
- positionInCircle
(point: Point, radius: number, angleInRadians: number) => Point;Returns the x and y coordinates for the current point in the circle, given its center point, radius, and angle.
- angle
(point1: Point, point2: Point) => number;Returns the angle formed by the connection of two points.
- middle
(point1: Point, point2: Point) => Point;Returns the midpoint between two points.
- nearest
(point: Point, points: Point[]) => Point | null;Returns the nearest point to the given point from the array, or null if points is empty.
- perimeter
(points: Point[]) => number;Returns the perimeter of the figure formed by the given points.
- pointWithoutCollision(minX, maxX, minY, maxY, distance, points)
interface Bounds {
min: number;
max: number;
}
(xBounds: Bounds, yBounds: Bounds, distance: number, points: Point[]) =>
Point | null;Returns a point that doesn't collide with any of the given points within the specified distance, or null if no such point could be found within the retry budget.
- randomPoint
([xBounds]: Bounds, [yBounds]: Bounds) => Point;Returns a random point within the given dimensions, if provided, otherwise in 100 units on both axes.
- randomPointInDistance
(point: Point, distance: number) => Point;Returns a random point within the given distance from the specified point.
- randomPoints
([xBounds]: Bounds, [yBounds]: Bounds, quantity: number,) => Point[]Returns a specified quantity of random points within the given dimensions, if dimensions are provided, otherwise in the range of 100.
- possibleConnections
(pointsCount: number) => number;Returns the quantity of possible connections among given quantity of points.
- circleArea
(radius: number) => number;Returns the area of the circle given its radius.
- center
(points: Point[]) => Point;Returns the center of given points.
- farthest
(point: Point, points: Point[]) => Point | null;Returns the farthest point from the given point among the given array; null if the array is empty.
- rotate
(points: Point[], centerPoint: Point, angleInRadians: number) => Point[]Returns the points rotated around the given center point.
- sort
(points: Point[], coordinate?: "x" | "y") => Point[]Mutates points in place (Array.sort) and returns the same array reference. Omit coordinate to sort primarily by x; "y" sorts by y.
- scale
(points: Point[], scaleFactorX: number, scaleFactorY: number) => Point[]Returns the scaled points.
- inLine
interface Line {
start: Point;
end: Point;
}
(point: Point, line: Line) => boolean;Returns boolean value indicating whether or not the given coordinates are on line defined by two other points.
- cross
(line1: Line, line2: Line) => boolean;Returns boolean value indicating if two lines each defined by two points intersect.
- move
(point: Point, xStep: number, yStep: number) => Point;Returns a point of with the new coordinates.
- lerp
(point1: Point, point2: Point, t: number) => Point;Returns the linearly interpolated point between point1 and point2 at parameter t (0 = point1, 1 = point2, 0.5 = midpoint). Values of t outside [0, 1] extrapolate beyond the two points.
- square
(point: Point, size: number, [direction]: "left" | "right" | "down" | "up" ) => Point[]Returns an array of points representing a shape of square.Takes four parameters: starting coordinates (x and y), size of square side, and direction which should be one of the values "left", "right", "up", "down".
- rectangle(point, width, height, [direction])
(point: Point, width: number, height: number, [direction]: "left" | "right" | "down" | "up" ) => Point[]Returns vertices of a rectangle starting at point, advancing width then height along the directional path (same directional convention as square).
- triangle(point, size, [direction])
(point: Point, size: number, [direction]: "left" | "right" | "down" | "up" ) => Point[]Returns an array of points representing a shape of triangle.Takes same parameters as square function.
- pentagon(center, radius, [angle])
(centerPoint: Point, radius: number, angle?: number) => Point[]Returns vertices of a pentagon around centerPoint with radius and rotation angle (degrees, default 0).
Point and Vector are structurally identical ({ x, y }); Vector represents a direction + magnitude rather than a position. Use subtract to get the displacement vector between two points.
- magnitude
(v: Vector) => number;Returns the length of the vector.
- normalize
(v: Vector) => Vector;Returns a unit vector (length 1) in the same direction. Returns { x: 0, y: 0 } for the zero vector.
- dotProduct
(v1: Vector, v2: Vector) => number;Returns the dot product of two vectors. Zero when the vectors are perpendicular.
- crossProduct
(v1: Vector, v2: Vector) => number;Returns the scalar (2D) cross product — the signed area of the parallelogram formed by the two vectors.
- add
(v1: Vector, v2: Vector) => Vector;Returns the component-wise sum of two vectors.
- subtract
(v1: Vector, v2: Vector) => Vector;Returns the component-wise difference of two vectors. Also how you get the displacement vector from v2 to v1 when passing two points.
- multiply
(v: Vector, scalar: number) => Vector;Scales a vector by a scalar.
- project
(v: Vector, onto: Vector) => Vector;Returns the component of v parallel to onto. Returns { x: 0, y: 0 } if onto is the zero vector.
- reflect
(v: Vector, normal: Vector) => Vector;Reflects v off a surface with the given unit normal.
- perpendicular
(v: Vector) => Vector;Rotates the vector 90° counter-clockwise.
- fromAngle
(angleInRadians: number, magnitude?: number) => Vector;Builds a vector from an angle (radians) and magnitude (default 1).
- degreesToRadians
(degrees: number) => number;Converts degrees to radians.
- radiansToDegrees
(radians: number) => number;Converts radians to degrees.
- inRange
(number: number, min: number, max: number) => boolean;Returns true if the given number is within the specified range.
- roundToPrecision
(number: number, precision: -100 | -10 | 0 | 10 | 100 | number) => number;Rounds the number to the given precision.
- average
(numbers: number[]) => number;Returns the average of all numbers in an array.
- intersection
<T>(arr1: T[], arr2: T[]) => T[]Returns the array of intersection of two arrays.
- difference
<T>(arr1: T[], arr2: T[]) => T[]Returns the array of difference of two arrays.
- chunk
<T>(arr: T[], perArr: number) => T[][]Returns an array splited into chunks based on elements count per chunk.
- removeDuplicates(arr)
<T>(arr: T[]) => T[]Returns the array without duplicates.
- sample
<T>(arr: T[], size?: number) => T | T[]Returns a random sample from an array with optional size argument for sampling length. If not specified, it returns only one element.
- randomNumber
(min: number, max: number) => number;Returns a random number within the given range.
- randomBoolean
() => boolean;Returns a random boolean value.
- uniqueId
([other ids]: string[]) => stringReturns a unique ID that's different from the provided IDs, or a random ID if no other IDs are given.
Use named imports so bundlers can tree-shake unused functions:
import { distance, collision, nearest, Point, Vector } from "pointscape";
import type { Bounds, Line, PointType, VectorType } from "pointscape";Functions take any { x: number; y: number }; you do not have to allocate a Point or Vector class instance. PointType/VectorType match instances of Point/Vector (the classes exported from this package) — the underlying Point/Vector types are structurally identical, but kept distinct to signal position vs. direction+magnitude.
Open a PR against the default branch; CI runs npm ci, tests, and npm run build on every push/PR (see .github/workflows/ci.yml).
git clone https://github.com/Arman2409/pointscape.git
cd pointscape
nvm use # optional: picks Node from .nvmrc
npm ci
npm test
npm run build- Bump
versioninpackage.jsonand updateCHANGELOG.md. npm loginwith an account that [maintainspointscape\*\*](https://www.npmjs.com/package/pointscape) on the public registry (https://registry.npmjs.org/`).npm publish— theprepackscript rebuildsdist/first.
Preview tarball contents locally: npm run publish:dry-run.
See also npm files publish guide — only dist/, readme, changelog, and license are published.