19

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.

James
  • 109,676
  • 31
  • 162
  • 175
  • 2
    see [Converting an svg arc to lines](https://stackoverflow.com/a/41544540/2521214) the center there is in `sx,sy` and `cx,cy` – Spektre Apr 14 '20 at 07:48
  • There is ambiguity whether this is the center of the arc, center of the elipse where rx and ry start and arc in a bounding box (see my codepen [Where is the center of the arc?](https://codepen.io/owaine1/pen/MWaKZKz)). I'm now to the idea it's center of ellipse and with the provided documentation it's about finding coords for shape at center, exactly as the circle and ellipse cx and cy attributes do. – Owain Evans Apr 16 '20 at 07:26

2 Answers2

13

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
// phi, startAngle, deltaAngle, endAngle are radians not degrees.
// SVG use degrees, convert it to radians by:
// phi = degree * Math.PI / 180;
function svgArcToCenterParam(x1, y1, rx, ry, phi, fA, fS, x2, y2) {
    var cx, cy, startAngle, deltaAngle, endAngle;
    var PIx2 = Math.PI * 2.0;

    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,
        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,
    startAngle: 2.356194477985314,
    deltaAngle: -3.141592627780225,
    endAngle: 5.497787157384675,
    clockwise: false
}
*/
cuixiping
  • 24,167
  • 8
  • 82
  • 93
  • 2
    This is a good implementation of the referenced SVG appendix. Unfortunately, it doesn't work in all cases. There are some (not very uncommon) edge cases in which the value inside the sqrt when calculating `coe` is negative. See `computeArc` in http://svn.apache.org/repos/asf/xmlgraphics/batik/branches/svg11/sources/org/apache/batik/ext/awt/geom/ExtendedGeneralPath.java. Notice that if the value is negative, this implementation takes the center point is (0,0), which seems to work for me. An example is an arc with rx=ry=60 from (0, 100) to (100, 0). – brianmearns Apr 06 '13 at 03:17
  • I fixed the bug you pointed out. Can you try again? @brianmearns – cuixiping Mar 21 '18 at 12:58
  • Sorry, 5 years, I've lost context for this. If i'm understanding correctly, it looks like my initial comment includes an example arc that you could use to test it. – brianmearns Mar 22 '18 at 13:34
  • I added the case in your comment to my answer. Thank you. @brianmearns – cuixiping Mar 23 '18 at 01:46
  • can you tell the formula for calculating rx and ry from center point ? – Kira Nov 20 '18 at 10:35
  • I had to make an adjustment to my Python-version of your algorithm to make small radii properly scale up to fit the ellipse to the end: https://github.com/Ghostkeeper/SVGToolpathReader/commit/0e04c39fddbd12000321782d38aa3631f9b603eb You might need to remove that if. – Ghostkeeper Feb 26 '19 at 00:19
  • @Kira I added the link of detailed explanation. – cuixiping Apr 17 '20 at 03:17
  • @cuixiping sorry to bother, the spec you're using is 11 years old, does it still apply today? @Ghostkeeper which `if` are you referring to? – thednp May 06 '22 at 06:35
  • I just gave this a test and it doesn't work. Also tested @Ghostkeeper's fix, not helping. – thednp May 06 '22 at 07:09
  • to @thednp : Please post your test case data here. – cuixiping May 16 '23 at 06:39
  • to @thednp : SVG use degrees, if your input is degree from svg, you should convert degree to radian with `phi = degree * Math.PI / 180;` – cuixiping May 16 '23 at 06:51
2

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).

MBo
  • 77,366
  • 5
  • 53
  • 86