svg-path-d
Version:
SVG path data (path[d] attribute content) manipulation library.
196 lines • 8.64 kB
JavaScript
"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