Reputation: 331
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
Reputation: 53597
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
Reputation: 17265
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?
<circle>
or paths using A
may not return more accurate valuesIt 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 |
Based on Primer on Bézier Curves: §42 Circular arcs and cubic Béziers the following helper function tweaks accuracy by
k
constant./**
* 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>
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
Reputation: 4443
You can check next implementation links:
path-data-polyfill
by Jarek Foksa.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
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
};
Upvotes: 0
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
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