You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
189 lines
4.7 KiB
189 lines
4.7 KiB
2 years ago
|
// Convert an arc to a sequence of cubic bézier curves
|
||
|
//
|
||
|
'use strict';
|
||
|
|
||
|
|
||
|
var TAU = Math.PI * 2;
|
||
|
|
||
|
|
||
|
/* eslint-disable space-infix-ops */
|
||
|
|
||
|
// Calculate an angle between two unit vectors
|
||
|
//
|
||
|
// Since we measure angle between radii of circular arcs,
|
||
|
// we can use simplified math (without length normalization)
|
||
|
//
|
||
|
function unit_vector_angle(ux, uy, vx, vy) {
|
||
|
var sign = (ux * vy - uy * vx < 0) ? -1 : 1;
|
||
|
var dot = ux * vx + uy * vy;
|
||
|
|
||
|
// Add this to work with arbitrary vectors:
|
||
|
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
|
||
|
|
||
|
// rounding errors, e.g. -1.0000000000000002 can screw up this
|
||
|
if (dot > 1.0) { dot = 1.0; }
|
||
|
if (dot < -1.0) { dot = -1.0; }
|
||
|
|
||
|
return sign * Math.acos(dot);
|
||
|
}
|
||
|
|
||
|
|
||
|
// Convert from endpoint to center parameterization,
|
||
|
// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
||
|
//
|
||
|
// Return [cx, cy, theta1, delta_theta]
|
||
|
//
|
||
|
function get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi) {
|
||
|
// Step 1.
|
||
|
//
|
||
|
// Moving an ellipse so origin will be the middlepoint between our two
|
||
|
// points. After that, rotate it to line up ellipse axes with coordinate
|
||
|
// axes.
|
||
|
//
|
||
|
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
|
||
|
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
|
||
|
|
||
|
var rx_sq = rx * rx;
|
||
|
var ry_sq = ry * ry;
|
||
|
var x1p_sq = x1p * x1p;
|
||
|
var y1p_sq = y1p * y1p;
|
||
|
|
||
|
// Step 2.
|
||
|
//
|
||
|
// Compute coordinates of the centre of this ellipse (cx', cy')
|
||
|
// in the new coordinate system.
|
||
|
//
|
||
|
var radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq);
|
||
|
|
||
|
if (radicant < 0) {
|
||
|
// due to rounding errors it might be e.g. -1.3877787807814457e-17
|
||
|
radicant = 0;
|
||
|
}
|
||
|
|
||
|
radicant /= (rx_sq * y1p_sq) + (ry_sq * x1p_sq);
|
||
|
radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1);
|
||
|
|
||
|
var cxp = radicant * rx/ry * y1p;
|
||
|
var cyp = radicant * -ry/rx * x1p;
|
||
|
|
||
|
// Step 3.
|
||
|
//
|
||
|
// Transform back to get centre coordinates (cx, cy) in the original
|
||
|
// coordinate system.
|
||
|
//
|
||
|
var cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2;
|
||
|
var cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2;
|
||
|
|
||
|
// Step 4.
|
||
|
//
|
||
|
// Compute angles (theta1, delta_theta).
|
||
|
//
|
||
|
var v1x = (x1p - cxp) / rx;
|
||
|
var v1y = (y1p - cyp) / ry;
|
||
|
var v2x = (-x1p - cxp) / rx;
|
||
|
var v2y = (-y1p - cyp) / ry;
|
||
|
|
||
|
var theta1 = unit_vector_angle(1, 0, v1x, v1y);
|
||
|
var delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y);
|
||
|
|
||
|
if (fs === 0 && delta_theta > 0) {
|
||
|
delta_theta -= TAU;
|
||
|
}
|
||
|
if (fs === 1 && delta_theta < 0) {
|
||
|
delta_theta += TAU;
|
||
|
}
|
||
|
|
||
|
return [ cx, cy, theta1, delta_theta ];
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Approximate one unit arc segment with bézier curves,
|
||
|
// see http://math.stackexchange.com/questions/873224
|
||
|
//
|
||
|
function approximate_unit_arc(theta1, delta_theta) {
|
||
|
var alpha = 4/3 * Math.tan(delta_theta/4);
|
||
|
|
||
|
var x1 = Math.cos(theta1);
|
||
|
var y1 = Math.sin(theta1);
|
||
|
var x2 = Math.cos(theta1 + delta_theta);
|
||
|
var y2 = Math.sin(theta1 + delta_theta);
|
||
|
|
||
|
return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ];
|
||
|
}
|
||
|
|
||
|
module.exports = function a2c(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
|
||
|
var sin_phi = Math.sin(phi * TAU / 360);
|
||
|
var cos_phi = Math.cos(phi * TAU / 360);
|
||
|
|
||
|
// Make sure radii are valid
|
||
|
//
|
||
|
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
|
||
|
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
|
||
|
|
||
|
if (x1p === 0 && y1p === 0) {
|
||
|
// we're asked to draw line to itself
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
if (rx === 0 || ry === 0) {
|
||
|
// one of the radii is zero
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
|
||
|
// Compensate out-of-range radii
|
||
|
//
|
||
|
rx = Math.abs(rx);
|
||
|
ry = Math.abs(ry);
|
||
|
|
||
|
var lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
||
|
if (lambda > 1) {
|
||
|
rx *= Math.sqrt(lambda);
|
||
|
ry *= Math.sqrt(lambda);
|
||
|
}
|
||
|
|
||
|
|
||
|
// Get center parameters (cx, cy, theta1, delta_theta)
|
||
|
//
|
||
|
var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi);
|
||
|
|
||
|
var result = [];
|
||
|
var theta1 = cc[2];
|
||
|
var delta_theta = cc[3];
|
||
|
|
||
|
// Split an arc to multiple segments, so each segment
|
||
|
// will be less than τ/4 (= 90°)
|
||
|
//
|
||
|
var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
|
||
|
delta_theta /= segments;
|
||
|
|
||
|
for (var i = 0; i < segments; i++) {
|
||
|
result.push(approximate_unit_arc(theta1, delta_theta));
|
||
|
theta1 += delta_theta;
|
||
|
}
|
||
|
|
||
|
// We have a bezier approximation of a unit circle,
|
||
|
// now need to transform back to the original ellipse
|
||
|
//
|
||
|
return result.map(function (curve) {
|
||
|
for (var i = 0; i < curve.length; i += 2) {
|
||
|
var x = curve[i + 0];
|
||
|
var y = curve[i + 1];
|
||
|
|
||
|
// scale
|
||
|
x *= rx;
|
||
|
y *= ry;
|
||
|
|
||
|
// rotate
|
||
|
var xp = cos_phi*x - sin_phi*y;
|
||
|
var yp = sin_phi*x + cos_phi*y;
|
||
|
|
||
|
// translate
|
||
|
curve[i + 0] = xp + cc[0];
|
||
|
curve[i + 1] = yp + cc[1];
|
||
|
}
|
||
|
|
||
|
return curve;
|
||
|
});
|
||
|
};
|