计算SVG圆弧的中心

发布于 2024-12-29 05:44:43 字数 599 浏览 0 评论 0原文

我有以下信息:

  • radiusX (rx)
  • radiusY (ry)
  • x1
  • y1
  • x2
  • y2

SVG 规范允许您通过指定其半径以及起点和终点来定义圆弧。还有其他选项,例如 Large-arc-flag 和 sweep-flag ,它们有助于定义起点如何到达终点。 更多详细信息请参见此处

我不太擅长数学,所以理解所有这些几乎是不可能的。

我想我正在寻找一个简单的方程,让我知道在 SVG 的 arc 命令接受的所有参数的情况下的 centerX 和 centerY 值。

任何帮助表示赞赏。

我搜索过 stackoverflow,但似乎没有一个答案可以用简单的英语解释解决方案。

I have the following information:

  • radiusX (rx)
  • radiusY (ry)
  • x1
  • y1
  • x2
  • y2

The SVG spec allows you to define an arc by specifying its radius, and start and end points. There are other options such as large-arc-flag and sweep-flag which help to define how you want the start-point to reach the end-point. More details here.

I am not mathematically inclined, so understanding all of this is near impossible.

I guess I am looking for a simple equation that results in me knowing the centerX and centerY values given all the arguments accepted by SVG's arc command.

Any help is appreciated.

I've search stackoverflow and none of the answers seem to explain the solution in plain english.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

不即不离 2025-01-05 05:44:43

根据 W3C SVG 1.1 规范: 从端点参数化到中心参数化的转换

您可以看看详细的解释。

这是一个 JavaScript 实现。

// svg : [A | a] (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+

function  radian( ux, uy, vx, vy ) {
    var  dot = ux * vx + uy * vy;
    var  mod = Math.sqrt( ( ux * ux + uy * uy ) * ( vx * vx + vy * vy ) );
    var  rad = Math.acos( dot / mod );
    if( ux * vy - uy * vx < 0.0 ) {
        rad = -rad;
    }
    return rad;
}

//conversion_from_endpoint_to_center_parameterization
//sample :  svgArcToCenterParam(200,200,50,50,0,1,1,300,200)
// x1 y1 rx ry φ fA fS x2 y2
// φ(degree) is degrees as same as SVG not radians.
// startAngle, deltaAngle, endAngle are radians not degrees.
function svgArcToCenterParam(x1, y1, rx, ry, degree, fA, fS, x2, y2) {
    var cx, cy, startAngle, deltaAngle, endAngle;
    var PIx2 = Math.PI * 2.0;
    var phi = degree * Math.PI / 180;

    if (rx < 0) {
        rx = -rx;
    }
    if (ry < 0) {
        ry = -ry;
    }
    if (rx == 0.0 || ry == 0.0) { // invalid arguments
        throw Error('rx and ry can not be 0');
    }

    // SVG use degrees, if your input is degree from svg,
    // you should convert degree to radian as following line.
    // phi = phi * Math.PI / 180;
    var s_phi = Math.sin(phi);
    var c_phi = Math.cos(phi);
    var hd_x = (x1 - x2) / 2.0; // half diff of x
    var hd_y = (y1 - y2) / 2.0; // half diff of y
    var hs_x = (x1 + x2) / 2.0; // half sum of x
    var hs_y = (y1 + y2) / 2.0; // half sum of y

    // F6.5.1
    var x1_ = c_phi * hd_x + s_phi * hd_y;
    var y1_ = c_phi * hd_y - s_phi * hd_x;

    // F.6.6 Correction of out-of-range radii
    //   Step 3: Ensure radii are large enough
    var lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
    if (lambda > 1) {
        rx = rx * Math.sqrt(lambda);
        ry = ry * Math.sqrt(lambda);
    }

    var rxry = rx * ry;
    var rxy1_ = rx * y1_;
    var ryx1_ = ry * x1_;
    var sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
    if (!sum_of_sq) {
        throw Error('start point can not be same as end point');
    }
    var coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
    if (fA == fS) { coe = -coe; }

    // F6.5.2
    var cx_ = coe * rxy1_ / ry;
    var cy_ = -coe * ryx1_ / rx;

    // F6.5.3
    cx = c_phi * cx_ - s_phi * cy_ + hs_x;
    cy = s_phi * cx_ + c_phi * cy_ + hs_y;

    var xcr1 = (x1_ - cx_) / rx;
    var xcr2 = (x1_ + cx_) / rx;
    var ycr1 = (y1_ - cy_) / ry;
    var ycr2 = (y1_ + cy_) / ry;

    // F6.5.5
    startAngle = radian(1.0, 0.0, xcr1, ycr1);

    // F6.5.6
    deltaAngle = radian(xcr1, ycr1, -xcr2, -ycr2);
    while (deltaAngle > PIx2) { deltaAngle -= PIx2; }
    while (deltaAngle < 0.0) { deltaAngle += PIx2; }
    if (fS == false || fS == 0) { deltaAngle -= PIx2; }
    endAngle = startAngle + deltaAngle;
    while (endAngle > PIx2) { endAngle -= PIx2; }
    while (endAngle < 0.0) { endAngle += PIx2; }

    var outputObj = { /* cx, cy, startAngle, deltaAngle */
        cx: cx,
        cy: cy,
        rx: rx,
        ry: ry,
        phi: phi,
        startAngle: startAngle,
        deltaAngle: deltaAngle,
        endAngle: endAngle,
        clockwise: (fS == true || fS == 1)
    }

    return outputObj;
}

使用示例:

svg

<path d="M 0 100 A 60 60 0 0 0 100 0"/>

js

var result = svgArcToCenterParam(0, 100, 60, 60, 0, 0, 0, 100, 0);
console.log(result);
/* will output:
{
    cx: 49.99999938964844,
    cy: 49.99999938964844,
    rx: 60,
    ry: 60,
    startAngle: 2.356194477985314,
    deltaAngle: -3.141592627780225,
    endAngle: 5.497787157384675,
    clockwise: false
}
*/

From W3C SVG 1.1 spec: Conversion from endpoint to center parameterization

You can take a look at the detailed explanation.

This is a javascript implementation.

// svg : [A | a] (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+

function  radian( ux, uy, vx, vy ) {
    var  dot = ux * vx + uy * vy;
    var  mod = Math.sqrt( ( ux * ux + uy * uy ) * ( vx * vx + vy * vy ) );
    var  rad = Math.acos( dot / mod );
    if( ux * vy - uy * vx < 0.0 ) {
        rad = -rad;
    }
    return rad;
}

//conversion_from_endpoint_to_center_parameterization
//sample :  svgArcToCenterParam(200,200,50,50,0,1,1,300,200)
// x1 y1 rx ry φ fA fS x2 y2
// φ(degree) is degrees as same as SVG not radians.
// startAngle, deltaAngle, endAngle are radians not degrees.
function svgArcToCenterParam(x1, y1, rx, ry, degree, fA, fS, x2, y2) {
    var cx, cy, startAngle, deltaAngle, endAngle;
    var PIx2 = Math.PI * 2.0;
    var phi = degree * Math.PI / 180;

    if (rx < 0) {
        rx = -rx;
    }
    if (ry < 0) {
        ry = -ry;
    }
    if (rx == 0.0 || ry == 0.0) { // invalid arguments
        throw Error('rx and ry can not be 0');
    }

    // SVG use degrees, if your input is degree from svg,
    // you should convert degree to radian as following line.
    // phi = phi * Math.PI / 180;
    var s_phi = Math.sin(phi);
    var c_phi = Math.cos(phi);
    var hd_x = (x1 - x2) / 2.0; // half diff of x
    var hd_y = (y1 - y2) / 2.0; // half diff of y
    var hs_x = (x1 + x2) / 2.0; // half sum of x
    var hs_y = (y1 + y2) / 2.0; // half sum of y

    // F6.5.1
    var x1_ = c_phi * hd_x + s_phi * hd_y;
    var y1_ = c_phi * hd_y - s_phi * hd_x;

    // F.6.6 Correction of out-of-range radii
    //   Step 3: Ensure radii are large enough
    var lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
    if (lambda > 1) {
        rx = rx * Math.sqrt(lambda);
        ry = ry * Math.sqrt(lambda);
    }

    var rxry = rx * ry;
    var rxy1_ = rx * y1_;
    var ryx1_ = ry * x1_;
    var sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
    if (!sum_of_sq) {
        throw Error('start point can not be same as end point');
    }
    var coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
    if (fA == fS) { coe = -coe; }

    // F6.5.2
    var cx_ = coe * rxy1_ / ry;
    var cy_ = -coe * ryx1_ / rx;

    // F6.5.3
    cx = c_phi * cx_ - s_phi * cy_ + hs_x;
    cy = s_phi * cx_ + c_phi * cy_ + hs_y;

    var xcr1 = (x1_ - cx_) / rx;
    var xcr2 = (x1_ + cx_) / rx;
    var ycr1 = (y1_ - cy_) / ry;
    var ycr2 = (y1_ + cy_) / ry;

    // F6.5.5
    startAngle = radian(1.0, 0.0, xcr1, ycr1);

    // F6.5.6
    deltaAngle = radian(xcr1, ycr1, -xcr2, -ycr2);
    while (deltaAngle > PIx2) { deltaAngle -= PIx2; }
    while (deltaAngle < 0.0) { deltaAngle += PIx2; }
    if (fS == false || fS == 0) { deltaAngle -= PIx2; }
    endAngle = startAngle + deltaAngle;
    while (endAngle > PIx2) { endAngle -= PIx2; }
    while (endAngle < 0.0) { endAngle += PIx2; }

    var outputObj = { /* cx, cy, startAngle, deltaAngle */
        cx: cx,
        cy: cy,
        rx: rx,
        ry: ry,
        phi: phi,
        startAngle: startAngle,
        deltaAngle: deltaAngle,
        endAngle: endAngle,
        clockwise: (fS == true || fS == 1)
    }

    return outputObj;
}

Usage example:

svg

<path d="M 0 100 A 60 60 0 0 0 100 0"/>

js

var result = svgArcToCenterParam(0, 100, 60, 60, 0, 0, 0, 100, 0);
console.log(result);
/* will output:
{
    cx: 49.99999938964844,
    cy: 49.99999938964844,
    rx: 60,
    ry: 60,
    startAngle: 2.356194477985314,
    deltaAngle: -3.141592627780225,
    endAngle: 5.497787157384675,
    clockwise: false
}
*/
乜一 2025-01-05 05:44:43

我正在考虑 x 轴旋转 = 0 的情况。
起点和终点的方程:

x1 = cx + rx * cos(StartAngle)

y1 = cy + ry * sin(StartAngle)

x2 = cx + rx * cos(EndAngle)

y2 = cy + ry * sin(EndAngle)

排除以下角度方程对给我们带来:

ry^2*(x1-cx)^2+rx^2*(y1-cy)^2=rx^2*ry^2

ry^2*(x2-cx)^2+rx^2*(y2- cy)^2=rx^2*ry^2

该方程组可以通过手动或数学包(Maple、Mathematica 等)的帮助来解析求解 (cx, cy)。二次方程有两个解(由于大弧旗和扫旗组合)。

I'm considering the case of x-axis-rotation = 0.
Equations for start and end points:

x1 = cx + rx * cos(StartAngle)

y1 = cy + ry * sin(StartAngle)

x2 = cx + rx * cos(EndAngle)

y2 = cy + ry * sin(EndAngle)

Excluding angles from equation pairs gives us:

ry^2*(x1-cx)^2+rx^2*(y1-cy)^2=rx^2*ry^2

ry^2*(x2-cx)^2+rx^2*(y2-cy)^2=rx^2*ry^2

This equation system can be analytically solved for (cx, cy) by hands or with help of math packets (Maple, Mathematica etc). There are two solutions of quadratic equation (due to large-arc-flag and sweep-flag combination).

影子的影子 2025-01-05 05:44:43

通过实施 https://stackoverflow.com/a/12329083/17719752 我遇到以下问题:

  • 如果 rx != ry,计算出的角度是错误的,
  • 有时弧度函数会导致舍入错误,导致 delta 角度没有结果

我不是确保我是否正确理解了返回的角度,以及我是否是唯一遇到此问题的人,但如果您似乎也有错误的 startAngle / endAngle,我已经更新了代码,遵循 官方圆弧到中心参数化转换 并修复(在我看来)半径的点被错误地添加到角度方程中:

/**
 * Computes the angle in radians between two 2D vectors
 * @param vx The x component of the first vector
 * @param vy The y component of the first vector
 * @param ux The x component of the second vector
 * @param uy The y component of the second vector
 * @returns The angle in radians between the two vectors
 */
function vectorAngle(ux, uy, vx, vy) {
    const dotProduct = ux * vx + uy * vy;
    const magnitudeU = Math.sqrt(ux * ux + uy * uy);
    const magnitudeV = Math.sqrt(vx * vx + vy * vy);

    let cosTheta = dotProduct / (magnitudeU * magnitudeV);

    // Fix rounding errors leading to NaN
    if (cosTheta > 1) {
        cosTheta = 1;
    } else if (cosTheta < -1) {
        cosTheta = -1;
    }

    const angle = Math.acos(cosTheta);

    // Determine the sign based on cross product (ux * vy - uy * vx)
    const sign = (ux * vy - uy * vx) >= 0 ? 1 : -1;
    return sign * angle;
}

/**
 * Calculate the center of the ellipse for the arc.
 * 
 * Based on the official W3C formula: https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter
 * 
 * @param x1 The x coordinate of the start point
 * @param y1 The y coordinate of the start point
 * @param x2 The x coordinate of the end point
 * @param y2 The y coordinate of the end point
 * @param rx The x radius of the ellipse
 * @param ry The y radius of the ellipse
 * @param rotation The rotation of the ellipse
 * @param largeArc The large arc flag
 * @param sweep The sweep flag
 */
function getCenterParameters(x1, y1, x2, y2, rx, ry, rotation, largeArc, sweep) {
    const fS = sweep === 1;
    const fA = largeArc === 1;
    const phi = rotation * (Math.PI / 180);

    // Step 0: Ensure valid parameters
    // F.6.6

    // Step 0.1: Ensure radii are non-zero
    if (rx === 0 || ry === 0) {
        // Treat as a straight line and stop further processing
        return {
            center: {x: (x1 + x2) / 2, y: (y1 + y2) / 2},
        };
    }

    // Step 1: Compute (x1′, y1′)
    // F.6.5.1

    const cosPhi = Math.cos(phi);
    const sinPhi = Math.sin(phi);

    const dx = (x1 - x2) / 2;
    const dy = (y1 - y2) / 2;

    const x1Prime = cosPhi * dx + sinPhi * dy;
    const y1Prime = -sinPhi * dx + cosPhi * dy;

    // Step 2: Compute (cx′, cy′)
    // F.6.5.2

    // Compute the square of radii
    const rx2 = rx * rx;
    const ry2 = ry * ry;

    // Compute the square of the transformed points
    const x1Prime2 = x1Prime * x1Prime;
    const y1Prime2 = y1Prime * y1Prime;


    // Step 0.3: Ensure radii are large enough
    const Lambda = (x1Prime2 / (rx * rx)) + (y1Prime2 / (ry * ry));

    if (Lambda > 1) {
        const scale = Math.sqrt(Lambda);
        rx *= scale;
        ry *= scale;
    }


    // Compute the denominator
    const denom = rx2 * y1Prime2 + ry2 * x1Prime2;

    // Compute the numerator
    const num = rx2 * ry2 - rx2 * y1Prime2 - ry2 * x1Prime2;

    // Handle the case where the numerator becomes negative, which would result in an imaginary number
    // This can happen if the radii are too small. So we clamp the number to 0 if it's negative.
    const adjustedNum = Math.max(num, 0);

    // Choose the sign for the square root based on fA and fS
    const sign = fA !== fS ? 1 : -1;

    // Compute (cx', cy')
    const sqrtTerm = sign * Math.sqrt(adjustedNum / denom);

    const cxPrime = sqrtTerm * (rx * y1Prime / ry);
    const cyPrime = sqrtTerm * (-ry * x1Prime / rx);


    // Step 3: Compute (cx, cy) from (cx', cy')
    // F.6.5.3

    // Compute the midpoints of the endpoints
    const midX = (x1 + x2) / 2;
    const midY = (y1 + y2) / 2;

    // Calculate (cx, cy)
    const cx = cosPhi * cxPrime - sinPhi * cyPrime + midX;
    const cy = sinPhi * cxPrime + cosPhi * cyPrime + midY;

    // Step 4: Compute theta1 and deltaTheta

    // F.6.5.5
    // Compute theta1
    // CAUTION: This does not result in the correct angle if rx and ry are different
    // const theta1 = vectorAngle(1, 0, (x1Prime - cxPrime) / rx, (y1Prime - cyPrime) / ry);

    const theta1 = vectorAngle(1, 0, (x1Prime - cxPrime), (y1Prime - cyPrime));

    // For global points rotate the vector (1, 0) by phi
    const rotatedXVector = [Math.cos(-phi), Math.sin(-phi)];
    const globalTheta1 = vectorAngle(rotatedXVector[0], rotatedXVector[1], (x1Prime - cxPrime), (y1Prime - cyPrime));

    // F.6.5.6
    // Compute deltaTheta

    // CAUTION: This does not result in the correct angle if rx and ry are different
    // const deltaTheta = vectorAngle(
    //     (x1Prime - cxPrime) / rx, (y1Prime - cyPrime) / ry,
    //     (-x1Prime - cxPrime) / rx, (-y1Prime - cyPrime) / ry
    // );

    const deltaTheta = vectorAngle(
        (x1Prime - cxPrime), (y1Prime - cyPrime),
        (-x1Prime - cxPrime), (-y1Prime - cyPrime)
    );

    // Modulo deltaTheta by 360 degrees
    let deltaThetaDegrees = deltaTheta * (180 / Math.PI) % 360;
    let globalTheta1Degrees = globalTheta1 * (180 / Math.PI) % 360;

    // Adjust deltaTheta based on the sweep flag fS
    if (!fS && deltaThetaDegrees > 0) {
        deltaThetaDegrees -= 360;
    } else if (fS && deltaThetaDegrees < 0) {
        deltaThetaDegrees += 360;
    }

    if (!fS && globalTheta1Degrees > 0) {
        globalTheta1Degrees -= 360;
    } else if (fS && globalTheta1Degrees < 0) {
        globalTheta1Degrees += 360;
    }

    // Convert theta1 to degrees
    const theta1Degrees = theta1 * (180 / Math.PI);


    return {
        // Center of the ellipse
        center: { x: cx, y: cy },
        rx: rx,
        ry: ry,
        // Local angles (relative to the ellipse's x-axis)
        startAngleDeg: theta1Degrees,
        deltaAngleDeg: deltaThetaDegrees,
        endAngleDeg: theta1Degrees + deltaThetaDegrees,
        startAngle: theta1,
        deltaAngle: deltaTheta,
        endAngle: theta1 + deltaTheta,
        // Global angles (relative to the global x-axis with vector (1, 0))
        startAngleGlobalDeg: globalTheta1Degrees,
        deltaAngleGlobalDeg: deltaThetaDegrees,
        endAngleGlobalDeg: globalTheta1Degrees + deltaThetaDegrees,
        startAngleGlobal: globalTheta1,
        deltaAngleGlobal: deltaTheta,
        endAngleGlobal: globalTheta1 + deltaTheta
    };

}

此 codepen 显示了现有计算角度之间的差异以及新的计算方法。左边的椭圆显示旧计算方法的计算角度,右边的椭圆显示新的角度:

显示旧(右)和新(左)解决方案之间计算角度差异的图像。

使用示例:

SVG:

<path d="M 120 100 A 50 150 0 0 1 130 170"/>

JS:


// Params of the SVG path
const x1 = 120;
const y1 = 100;
const rx = 50;
const ry = 150;
const rotation = 0;
const largeArc = 0;
const sweep = 1;
const x2 = 130;
const y2 = 170;
const centerParams = getCenterParameters(x0, y0, x1, y1, rx, ry, rotation, largeArc, sweep);

/* Will output:
{
  "center": {
      "x": 80.54825251572072,
      "y": 192.15224676550196
  },
  "rx": 50,
  "ry": 150,
  "startAngleDeg": -66.82351270319913,
  "deltaAngleDeg": 42.693194379779385,
  "endAngleDeg": -24.130318323419743,
  "startAngle": -1.166290314419081,
  "deltaAngle": 0.7451368101210887,
  "endAngle": -0.42115350429799236,
  "startAngleGlobal": -1.166290314419081,
  "deltaAngleGlobal": 0.7451368101210887,
  "endAngleGlobal": -0.42115350429799236
}*/

With the implementation of https://stackoverflow.com/a/12329083/17719752 I have the following problem:

  • if rx != ry, the calculated angles are wrong
  • sometimes the radians function leads to rounding errors, resulting in no result for the delta angle

I'm not sure if I understood the returned angles correctly and if I'm the only one with this problem, but in case you also seem to have a wrong startAngle / endAngle, I've updated the code, following the official arc to center parameterization conversion and fixing the point where (in my opinion) the radii are wrongly added to the angle equation:

/**
 * Computes the angle in radians between two 2D vectors
 * @param vx The x component of the first vector
 * @param vy The y component of the first vector
 * @param ux The x component of the second vector
 * @param uy The y component of the second vector
 * @returns The angle in radians between the two vectors
 */
function vectorAngle(ux, uy, vx, vy) {
    const dotProduct = ux * vx + uy * vy;
    const magnitudeU = Math.sqrt(ux * ux + uy * uy);
    const magnitudeV = Math.sqrt(vx * vx + vy * vy);

    let cosTheta = dotProduct / (magnitudeU * magnitudeV);

    // Fix rounding errors leading to NaN
    if (cosTheta > 1) {
        cosTheta = 1;
    } else if (cosTheta < -1) {
        cosTheta = -1;
    }

    const angle = Math.acos(cosTheta);

    // Determine the sign based on cross product (ux * vy - uy * vx)
    const sign = (ux * vy - uy * vx) >= 0 ? 1 : -1;
    return sign * angle;
}

/**
 * Calculate the center of the ellipse for the arc.
 * 
 * Based on the official W3C formula: https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter
 * 
 * @param x1 The x coordinate of the start point
 * @param y1 The y coordinate of the start point
 * @param x2 The x coordinate of the end point
 * @param y2 The y coordinate of the end point
 * @param rx The x radius of the ellipse
 * @param ry The y radius of the ellipse
 * @param rotation The rotation of the ellipse
 * @param largeArc The large arc flag
 * @param sweep The sweep flag
 */
function getCenterParameters(x1, y1, x2, y2, rx, ry, rotation, largeArc, sweep) {
    const fS = sweep === 1;
    const fA = largeArc === 1;
    const phi = rotation * (Math.PI / 180);

    // Step 0: Ensure valid parameters
    // F.6.6

    // Step 0.1: Ensure radii are non-zero
    if (rx === 0 || ry === 0) {
        // Treat as a straight line and stop further processing
        return {
            center: {x: (x1 + x2) / 2, y: (y1 + y2) / 2},
        };
    }

    // Step 1: Compute (x1′, y1′)
    // F.6.5.1

    const cosPhi = Math.cos(phi);
    const sinPhi = Math.sin(phi);

    const dx = (x1 - x2) / 2;
    const dy = (y1 - y2) / 2;

    const x1Prime = cosPhi * dx + sinPhi * dy;
    const y1Prime = -sinPhi * dx + cosPhi * dy;

    // Step 2: Compute (cx′, cy′)
    // F.6.5.2

    // Compute the square of radii
    const rx2 = rx * rx;
    const ry2 = ry * ry;

    // Compute the square of the transformed points
    const x1Prime2 = x1Prime * x1Prime;
    const y1Prime2 = y1Prime * y1Prime;


    // Step 0.3: Ensure radii are large enough
    const Lambda = (x1Prime2 / (rx * rx)) + (y1Prime2 / (ry * ry));

    if (Lambda > 1) {
        const scale = Math.sqrt(Lambda);
        rx *= scale;
        ry *= scale;
    }


    // Compute the denominator
    const denom = rx2 * y1Prime2 + ry2 * x1Prime2;

    // Compute the numerator
    const num = rx2 * ry2 - rx2 * y1Prime2 - ry2 * x1Prime2;

    // Handle the case where the numerator becomes negative, which would result in an imaginary number
    // This can happen if the radii are too small. So we clamp the number to 0 if it's negative.
    const adjustedNum = Math.max(num, 0);

    // Choose the sign for the square root based on fA and fS
    const sign = fA !== fS ? 1 : -1;

    // Compute (cx', cy')
    const sqrtTerm = sign * Math.sqrt(adjustedNum / denom);

    const cxPrime = sqrtTerm * (rx * y1Prime / ry);
    const cyPrime = sqrtTerm * (-ry * x1Prime / rx);


    // Step 3: Compute (cx, cy) from (cx', cy')
    // F.6.5.3

    // Compute the midpoints of the endpoints
    const midX = (x1 + x2) / 2;
    const midY = (y1 + y2) / 2;

    // Calculate (cx, cy)
    const cx = cosPhi * cxPrime - sinPhi * cyPrime + midX;
    const cy = sinPhi * cxPrime + cosPhi * cyPrime + midY;

    // Step 4: Compute theta1 and deltaTheta

    // F.6.5.5
    // Compute theta1
    // CAUTION: This does not result in the correct angle if rx and ry are different
    // const theta1 = vectorAngle(1, 0, (x1Prime - cxPrime) / rx, (y1Prime - cyPrime) / ry);

    const theta1 = vectorAngle(1, 0, (x1Prime - cxPrime), (y1Prime - cyPrime));

    // For global points rotate the vector (1, 0) by phi
    const rotatedXVector = [Math.cos(-phi), Math.sin(-phi)];
    const globalTheta1 = vectorAngle(rotatedXVector[0], rotatedXVector[1], (x1Prime - cxPrime), (y1Prime - cyPrime));

    // F.6.5.6
    // Compute deltaTheta

    // CAUTION: This does not result in the correct angle if rx and ry are different
    // const deltaTheta = vectorAngle(
    //     (x1Prime - cxPrime) / rx, (y1Prime - cyPrime) / ry,
    //     (-x1Prime - cxPrime) / rx, (-y1Prime - cyPrime) / ry
    // );

    const deltaTheta = vectorAngle(
        (x1Prime - cxPrime), (y1Prime - cyPrime),
        (-x1Prime - cxPrime), (-y1Prime - cyPrime)
    );

    // Modulo deltaTheta by 360 degrees
    let deltaThetaDegrees = deltaTheta * (180 / Math.PI) % 360;
    let globalTheta1Degrees = globalTheta1 * (180 / Math.PI) % 360;

    // Adjust deltaTheta based on the sweep flag fS
    if (!fS && deltaThetaDegrees > 0) {
        deltaThetaDegrees -= 360;
    } else if (fS && deltaThetaDegrees < 0) {
        deltaThetaDegrees += 360;
    }

    if (!fS && globalTheta1Degrees > 0) {
        globalTheta1Degrees -= 360;
    } else if (fS && globalTheta1Degrees < 0) {
        globalTheta1Degrees += 360;
    }

    // Convert theta1 to degrees
    const theta1Degrees = theta1 * (180 / Math.PI);


    return {
        // Center of the ellipse
        center: { x: cx, y: cy },
        rx: rx,
        ry: ry,
        // Local angles (relative to the ellipse's x-axis)
        startAngleDeg: theta1Degrees,
        deltaAngleDeg: deltaThetaDegrees,
        endAngleDeg: theta1Degrees + deltaThetaDegrees,
        startAngle: theta1,
        deltaAngle: deltaTheta,
        endAngle: theta1 + deltaTheta,
        // Global angles (relative to the global x-axis with vector (1, 0))
        startAngleGlobalDeg: globalTheta1Degrees,
        deltaAngleGlobalDeg: deltaThetaDegrees,
        endAngleGlobalDeg: globalTheta1Degrees + deltaThetaDegrees,
        startAngleGlobal: globalTheta1,
        deltaAngleGlobal: deltaTheta,
        endAngleGlobal: globalTheta1 + deltaTheta
    };

}

This codepen shows the differences of the calculated angles between the existing and the new calculation method. The left ellipse shows the calculated angles of the old calculated method, the right ellipse the new ones:

Image showing the difference in the calculated angles between the old (right) and the new (left) solution.

Usage example:

SVG:

<path d="M 120 100 A 50 150 0 0 1 130 170"/>

JS:


// Params of the SVG path
const x1 = 120;
const y1 = 100;
const rx = 50;
const ry = 150;
const rotation = 0;
const largeArc = 0;
const sweep = 1;
const x2 = 130;
const y2 = 170;
const centerParams = getCenterParameters(x0, y0, x1, y1, rx, ry, rotation, largeArc, sweep);

/* Will output:
{
  "center": {
      "x": 80.54825251572072,
      "y": 192.15224676550196
  },
  "rx": 50,
  "ry": 150,
  "startAngleDeg": -66.82351270319913,
  "deltaAngleDeg": 42.693194379779385,
  "endAngleDeg": -24.130318323419743,
  "startAngle": -1.166290314419081,
  "deltaAngle": 0.7451368101210887,
  "endAngle": -0.42115350429799236,
  "startAngleGlobal": -1.166290314419081,
  "deltaAngleGlobal": 0.7451368101210887,
  "endAngleGlobal": -0.42115350429799236
}*/
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文