'How to get axis-aligned bounding box of an ellipse with all given parameters?

void ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle [, anticlockwise]);

The canvas context 2D API ellipse() method creates an elliptical arc centered at (x, y) with the radii radiusX and radiusY. The path starts at startAngle and ends at endAngle, and travels in the direction given by anticlockwise.

How to get the axis-aligned bounding box of a ellipse with the given parameters:x, y, radiusX, radiusY, rotation, startAngle, endAngle , anticlockwise?



Solution 1:[1]

Two solutions

This answer contains two exact solutions it is not an approximation.

The solutions are

  1. boundEllipseAll will find the bounds of the full ellipse. If is a lot less complex than the full solution but you need to ensure that the x radius is greater than the y radius (eg rotate the ellipse 90 deg and swap the x, y radius)

  2. boundEllipse Will find the bounds for a segment of an ellipse. It will work for all ellipses however I have not included the CCW flag. To get the bounds for CCW ellipse swap the start and end angles.

    It works by first finding the x, y coords at the start and end points calculating the min and max along each axis. It then calculates the extrema angles, first for the x axis extremes, then the y axis extremes.

    If the extrema angle is between the start and end angle, the x,y position of that angle is calculated and the point tested against the min and max extent.

There is a lot of room to optimize as many of the points need only the x, or y parts, and the inner while loop in function extrema can exit early if min and max are change for the axis it is working on.

Example

The example ensures I have not made any mistakes, and uses the second solution, animating an ellipse by moving the start and end angles, rotation, and y axis radius. Drawing the bounding box and the ellipse it bounds.

Update April 2022

Example shows use of both full ellipse boundEllipseAll and ellipse segment boundEllipse.

Note that boundEllipse is only for ellipse segment where endAngle n and startAngle m fit the rule {m <= n <= m + 2Pi}

Fixed bug in boundEllipse that did not show full ellipse when endAngle == startAngle + 2 * Math.PI

const ctx = canvas.getContext("2d");
const W = 200, H= 180;
const TAU = Math.PI * 2;
const ellipse = {
    x: W / 2, 
    y: H / 2, 
    rx: W / 3, 
    ry: W / 3, 
    rotate: 0, 
    startAng: 0, 
    endAng: Math.PI * 2,
    dir: false,
};
function boundEllipseAll({x, y, rx, ry, rotate}) {
    const xAx = Math.cos(rotate);
    const xAy = Math.sin(rotate);    
    const w =  ((rx  * xAx) ** 2 + (ry * xAy) ** 2) ** 0.5;
    const h = ((rx * xAy) ** 2 + (ry * xAx) ** 2) ** 0.5;
    return {x: -w + x, y: -h + y, w: w * 2, h: h * 2};
}
function boundEllipse({x, y, rx, ry, rotate, startAng, endAng}) {
    const normalizeAng = ang => (ang % TAU + TAU) % TAU;
    const getPoint = ang => {
        const cA = Math.cos(ang);
        const sA = Math.sin(ang);
        return [cA * rx * xAx - sA * ry * xAy, cA * rx * xAy + sA * ry * xAx];
    }
    const extrema = a => {  // from angle
        var i = 0;
        while(i < 4) {
            const ang = normalizeAng(a + Math.PI * (i / 2));
            if ((ang > startAng && ang < endAng) || (ang + TAU  > startAng && ang + TAU < endAng)) {
                const [xx, yy] = getPoint(ang);
                minX = Math.min(minX, xx);
                maxX = Math.max(maxX, xx);
                minY = Math.min(minY, yy);
                maxY = Math.max(maxY, yy);                
            }
            i ++;
        }        
    }
    // UPDATE bug fix (1) for full ellipse
    const checkFull = startAng !== endAng;  // Update fix (1)
    startAng = normalizeAng(startAng);
    endAng = normalizeAng(endAng);
    (checkFull && startAng === endAng) && (endAng += TAU); // Update fix (1)
    const xAx = Math.cos(rotate);
    const xAy = Math.sin(rotate);    
    endAng += endAng < startAng ? TAU : 0;
    const [sx, sy] = getPoint(startAng);
    const [ex, ey] = getPoint(endAng);
    var minX = Math.min(sx, ex);
    var maxX = Math.max(sx, ex);
    var minY = Math.min(sy, ey);
    var maxY = Math.max(sy, ey);    
    extrema(-Math.atan((ry * xAy) / (rx * xAx)));  // Add x Axis extremas
    extrema(-Math.atan((rx * xAy) / (ry * xAx)));  // Add y Axis extremas
    return {x: minX + x, y: minY + y, w: maxX - minX, h: maxY - minY};
}
function drawExtent({x,y,w,h}) {
    ctx.moveTo(x,y);
    ctx.rect(x, y, w, h);
}
function drawEllipse({x, y, rx, ry, rotate, startAng, endAng, dir}) {
    ctx.ellipse(x, y, rx, ry, rotate, startAng, endAng, dir);
}
function drawFullEllipse({x, y, rx, ry, rotate, dir}) {
    ctx.ellipse(x, y, rx, ry, rotate, 0, TAU, dir);
}
mainLoop(0);
function mainLoop(time) {
    ctx.clearRect(0, 0, W, H);
    
    // Animate ellipse
    ellipse.startAng = time / 1000;
    ellipse.endAng = time / 2000;
    ellipse.rotate = Math.cos(time / 14000) * Math.PI * 2;
    ellipse.ry = Math.cos(time / 6000) * (W / 4 - 10) +  (W / 4);
    
    // Draw full ellipse and bounding box.
    ctx.strokeStyle = "#F008";
    ctx.beginPath();
    drawFullEllipse(ellipse);
    drawExtent(boundEllipseAll(ellipse));
    ctx.stroke();    
    
    // Draw ellipse segment and bounding box.
    ctx.strokeStyle = "#0008";
    ctx.beginPath();
    drawEllipse(ellipse);
    drawExtent(boundEllipse(ellipse));
    ctx.stroke();

    requestAnimationFrame(mainLoop)
}
canvas { border: 1px solid black }
<canvas id="canvas" width="200" height="180"></canvas>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1