UNPKG

@liammartens/svg-path-properties

Version:

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

317 lines (279 loc) 9.18 kB
import { Properties, Point, PointProperties } from "./types"; export class Arc implements Properties { private x0: number; private y0: number; private rx: number; private ry: number; private xAxisRotate: number; private LargeArcFlag: boolean; private SweepFlag: boolean; private x1: number; private y1: number; private length: number; constructor( x0: number, y0: number, rx: number, ry: number, xAxisRotate: number, LargeArcFlag: boolean, SweepFlag: boolean, x1: number, y1: number ) { 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: number) { return pointOnEllipticalArc( { x: x0, y: y0 }, rx, ry, xAxisRotate, LargeArcFlag, SweepFlag, { x: x1, y: y1 }, t ); }); this.length = lengthProperties.arcLength; } public getTotalLength = () => { return this.length; }; public getPointAtLength = (fractionLength: number): Point => { 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 }; }; public getTangentAtLength = (fractionLength: number): Point => { 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: Point; 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 }; } }; public getPropertiesAtLength = (fractionLength: number): PointProperties => { const tangent = this.getTangentAtLength(fractionLength); const point = this.getPointAtLength(fractionLength); return { x: point.x, y: point.y, tangentX: tangent.x, tangentY: tangent.y }; }; } interface PointOnEllipticalArc { x: number; y: number; ellipticalArcAngle: number; } const pointOnEllipticalArc = ( p0: Point, rx: number, ry: number, xAxisRotation: number, largeArcFlag: boolean, sweepFlag: boolean, p1: Point, t: number ): PointOnEllipticalArc => { // 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: number, pointOnCurveFunc: (t: number) => Point ) => { // 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: number, m: number) => { return ((x % m) + m) % m; }; const toRadians = (angle: number) => { return angle * (Math.PI / 180); }; const distance = (p0: Point, p1: Point) => { return Math.sqrt(Math.pow(p1.x - p0.x, 2) + Math.pow(p1.y - p0.y, 2)); }; const clamp = (val: number, min: number, max: number) => { return Math.min(Math.max(val, min), max); }; const angleBetween = (v0: Point, v1: Point) => { 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; };