如何通过两个点和半径大小计算椭圆的中心

23

在为基于其自身VML格式的Internet Explorer实现SVG时,我遇到了将SVG椭圆弧转换为VML椭圆弧的问题。

在VML中,通过两个点的两个角度和半径长度定义一条弧线, 在SVG中,通过椭圆边界框上两个点的两个坐标对定义一条弧线。

因此,问题是:如何将椭圆的两个点的角度表达为它们的坐标对。 一个中间问题可能是:如何通过椭圆曲线上一对点的坐标找到椭圆的中心。

更新: 假设一个椭圆通常放置(它的半径平行于线性坐标系统轴),因此不会应用旋转。

更新: 这个问题与svg:ellipse元素无关,而与svg:path元素中的“a”椭圆弧命令有关(SVG Paths: The elliptical arc curve commands)

6个回答

31
所以解决方案在这里:
椭圆的参数化公式:
x = x0 + a * cos(t) y = y0 + b * sin(t)
将两个已知点的坐标代入:
x1 = x0 + a * cos(t1) x2 = x0 + a * cos(t2) y1 = y0 + b * sin(t1) y2 = y0 + b * sin(t2)
现在我们有一个带有4个变量的方程组:椭圆中心(x0 / y0)和两个角度t1,t2。
为了消除中心坐标,让我们按顺序减去方程:
x1 - x2 = a * (cos(t1) - cos(t2)) y1 - y2 = b * (sin(t1) - sin(t2))
这可以重写为(使用乘积到和恒等式公式):
(x1 - x2) / (2 * a) = sin((t1 + t2) / 2) * sin((t1 - t2) / 2) (y2 - y1) / (2 * b) = cos((t1 + t2) / 2) * sin((t1 - t2) / 2)
替换一些方程:
r1:(x1 - x2) / (2 * a) r2:(y2 - y1) / (2 * b) a1:(t1 + t2) / 2 a2:(t1 - t2) / 2
然后我们得到简单的方程组:
r1 = sin(a1) * sin(a2) r2 = cos(a1) * sin(a2)
将第一个方程除以第二个方程得到:
a1 = arctan(r1/r2)
将此结果添加到第一个方程中得到:
a2 = arcsin(r2 / cos(arctan(r1/r2)))
或者,简单地(使用三角函数和反三角函数的组合):
a2 = arcsin(r2 / (1 / sqrt(1 + (r1/r2)^2)))
甚至更简单:
a2 = arcsin(sqrt(r1^2 + r2^2))
现在可以轻松解决最初的四方程组,并找到所有角度以及椭圆中心坐标。

3
明确表示:t1 = a1+a2,t2 = a1-a2,x0 = x1 - acos(t1),y0 = y1 - bsin(t1) - AndrewS
1
请问你能提供一个将SVG弧线转换为VML弧线的示例吗?例如,"A80 80 0 1 0 200 200" 将产生一个从半径80的顺时针弧线到点200,200的大弧线。在VML中如何复制这个效果呢?谢谢! - DeadPassive
另外,y0 = y1 - b*sin(t1) 正确吗?还是应该是 sin(t2)? - DeadPassive
在最后一步要小心,将 r2 放入 sqrt 中会使其符号消失,从而导致某些情况下得到错误的结果! - Taco de Wolff
我已经实现了这个并检查了导出。这里有一个微妙的错误。r2 应该是 (y1 - y2) / (2 * b)(而不是 (y2 - y1) / (2 * b)),并且 a1 的结果为 arctan(-r1/r2)(而不是 arctan(r1/r2))。在我的实现中,原始方程导致椭圆位于 x1、y1 的相反侧,而通过我的更改,椭圆现在处于正确的位置。 - Timwi
我认为句子“将此结果添加到第一个方程中”应该改为“将此结果插入到第二个方程中”。否则,回答很好!我已经将其编写成代码并在下面发布了。 - NoBullsh1t

9
您发布的椭圆曲线弧链接包括椭圆弧实现注释的链接
在那里,您将找到从端点到中心参数化的转换方程式
这是我使用Sylvester.js执行矩阵和向量计算,从椭圆弧路径交互演示中获取的这些方程式的JavaScript实现。
// Calculate the centre of the ellipse
// Based on http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
var x1 = 150;  // Starting x-point of the arc
var y1 = 150;  // Starting y-point of the arc
var x2 = 400;  // End x-point of the arc
var y2 = 300;  // End y-point of the arc
var fA = 1;    // Large arc flag
var fS = 1;    // Sweep flag
var rx = 100;  // Horizontal radius of ellipse
var ry =  50;  // Vertical radius of ellipse
var phi = 0;   // Angle between co-ord system and ellipse x-axes

var Cx, Cy;

// Step 1: Compute (x1′, y1′)
var M = $M([
               [ Math.cos(phi), Math.sin(phi)],
               [-Math.sin(phi), Math.cos(phi)]
            ]);
var V = $V( [ (x1-x2)/2, (y1-y2)/2 ] );
var P = M.multiply(V);

var x1p = P.e(1);  // x1 prime
var y1p = P.e(2);  // y1 prime


// Ensure radii are large enough
// Based on http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
// Step (a): Ensure radii are non-zero
// Step (b): Ensure radii are positive
rx = Math.abs(rx);
ry = Math.abs(ry);
// Step (c): Ensure radii are large enough
var lambda = ( (x1p * x1p) / (rx * rx) ) + ( (y1p * y1p) / (ry * ry) );
if(lambda > 1)
{
    rx = Math.sqrt(lambda) * rx;
    ry = Math.sqrt(lambda) * ry;
}


// Step 2: Compute (cx′, cy′)
var sign = (fA == fS)? -1 : 1;
// Bit of a hack, as presumably rounding errors were making his negative inside the square root!
if((( (rx*rx*ry*ry) - (rx*rx*y1p*y1p) - (ry*ry*x1p*x1p) ) / ( (rx*rx*y1p*y1p) + (ry*ry*x1p*x1p) )) < 1e-7)
    var co = 0;
else
    var co = sign * Math.sqrt( ( (rx*rx*ry*ry) - (rx*rx*y1p*y1p) - (ry*ry*x1p*x1p) ) / ( (rx*rx*y1p*y1p) + (ry*ry*x1p*x1p) ) );
var V = $V( [rx*y1p/ry, -ry*x1p/rx] );
var Cp = V.multiply(co);

// Step 3: Compute (cx, cy) from (cx′, cy′)
var M = $M([
               [ Math.cos(phi), -Math.sin(phi)],
               [ Math.sin(phi),  Math.cos(phi)]
            ]);
var V = $V( [ (x1+x2)/2, (y1+y2)/2 ] );
var C = M.multiply(Cp).add(V);

Cx = C.e(1);
Cy = C.e(2);

3

椭圆不能仅由两个点定义。即使是圆(一种特殊的椭圆),也需要三个点来定义。

即使有三个点,也会有无限个通过这三个点的椭圆(想象一下旋转)。

请注意,边界框建议椭圆的中心,并且很可能假定其长轴和短轴与x,y(或y,x)轴平行。


确实,我忘记在那上面加注释了。现在会加上的。 - Sergey Ilinsky

1

中间问题相当简单...你不需要。你可以从边界框中计算出椭圆的中心(也就是说,只要椭圆在框中心,框的中心就是椭圆的中心)。

对于你的第一个问题,我建议查看椭圆方程的极坐标形式,该形式可在Wikipedia上找到。您还需要计算椭圆的离心率。

或者您可以通过暴力法从边界框中获取值...计算点是否位于椭圆上并匹配角度,并遍历边界框中的每个点。


我在谈论svg:path中的椭圆弧,而不是椭圆。 至于数学方面,我已经做了三篇关于在极坐标和直角坐标系中解方程的论文,但仍然没有成功。 - Sergey Ilinsky
它应该仍然是同样的过程。椭圆弧只是由椭圆的2个“边界点”组成的一部分。边界矩形应足够提供您构造完整椭圆所需的所有信息。然后,您只需要将角度输入公式即可。 - workmad3
我再次没有正确表达问题。给出的不是边界矩形,而是半径的大小。 - Sergey Ilinsky
好的,给定的半径将让您制定一个边界矩形。即 x 半径将让您获得 +x 和 -x 的值,y 也是一样。这些组合将为您提供边界矩形的所有点。如果没有给出中心,则唯一的结论是它为 0,0。 - workmad3
这是问题,中心不在0,0点(它可以在任何地方找到),一个中间步骤可以是找到中心坐标,然后计算角度会更容易(给定点的坐标和边界矩形大小)。 - Sergey Ilinsky

0

基于Rikki的回答,使用TypeScript实现。

计算过程中使用默认的DOMMatrix和DOMPoint(在最新版本Chrome v.80中测试)而非外部库。

 ellipseCenter(
    x1: number,
    y1: number,
    rx: number,
    ry: number,
    rotateDeg: number,
    fa: number,
    fs: number,
    x2: number,
    y2: number
  ): DOMPoint {
    const phi = ((rotateDeg % 360) * Math.PI) / 180;
    const m = new DOMMatrix([
      Math.cos(phi),
      -Math.sin(phi),
      Math.sin(phi),
      Math.cos(phi),
      0,
      0,
    ]);
    let v = new DOMPoint((x1 - x2) / 2, (y1 - y2) / 2).matrixTransform(m);
    const x1p = v.x;
    const y1p = v.y;
    rx = Math.abs(rx);
    ry = Math.abs(ry);
    const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
    if (lambda > 1) {
      rx = Math.sqrt(lambda) * rx;
      ry = Math.sqrt(lambda) * ry;
    }
    const sign = fa === fs ? -1 : 1;
    const div =
      (rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p) /
      (rx * rx * y1p * y1p + ry * ry * x1p * x1p);

    const co = sign * Math.sqrt(Math.abs(div));

    // inverse matrix b and c
    m.b *= -1;
    m.c *= -1;
    v = new DOMPoint(
      ((rx * y1p) / ry) * co,
      ((-ry * x1p) / rx) * co
    ).matrixTransform(m);
    v.x += (x1 + x2) / 2;
    v.y += (y1 + y2) / 2;
    return v;
  }


0

使用代码回答问题的一部分

如何通过椭圆曲线上一对点的坐标找到椭圆的中心。

这是一个TypeScript函数,它基于Sergey Illinsky的优秀答案(在我的看法中有些不完整)。它计算具有给定半径的椭圆的中心,条件是提供的两个点ab必须位于椭圆的周长上。由于此问题通常有两个解决方案,因此代码选择将椭圆放置在这两个点“上方”的解决方案:

(请注意,椭圆的长轴和短轴必须平行于水平/垂直)

/**
 * We're in 2D, so that's what our vertors look like
 */
export type Point = [number, number];

/**
 * Calculates the vector that connects the two points
 */
function deltaXY (from: Point, to: Point): Point {
    return [to[0]-from[0], to[1]-from[1]];
}

/**
 * Calculates the sum of an arbitrary amount of vertors
 */
function vecAdd (...vectors: Point[]): Point {
    return vectors.reduce((acc, curr) => [acc[0]+curr[0], acc[1]+curr[1]], [0, 0]);
}

/**
 * Given two points a and b, as well as ellipsis radii rX and rY, this 
 * function calculates the center-point of the ellipse, so that it
 * is "above" the two points and has them on the circumference
 */
function topLeftOfPointsCenter (a: Point, b: Point, rX: number, rY: number): Point {
    const delta = deltaXY(a, b);
    
    // Sergey's work leads up to a simple system of liner equations. 
    // Here, we calculate its general solution for the first of the two angles (t1)
    const A = Math.asin(Math.sqrt((delta[0]/(2*rX))**2+(delta[1]/(2*rY))**2));
    const B = Math.atan(-delta[0]/delta[1] * rY/rX);
    const alpha = A + B;
    
    // This may be the new center, but we don't know to which of the two
    // solutions it belongs, yet
    let newCenter = vecAdd(a, [
        rX * Math.cos(alpha),
        rY * Math.sin(alpha)
    ]);

    // Figure out if it is the correct solution, and adjusting if not
    const mean = vecAdd(a, [delta[0] * 0.5, delta[1] * 0.5]);
    const factor = mean[1] > newCenter[1] ? 1 : -1;
    const offMean = deltaXY(mean, newCenter);
    newCenter = vecAdd(mean, [offMean[0] * factor, offMean[1] * factor]);

    return newCenter;
}

此函数不会检查解决方案是否可行,也就是说,提供的半径是否足够连接这两个点!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接