UNPKG

@remotion/paths

Version:

Utilities for working with SVG paths

226 lines (225 loc) 8.62 kB
"use strict"; // Copied from: https://github.com/rveciana/svg-path-properties Object.defineProperty(exports, "__esModule", { value: true }); exports.makeArc = void 0; const mod = (x, m) => { return ((x % m) + m) % m; }; const toRadians = (angle) => { return angle * (Math.PI / 180); }; const distance = (p0, p1) => { return Math.sqrt((p1.x - p0.x) ** 2 + (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((v0.x ** 2 + v0.y ** 2) * (v1.x ** 2 + 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; }; 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 = transformedPoint.x ** 2 / rx ** 2 + transformedPoint.y ** 2 / ry ** 2; if (radiiCheck > 1) { rx *= Math.sqrt(radiiCheck); ry *= Math.sqrt(radiiCheck); } // Step #2: Compute transformedCenter const cSquareNumerator = rx ** 2 * ry ** 2 - rx ** 2 * transformedPoint.y ** 2 - ry ** 2 * transformedPoint.x ** 2; const cSquareRootDenom = rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * 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, 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, approximationLines, }; }; const makeArc = ({ x0, y0, rx, ry, xAxisRotate, LargeArcFlag, SweepFlag, x1, y1, }) => { const lengthProperties = approximateArcLengthOfCurve(300, (t) => { return pointOnEllipticalArc({ p0: { x: x0, y: y0 }, rx, ry, xAxisRotation: xAxisRotate, largeArcFlag: LargeArcFlag, sweepFlag: SweepFlag, p1: { x: x1, y: y1 }, t, }); }); const length = lengthProperties.arcLength; const getPointAtLength = (fractionLength) => { if (fractionLength < 0) { fractionLength = 0; } else if (fractionLength > length) { fractionLength = length; } const position = pointOnEllipticalArc({ p0: { x: x0, y: y0 }, rx, ry, xAxisRotation: xAxisRotate, largeArcFlag: LargeArcFlag, sweepFlag: SweepFlag, p1: { x: x1, y: y1 }, t: fractionLength / length, }); return { x: position.x, y: position.y }; }; return { getPointAtLength, getTangentAtLength: (fractionLength) => { if (fractionLength < 0) { fractionLength = 0; } else if (fractionLength > length) { fractionLength = length; } const point_dist = 0.05; // needs testing const p1 = getPointAtLength(fractionLength); let p2; if (fractionLength < 0) { fractionLength = 0; } else if (fractionLength > length) { fractionLength = length; } if (fractionLength < length - point_dist) { p2 = getPointAtLength(fractionLength + point_dist); } else { p2 = 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 < length - point_dist) { return { x: -xDist / dist, y: -yDist / dist }; } return { x: xDist / dist, y: yDist / dist }; }, getTotalLength: () => { return length; }, type: 'arc', }; }; exports.makeArc = makeArc;