Skip to content

Commit

Permalink
Expose fitCubic and createTangent
Browse files Browse the repository at this point in the history
  • Loading branch information
aeplay committed Jun 12, 2020
1 parent 088c35d commit bfb05ce
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 123 deletions.
230 changes: 116 additions & 114 deletions lib/fit-curve.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@
}
}

/**
* @preserve JavaScript implementation of
* Algorithm for Automatically Fitting Digitized Curves
* by Philip J. Schneider
* "Graphics Gems", Academic Press, 1990
*
* The MIT License (MIT)
*
* https://github.com/soswow/fit-curves
/**
* @preserve JavaScript implementation of
* Algorithm for Automatically Fitting Digitized Curves
* by Philip J. Schneider
* "Graphics Gems", Academic Press, 1990
*
* The MIT License (MIT)
*
* https://github.com/soswow/fit-curves
*/

/**
* Fit one or more Bezier curves to a set of points.
*
* @param {Array<Array<Number>>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
* @param {Number} maxError - Tolerance, squared error between points and fitted curve
* @returns {Array<Array<Array<Number>>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
/**
* Fit one or more Bezier curves to a set of points.
*
* @param {Array<Array<Number>>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
* @param {Number} maxError - Tolerance, squared error between points and fitted curve
* @returns {Array<Array<Array<Number>>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
*/
function fitCurve(points, maxError, progressCallback) {
if (!Array.isArray(points)) {
Expand Down Expand Up @@ -67,15 +67,15 @@
return fitCubic(points, leftTangent, rightTangent, maxError, progressCallback);
}

/**
* Fit a Bezier curve to a (sub)set of digitized points.
* Your code should not call this function directly. Use {@link fitCurve} instead.
*
* @param {Array<Array<Number>>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
* @param {Array<Number>} leftTangent - Unit tangent vector at start point
* @param {Array<Number>} rightTangent - Unit tangent vector at end point
* @param {Number} error - Tolerance, squared error between points and fitted curve
* @returns {Array<Array<Array<Number>>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
/**
* Fit a Bezier curve to a (sub)set of digitized points.
* Your code should not call this function directly. Use {@link fitCurve} instead.
*
* @param {Array<Array<Number>>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
* @param {Array<Number>} leftTangent - Unit tangent vector at start point
* @param {Array<Number>} rightTangent - Unit tangent vector at end point
* @param {Number} error - Tolerance, squared error between points and fitted curve
* @returns {Array<Array<Array<Number>>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y]
*/
function fitCubic(points, leftTangent, rightTangent, error, progressCallback) {
var MaxIterations = 20; //Max times to try iterating (to find an acceptable curve)
Expand Down Expand Up @@ -169,15 +169,15 @@
//To and from need to point in opposite directions:
fromCenterTangent = maths.mulItems(toCenterTangent, -1);

/*
Note: An alternative to this "divide and conquer" recursion could be to always
let new curve segments start by trying to go all the way to the end,
instead of only to the end of the current subdivided polyline.
That might let many segments fit a few points more, reducing the number of total segments.
However, a few tests have shown that the segment reduction is insignificant
(240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves on both),
and the results take twice as many steps and milliseconds to finish,
without looking any better than what we already have.
/*
Note: An alternative to this "divide and conquer" recursion could be to always
let new curve segments start by trying to go all the way to the end,
instead of only to the end of the current subdivided polyline.
That might let many segments fit a few points more, reducing the number of total segments.
However, a few tests have shown that the segment reduction is insignificant
(240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves on both),
and the results take twice as many steps and milliseconds to finish,
without looking any better than what we already have.
*/
beziers = beziers.concat(fitCubic(points.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, progressCallback));
beziers = beziers.concat(fitCubic(points.slice(splitPoint), fromCenterTangent, rightTangent, error, progressCallback));
Expand Down Expand Up @@ -213,14 +213,14 @@
return [bezCurve, maxError, splitPoint];
}

/**
* Use least-squares method to find Bezier control points for region.
*
* @param {Array<Array<Number>>} points - Array of digitized points
* @param {Array<Number>} parameters - Parameter values for region
* @param {Array<Number>} leftTangent - Unit tangent vector at start point
* @param {Array<Number>} rightTangent - Unit tangent vector at end point
* @returns {Array<Array<Number>>} Approximated Bezier curve: [first-point, control-point-1, control-point-2, second-point] where points are [x, y]
/**
* Use least-squares method to find Bezier control points for region.
*
* @param {Array<Array<Number>>} points - Array of digitized points
* @param {Array<Number>} parameters - Parameter values for region
* @param {Array<Number>} leftTangent - Unit tangent vector at start point
* @param {Array<Number>} rightTangent - Unit tangent vector at end point
* @returns {Array<Array<Number>>} Approximated Bezier curve: [first-point, control-point-1, control-point-2, second-point] where points are [x, y]
*/
function generateBezier(points, parameters, leftTangent, rightTangent) {
var bezCurve,
Expand Down Expand Up @@ -311,50 +311,50 @@
return bezCurve;
};

/**
* Given set of points and their parameterization, try to find a better parameterization.
*
* @param {Array<Array<Number>>} bezier - Current fitted curve
* @param {Array<Array<Number>>} points - Array of digitized points
* @param {Array<Number>} parameters - Current parameter values
* @returns {Array<Number>} New parameter values
/**
* Given set of points and their parameterization, try to find a better parameterization.
*
* @param {Array<Array<Number>>} bezier - Current fitted curve
* @param {Array<Array<Number>>} points - Array of digitized points
* @param {Array<Number>} parameters - Current parameter values
* @returns {Array<Number>} New parameter values
*/
function reparameterize(bezier, points, parameters) {
/*
var j, len, point, results, u;
results = [];
for (j = 0, len = points.length; j < len; j++) {
point = points[j], u = parameters[j];
results.push(newtonRaphsonRootFind(bezier, point, u));
}
return results;
/*
var j, len, point, results, u;
results = [];
for (j = 0, len = points.length; j < len; j++) {
point = points[j], u = parameters[j];
results.push(newtonRaphsonRootFind(bezier, point, u));
}
return results;
//*/
return parameters.map(function (p, i) {
return newtonRaphsonRootFind(bezier, points[i], p);
});
};

/**
* Use Newton-Raphson iteration to find better root.
*
* @param {Array<Array<Number>>} bez - Current fitted curve
* @param {Array<Number>} point - Digitized point
* @param {Number} u - Parameter value for "P"
* @returns {Number} New u
/**
* Use Newton-Raphson iteration to find better root.
*
* @param {Array<Array<Number>>} bez - Current fitted curve
* @param {Array<Number>} point - Digitized point
* @param {Number} u - Parameter value for "P"
* @returns {Number} New u
*/
function newtonRaphsonRootFind(bez, point, u) {
/*
Newton's root finding algorithm calculates f(x)=0 by reiterating
x_n+1 = x_n - f(x_n)/f'(x_n)
We are trying to find curve parameter u for some point p that minimizes
the distance from that point to the curve. Distance point to curve is d=q(u)-p.
At minimum distance the point is perpendicular to the curve.
We are solving
f = q(u)-p * q'(u) = 0
with
f' = q'(u) * q'(u) + q(u)-p * q''(u)
gives
u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)|
/*
Newton's root finding algorithm calculates f(x)=0 by reiterating
x_n+1 = x_n - f(x_n)/f'(x_n)
We are trying to find curve parameter u for some point p that minimizes
the distance from that point to the curve. Distance point to curve is d=q(u)-p.
At minimum distance the point is perpendicular to the curve.
We are solving
f = q(u)-p * q'(u) = 0
with
f' = q'(u) * q'(u) + q(u)-p * q''(u)
gives
u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)|
*/

var d = maths.subtract(bezier.q(bez, u), point),
Expand All @@ -369,11 +369,11 @@
}
};

/**
* Assign parameter values to digitized points using relative distances between points.
*
* @param {Array<Array<Number>>} points - Array of digitized points
* @returns {Array<Number>} Parameter values
/**
* Assign parameter values to digitized points using relative distances between points.
*
* @param {Array<Array<Number>>} points - Array of digitized points
* @returns {Array<Number>} Parameter values
*/
function chordLengthParameterize(points) {
var u = [],
Expand All @@ -395,13 +395,13 @@
return u;
};

/**
* Find the maximum squared distance of digitized points to fitted curve.
*
* @param {Array<Array<Number>>} points - Array of digitized points
* @param {Array<Array<Number>>} bez - Fitted curve
* @param {Array<Number>} parameters - Parameterization of points
* @returns {Array<Number>} Maximum error (squared) and point of max error
/**
* Find the maximum squared distance of digitized points to fitted curve.
*
* @param {Array<Array<Number>>} points - Array of digitized points
* @param {Array<Array<Number>>} bez - Fitted curve
* @param {Array<Number>} parameters - Parameterization of points
* @returns {Array<Number>} Maximum error (squared) and point of max error
*/
function computeMaxError(points, bez, parameters) {
var dist, //Current error
Expand Down Expand Up @@ -463,28 +463,28 @@
return 1;
}

/*
'param' is a value between 0 and 1 telling us the relative position
of a point on the source polyline (linearly from the start (0) to the end (1)).
To see if a given curve - 'bez' - is a close approximation of the polyline,
we compare such a poly-point to the point on the curve that's the same
relative distance along the curve's length.
But finding that curve-point takes a little work:
There is a function "B(t)" to find points along a curve from the parametric parameter 't'
(also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660
http://pomax.github.io/bezierinfo/#explanation),
but 't' isn't linear by length (http://gamedev.stackexchange.com/questions/105230).
So, we sample some points along the curve using a handful of values for 't'.
Then, we calculate the length between those samples via plain euclidean distance;
B(t) concentrates the points around sharp turns, so this should give us a good-enough outline of the curve.
Thus, for a given relative distance ('param'), we can now find an upper and lower value
for the corresponding 't' by searching through those sampled distances.
Finally, we just use linear interpolation to find a better value for the exact 't'.
More info:
http://gamedev.stackexchange.com/questions/105230/points-evenly-spaced-along-a-bezier-curve
http://stackoverflow.com/questions/29438398/cheap-way-of-calculating-cubic-bezier-length
http://steve.hollasch.net/cgindex/curves/cbezarclen.html
https://github.com/retuxx/tinyspline
/*
'param' is a value between 0 and 1 telling us the relative position
of a point on the source polyline (linearly from the start (0) to the end (1)).
To see if a given curve - 'bez' - is a close approximation of the polyline,
we compare such a poly-point to the point on the curve that's the same
relative distance along the curve's length.
But finding that curve-point takes a little work:
There is a function "B(t)" to find points along a curve from the parametric parameter 't'
(also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660
http://pomax.github.io/bezierinfo/#explanation),
but 't' isn't linear by length (http://gamedev.stackexchange.com/questions/105230).
So, we sample some points along the curve using a handful of values for 't'.
Then, we calculate the length between those samples via plain euclidean distance;
B(t) concentrates the points around sharp turns, so this should give us a good-enough outline of the curve.
Thus, for a given relative distance ('param'), we can now find an upper and lower value
for the corresponding 't' by searching through those sampled distances.
Finally, we just use linear interpolation to find a better value for the exact 't'.
More info:
http://gamedev.stackexchange.com/questions/105230/points-evenly-spaced-along-a-bezier-curve
http://stackoverflow.com/questions/29438398/cheap-way-of-calculating-cubic-bezier-length
http://steve.hollasch.net/cgindex/curves/cbezarclen.html
https://github.com/retuxx/tinyspline
*/
var lenMax, lenMin, tMax, tMin, t;

Expand All @@ -505,16 +505,16 @@
return t;
}

/**
* Creates a vector of length 1 which shows the direction from B to A
/**
* Creates a vector of length 1 which shows the direction from B to A
*/
function createTangent(pointA, pointB) {
return maths.normalize(maths.subtract(pointA, pointB));
}

/*
Simplified versions of what we need from math.js
Optimized for our input, which is only numbers and 1x2 arrays (i.e. [x, y] coordinates).
/*
Simplified versions of what we need from math.js
Optimized for our input, which is only numbers and 1x2 arrays (i.e. [x, y] coordinates).
*/

var maths = function () {
Expand Down Expand Up @@ -625,4 +625,6 @@
}();

module.exports = fitCurve;
module.exports.fitCubic = fitCubic;
module.exports.createTangent = createTangent;
});
Loading

0 comments on commit bfb05ce

Please sign in to comment.