Skip to content

Instantly share code, notes, and snippets.

@steveruizok
Last active May 13, 2022 06:33
Show Gist options
  • Save steveruizok/abdb58181428313d610568c7c1139a4e to your computer and use it in GitHub Desktop.
Save steveruizok/abdb58181428313d610568c7c1139a4e to your computer and use it in GitHub Desktop.
Find the point(s) where a line segment intersects an ellipse.
/**
* Find the point(s) where a line segment intersects an ellipse.
* @param x0 The x-axis coordinate of the line's start point.
* @param y0 The y-axis coordinate of the line's start point.
* @param x1 The x-axis coordinate of the line's end point.
* @param y1 The y-axis coordinate of the line's end point.
* @param cx The x-axis (horizontal) coordinate of the ellipse's center.
* @param cy The y-axis (vertical) coordinate of the ellipse's center.
* @param rx The ellipse's major-axis radius. Must be non-negative.
* @param ry The ellipse's minor-axis radius. Must be non-negative.
* @param rotation The rotation of the ellipse, expressed in radians.
* @param segment_only When true, will test the segment as a line (of infinite length).
*/
export function getEllipseSegmentIntersections(
x0: number,
y0: number,
x1: number,
y1: number,
cx: number,
cy: number,
rx: number,
ry: number,
rotation = 0,
segment_only = true
) {
// If the ellipse or line segment are empty, return no tValues.
if (rx === 0 || ry === 0 || (x0 === x1 && y0 === y1)) {
return []
}
// Get the semimajor and semiminor axes.
rx = rx < 0 ? rx : -rx
ry = ry < 0 ? ry : -ry
// Rotate points.
if (rotation !== 0) {
;[x0, y0] = rotatePoint(x0, y0, cx, cy, -rotation)
;[x1, y1] = rotatePoint(x1, y1, cx, cy, -rotation)
}
// Translate so the ellipse is centered at the origin.
x0 -= cx
y0 -= cy
x1 -= cx
y1 -= cy
// Calculate the quadratic parameters.
var A = ((x1 - x0) * (x1 - x0)) / rx / rx + ((y1 - y0) * (y1 - y0)) / ry / ry
var B = (2 * x0 * (x1 - x0)) / rx / rx + (2 * y0 * (y1 - y0)) / ry / ry
var C = (x0 * x0) / rx / rx + (y0 * y0) / ry / ry - 1
// Make a list of t values (normalized points on the line where intersections occur).
var tValues: number[] = []
// Calculate the discriminant.
var discriminant = B * B - 4 * A * C
if (discriminant === 0) {
// One real solution.
tValues.push(-B / 2 / A)
} else if (discriminant > 0) {
// Two real solutions.
tValues.push((-B + Math.sqrt(discriminant)) / 2 / A)
tValues.push((-B - Math.sqrt(discriminant)) / 2 / A)
}
return (
tValues
// Filter to only points that are on the segment.
.filter(t => !segment_only || (t >= 0 && t <= 1))
// Solve for points.
.map(t => [x0 + (x1 - x0) * t + cx, y0 + (y1 - y0) * t + cy])
// Counter-rotate points
.map(p =>
rotation === 0 ? p : rotatePoint(p[0], p[1], cx, cy, rotation)
)
)
}
/**
* Rotate a point around a center.
* @param x0 The x-axis coordinate of the point.
* @param y0 The y-axis coordinate of the point.
* @param cx The x-axis coordinate of rotation point.
* @param cy The y-axis coordinate of rotation point.
* @param rotation The rotation, expressed in radians.
*/
export function rotatePoint(
x: number,
y: number,
cx: number,
cy: number,
rotation: number
) {
const s = Math.sin(rotation)
const c = Math.cos(rotation)
const px = x - cx
const py = y - cy
const nx = px * c - py * s
const ny = px * s + py * c
return [nx + cx, ny + cy]
}
// It may not matter to you whether the intersection points returned by the above function
// are in a consistent order (e.g. clockwise or anti-clockwise order), but if you do need
// to check this, here's a function you can use to do so. (It does matter in my case, so I
// reverse the array returned by `getEllipseSegmentIntersections` if the points are not in
// clockwise order).
/**
* Get whether points are in clockwise order relative to a third point.
* @param x0 The x-axis coordinate of the first point.
* @param y0 The y-axis coordinate of the first point.
* @param x1 The x-axis coordinate of the second point.
* @param y1 The y-axis coordinate of the second point.
* @param cx The x-axis coordinate of the center point.
* @param cy The y-axis coordinate of the center point.
*/
export function pointsAreClockwise(
x0: number,
y0: number,
x1: number,
y1: number,
cx: number,
cy: number
) {
return Math.atan2(y0 - cy, x0 - cx) - Math.atan2(y1 - cy, x1 - cx) > 0
}
@steveruizok
Copy link
Author

steveruizok commented Jul 19, 2020

Adapted from Rod Stephen's original C# code here. Adds support for rotation.

@steveruizok
Copy link
Author

Added a second file with a function (arePointsClockwise) to check whether the returned points are in clockwise order. This may not be necessary in every case, so I've split it out from the main function. I use it like this:

const ints = getEllipseSegmentIntersections(
	px0,
	py0,
	px1,
	py1,
	cx,
	cy,
	rx,
	ry,
	angle
)

if (
	!pointsAreClockwise(
		ints[0],
		ints[0],
		ints[1],
		ints[1],
		cx,
		cy
	)
) {
	ints.reverse()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment