如何获取具有所有给定参数的椭圆的轴对齐边界框?

问题描述 投票:0回答:2
void ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle [, anticlockwise]);

canvas context 2D API ellipse() 方法创建一个以 (x, y) 为中心、半径为 radiusX 和 radiusY 的椭圆弧。路径从 startAngle 开始,到 endAngle 结束,并按逆时针方向行进。

如何使用给定参数获取椭圆的轴对齐边界框:x,y,radiusX,radiusY,旋转,startAngle,endAngle,逆时针?

javascript canvas
2个回答
6
投票

两种解决方案

这个答案包含两个精确的解决方案,它不是近似值。

解决办法是

  1. boundEllipseAll
    将找到完整椭圆的边界。如果比完整解决方案复杂得多,但您需要确保 x 半径大于 y 半径(例如,将椭圆旋转 90 度并交换 x、y 半径)

  2. boundEllipse
    将找到椭圆段的边界。它适用于所有省略号,但我没有包括 CCW 标志。要获得 CCW 椭圆的边界,请交换起始角度和结束角度。

    它的工作原理是首先找到起点和终点的 x、y 坐标,计算沿每个轴的最小值和最大值。然后,它计算极值角度,首先计算 x 轴极值,然后计算 y 轴极值。

    如果极值角度位于起始角度和结束角度之间,则计算该角度的 x,y 位置,并根据最小和最大范围测试点。

有很大的优化空间,因为许多点只需要 x 或 y 部分,并且如果正在处理的轴的 min 和 max 发生变化,函数

extrema
中的内部 while 循环可以提前退出。

示例

该示例确保我没有犯任何错误,并使用第二种解决方案,通过移动开始和结束角度、旋转和 y 轴半径来制作椭圆动画。绘制边界框及其边界的椭圆。

2022 年 4 月更新

示例显示了完整椭圆

boundEllipseAll
和椭圆段
boundEllipse
的使用。

注意

boundEllipse
仅适用于椭圆线段,其中
endAngle
n 和
startAngle
m 符合规则
{m <= n <= m + 2Pi}

修复了

boundEllipse
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>


0
投票

我无法评论 @Blindman67 的答案,因为我在 stackoverflow 上的声誉点还不够。所以我把它摇到这里。

万一有人像我一样想知道代码中的这一行:

extrema(-Math.atan((rx * xAy) / (ry * xAx)))
,这就是为什么它不是
Math.atan( (ry * xAx)/(rx * xAy) )
,注意分母和分子的位置交换 ,因为前面的负号,后面加上 Pi/2 ,也能得到同样的效果。

前一行的负号来自表达式本身,因此具有完全不同的含义。所以不要像我一样被误导。

如果您将相关行更改为: 'Math.atan( (ry * xAx)/(rx * xAy) )' ,根据我的测试,它仍然有效。

© www.soinside.com 2019 - 2024. All rights reserved.