Vogel Vogel
Vogel Vogel

Reputation: 331

SVG: Convert Arcs to Cubic Bezier

I'm trying to do something that i though would be pretty simple: Replacing all Arcs in an SVG path with Cubic Bezier Curves.

This: http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes doesn't really help me as it doesn't really say anything about conversion.

I know how to make simple Arcs, but the SVG Arcs do have a lot of parameters.

So what i need is basically just an algorithm that takes:

rx ry x-axis-rotation large-arc-flag sweep-flag x y

(and also the start point of the arc)

and calculates:

x1 y1 x2 y2 x y

(of course, the start point, x and y keep the same values...)

Does anybody know something like this?

Thanks in advance! :-)

Upvotes: 11

Views: 6102

Answers (6)

Turns out this is impossible. I mean, mathematically, at least. You can't do circular arcs with plain cubic Bezier curves, you're always going to have an error, so depending on the arc's angle, you're going to probably need more than one Bezier curve.

With that said, cubic Bezier curves work quite well for quarter-circle arcs, so if you have arcs smaller than that, you can use a single curve, and if you have wider arcs, you can simply divide it by a sensible number (between quarter and half circle? Use two curves. Between half and three quarts? Use three. Full circle? Four curves. Pretty simple).

So, how much work is this? Turns out, if you have to do it from scratch: a fair bit of work, but you can just jump to the "okay so what is the final formula I need" and then it becomes relatively simple.

If we have angle phi, then the cubic curve approximation of your arc, provided we align the start of our arc with the x-axis (so that the arc starts at y=0, and runs counter clockwise from there), and we have an arc radius R, is:

start coordinate = {
  x: R,
  y: 0
}

control point 1 = {
  x: R,
  y: R * 4/3 * tan( phi / 4)
}

control point 2 = {
  x: R * ( cos(phi) + 4/3 * tan( phi/4 ) * sin(phi) ),
  y: R * ( sin(phi) - 4/3 * tan( phi/4 ) * cos(phi) ),
}

end coordinate = {
  x: R * cos(phi),
  y: R * sin(phi)
}

Maths! But really just "plug in angle, get the coordinates we need". Simple!

But what if our arc is not aligned to the x-axis? We apply a dumb "rotate + translate" to align our arc, and then we run the rotation/translation in reverse when we're done. Explained here.

Upvotes: 12

herrstrietzel
herrstrietzel

Reputation: 17265

Cubic approximation vs. perfect circle/ellipse elements

As explained by Mike 'Pomax' Kamermans and others we can only approximate a circular or elliptic arc.

Most conversion functions/helpers like Dmitry Baranovskiy's snap.svg/raphael.js a2c() split the arc into segments at each 90° – resulting in a rendering that's not visually distinguishable from the original arc.

However, you might occasionally need a better accuracy e.g. for length or area calculations.

... so you probably shouldn't use cubic bézier sloppy approximations at all for the aforementioned applications and opt for primitives like <circle> or the precise A arc command for <path> elements, right?

Primitives like <circle> or paths using A may not return more accurate values

It turns out, the natively supported browser method getTotalLength() doesn't return very accurate lengths when applied to primitives or A arcto path commands.

Besides, we get significant deviations in different browsers.

// circle primitive
let rx = 20;
c1Math = 2 * Math.PI * rx;
c1Method = circle.getTotalLength();
cCircleMath.textContent = c1Math;
cCircleMethod.textContent = c1Method;
cCircleDiff.textContent = c1Math - c1Method;

// circle primitive
let ry = 30;
c2MathEllipse = getEllipseLength(rx, ry);
c2Method = ellipse.getTotalLength();
cEllipseMath.textContent = c2MathEllipse;
cEllipseMethod.textContent = c2Method;
cEllipseDiff.textContent = c2MathEllipse - c2Method;

//cubic simple
cMethod2C = pathCircleC.getTotalLength();
cCircleMethod2C.textContent = cMethod2C;
cCircleDiff2C.textContent = c1Math - cMethod2C;

cMethod2EllipseC = pathEllipseC.getTotalLength();
cEllipseMethod2C.textContent = cMethod2EllipseC;
cEllipseDiff2C.textContent = c2MathEllipse - cMethod2EllipseC;

//arcto
cMethod2 = pathCircle.getTotalLength();
cCircleMethod2.textContent = cMethod2;
cCircleDiff2.textContent = c1Math - cMethod2;

cMethod2Ellipse = pathEllipse.getTotalLength();
cEllipseMethod2.textContent = cMethod2Ellipse;
cEllipseDiff2.textContent = c2MathEllipse - cMethod2Ellipse;

//cubic
let options = {
  convertQuadratic: true,
  convertArc: true,
  unshort: true,
  arcAccuracy: 1
};
let pathDataCircle = parseDtoPathData(pathCircleCubic.getAttribute("d"));
pathDataCircle = normalizePathData(pathDataCircle, options);
pathCircleCubic.setAttribute("d", pathDataToD(pathDataCircle));
let cCircle3 = pathCircleCubic.getTotalLength();
cCircleMethod3.textContent = cCircle3;
cCircleDiff3.textContent = c1Math - cCircle3;

let pathDataEllipse = parseDtoPathData(pathEllipseCubic.getAttribute("d"));
pathDataEllipse = normalizePathData(pathDataEllipse, options);
pathEllipseCubic.setAttribute("d", pathDataToD(pathDataEllipse));

let cEllipse3 = pathEllipseCubic.getTotalLength();
cEllipseMethod3.textContent = cEllipse3;
cEllipseDiff3.textContent = c2MathEllipse - cEllipse3;

//HQ

options.arcAccuracy = 2;

let pathDataCircle2 = parseDtoPathData(pathCircleCubic2.getAttribute("d"));
pathDataCircle2 = normalizePathData(pathDataCircle2, options);
pathCircleCubic2.setAttribute("d", pathDataToD(pathDataCircle2));

let cCircle4 = pathCircleCubic2.getTotalLength();
cCircleMethod4.textContent = cCircle4;
cCircleDiff4.textContent = c1Math - cCircle4;


let pathDataEllipse2 = parseDtoPathData(pathEllipseCubic2.getAttribute("d"));
pathDataEllipse2 = normalizePathData(pathDataEllipse2, options);
pathEllipseCubic2.setAttribute("d", pathDataToD(pathDataEllipse2));

let cEllipse4 = pathEllipseCubic2.getTotalLength();
cEllipseMethod4.textContent = cEllipse4;
cEllipseDiff4.textContent = c2MathEllipse - cEllipse4;



/**
 * Ramanujan approximation
 * based on: https://www.mathsisfun.com/geometry/ellipse-perimeter.html#tool
 */
function getEllipseLength(rx, ry) {
  // is circle
  if (rx === ry) {
    //console.log('is circle')
    return 2 * Math.PI * rx;
  }
  let h = Math.pow((rx - ry) / (rx + ry), 2);
  let length =
    Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)));
  return length;
}
<style>
body{
    font-family:sans-serif;
}
svg{
  border:1px solid #ccc;
  overflow:visible;
}

.diff{
  color:red
}

path {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
  fill: transparent;
  fill: #000;
  stroke-width: 0.25% !important;
}
</style>

<h3>1. Primitives</h3>
<svg id="svg" viewBox="0 0 100 60"> 
  <circle id="circle" cx="30" cy="30" r="20"/>
  <ellipse id="ellipse" cx="75" cy="30" rx="20" ry="30"/>
</svg>

<p class="result">
Circumference Circle (Math): <span id="cCircleMath"></span>  <br />  
Circumference Circle (getTotallength): <span id="cCircleMethod"></span>  <br />
Difference: <span class="diff" id="cCircleDiff"></span>  <br />
Circumference ellipse (Math): <span id="cEllipseMath"></span>  <br />  
Circumference ellipse (getTotallength): <span id="cEllipseMethod"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff"></span>  <br />
</p>


<h3>2. Paths Arctos</h3>
<svg viewBox="0 0 100 60"> 
  <path id="pathCircle" d="M50 30A20 20 0 0 1 30 50A20 20 0 0 1 10 30A20 20 0 0 1 30 10A20 20 0 0 1 50 30Z"></path>
  <path id="pathEllipse" d="M95 30A20 30 0 0 1 75 60A20 30 0 0 1 55 30A20 30 0 0 1 75 0A20 30 0 0 1 95 30Z"></path>
</svg>
<p class="result">
Circumference Circle (getTotallength): <span id="cCircleMethod2"></span>  <br />
Difference: <span class="diff" id="cCircleDiff2"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod2"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff2"></span>  <br />
</p>



<h3>3.1 Paths Cubic: a2c(); Dmitry Baranovskiy</h3>
<svg viewBox="0 0 100 60"> 
  <path id="pathCircleC" d="M50 30C50 41.046 41.046 50 30 50C18.954 50 10 41.046 10 30C10 18.954 18.954 10 30 10C41.046 10 50 18.954 50 30Z"></path>
  <path id="pathEllipseC" d="M95 30C95 46.569 86.046 60 75 60C63.954 60 55 46.569 55 30C55 13.431 63.954 0 75 0C86.046 0 95 13.431 95 30Z"></path>
</svg>
<p class="result">
Circumference Circle (getTotallength): <span id="cCircleMethod2C"></span>  <br />
Difference: <span class="diff" id="cCircleDiff2C"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod2C"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff2C"></span>  <br />
</p>


<h3>3.2 Paths Cubic Beziers (k=0.551785 for 90°)</h3>
<svg  viewBox="0 0 100 60"> 
  <path id="pathCircleCubic" d="M50 30A20 20 0 0 1 30 50A20 20 0 0 1 10 30A20 20 0 0 1 30 10A20 20 0 0 1 50 30Z"></path>
  <path id="pathEllipseCubic" d="M95 30A20 30 0 0 1 75 60A20 30 0 0 1 55 30A20 30 0 0 1 75 0A20 30 0 0 1 95 30Z"></path>
</svg>

<p class="result">
Circumference Circle (getTotallength): <span id="cCircleMethod3"></span>  <br />
Difference: <span class="diff" id="cCircleDiff3"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod3"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff3"></span>  <br />
</p>


<h3>4. Paths Cubic Beziers - more segments</h3>
<svg  viewBox="0 0 100 60"> 
  <path id="pathCircleCubic2" d="M50 30A20 20 0 0 1 30 50A20 20 0 0 1 10 30A20 20 0 0 1 30 10A20 20 0 0 1 50 30Z"></path>
  <path id="pathEllipseCubic2" d="M95 30A20 30 0 0 1 75 60A20 30 0 0 1 55 30A20 30 0 0 1 75 0A20 30 0 0 1 95 30Z"></path>
</svg>

<p class="result">
Circumference Ellipse (getTotallength): <span id="cCircleMethod4"></span>  <br />
Difference: <span class="diff" id="cCircleDiff4"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod4"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff4"></span>  <br />
</p>


<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>

      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>


<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>

      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>


<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svgHelpers@main/js/pathData.parseToPathData.js"></script>

Although one would expect to get the best results with the "lossless" <circle> or A arcto representations – a better cubic approximation might in fact return more accurate length results.

In other words: the (expected/theoretical) advantage of more accurate geometry-based elements/notations could prove to be deceptive.

Chrome's length results for a circle (r=20)

length result
Circumference circle (2π*r) 125.66370614359172
Circumference circle (getTotallength) 124.85393524169922
Difference 0.8097709018925059

Firefox is way more precise.

length result
Circumference circle (2π*r) 125.66370614359172
Circumference circle (getTotallength) 125.68115997314453
Difference -0.01745382955280661

Custom arc-to-cubic helper function

Based on Primer on Bézier Curves: §42 Circular arcs and cubic Béziers the following helper function tweaks accuracy by

  • splitting an arc into more/smaller segments
  • optimize certain arcs ( e.g quarter circles) by using an optimized k constant.

Example 1: convert from points

/** 
 * convert arctocommands to cubic bezier
 * based on a2c.js
 * https://github.com/fontello/svgpath/blob/master/lib/a2c.js
 * returns pathData array
 */

function arcToBezier(p0, values, splitSegments = 1, quadratic = false) {
  p0 = Array.isArray(p0) ? {
    x: p0[0],
    y: p0[1]
  } : p0;
  const TAU = Math.PI * 2;
  let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;

  if (rx === 0 || ry === 0) {
    return []
  }

  let phi = rotation ? rotation * TAU / 360 : 0;
  let sinphi = phi ? Math.sin(phi) : 0
  let cosphi = phi ? Math.cos(phi) : 1
  let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2
  let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2

  if (pxp === 0 && pyp === 0) {
    return []
  }
  rx = Math.abs(rx)
  ry = Math.abs(ry)
  let lambda =
    pxp * pxp / (rx * rx) +
    pyp * pyp / (ry * ry)
  if (lambda > 1) {
    let lambdaRt = Math.sqrt(lambda);
    rx *= lambdaRt
    ry *= lambdaRt
  }


  /** 
   * parametrize arc to 
   * get center point start and end angles
   */
  let rxsq = rx * rx,
    rysq = rx === ry ? rxsq : ry * ry

  let pxpsq = pxp * pxp,
    pypsq = pyp * pyp
  let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)

  if (radicant <= 0) {
    radicant = 0
  } else {
    radicant /= (rxsq * pypsq) + (rysq * pxpsq)
    radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1)
  }

  let centerxp = radicant ? radicant * rx / ry * pyp : 0
  let centeryp = radicant ? radicant * -ry / rx * pxp : 0
  let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2
  let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2

  let vx1 = (pxp - centerxp) / rx
  let vy1 = (pyp - centeryp) / ry
  let vx2 = (-pxp - centerxp) / rx
  let vy2 = (-pyp - centeryp) / ry

  // get start and end angle
  const vectorAngle = (ux, uy, vx, vy) => {
    let dot = +(ux * vx + uy * vy).toFixed(9)
    if (dot === 1 || dot === -1) {
      return dot === 1 ? 0 : Math.PI
    }
    dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot)
    let sign = (ux * vy - uy * vx < 0) ? -1 : 1
    return sign * Math.acos(dot);
  }

  let ang1 = vectorAngle(1, 0, vx1, vy1),
    ang2 = vectorAngle(vx1, vy1, vx2, vy2)

  if (sweepFlag === 0 && ang2 > 0) {
    ang2 -= Math.PI * 2
  } else if (sweepFlag === 1 && ang2 < 0) {
    ang2 += Math.PI * 2
  }

  let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0)

  // increase segments for more accureate length calculations
  splitSegments = quadratic ? splitSegments * 2 : splitSegments;
  let segments = ratio * splitSegments;
  ang2 /= segments
  let pathData = [];


  // If 90 degree circular arc, use a constant
  // https://pomax.github.io/bezierinfo/#circles_cubic
  // k=0.551784777779014

  const angle90 = 1.5707963267948966;
  const k = 0.551785
  let a = ang2 === angle90 ? k :
    (
      ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
    );

  let cos2 = ang2 ? Math.cos(ang2) : 1;
  let sin2 = ang2 ? Math.sin(ang2) : 0;
  let type = !quadratic ? 'C' : 'Q';

  const approxUnitArc = (ang1, ang2, a, cos2, sin2) => {
    let x1 = ang1 != ang2 ? Math.cos(ang1) : cos2;
    let y1 = ang1 != ang2 ? Math.sin(ang1) : sin2;
    let x2 = Math.cos(ang1 + ang2);
    let y2 = Math.sin(ang1 + ang2);

    return [{
        x: x1 - y1 * a,
        y: y1 + x1 * a
      },
      {
        x: x2 + y2 * a,
        y: y2 - x2 * a
      },
      {
        x: x2,
        y: y2
      }
    ];
  }

  for (let i = 0; i < segments; i++) {
    let com = {
      type: type,
      values: []
    }
    let curve = approxUnitArc(ang1, ang2, a, cos2, sin2);

    curve.forEach((pt) => {
      let x = pt.x * rx
      let y = pt.y * ry
      com.values.push(cosphi * x - sinphi * y + centerx, sinphi * x + cosphi * y + centery)
    })

    //convert to quadratic
    if (quadratic) {
      let p = {
        x: com.values[4],
        y: com.values[5]
      }
      let cp1 = {
        x: (com.values[0] - p0.x) * (1 + c) + p0.x,
        y: (com.values[1] - p0.y) * (1 + c) + p0.y
      };
      com.values = [cp1.x, cp1.y, p.x, p.y]
      p0 = p
    }

    pathData.push(com);
    ang1 += ang2
  }

  return pathData;

}


/**
 * serialize pathData array to 
 * d attribute string 
 */
function pathDataToD(pathData, decimals = -1, minify = false) {
  // implicit l command
  if (pathData[1].type === "l" && minify) {
    pathData[0].type = "m";
  }
  let d = `${pathData[0].type}${pathData[0].values.join(" ")}`;

  for (let i = 1; i < pathData.length; i++) {
    let com0 = pathData[i - 1];
    let com = pathData[i];

    let type = (com0.type === com.type && minify) ?
      " " :
      ((com0.type === "m" && com.type === "l") ||
        (com0.type === "M" && com.type === "l") ||
        (com0.type === "M" && com.type === "L")) &&
      minify ?
      " " : com.type;

    // round
    if (decimals >= 0) {
      com.values = com.values.map(val => {
        return +val.toFixed(decimals)
      })
    }

    //type = com.type;
    d += `${type}${com.values.join(" ")}`;
  }

  d = minify ?
    d
    .replaceAll(" 0.", " .")
    .replaceAll(" -", "-")
    .replace(/\s+([A-Za-z])/g, "$1")
    .replaceAll("Z", "z") :
    d;
  return d;
}

/**
 * convert quadratic commands to cubic
 */
function pathDataQuadratic2Cubic(p0, com) {
  if (Array.isArray(p0)) {
    p0 = {
      x: p0[0],
      y: p0[1]
    }
  }
  let cp1 = {
    x: p0.x + 2 / 3 * (com[0] - p0.x),
    y: p0.y + 2 / 3 * (com[1] - p0.y)
  }
  let cp2 = {
    x: com[2] + 2 / 3 * (com[0] - com[2]),
    y: com[3] + 2 / 3 * (com[1] - com[3])
  }
  return ({
    type: "C",
    values: [cp1.x, cp1.y, cp2.x, cp2.y, com[2], com[3]]
  });
}
svg {
  border: 1px solid #ccc;
  overflow: visible;
}

.marker {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
}
<p>
  <br>Circumference: <span id="arcLength"></span>
  <br>Difference: <span id="diff"></span>
</p>

<svg viewBox="0 0 60 60">
  <path id="pathCircle" d="M 50 30
           A20 20 0 0 1 10 30
           " stroke="#000" fill="none" stroke-width="1%"></path>
  <path id="pathCircleCubic" class="marker" d="" stroke="red" fill="none" stroke-width="0.5%"></path>
  
<!-- markers to show commands -->
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>
      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>



<script>
  // starting point M
  let p0 = [50, 30]
  // Arc command
  let values = [20, 20, 20, 0, 1, 10, 30]

  // expected mathematical circuimference
  let r = 20
  let circumference = Math.PI * r
  let accuracy = 2


  window.addEventListener('DOMContentLoaded', e => {
    // get pathdata converting arc to cubic bezier
    let pathDataArc = arcToBezier(p0, values, accuracy);
    // apply
    let d = `M ${p0.join(' ')} ` + pathDataToD(pathDataArc)
    pathCircleCubic.setAttribute('d', d)


    let pathLength = +pathCircleCubic.getTotalLength().toFixed(5)
    arcLength.textContent = pathLength + ' / ' + circumference.toFixed(5)
    diff.textContent = circumference - pathLength

  })
</script>

Example 2: convert from pathData

This example wraps the previous arcToBezier() function in a path data parsing and normalization helper

svg{
  border:1px solid #ccc;
  overflow:visible;
}

.marker
 {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
}
<svg viewBox="0 0 100 100">
  <path id="pathEllipse" d="M95 30
                            A20 30 45 01 75 60
                            A20 30 45 11 95 30
                            Z" fill="none" stroke-width="1" stroke="#000"></path>

  <path id="pathEllipseC" class="marker" d="" fill="none" stroke="red" stroke-width="0.5"></path>
  <!-- markers to show commands -->
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>
      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>

<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svgHelpers@main/js/pathData.parseToPathData.js"></script>


<script>
  window.addEventListener('DOMContentLoaded', e => {
    //parse
    let d = pathEllipse.getAttribute('d')
    let pathData = parseDtoPathData(d);
    //cubic
    let options = {
      convertArc: true,
      // optional: split arc segments
      arcAccuracy: 1,
      // convert shorthands
      unshort: true,
    };
    pathData = normalizePathData(pathData, options);
    pathEllipseC.setAttribute('d', pathDataToD(pathData))
  })
</script>

Upvotes: 0

Ievgen
Ievgen

Reputation: 4443

You can check next implementation links:

When polyfill is used you can just call getPathData from the element.

svgGeometryElement.getPathData( {normalize: true} )

Implementation is based on the next standart: W3.org

Upvotes: 4

Petrov. Ivan Petrov
Petrov. Ivan Petrov

Reputation: 54

Selected answer provides partial solution to circled arcs. While researching the topic, found an npm for a function extracted from SVG path, to addres excatly this, and here is a quick codesandbox hope this helps someone

arcpath.setAttribute("d", "M 0 200 A 100 20 0 1 0 100 200");
const options = {
  px: 0,
  py: 200,
  cx: 100,
  cy: 200,
  rx: 100,
  ry: 20,
  xAxisRotation: 0,
  largeArcFlag: 1,
  sweepFlag: 0
};

arc to cubic curves

Upvotes: 0

Paul LeBeau
Paul LeBeau

Reputation: 101830

Most SVG rendering libraries have to do this because 2D graphics libraries don't seem to directly support arcs that are rotated with respect to the X axis.

So you could look up the code in, say, Batik. Or look at the arcTo() method in my SVG library (which also borrows from Batik):

AndroidSVG / SVGAndroidRenderer.java / line 2889

It's Java, but should be easily converted to JS.

Upvotes: 10

cdoublev
cdoublev

Reputation: 784

You can also look at this function from SnapSVG which might have been used somehow by Adobe Illustrator (it is hosted by a user named "adobe-webplatform"), to convert arc to cubic commands. It's also used in SVGO(ptimizer).

I'm still trying to decypher it, but the standard is actually pretty helpfull for that.

Upvotes: 3

Related Questions