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,逆时针?
这个答案包含两个精确的解决方案,它不是近似值。
解决办法是
boundEllipseAll
将找到完整椭圆的边界。如果比完整解决方案复杂得多,但您需要确保 x 半径大于 y 半径(例如,将椭圆旋转 90 度并交换 x、y 半径)
boundEllipse
将找到椭圆段的边界。它适用于所有省略号,但我没有包括 CCW 标志。要获得 CCW 椭圆的边界,请交换起始角度和结束角度。
它的工作原理是首先找到起点和终点的 x、y 坐标,计算沿每个轴的最小值和最大值。然后,它计算极值角度,首先计算 x 轴极值,然后计算 y 轴极值。
如果极值角度位于起始角度和结束角度之间,则计算该角度的 x,y 位置,并根据最小和最大范围测试点。
有很大的优化空间,因为许多点只需要 x 或 y 部分,并且如果正在处理的轴的 min 和 max 发生变化,函数
extrema
中的内部 while 循环可以提前退出。
该示例确保我没有犯任何错误,并使用第二种解决方案,通过移动开始和结束角度、旋转和 y 轴半径来制作椭圆动画。绘制边界框及其边界的椭圆。
示例显示了完整椭圆
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>
我无法评论 @Blindman67 的答案,因为我在 stackoverflow 上的声誉点还不够。所以我把它摇到这里。
万一有人像我一样想知道代码中的这一行:
extrema(-Math.atan((rx * xAy) / (ry * xAx)))
,这就是为什么它不是 Math.atan( (ry * xAx)/(rx * xAy) )
,注意分母和分子的位置交换
,因为前面的负号,后面加上 Pi/2 ,也能得到同样的效果。
前一行的负号来自表达式本身,因此具有完全不同的含义。所以不要像我一样被误导。
如果您将相关行更改为: 'Math.atan( (ry * xAx)/(rx * xAy) )' ,根据我的测试,它仍然有效。