svg-path-commander
Version:
🛹 Modern TypeScript tools for SVG
1,947 lines (1,946 loc) • 119 kB
JavaScript
/*!
* SVGPathCommander v2.2.1 (http://thednp.github.io/svg-path-commander)
* Copyright 2026 © thednp
* Licensed under MIT (https://github.com/thednp/svg-path-commander/blob/master/LICENSE)
*/
import CSSMatrix from "@thednp/dommatrix";
//#region src/math/midPoint.ts
/**
* Returns the coordinates of a specified distance
* ratio between two points.
*
* @param a the first point coordinates
* @param b the second point coordinates
* @param t the ratio
* @returns the midpoint coordinates
*/
const midPoint = ([ax, ay], [bx, by], t) => {
return [ax + (bx - ax) * t, ay + (by - ay) * t];
};
//#endregion
//#region src/math/distanceSquareRoot.ts
/**
* Returns the square root of the distance
* between two given points.
*
* @param a the first point coordinates
* @param b the second point coordinates
* @returns the distance value
*/
const distanceSquareRoot = (a, b) => {
return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
};
//#endregion
//#region src/math/lineTools.ts
/**
* Returns length for line segments (MoveTo, LineTo).
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the line segment length
*/
const getLineLength = (x1, y1, x2, y2) => {
return distanceSquareRoot([x1, y1], [x2, y2]);
};
/**
* Returns a point along the line segments (MoveTo, LineTo).
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @param distance the distance to point in [0-1] range
* @returns the point at length
*/
const getPointAtLineLength = (x1, y1, x2, y2, distance) => {
let point = {
x: x1,
y: y1
};
if (typeof distance === "number") {
const length = distanceSquareRoot([x1, y1], [x2, y2]);
if (distance <= 0) point = {
x: x1,
y: y1
};
else if (distance >= length) point = {
x: x2,
y: y2
};
else {
const [x, y] = midPoint([x1, y1], [x2, y2], distance / length);
point = {
x,
y
};
}
}
return point;
};
/**
* Returns bounding box for line segments (MoveTo, LineTo).
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the bounding box for line segments
*/
const getLineBBox = (x1, y1, x2, y2) => {
const { min, max } = Math;
return [
min(x1, x2),
min(y1, y2),
max(x1, x2),
max(y1, y2)
];
};
const lineTools = {
getLineBBox,
getLineLength,
getPointAtLineLength
};
//#endregion
//#region src/math/arcTools.ts
/**
* Returns the Arc segment length.
* @param rx radius along X axis
* @param ry radius along Y axis
* @param theta the angle in radians
* @returns the arc length
*/
const arcLength = (rx, ry, theta) => {
const halfTheta = theta / 2;
const sinHalfTheta = Math.sin(halfTheta);
const cosHalfTheta = Math.cos(halfTheta);
const term1 = rx ** 2 * sinHalfTheta ** 2;
const term2 = ry ** 2 * cosHalfTheta ** 2;
const length = Math.sqrt(term1 + term2) * theta;
return Math.abs(length);
};
/**
* Find point on ellipse at given angle around ellipse (theta);
* @param cx the center X
* @param cy the center Y
* @param rx the radius X
* @param ry the radius Y
* @param alpha the arc rotation angle in radians
* @param theta the arc sweep angle in radians
* @returns a point around ellipse at given angle
*/
const arcPoint = (cx, cy, rx, ry, alpha, theta) => {
const { sin, cos } = Math;
const cosA = cos(alpha);
const sinA = sin(alpha);
const x = rx * cos(theta);
const y = ry * sin(theta);
return [cx + cosA * x - sinA * y, cy + sinA * x + cosA * y];
};
/**
* Returns the angle between two points.
* @param v0 starting point
* @param v1 ending point
* @returns the angle in radian
*/
const angleBetween = (v0, v1) => {
const { x: v0x, y: v0y } = v0;
const { x: v1x, y: v1y } = v1;
const p = v0x * v1x + v0y * v1y;
const n = Math.sqrt((v0x ** 2 + v0y ** 2) * (v1x ** 2 + v1y ** 2));
return (v0x * v1y - v0y * v1x < 0 ? -1 : 1) * Math.acos(p / n);
};
/**
* Returns the following properties for an Arc segment: center, start angle,
* end angle, and radiuses on X and Y axis.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param RX the radius on X axis
* @param RY the radius on Y axis
* @param angle the ellipse rotation in degrees
* @param LAF the large arc flag
* @param SF the sweep flag
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns properties specific to Arc segments
*/
const getArcProps = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
const { abs, sin, cos, sqrt, PI } = Math;
let rx = abs(RX);
let ry = abs(RY);
const xRotRad = (angle % 360 + 360) % 360 * (PI / 180);
if (x1 === x && y1 === y) return {
rx,
ry,
startAngle: 0,
endAngle: 0,
center: {
x,
y
}
};
if (rx === 0 || ry === 0) return {
rx,
ry,
startAngle: 0,
endAngle: 0,
center: {
x: (x + x1) / 2,
y: (y + y1) / 2
}
};
const dx = (x1 - x) / 2;
const dy = (y1 - y) / 2;
const transformedPoint = {
x: cos(xRotRad) * dx + sin(xRotRad) * dy,
y: -sin(xRotRad) * dx + cos(xRotRad) * dy
};
const radiiCheck = transformedPoint.x ** 2 / rx ** 2 + transformedPoint.y ** 2 / ry ** 2;
if (radiiCheck > 1) {
rx *= sqrt(radiiCheck);
ry *= sqrt(radiiCheck);
}
let cRadicand = (rx ** 2 * ry ** 2 - rx ** 2 * transformedPoint.y ** 2 - ry ** 2 * transformedPoint.x ** 2) / (rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * transformedPoint.x ** 2);
cRadicand = cRadicand < 0 ? 0 : cRadicand;
const cCoef = (LAF !== SF ? 1 : -1) * sqrt(cRadicand);
const transformedCenter = {
x: cCoef * (rx * transformedPoint.y / ry),
y: cCoef * (-(ry * transformedPoint.x) / rx)
};
const center = {
x: cos(xRotRad) * transformedCenter.x - sin(xRotRad) * transformedCenter.y + (x1 + x) / 2,
y: sin(xRotRad) * transformedCenter.x + cos(xRotRad) * transformedCenter.y + (y1 + y) / 2
};
const startVector = {
x: (transformedPoint.x - transformedCenter.x) / rx,
y: (transformedPoint.y - transformedCenter.y) / ry
};
const startAngle = angleBetween({
x: 1,
y: 0
}, startVector);
let sweepAngle = angleBetween(startVector, {
x: (-transformedPoint.x - transformedCenter.x) / rx,
y: (-transformedPoint.y - transformedCenter.y) / ry
});
if (!SF && sweepAngle > 0) sweepAngle -= 2 * PI;
else if (SF && sweepAngle < 0) sweepAngle += 2 * PI;
sweepAngle %= 2 * PI;
return {
center,
startAngle,
endAngle: startAngle + sweepAngle,
rx,
ry
};
};
/**
* Returns the length of an Arc segment.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param c1x the first control point X
* @param c1y the first control point Y
* @param c2x the second control point X
* @param c2y the second control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the length of the Arc segment
*/
const getArcLength = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
const { rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
return arcLength(rx, ry, endAngle - startAngle);
};
/**
* Returns a point along an Arc segment at a given distance.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param RX the radius on X axis
* @param RY the radius on Y axis
* @param angle the ellipse rotation in degrees
* @param LAF the large arc flag
* @param SF the sweep flag
* @param x2 the ending point X
* @param y2 the ending point Y
* @param distance the distance along the arc
* @returns a point along the Arc segment
*/
const getPointAtArcLength = (x1, y1, RX, RY, angle, LAF, SF, x, y, distance) => {
let point = {
x: x1,
y: y1
};
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
if (typeof distance === "number") {
const length = arcLength(rx, ry, endAngle - startAngle);
if (distance <= 0) point = {
x: x1,
y: y1
};
else if (distance >= length) point = {
x,
y
};
else {
if (x1 === x && y1 === y) return {
x,
y
};
if (rx === 0 || ry === 0) return getPointAtLineLength(x1, y1, x, y, distance);
const { PI, cos, sin } = Math;
const sweepAngle = endAngle - startAngle;
const xRotRad = (angle % 360 + 360) % 360 * (PI / 180);
const alpha = startAngle + sweepAngle * (distance / length);
const ellipseComponentX = rx * cos(alpha);
const ellipseComponentY = ry * sin(alpha);
point = {
x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y
};
}
}
return point;
};
/**
* Returns the extrema for an Arc segment in the following format:
* [MIN_X, MIN_Y, MAX_X, MAX_Y]
*
* @see https://github.com/herrstrietzel/svg-pathdata-getbbox
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param RX the radius on X axis
* @param RY the radius on Y axis
* @param angle the ellipse rotation in degrees
* @param LAF the large arc flag
* @param SF the sweep flag
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the extrema of the Arc segment
*/
const getArcBBox = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
const deltaAngle = endAngle - startAngle;
const { min, max, tan, atan2, PI } = Math;
const { x: cx, y: cy } = center;
const alpha = angle * PI / 180;
const tangent = tan(alpha);
/**
* find min/max from zeroes of directional derivative along x and y
* along x axis
*/
const theta = atan2(-ry * tangent, rx);
const angle1 = theta;
const angle2 = theta + PI;
const angle3 = atan2(ry, rx * tangent);
const angle4 = angle3 + PI;
const xArray = [x];
const yArray = [y];
let xMin = min(x1, x);
let xMax = max(x1, x);
let yMin = min(y1, y);
let yMax = max(y1, y);
const pP2 = arcPoint(cx, cy, rx, ry, alpha, endAngle - deltaAngle * 1e-5);
const pP3 = arcPoint(cx, cy, rx, ry, alpha, endAngle - deltaAngle * .99999);
/**
* expected extremes
* if leaving inner bounding box
* (between segment start and end point)
* otherwise exclude elliptic extreme points
*/
if (pP2[0] > xMax || pP3[0] > xMax) {
const p1 = arcPoint(cx, cy, rx, ry, alpha, angle1);
xArray.push(p1[0]);
yArray.push(p1[1]);
}
if (pP2[0] < xMin || pP3[0] < xMin) {
const p2 = arcPoint(cx, cy, rx, ry, alpha, angle2);
xArray.push(p2[0]);
yArray.push(p2[1]);
}
if (pP2[1] < yMin || pP3[1] < yMin) {
const p4 = arcPoint(cx, cy, rx, ry, alpha, angle4);
xArray.push(p4[0]);
yArray.push(p4[1]);
}
if (pP2[1] > yMax || pP3[1] > yMax) {
const p3 = arcPoint(cx, cy, rx, ry, alpha, angle3);
xArray.push(p3[0]);
yArray.push(p3[1]);
}
xMin = min.apply([], xArray);
yMin = min.apply([], yArray);
xMax = max.apply([], xArray);
yMax = max.apply([], yArray);
return [
xMin,
yMin,
xMax,
yMax
];
};
const arcTools = {
angleBetween,
arcLength,
arcPoint,
getArcBBox,
getArcLength,
getArcProps,
getPointAtArcLength
};
//#endregion
//#region src/math/bezier.ts
/**
* Tools from bezier.js by Mike 'Pomax' Kamermans
* @see https://github.com/Pomax/bezierjs
*/
const Tvalues = [
-.06405689286260563,
.06405689286260563,
-.1911188674736163,
.1911188674736163,
-.3150426796961634,
.3150426796961634,
-.4337935076260451,
.4337935076260451,
-.5454214713888396,
.5454214713888396,
-.6480936519369755,
.6480936519369755,
-.7401241915785544,
.7401241915785544,
-.820001985973903,
.820001985973903,
-.8864155270044011,
.8864155270044011,
-.9382745520027328,
.9382745520027328,
-.9747285559713095,
.9747285559713095,
-.9951872199970213,
.9951872199970213
];
const Cvalues = [
.12793819534675216,
.12793819534675216,
.1258374563468283,
.1258374563468283,
.12167047292780339,
.12167047292780339,
.1155056680537256,
.1155056680537256,
.10744427011596563,
.10744427011596563,
.09761865210411388,
.09761865210411388,
.08619016153195327,
.08619016153195327,
.0733464814110803,
.0733464814110803,
.05929858491543678,
.05929858491543678,
.04427743881741981,
.04427743881741981,
.028531388628933663,
.028531388628933663,
.0123412297999872,
.0123412297999872
];
/**
* @param points
* @returns
*/
const deriveBezier = (points) => {
const dpoints = [];
for (let p = points, d = p.length, c = d - 1; d > 1; d -= 1, c -= 1) {
const list = [];
for (let j = 0; j < c; j += 1) list.push({
x: c * (p[j + 1].x - p[j].x),
y: c * (p[j + 1].y - p[j].y),
t: 0
});
dpoints.push(list);
p = list;
}
return dpoints;
};
/**
* @param points
* @param t
*/
const computeBezier = (points, t) => {
if (t === 0) {
points[0].t = 0;
return points[0];
}
const order = points.length - 1;
if (t === 1) {
points[order].t = 1;
return points[order];
}
const mt = 1 - t;
let p = points;
if (order === 0) {
points[0].t = t;
return points[0];
}
if (order === 1) return {
x: mt * p[0].x + t * p[1].x,
y: mt * p[0].y + t * p[1].y,
t
};
const mt2 = mt * mt;
const t2 = t * t;
let a = 0;
let b = 0;
let c = 0;
let d = 0;
if (order === 2) {
p = [
p[0],
p[1],
p[2],
{
x: 0,
y: 0
}
];
a = mt2;
b = mt * t * 2;
c = t2;
} else if (order === 3) {
a = mt2 * mt;
b = mt2 * t * 3;
c = mt * t2 * 3;
d = t * t2;
}
return {
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
t
};
};
const calculateBezier = (derivativeFn, t) => {
const d = derivativeFn(t);
const l = d.x * d.x + d.y * d.y;
return Math.sqrt(l);
};
const bezierLength = (derivativeFn) => {
const z = .5;
const len = Tvalues.length;
let sum = 0;
for (let i = 0, t; i < len; i++) {
t = z * Tvalues[i] + z;
sum += Cvalues[i] * calculateBezier(derivativeFn, t);
}
return z * sum;
};
/**
* Returns the length of CubicBezier / Quad segment.
* @param curve cubic / quad bezier segment
*/
const getBezierLength = (curve) => {
const points = [];
for (let idx = 0, len = curve.length, step = 2; idx < len; idx += step) points.push({
x: curve[idx],
y: curve[idx + 1]
});
const dpoints = deriveBezier(points);
return bezierLength((t) => {
return computeBezier(dpoints[0], t);
});
};
const CBEZIER_MINMAX_EPSILON = 1e-8;
/**
* Returns the most extreme points in a Quad Bezier segment.
* @param A an array which consist of X/Y values
*/
const minmaxQ = ([v1, cp, v2]) => {
const min = Math.min(v1, v2);
const max = Math.max(v1, v2);
if (cp >= v1 ? v2 >= cp : v2 <= cp) return [min, max];
const E = (v1 * v2 - cp * cp) / (v1 - 2 * cp + v2);
return E < min ? [E, max] : [min, E];
};
/**
* Returns the most extreme points in a Cubic Bezier segment.
* @param A an array which consist of X/Y values
* @see https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127
*/
const minmaxC = ([v1, cp1, cp2, v2]) => {
const K = v1 - 3 * cp1 + 3 * cp2 - v2;
if (Math.abs(K) < 1e-8) {
if (v1 === v2 && v1 === cp1) return [v1, v2];
return minmaxQ([
v1,
-.5 * v1 + 1.5 * cp1,
v1 - 3 * cp1 + 3 * cp2
]);
}
const T = -v1 * cp2 + v1 * v2 - cp1 * cp2 - cp1 * v2 + cp1 * cp1 + cp2 * cp2;
if (T <= 0) return [Math.min(v1, v2), Math.max(v1, v2)];
const S = Math.sqrt(T);
let min = Math.min(v1, v2);
let max = Math.max(v1, v2);
const L = v1 - 2 * cp1 + cp2;
for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) if (R > 0 && R < 1) {
const Q = v1 * (1 - R) * (1 - R) * (1 - R) + cp1 * 3 * (1 - R) * (1 - R) * R + cp2 * 3 * (1 - R) * R * R + v2 * R * R * R;
if (Q < min) min = Q;
if (Q > max) max = Q;
}
return [min, max];
};
const bezierTools = {
bezierLength,
calculateBezier,
CBEZIER_MINMAX_EPSILON,
computeBezier,
Cvalues,
deriveBezier,
getBezierLength,
minmaxC,
minmaxQ,
Tvalues
};
//#endregion
//#region src/math/cubicTools.ts
/**
* Returns a point at a given length of a CubicBezier segment.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param c1x the first control point X
* @param c1y the first control point Y
* @param c2x the second control point X
* @param c2y the second control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @param t a [0-1] ratio
* @returns the point at cubic-bezier segment length
*/
const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2], t) => {
const t1 = 1 - t;
return {
x: t1 ** 3 * x1 + 3 * t1 ** 2 * t * c1x + 3 * t1 * t ** 2 * c2x + t ** 3 * x2,
y: t1 ** 3 * y1 + 3 * t1 ** 2 * t * c1y + 3 * t1 * t ** 2 * c2y + t ** 3 * y2
};
};
/**
* Returns the length of a CubicBezier segment.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param c1x the first control point X
* @param c1y the first control point Y
* @param c2x the second control point X
* @param c2y the second control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the CubicBezier segment length
*/
const getCubicLength = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
return getBezierLength([
x1,
y1,
c1x,
c1y,
c2x,
c2y,
x2,
y2
]);
};
/**
* Returns the point along a CubicBezier segment at a given distance.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param c1x the first control point X
* @param c1y the first control point Y
* @param c2x the second control point X
* @param c2y the second control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @param distance the distance to look at
* @returns the point at CubicBezier length
*/
const getPointAtCubicLength = (x1, y1, c1x, c1y, c2x, c2y, x2, y2, distance) => {
const distanceIsNumber = typeof distance === "number";
let point = {
x: x1,
y: y1
};
if (distanceIsNumber) {
const currentLength = getBezierLength([
x1,
y1,
c1x,
c1y,
c2x,
c2y,
x2,
y2
]);
if (distance <= 0) {} else if (distance >= currentLength) point = {
x: x2,
y: y2
};
else point = getPointAtCubicSegmentLength([
x1,
y1,
c1x,
c1y,
c2x,
c2y,
x2,
y2
], distance / currentLength);
}
return point;
};
/**
* Returns the bounding box of a CubicBezier segment in the following format:
* [MIN_X, MIN_Y, MAX_X, MAX_Y]
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param c1x the first control point X
* @param c1y the first control point Y
* @param c2x the second control point X
* @param c2y the second control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the extrema of the CubicBezier segment
*/
const getCubicBBox = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
const cxMinMax = minmaxC([
x1,
c1x,
c2x,
x2
]);
const cyMinMax = minmaxC([
y1,
c1y,
c2y,
y2
]);
return [
cxMinMax[0],
cyMinMax[0],
cxMinMax[1],
cyMinMax[1]
];
};
const cubicTools = {
getCubicBBox,
getCubicLength,
getPointAtCubicLength,
getPointAtCubicSegmentLength
};
//#endregion
//#region src/math/quadTools.ts
/**
* Returns the {x,y} coordinates of a point at a
* given length of a quadratic-bezier segment.
*
* @see https://github.com/substack/point-at-length
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param cx the control point X
* @param cy the control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @param t a [0-1] ratio
* @returns the requested {x,y} coordinates
*/
const getPointAtQuadSegmentLength = ([x1, y1, cx, cy, x2, y2], t) => {
const t1 = 1 - t;
return {
x: t1 ** 2 * x1 + 2 * t1 * t * cx + t ** 2 * x2,
y: t1 ** 2 * y1 + 2 * t1 * t * cy + t ** 2 * y2
};
};
/**
* Returns the length of a QuadraticBezier segment.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param cx the control point X
* @param cy the control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the QuadraticBezier segment length
*/
const getQuadLength = (x1, y1, cx, cy, x2, y2) => {
return getBezierLength([
x1,
y1,
cx,
cy,
x2,
y2
]);
};
/**
* Returns the point along a QuadraticBezier segment at a given distance.
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param cx the control point X
* @param cy the control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @param distance the distance to look at
* @returns the point at QuadraticBezier length
*/
const getPointAtQuadLength = (x1, y1, cx, cy, x2, y2, distance) => {
const distanceIsNumber = typeof distance === "number";
let point = {
x: x1,
y: y1
};
if (distanceIsNumber) {
const currentLength = getBezierLength([
x1,
y1,
cx,
cy,
x2,
y2
]);
if (distance <= 0) {} else if (distance >= currentLength) point = {
x: x2,
y: y2
};
else point = getPointAtQuadSegmentLength([
x1,
y1,
cx,
cy,
x2,
y2
], distance / currentLength);
}
return point;
};
/**
* Returns the bounding box of a QuadraticBezier segment in the following format:
* [MIN_X, MIN_Y, MAX_X, MAX_Y]
*
* @param x1 the starting point X
* @param y1 the starting point Y
* @param cx the control point X
* @param cy the control point Y
* @param x2 the ending point X
* @param y2 the ending point Y
* @returns the extrema of the QuadraticBezier segment
*/
const getQuadBBox = (x1, y1, cx, cy, x2, y2) => {
const cxMinMax = minmaxQ([
x1,
cx,
x2
]);
const cyMinMax = minmaxQ([
y1,
cy,
y2
]);
return [
cxMinMax[0],
cyMinMax[0],
cxMinMax[1],
cyMinMax[1]
];
};
const quadTools = {
getPointAtQuadLength,
getPointAtQuadSegmentLength,
getQuadBBox,
getQuadLength
};
//#endregion
//#region src/math/polygonTools.ts
/**
* d3-polygon-area
* @see https://github.com/d3/d3-polygon
*
* Returns the area of a polygon.
*
* @param polygon Array of [x, y]
* @returns Signed area
*/
const polygonArea = (polygon) => {
const n = polygon.length;
let i = -1;
let a;
let b = polygon[n - 1];
let area = 0;
while (++i < n) {
a = b;
b = polygon[i];
area += a[1] * b[0] - a[0] * b[1];
}
return area / 2;
};
/**
* d3-polygon-length
* https://github.com/d3/d3-polygon
*
* Returns the perimeter of a polygon.
*
* @param polygon an array of coordinates
* @returns the polygon length
*/
const polygonLength = (polygon) => {
return polygon.reduce((length, point, i) => {
if (i) return length + distanceSquareRoot(polygon[i - 1], point);
return 0;
}, 0);
};
/**
* Computes the centroid (geometric center) of a polygon.
* Uses average of all endpoint coordinates (robust for polygons and curves).
*
* @param polygon A polygon with consists of [x, y] tuples
* @returns [x, y] centroid
*/
const polygonCentroid = (polygon) => {
if (polygon.length === 0) return [0, 0];
let sumX = 0;
let sumY = 0;
for (const [x, y] of polygon) {
sumX += x;
sumY += y;
}
const count = polygon.length;
return [sumX / count, sumY / count];
};
const polygonTools = {
polygonArea,
polygonLength,
polygonCentroid
};
//#endregion
//#region src/math/rotateVector.ts
/**
* Returns an {x,y} vector rotated by a given
* angle in radian.
*
* @param x the initial vector x
* @param y the initial vector y
* @param rad the radian vector angle
* @returns the rotated vector
*/
const rotateVector = (x, y, rad) => {
const { sin, cos } = Math;
return {
x: x * cos(rad) - y * sin(rad),
y: x * sin(rad) + y * cos(rad)
};
};
//#endregion
//#region src/math/roundTo.ts
/**
* Rounds a number to the specified number of decimal places.
*
* @param n - The number to round
* @param round - Number of decimal places
* @returns The rounded number
*/
const roundTo = (n, round) => {
const pow = round >= 1 ? 10 ** round : 1;
return round > 0 ? Math.round(n * pow) / pow : Math.round(n);
};
//#endregion
//#region src/parser/paramsCount.ts
/** Segment params length */
const paramsCounts = {
a: 7,
c: 6,
h: 1,
l: 2,
m: 2,
r: 4,
q: 4,
s: 4,
t: 2,
v: 1,
z: 0
};
//#endregion
//#region src/parser/finalizeSegment.ts
/**
* Breaks the parsing of a pathString once a segment is finalized.
*
* @param path - The PathParser instance
*/
const finalizeSegment = (path) => {
let pathCommand = path.pathValue[path.segmentStart];
let relativeCommand = pathCommand.toLowerCase();
const { data } = path;
while (data.length >= paramsCounts[relativeCommand]) {
if (relativeCommand === "m" && data.length > 2) {
path.segments.push([pathCommand].concat(data.splice(0, 2)));
relativeCommand = "l";
pathCommand = pathCommand === "m" ? "l" : "L";
} else path.segments.push([pathCommand].concat(data.splice(0, paramsCounts[relativeCommand])));
if (!paramsCounts[relativeCommand]) break;
}
};
//#endregion
//#region src/util/error.ts
/** Error prefix used in all SVGPathCommander TypeError messages. */
const error = "SVGPathCommanderError";
//#endregion
//#region src/parser/scanFlag.ts
/**
* Validates an A (arc-to) specific path command value.
* Usually a `large-arc-flag` or `sweep-flag`.
*
* @param path - The PathParser instance
*/
const scanFlag = (path) => {
const { index, pathValue } = path;
const code = pathValue.charCodeAt(index);
if (code === 48) {
path.param = 0;
path.index += 1;
return;
}
if (code === 49) {
path.param = 1;
path.index += 1;
return;
}
path.err = `${error}: invalid Arc flag "${pathValue[index]}", expecting 0 or 1 at index ${index}`;
};
//#endregion
//#region src/parser/isDigit.ts
/**
* Checks if a character is a digit.
*
* @param code the character to check
* @returns check result
*/
const isDigit = (code) => {
return code >= 48 && code <= 57;
};
//#endregion
//#region src/parser/invalidPathValue.ts
/** Error message prefix used when a path string cannot be parsed. */
const invalidPathValue = "Invalid path value";
//#endregion
//#region src/parser/scanParam.ts
/**
* Validates every character of the path string,
* every path command, negative numbers or floating point numbers.
*
* @param path - The PathParser instance
*/
const scanParam = (path) => {
const { max, pathValue, index: start } = path;
let index = start;
let zeroFirst = false;
let hasCeiling = false;
let hasDecimal = false;
let hasDot = false;
let ch;
if (index >= max) {
path.err = `${error}: ${invalidPathValue} at index ${index}, "pathValue" is missing param`;
return;
}
ch = pathValue.charCodeAt(index);
if (ch === 43 || ch === 45) {
index += 1;
ch = pathValue.charCodeAt(index);
}
if (!isDigit(ch) && ch !== 46) {
path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" is not a number`;
return;
}
if (ch !== 46) {
zeroFirst = ch === 48;
index += 1;
ch = pathValue.charCodeAt(index);
if (zeroFirst && index < max) {
if (ch && isDigit(ch)) {
path.err = `${error}: ${invalidPathValue} at index ${start}, "${pathValue[start]}" illegal number`;
return;
}
}
while (index < max && isDigit(pathValue.charCodeAt(index))) {
index += 1;
hasCeiling = true;
}
ch = pathValue.charCodeAt(index);
}
if (ch === 46) {
hasDot = true;
index += 1;
while (isDigit(pathValue.charCodeAt(index))) {
index += 1;
hasDecimal = true;
}
ch = pathValue.charCodeAt(index);
}
if (ch === 101 || ch === 69) {
if (hasDot && !hasCeiling && !hasDecimal) {
path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" invalid float exponent`;
return;
}
index += 1;
ch = pathValue.charCodeAt(index);
if (ch === 43 || ch === 45) index += 1;
if (index < max && isDigit(pathValue.charCodeAt(index))) while (index < max && isDigit(pathValue.charCodeAt(index))) index += 1;
else {
path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" invalid integer exponent`;
return;
}
}
path.index = index;
path.param = +path.pathValue.slice(start, index);
};
//#endregion
//#region src/parser/isSpace.ts
/**
* Checks if the character is a space.
*
* @param ch the character to check
* @returns check result
*/
const isSpace = (ch) => {
return [
5760,
6158,
8192,
8193,
8194,
8195,
8196,
8197,
8198,
8199,
8200,
8201,
8202,
8239,
8287,
12288,
65279,
10,
13,
8232,
8233,
32,
9,
11,
12,
160
].includes(ch);
};
//#endregion
//#region src/parser/skipSpaces.ts
/**
* Points the parser to the next character in the
* path string every time it encounters any kind of
* space character.
*
* @param path - The PathParser instance
*/
const skipSpaces = (path) => {
const { pathValue, max } = path;
while (path.index < max && isSpace(pathValue.charCodeAt(path.index))) path.index += 1;
};
//#endregion
//#region src/parser/isPathCommand.ts
/**
* Checks if the character is a path command.
*
* @param code the character to check
* @returns check result
*/
const isPathCommand = (code) => {
switch (code | 32) {
case 109:
case 122:
case 108:
case 104:
case 118:
case 99:
case 115:
case 113:
case 116:
case 97: return true;
default: return false;
}
};
//#endregion
//#region src/parser/isDigitStart.ts
/**
* Checks if the character is or belongs to a number.
* [0-9]|+|-|.
*
* @param code the character to check
* @returns check result
*/
const isDigitStart = (code) => {
return isDigit(code) || code === 43 || code === 45 || code === 46;
};
//#endregion
//#region src/parser/isArcCommand.ts
/**
* Checks if the character is an A (arc-to) path command.
*
* @param code the character to check
* @returns check result
*/
const isArcCommand = (code) => {
return (code | 32) === 97;
};
//#endregion
//#region src/parser/isMoveCommand.ts
/**
* Checks if the character is a MoveTo command.
*
* @param code the character to check
* @returns check result
*/
const isMoveCommand = (code) => {
switch (code | 32) {
case 109:
case 77: return true;
default: return false;
}
};
//#endregion
//#region src/parser/scanSegment.ts
/**
* Scans every character in the path string to determine
* where a segment starts and where it ends.
*
* @param path - The PathParser instance
*/
const scanSegment = (path) => {
const { max, pathValue, index, segments } = path;
const cmdCode = pathValue.charCodeAt(index);
const reqParams = paramsCounts[pathValue[index].toLowerCase()];
path.segmentStart = index;
if (!isPathCommand(cmdCode)) {
path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a path command at index ${index}`;
return;
}
const lastSegment = segments[segments.length - 1];
if (!isMoveCommand(cmdCode) && lastSegment?.[0]?.toLocaleLowerCase() === "z") {
path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a MoveTo path command at index ${index}`;
return;
}
path.index += 1;
skipSpaces(path);
path.data = [];
if (!reqParams) {
finalizeSegment(path);
return;
}
for (;;) {
for (let i = reqParams; i > 0; i -= 1) {
if (isArcCommand(cmdCode) && (i === 3 || i === 4)) scanFlag(path);
else scanParam(path);
if (path.err.length) return;
path.data.push(path.param);
skipSpaces(path);
if (path.index < max && pathValue.charCodeAt(path.index) === 44) {
path.index += 1;
skipSpaces(path);
}
}
if (path.index >= path.max) break;
if (!isDigitStart(pathValue.charCodeAt(path.index))) break;
}
finalizeSegment(path);
};
//#endregion
//#region src/parser/pathParser.ts
/**
* The `PathParser` is used by the `parsePathString` static method
* to generate a `pathArray`.
*
* @param pathString - The SVG path string to parse
*/
var PathParser = class {
constructor(pathString) {
this.segments = [];
this.pathValue = pathString;
this.max = pathString.length;
this.index = 0;
this.param = 0;
this.segmentStart = 0;
this.data = [];
this.err = "";
}
};
//#endregion
//#region src/parser/parsePathString.ts
/**
* Parses a path string value and returns an array
* of segments we like to call `PathArray`.
*
* If parameter value is already a `PathArray`,
* return a clone of it.
* @example
* parsePathString("M 0 0L50 50")
* // => [["M",0,0],["L",50,50]]
*
* @param pathInput the string to be parsed
* @returns the resulted `pathArray` or error string
*/
const parsePathString = (pathInput) => {
if (typeof pathInput !== "string") return pathInput.slice(0);
const path = new PathParser(pathInput);
skipSpaces(path);
while (path.index < path.max && !path.err.length) scanSegment(path);
if (!path.err.length) {
if (path.segments.length)
/**
* force absolute first M
* getPathBBox calculation requires first segment to be absolute
* @see https://github.com/thednp/svg-path-commander/pull/49
*/
path.segments[0][0] = "M";
} else throw TypeError(path.err);
return path.segments;
};
//#endregion
//#region src/process/absolutizeSegment.ts
/**
* Returns an absolute segment of a `PathArray` object.
*
* @param segment the segment object
* @param index the segment index
* @param lastX the last known X value
* @param lastY the last known Y value
* @returns the absolute segment
*/
const absolutizeSegment = (segment, index, lastX, lastY) => {
const [pathCommand] = segment;
const absCommand = pathCommand.toUpperCase();
if (index === 0 || absCommand === pathCommand) return segment;
if (absCommand === "A") return [
absCommand,
segment[1],
segment[2],
segment[3],
segment[4],
segment[5],
segment[6] + lastX,
segment[7] + lastY
];
else if (absCommand === "V") return [absCommand, segment[1] + lastY];
else if (absCommand === "H") return [absCommand, segment[1] + lastX];
else if (absCommand === "L") return [
absCommand,
segment[1] + lastX,
segment[2] + lastY
];
else {
const absValues = [];
const seglen = segment.length;
for (let j = 1; j < seglen; j += 1) absValues.push(segment[j] + (j % 2 ? lastX : lastY));
return [absCommand].concat(absValues);
}
};
//#endregion
//#region src/process/iterate.ts
/**
* Iterates over a `PathArray`, executing a callback for each segment.
* The callback can:
* - Read current position (`x`, `y`)
* - Modify the segment (return new segment)
* - Stop early (return `false`)
*
* The iterator maintains accurate current point (`x`, `y`) and subpath start (`mx`, `my`)
* while correctly handling relative/absolute commands, including H/V and Z.
*
* **Important**: If the callback returns a new segment with more coordinates (e.g., Q → C),
* the path length may increase, and iteration will continue over new segments.
*
* @template T - Specific PathArray type (e.g., CurveArray, PolylineArray)
* @param path - The source `PathArray` to iterate over
* @param iterator - Callback function for each segment
* @param iterator.segment - Current path segment
* @param iterator.index - Index of current segment
* @param iterator.x - Current X position (after applying relative offset)
* @param iterator.y - Current Y position (after applying relative offset)
* @returns The modified `path` (or original if no changes)
*
* @example
* iterate(path, (seg, i, x, y) => {
* if (seg[0] === 'L') return ['C', x, y, seg[1], seg[2], seg[1], seg[2]];
* });
*/
const iterate = (path, iterator) => {
let x = 0;
let y = 0;
let mx = 0;
let my = 0;
let i = 0;
while (i < path.length) {
const segment = path[i];
const [pathCommand] = segment;
const absCommand = pathCommand.toUpperCase();
const isRelative = absCommand !== pathCommand;
const iteratorResult = iterator(segment, i, x, y);
if (iteratorResult === false) break;
if (absCommand === "Z") {
x = mx;
y = my;
} else if (absCommand === "H") x = segment[1] + (isRelative ? x : 0);
else if (absCommand === "V") y = segment[1] + (isRelative ? y : 0);
else {
const segLen = segment.length;
x = segment[segLen - 2] + (isRelative ? x : 0);
y = segment[segLen - 1] + (isRelative ? y : 0);
if (absCommand === "M") {
mx = x;
my = y;
}
}
if (iteratorResult) path[i] = iteratorResult;
i += 1;
}
return path;
};
//#endregion
//#region src/convert/pathToAbsolute.ts
/**
* Parses a path string value or object and returns an array
* of segments, all converted to absolute values.
*
* @param pathInput - The path string or PathArray
* @returns The resulted PathArray with absolute values
*
* @example
* ```ts
* pathToAbsolute('M10 10l80 80')
* // => [['M', 10, 10], ['L', 90, 90]]
* ```
*/
const pathToAbsolute = (pathInput) => {
return iterate(parsePathString(pathInput), absolutizeSegment);
};
//#endregion
//#region src/process/relativizeSegment.ts
/**
* Returns a relative segment of a `PathArray` object.
*
* @param segment the segment object
* @param index the segment index
* @param lastX the last known X value
* @param lastY the last known Y value
* @returns the relative segment
*/
const relativizeSegment = (segment, index, lastX, lastY) => {
const [pathCommand] = segment;
const relCommand = pathCommand.toLowerCase();
if (index === 0 || pathCommand === relCommand) return segment;
if (relCommand === "a") return [
relCommand,
segment[1],
segment[2],
segment[3],
segment[4],
segment[5],
segment[6] - lastX,
segment[7] - lastY
];
else if (relCommand === "v") return [relCommand, segment[1] - lastY];
else if (relCommand === "h") return [relCommand, segment[1] - lastX];
else if (relCommand === "l") return [
relCommand,
segment[1] - lastX,
segment[2] - lastY
];
else {
const relValues = [];
const seglen = segment.length;
for (let j = 1; j < seglen; j += 1) relValues.push(segment[j] - (j % 2 ? lastX : lastY));
return [relCommand].concat(relValues);
}
};
//#endregion
//#region src/convert/pathToRelative.ts
/**
* Parses a path string value or object and returns an array
* of segments, all converted to relative values.
*
* @param pathInput - The path string or PathArray
* @returns The resulted PathArray with relative values
*
* @example
* ```ts
* pathToRelative('M10 10L90 90')
* // => [['M', 10, 10], ['l', 80, 80]]
* ```
*/
const pathToRelative = (pathInput) => {
return iterate(parsePathString(pathInput), relativizeSegment);
};
//#endregion
//#region src/process/arcToCubic.ts
/**
* Converts A (arc-to) segments to C (cubic-bezier-to).
*
* For more information of where this math came from visit:
* http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
*
* @param X1 the starting x position
* @param Y1 the starting y position
* @param RX x-radius of the arc
* @param RY y-radius of the arc
* @param angle x-axis-rotation of the arc
* @param LAF large-arc-flag of the arc
* @param SF sweep-flag of the arc
* @param X2 the ending x position
* @param Y2 the ending y position
* @param recursive the parameters needed to split arc into 2 segments
* @returns the resulting cubic-bezier segment(s)
*/
const arcToCubic = (X1, Y1, RX, RY, angle, LAF, SF, X2, Y2, recursive) => {
let x1 = X1;
let y1 = Y1;
let rx = RX;
let ry = RY;
let x2 = X2;
let y2 = Y2;
const d120 = Math.PI * 120 / 180;
const rad = Math.PI / 180 * (+angle || 0);
let res = [];
let xy;
let f1;
let f2;
let cx;
let cy;
if (!recursive) {
xy = rotateVector(x1, y1, -rad);
x1 = xy.x;
y1 = xy.y;
xy = rotateVector(x2, y2, -rad);
x2 = xy.x;
y2 = xy.y;
const x = (x1 - x2) / 2;
const y = (y1 - y2) / 2;
let h = x * x / (rx * rx) + y * y / (ry * ry);
if (h > 1) {
h = Math.sqrt(h);
rx *= h;
ry *= h;
}
const rx2 = rx * rx;
const ry2 = ry * ry;
const k = (LAF === SF ? -1 : 1) * Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
cx = k * rx * y / ry + (x1 + x2) / 2;
cy = k * -ry * x / rx + (y1 + y2) / 2;
f1 = Math.asin(((y1 - cy) / ry * 10 ** 9 >> 0) / 10 ** 9);
f2 = Math.asin(((y2 - cy) / ry * 10 ** 9 >> 0) / 10 ** 9);
f1 = x1 < cx ? Math.PI - f1 : f1;
f2 = x2 < cx ? Math.PI - f2 : f2;
if (f1 < 0) f1 = Math.PI * 2 + f1;
if (f2 < 0) f2 = Math.PI * 2 + f2;
if (SF && f1 > f2) f1 -= Math.PI * 2;
if (!SF && f2 > f1) f2 -= Math.PI * 2;
} else [f1, f2, cx, cy] = recursive;
let df = f2 - f1;
if (Math.abs(df) > d120) {
const f2old = f2;
const x2old = x2;
const y2old = y2;
f2 = f1 + d120 * (SF && f2 > f1 ? 1 : -1);
x2 = cx + rx * Math.cos(f2);
y2 = cy + ry * Math.sin(f2);
res = arcToCubic(x2, y2, rx, ry, angle, 0, SF, x2old, y2old, [
f2,
f2old,
cx,
cy
]);
}
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 m1 = [x1, y1];
const m2 = [x1 + hx * s1, y1 - hy * c1];
const m3 = [x2 + hx * s2, y2 - hy * c2];
const m4 = [x2, y2];
m2[0] = 2 * m1[0] - m2[0];
m2[1] = 2 * m1[1] - m2[1];
if (recursive) return [
m2[0],
m2[1],
m3[0],
m3[1],
m4[0],
m4[1]
].concat(res);
res = [
m2[0],
m2[1],
m3[0],
m3[1],
m4[0],
m4[1]
].concat(res);
const newres = [];
for (let i = 0, ii = res.length; i < ii; i += 1) newres[i] = i % 2 ? rotateVector(res[i - 1], res[i], rad).y : rotateVector(res[i], res[i + 1], rad).x;
return newres;
};
//#endregion
//#region src/process/quadToCubic.ts
/**
* Converts a Q (quadratic-bezier) segment to C (cubic-bezier).
*
* @param x1 curve start x
* @param y1 curve start y
* @param qx control point x
* @param qy control point y
* @param x2 curve end x
* @param y2 curve end y
* @returns the cubic-bezier segment
*/
const quadToCubic = (x1, y1, qx, qy, x2, y2) => {
const r13 = 1 / 3;
const r23 = 2 / 3;
return [
r13 * x1 + r23 * qx,
r13 * y1 + r23 * qy,
r13 * x2 + r23 * qx,
r13 * y2 + r23 * qy,
x2,
y2
];
};
//#endregion
//#region src/process/lineToCubic.ts
/**
* Converts an L (line-to) segment to C (cubic-bezier).
*
* @param x1 line start x
* @param y1 line start y
* @param x2 line end x
* @param y2 line end y
* @returns the cubic-bezier segment
*/
const lineToCubic = (x1, y1, x2, y2) => {
const c1 = midPoint([x1, y1], [x2, y2], 1 / 3);
const c2 = midPoint([x1, y1], [x2, y2], 2 / 3);
return [
c1[0],
c1[1],
c2[0],
c2[1],
x2,
y2
];
};
//#endregion
//#region src/process/segmentToCubic.ts
/**
* Converts any segment to C (cubic-bezier).
*
* @param segment the source segment
* @param params the source segment parameters
* @returns the cubic-bezier segment
*/
const segmentToCubic = (segment, params) => {
const pathCommand = segment[0];
const values = segment.slice(1).map(Number);
const [x, y] = values;
const { x1: px1, y1: py1 } = params;
if (!"TQ".includes(pathCommand)) {
params.qx = null;
params.qy = null;
}
if (pathCommand === "M") {
params.mx = x;
params.my = y;
params.x = x;
params.y = y;
return segment;
} else if (pathCommand === "A") return ["C"].concat(arcToCubic(px1, py1, values[0], values[1], values[2], values[3], values[4], values[5], values[6]));
else if (pathCommand === "Q") {
params.qx = x;
params.qy = y;
return ["C"].concat(quadToCubic(px1, py1, values[0], values[1], values[2], values[3]));
} else if (pathCommand === "L") return ["C"].concat(lineToCubic(px1, py1, x, y));
else if (pathCommand === "Z") return ["C"].concat(lineToCubic(px1, py1, params.mx, params.my));
return segment;
};
//#endregion
//#region src/process/normalizeSegment.ts
/**
* Normalizes a single segment of a `pathArray` object.
*
* @param segment the segment object
* @param params the normalization parameters
* @returns the normalized segment
*/
const normalizeSegment = (segment, params) => {
const [pathCommand] = segment;
const absCommand = pathCommand.toUpperCase();
const isRelative = pathCommand !== absCommand;
const { x1: px1, y1: py1, x2: px2, y2: py2, x, y } = params;
const values = segment.slice(1);
let absValues = values.map((n, j) => n + (isRelative ? j % 2 ? y : x : 0));
if (!"TQ".includes(absCommand)) {
params.qx = null;
params.qy = null;
}
if (absCommand === "A") {
absValues = values.slice(0, -2).concat(values[5] + (isRelative ? x : 0), values[6] + (isRelative ? y : 0));
return ["A"].concat(absValues);
} else if (absCommand === "H") return [
"L",
segment[1] + (isRelative ? x : 0),
py1
];
else if (absCommand === "V") return [
"L",
px1,
segment[1] + (isRelative ? y : 0)
];
else if (absCommand === "L") return [
"L",
segment[1] + (isRelative ? x : 0),
segment[2] + (isRelative ? y : 0)
];
else if (absCommand === "M") return [
"M",
segment[1] + (isRelative ? x : 0),
segment[2] + (isRelative ? y : 0)
];
else if (absCommand === "C") return ["C"].concat(absValues);
else if (absCommand === "S") {
const x1 = px1 * 2 - px2;
const y1 = py1 * 2 - py2;
params.x1 = x1;
params.y1 = y1;
return [
"C",
x1,
y1
].concat(absValues);
} else if (absCommand === "T") {
const qx = px1 * 2 - (params.qx ? params.qx : 0);
const qy = py1 * 2 - (params.qy ? params.qy : 0);
params.qx = qx;
params.qy = qy;
return [
"Q",
qx,
qy
].concat(absValues);
} else if (absCommand === "Q") {
const [nqx, nqy] = absValues;
params.qx = nqx;
params.qy = nqy;
return ["Q"].concat(absValues);
} else if (absCommand === "Z") return ["Z"];
return segment;
};
//#endregion
//#region src/parser/paramsParser.ts
/**
* Default parser parameters object used to track position state
* while iterating through path segments.
*/
const paramsParser = {
mx: 0,
my: 0,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
x: 0,
y: 0,
qx: null,
qy: null
};
//#endregion
//#region src/convert/pathToCurve.ts
/**
* Parses a path string or PathArray and returns a new one
* in which all segments are converted to cubic-bezier.
*
* @param pathInput - The path string or PathArray
* @returns The resulted CurveArray with all segments as cubic beziers
*
* @example
* ```ts
* pathToCurve('M10 50q15 -25 30 0')
* // => [['M', 10, 50], ['C', 25, 25, 40, 50, 40, 50]]
* ```
*/
const pathToCurve = (pathInput) => {
const params = { ...paramsParser };
const path = parsePathString(pathInput);
return iterate(path, (seg, index, lastX, lastY) => {
params.x = lastX;
params.y = lastY;
const normalSegment = normalizeSegment(seg, params);
if (normalSegment[0] === "M") {
params.mx = normalSegment[1];
params.my = normalSegment[2];
}
let result = segmentToCubic(normalSegment, params);
if (result[0] === "C" && result.length > 7) {
path.splice(index + 1, 0, ["C"].concat(result.slice(7)));
result = result.slice(0, 7);
}
const seglen = result.length;
params.x1 = +result[seglen - 2];
params.y1 = +result[seglen - 1];
params.x2 = +result[seglen - 4] || params.x1;
params.y2 = +result[seglen - 3] || params.y1;
return result;
});
};
//#endregion
//#region src/options/options.ts
/** SVGPathCommander default options */
const defaultOptions = {
origin: [
0,
0,
0
],
round: 4
};
//#endregion
//#region src/convert/pathToString.ts
/**
* Returns a valid `d` attribute string value created
* by rounding values and concatenating the PathArray segments.
*
* @param path - The PathArray object
* @param roundOption - Amount of decimals to round values to, or "off"
* @returns The concatenated path string
*
* @example
* ```ts
* pathToString([['M', 10, 10], ['L', 90, 90]], 2)
* // => 'M10 10L90 90'
* ```
*/
const pathToString = (path, roundOption) => {
const pathLen = path.length;
let { round } = defaultOptions;
let segment = path[0];
let result = "";
round = roundOption === "off" ? roundOption : typeof roundOption === "number" && roundOption >= 0 ? roundOption : typeof round === "number" && round >= 0 ? round : "off";
for (let i = 0; i < pathLen; i += 1) {
segment = path[i];
const [pathCommand] = segment;
const values = segment.slice(1);
result += pathCommand;
if (round === "off") result += values.join(" ");
else {
let j = 0;
const valLen = values.length;
while (j < valLen) {
result += roundTo(values[j], round);
if (j !== valLen - 1) result += " ";
j += 1;
}
}
}
return result;
};
//#endregion
//#region src/util/getPathBBox.ts
/**
* Calculates the bounding box of a path.
*
* @param pathInput - The path string or PathArray
* @returns An object with width, height, x, y, x2, y2, cx, cy, cz properties
*
* @example
* ```ts
* getPathBBox('M0 0L100 0L100 100L0 100Z')
* // => { x: 0, y: 0, width: 100, height: 100, x2: 100, y2: 100, cx: 50, cy: 50, cz: 150 }
* ```
*/
const getPathBBox = (pathInput) => {
if (!pathInput) return {
x: 0,
y: 0,
width: 0,
height: 0,
x2: 0,
y2: 0,
cx: 0,
cy: 0,
cz: 0
};
const path = parsePathString(pathInput);
let pathCommand = "M";
let mx = 0;
let my = 0;
const { max, min } = Math;
let xMin = Infinity;
let yMin = Infinity;
let xMax = -Infinity;
let yMax = -Infinity;
let minX = 0;
let minY = 0;
let maxX = 0;
let maxY = 0;
let paramX1 = 0;
let paramY1 = 0;
let paramX2 = 0;
let paramY2 = 0;
let paramQX = 0;
let paramQY = 0;
iterate(path, (seg, index, lastX, lastY) => {
[pathCommand] = seg;
const absCommand = pathCommand.toUpperCase();
const absoluteSegment = absCommand !== pathCommand ? absolutizeSegment(seg, index, lastX, lastY) : seg.slice(0);
const normalSegment = absCommand === "V" ? [
"L",
lastX,
absoluteSegment[1]
] : absCommand === "H" ? [
"L",
absoluteSegment[1],
lastY
] : absoluteSegment;
[pathCommand] = normalSegment;
if (!"TQ".includes(absCommand)) {
paramQX = 0;
paramQY = 0;
}
if (pathCommand === "M") {
[, mx, my] = normalSegment;
minX = mx;
minY = my;
maxX = mx;
maxY = my;
} else if (pathCommand === "L") [minX, minY, maxX, maxY] = getLineBBox(lastX, lastY, normalSegment[1], normalSegment[2]);
else if (pathCommand === "A") [minX, minY, maxX, maxY] = getArcBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6], normalSegment[7]);
else if (pathCommand === "S") {
const cp1x = paramX1 * 2 - paramX2;
const cp1y = paramY1 * 2 - paramY2;
[minX, minY, maxX, maxY] = getCubicBBox(lastX, lastY, cp1x, cp1y, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
} else if (pathCommand === "C") [minX, minY, maxX, maxY] = getCubicBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6]);
else if (pathCommand === "T") {
paramQX = paramX1 * 2 - paramQX;
paramQY = paramY1 * 2 - paramQY;
[minX, minY, maxX,