Contents
raw Math
RAW Math Computer Graphics Computational Geometry

Line-Segment Ellipse Intersection

Robert Eisele

A line segment can meet an ellipse at two points, touch it at one point, or miss it entirely. The direct coordinate equation becomes cumbersome when the ellipse is translated and rotated. A change of coordinates removes that complexity: transform the segment into the local coordinate system of the ellipse, where the ellipse is axis-aligned, and then scale it into a unit circle.

A B P1 P2 C

A finite line segment intersects a rotated ellipse at the points \(P_1\) and \(P_2\).

Parametric Segment

Let the segment run from \(A\) to \(B\), with direction

\[ \mathbf{v}=B-A. \]

Every point on its supporting line can be written as

\[ L(t)=A+t\mathbf{v}. \]

The point lies on the finite segment exactly when \(t\in[0,1]\).

Transforming into Ellipse Space

Consider an ellipse centered at \(C\), with positive radii \(r_x\) and \(r_y\), rotated counterclockwise by \(\varphi\). A world-space point \(P\) is moved into the local, axis-aligned coordinate system of the ellipse by

\[ P'=R(-\varphi)(P-C), \]

where

\[ R(\alpha)= \begin{pmatrix} \cos\alpha & -\sin\alpha\\ \sin\alpha & \cos\alpha \end{pmatrix}. \]

Apply the same transformation to the segment endpoints:

\[ A'=R(-\varphi)(A-C), \qquad B'=R(-\varphi)(B-C), \qquad \mathbf{v}'=B'-A'. \]

The ellipse now has the familiar equation

\[ \frac{x^2}{r_x^2}+\frac{y^2}{r_y^2}=1. \]

Dividing local x-coordinates by \(r_x\) and y-coordinates by \(r_y\) scales this ellipse into the unit circle. Define

\[ \mathbf{p}= \begin{pmatrix}A'_x/r_x\\A'_y/r_y\end{pmatrix}, \qquad \mathbf{d}= \begin{pmatrix}v'_x/r_x\\v'_y/r_y\end{pmatrix}. \]

The transformed segment is \(\mathbf{p}+t\mathbf{d}\), and its intersections satisfy

\[ \lVert\mathbf{p}+t\mathbf{d}\rVert^2=1. \]

The Quadratic Equation

Expanding the squared norm gives

\[ (\mathbf{d}\cdot\mathbf{d})t^2 +2(\mathbf{p}\cdot\mathbf{d})t +(\mathbf{p}\cdot\mathbf{p}-1)=0. \]

Thus

\[ a=\mathbf{d}\cdot\mathbf{d}, \qquad b=2\mathbf{p}\cdot\mathbf{d}, \qquad c=\mathbf{p}\cdot\mathbf{p}-1. \]

The discriminant

\[ \Delta=b^2-4ac \]

classifies the supporting line:

For the finite segment, only roots in \([0,1]\) are retained.

Why the Same Parameter Works in World Space

Translation, rotation, and nonzero scaling are affine transformations. If \(T\) denotes their composition, then

\[ T(A+t(B-A))=T(A)+t(T(B)-T(A)). \]

The transformation changes the geometry but not the parameter \(t\). Once a valid root has been found in the normalized ellipse space, the corresponding world-space point follows directly from the original segment:

\[ \boxed{P=A+t(B-A)}. \]

No separate inverse transformation of each intersection point is necessary.

Numerically Stable Roots

The familiar quadratic formula

\[ t_{1,2}=\frac{-b\pm\sqrt{\Delta}}{2a} \]

can lose accuracy when \(-b\) and \(\sqrt{\Delta}\) nearly cancel. A stable evaluation first computes

\[ q=-\frac12\left(b+\operatorname{sgn}(b)\sqrt{\Delta}\right), \]

using \(\operatorname{sgn}(0)=1\), and then obtains the roots from

\[ t_1=\frac{q}{a}, \qquad t_2=\frac{c}{q}. \]

Their product is \(c/a\), as required by Vieta's formulas. For a tangent, \(\Delta=0\), there is only the double root \(-b/(2a)\).

Degenerate Segment

If \(A=B\), then \(\mathbf{d}=\mathbf{0}\) and the quadratic coefficient \(a\) vanishes. The segment is a single point. It intersects the ellipse exactly when its normalized local coordinate \(\mathbf{p}\) satisfies

\[ \lVert\mathbf{p}\rVert^2=1. \]

A point strictly inside the ellipse is not a boundary intersection. This distinction matters because the task is to intersect the segment with the ellipse curve, not with the filled elliptical region.

JavaScript Implementation with Vector2.js

The implementation uses Vector2.js for translation, rotation, scaling, dot products, and interpolation along the segment. It returns zero, one, or two Vector2 points in increasing segment-parameter order.

function intersectSegmentEllipse(A, B, center, rx, ry, angle, epsilon = 1e-12) {
  if (!(rx > 0) || !(ry > 0)) {
    throw new RangeError("Ellipse radii must be positive");
  }

  A = Vector2(A);
  B = Vector2(B);
  center = Vector2(center);

  const localA = A.sub(center).rotate(-angle);
  const localB = B.sub(center).rotate(-angle);
  const localDirection = Vector2.fromPoints(localA, localB);
  const p = Vector2(localA.x / rx, localA.y / ry);
  const d = Vector2(localDirection.x / rx, localDirection.y / ry);

  const a = d.norm2();
  const b = 2 * p.dot(d);
  const c = p.norm2() - 1;

  if (a <= epsilon * epsilon) {
    return Math.abs(c) <= epsilon ? [A] : [];
  }

  const discriminant = b * b - 4 * a * c;
  const discriminantTolerance = epsilon * (b * b + Math.abs(4 * a * c) + 1);

  if (discriminant < -discriminantTolerance) {
    return [];
  }

  const direction = Vector2.fromPoints(A, B);
  const pointAt = t => A.add(direction.scale(Math.max(0, Math.min(1, t))));

  if (Math.abs(discriminant) <= discriminantTolerance) {
    const t = -b / (2 * a);
    return t >= -epsilon && t <= 1 + epsilon ? [pointAt(t)] : [];
  }

  const sqrtDiscriminant = Math.sqrt(discriminant);
  const q = -0.5 * (b + (b >= 0 ? sqrtDiscriminant : -sqrtDiscriminant));
  let t1 = q / a;
  let t2 = c / q;

  if (t1 > t2) {
    [t1, t2] = [t2, t1];
  }

  const intersections = [];
  if (t1 >= -epsilon && t1 <= 1 + epsilon) {
    intersections.push(pointAt(t1));
  }
  if (t2 >= -epsilon && t2 <= 1 + epsilon
      && Math.abs(t2 - t1) > epsilon) {
    intersections.push(pointAt(t2));
  }
  return intersections;
}

The calculation uses a constant number of vector and scalar operations, so both its time and additional-space complexity are \(O(1)\).