UNPKG

@liammartens/svg-path-properties

Version:

Calculate the length for an SVG path, to use it with node or a Canvas element

222 lines (221 loc) 9.12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Arc = void 0; class Arc { constructor(x0, y0, rx, ry, xAxisRotate, LargeArcFlag, SweepFlag, x1, y1) { this.getTotalLength = () => { return this.length; }; this.getPointAtLength = (fractionLength) => { if (fractionLength < 0) { fractionLength = 0; } else if (fractionLength > this.length) { fractionLength = this.length; } const position = pointOnEllipticalArc({ x: this.x0, y: this.y0 }, this.rx, this.ry, this.xAxisRotate, this.LargeArcFlag, this.SweepFlag, { x: this.x1, y: this.y1 }, fractionLength / this.length); return { x: position.x, y: position.y }; }; this.getTangentAtLength = (fractionLength) => { if (fractionLength < 0) { fractionLength = 0; } else if (fractionLength > this.length) { fractionLength = this.length; } const point_dist = 0.05; // needs testing const p1 = this.getPointAtLength(fractionLength); let p2; if (fractionLength < 0) { fractionLength = 0; } else if (fractionLength > this.length) { fractionLength = this.length; } if (fractionLength < this.length - point_dist) { p2 = this.getPointAtLength(fractionLength + point_dist); } else { p2 = this.getPointAtLength(fractionLength - point_dist); } const xDist = p2.x - p1.x; const yDist = p2.y - p1.y; const dist = Math.sqrt(xDist * xDist + yDist * yDist); if (fractionLength < this.length - point_dist) { return { x: -xDist / dist, y: -yDist / dist }; } else { return { x: xDist / dist, y: yDist / dist }; } }; this.getPropertiesAtLength = (fractionLength) => { const tangent = this.getTangentAtLength(fractionLength); const point = this.getPointAtLength(fractionLength); return { x: point.x, y: point.y, tangentX: tangent.x, tangentY: tangent.y }; }; this.x0 = x0; this.y0 = y0; this.rx = rx; this.ry = ry; this.xAxisRotate = xAxisRotate; this.LargeArcFlag = LargeArcFlag; this.SweepFlag = SweepFlag; this.x1 = x1; this.y1 = y1; const lengthProperties = approximateArcLengthOfCurve(300, function (t) { return pointOnEllipticalArc({ x: x0, y: y0 }, rx, ry, xAxisRotate, LargeArcFlag, SweepFlag, { x: x1, y: y1 }, t); }); this.length = lengthProperties.arcLength; } } exports.Arc = Arc; const pointOnEllipticalArc = (p0, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, p1, t) => { // In accordance to: http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters rx = Math.abs(rx); ry = Math.abs(ry); xAxisRotation = mod(xAxisRotation, 360); const xAxisRotationRadians = toRadians(xAxisRotation); // If the endpoints are identical, then this is equivalent to omitting the elliptical arc segment entirely. if (p0.x === p1.x && p0.y === p1.y) { return { x: p0.x, y: p0.y, ellipticalArcAngle: 0 }; // Check if angle is correct } // If rx = 0 or ry = 0 then this arc is treated as a straight line segment joining the endpoints. if (rx === 0 || ry === 0) { //return this.pointOnLine(p0, p1, t); return { x: 0, y: 0, ellipticalArcAngle: 0 }; // Check if angle is correct } // Following "Conversion from endpoint to center parameterization" // http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter // Step #1: Compute transformedPoint const dx = (p0.x - p1.x) / 2; const dy = (p0.y - p1.y) / 2; const transformedPoint = { x: Math.cos(xAxisRotationRadians) * dx + Math.sin(xAxisRotationRadians) * dy, y: -Math.sin(xAxisRotationRadians) * dx + Math.cos(xAxisRotationRadians) * dy }; // Ensure radii are large enough const radiiCheck = Math.pow(transformedPoint.x, 2) / Math.pow(rx, 2) + Math.pow(transformedPoint.y, 2) / Math.pow(ry, 2); if (radiiCheck > 1) { rx = Math.sqrt(radiiCheck) * rx; ry = Math.sqrt(radiiCheck) * ry; } // Step #2: Compute transformedCenter const cSquareNumerator = Math.pow(rx, 2) * Math.pow(ry, 2) - Math.pow(rx, 2) * Math.pow(transformedPoint.y, 2) - Math.pow(ry, 2) * Math.pow(transformedPoint.x, 2); const cSquareRootDenom = Math.pow(rx, 2) * Math.pow(transformedPoint.y, 2) + Math.pow(ry, 2) * Math.pow(transformedPoint.x, 2); let cRadicand = cSquareNumerator / cSquareRootDenom; // Make sure this never drops below zero because of precision cRadicand = cRadicand < 0 ? 0 : cRadicand; const cCoef = (largeArcFlag !== sweepFlag ? 1 : -1) * Math.sqrt(cRadicand); const transformedCenter = { x: cCoef * ((rx * transformedPoint.y) / ry), y: cCoef * (-(ry * transformedPoint.x) / rx) }; // Step #3: Compute center const center = { x: Math.cos(xAxisRotationRadians) * transformedCenter.x - Math.sin(xAxisRotationRadians) * transformedCenter.y + (p0.x + p1.x) / 2, y: Math.sin(xAxisRotationRadians) * transformedCenter.x + Math.cos(xAxisRotationRadians) * transformedCenter.y + (p0.y + p1.y) / 2 }; // Step #4: Compute start/sweep angles // Start angle of the elliptical arc prior to the stretch and rotate operations. // Difference between the start and end angles const startVector = { x: (transformedPoint.x - transformedCenter.x) / rx, y: (transformedPoint.y - transformedCenter.y) / ry }; const startAngle = angleBetween({ x: 1, y: 0 }, startVector); const endVector = { x: (-transformedPoint.x - transformedCenter.x) / rx, y: (-transformedPoint.y - transformedCenter.y) / ry }; let sweepAngle = angleBetween(startVector, endVector); if (!sweepFlag && sweepAngle > 0) { sweepAngle -= 2 * Math.PI; } else if (sweepFlag && sweepAngle < 0) { sweepAngle += 2 * Math.PI; } // We use % instead of `mod(..)` because we want it to be -360deg to 360deg(but actually in radians) sweepAngle %= 2 * Math.PI; // From http://www.w3.org/TR/SVG/implnote.html#ArcParameterizationAlternatives const angle = startAngle + sweepAngle * t; const ellipseComponentX = rx * Math.cos(angle); const ellipseComponentY = ry * Math.sin(angle); const point = { x: Math.cos(xAxisRotationRadians) * ellipseComponentX - Math.sin(xAxisRotationRadians) * ellipseComponentY + center.x, y: Math.sin(xAxisRotationRadians) * ellipseComponentX + Math.cos(xAxisRotationRadians) * ellipseComponentY + center.y, ellipticalArcStartAngle: startAngle, ellipticalArcEndAngle: startAngle + sweepAngle, ellipticalArcAngle: angle, ellipticalArcCenter: center, resultantRx: rx, resultantRy: ry }; return point; }; const approximateArcLengthOfCurve = (resolution, pointOnCurveFunc) => { // Resolution is the number of segments we use resolution = resolution ? resolution : 500; let resultantArcLength = 0; const arcLengthMap = []; const approximationLines = []; let prevPoint = pointOnCurveFunc(0); let nextPoint; for (let i = 0; i < resolution; i++) { const t = clamp(i * (1 / resolution), 0, 1); nextPoint = pointOnCurveFunc(t); resultantArcLength += distance(prevPoint, nextPoint); approximationLines.push([prevPoint, nextPoint]); arcLengthMap.push({ t: t, arcLength: resultantArcLength }); prevPoint = nextPoint; } // Last stretch to the endpoint nextPoint = pointOnCurveFunc(1); approximationLines.push([prevPoint, nextPoint]); resultantArcLength += distance(prevPoint, nextPoint); arcLengthMap.push({ t: 1, arcLength: resultantArcLength }); return { arcLength: resultantArcLength, arcLengthMap: arcLengthMap, approximationLines: approximationLines }; }; const mod = (x, m) => { return ((x % m) + m) % m; }; const toRadians = (angle) => { return angle * (Math.PI / 180); }; const distance = (p0, p1) => { return Math.sqrt(Math.pow(p1.x - p0.x, 2) + Math.pow(p1.y - p0.y, 2)); }; const clamp = (val, min, max) => { return Math.min(Math.max(val, min), max); }; const angleBetween = (v0, v1) => { const p = v0.x * v1.x + v0.y * v1.y; const n = Math.sqrt((Math.pow(v0.x, 2) + Math.pow(v0.y, 2)) * (Math.pow(v1.x, 2) + Math.pow(v1.y, 2))); const sign = v0.x * v1.y - v0.y * v1.x < 0 ? -1 : 1; const angle = sign * Math.acos(p / n); return angle; };