UNPKG

svg-path-commander

Version:

🛹 Modern TypeScript tools for SVG

1,947 lines (1,946 loc) • 119 kB
/*! * 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,