Intersection.js is a dependency-free TypeScript library for finding intersection points between the primitives used in two-dimensional vector graphics: line segments, circles, ellipses, elliptical arcs, and cubic Bézier curves. It returns not only the coordinates of each hit, but also the parameter on both participating shapes. This makes the results useful for splitting paths, ordering collisions, placing markers, and interpolating values at the exact point where two shapes meet.
Installation
Version 0.1.0 requires Node.js 18 or newer and has no runtime dependencies:
npm install intersection
The package provides ECMAScript modules, CommonJS, source maps, and TypeScript declarations from the same build:
import {
lineLine,
lineCircle,
lineBezier,
ellipseEllipse,
} from 'intersection'; const { lineLine, lineCircle } = require('intersection'); Shapes are structural objects rather than library-specific classes. Plain object literals, DOMPoint instances, and compatible vector classes can therefore be passed without conversion.
Start with Two Line Segments
The simplest operation takes the start and end point of each segment:
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 coordinates identify the crossing itself. t1 and t2 locate it on the first and second segment, where zero denotes the start and one denotes the end. These parameters preserve information that would be expensive and less accurate to reconstruct from the coordinates later.
Parallel segments return an empty array. Collinear segments also return an empty array, whether they are disjoint or overlap, because a shared interval is not one intersection point. Use isParallel() and isCollinear() when an application needs to distinguish these cases.
Circle, Ellipse, and Arc Intersections
A segment can meet a circle or ellipse at zero, one, or two points. A tangent is reported once rather than as two coincident roots:
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 }] lineEllipse() supports independent horizontal and vertical radii as well as rotation. The line-segment / ellipse intersection becomes a quadratic after the segment is transformed into the ellipse's unit-circle frame. This avoids approximating the boundary with a polygon.
const ellipse = {
x: 0,
y: 0,
rx: 5,
ry: 2,
phi: Math.PI / 6,
};
const points = lineEllipse(
{ x: -6, y: 0 },
{ x: 6, y: 0 },
ellipse,
); For circles, ellipses, and arcs, the corresponding result parameter is a parametric angle in the interval [0, 2π). On an ellipse this is generally not the polar angle seen from the center. The helpers ellipsePointAt() and ellipseAngleAt() convert between the parameter and a point.
Line Segments and Bézier Curves
A cubic Bézier curve may cross a line segment up to three times. The line-segment / Bézier curve intersection substitutes the curve's coordinate polynomials into the implicit line equation. The result is a cubic equation, which Intersection.js solves directly rather than by sampling or recursive subdivision.
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.map((point) => point.t1));
// [0, 0.5, 1] Here t1 is the Bézier parameter and t2 belongs to the segment. A quadratic curve can be elevated exactly with quadraticToCubic() before it is passed to the same function. The elevation changes the control polygon but not the curve.
Ellipse Against Ellipse
Two ellipses can have up to four isolated intersections. Intersection.js maps the first ellipse to the unit circle, carries the second ellipse to a conic, and inserts the rational parametrization of the circle. Clearing the denominator leaves a quartic polynomial:
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); The half-angle substitution cannot represent the angle π, so the implementation tests that point separately. This detail matters when two ellipses touch at the leftmost point of the transformed unit circle. Coincident ellipses return an empty array because they share infinitely many points rather than a finite result set.
Bézier Against Circle, Ellipse, or Arc
Transforming an ellipse to the unit circle turns the intersection condition into a sum of two squared cubic polynomials. The resulting degree-six polynomial can represent up to six intersections:
const points = bezierEllipse(curve, {
x: 0,
y: 0,
rx: 4,
ry: 2,
phi: Math.PI / 6,
}); bezierCircle() is the circle form of the operation. bezierArc() keeps only roots whose ellipse parameter lies inside the requested angular sweep. Roots are isolated on the closed interval from zero to one, refined, ordered along the curve, and deduplicated.
Two Cubic Bézier Curves
Two cubic curves can have up to nine isolated intersections. Unlike the other shape pairs, this problem has no practical closed-form reduction used by the library. bezierBezier() first subdivides the curves and rejects pairs of control hulls whose bounding boxes do not overlap. It then refines every surviving candidate with a damped least-squares iteration.
const points = bezierBezier(firstCurve, secondCurve); The damping makes tangential contact tractable. An ordinary Newton step becomes singular exactly where two curves touch, while the damped system can continue minimizing the squared distance. Transversal crossings are normally accurate to floating-point rounding; tangential contact is resolved to approximately 1e-7 of the curves' extent.
SVG Elliptical Arcs
SVG path data stores an A command in endpoint form, while intersection routines need a center, corrected radii, rotation, and angular sweep. arcFromSvg() performs the conversion defined by the SVG implementation notes, including enlarging radii that are too small to connect the endpoints:
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 argument is in radians, unlike the degrees stored in SVG path syntax. Coincident endpoints return null because SVG omits that arc; a zero radius also returns null because the command becomes a straight line. Equal start and end angles in an explicit Arc represent the complete ellipse.
Supported Shape Pairs
| Function | Maximum points | Method |
|---|---|---|
lineLine() | 1 | Cross products |
lineCircle() | 2 | Quadratic equation |
lineEllipse() | 2 | Unit-circle transform |
lineArc() | 2 | Ellipse intersection and sweep filter |
lineBezier() | 3 | Cubic equation |
circleCircle() | 2 | Radical line |
ellipseEllipse() | 4 | Quartic equation |
arcArc() | 4 | Ellipse intersections and sweep filters |
bezierCircle() | 6 | Degree-six polynomial |
bezierEllipse() | 6 | Degree-six polynomial |
bezierArc() | 6 | Ellipse intersections and sweep filter |
bezierBezier() | 9 | Subdivision and damped least squares |
The specialized circleCircle() implementation uses the intersection geometry of two circles directly. It therefore avoids routing the most common curved-shape pair through the more general quartic ellipse solver.
Bounding Boxes and Shape Helpers
Intersection.js also exports exact axis-aligned bounds for points, segments, circles, ellipses, and cubic curves. The cubic bounds include derivative roots, so they enclose the curve itself rather than merely sampling it:
const firstBounds = boundsOfCubic(firstCurve);
const secondBounds = boundsOfCubic(secondCurve);
if (boundsOverlap(firstBounds, secondBounds)) {
const points = bezierBezier(firstCurve, secondCurve);
} boundsOfCubicHull() returns the cheaper control-polygon box for broad-phase rejection. rectRect() accepts boxes in { left, top, right, bottom } form, including values compatible with getBoundingClientRect(). Additional helpers evaluate points and tangents, split cubic curves, normalize angles, and convert circles or quadratic curves into the forms accepted by the intersection functions.
Numerical and Degenerate Cases
Geometry code is defined as much by its edge cases as by ordinary crossings. Intersection.js applies the following rules consistently:
- A tangent is one point, not a duplicated pair.
- Results are ordered by the first result parameter and coincident numerical roots are deduplicated.
- Zero-length segments, non-positive radii, and non-finite coordinates return an empty array.
- Collinear overlaps, coincident ellipses, and curves sharing a span return an empty array because the common set is not a finite list of points.
- Length tolerances scale with the magnitude of the input instead of relying on one absolute epsilon.
The scale-aware comparisons are important in CAD coordinates, map projections, and document coordinate systems. Coordinates in the millions should have the same qualitative behavior as the same drawing near the unit square. Functions return [] for unsupported degeneracies rather than throwing or emitting coordinates containing NaN.
Where Intersection.js Fits
The library focuses on isolated intersection points between bounded 2D primitives. It is a good fit for SVG editors, path splitting, snapping, hit testing, collision candidates, plotting, diagram tools, and computational-geometry experiments. It is not a polygon Boolean engine, a path parser, or a replacement for an exact-arithmetic geometry kernel. Overlapping spans and filled-area operations require a representation richer than an array of points.
By keeping shape input structural, output explicit, and the solvers dependency-free, Intersection.js can sit below a rendering framework without dictating how shapes are stored or displayed.