svg-parse-path-normalized
Version:
Parse svg path data from string and normalize it to calculable value arrays
1,461 lines (1,245 loc) • 50.5 kB
JavaScript
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) {
// CommonJS (Node.js) environment
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
// AMD environment
define([], factory);
} else {
// Browser environment
root.pathDataConvert = factory();
}
})(this, function () {
var pathDataConvert = {};
/** function defs **/
/** chainable prototype methods */
Array.prototype.convert = function (options) {
return convertPathData(this, options);
}
Array.prototype.toAbsolute = function (decimals = -1) {
return pathDataToAbsolute(this, decimals);
}
Array.prototype.toRelative = function (decimals = -1) {
return pathDataToRelative(this, decimals);
}
Array.prototype.toLonghands = function (decimals = -1) {
return pathDataToLonghands(this, decimals);
}
Array.prototype.toShorthands = function (decimals = -1) {
return pathDataToShorthands(this, decimals);
}
Array.prototype.round = function (decimals = -1) {
return roundPathData(this, decimals);
}
Array.prototype.toQuadratic = function (precision = 0.1) {
return pathDataToQuadratic(this, precision);
}
Array.prototype.toVerbose = function () {
return pathDataToVerbose(this);
}
/**
* converts all commands to absolute
* optional: convert shorthands; arcs to cubics
*/
function convertPathData(pathData, options) {
// analyze pathdata
let commandTokens = pathData.map(com => { return com.type }).join('')
let hasRel = /[astvqmhlc]/g.test(commandTokens);
let hasShorthands = /[hstv]/gi.test(commandTokens);
let hasQuadratics = /[qt]/gi.test(commandTokens);
let hasArcs = /[a]/gi.test(commandTokens);
// merge default options
let defaults = {
normalize: null,
optimize: false,
toAbsolute: true,
toRelative: false,
quadraticToCubic: false,
cubicToQuadratic: false,
cubicToQuadraticPrecision: 0.1,
lineToCubic: false,
toLonghands: true,
toShorthands: false,
arcToCubic: false,
arcParam: false,
arcAccuracy: 1,
decimals: -1,
}
options = {
...defaults,
...options
}
let { normalize, optimize, toAbsolute, toRelative, quadraticToCubic, cubicToQuadratic, cubicToQuadraticPrecision, lineToCubic, toLonghands, toShorthands, arcToCubic, arcParam, arcAccuracy, decimals } = options;
if (normalize === true) {
toAbsolute = true
toLonghands = true
arcToCubic = true
quadraticToCubic = true
toShorthands = false
}
if (optimize === true) {
toRelative = true
toShorthands = true
decimals = 3
}
// nothing to convert – passthrough
if (!hasRel && !hasShorthands && !hasQuadratics && !hasArcs && !toRelative && !toShorthands && !lineToCubic) {
return pathData
}
/**
* convert to absolute
*/
// add M
let pathDataAbs = [pathData[0]];
let lastX = pathData[0].values[0];
let lastY = pathData[0].values[1];
let offX = lastX;
let offY = lastY;
/**
* arcToCubic, quadraticToCubic, toLonghands
* will force toAbsolute conversion
*/
if (arcToCubic || toLonghands || quadraticToCubic || cubicToQuadratic || arcParam || lineToCubic) {
toAbsolute = true
}
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let { type, values } = com;
let typeRel = type.toLowerCase();
let typeAbs = type.toUpperCase();
let valuesL = values.length;
let isRelative = type === typeRel;
let comPrev = pathData[i - 1];
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
if (isRelative && toAbsolute) {
com.type = typeAbs;
switch (typeRel) {
case "a":
com.values = [
values[0],
values[1],
values[2],
values[3],
values[4],
values[5] + offX,
values[6] + offY
];
break;
case "h":
case "v":
com.values = type === 'h' ? [values[0] + offX] : [values[0] + offY];
break;
case 'm':
case 'l':
case 't':
com.values = [values[0] + offX, values[1] + offY]
break;
case "c":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY,
values[4] + offX,
values[5] + offY
];
break;
case "q":
case "s":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY,
];
break;
}
}
// is absolute
else {
offX = 0;
offY = 0;
}
/**
* convert shorthands
*/
if (toLonghands && hasShorthands || (com.type === 'T' && quadraticToCubic) || lineToCubic) {
let cp1X, cp1Y, cpN1X, cpN1Y, cp2X, cp2Y;
if (com.type === 'H' || com.type === 'V') {
com.values = com.type === 'H' ? [com.values[0], lastY] : [lastX, com.values[0]];
com.type = 'L';
}
else if (com.type === 'T' || com.type === 'S') {
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] = valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
// new control point
cpN1X = com.type === 'T' ? lastX + (lastX - cp1X) : 2 * lastX - cp2X;
cpN1Y = com.type === 'T' ? lastY + (lastY - cp1Y) : 2 * lastY - cp2Y;
com.values = [cpN1X, cpN1Y, com.values].flat();
com.type = com.type === 'T' ? 'Q' : 'C';
}
}
// convert quadratic to cubic
if (quadraticToCubic && hasQuadratics && com.type === 'Q') {
com = quadratic2Cubic(p0, com.values)
}
// parametrized arc rx and ry values
if (arcParam && typeRel === 'a') {
let arcData = svgArcToCenterParam(lastX, lastY, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
//console.log(arcData);
com.values = [arcData.rx, arcData.ry, values[2], values[3], values[4], values[5], values[6]]
}
/**
* linetos to cubic
* facilitates morphing animations
*/
if (lineToCubic && com.type === 'L') {
com = { type: 'C', values: [lastX, lastY, com.values[0], com.values[1], com.values[0], com.values[1]] }
}
//convert arcs to cubics
if (arcToCubic && hasArcs && com.type === 'A') {
// add all C commands instead of Arc
let cubicArcs = arcToBezier({ x: lastX, y: lastY }, com.values, arcAccuracy);
cubicArcs.forEach((cubicArc) => {
pathDataAbs.push(cubicArc);
});
}
else {
// add command
pathDataAbs.push(com)
}
// update offsets
lastX = valuesL > 1 ? values[valuesL - 2] + offX : (typeRel === 'h' ? values[0] + offX : lastX);
lastY = valuesL > 1 ? values[valuesL - 1] + offY : (typeRel === 'v' ? values[0] + offY : lastY);
offX = lastX;
offY = lastY;
};
// to quadratic
if (cubicToQuadratic) {
pathDataAbs = pathDataToQuadratic(pathDataAbs, cubicToQuadraticPrecision)
}
// to shorthands
if (toShorthands) {
pathDataAbs = pathDataToShorthands(pathDataAbs, decimals)
}
// to Relative
if (toRelative) {
pathDataAbs = pathDataToRelative(pathDataAbs, decimals)
}
// round if not already rounded
if (!toShorthands && !toRelative && decimals > -1) {
pathDataAbs = roundPathData(pathDataAbs, decimals)
}
return pathDataAbs;
}
/**
* convert quadratic commands to cubic
*/
function quadratic2Cubic(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]] });
}
function roundPathData(pathData, decimals = -1) {
pathData.forEach((com, c) => {
if (decimals >= 0) {
com.values.forEach((val, v) => {
pathData[c].values[v] = +val.toFixed(decimals);
});
}
});
return pathData;
}
/**
* This is just a port of Dmitry Baranovskiy's
* pathToRelative/Absolute methods used in snap.svg
* https://github.com/adobe-webplatform/Snap.svg/
*
* Demo: https://codepen.io/herrstrietzel/pen/poVKbgL
*/
// convert to relative commands
function pathDataToRelative(pathData, decimals = -1) {
// round coordinates to prevent distortions
if (decimals >= 0) {
pathData[0].values = pathData[0].values.map(val => { return +val.toFixed(decimals) })
}
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
// loop through commands
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
// round coordinates to prevent distortions
if (decimals >= 0 && com.values.length) {
com.values = com.values.map(val => { return +val.toFixed(decimals) })
}
let { type, values } = com;
let typeRel = type.toLowerCase();
// is absolute
if (type != typeRel) {
type = typeRel;
com.type = type;
// check current command types
switch (typeRel) {
case "a":
values[5] = +(values[5] - x);
values[6] = +(values[6] - y);
break;
case "v":
values[0] = +(values[0] - y);
break;
case "m":
mx = values[0];
my = values[1];
default:
// other commands
if (values.length) {
for (let v = 0; v < values.length; v++) {
// even value indices are y coordinates
values[v] = values[v] - (v % 2 ? y : x);
}
}
}
}
// is already relative
else {
if (type == "m") {
mx = values[0] + x;
my = values[1] + y;
}
}
let vLen = values.length;
switch (type) {
case "z":
x = mx;
y = my;
break;
case "h":
x += values[vLen - 1];
break;
case "v":
y += values[vLen - 1];
break;
default:
x += values[vLen - 2];
y += values[vLen - 1];
}
// round final relative values
if (decimals > -1) {
com.values = com.values.map(val => { return +val.toFixed(decimals) })
}
}
return pathData;
}
function pathDataToAbsolute(pathData, decimals = -1) {
// round coordinates to prevent distortions
if (decimals >= 0) {
pathData[0].values = pathData[0].values.map(val => { return +val.toFixed(decimals) })
}
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
// loop through commands
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
// round coordinates to prevent distortions
if (decimals >= 0 && com.values.length) {
com.values = com.values.map(val => { return +val.toFixed(decimals) })
}
let { type, values } = com;
let typeAbs = type.toUpperCase();
if (type != typeAbs) {
type = typeAbs;
com.type = type;
// check current command types
switch (typeAbs) {
case "A":
values[5] = +(values[5] + x);
values[6] = +(values[6] + y);
break;
case "V":
values[0] = +(values[0] + y);
break;
case "H":
values[0] = +(values[0] + x);
break;
case "M":
mx = +values[0] + x;
my = +values[1] + y;
default:
// other commands
if (values.length) {
for (let v = 0; v < values.length; v++) {
// even value indices are y coordinates
values[v] = values[v] + (v % 2 ? y : x);
}
}
}
}
// is already absolute
let vLen = values.length;
switch (type) {
case "Z":
x = +mx;
y = +my;
break;
case "H":
x = values[0];
break;
case "V":
y = values[0];
break;
case "M":
mx = values[vLen - 2];
my = values[vLen - 1];
default:
x = values[vLen - 2];
y = values[vLen - 1];
}
// round final absolute values
if (decimals > -1) {
com.values = com.values.map(val => { return +val.toFixed(decimals) })
}
}
return pathData;
}
/**
* decompose/convert shorthands to "longhand" commands:
* H, V, S, T => L, L, C, Q
* reversed method: pathDataToShorthands()
*/
function pathDataToLonghands(pathData, decimals = -1, test = true) {
// analyze pathdata – if you're sure your data is already absolute skip it via test=false
let hasRel;
if (test) {
let commandTokens = pathData.map(com => { return com.type }).join('')
let hasShorthands = /[hstv]/gi.test(commandTokens);
hasRel = /[astvqmhlc]/g.test(commandTokens);
if (!hasShorthands) {
return pathData;
}
}
pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
let pathDataLonghand = [];
let comPrev = {
type: "M",
values: pathData[0].values
};
pathDataLonghand.push(comPrev);
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let { type, values } = com;
let valuesL = values.length;
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
let [prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
switch (type) {
case "H":
comPrev = {
type: "L",
values: [values[0], prevY]
};
break;
case "V":
comPrev = {
type: "L",
values: [prevX, values[0]]
};
break;
case "T":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control point
cpN1X = prevX + (prevX - cp1X);
cpN1Y = prevY + (prevY - cp1Y);
comPrev = {
type: "Q",
values: [cpN1X, cpN1Y, x, y]
};
break;
case "S":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] =
valuesPrevL > 2 ?
[valuesPrev[2], valuesPrev[3]] :
[valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control points
cpN1X = 2 * prevX - cp2X;
cpN1Y = 2 * prevY - cp2Y;
cpN2X = values[0];
cpN2Y = values[1];
comPrev = {
type: "C",
values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
};
break;
default:
comPrev = {
type: type,
values: values
};
}
// round final longhand values
if (decimals > -1) {
comPrev.values = comPrev.values.map(val => { return +val.toFixed(decimals) })
}
pathDataLonghand.push(comPrev);
}
return pathDataLonghand;
}
/**
* apply shorthand commands if possible
* L, L, C, Q => H, V, S, T
* reversed method: pathDataToLonghands()
*/
function pathDataToShorthands(pathData, decimals = -1, test = true) {
/**
* analyze pathdata – if you're sure your data is already absolute skip it via test=false
*/
let hasRel
if (test) {
let commandTokens = pathData.map(com => { return com.type }).join('')
hasRel = /[astvqmhlc]/g.test(commandTokens);
}
pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
let comShort = {
type: "M",
values: pathData[0].values
};
let pathDataShorts = [comShort];
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let { type, values } = com;
let valuesL = values.length;
let comPrev = pathData[i - 1];
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
let cp1X, cp1Y, cp2X, cp2Y;
let [prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
let val0R, cpN1XR, val1R, cpN1YR, cpN1X, cpN1Y, cpN2X, cpN2Y, prevXR, prevYR;
switch (type) {
case "L":
// round coordinates for some tolerance
[val0R, prevXR, val1R, prevYR] = [
values[0],
prevX,
values[1],
prevY
]
if (comPrev.type !== 'H' && comPrev.type !== 'V') {
[val0R, prevXR, val1R, prevYR] = [val0R, prevXR, val1R, prevYR].map((val) => {
return +(val).toFixed(2);
});
}
if (prevYR == val1R && prevXR !== val0R) {
comShort = {
type: "H",
values: [values[0]]
};
} else if (prevXR == val0R && prevYR !== val1R) {
comShort = {
type: "V",
values: [values[1]]
};
} else {
comShort = com;
}
break;
case "Q":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// Q control point
cpN1X = prevX + (prevX - cp1X);
cpN1Y = prevY + (prevY - cp1Y);
/**
* control points can be reflected
* use rounded values for better tolerance
*/
[val0R, cpN1XR, val1R, cpN1YR] = [
values[0],
cpN1X,
values[1],
cpN1Y
].map((val) => {
return +(val).toFixed(1);
});
if (val0R == cpN1XR && val1R == cpN1YR) {
comShort = {
type: "T",
values: [x, y]
};
} else {
comShort = com;
}
break;
case "C":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] =
valuesPrevL > 2 ?
[valuesPrev[2], valuesPrev[3]] :
[valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// C control points
cpN1X = 2 * prevX - cp2X;
cpN1Y = 2 * prevY - cp2Y;
cpN2X = values[2];
cpN2Y = values[3];
/**
* control points can be reflected
* use rounded values for better tolerance
*/
[val0R, cpN1XR, val1R, cpN1YR] = [
values[0],
cpN1X,
values[1],
cpN1Y
].map((val) => {
return +(val).toFixed(1);
});
if (val0R == cpN1XR && val1R == cpN1YR) {
comShort = {
type: "S",
values: [cpN2X, cpN2Y, x, y]
};
} else {
comShort = com;
}
break;
default:
comShort = {
type: type,
values: values
};
}
// round final values
if (decimals > -1) {
comShort.values = comShort.values.map(val => { return +val.toFixed(decimals) })
}
pathDataShorts.push(comShort);
}
return pathDataShorts;
}
/**
* based on puzrin's
* fontello/cubic2quad
* https://github.com/fontello/cubic2quad/blob/master/test/cubic2quad.js
*/
function pathDataToQuadratic(pathData, precision = 0.1) {
pathData = pathDataToLonghands(pathData)
let newPathData = [pathData[0]];
for (let i = 1; i < pathData.length; i++) {
let comPrev = pathData[i - 1];
let com = pathData[i];
let [type, values] = [com.type, com.values];
let [typePrev, valuesPrev] = [comPrev.type, comPrev.values];
let valuesPrevL = valuesPrev.length;
let [xPrev, yPrev] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// convert C to Q
if (type == "C") {
let quadCommands = cubicToQuad(
xPrev,
yPrev,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5],
precision
);
quadCommands.forEach(comQ => {
newPathData.push(comQ)
})
} else {
newPathData.push(com);
}
}
return newPathData;
}
function cubicToQuad(x0, y0, cp1x, cp1y, cp2x, cp2y, px, py, precision) {
const quadSolve = (x0, y0, cp1x) => {
if (0 === x0)
return 0 === y0 ? [] : [-cp1x / y0];
let o = y0 * y0 - 4 * x0 * cp1x;
if (Math.abs(o) < 1e-16)
return [-y0 / (2 * x0)];
if (o < 0)
return [];
let r = Math.sqrt(o);
return [
(-y0 - r) / (2 * x0),
(-y0 + r) / (2 * x0)
];
}
const solveInflections = (x0, y0, cp1x, cp1y, cp2x, cp2y, px, py) => {
return quadSolve(
-px * (y0 - 2 * cp1y + cp2y) +
cp2x * (2 * y0 - 3 * cp1y + py) +
x0 * (cp1y - 2 * cp2y + py) -
cp1x * (y0 - 3 * cp2y + 2 * py),
px * (y0 - cp1y) +
3 * cp2x * (-y0 + cp1y) +
cp1x * (2 * y0 - 3 * cp2y + py) -
x0 * (2 * cp1y - 3 * cp2y + py),
cp2x * (y0 - cp1y) + x0 * (cp1y - cp2y) + cp1x * (-y0 + cp2y)
)
.filter(function (x0) {
return x0 > 1e-8 && x0 < 1 - 1e-8;
})
.sort((x0, y0) => { return x0 - y0 })
}
const subdivideCubic = (x0, y0, cp1x, cp1y, cp2x, cp2y, px, py, precision) => {
let s = 1 - precision, f = x0 * s + cp1x * precision, l = cp1x * s + cp2x * precision, d = cp2x * s + px * precision, h = f * s + l * precision, p = l * s + d * precision, y = h * s + p * precision, P = y0 * s + cp1y * precision, m = cp1y * s + cp2y * precision, x = cp2y * s + py * precision, b = P * s + m * precision, v = m * s + x * precision, w = b * s + v * precision;
return [
[x0, y0, f, P, h, b, y, w],
[y, w, p, v, d, x, px, py]
];
}
let s = solveInflections(x0, y0, cp1x, cp1y, cp2x, cp2y, px, py);
let pts
if (!s.length) {
//return _cubicToQuad(x0, y0, cp1x, cp1y, cp2x, cp2y, px, py, precision);
pts = _cubicToQuad(x0, y0, cp1x, cp1y, cp2x, cp2y, px, py, precision);
} else {
for (
var f,
l,
d = [],
h = [x0, y0, cp1x, cp1y, cp2x, cp2y, px, py],
p = 0, y = 0;
y < s.length;
y++
) {
// subdivide the cubic bezier curve
l = subdivideCubic(h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1 - (1 - s[y]) / (1 - p)
);
// compute the quadratic Bezier curve using the divided cubic segment
f = _cubicToQuad(l[0][0], l[0][1], l[0][2], l[0][3], l[0][4], l[0][5], l[0][6], l[0][7], precision
);
d = d.concat(f.slice(0, -2));
h = l[1];
p = s[y];
}
// compute the quadratic Bezier curve using the cubic control points
f = _cubicToQuad(h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], precision);
pts = d.concat(f);
}
// return pathdata commands
let commands = [];
for (let j = 2; j < pts.length; j += 4) {
commands.push({
type: "Q",
values: [pts[j], pts[j + 1], pts[j + 2], pts[j + 3]]
});
}
return commands;
function _cubicToQuad(x0, y0, cp1x, cp1y, cp2x, cp2y, px, py, c = 0.1) {
const calcPowerCoefficients = (p0, cp1, cp2, p) => {
return [
{
x: (p.x - p0.x) + (cp1.x - cp2.x) * 3,
y: (p.y - p0.y) + (cp1.y - cp2.y) * 3
},
{
x: (p0.x + cp2.x) * 3 - cp1.x * 6,
y: (p0.y + cp2.y) * 3 - cp1.y * 6
},
{
x: (cp1.x - p0.x) * 3,
y: (cp1.y - p0.y) * 3
},
p0
];
}
const isApproximationClose = (p0, cp1, cp2, p, pointArr, precision) => {
for (let u = 1 / pointArr.length, a = 0; a < pointArr.length; a++) {
if (!isSegmentApproximationClose(p0, cp1, cp2, p, a * u, (a + 1) * u, pointArr[a][0], pointArr[a][1], pointArr[a][2], precision)) {
return false;
}
}
return true;
}
const calcPoint = (p0, cp1, cp2, p, t) => {
return {
x: ((p0.x * t + cp1.x) * t + cp2.x) * t + p.x,
y: ((p0.y * t + cp1.y) * t + cp2.y) * t + p.y,
};
}
const calcPointQuad = (p0, cp1, p, t) => {
return {
x: ((p0.x * t + cp1.x) * t) + p.x,
y: ((p0.y * t + cp1.y) * t) + p.y,
}
}
const calcPointDerivative = (p0, cp1, p, k, t) => {
return {
x: ((p0.x * 3 * t + cp1.x * 2) * t) + p.x,
y: ((p0.y * 3 * t + cp1.y * 2) * t) + p.y,
}
}
const processSegment = (p0, cp1, cp2, p, t1, t2) => {
var u = calcPoint(p0, cp1, cp2, p, t1),
a = calcPoint(p0, cp1, cp2, p, t2),
c = calcPointDerivative(p0, cp1, cp2, p, t1),
s = calcPointDerivative(p0, cp1, cp2, p, t2),
f = -c.x * s.y + s.x * c.y;
return Math.abs(f) < 1e-8 ? [
u,
{
x: (u.x + a.x) / 2,
y: (u.y + a.y) / 2
},
a
]
: [
u,
{
x: (c.x * (a.y * s.x - a.x * s.y) + s.x * (u.x * c.y - u.y * c.x)) / f,
y: (c.y * (a.y * s.x - a.x * s.y) + s.y * (u.x * c.y - u.y * c.x)) / f
},
a
];
}
const isSegmentApproximationClose = (p0, cp1, cp2, p, t1, t2, px, py, c, precision) => {
const calcPowerCoefficientsQuad = (p0, cp1, p) => {
return [
{ x: cp1.x * -2 + p0.x + p.x, y: cp1.y * -2 + p0.y + p.y },
{ x: (cp1.x - p0.x) * 2, y: (cp1.y - p0.y) * 2, }, p0
]
}
const minDistanceToLineSq = (p0, cp1, p) => {
let o = { x: (p.x - cp1.x), y: (p.y - cp1.y), }
let r = (p0.x - cp1.x) * o.x + (p0.y - cp1.y) * o.y;
let e = o.x * o.x + o.y * o.y;
let result = 0;
if (e != 0) {
result = r / e
}
if (result <= 0) {
result = Math.pow((p0.x - cp1.x), 2) + Math.pow((p0.y - cp1.y), 2);
} else if (result >= 1) {
result = Math.pow((p0.x - p.x), 2) + Math.pow((p0.y - p.y), 2)
} else {
result = Math.pow((p0.x - (cp1.x + o.x * result)), 2) + Math.pow((p0.y - (cp1.y + o.y * result)), 2);
}
return result
}
let l, d, h, p2, y,
P = calcPowerCoefficientsQuad(px, py, c),
m = P[0],
x = P[1],
b = P[2],
v = precision * precision,
w = [],
g = [];
for (l = (t2 - t1) / 10, d = 0, t = t1; d <= 10; d++, t += l) {
w.push(calcPoint(p0, cp1, cp2, p, t))
}
for (l = 0.1, d = 0, t = 0; d <= 10; d++, t += l) {
g.push(calcPointQuad(m, x, b, t))
}
for (d = 1; d < w.length - 1; d++) {
for (y = 1 / 0, h = 0; h < g.length - 1; h++) {
p2 = minDistanceToLineSq(w[d], g[h], g[h + 1]), y = Math.min(y, p2)
}
if (y > v) {
return false;
}
}
for (d = 1; d < g.length - 1; d++) {
for (y = 1 / 0, h = 0; h < w.length - 1; h++)
p2 = minDistanceToLineSq(g[d], w[h], w[h + 1]), y = Math.min(y, p2);
if (y > v)
return false;
}
return true;
}
for (
f = { x: x0, y: y0 },
l = { x: cp1x, y: cp1y },
d = { x: cp2x, y: cp2y },
h = { x: px, y: py },
p = calcPowerCoefficients(f, l, d, h),
y = p[0],
P = p[1],
m = p[2], x = p[3],
b = 1; b <= 8; b++) {
s = [];
for (let v = 0; v < 1; v += 1 / b) {
s.push(processSegment(y, P, m, x, v, v + 1 / b));
}
let b1 = ((s[0][1].x - f.x) * (l.x - f.x)) + ((s[0][1].y - f.y) * (l.y - f.y))
let b2 = ((s[0][1].x - h.x) * (d.x - h.x)) + ((s[0][1].y - h.y) * (d.y - h.y))
if (
(1 !== b || !(b1 < 0 || b2 < 0)) &&
isApproximationClose(y, P, m, x, s, c)
) {
break;
}
}
//return pts;
let pts = [s[0][0].x, s[0][0].y];
for (let i = 0; i < s.length; i++) {
pts.push(s[i][1].x, s[i][1].y, s[i][2].x, s[i][2].y)
}
return pts
}
}
/**
* convert arctocommands to cubic bezier
* based on puzrin's a2c.js
* https://github.com/fontello/svgpath/blob/master/lib/a2c.js
* returns pathData array
*/
function arcToBezier(p0, values, splitSegments = 1) {
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
}
//ratio must be at least 1
let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1
// increase segments for more accureate length calculations
let segments = ratio * splitSegments;
ang2 /= segments
let pathDataArc = [];
// 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 = 'C'
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)
})
pathDataArc.push(com);
ang1 += ang2
}
return pathDataArc;
}
/**
* add readable command point data
* to pathData command objects
*/
function pathDataToVerbose(pathData) {
let pathDataOriginal = JSON.parse(JSON.stringify(pathData))
// normalize
pathData = pathDataToLonghands(pathDataToAbsolute(pathData));
let pathDataVerbose = [];
let pathDataL = pathData.length;
let closed = pathData[pathDataL - 1].type.toLowerCase() === 'z' ? true : false;
pathData.forEach((com, i) => {
let {
type,
values
} = com;
let comO = pathDataOriginal[i];
let typeO = comO.type;
let valuesO = comO.values;
let typeLc = typeO.toLowerCase();
let valuesL = values.length;
let isRel = typeO === typeO.toLowerCase();
let comPrev = pathData[i - 1] ? pathData[i - 1] : false;
let comPrevValues = comPrev ? comPrev.values : [];
let comPrevValuesL = comPrevValues.length;
let p0 = {
x: comPrevValues[comPrevValuesL - 2],
y: comPrevValues[comPrevValuesL - 1]
}
let p = valuesL ? {
x: values[valuesL - 2],
y: values[valuesL - 1]
} : (i === pathData.length - 1 && closed ? pathData[0].values : false);
let comObj = {
type: typeO,
values: valuesO,
valuesAbsolute: values,
pFinal: p,
isRelative: isRel
}
if (comPrevValuesL) {
comObj.pPrev = p0
}
switch (typeLc) {
case 'q':
comObj.cp1 = {
x: values[valuesL - 4],
y: values[valuesL - 3]
}
break;
case 'c':
comObj.cp1 = {
x: values[valuesL - 6],
y: values[valuesL - 5]
}
comObj.cp2 = {
x: values[valuesL - 4],
y: values[valuesL - 3]
}
break;
case 'a':
// parametrized arc rx and ry values
let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
comObj.rx = arcData.rx
comObj.ry = arcData.ry
comObj.xAxisRotation = values[2]
comObj.largeArcFlag = values[3]
comObj.sweepFlag = values[4]
comObj.startAngle = arcData.startAngle
comObj.endAngle = arcData.endAngle
comObj.deltaAngle = arcData.deltaAngle
break;
}
pathDataVerbose.push(comObj);
});
return pathDataVerbose;
}
/**
* convert pathData nested array notation
* as used in snap and other libraries
*/
function convertArrayPathData(pathDataArray) {
let pathData = [];
pathDataArray.forEach(com => {
let type = com.shift();
pathData.push({
type: type,
values: com
})
})
return pathData;
}
function revertPathDataToArray(pathData) {
let pathDataArray = [];
pathData.forEach(com => {
pathDataArray.push([com.type, com.values].flat())
})
return pathDataArray;
}
/**
* parmetrize path arcto commands
* based on @cuixiping;
* https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
*/
function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2) {
let {cos, sin, atan2, sqrt, abs, min, max, PI} = Math;
// helper for angle calculation
const getAngle = (cx, cy, x, y) => {
return atan2(y - cy, x - cx);
};
// make sure rx, ry are positive
rx = abs(rx);
ry = abs(ry);
/**
* if rx===ry x-axis rotation is ignored
* otherwise convert degrees to radians
*/
let phi = rx === ry ? 0 : (xAxisRotation * PI) / 180;
let cx, cy
// create data object
let arcData = {
cx: 0,
cy: 0,
// rx/ry values may be deceptive in arc commands
rx: rx,
ry: ry,
startAngle: 0,
endAngle: 0,
deltaAngle: 0,
clockwise: sweep
};
if (rx == 0 || ry == 0) {
// invalid arguments
throw Error("rx and ry can not be 0");
}
let s_phi = !phi ? 0 : sin(phi);
let c_phi = !phi ? 1 : cos(phi);
let hd_x = (x1 - x2) / 2;
let hd_y = (y1 - y2) / 2;
let hs_x = (x1 + x2) / 2;
let hs_y = (y1 + y2) / 2;
// F6.5.1
let x1_ = !phi ? hd_x : c_phi * hd_x + s_phi * hd_y;
let y1_ = !phi ? hd_y : c_phi * hd_y - s_phi * hd_x;
// F.6.6 Correction of out-of-range radii
// Step 3: Ensure radii are large enough
let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
if (lambda > 1) {
rx = rx * sqrt(lambda);
ry = ry * sqrt(lambda);
// save real rx/ry
arcData.rx = rx;
arcData.ry = ry;
}
let rxry = rx * ry;
let rxy1_ = rx * y1_;
let ryx1_ = ry * x1_;
let sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
if (!sum_of_sq) {
throw Error("start point can not be same as end point");
}
let coe = sqrt(abs((rxry * rxry - sum_of_sq) / sum_of_sq));
if (largeArc == sweep) {
coe = -coe;
}
// F6.5.2
let cx_ = (coe * rxy1_) / ry;
let cy_ = (-coe * ryx1_) / rx;
/** F6.5.3
* center point of ellipse
*/
cx = !phi ? hs_x + cx_ : c_phi * cx_ - s_phi * cy_ + hs_x;
cy = !phi ? hs_y + cy_ : s_phi * cx_ + c_phi * cy_ + hs_y;
arcData.cy = cy;
arcData.cx = cx;
/** F6.5.5
* calculate angles between center point and
* commands starting and final on path point
*/
let startAngle = getAngle(cx, cy, x1, y1);
let endAngle = getAngle(cx, cy, x2, y2);
endAngle = (endAngle<0 && startAngle>0 && !largeArc) || (endAngle<startAngle && largeArc) ? endAngle+PI*2 : endAngle;
let deltaAngle = endAngle - startAngle
//let deltaAngle = largeArc ? 360 +
arcData.startAngle = startAngle;
arcData.endAngle = endAngle;
arcData.deltaAngle = deltaAngle;
return arcData;
}
pathDataConvert.convertPathData = convertPathData;
pathDataConvert.quadratic2Cubic = quadratic2Cubic;
pathDataConvert.roundPathData = roundPathData;
pathDataConvert.pathDataToRelative = pathDataToRelative;
pathDataConvert.pathDataToAbsolute = pathDataToAbsolute;
pathDataConvert.pathDataToLonghands = pathDataToLonghands;
pathDataConvert.pathDataToShorthands = pathDataToShorthands;
pathDataConvert.pathDataToQuadratic = pathDataToQuadratic;
pathDataConvert.cubicToQuad = cubicToQuad;
pathDataConvert.arcToBezier = arcToBezier;
pathDataConvert.pathDataToVerbose = pathDataToVerbose;
pathDataConvert.convertArrayPathData = convertArrayPathData;
pathDataConvert.revertPathDataToArray = revertPathDataTo