Finding where two geometric shapes meet sounds simple until ellipses are rotated, curves touch without crossing, or coordinates become large enough for a fixed epsilon to fail. Intersection.js handles these cases for the primitives used in 2D vector graphics: line segments, circles, ellipses, elliptical arcs, and Bezier curves.
A line-segment intersection can be calculated as follows:
import { lineLine } from 'intersection';
const points = lineLine(
{ x: -1, y: 2 },
{ x: 5, y: 2 },
{ x: 1, y: -1 },
{ x: 4, y: 4 },
);
console.log(points);
// [{ x: 2.8, y: 2, t1: 0.6333333333333333, t2: 0.6 }]The returned point also contains its position on both operands. This makes the result useful for more than drawing a dot, where you can split a path, sort collisions along a stroke, or interpolate another value exactly where the shapes meet.
Every intersection function returns an array. No intersection is represented
by []; a tangent produces one point, and ordinary crossings produce as many
points as the pair of shapes permits. Invalid or degenerate geometry also
returns [] instead of throwing an exception.
A line segment can meet a circle in zero, one, or two points. Results are ordered along the segment:
import { lineCircle } from 'intersection';
const points = lineCircle(
{ x: -3, y: 0 },
{ x: 3, y: 0 },
{ x: 0, y: 0, r: 2 },
);
console.log(points.map(({ x, y }) => ({ x, y })));
// [{ x: -2, y: 0 }, { x: 2, y: 0 }]A cubic Bezier curve can cross one line segment up to three times. The line segment / Bezier curve intersection reduces the problem to solving a cubic equation directly, without sampling the curve:
import { lineBezier } from 'intersection';
const curve = {
p0: { x: 0, y: 0 },
p1: { x: 1, y: 3 },
p2: { x: 2, y: -3 },
p3: { x: 3, y: 0 },
};
const points = lineBezier(
{ x: -1, y: 0 },
{ x: 4, y: 0 },
curve,
);
console.log(points.length); // 3
console.log(points.map((point) => point.t1)); // [0, 0.5, 1]For a quadratic Bezier curve, use quadraticToCubic() first. Degree elevation
is exact and does not alter the shape.
Ellipses may be rotated independently. Their rotations are given in radians:
import { ellipseEllipse } from 'intersection';
const first = { x: 0, y: 0, rx: 5, ry: 2, phi: Math.PI / 6 };
const second = { x: 2, y: 0, rx: 4, ry: 1.5, phi: -Math.PI / 8 };
const points = ellipseEllipse(first, second);
// zero to four intersection pointsThe first ellipse is transformed to a unit circle. Substituting the rational parametrization of that circle leaves a quartic equation, so all intersections can be found without tracing or sampling either ellipse.
Two cubic Bezier curves can have up to nine intersections:
import { bezierBezier } from 'intersection';
const first = {
p0: { x: 0, y: 0 },
p1: { x: 1, y: 4 },
p2: { x: 3, y: -4 },
p3: { x: 4, y: 0 },
};
const second = {
p0: { x: 0, y: 2 },
p1: { x: 1, y: -2 },
p2: { x: 3, y: 4 },
p3: { x: 4, y: -2 },
};
const points = bezierBezier(first, second);The curves are subdivided to isolate candidates and each candidate is refined with damped least squares. The damping is important for tangencies, where an ordinary Newton iteration has a singular Jacobian.
SVG path data describes an elliptical arc by two endpoints, two radii, a
rotation, and two flags. arcFromSvg() converts an A command into the centre
form used by the intersection functions:
import { arcFromSvg, lineArc } from 'intersection';
// <path d="M 100 100 A 60 40 30 0 1 200 160" />
const arc = arcFromSvg(
{ x: 100, y: 100 },
60,
40,
30 * Math.PI / 180,
false,
true,
{ x: 200, y: 160 },
);
if (arc !== null) {
const points = lineArc(
{ x: 0, y: 120 },
{ x: 300, y: 120 },
arc,
);
}The rotation passed to arcFromSvg() is in radians, while SVG path data stores
it in degrees. If the specified radii are too small to connect the endpoints,
they are enlarged according to the SVG implementation notes.
arcFromSvg() returns null when coincident endpoints describe no arc or when
a zero radius turns the command into a straight line.
The bounding-box helpers calculate extrema analytically rather than sampling:
import { boundsOfCubic, boundsOverlap } from 'intersection';
const firstBounds = boundsOfCubic(first);
const secondBounds = boundsOfCubic(second);
if (boundsOverlap(firstBounds, secondBounds)) {
// A detailed intersection test may be worthwhile.
}This is useful as a fast rejection step before a more expensive curve intersection.
Intersection.js accepts plain objects. Types are structural, so DOMPoint,
@rawify/vector2, and any other object containing finite x and y values can
be used as a point.
{ x: 10, y: 20 }{ x: 10, y: 20, r: 5 }{ x: 10, y: 20, rx: 8, ry: 4, phi: Math.PI / 6 }The optional phi property rotates the rx axis in radians and defaults to
zero.
{
x: 10,
y: 20,
rx: 8,
ry: 4,
phi: Math.PI / 6,
alpha: 0,
beta: Math.PI,
}An arc is swept counter-clockwise from alpha to beta. Equal angles modulo
2 * Math.PI represent the whole ellipse rather than an empty sweep.
{
p0: { x: 0, y: 0 },
p1: { x: 1, y: 3 },
p2: { x: 2, y: -3 },
p3: { x: 3, y: 0 },
}{
p0: { x: 0, y: 0 },
p1: { x: 1.5, y: 3 },
p2: { x: 3, y: 0 },
}Quadratic curves can be converted with quadraticToCubic() before they are
passed to an intersection function.
Each result contains the Cartesian point and one parameter for each operand:
interface Intersection {
x: number;
y: number;
t1: number;
t2: number;
}For a line segment or Bezier curve, its parameter lies in [0, 1] from start
to end. For a circle, ellipse, or arc, it is the parametric angle in [0, 2π).
The parameters are assigned as follows:
| functions | t1 |
t2 |
|---|---|---|
lineLine() |
first segment | second segment |
lineCircle(), lineEllipse(), lineArc() |
line segment | circle, ellipse, or arc |
lineBezier() |
Bezier curve | line segment |
circleCircle(), ellipseEllipse(), arcArc() |
first shape | second shape |
bezierCircle(), bezierEllipse(), bezierArc() |
Bezier curve | circle, ellipse, or arc |
bezierBezier() |
first curve | second curve |
The parametric angle of an ellipse is the t in
center + R(phi) * (rx cos(t), ry sin(t)). It is not generally the polar angle
seen from the centre; both angles are equal only for a circle. Use
ellipsePointAt() and ellipseAngleAt() to convert between a point and the
ellipse parameter.
Results are ordered by t1 and duplicate points are removed. A tangency is
therefore returned once rather than as two coincident points.
Returns at most one intersection between two line segments. Parallel,
collinear, overlapping, and zero-length segments return []. Use
isParallel() or isCollinear() when the distinction matters.
Returns up to two intersections between a line segment and a circle.
Returns up to two points from the intersection between a line segment and a full ellipse. Rotated ellipses are supported.
Returns up to two intersections between a line segment and an elliptical arc.
It has the same result as lineEllipse(), filtered to the arc's sweep.
Returns up to three intersections between a line segment and a cubic Bezier curve.
Returns up to two intersection points of two circles.
Concentric circles return [], including identical circles that share
infinitely many points.
Returns up to four intersections between two full ellipses. Circles and rotated
ellipses are accepted. Coincident ellipses return [] because their
intersection is not a finite list of points.
Returns up to four intersections between two elliptical arcs.
Returns up to six intersections between a cubic Bezier curve and a circle.
Returns up to six intersections between a cubic Bezier curve and a full ellipse.
Returns up to six intersections between a cubic Bezier curve and an elliptical arc.
Returns up to nine intersections between two cubic Bezier curves.
Converts an SVG elliptical arc command from endpoint form to centre form. The
function returns an Arc or null when the SVG command does not describe an
arc.
Converts a circle and two angles into an Arc for use with the arc functions.
Raises a quadratic Bezier curve to a cubic Bezier curve without changing its shape.
cubicPointAt(), cubicTangentAt(), splitCubic(), and
cubicControlPoints() evaluate and manipulate cubic Bezier curves.
circleToEllipse(), ellipsePointAt(), ellipseAngleAt(),
normalizeAngle(), and isAngleInArc() convert shapes, points, and angles.
TAU contains the value 2 * Math.PI.
boundsOfPoints(), boundsOfSegment(), boundsOfCubic(),
boundsOfCubicHull(), boundsOfCircle(), and boundsOfEllipse() return boxes
in { minX, minY, maxX, maxY } form.
boundsOverlap() checks two such boxes. rectRect() performs the same test for
boxes in { left, top, right, bottom } form, including objects returned by
getBoundingClientRect().
Geometry algorithms are often decided by their edge cases. Intersection.js uses the following rules consistently:
- A tangency returns one point.
- Collinear or overlapping segments return
[]because the common part is a segment rather than one intersection point. - Coincident ellipses and curves sharing a span return
[]because they have infinitely many common points. - Zero-length segments, non-positive radii, non-finite coordinates, and
non-finite control points return
[]. - Tolerances scale with the magnitude of the input. Coordinates in the millions therefore behave like coordinates around the unit square.
Every pair except bezierBezier() reduces to a polynomial. Equations up to
degree three are solved in closed form; higher degrees use bracketed Newton
iteration. bezierBezier() uses subdivision followed by damped least squares.
You can install Intersection.js via npm:
npm install intersectionOr with yarn:
yarn add intersectionAlternatively, download or clone the repository:
git clone https://github.com/rawify/Intersection.js.gitIn an ES module project:
import { lineLine, circleCircle, bezierEllipse } from 'intersection';Or in a CommonJS project:
const { lineLine, circleCircle, bezierEllipse } = require('intersection');All public TypeScript types are exported from the package:
import type {
Arc,
Bounds,
Circle,
CubicBezier,
Ellipse,
Intersection,
Point,
QuadraticBezier,
Rect,
} from 'intersection';After cloning the Git repository, install the dependencies and build the package:
npm install
npm run buildThe build creates ESM, CommonJS, source maps, and TypeScript declarations in
dist/.
Testing the source against the shipped test suite is as easy as:
npm testTo run the type checker and the complete test suite together:
npm run checkCopyright (c) 2026, Robert Eisele Licensed under the MIT license.