diff --git a/src/path/Curve.js b/src/path/Curve.js index c76fe6b6..9738eb50 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -302,7 +302,12 @@ var Curve = this.Curve = Base.extend({ } // Amount of integral evaluations - var numEval = 16; + function getIterations(a, b) { + // Guess required precision based and size of range... + // TODO: There should be much better educated guesses for + // this. Also, what does this depend on? Required precision? + return Math.max(2, Math.min(16, Math.ceil(Math.abs(b - a) * 32))); + } return { getPoint: function(parameter) { @@ -331,7 +336,7 @@ var Curve = this.Curve = Base.extend({ } var ds = getLengthIntegrand( p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y); - return Numerical.integrate(ds, a, b, numEval); + return Numerical.integrate(ds, a, b, getIterations(a, b)); }, getParameter: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, @@ -347,39 +352,39 @@ var Curve = this.Curve = Base.extend({ return Math.max(Math.min(start + length / Math.sqrt(dx * dx + dy * dy), 0, 1)); } - // Let's use the Van Wijngaarden–Dekker–Brent Method to find - // solutions more reliably than with False Position Method. - // The precision of 5 iterations seems enough for this + // See if we're going forward or backward, and handle cases + // differently var forward = length > 0, + a = forward ? start : 0, + b = forward ? 1 : start, + length = Math.abs(length), // Use integrand to calculate both range length and part // lengths in f(t) below. ds = getLengthIntegrand( p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y), - a, b, f; - // See if we're going forward or backward, and handle cases - // differently - if (forward) { // Normal way - a = start; - b = 1; - // We're moving b to the right to find root for length - f = function(t) { - return Numerical.integrate(ds, a, t, numEval) - length; - } - } else { // Going backwards - a = 0; - b = start; - length = -length; - // We're moving a to the left to find root for length - f = function(t) { - return Numerical.integrate(ds, t, b, numEval) - length; - } - } - var rangeLength = Numerical.integrate(ds, a, b, numEval); + // Get length of total range + rangeLength = Numerical.integrate(ds, a, b, + getIterations(a, b)), + // Use length / rangeLength for an initial guess for t, to + // bring us closer: + guess = length / rangeLength, + len = 0; if (length >= rangeLength) return forward ? b : a; - // Use length / rangeLength for an initial guess for t, to - // bring us closer: - var guess = length / rangeLength; + // Iteratively calculate curve range lengths, and add them up, + // using integration precision depending on the size of the + // range. This is much faster and also more precise than not + // modifing start and calculating total length each time. + function f(t) { + var count = getIterations(start, t); + if (start < t) { + len += Numerical.integrate(ds, start, t, count); + } else { + len -= Numerical.integrate(ds, t, start, count); + } + start = t; + return len - length; + } return Numerical.findRootNewton(f, ds, forward ? a : b - guess, // a forward ? a + guess : b, // b diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 179b1a83..4591c943 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -74,13 +74,20 @@ var Numerical = new function() { }, /** - * Newton-Raphson Method Using Derivative + * Newton-Raphson Method Using Derivative. This is a special version + * that clips results to 0 .. 1, as required by Paper.js iterative + * approach for curve time parametrization: Ending up far outside + * the bezier curve boundaries resulted in inprecision of added up + * curve lengths. */ findRootNewton: function(f, fd, a, b, n, tol) { var x = 0.5 * (a + b); for (var i = 0; i < n; i++) { var dx = f(x) / fd(x); x -= dx; + // Clip to 0 .. t .. 1. See comment above + if (x < 0) x = 0; + else if (x > 1) x = 1; if (Math.abs(dx) < tol) return x; }