Last active
October 19, 2020 09:10
-
-
Save cdoublev/1912e3806e960fa36051a1a311396b08 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Internet Systems Consortium license | |
* =================================== | |
* | |
* Copyright (c) `2017`, `Colin Meinke` | |
* | |
* Permission to use, copy, modify, and/or distribute this software for any purpose | |
* with or without fee is hereby granted, provided that the above copyright notice | |
* and this permission notice appear in all copies. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
* FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS | |
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER | |
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF | |
* THIS SOFTWARE. | |
*/ | |
/** | |
* Implementation based from SVGO (based from Snap.svg, based from the spec) | |
* | |
* https://github.com/svg/svgo/blob/master/plugins/_path.js#L904 | |
* | |
* TODO: return absolute values. | |
*/ | |
const arcToBezier = ({ | |
cx: x2, cy: y2, | |
px: x1, py: y1, | |
recursive, | |
rx, ry, | |
largeArcFlag: fA = 0, sweepFlag: fS = 0, | |
xAxisRotation: angle = 0, | |
}) => { | |
// for more information of where this Math came from visit: | |
// http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes | |
const _120 = Math.PI * 120 / 180 | |
const phi = Math.PI / 180 * (+angle || 0) | |
const rotateX = (x, y, phi) => x * Math.cos(phi) - y * Math.sin(phi) | |
const rotateY = (x, y, phi) => x * Math.sin(phi) + y * Math.cos(phi) | |
let res = [] | |
if (!recursive) { | |
// Step 1: compute (x1′, y1′) | |
x1 = rotateX(x1, y1, -phi) | |
y1 = rotateY(x1, y1, -phi) | |
x2 = rotateX(x2, y2, -phi) | |
y2 = rotateY(x2, y2, -phi) | |
const x = (x1 - x2) / 2 | |
const y = (y1 - y2) / 2 | |
// Step 2: compute (cx′, cy′) | |
let h = (x * x) / (rx * rx) + (y * y) / (ry * ry) | |
if (h > 1) { | |
h = Math.sqrt(h) | |
rx = h * rx | |
ry = h * ry | |
} | |
const rx2 = rx * rx | |
const ry2 = ry * ry | |
const k = (fA == fS ? -1 : 1) * | |
Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))) | |
var cx = k * rx * y / ry + (x1 + x2) / 2 | |
var cy = k * -ry * x / rx + (y1 + y2) / 2 | |
var f1 = Math.asin(((y1 - cy) / ry).toFixed(9)) | |
var f2 = Math.asin(((y2 - cy) / ry).toFixed(9)) | |
if (x1 < cx) { | |
f1 = Math.PI - f1 | |
} | |
if (x2 < cx) { | |
f2 = Math.PI - f2 | |
} | |
if (f1 < 0) { | |
f1 = Math.PI * 2 + f1 | |
} | |
if (f2 < 0) { | |
f2 = Math.PI * 2 + f2 | |
} | |
if (fS && f1 > f2) { | |
f1 = f1 - Math.PI * 2 | |
} | |
if (!fS && f2 > f1) { | |
f2 = f2 - Math.PI * 2 | |
} | |
} else { | |
f1 = recursive[0] | |
f2 = recursive[1] | |
cx = recursive[2] | |
cy = recursive[3] | |
} | |
let df = f2 - f1 | |
if (Math.abs(df) > _120) { | |
const f2old = f2 | |
const x2old = x2 | |
const y2old = y2 | |
f2 = f1 + _120 * (fS && f2 > f1 ? 1 : -1) | |
x2 = cx + rx * Math.cos(f2) | |
y2 = cy + ry * Math.sin(f2) | |
res = arcToBezier({ | |
cx: x2old, cy: y2old, | |
px: x2, py: y2, | |
recursive: [f2, f2old, cx, cy], | |
rx, ry, | |
largeArcFlag: 0, sweepFlag: fS, | |
xAxisRotation: angle, | |
}) | |
} | |
df = f2 - f1 | |
const c1 = Math.cos(f1) | |
const s1 = Math.sin(f1) | |
const c2 = Math.cos(f2) | |
const s2 = Math.sin(f2) | |
const t = Math.tan(df / 4) | |
const hx = 4 / 3 * rx * t | |
const hy = 4 / 3 * ry * t | |
const m = [ | |
- hx * s1, hy * c1, | |
x2 + hx * s2 - x1, y2 - hy * c2 - y1, | |
x2 - x1, y2 - y1 | |
] | |
if (recursive) { | |
return m.concat(res) | |
} | |
res = m.concat(res) | |
let newres = [] | |
for (let i = 0, n = res.length; i < n; i++) { | |
newres[i] = i % 2 ? rotateY(res[i - 1], res[i], phi) : rotateX(res[i], res[i + 1], phi) | |
} | |
return newres | |
} | |
const stringifyCubicBezier = array => `c${array.join()}` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
* { box-sizing: border-box } | |
body { height: 100vh; display: flex; padding: 2em } | |
svg { height: 100%; margin: auto } | |
</style> | |
</head> | |
<body> | |
<svg viewBox="0 0 780 1000"> | |
<path d="" /> | |
<path d="" /> | |
</svg> | |
<!-- <script src="./svg-arc-to-cubic-bezier.js"></script> --> | |
<script src="./a2c.js"></script> | |
<script> | |
const $svg = document.querySelector('svg') | |
const [$pathWithArc, $pathWithCubic] = document.getElementsByTagName('path') | |
// Path 1 (with arc commands) | |
const defWithArc = [ | |
'M778 389', | |
'A388 388 0 0 1 643 683', | |
'c-43 43-69 103-69 169 0 13-7 24-16 30', | |
'A148 148 0 0 1 510 937 148 148 0 0 1 267 937', | |
'c-20-14-36-33-47-55-10-6-16-17-16-30', | |
'A240 240 0 0 0 117 667', | |
'A388 388 0 0 1 389 0', | |
'c215 0 389 174 389 389z' | |
].join(' ') | |
// Path 2 (only with cubic curves) | |
const a2cPoints1 = arcToBezier({ px: 778, py: 389, rx: 388, ry: 388, sweepFlag: 1, cx: 643, cy: 683 }) | |
const a2cPoints2 = arcToBezier({ px: 558, py: 882, rx: 148, ry: 148, sweepFlag: 1, cx: 510, cy: 937 }) | |
const a2cPoints3 = arcToBezier({ px: 510, py: 937, rx: 148, ry: 148, sweepFlag: 1, cx: 267, cy: 937 }) | |
const a2cPoints4 = arcToBezier({ px: 204, py: 852, rx: 240, ry: 240, cx: 117, cy: 667 }) | |
const a2cPoints5 = arcToBezier({ px: 117, py: 667, rx: 388, ry: 388, sweepFlag: 1, cx: 389, cy: 0 }) | |
const defWithCubic = [ | |
'M778 389', stringifyCubicBezier(a2cPoints1), 'c-43 43-69 103-69 169 0 13-7 24-16 30', | |
stringifyCubicBezier(a2cPoints2), stringifyCubicBezier(a2cPoints3), 'c-20-14-36-33-47-55-10-6-16-17-16-30', | |
stringifyCubicBezier(a2cPoints4), stringifyCubicBezier(a2cPoints5), 'c215 0 389 174 389 389', 'z', | |
].join(' ') | |
$pathWithArc.setAttribute('d', defWithArc) | |
$pathWithCubic.setAttribute('d', defWithCubic) | |
$pathWithArc.setAttribute('fill', '#ff000033') | |
$pathWithCubic.setAttribute('fill', '#ff000033') | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Internet Systems Consortium license | |
* =================================== | |
* | |
* Copyright (c) `2017`, `Colin Meinke` | |
* | |
* Permission to use, copy, modify, and/or distribute this software for any purpose | |
* with or without fee is hereby granted, provided that the above copyright notice | |
* and this permission notice appear in all copies. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
* FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS | |
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER | |
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF | |
* THIS SOFTWARE. | |
*/ | |
const TAU = Math.PI * 2 | |
const mapToEllipse = ({ x, y }, rx, ry, cosphi, sinphi, centerx, centery) => { | |
x *= rx | |
y *= ry | |
const xp = cosphi * x - sinphi * y | |
const yp = sinphi * x + cosphi * y | |
return { | |
x: xp + centerx, | |
y: yp + centery | |
} | |
} | |
const approxUnitArc = (ang1, ang2) => { | |
// See http://spencermortensen.com/articles/bezier-circle/ for the derivation | |
// of this constant. | |
// Note: We need to keep the sign of ang2, because this determines the | |
// direction of the arc using the sweep-flag parameter. | |
const c = 0.551915024494 * (ang2 < 0 ? -1 : 1) | |
const x1 = Math.cos(ang1) | |
const y1 = Math.sin(ang1) | |
const x2 = Math.cos(ang1 + ang2) | |
const y2 = Math.sin(ang1 + ang2) | |
return [ | |
{ | |
x: x1 - y1 * c, | |
y: y1 + x1 * c | |
}, | |
{ | |
x: x2 + y2 * c, | |
y: y2 - x2 * c | |
}, | |
{ | |
x: x2, | |
y: y2 | |
} | |
] | |
} | |
const vectorAngle = (ux, uy, vx, vy) => { | |
const sign = (ux * vy - uy * vx < 0) ? -1 : 1 | |
const umag = Math.sqrt(ux * ux + uy * uy) | |
const vmag = Math.sqrt(ux * ux + uy * uy) | |
const dot = ux * vx + uy * vy | |
let div = dot / (umag * vmag) | |
if (div > 1) { | |
div = 1 | |
} | |
if (div < -1) { | |
div = -1 | |
} | |
return sign * Math.acos(div) | |
} | |
const getArcCenter = ( | |
px, | |
py, | |
cx, | |
cy, | |
rx, | |
ry, | |
largeArcFlag, | |
sweepFlag, | |
sinphi, | |
cosphi, | |
pxp, | |
pyp | |
) => { | |
const rxsq = Math.pow(rx, 2) | |
const rysq = Math.pow(ry, 2) | |
const pxpsq = Math.pow(pxp, 2) | |
const pypsq = Math.pow(pyp, 2) | |
let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq) | |
if (radicant < 0) { | |
radicant = 0 | |
} | |
radicant /= (rxsq * pypsq) + (rysq * pxpsq) | |
radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1) | |
const centerxp = radicant * rx / ry * pyp | |
const centeryp = radicant * -ry / rx * pxp | |
const centerx = cosphi * centerxp - sinphi * centeryp + (px + cx) / 2 | |
const centery = sinphi * centerxp + cosphi * centeryp + (py + cy) / 2 | |
const vx1 = (pxp - centerxp) / rx | |
const vy1 = (pyp - centeryp) / ry | |
const vx2 = (-pxp - centerxp) / rx | |
const vy2 = (-pyp - centeryp) / ry | |
let ang1 = vectorAngle(1, 0, vx1, vy1) | |
let ang2 = vectorAngle(vx1, vy1, vx2, vy2) | |
if (sweepFlag === 0 && ang2 > 0) { | |
ang2 -= TAU | |
} | |
if (sweepFlag === 1 && ang2 < 0) { | |
ang2 += TAU | |
} | |
return [ centerx, centery, ang1, ang2 ] | |
} | |
const arcToBezier = ({ | |
px, | |
py, | |
cx, | |
cy, | |
rx, | |
ry, | |
xAxisRotation = 0, | |
largeArcFlag = 0, | |
sweepFlag = 0 | |
}) => { | |
const curves = [] | |
if (rx === 0 || ry === 0) { | |
return [] | |
} | |
const sinphi = Math.sin(xAxisRotation * TAU / 360) | |
const cosphi = Math.cos(xAxisRotation * TAU / 360) | |
const pxp = cosphi * (px - cx) / 2 + sinphi * (py - cy) / 2 | |
const pyp = -sinphi * (px - cx) / 2 + cosphi * (py - cy) / 2 | |
if (pxp === 0 && pyp === 0) { | |
return [] | |
} | |
rx = Math.abs(rx) | |
ry = Math.abs(ry) | |
const lambda = | |
Math.pow(pxp, 2) / Math.pow(rx, 2) + | |
Math.pow(pyp, 2) / Math.pow(ry, 2) | |
if (lambda > 1) { | |
rx *= Math.sqrt(lambda) | |
ry *= Math.sqrt(lambda) | |
} | |
let [ centerx, centery, ang1, ang2 ] = getArcCenter( | |
px, | |
py, | |
cx, | |
cy, | |
rx, | |
ry, | |
largeArcFlag, | |
sweepFlag, | |
sinphi, | |
cosphi, | |
pxp, | |
pyp | |
) | |
// If 'ang2' == 90.0000000001, then `ratio` will evaluate to | |
// 1.0000000001. This causes `segments` to be greater than one, which is an | |
// unecessary split, and adds extra points to the bezier curve. To alleviate | |
// this issue, we round to 1.0 when the ratio is close to 1.0. | |
let ratio = Math.abs(ang2) / (TAU / 4) | |
if (Math.abs(1.0 - ratio) < 0.0000001) { | |
ratio = 1.0 | |
} | |
const segments = Math.max(Math.ceil(ratio), 1) | |
ang2 /= segments | |
for (let i = 0; i < segments; i++) { | |
curves.push(approxUnitArc(ang1, ang2)) | |
ang1 += ang2 | |
} | |
return curves.map(curve => { | |
const { x: x1, y: y1 } = mapToEllipse(curve[ 0 ], rx, ry, cosphi, sinphi, centerx, centery) | |
const { x: x2, y: y2 } = mapToEllipse(curve[ 1 ], rx, ry, cosphi, sinphi, centerx, centery) | |
const { x, y } = mapToEllipse(curve[ 2 ], rx, ry, cosphi, sinphi, centerx, centery) | |
return { x1, y1, x2, y2, x, y } | |
}) | |
} | |
const stringifyCubicBezier = points => points.reduce( | |
(definition, cubicPoints) => | |
`${definition} ${Object.values(cubicPoints).reduce((points, point) => `${points} ${point}`)}`, | |
'C') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment