2011-06-05 14:27:18 -04:00
|
|
|
/*
|
|
|
|
* Paper.js
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-05 14:27:18 -04:00
|
|
|
* This file is part of Paper.js, a JavaScript Vector Graphics Library,
|
|
|
|
* based on Scriptographer.org and designed to be largely API compatible.
|
|
|
|
* http://paperjs.org/
|
|
|
|
* http://scriptographer.org/
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-05 14:27:18 -04:00
|
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-05 14:27:18 -04:00
|
|
|
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
|
|
|
|
* http://lehni.org/ & http://jonathanpuckey.com/
|
2011-06-30 06:01:51 -04:00
|
|
|
*
|
2011-06-05 14:27:18 -04:00
|
|
|
* All rights reserved.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// An Algorithm for Automatically Fitting Digitized Curves
|
|
|
|
// by Philip J. Schneider
|
|
|
|
// from "Graphics Gems", Academic Press, 1990
|
|
|
|
var PathFitter = Base.extend({
|
|
|
|
initialize: function(path, error) {
|
|
|
|
this.points = [];
|
2011-06-05 16:25:38 -04:00
|
|
|
var segments = path._segments,
|
|
|
|
prev;
|
2011-06-05 18:14:36 -04:00
|
|
|
// Copy over points from path and filter out adjacent duplicates.
|
2011-06-05 16:25:38 -04:00
|
|
|
for (var i = 0, l = segments.length; i < l; i++) {
|
|
|
|
var point = segments[i].point.clone();
|
|
|
|
if (!prev || !prev.equals(point)) {
|
2011-06-12 13:40:24 -04:00
|
|
|
this.points.push(point);
|
2011-06-05 16:25:38 -04:00
|
|
|
prev = point;
|
|
|
|
}
|
|
|
|
}
|
2011-06-05 14:27:18 -04:00
|
|
|
this.error = error;
|
|
|
|
},
|
|
|
|
|
2011-06-05 16:26:01 -04:00
|
|
|
fit: function() {
|
2011-06-05 14:27:18 -04:00
|
|
|
this.segments = [new Segment(this.points[0])];
|
|
|
|
this.fitCubic(0, this.points.length - 1,
|
|
|
|
// Left Tangent
|
|
|
|
this.points[1].subtract(this.points[0]).normalize(),
|
|
|
|
// Right Tangent
|
|
|
|
this.points[this.points.length - 2].subtract(
|
|
|
|
this.points[this.points.length - 1]).normalize());
|
|
|
|
return this.segments;
|
|
|
|
},
|
|
|
|
|
|
|
|
// Fit a Bezier curve to a (sub)set of digitized points
|
2011-06-05 17:06:24 -04:00
|
|
|
fitCubic: function(first, last, tan1, tan2) {
|
2011-06-05 14:27:18 -04:00
|
|
|
// Use heuristic if region only has two points in it
|
|
|
|
if (last - first == 1) {
|
|
|
|
var pt1 = this.points[first],
|
|
|
|
pt2 = this.points[last],
|
|
|
|
dist = pt1.getDistance(pt2) / 3;
|
2011-06-05 17:06:24 -04:00
|
|
|
this.addCurve([pt1, pt1.add(tan1.normalize(dist)),
|
|
|
|
pt2.add(tan2.normalize(dist)), pt2]);
|
2011-06-05 14:27:18 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Parameterize points, and attempt to fit curve
|
|
|
|
var uPrime = this.chordLengthParameterize(first, last),
|
2011-06-05 18:14:28 -04:00
|
|
|
maxError = Math.max(this.error, this.error * this.error),
|
2011-06-05 14:27:18 -04:00
|
|
|
error,
|
|
|
|
split;
|
2011-06-05 16:31:58 -04:00
|
|
|
// Try 4 iterations
|
2011-06-05 18:14:28 -04:00
|
|
|
for (var i = 0; i <= 4; i++) {
|
2011-06-05 17:06:24 -04:00
|
|
|
var curve = this.generateBezier(first, last, uPrime, tan1, tan2);
|
2011-06-05 14:27:18 -04:00
|
|
|
// Find max deviation of points to fitted curve
|
2011-06-05 17:06:24 -04:00
|
|
|
var max = this.findMaxError(first, last, curve, uPrime);
|
2011-06-05 18:14:28 -04:00
|
|
|
if (max.error < this.error) {
|
2011-06-05 17:06:24 -04:00
|
|
|
this.addCurve(curve);
|
2011-06-05 14:27:18 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
split = max.index;
|
2011-06-05 18:14:28 -04:00
|
|
|
// If error not too large, try reparameterization and iteration
|
|
|
|
if (max.error >= maxError)
|
2011-06-05 14:27:18 -04:00
|
|
|
break;
|
2011-06-05 18:14:28 -04:00
|
|
|
this.reparameterize(first, last, uPrime, curve);
|
|
|
|
maxError = max.error;
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
// Fitting failed -- split at max error point and fit recursively
|
|
|
|
var V1 = this.points[split - 1].subtract(this.points[split]),
|
|
|
|
V2 = this.points[split].subtract(this.points[split + 1]),
|
2011-06-05 17:06:24 -04:00
|
|
|
tanCenter = V1.add(V2).divide(2).normalize();
|
|
|
|
this.fitCubic(first, split, tan1, tanCenter);
|
|
|
|
this.fitCubic(split, last, tanCenter.negate(), tan2);
|
2011-06-05 14:27:18 -04:00
|
|
|
},
|
|
|
|
|
2011-06-05 17:06:24 -04:00
|
|
|
addCurve: function(curve) {
|
2011-06-05 14:27:18 -04:00
|
|
|
var prev = this.segments[this.segments.length - 1];
|
2011-06-05 17:06:24 -04:00
|
|
|
prev.setHandleOut(curve[1].subtract(curve[0]));
|
2011-06-05 14:27:18 -04:00
|
|
|
this.segments.push(
|
2011-06-05 17:06:24 -04:00
|
|
|
new Segment(curve[3], curve[2].subtract(curve[3])));
|
2011-06-05 14:27:18 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Use least-squares method to find Bezier control points for region.
|
2011-06-05 17:06:24 -04:00
|
|
|
generateBezier: function(first, last, uPrime, tan1, tan2) {
|
2011-06-05 17:09:19 -04:00
|
|
|
var epsilon = Numerical.TOLERANCE,
|
2011-06-05 14:27:18 -04:00
|
|
|
pt1 = this.points[first],
|
2011-06-05 17:09:19 -04:00
|
|
|
pt2 = this.points[last],
|
|
|
|
// Create the C and X matrices
|
|
|
|
C = [[0, 0], [0, 0]],
|
2011-06-05 14:27:18 -04:00
|
|
|
X = [0, 0];
|
|
|
|
|
2011-06-05 17:09:19 -04:00
|
|
|
for (var i = 0, l = last - first + 1; i < l; i++) {
|
2011-06-05 14:27:18 -04:00
|
|
|
var u = uPrime[i],
|
|
|
|
t = 1 - u,
|
|
|
|
b = 3 * u * t,
|
2011-06-05 16:44:34 -04:00
|
|
|
b0 = t * t * t,
|
|
|
|
b1 = b * t,
|
|
|
|
b2 = b * u,
|
|
|
|
b3 = u * u * u,
|
2011-06-05 17:06:24 -04:00
|
|
|
a1 = tan1.normalize(b1),
|
|
|
|
a2 = tan2.normalize(b2),
|
2011-06-05 14:27:18 -04:00
|
|
|
tmp = this.points[first + i]
|
2011-06-05 16:44:34 -04:00
|
|
|
.subtract(pt1.multiply(b0 + b1))
|
|
|
|
.subtract(pt2.multiply(b2 + b3));
|
|
|
|
C[0][0] += a1.dot(a1);
|
|
|
|
C[0][1] += a1.dot(a2);
|
|
|
|
// C[1][0] += a1.dot(a2);
|
|
|
|
C[1][0] = C[0][1];
|
|
|
|
C[1][1] += a2.dot(a2);
|
2011-06-05 16:51:01 -04:00
|
|
|
X[0] += a1.dot(tmp);
|
|
|
|
X[1] += a2.dot(tmp);
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Compute the determinants of C and X
|
2011-06-05 17:09:19 -04:00
|
|
|
var detC0C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1],
|
|
|
|
alpha1, alpha2;
|
|
|
|
if (Math.abs(detC0C1) > epsilon) {
|
2011-06-05 16:45:29 -04:00
|
|
|
// Kramer's rule
|
2011-06-05 17:09:19 -04:00
|
|
|
var detC0X = C[0][0] * X[1] - C[1][0] * X[0],
|
|
|
|
detXC1 = X[0] * C[1][1] - X[1] * C[0][1];
|
2011-06-05 16:45:29 -04:00
|
|
|
// Derive alpha values
|
2011-06-05 17:09:19 -04:00
|
|
|
alpha1 = detXC1 / detC0C1;
|
|
|
|
alpha2 = detC0X / detC0C1;
|
2011-06-05 16:45:29 -04:00
|
|
|
} else {
|
2011-06-05 17:09:19 -04:00
|
|
|
// Matrix is under-determined, try assuming alpha1 == alpha2
|
2011-06-05 16:45:29 -04:00
|
|
|
var c0 = C[0][0] + C[0][1],
|
|
|
|
c1 = C[1][0] + C[1][1];
|
2011-06-05 17:09:19 -04:00
|
|
|
if (Math.abs(c0) > epsilon) {
|
|
|
|
alpha1 = alpha2 = X[0] / c0;
|
|
|
|
} else if (Math.abs(c0) > epsilon) {
|
|
|
|
alpha1 = alpha2 = X[1] / c1;
|
2011-06-05 16:45:29 -04:00
|
|
|
} else {
|
|
|
|
// Handle below
|
2011-06-05 17:09:19 -04:00
|
|
|
alpha1 = alpha2 = 0.;
|
2011-06-05 16:45:29 -04:00
|
|
|
}
|
|
|
|
}
|
2011-06-05 14:27:18 -04:00
|
|
|
|
|
|
|
// If alpha negative, use the Wu/Barsky heuristic (see text)
|
|
|
|
// (if alpha is 0, you get coincident control points that lead to
|
|
|
|
// divide by zero in any subsequent NewtonRaphsonRootFind() call.
|
2011-06-05 17:09:19 -04:00
|
|
|
var segLength = pt2.getDistance(pt1);
|
|
|
|
epsilon *= segLength;
|
|
|
|
if (alpha1 < epsilon || alpha2 < epsilon) {
|
2011-06-30 06:01:51 -04:00
|
|
|
// fall back on standard (probably inaccurate) formula,
|
2011-06-05 14:27:18 -04:00
|
|
|
// and subdivide further if needed.
|
2011-06-05 17:09:19 -04:00
|
|
|
alpha1 = alpha2 = segLength / 3;
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// First and last control points of the Bezier curve are
|
|
|
|
// positioned exactly at the first and last data points
|
|
|
|
// Control points 1 and 2 are positioned an alpha distance out
|
|
|
|
// on the tangent vectors, left and right, respectively
|
2011-06-05 17:09:19 -04:00
|
|
|
return [pt1, pt1.add(tan1.normalize(alpha1)),
|
|
|
|
pt2.add(tan2.normalize(alpha2)), pt2];
|
2011-06-05 14:27:18 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Given set of points and their parameterization, try to find
|
|
|
|
// a better parameterization.
|
2011-06-05 17:06:24 -04:00
|
|
|
reparameterize: function(first, last, u, curve) {
|
2011-06-05 14:27:18 -04:00
|
|
|
for (var i = first; i <= last; i++) {
|
2011-06-05 18:14:28 -04:00
|
|
|
u[i - first] = this.findRoot(curve, this.points[i], u[i - first]);
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Use Newton-Raphson iteration to find better root.
|
2011-06-05 17:06:24 -04:00
|
|
|
findRoot: function(curve, point, u) {
|
|
|
|
var curve1 = [],
|
|
|
|
curve2 = [];
|
2011-06-05 14:27:18 -04:00
|
|
|
// Generate control vertices for Q'
|
|
|
|
for (var i = 0; i <= 2; i++) {
|
2011-06-05 17:06:24 -04:00
|
|
|
curve1[i] = curve[i + 1].subtract(curve[i]).multiply(3);
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
// Generate control vertices for Q''
|
|
|
|
for (var i = 0; i <= 1; i++) {
|
2011-06-05 17:06:24 -04:00
|
|
|
curve2[i] = curve1[i + 1].subtract(curve1[i]).multiply(2);
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
// Compute Q(u), Q'(u) and Q''(u)
|
2011-06-05 17:06:24 -04:00
|
|
|
var pt = this.evaluate(3, curve, u),
|
|
|
|
pt1 = this.evaluate(2, curve1, u),
|
|
|
|
pt2 = this.evaluate(1, curve2, u),
|
|
|
|
diff = pt.subtract(point),
|
|
|
|
df = pt1.dot(pt1) + diff.dot(pt2);
|
|
|
|
// Compute f(u) / f'(u)
|
2011-06-05 16:59:42 -04:00
|
|
|
if (Math.abs(df) < Numerical.TOLERANCE)
|
2011-06-05 14:27:18 -04:00
|
|
|
return u;
|
|
|
|
// u = u - f(u) / f'(u)
|
2011-06-06 12:18:22 -04:00
|
|
|
return u - diff.dot(pt1) / df;
|
2011-06-05 14:27:18 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Evaluate a Bezier curve at a particular parameter value
|
2011-06-05 17:06:24 -04:00
|
|
|
evaluate: function(degree, curve, t) {
|
2011-06-05 14:27:18 -04:00
|
|
|
// Copy array
|
2011-06-05 17:06:24 -04:00
|
|
|
var tmp = curve.slice();
|
2011-06-05 14:27:18 -04:00
|
|
|
// Triangle computation
|
|
|
|
for (var i = 1; i <= degree; i++) {
|
|
|
|
for (var j = 0; j <= degree - i; j++) {
|
2011-06-05 17:06:24 -04:00
|
|
|
tmp[j] = tmp[j].multiply(1 - t).add(tmp[j + 1].multiply(t));
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
}
|
2011-06-05 17:06:24 -04:00
|
|
|
return tmp[0];
|
2011-06-05 14:27:18 -04:00
|
|
|
},
|
|
|
|
|
2011-06-30 06:01:51 -04:00
|
|
|
// Assign parameter values to digitized points
|
2011-06-05 14:27:18 -04:00
|
|
|
// using relative distances between points.
|
|
|
|
chordLengthParameterize: function(first, last) {
|
|
|
|
var u = [0];
|
|
|
|
for (var i = first + 1; i <= last; i++) {
|
|
|
|
u[i - first] = u[i - first - 1]
|
|
|
|
+ this.points[i].getDistance(this.points[i - 1]);
|
|
|
|
}
|
2011-06-05 18:00:33 -04:00
|
|
|
for (var i = 1, m = last - first; i <= m; i++) {
|
|
|
|
u[i] /= u[m];
|
2011-06-05 14:27:18 -04:00
|
|
|
}
|
|
|
|
return u;
|
|
|
|
},
|
|
|
|
|
2011-06-05 16:31:58 -04:00
|
|
|
// Find the maximum squared distance of digitized points to fitted curve.
|
2011-06-05 17:06:24 -04:00
|
|
|
findMaxError: function(first, last, curve, u) {
|
2011-06-05 14:27:18 -04:00
|
|
|
var index = Math.floor((last - first + 1) / 2),
|
|
|
|
maxDist = 0;
|
|
|
|
for (var i = first + 1; i < last; i++) {
|
2011-06-05 17:06:24 -04:00
|
|
|
var P = this.evaluate(3, curve, u[i - first]);
|
2011-06-05 14:27:18 -04:00
|
|
|
var v = P.subtract(this.points[i]);
|
|
|
|
var dist = v.x * v.x + v.y * v.y; // squared
|
|
|
|
if (dist >= maxDist) {
|
|
|
|
maxDist = dist;
|
|
|
|
index = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
error: maxDist,
|
|
|
|
index: index
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|