@joint/core
Version:
JavaScript diagramming library
957 lines (720 loc) • 36.2 kB
JavaScript
import { Point } from './point.mjs';
import { Rect } from './rect.mjs';
import { Line } from './line.mjs';
import { Polyline } from './polyline.mjs';
import { types } from './types.mjs';
const {
abs,
sqrt,
min,
max,
pow
} = Math;
export const Curve = function(p1, p2, p3, p4) {
if (!(this instanceof Curve)) {
return new Curve(p1, p2, p3, p4);
}
if (p1 instanceof Curve) {
return new Curve(p1.start, p1.controlPoint1, p1.controlPoint2, p1.end);
}
this.start = new Point(p1);
this.controlPoint1 = new Point(p2);
this.controlPoint2 = new Point(p3);
this.end = new Point(p4);
};
// Curve passing through points.
// Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx).
// @param {array} points Array of points through which the smooth line will go.
// @return {array} curves.
Curve.throughPoints = (function() {
// Get open-ended Bezier Spline Control Points.
// @param knots Input Knot Bezier spline points (At least two points!).
// @param firstControlPoints Output First Control points. Array of knots.length - 1 length.
// @param secondControlPoints Output Second Control points. Array of knots.length - 1 length.
function getCurveControlPoints(knots) {
var firstControlPoints = [];
var secondControlPoints = [];
var n = knots.length - 1;
var i;
// Special case: Bezier curve should be a straight line.
if (n == 1) {
// 3P1 = 2P0 + P3
firstControlPoints[0] = new Point(
(2 * knots[0].x + knots[1].x) / 3,
(2 * knots[0].y + knots[1].y) / 3
);
// P2 = 2P1 – P0
secondControlPoints[0] = new Point(
2 * firstControlPoints[0].x - knots[0].x,
2 * firstControlPoints[0].y - knots[0].y
);
return [firstControlPoints, secondControlPoints];
}
// Calculate first Bezier control points.
// Right hand side vector.
var rhs = [];
// Set right hand side X values.
for (i = 1; i < n - 1; i++) {
rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x;
}
rhs[0] = knots[0].x + 2 * knots[1].x;
rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0;
// Get first control points X-values.
var x = getFirstControlPoints(rhs);
// Set right hand side Y values.
for (i = 1; i < n - 1; ++i) {
rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y;
}
rhs[0] = knots[0].y + 2 * knots[1].y;
rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0;
// Get first control points Y-values.
var y = getFirstControlPoints(rhs);
// Fill output arrays.
for (i = 0; i < n; i++) {
// First control point.
firstControlPoints.push(new Point(x[i], y[i]));
// Second control point.
if (i < n - 1) {
secondControlPoints.push(new Point(
2 * knots [i + 1].x - x[i + 1],
2 * knots[i + 1].y - y[i + 1]
));
} else {
secondControlPoints.push(new Point(
(knots[n].x + x[n - 1]) / 2,
(knots[n].y + y[n - 1]) / 2
));
}
}
return [firstControlPoints, secondControlPoints];
}
// Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points.
// @param rhs Right hand side vector.
// @return Solution vector.
function getFirstControlPoints(rhs) {
var n = rhs.length;
// `x` is a solution vector.
var x = [];
var tmp = [];
var b = 2.0;
x[0] = rhs[0] / b;
// Decomposition and forward substitution.
for (var i = 1; i < n; i++) {
tmp[i] = 1 / b;
b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
x[i] = (rhs[i] - x[i - 1]) / b;
}
for (i = 1; i < n; i++) {
// Backsubstitution.
x[n - i - 1] -= tmp[n - i] * x[n - i];
}
return x;
}
return function(points) {
if (!points || (Array.isArray(points) && points.length < 2)) {
throw new Error('At least 2 points are required');
}
var controlPoints = getCurveControlPoints(points);
var curves = [];
var n = controlPoints[0].length;
for (var i = 0; i < n; i++) {
var controlPoint1 = new Point(controlPoints[0][i].x, controlPoints[0][i].y);
var controlPoint2 = new Point(controlPoints[1][i].x, controlPoints[1][i].y);
curves.push(new Curve(points[i], controlPoint1, controlPoint2, points[i + 1]));
}
return curves;
};
})();
Curve.prototype = {
type: types.Curve,
// Returns a bbox that tightly envelops the curve.
bbox: function() {
var start = this.start;
var controlPoint1 = this.controlPoint1;
var controlPoint2 = this.controlPoint2;
var end = this.end;
var x0 = start.x;
var y0 = start.y;
var x1 = controlPoint1.x;
var y1 = controlPoint1.y;
var x2 = controlPoint2.x;
var y2 = controlPoint2.y;
var x3 = end.x;
var y3 = end.y;
var points = new Array(); // local extremes
var tvalues = new Array(); // t values of local extremes
var bounds = [new Array(), new Array()];
var a, b, c, t;
var t1, t2;
var b2ac, sqrtb2ac;
for (var i = 0; i < 2; ++i) {
if (i === 0) {
b = 6 * x0 - 12 * x1 + 6 * x2;
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
c = 3 * x1 - 3 * x0;
} else {
b = 6 * y0 - 12 * y1 + 6 * y2;
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
c = 3 * y1 - 3 * y0;
}
if (abs(a) < 1e-12) { // Numerical robustness
if (abs(b) < 1e-12) { // Numerical robustness
continue;
}
t = -c / b;
if ((0 < t) && (t < 1)) tvalues.push(t);
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = sqrt(b2ac);
if (b2ac < 0) continue;
t1 = (-b + sqrtb2ac) / (2 * a);
if ((0 < t1) && (t1 < 1)) tvalues.push(t1);
t2 = (-b - sqrtb2ac) / (2 * a);
if ((0 < t2) && (t2 < 1)) tvalues.push(t2);
}
var j = tvalues.length;
var jlen = j;
var mt;
var x, y;
while (j--) {
t = tvalues[j];
mt = 1 - t;
x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3);
bounds[0][j] = x;
y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3);
bounds[1][j] = y;
points[j] = { X: x, Y: y };
}
tvalues[jlen] = 0;
tvalues[jlen + 1] = 1;
points[jlen] = { X: x0, Y: y0 };
points[jlen + 1] = { X: x3, Y: y3 };
bounds[0][jlen] = x0;
bounds[1][jlen] = y0;
bounds[0][jlen + 1] = x3;
bounds[1][jlen + 1] = y3;
tvalues.length = jlen + 2;
bounds[0].length = jlen + 2;
bounds[1].length = jlen + 2;
points.length = jlen + 2;
var left = min.apply(null, bounds[0]);
var top = min.apply(null, bounds[1]);
var right = max.apply(null, bounds[0]);
var bottom = max.apply(null, bounds[1]);
return new Rect(left, top, (right - left), (bottom - top));
},
clone: function() {
return new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end);
},
// Returns the point on the curve closest to point `p`
closestPoint: function(p, opt) {
return this.pointAtT(this.closestPointT(p, opt));
},
closestPointLength: function(p, opt) {
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision;
var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions;
var localOpt = { precision: precision, subdivisions: subdivisions };
return this.lengthAtT(this.closestPointT(p, localOpt), localOpt);
},
closestPointNormalizedLength: function(p, opt) {
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision;
var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions;
var localOpt = { precision: precision, subdivisions: subdivisions };
var cpLength = this.closestPointLength(p, localOpt);
if (!cpLength) return 0;
var length = this.length(localOpt);
if (length === 0) return 0;
return cpLength / length;
},
// Returns `t` of the point on the curve closest to point `p`
closestPointT: function(p, opt) {
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision;
var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions;
// does not use localOpt
// identify the subdivision that contains the point:
var investigatedSubdivision;
var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced
var investigatedSubdivisionEndT;
var distFromStart; // distance of point from start of baseline
var distFromEnd; // distance of point from end of baseline
var chordLength; // distance between start and end of the subdivision
var minSumDist; // lowest observed sum of the two distances
var n = subdivisions.length;
var subdivisionSize = (n ? (1 / n) : 0);
for (var i = 0; i < n; i++) {
var currentSubdivision = subdivisions[i];
var startDist = currentSubdivision.start.distance(p);
var endDist = currentSubdivision.end.distance(p);
var sumDist = startDist + endDist;
// check that the point is closest to current subdivision and not any other
if (!minSumDist || (sumDist < minSumDist)) {
investigatedSubdivision = currentSubdivision;
investigatedSubdivisionStartT = i * subdivisionSize;
investigatedSubdivisionEndT = (i + 1) * subdivisionSize;
distFromStart = startDist;
distFromEnd = endDist;
chordLength = currentSubdivision.start.distance(currentSubdivision.end);
minSumDist = sumDist;
}
}
var precisionRatio = pow(10, -precision);
// recursively divide investigated subdivision:
// until distance between baselinePoint and closest path endpoint is within 10^(-precision)
// then return the closest endpoint of that final subdivision
while (true) {
// check if we have reached at least one required observed precision
// - calculated as: the difference in distances from point to start and end divided by the distance
// - note that this function is not monotonic = it doesn't converge stably but has "teeth"
// - the function decreases while one of the endpoints is fixed but "jumps" whenever we switch
// - this criterion works well for points lying far away from the curve
var startPrecisionRatio = (distFromStart ? (abs(distFromStart - distFromEnd) / distFromStart) : 0);
var endPrecisionRatio = (distFromEnd ? (abs(distFromStart - distFromEnd) / distFromEnd) : 0);
var hasRequiredPrecision = ((startPrecisionRatio < precisionRatio) || (endPrecisionRatio < precisionRatio));
// check if we have reached at least one required minimal distance
// - calculated as: the subdivision chord length multiplied by precisionRatio
// - calculation is relative so it will work for arbitrarily large/small curves and their subdivisions
// - this is a backup criterion that works well for points lying "almost at" the curve
var hasMinimalStartDistance = (distFromStart ? (distFromStart < (chordLength * precisionRatio)) : true);
var hasMinimalEndDistance = (distFromEnd ? (distFromEnd < (chordLength * precisionRatio)) : true);
var hasMinimalDistance = (hasMinimalStartDistance || hasMinimalEndDistance);
// do we stop now?
if (hasRequiredPrecision || hasMinimalDistance) {
return ((distFromStart <= distFromEnd) ? investigatedSubdivisionStartT : investigatedSubdivisionEndT);
}
// otherwise, set up for next iteration
var divided = investigatedSubdivision.divide(0.5);
subdivisionSize /= 2;
var startDist1 = divided[0].start.distance(p);
var endDist1 = divided[0].end.distance(p);
var sumDist1 = startDist1 + endDist1;
var startDist2 = divided[1].start.distance(p);
var endDist2 = divided[1].end.distance(p);
var sumDist2 = startDist2 + endDist2;
if (sumDist1 <= sumDist2) {
investigatedSubdivision = divided[0];
investigatedSubdivisionEndT -= subdivisionSize; // subdivisionSize was already halved
distFromStart = startDist1;
distFromEnd = endDist1;
} else {
investigatedSubdivision = divided[1];
investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved
distFromStart = startDist2;
distFromEnd = endDist2;
}
}
},
closestPointTangent: function(p, opt) {
return this.tangentAtT(this.closestPointT(p, opt));
},
// Returns `true` if the area surrounded by the curve contains the point `p`.
// Implements the even-odd algorithm (self-intersections are "outside").
// Closes open curves (always imagines a closing segment).
// Precision may be adjusted by passing an `opt` object.
containsPoint: function(p, opt) {
var polyline = this.toPolyline(opt);
return polyline.containsPoint(p);
},
// Divides the curve into two at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided.
// For a function that uses `t`, use Curve.divideAtT().
divideAt: function(ratio, opt) {
if (ratio <= 0) return this.divideAtT(0);
if (ratio >= 1) return this.divideAtT(1);
var t = this.tAt(ratio, opt);
return this.divideAtT(t);
},
// Divides the curve into two at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided.
divideAtLength: function(length, opt) {
var t = this.tAtLength(length, opt);
return this.divideAtT(t);
},
// Divides the curve into two at point defined by `t` between 0 and 1.
// Using de Casteljau's algorithm (http://math.stackexchange.com/a/317867).
// Additional resource: https://pomax.github.io/bezierinfo/#decasteljau
divideAtT: function(t) {
var start = this.start;
var controlPoint1 = this.controlPoint1;
var controlPoint2 = this.controlPoint2;
var end = this.end;
// shortcuts for `t` values that are out of range
if (t <= 0) {
return [
new Curve(start, start, start, start),
new Curve(start, controlPoint1, controlPoint2, end)
];
}
if (t >= 1) {
return [
new Curve(start, controlPoint1, controlPoint2, end),
new Curve(end, end, end, end)
];
}
var dividerPoints = this.getSkeletonPoints(t);
var startControl1 = dividerPoints.startControlPoint1;
var startControl2 = dividerPoints.startControlPoint2;
var divider = dividerPoints.divider;
var dividerControl1 = dividerPoints.dividerControlPoint1;
var dividerControl2 = dividerPoints.dividerControlPoint2;
// return array with two new curves
return [
new Curve(start, startControl1, startControl2, divider),
new Curve(divider, dividerControl1, dividerControl2, end)
];
},
// Returns the distance between the curve's start and end points.
endpointDistance: function() {
return this.start.distance(this.end);
},
// Checks whether two curves are exactly the same.
equals: function(c) {
return !!c &&
this.start.x === c.start.x &&
this.start.y === c.start.y &&
this.controlPoint1.x === c.controlPoint1.x &&
this.controlPoint1.y === c.controlPoint1.y &&
this.controlPoint2.x === c.controlPoint2.x &&
this.controlPoint2.y === c.controlPoint2.y &&
this.end.x === c.end.x &&
this.end.y === c.end.y;
},
// Returns five helper points necessary for curve division.
getSkeletonPoints: function(t) {
var start = this.start;
var control1 = this.controlPoint1;
var control2 = this.controlPoint2;
var end = this.end;
// shortcuts for `t` values that are out of range
if (t <= 0) {
return {
startControlPoint1: start.clone(),
startControlPoint2: start.clone(),
divider: start.clone(),
dividerControlPoint1: control1.clone(),
dividerControlPoint2: control2.clone()
};
}
if (t >= 1) {
return {
startControlPoint1: control1.clone(),
startControlPoint2: control2.clone(),
divider: end.clone(),
dividerControlPoint1: end.clone(),
dividerControlPoint2: end.clone()
};
}
var midpoint1 = (new Line(start, control1)).pointAt(t);
var midpoint2 = (new Line(control1, control2)).pointAt(t);
var midpoint3 = (new Line(control2, end)).pointAt(t);
var subControl1 = (new Line(midpoint1, midpoint2)).pointAt(t);
var subControl2 = (new Line(midpoint2, midpoint3)).pointAt(t);
var divider = (new Line(subControl1, subControl2)).pointAt(t);
var output = {
startControlPoint1: midpoint1,
startControlPoint2: subControl1,
divider: divider,
dividerControlPoint1: subControl2,
dividerControlPoint2: midpoint3
};
return output;
},
// Returns a list of curves whose flattened length is better than `opt.precision`.
// That is, observed difference in length between recursions is less than 10^(-3) = 0.001 = 0.1%
// (Observed difference is not real precision, but close enough as long as special cases are covered)
// As a rule of thumb, increasing `precision` by 1 requires 2 more iterations (= levels of division operations)
// - Precision 0 (endpointDistance) - 0 iterations => total of 2^0 - 1 = 0 operations (1 subdivision)
// - Precision 1 (<10% error) - 2 iterations => total of 2^2 - 1 = 3 operations (4 subdivisions)
// - Precision 2 (<1% error) - 4 iterations => total of 2^4 - 1 = 15 operations requires 4 division operations on all elements (15 operations total) (16 subdivisions)
// - Precision 3 (<0.1% error) - 6 iterations => total of 2^6 - 1 = 63 operations - acceptable when drawing (64 subdivisions)
// - Precision 4 (<0.01% error) - 8 iterations => total of 2^8 - 1 = 255 operations - high resolution, can be used to interpolate `t` (256 subdivisions)
// (Variation of 1 recursion worse or better is possible depending on the curve, doubling/halving the number of operations accordingly)
getSubdivisions: function(opt) {
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision;
// not using opt.subdivisions
// not using localOpt
var start = this.start;
var control1 = this.controlPoint1;
var control2 = this.controlPoint2;
var end = this.end;
var subdivisions = [new Curve(start, control1, control2, end)];
if (precision === 0) return subdivisions;
// special case #1: point-like curves
// - no need to calculate subdivisions, they would all be identical
var isPoint = !this.isDifferentiable();
if (isPoint) return subdivisions;
var previousLength = this.endpointDistance();
var precisionRatio = pow(10, -precision);
// special case #2: sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1
// - not a problem for further iterations because cubic curves cannot have more than two local extrema
// - (i.e. cubic curves cannot intersect the baseline more than once)
// - therefore starting from iteration = 2 ensures that subsequent iterations do not produce sampling with equal length
// - (unless it's a straight-line curve, see below)
var minIterations = 2; // = 2*1
// special case #3: straight-line curves have the same observed length in all iterations
// - this causes observed precision ratio to always be 0 (= lower than `precisionRatio`, which is our exit condition)
// - we enforce the expected number of iterations = 2 * precision
var isLine = ((control1.cross(start, end) === 0) && (control2.cross(start, end) === 0));
if (isLine) {
minIterations = (2 * precision);
}
// recursively divide curve at `t = 0.5`
// until we reach `minIterations`
// and until the difference between observed length at subsequent iterations is lower than `precision`
var iteration = 0;
while (true) {
iteration += 1;
// divide all subdivisions
var newSubdivisions = [];
var numSubdivisions = subdivisions.length;
for (var i = 0; i < numSubdivisions; i++) {
var currentSubdivision = subdivisions[i];
var divided = currentSubdivision.divide(0.5); // dividing at t = 0.5 (not at middle length!)
newSubdivisions.push(divided[0], divided[1]);
}
// measure new length
var length = 0;
var numNewSubdivisions = newSubdivisions.length;
for (var j = 0; j < numNewSubdivisions; j++) {
var currentNewSubdivision = newSubdivisions[j];
length += currentNewSubdivision.endpointDistance();
}
// check if we have reached minimum number of iterations
if (iteration >= minIterations) {
// check if we have reached required observed precision
var observedPrecisionRatio = ((length !== 0) ? ((length - previousLength) / length) : 0);
if (observedPrecisionRatio < precisionRatio) {
return newSubdivisions;
}
}
// otherwise, set up for next iteration
subdivisions = newSubdivisions;
previousLength = length;
}
},
isDifferentiable: function() {
var start = this.start;
var control1 = this.controlPoint1;
var control2 = this.controlPoint2;
var end = this.end;
return !(start.equals(control1) && control1.equals(control2) && control2.equals(end));
},
// Returns flattened length of the curve with precision better than `opt.precision`; or using `opt.subdivisions` provided.
length: function(opt) {
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call
var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions;
// not using localOpt
var length = 0;
var n = subdivisions.length;
for (var i = 0; i < n; i++) {
var currentSubdivision = subdivisions[i];
length += currentSubdivision.endpointDistance();
}
return length;
},
// Returns distance along the curve up to `t` with precision better than requested `opt.precision`. (Not using `opt.subdivisions`.)
lengthAtT: function(t, opt) {
if (t <= 0) return 0;
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision;
// not using opt.subdivisions
// not using localOpt
var subCurve = this.divide(t)[0];
var subCurveLength = subCurve.length({ precision: precision });
return subCurveLength;
},
// Returns point at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided.
// Mirrors Line.pointAt() function.
// For a function that tracks `t`, use Curve.pointAtT().
pointAt: function(ratio, opt) {
if (ratio <= 0) return this.start.clone();
if (ratio >= 1) return this.end.clone();
var t = this.tAt(ratio, opt);
return this.pointAtT(t);
},
// Returns point at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided.
pointAtLength: function(length, opt) {
var t = this.tAtLength(length, opt);
return this.pointAtT(t);
},
// Returns the point at provided `t` between 0 and 1.
// `t` does not track distance along curve as it does in Line objects.
// Non-linear relationship, speeds up and slows down as curve warps!
// For linear length-based solution, use Curve.pointAt().
pointAtT: function(t) {
if (t <= 0) return this.start.clone();
if (t >= 1) return this.end.clone();
return this.getSkeletonPoints(t).divider;
},
// Default precision
PRECISION: 3,
round: function(precision) {
this.start.round(precision);
this.controlPoint1.round(precision);
this.controlPoint2.round(precision);
this.end.round(precision);
return this;
},
scale: function(sx, sy, origin) {
this.start.scale(sx, sy, origin);
this.controlPoint1.scale(sx, sy, origin);
this.controlPoint2.scale(sx, sy, origin);
this.end.scale(sx, sy, origin);
return this;
},
// Returns a tangent line at requested `ratio` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided.
tangentAt: function(ratio, opt) {
if (!this.isDifferentiable()) return null;
if (ratio < 0) ratio = 0;
else if (ratio > 1) ratio = 1;
var t = this.tAt(ratio, opt);
return this.tangentAtT(t);
},
// Returns a tangent line at requested `length` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided.
tangentAtLength: function(length, opt) {
if (!this.isDifferentiable()) return null;
var t = this.tAtLength(length, opt);
return this.tangentAtT(t);
},
// Returns a tangent line at requested `t`.
tangentAtT: function(t) {
if (!this.isDifferentiable()) return null;
if (t < 0) t = 0;
else if (t > 1) t = 1;
var skeletonPoints = this.getSkeletonPoints(t);
var p1 = skeletonPoints.startControlPoint2;
var p2 = skeletonPoints.dividerControlPoint1;
var tangentStart = skeletonPoints.divider;
var tangentLine = new Line(p1, p2);
tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y); // move so that tangent line starts at the point requested
return tangentLine;
},
// Returns `t` at requested `ratio` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided.
tAt: function(ratio, opt) {
if (ratio <= 0) return 0;
if (ratio >= 1) return 1;
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision;
var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions;
var localOpt = { precision: precision, subdivisions: subdivisions };
var curveLength = this.length(localOpt);
var length = curveLength * ratio;
return this.tAtLength(length, localOpt);
},
// Returns `t` at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided.
// Uses `precision` to approximate length within `precision` (always underestimates)
// Then uses a binary search to find the `t` of a subdivision endpoint that is close (within `precision`) to the `length`, if the curve was as long as approximated
// As a rule of thumb, increasing `precision` by 1 causes the algorithm to go 2^(precision - 1) deeper
// - Precision 0 (chooses one of the two endpoints) - 0 levels
// - Precision 1 (chooses one of 5 points, <10% error) - 1 level
// - Precision 2 (<1% error) - 3 levels
// - Precision 3 (<0.1% error) - 7 levels
// - Precision 4 (<0.01% error) - 15 levels
tAtLength: function(length, opt) {
var fromStart = true;
if (length < 0) {
fromStart = false; // negative lengths mean start calculation from end point
length = -length; // absolute value
}
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision;
var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions;
var localOpt = { precision: precision, subdivisions: subdivisions };
// identify the subdivision that contains the point at requested `length`:
var investigatedSubdivision;
var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced
var investigatedSubdivisionEndT;
//var baseline; // straightened version of subdivision to investigate
//var baselinePoint; // point on the baseline that is the requested distance away from start
var baselinePointDistFromStart; // distance of baselinePoint from start of baseline
var baselinePointDistFromEnd; // distance of baselinePoint from end of baseline
var l = 0; // length so far
var n = subdivisions.length;
var subdivisionSize = 1 / n;
for (var i = 0; i < n; i++) {
var index = (fromStart ? i : (n - 1 - i));
var currentSubdivision = subdivisions[i];
var d = currentSubdivision.endpointDistance(); // length of current subdivision
if (length <= (l + d)) {
investigatedSubdivision = currentSubdivision;
investigatedSubdivisionStartT = index * subdivisionSize;
investigatedSubdivisionEndT = (index + 1) * subdivisionSize;
baselinePointDistFromStart = (fromStart ? (length - l) : ((d + l) - length));
baselinePointDistFromEnd = (fromStart ? ((d + l) - length) : (length - l));
break;
}
l += d;
}
if (!investigatedSubdivision) return (fromStart ? 1 : 0); // length requested is out of range - return maximum t
// note that precision affects what length is recorded
// (imprecise measurements underestimate length by up to 10^(-precision) of the precise length)
// e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1
var curveLength = this.length(localOpt);
var precisionRatio = pow(10, -precision);
// recursively divide investigated subdivision:
// until distance between baselinePoint and closest path endpoint is within 10^(-precision)
// then return the closest endpoint of that final subdivision
while (true) {
// check if we have reached required observed precision
var observedPrecisionRatio;
observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromStart / curveLength) : 0);
if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionStartT;
observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromEnd / curveLength) : 0);
if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionEndT;
// otherwise, set up for next iteration
var newBaselinePointDistFromStart;
var newBaselinePointDistFromEnd;
var divided = investigatedSubdivision.divide(0.5);
subdivisionSize /= 2;
var baseline1Length = divided[0].endpointDistance();
var baseline2Length = divided[1].endpointDistance();
if (baselinePointDistFromStart <= baseline1Length) { // point at requested length is inside divided[0]
investigatedSubdivision = divided[0];
investigatedSubdivisionEndT -= subdivisionSize; // sudivisionSize was already halved
newBaselinePointDistFromStart = baselinePointDistFromStart;
newBaselinePointDistFromEnd = baseline1Length - newBaselinePointDistFromStart;
} else { // point at requested length is inside divided[1]
investigatedSubdivision = divided[1];
investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved
newBaselinePointDistFromStart = baselinePointDistFromStart - baseline1Length;
newBaselinePointDistFromEnd = baseline2Length - newBaselinePointDistFromStart;
}
baselinePointDistFromStart = newBaselinePointDistFromStart;
baselinePointDistFromEnd = newBaselinePointDistFromEnd;
}
},
// Returns an array of points that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided.
// Flattened length is no more than 10^(-precision) away from real curve length.
toPoints: function(opt) {
opt = opt || {};
var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call
var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions;
// not using localOpt
var points = [subdivisions[0].start.clone()];
var n = subdivisions.length;
for (var i = 0; i < n; i++) {
var currentSubdivision = subdivisions[i];
points.push(currentSubdivision.end.clone());
}
return points;
},
// Returns a polyline that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided.
// Flattened length is no more than 10^(-precision) away from real curve length.
toPolyline: function(opt) {
return new Polyline(this.toPoints(opt));
},
toString: function() {
return this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end;
},
translate: function(tx, ty) {
this.start.translate(tx, ty);
this.controlPoint1.translate(tx, ty);
this.controlPoint2.translate(tx, ty);
this.end.translate(tx, ty);
return this;
}
};
Curve.prototype.divide = Curve.prototype.divideAtT;