raw Software

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.

View Intersection.js on GitHub
Three types of geometric intersections A line crossing a circle, a line crossing a cubic Bézier curve three times, and two rotated ellipses crossing at four points. Line / circle Line / Bézier curve Ellipse / ellipse

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()1Cross products
lineCircle()2Quadratic equation
lineEllipse()2Unit-circle transform
lineArc()2Ellipse intersection and sweep filter
lineBezier()3Cubic equation
circleCircle()2Radical line
ellipseEllipse()4Quartic equation
arcArc()4Ellipse intersections and sweep filters
bezierCircle()6Degree-six polynomial
bezierEllipse()6Degree-six polynomial
bezierArc()6Ellipse intersections and sweep filter
bezierBezier()9Subdivision 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:

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.