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
}
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
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)
splitSegments = quadratic ? splitSegments * 2 : splitSegments;
let segments = ratio * splitSegments;
ang2 /= segments
let pathData = [];
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)
})
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;
}
function pathDataToD(pathData, decimals = -1, minify = false) {
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;
if (decimals >= 0) {
com.values = com.values.map(val => {
return +val.toFixed(decimals)
})
}
d += `${type}${com.values.join(" ")}`;
}
d = minify ?
d
.replaceAll(" 0.", " .")
.replaceAll(" -", "-")
.replace(/\s+([A-Za-z])/g, "$1")
.replaceAll("Z", "z") :
d;
return d;
}
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>
<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>
let p0 = [50, 30]
let values = [20, 20, 20, 0, 1, 10, 30]
let r = 20
let circumference = Math.PI * r
let accuracy = 2
window.addEventListener('DOMContentLoaded', e => {
let pathDataArc = arcToBezier(p0, values, accuracy);
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>