Skip to content

Instantly share code, notes, and snippets.

@cdoublev
Last active October 19, 2020 09:10
Show Gist options
  • Save cdoublev/1912e3806e960fa36051a1a311396b08 to your computer and use it in GitHub Desktop.
Save cdoublev/1912e3806e960fa36051a1a311396b08 to your computer and use it in GitHub Desktop.
/**
* 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()}`
<!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>
/**
* 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