UNPKG

svg-path-d

Version:

SVG path data (path[d] attribute content) manipulation library.

196 lines 8.64 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getEllipticalArcBoundingRect = exports.approximateEllipticalArc = exports.ellipticalArcToCurve = exports.getEllipseTangent = exports.getEllipsePoint = exports.getCenterParams = void 0; // tslint:disable: variable-name var math2d_1 = require("./utils/math2d"); var path_node_1 = require("./path-node"); // Specification: https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter function getCenterParams(node) { // Given the following variables: // x1 y1 x2 y2 fA fS rx ry phi var x1 = path_node_1.getX(node.prev); var y1 = path_node_1.getY(node.prev); var x2 = node.x; var y2 = node.y; var fA = node.largeArcFlag; var fB = node.sweepFlag; var phi = (node.angle * Math.PI) / 180; // Correction Step 1: Ensure radii are positive var rx = Math.abs(node.rx); var ry = Math.abs(node.ry); // Correction Step 2: Ensure radii are non-zero // If rx = 0 or ry = 0, then treat this as a straight line from (x1, y1) to (x2, y2) and stop. if (rx === 0 || ry === 0) { return { cx: (x1 + x2) / 2, cy: (y1 + y2) / 2, rx: rx, ry: ry, phi: phi, theta: 0, deltaTheta: 0 }; } // Step 1: Compute (x1′, y1′) var cosPhi = Math.cos(phi); var sinPhi = Math.sin(phi); var dx = (x1 - x2) / 2; var dy = (y1 - y2) / 2; var x1_ = cosPhi * dx + sinPhi * dy; var y1_ = -sinPhi * dx + cosPhi * dy; // Correction Step 3: Ensure radii are large enough var L = (x1_ * x1_ * ry * ry + y1_ * y1_ * rx * rx) / (rx * rx * ry * ry); if (L > 1) { // Scale up rx *= Math.sqrt(L); ry *= Math.sqrt(L); } // Step 2: Compute (cx′, cy′) var M = (fA === fB ? -1 : 1) * Math.sqrt(Math.max(rx * rx * ry * ry - rx * rx * y1_ * y1_ - ry * ry * x1_ * x1_, 0) / (rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_)); var cx_ = (M * rx * y1_) / ry; var cy_ = (M * -ry * x1_) / rx; // Step 3: Compute (cx, cy) from (cx′, cy′) var cx = cosPhi * cx_ - sinPhi * cy_ || 0; var cy = sinPhi * cx_ + cosPhi * cy_ || 0; // Step 4: Compute theta and deltaTheta var ux = (x1_ - cx_) / rx; var uy = (y1_ - cy_) / ry; var vx = -(x1_ + cx_) / rx; var vy = -(y1_ + cy_) / ry; var theta = math2d_1.twoVectorsAngle(1, 0, ux, uy) || 0; var deltaTheta = math2d_1.twoVectorsAngle(ux, uy, vx, vy) || 0; if (fB) { // deltaTheta should be >= 0 if (deltaTheta < 0) { deltaTheta += 2 * Math.PI; } } else { // deltaTheta should be <= 0 if (deltaTheta > 0) { deltaTheta -= 2 * Math.PI; } } return { cx: cx + (x1 + x2) / 2, cy: cy + (y1 + y2) / 2, rx: rx, ry: ry, phi: phi, theta: theta, deltaTheta: deltaTheta }; } exports.getCenterParams = getCenterParams; function getEllipsePoint(ellipse, theta) { // An arbitrary point (x, y) on the elliptical arc can be described by the 2-dimensional matrix equation // https://www.w3.org/TR/SVG/implnote.html#ArcParameterizationAlternatives var cosPhi = Math.cos(ellipse.phi); var sinPhi = Math.sin(ellipse.phi); var x1 = ellipse.rx * Math.cos(theta); var y1 = ellipse.ry * Math.sin(theta); var x1_ = cosPhi * x1 - sinPhi * y1; var y1_ = sinPhi * x1 + cosPhi * y1; var x = x1_ + ellipse.cx; var y = y1_ + ellipse.cy; return { x: x, y: y }; } exports.getEllipsePoint = getEllipsePoint; // Derivative of // cos'(theta) = -sin(theta) // sin'(theta) = cos(theta) // // x'(theta) = cosPhi * rx * cos'(theta) - sinPhi * ry * sin'(theta); // y'(theta) = sinPhi * rx * cos'(theta) + cosPhi * ry * sin'(theta); // // x'(theta) = -cosPhi * rx * Math.sin(theta) - sinPhi * ry * Math.cos(theta); // y'(theta) = -sinPhi * rx * Math.sin(theta) + cosPhi * ry * Math.cos(theta); function getEllipseTangent(ellipse, theta) { var cosPhi = Math.cos(ellipse.phi); var sinPhi = Math.sin(ellipse.phi); var dx1 = -ellipse.rx * Math.sin(theta); var dy1 = ellipse.ry * Math.cos(theta); var x = cosPhi * dx1 - sinPhi * dy1; var y = sinPhi * dx1 + cosPhi * dy1; return { x: x, y: y }; } exports.getEllipseTangent = getEllipseTangent; function ellipticalArcToCurve(x, y, ellipse, theta1, theta2, prev) { var t1 = getEllipseTangent(ellipse, theta1); var t2 = getEllipseTangent(ellipse, theta2); var t = (4 * Math.tan((theta2 - theta1) / 4)) / 3; var x1 = path_node_1.getX(prev) + t * t1.x; var y1 = path_node_1.getY(prev) + t * t1.y; var x2 = x - t * t2.x; var y2 = y - t * t2.y; return { name: 'C', x1: x1, y1: y1, x2: x2, y2: y2, x: x, y: y, prev: prev }; } exports.ellipticalArcToCurve = ellipticalArcToCurve; function approximateEllipticalArc(node) { var ellipse = getCenterParams(node); if (ellipse.rx <= 0 || ellipse.ry <= 0 || !ellipse.deltaTheta) { // Treat this as a straight line and stop. return [{ name: 'L', x: node.x, y: node.y }]; } // Determine the number of curves to use in the approximation. var theta = ellipse.theta, deltaTheta = ellipse.deltaTheta; if (Math.abs(deltaTheta) > (4 * Math.PI) / 3) { // Three-part split. var theta1 = theta + deltaTheta / 3; var theta2 = theta + (2 * deltaTheta) / 3; var theta3 = theta + deltaTheta; var p1 = getEllipsePoint(ellipse, theta1); var p2 = getEllipsePoint(ellipse, theta2); var c1 = ellipticalArcToCurve(p1.x, p1.y, ellipse, theta, theta1, node.prev); var c2 = ellipticalArcToCurve(p2.x, p2.y, ellipse, theta1, theta2, c1); var c3 = ellipticalArcToCurve(node.x, node.y, ellipse, theta2, theta3, c2); return [c1, c2, c3]; } else if (Math.abs(deltaTheta) > (2 * Math.PI) / 3) { // Two-part split. var theta1 = theta + deltaTheta / 2; var theta2 = theta + deltaTheta; var p1 = getEllipsePoint(ellipse, theta1); var c1 = ellipticalArcToCurve(p1.x, p1.y, ellipse, theta, theta1, node.prev); var c2 = ellipticalArcToCurve(node.x, node.y, ellipse, theta1, theta2, c1); return [c1, c2]; } else { return [ellipticalArcToCurve(node.x, node.y, ellipse, theta, theta + deltaTheta, node.prev)]; } } exports.approximateEllipticalArc = approximateEllipticalArc; function getEllipticalArcBoundingRect(node) { var rc = math2d_1.fromPoint(node.x, node.y); math2d_1.addPoint(rc, path_node_1.getX(node.prev), path_node_1.getY(node.prev)); var cp = getCenterParams(node); if (cp.rx === 0 || cp.ry === 0 || cp.deltaTheta === 0) { // It's a straight line. return rc; } var thetaMin = cp.theta + Math.min(cp.deltaTheta, 0); var thetaMax = cp.theta + Math.max(cp.deltaTheta, 0); // Compute extremes using parametric description of ellipse: // x(theta) = cx + rx * cos(theta) * cos(phi) - ry * sin(theta) * sin(phi) // y(theta) = cy + rx * cos(theta) * sin(phi) + ry * sin(theta) * cos(phi) // To compute the bounding box of the whole ellipse we need to find for which value of theta the above mentioned // functions reach the local extremes. It means where the first derivatives of x and y according to theta are zero. // We will get this two equations: // 0 = -rx * sin(theta) * cos(phi) - ry * cos(theta) * sin(phi) // 0 = -rx * sin(theta) * sin(phi) - ry * cos(theta) * cos(phi) // which give the solution for x: // theta = -atan(ry * tan(phi) / rx) + PI * n // and for y: // theta = atan(ry / (tan(phi) * rx)) + PI * n var thetaX = -Math.atan2(cp.ry * Math.tan(cp.phi), cp.rx); // rolling back for (; thetaX > thetaMin; thetaX -= Math.PI) ; // testing for (; thetaX < thetaMax; thetaX += Math.PI) { if (thetaX > thetaMin) { var point = getEllipsePoint(cp, thetaX); math2d_1.addPoint(rc, point.x, point.y); } } var thetaY = Math.atan2(cp.ry, Math.tan(cp.phi) * cp.rx); // rolling back for (; thetaY > thetaMin; thetaY -= Math.PI) ; // testing for (; thetaY < thetaMax; thetaY += Math.PI) { if (thetaY > thetaMin) { var point = getEllipsePoint(cp, thetaY); math2d_1.addPoint(rc, point.x, point.y); } } return rc; } exports.getEllipticalArcBoundingRect = getEllipticalArcBoundingRect; //# sourceMappingURL=arc-node.js.map