A Pen by Andreas Borgen on CodePen.
Last active
April 30, 2024 09:10
-
-
Save Sphinxxxx/43e307ab73b8bed142c3395b420b9ba3 to your computer and use it in GitHub Desktop.
Bezier Curve testing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h2>Bezier curve testing</h2> | |
<svg width="550" height="270" > | |
<circle id="dot" r="2" cx="-10" cy="-10" | |
stroke="black" opacity=".5" stroke-width="1" fill="none"/> | |
<path id="approx" | |
stroke="dodgerblue" stroke-width="1" fill="none" /> | |
<path id="progress" | |
stroke="limegreen" stroke-width="2" fill="none" /> | |
<circle id="splitCompPos" r="4" cx="-10" cy="-10" | |
stroke="green" stroke-width="3" fill="none"/> | |
<text id="splitPoint" font-family="sans-serif" font-size="20px" font-weight="bold"></text> | |
</svg> | |
<select id="steps"></select> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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). | |
*/ | |
class maths { | |
//zeros = logAndRun(math.zeros); | |
static zeros_Xx2x2(x) { | |
var zs = []; | |
while(x--) { zs.push([0,0]); } | |
return zs | |
} | |
//multiply = logAndRun(math.multiply); | |
static mulItems(items, multiplier) { | |
//return items.map(x => x*multiplier); | |
return [items[0]*multiplier, items[1]*multiplier]; | |
} | |
static mulMatrix(m1, m2) { | |
//https://en.wikipedia.org/wiki/Matrix_multiplication#Matrix_product_.28two_matrices.29 | |
//Simplified to only handle 1-dimensional matrices (i.e. arrays) of equal length: | |
// return m1.reduce((sum,x1,i) => sum + (x1*m2[i]), | |
// 0); | |
return (m1[0]*m2[0]) + (m1[1]*m2[1]); | |
} | |
//Only used to subract to points (or at least arrays): | |
// subtract = logAndRun(math.subtract); | |
static subtract(arr1, arr2) { | |
//return arr1.map((x1, i) => x1 - arr2[i]); | |
return [arr1[0]-arr2[0], arr1[1]-arr2[1]]; | |
} | |
//add = logAndRun(math.add); | |
static addArrays(arr1, arr2) { | |
//return arr1.map((x1, i) => x1 + arr2[i]); | |
return [arr1[0]+arr2[0], arr1[1]+arr2[1]]; | |
} | |
static addItems(items, addition) { | |
//return items.map(x => x+addition); | |
return [items[0]+addition, items[1]+addition]; | |
} | |
//var sum = logAndRun(math.sum); | |
static sum(items) { | |
return items.reduce((sum,x) => sum + x); | |
} | |
//chain = math.chain; | |
//Only used on two arrays. The dot product is equal to the matrix product in this case: | |
// dot = logAndRun(math.dot); | |
static dot(m1, m2) { | |
return maths.mulMatrix(m1, m2); | |
} | |
//https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm | |
// var norm = logAndRun(math.norm); | |
static vectorLen(v) { | |
var a = v[0], b = v[1]; | |
return Math.sqrt(a*a + b*b); | |
} | |
//math.divide = logAndRun(math.divide); | |
static divItems(items, divisor) { | |
//return items.map(x => x/divisor); | |
return [items[0]/divisor, items[1]/divisor]; | |
} | |
//var dotPow = logAndRun(math.dotPow); | |
static squareItems(items) { | |
//return items.map(x => x*x); | |
var a = items[0], b = items[1]; | |
return [a*a, b*b]; | |
} | |
static normalize(v) { | |
return this.divItems(v, this.vectorLen(v)); | |
} | |
//Math.pow = logAndRun(Math.pow); | |
} | |
class bezier { | |
//Evaluates cubic bezier at t, return point | |
static q(ctrlPoly, t) { | |
var tx = 1.0 - t; | |
var pA = maths.mulItems( ctrlPoly[0], tx * tx * tx ), | |
pB = maths.mulItems( ctrlPoly[1], 3 * tx * tx * t ), | |
pC = maths.mulItems( ctrlPoly[2], 3 * tx * t * t ), | |
pD = maths.mulItems( ctrlPoly[3], t * t * t ); | |
return maths.addArrays(maths.addArrays(pA, pB), maths.addArrays(pC, pD)); | |
} | |
//Evaluates cubic bezier first derivative at t, return point | |
static qprime(ctrlPoly, t) { | |
var tx = 1.0 - t; | |
var pA = maths.mulItems( maths.subtract(ctrlPoly[1], ctrlPoly[0]), 3 * tx * tx ), | |
pB = maths.mulItems( maths.subtract(ctrlPoly[2], ctrlPoly[1]), 6 * tx * t ), | |
pC = maths.mulItems( maths.subtract(ctrlPoly[3], ctrlPoly[2]), 3 * t * t ); | |
return maths.addArrays(maths.addArrays(pA, pB), pC); | |
} | |
//Evaluates cubic bezier second derivative at t, return point | |
static qprimeprime(ctrlPoly, t) { | |
return maths.addArrays(maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[2], maths.mulItems(ctrlPoly[1], 2)), ctrlPoly[0]), 6 * (1.0 - t) ), | |
maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[3], maths.mulItems(ctrlPoly[2], 2)), ctrlPoly[1]), 6 * t )); | |
} | |
} | |
/** | |
* 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) { | |
var len = points.length, | |
leftTangent = createTangent(points[1], points[0]), | |
rightTangent = createTangent(points[len - 2], points[len - 1]); | |
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] | |
*/ | |
function fitCubic(points, leftTangent, rightTangent, error, progressCallback) { | |
const MaxIterations = 20; //Max times to try iterating (to find an acceptable curve) | |
var bezCurve, //Control points of fitted Bezier curve | |
u, //Parameter values for point | |
uPrime, //Improved parameter values | |
maxError, prevErr, //Maximum fitting error | |
splitPoint, prevSplit, //Point to split point set at if we need more than one curve | |
centerVector, toCenterTangent, fromCenterTangent, //Unit tangent vector(s) at splitPoint | |
beziers, //Array of fitted Bezier curves if we need more than one curve | |
dist, i; | |
//console.log('fitCubic, ', points.length); | |
//Use heuristic if region only has two points in it | |
if (points.length === 2) { | |
dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0; | |
bezCurve = [ | |
points[0], | |
maths.addArrays(points[0], maths.mulItems(leftTangent, dist)), | |
maths.addArrays(points[1], maths.mulItems(rightTangent, dist)), | |
points[1] | |
]; | |
return [bezCurve]; | |
} | |
//Parameterize points, and attempt to fit curve | |
u = chordLengthParameterize(points); | |
[bezCurve, maxError, splitPoint] = generateAndReport(points, u, u, leftTangent, rightTangent, progressCallback) | |
if (maxError < error) { | |
return [bezCurve]; | |
} | |
//If error not too large, try some reparameterization and iteration | |
if (maxError < (error*error)) { | |
uPrime = u; | |
prevErr = maxError; | |
prevSplit = splitPoint; | |
for (i = 0; i < MaxIterations; i++) { | |
uPrime = reparameterize(bezCurve, points, uPrime); | |
[bezCurve, maxError, splitPoint] = generateAndReport(points, u, uPrime, leftTangent, rightTangent, progressCallback); | |
if (maxError < error) { | |
return [bezCurve]; | |
} | |
//If the development of the fitted curve grinds to a halt, | |
//we abort this attempt (and try a shorter curve): | |
else if(splitPoint === prevSplit) { | |
let errChange = maxError/prevErr; | |
if((errChange > .9999) && (errChange < 1.0001)) { | |
break; | |
} | |
} | |
prevErr = maxError; | |
prevSplit = splitPoint; | |
} | |
} | |
//Fitting failed -- split at max error point and fit recursively | |
beziers = []; | |
//To create a smooth transition from one curve segment to the next, | |
//we calculate the tangent of the points directly before and after the center, | |
//and use that same tangent both to and from the center point. | |
centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint + 1]); | |
//However, should those two points be equal, the normal tangent calculation will fail. | |
//---Instead, we assume that the center is a sharp corner and calculate the "to/from" tangents separately.--- | |
//Instead, we calculate the tangent from that "double-point" to the center point, and rotate 90deg clockwise. | |
if((centerVector[0] === 0) && (centerVector[1] === 0)) { | |
//toCenterTangent = createTangent(points[splitPoint - 1], points[splitPoint]); | |
//fromCenterTangent = createTangent(points[splitPoint + 1], points[splitPoint]); | |
//[x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660 | |
centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint]) | |
.reverse(); | |
centerVector[0] = -centerVector[0]; | |
} | |
toCenterTangent = maths.normalize(centerVector); | |
//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. | |
*/ | |
beziers = beziers.concat(fitCubic(points.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, progressCallback)); | |
beziers = beziers.concat(fitCubic(points.slice(splitPoint), fromCenterTangent, rightTangent, error, progressCallback)); | |
return beziers; | |
}; | |
function generateAndReport(points, paramsOrig, paramsPrime, leftTangent, rightTangent, progressCallback) { | |
var bezCurve, maxError, splitPoint; | |
bezCurve = generateBezier(points, paramsPrime, leftTangent, rightTangent, progressCallback); | |
//Find max deviation of points to fitted curve. | |
//Here we always use the original parameters (from chordLengthParameterize()), | |
//because we need to compare the current curve to the actual source polyline, | |
//and not the currently iterated parameters which reparameterize() & generateBezier() use, | |
//as those have probably drifted far away and may no longer be in ascending order. | |
[maxError, splitPoint] = computeMaxError(points, bezCurve, paramsOrig); | |
if(progressCallback) { | |
progressCallback({ | |
bez: bezCurve, | |
points: points, | |
params: paramsOrig, | |
maxErr: maxError, | |
maxPoint: splitPoint, | |
}); | |
} | |
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] | |
*/ | |
function generateBezier(points, parameters, leftTangent, rightTangent) { | |
var bezCurve, //Bezier curve ctl pts | |
A, a, //Precomputed rhs for eqn | |
C, X, //Matrices C & X | |
det_C0_C1, det_C0_X, det_X_C1, //Determinants of matrices | |
alpha_l, alpha_r, //Alpha values, left and right | |
epsilon, segLength, | |
i, len, tmp, u, ux, | |
firstPoint = points[0], | |
lastPoint = points[points.length-1]; | |
bezCurve = [firstPoint, null, null, lastPoint]; | |
//console.log('gb', parameters.length); | |
//Compute the A's | |
A = maths.zeros_Xx2x2(parameters.length); | |
for (i = 0, len = parameters.length; i < len; i++) { | |
u = parameters[i]; | |
ux = 1 - u; | |
a = A[i]; | |
a[0] = maths.mulItems(leftTangent, 3 * u * (ux*ux)); | |
a[1] = maths.mulItems(rightTangent, 3 * ux * (u*u)); | |
} | |
//Create the C and X matrices | |
C = [[0,0], [0,0]]; | |
X = [0,0]; | |
for (i = 0, len = points.length; i < len; i++) { | |
u = parameters[i]; | |
a = A[i]; | |
C[0][0] += maths.dot(a[0], a[0]); | |
C[0][1] += maths.dot(a[0], a[1]); | |
C[1][0] += maths.dot(a[0], a[1]); | |
C[1][1] += maths.dot(a[1], a[1]); | |
tmp = maths.subtract(points[i], bezier.q([firstPoint, firstPoint, lastPoint, lastPoint], u)); | |
X[0] += maths.dot(a[0], tmp); | |
X[1] += maths.dot(a[1], tmp); | |
} | |
//Compute the determinants of C and X | |
det_C0_C1 = (C[0][0] * C[1][1]) - (C[1][0] * C[0][1]); | |
det_C0_X = (C[0][0] * X[1] ) - (C[1][0] * X[0] ); | |
det_X_C1 = (X[0] * C[1][1]) - (X[1] * C[0][1]); | |
//Finally, derive alpha values | |
alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1; | |
alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1; | |
//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. | |
segLength = maths.vectorLen(maths.subtract(firstPoint, lastPoint)); | |
epsilon = 1.0e-6 * segLength; | |
if (alpha_l < epsilon || alpha_r < epsilon) { | |
//Fall back on standard (probably inaccurate) formula, and subdivide further if needed. | |
bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, segLength / 3.0)); | |
bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, segLength / 3.0)); | |
} else { | |
//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 | |
bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, alpha_l)); | |
bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, alpha_r)); | |
} | |
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 | |
*/ | |
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; | |
//*/ | |
return parameters.map((p, i) => 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 | |
*/ | |
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)| | |
*/ | |
var d = maths.subtract(bezier.q(bez, u), point), | |
qprime = bezier.qprime(bez, u), | |
numerator = /*sum(*/maths.mulMatrix(d, qprime)/*)*/, | |
denominator = maths.sum(maths.addItems( maths.squareItems(qprime), maths.mulMatrix(d, bezier.qprimeprime(bez, u)) )); | |
if (denominator === 0) { | |
return u; | |
} else { | |
return u - (numerator/denominator); | |
} | |
}; | |
/** | |
* 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 = [], currU, prevU, prevP; | |
points.forEach((p, i) => { | |
currU = i ? prevU + maths.vectorLen(maths.subtract(p, prevP)) | |
: 0; | |
u.push(currU); | |
prevU = currU; | |
prevP = p; | |
}) | |
u = u.map(x => x/prevU); | |
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 | |
*/ | |
function computeMaxError(points, bez, parameters) { | |
var dist, //Current error | |
maxDist, //Maximum error | |
splitPoint, //Point of maximum error | |
v, //Vector from point to curve | |
i, count, point, t; | |
maxDist = 0; | |
splitPoint = points.length / 2; | |
for (i = 0, count = points.length; i < count; i++) { | |
point = points[i]; | |
//Find 't' for a point on the bez curve that's as close to 'point' as possible: | |
t = find_t(bez, parameters[i]); | |
v = maths.subtract(bezier.q(bez, t), point); | |
dist = v[0]*v[0] + v[1]*v[1]; | |
if (dist > maxDist) { | |
maxDist = dist; | |
splitPoint = i; | |
} | |
} | |
return [maxDist, splitPoint]; | |
}; | |
function find_t(bez, param) { | |
if(param <= 0) { return 0; } | |
if(param >= 1) { 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 a mysterious 't' value | |
(also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660), | |
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 | |
*/ | |
const B_parts = 10; | |
var lenMax, lenMin, tMax, tMin, t; | |
//Sample 't's and map them to relative distances along the curve: | |
bez.__B_t_dist_map = bez.__B_t_dist_map || (function() { | |
var B_t_dist = [0], | |
B_t_prev = bez[0], B_t_curr, sumLen = 0; | |
for(var i=1; i<=B_parts; i++) { | |
B_t_curr = bezier.q(bez, i/B_parts); | |
sumLen += maths.vectorLen(maths.subtract(B_t_curr, B_t_prev)); | |
B_t_dist.push(sumLen); | |
B_t_prev = B_t_curr; | |
} | |
//Normalize B_length to the same interval as the parameter distances; 0 to 1: | |
B_t_dist = B_t_dist.map(x => x/sumLen); | |
return B_t_dist; | |
})(); | |
//Find the two t-s that the current param distance lies between, | |
//and then interpolate a somewhat accurate value for the exact t: | |
var t_distMap = bez.__B_t_dist_map; | |
for(var i = 1; i <= B_parts; i++) { | |
if(param <= t_distMap[i]) { | |
tMin = (i-1) / B_parts; | |
tMax = i / B_parts; | |
lenMin = t_distMap[i-1]; | |
lenMax = t_distMap[i]; | |
t = (param-lenMin)/(lenMax-lenMin) * (tMax-tMin) + tMin; | |
break; | |
} | |
} | |
return t; | |
} | |
/** | |
* 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)); | |
} | |
/* TESTING */ | |
(function(undefined) { | |
"use strict"; | |
var _svg = document.querySelector('svg'), | |
_dot = document.querySelector('#dot'), | |
_approx = document.querySelector('#approx'), | |
_progress = document.querySelector('#progress'), | |
_splitPoint = document.querySelector('#splitPoint'), | |
_splitCurvePos = document.querySelector('#splitCompPos'), | |
_steps = document.querySelector('#steps'), | |
_points = [ | |
/**/ | |
[193,116],[194,115],[197,112],[200,107],[203,103],[211, 89],[218, 72],[223, 45],[223, 29],[220, 20], | |
[210, 13],[196, 9],[176, 9],[150, 17],[114, 35],[ 74, 69],[ 42,109],[ 21,144],[ 9,171],[ 9,187], | |
[ 15,201],[ 25,212],[ 44,224],[ 66,235],[ 90,240],[112,237],[132,224],[146,208],[156,190],[160,176], | |
[160,167],[157,158],[154,153],[152,150],[152,148],[153,148],[153,149],[153,152],[152,156],[152,160], | |
[151,174],[150,191],[150,207],[153,220],[156,229],[159,233],[162,234],[170,233],[179,227],[189,215], | |
[196,199],[199,185],[199,177],[197,171],[193,166],[191,163],[189,160],[189,163],[189,167],[189,173], | |
[189,178],[192,193],[196,207],[201,218],[208,224],[212,226],[216,226],[224,219],[232,205],[236,192], | |
[237,182],[238,179],[238,178],[239,179],[240,181],[242,184],[243,188],[247,201],[248,209],[248,213], | |
[248,215],[248,212],[246,207],[244,203],[242,194],[239,184],[239,181],[242,179],[245,178],[251,178], | |
[262,178],[276,178],[286,179],[294,175],[296,172],[298,168],[299,166],[299,163],[299,160],[299,159], | |
[300,163],[301,167],[301,175],[302,182],[303,198],[306,212],[309,220],[313,223],[313,224],[315,223], | |
[316,216],[320,205],[324,197],[324,191],[324,186],[322,182],[321,180], | |
[320,179],[321,180],[324,182], | |
[329,185],[332,187],[342,189],[353,188],[366,184],[379,175],[386,161],[388,148],[384,138],[380,135], | |
[373,135],[365,139],[357,168],[357,192],[364,212],[379,230],[402,242],[436,250],[483,249],[546,229] | |
/**/ | |
]; | |
//_points = [[100,151],[101,150],[102,148],[104,145],[105,141],[109,129],[111,119],[111,112],[110,107],[108,105],[105,104],[103,104],[98,107],[90,114],[80,127],[69,144],[53,170],[45,184],[43,194],[45,203],[48,209],[52,213],[61,218],[68,219],[78,219],[86,214],[93,205],[96,196],[101,182],[104,173],[104,166],[104,162],[104,160],[104,161],[103,161],[102,161],[101,160],[101,159],[100,158],[100,157],[100,159],[100,163],[101,168],[101,173],[104,182],[107,187],[110,191],[114,192],[116,191],[119,187],[123,177],[127,161],[130,145],[134,128],[138,118],[138,114],[138,115],[138,116],[138,119],[138,123],[140,139],[142,157],[143,171],[143,179],[143,184],[143,186],[143,185],[143,181],[144,176],[145,172],[147,159],[149,150],[152,142],[155,139],[156,137],[158,139],[161,146],[163,152],[166,159],[170,170],[173,178],[174,178],[175,178],[176,177],[178,174],[179,170],[184,162],[190,152],[194,142],[199,135],[203,132],[205,133],[205,136],[206,139],[207,142],[210,149],[211,153],[211,155],[211,154],[210,150],[210,147],[206,138],[202,128],[200,120],[199,119],[198,121],[198,125],[197,130],[197,134],[196,138],[195,147],[195,150],[195,153],[198,155],[200,156],[203,156],[205,156],[207,155],[209,147],[213,134],[217,118],[222,95],[223,75],[224,57],[224,42],[224,37],[224,36],[224,39],[222,51],[221,68],[218,88],[214,121],[213,146],[213,167],[217,181],[221,188],[222,190],[223,190],[225,185],[230,174],[235,162],[239,152],[240,145],[240,143],[240,144],[241,148],[241,153],[241,162],[241,166],[241,168],[241,165],[241,160],[241,156],[241,148],[242,144],[243,143],[247,144],[257,149],[283,165],[301,174],[312,178],[316,178],[317,176],[319,170],[321,161],[323,153],[322,144],[320,136],[320,134],[319,133],[315,136],[312,146],[307,164],[303,185],[303,203],[306,214],[310,217],[311,219],[314,219],[318,213],[324,200],[333,184],[349,163],[360,159],[367,159],[370,159],[373,165],[377,173],[381,184],[385,192],[385,193],[384,190],[382,184],[378,177],[373,169],[364,159],[362,157],[361,157],[359,166],[355,180],[353,195],[353,205],[354,212],[356,214],[358,213],[362,202],[373,187],[389,171],[404,158],[414,148],[418,140],[419,137],[419,134],[417,132],[417,131],[416,131],[415,131],[414,131],[414,130],[413,134],[411,138],[408,144],[405,155],[404,171],[407,176],[410,178],[415,176],[423,173],[433,170],[439,169],[442,170],[448,184],[453,204],[453,217],[451,220],[448,219],[440,212],[431,205],[425,201],[423,201]]; | |
//_points = [[100,200], [200,150], [300,100], [400,150], [500,200]]; | |
//_points = [ | |
// [ 5, 5], | |
// [ 5, 50], | |
// [110, 140], | |
// [210, 160], | |
// [320, 110], | |
// [400, 20], | |
// [400, 5] | |
//]; | |
function cloneSVGElement(orig, atts) { | |
var clone = orig.cloneNode(); | |
Object.keys(atts || {}).forEach(name => clone.setAttribute(name, atts[name])); | |
_svg.insertBefore(clone, orig); | |
return clone; | |
} | |
function createSVGData(curves) { | |
function roundItems(items) { | |
//return items; | |
return items.map(x => x.toFixed(1)); | |
} | |
var pd = curves.map(function(x, i) { | |
var c1 = roundItems(x[1]), | |
c2 = roundItems(x[2]), | |
end = roundItems(x[3]); | |
var segment = (i === 0) ? 'M'+ roundItems(x[0]) +' ' : ''; | |
segment += 'C'+ [c1, c2, end].join(' '); | |
return segment; | |
}).join(' '); | |
return pd; | |
} | |
function approxPath(points, error, progress) { | |
var curves, path, start, end; | |
function onProgress(state) { | |
//console.log(state); | |
progress.push(state); | |
} | |
start = (new Date()).getTime(); | |
curves = fitCurve(points, error, onProgress); | |
end = (new Date()).getTime(); | |
console.log('error '+ error + ':', | |
points.length + ' points ->', | |
curves.length + ' curves (', | |
progress.length + ' steps, ' + (end-start) + 'ms)'); | |
var path = createSVGData(curves); | |
return path; | |
} | |
console.log(''); | |
//Draw the original sampled points for comparison: | |
for(var i = 0; i < _points.length; i++) { | |
cloneSVGElement(_dot, { | |
'cx': _points[i][0], | |
'cy': _points[i][1], | |
}); | |
} | |
//Find the Bezier approximation: | |
var progress = [], | |
fitted = approxPath(_points, 100, progress); | |
cloneSVGElement(_approx, { | |
'd': fitted, | |
'stroke': 'dodgerblue', | |
}); | |
console.log(fitted); | |
//Examine the steps it took to create that Bezier: | |
if(progress.length) { | |
function renderStep(index) { | |
if(!index) { return; } | |
var state = progress[index], | |
bez = state.bez; | |
/*Testing spacing of B(t) markers with specific curves: | |
var testMarkers = [0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1]; | |
//var b = 'M373,135C699,429,69,119,546,229'; | |
//var b = 'M373,135C345,310,242,198,546,229'; | |
var b = 'M373,135C750,-200,0,399,546,229'; | |
//var b = 'M373,135C914,-55,13,398,546,229'; | |
//var b = 'M373,135C724,2,417,-94,546,229'; | |
b = b.replace(/[^-\d]+/g, ' ').trim().split(' ').map(Number); | |
bez = [ [b[0],b[1]], [b[2],b[3]], [b[4],b[5]], [b[6],b[7]] ]; | |
testMarkers.forEach((markerDist, i) => { | |
var t = find_t(bez, markerDist), | |
markerPos = bezier.q(bez, t); | |
cloneSVGElement(_splitCurvePos, { | |
'stroke': 'gold', | |
'stroke-width': 2, | |
'opacity': .7, | |
'cx': markerPos[0], | |
'cy': markerPos[1], | |
}); | |
}); | |
//*/ | |
var path = createSVGData([bez]); | |
_progress.setAttribute('d', path); | |
console.log('Step', index+':\t', path); | |
if(state.maxPoint) { | |
let maxPoint = state.maxPoint, | |
maxPointPos = state.points[maxPoint], | |
maxCurvePos = bezier.q(bez, find_t(bez, state.params[maxPoint])); | |
console.log('Max', state.params[maxPoint]); | |
_splitPoint.textContent = '◣' + Math.round(state.maxErr); | |
_splitPoint.setAttribute('x', maxPointPos[0]); | |
_splitPoint.setAttribute('y', maxPointPos[1]); | |
_splitCurvePos.setAttribute('cx', maxCurvePos[0]); | |
_splitCurvePos.setAttribute('cy', maxCurvePos[1]); | |
} | |
} | |
var segStart, segEnd, | |
segNum = 0, segAttempt = 0, segIter = 0; | |
progress.forEach((step, i) => { | |
var start = step.bez[0].toString(), | |
end = step.bez[3].toString(), | |
descr, option; | |
if(start !== segStart) { | |
segStart = start; | |
segNum++; | |
segAttempt = 0; | |
segIter = 0; | |
} | |
if(end !== segEnd) { | |
segEnd = end; | |
segAttempt++; | |
segIter = 0; | |
} | |
segIter++; | |
option = document.createElement('option'); | |
option.value = i; | |
option.textContent = ['Segment '+segNum, 'Attempt '+segAttempt, 'Iteration '+segIter].join(', '); | |
_steps.appendChild(option); | |
}); | |
_steps.onchange = () => renderStep(_steps.value); | |
_steps.value = (parseInt(window.location.hash.replace('#', '')) || 0); | |
_steps.onchange(); | |
} | |
})(); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="//cdn.rawgit.com/Sphinxxxx/fit-curve/v0.9.1/lib/fit-curve.js"></script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment