fit-curve.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. /**
  2. * @preserve JavaScript implementation of
  3. * Algorithm for Automatically Fitting Digitized Curves
  4. * by Philip J. Schneider
  5. * "Graphics Gems", Academic Press, 1990
  6. *
  7. * The MIT License (MIT)
  8. *
  9. * https://github.com/soswow/fit-curves
  10. */
  11. /**
  12. * Fit one or more Bezier curves to a set of points.
  13. *
  14. * @param {Array<Array<Number>>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
  15. * @param {Number} maxError - Tolerance, squared error between points and fitted curve
  16. * @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]
  17. */
  18. function fitCurve(points, maxError, progressCallback) {
  19. if (!Array.isArray(points)) {
  20. throw new TypeError("First argument should be an array");
  21. }
  22. points.forEach((point) => {
  23. if(!Array.isArray(point) || point.some(item => typeof item !== 'number')
  24. || point.length !== points[0].length) {
  25. throw Error("Each point should be an array of numbers. Each point should have the same amount of numbers.");
  26. }
  27. });
  28. // Remove duplicate points
  29. points = points.filter((point, i) =>
  30. i === 0 || !point.every((val, j) => val === points[i-1][j])
  31. );
  32. if (points.length < 2) {
  33. return [];
  34. }
  35. const len = points.length;
  36. const leftTangent = createTangent(points[1], points[0]);
  37. const rightTangent = createTangent(points[len - 2], points[len - 1]);
  38. return fitCubic(points, leftTangent, rightTangent, maxError, progressCallback);
  39. }
  40. /**
  41. * Fit a Bezier curve to a (sub)set of digitized points.
  42. * Your code should not call this function directly. Use {@link fitCurve} instead.
  43. *
  44. * @param {Array<Array<Number>>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
  45. * @param {Array<Number>} leftTangent - Unit tangent vector at start point
  46. * @param {Array<Number>} rightTangent - Unit tangent vector at end point
  47. * @param {Number} error - Tolerance, squared error between points and fitted curve
  48. * @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]
  49. */
  50. function fitCubic(points, leftTangent, rightTangent, error, progressCallback) {
  51. const MaxIterations = 20; //Max times to try iterating (to find an acceptable curve)
  52. var bezCurve, //Control points of fitted Bezier curve
  53. u, //Parameter values for point
  54. uPrime, //Improved parameter values
  55. maxError, prevErr, //Maximum fitting error
  56. splitPoint, prevSplit, //Point to split point set at if we need more than one curve
  57. centerVector, toCenterTangent, fromCenterTangent, //Unit tangent vector(s) at splitPoint
  58. beziers, //Array of fitted Bezier curves if we need more than one curve
  59. dist, i;
  60. //console.log('fitCubic, ', points.length);
  61. //Use heuristic if region only has two points in it
  62. if (points.length === 2) {
  63. dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0;
  64. bezCurve = [
  65. points[0],
  66. maths.addArrays(points[0], maths.mulItems(leftTangent, dist)),
  67. maths.addArrays(points[1], maths.mulItems(rightTangent, dist)),
  68. points[1]
  69. ];
  70. return [bezCurve];
  71. }
  72. //Parameterize points, and attempt to fit curve
  73. u = chordLengthParameterize(points);
  74. [bezCurve, maxError, splitPoint] = generateAndReport(points, u, u, leftTangent, rightTangent, progressCallback)
  75. if ((maxError === 0) || (maxError < error)) {
  76. return [bezCurve];
  77. }
  78. //If error not too large, try some reparameterization and iteration
  79. if (maxError < (error*error)) {
  80. uPrime = u;
  81. prevErr = maxError;
  82. prevSplit = splitPoint;
  83. for (i = 0; i < MaxIterations; i++) {
  84. uPrime = reparameterize(bezCurve, points, uPrime);
  85. [bezCurve, maxError, splitPoint] = generateAndReport(points, u, uPrime, leftTangent, rightTangent, progressCallback);
  86. if (maxError < error) {
  87. return [bezCurve];
  88. }
  89. //If the development of the fitted curve grinds to a halt,
  90. //we abort this attempt (and try a shorter curve):
  91. else if(splitPoint === prevSplit) {
  92. let errChange = maxError/prevErr;
  93. if((errChange > .9999) && (errChange < 1.0001)) {
  94. break;
  95. }
  96. }
  97. prevErr = maxError;
  98. prevSplit = splitPoint;
  99. }
  100. }
  101. //Fitting failed -- split at max error point and fit recursively
  102. beziers = [];
  103. //To create a smooth transition from one curve segment to the next, we
  104. //calculate the line between the points directly before and after the
  105. //center, and use that as the tangent both to and from the center point.
  106. centerVector = maths.subtract(points[splitPoint-1], points[splitPoint+1]);
  107. //However, this won't work if they're the same point, because the line we
  108. //want to use as a tangent would be 0. Instead, we calculate the line from
  109. //that "double-point" to the center point, and use its tangent.
  110. if(centerVector.every(val => val === 0)) {
  111. //[x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660
  112. centerVector = maths.subtract(points[splitPoint-1], points[splitPoint]);
  113. [centerVector[0],centerVector[1]] = [-centerVector[1],centerVector[0]];
  114. }
  115. toCenterTangent = maths.normalize(centerVector);
  116. //To and from need to point in opposite directions:
  117. fromCenterTangent = maths.mulItems(toCenterTangent, -1);
  118. /*
  119. Note: An alternative to this "divide and conquer" recursion could be to always
  120. let new curve segments start by trying to go all the way to the end,
  121. instead of only to the end of the current subdivided polyline.
  122. That might let many segments fit a few points more, reducing the number of total segments.
  123. However, a few tests have shown that the segment reduction is insignificant
  124. (240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves on both),
  125. and the results take twice as many steps and milliseconds to finish,
  126. without looking any better than what we already have.
  127. */
  128. beziers = beziers.concat(fitCubic(points.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, progressCallback));
  129. beziers = beziers.concat(fitCubic(points.slice(splitPoint), fromCenterTangent, rightTangent, error, progressCallback));
  130. return beziers;
  131. };
  132. function generateAndReport(points, paramsOrig, paramsPrime, leftTangent, rightTangent, progressCallback) {
  133. var bezCurve, maxError, splitPoint;
  134. bezCurve = generateBezier(points, paramsPrime, leftTangent, rightTangent, progressCallback);
  135. //Find max deviation of points to fitted curve.
  136. //Here we always use the original parameters (from chordLengthParameterize()),
  137. //because we need to compare the current curve to the actual source polyline,
  138. //and not the currently iterated parameters which reparameterize() & generateBezier() use,
  139. //as those have probably drifted far away and may no longer be in ascending order.
  140. [maxError, splitPoint] = computeMaxError(points, bezCurve, paramsOrig);
  141. if(progressCallback) {
  142. progressCallback({
  143. bez: bezCurve,
  144. points: points,
  145. params: paramsOrig,
  146. maxErr: maxError,
  147. maxPoint: splitPoint,
  148. });
  149. }
  150. return [bezCurve, maxError, splitPoint];
  151. }
  152. /**
  153. * Use least-squares method to find Bezier control points for region.
  154. *
  155. * @param {Array<Array<Number>>} points - Array of digitized points
  156. * @param {Array<Number>} parameters - Parameter values for region
  157. * @param {Array<Number>} leftTangent - Unit tangent vector at start point
  158. * @param {Array<Number>} rightTangent - Unit tangent vector at end point
  159. * @returns {Array<Array<Number>>} Approximated Bezier curve: [first-point, control-point-1, control-point-2, second-point] where points are [x, y]
  160. */
  161. function generateBezier(points, parameters, leftTangent, rightTangent) {
  162. var bezCurve, //Bezier curve ctl pts
  163. A, a, //Precomputed rhs for eqn
  164. C, X, //Matrices C & X
  165. det_C0_C1, det_C0_X, det_X_C1, //Determinants of matrices
  166. alpha_l, alpha_r, //Alpha values, left and right
  167. epsilon, segLength,
  168. i, len, tmp, u, ux,
  169. firstPoint = points[0],
  170. lastPoint = points[points.length-1];
  171. bezCurve = [firstPoint, null, null, lastPoint];
  172. //console.log('gb', parameters.length);
  173. //Compute the A's
  174. A = maths.zeros_Xx2x2(parameters.length);
  175. for (i = 0, len = parameters.length; i < len; i++) {
  176. u = parameters[i];
  177. ux = 1 - u;
  178. a = A[i];
  179. a[0] = maths.mulItems(leftTangent, 3 * u * (ux*ux));
  180. a[1] = maths.mulItems(rightTangent, 3 * ux * (u*u));
  181. }
  182. //Create the C and X matrices
  183. C = [[0,0], [0,0]];
  184. X = [0,0];
  185. for (i = 0, len = points.length; i < len; i++) {
  186. u = parameters[i];
  187. a = A[i];
  188. C[0][0] += maths.dot(a[0], a[0]);
  189. C[0][1] += maths.dot(a[0], a[1]);
  190. C[1][0] += maths.dot(a[0], a[1]);
  191. C[1][1] += maths.dot(a[1], a[1]);
  192. tmp = maths.subtract(points[i], bezier.q([firstPoint, firstPoint, lastPoint, lastPoint], u));
  193. X[0] += maths.dot(a[0], tmp);
  194. X[1] += maths.dot(a[1], tmp);
  195. }
  196. //Compute the determinants of C and X
  197. det_C0_C1 = (C[0][0] * C[1][1]) - (C[1][0] * C[0][1]);
  198. det_C0_X = (C[0][0] * X[1] ) - (C[1][0] * X[0] );
  199. det_X_C1 = (X[0] * C[1][1]) - (X[1] * C[0][1]);
  200. //Finally, derive alpha values
  201. alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
  202. alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
  203. //If alpha negative, use the Wu/Barsky heuristic (see text).
  204. //If alpha is 0, you get coincident control points that lead to
  205. //divide by zero in any subsequent NewtonRaphsonRootFind() call.
  206. segLength = maths.vectorLen(maths.subtract(firstPoint, lastPoint));
  207. epsilon = 1.0e-6 * segLength;
  208. if (alpha_l < epsilon || alpha_r < epsilon) {
  209. //Fall back on standard (probably inaccurate) formula, and subdivide further if needed.
  210. bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, segLength / 3.0));
  211. bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, segLength / 3.0));
  212. } else {
  213. //First and last control points of the Bezier curve are
  214. //positioned exactly at the first and last data points
  215. //Control points 1 and 2 are positioned an alpha distance out
  216. //on the tangent vectors, left and right, respectively
  217. bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, alpha_l));
  218. bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, alpha_r));
  219. }
  220. return bezCurve;
  221. };
  222. /**
  223. * Given set of points and their parameterization, try to find a better parameterization.
  224. *
  225. * @param {Array<Array<Number>>} bezier - Current fitted curve
  226. * @param {Array<Array<Number>>} points - Array of digitized points
  227. * @param {Array<Number>} parameters - Current parameter values
  228. * @returns {Array<Number>} New parameter values
  229. */
  230. function reparameterize(bezier, points, parameters) {
  231. /*
  232. var j, len, point, results, u;
  233. results = [];
  234. for (j = 0, len = points.length; j < len; j++) {
  235. point = points[j], u = parameters[j];
  236. results.push(newtonRaphsonRootFind(bezier, point, u));
  237. }
  238. return results;
  239. //*/
  240. return parameters.map((p, i) => newtonRaphsonRootFind(bezier, points[i], p));
  241. };
  242. /**
  243. * Use Newton-Raphson iteration to find better root.
  244. *
  245. * @param {Array<Array<Number>>} bez - Current fitted curve
  246. * @param {Array<Number>} point - Digitized point
  247. * @param {Number} u - Parameter value for "P"
  248. * @returns {Number} New u
  249. */
  250. function newtonRaphsonRootFind(bez, point, u) {
  251. /*
  252. Newton's root finding algorithm calculates f(x)=0 by reiterating
  253. x_n+1 = x_n - f(x_n)/f'(x_n)
  254. We are trying to find curve parameter u for some point p that minimizes
  255. the distance from that point to the curve. Distance point to curve is d=q(u)-p.
  256. At minimum distance the point is perpendicular to the curve.
  257. We are solving
  258. f = q(u)-p * q'(u) = 0
  259. with
  260. f' = q'(u) * q'(u) + q(u)-p * q''(u)
  261. gives
  262. u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)|
  263. */
  264. var d = maths.subtract(bezier.q(bez, u), point),
  265. qprime = bezier.qprime(bez, u),
  266. numerator = maths.mulMatrix(d, qprime),
  267. denominator = maths.sum(maths.squareItems(qprime)) + 2 * maths.mulMatrix(d, bezier.qprimeprime(bez, u));
  268. if (denominator === 0) {
  269. return u;
  270. } else {
  271. return u - (numerator/denominator);
  272. }
  273. };
  274. /**
  275. * Assign parameter values to digitized points using relative distances between points.
  276. *
  277. * @param {Array<Array<Number>>} points - Array of digitized points
  278. * @returns {Array<Number>} Parameter values
  279. */
  280. function chordLengthParameterize(points) {
  281. var u = [], currU, prevU, prevP;
  282. points.forEach((p, i) => {
  283. currU = i ? prevU + maths.vectorLen(maths.subtract(p, prevP))
  284. : 0;
  285. u.push(currU);
  286. prevU = currU;
  287. prevP = p;
  288. })
  289. u = u.map(x => x/prevU);
  290. return u;
  291. };
  292. /**
  293. * Find the maximum squared distance of digitized points to fitted curve.
  294. *
  295. * @param {Array<Array<Number>>} points - Array of digitized points
  296. * @param {Array<Array<Number>>} bez - Fitted curve
  297. * @param {Array<Number>} parameters - Parameterization of points
  298. * @returns {Array<Number>} Maximum error (squared) and point of max error
  299. */
  300. function computeMaxError(points, bez, parameters) {
  301. var dist, //Current error
  302. maxDist, //Maximum error
  303. splitPoint, //Point of maximum error
  304. v, //Vector from point to curve
  305. i, count, point, t;
  306. maxDist = 0;
  307. splitPoint = Math.floor(points.length / 2);
  308. const t_distMap = mapTtoRelativeDistances(bez, 10);
  309. for (i = 0, count = points.length; i < count; i++) {
  310. point = points[i];
  311. //Find 't' for a point on the bez curve that's as close to 'point' as possible:
  312. t = find_t(bez, parameters[i], t_distMap, 10);
  313. v = maths.subtract(bezier.q(bez, t), point);
  314. dist = v[0]*v[0] + v[1]*v[1];
  315. if (dist > maxDist) {
  316. maxDist = dist;
  317. splitPoint = i;
  318. }
  319. }
  320. return [maxDist, splitPoint];
  321. };
  322. //Sample 't's and map them to relative distances along the curve:
  323. var mapTtoRelativeDistances = function (bez, B_parts) {
  324. var B_t_curr;
  325. var B_t_dist = [0];
  326. var B_t_prev = bez[0];
  327. var sumLen = 0;
  328. for (var i=1; i<=B_parts; i++) {
  329. B_t_curr = bezier.q(bez, i/B_parts);
  330. sumLen += maths.vectorLen(maths.subtract(B_t_curr, B_t_prev));
  331. B_t_dist.push(sumLen);
  332. B_t_prev = B_t_curr;
  333. }
  334. //Normalize B_length to the same interval as the parameter distances; 0 to 1:
  335. B_t_dist = B_t_dist.map(x => x/sumLen);
  336. return B_t_dist;
  337. };
  338. function find_t(bez, param, t_distMap, B_parts) {
  339. if(param < 0) { return 0; }
  340. if(param > 1) { return 1; }
  341. /*
  342. 'param' is a value between 0 and 1 telling us the relative position
  343. of a point on the source polyline (linearly from the start (0) to the end (1)).
  344. To see if a given curve - 'bez' - is a close approximation of the polyline,
  345. we compare such a poly-point to the point on the curve that's the same
  346. relative distance along the curve's length.
  347. But finding that curve-point takes a little work:
  348. There is a function "B(t)" to find points along a curve from the parametric parameter 't'
  349. (also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660
  350. http://pomax.github.io/bezierinfo/#explanation),
  351. but 't' isn't linear by length (http://gamedev.stackexchange.com/questions/105230).
  352. So, we sample some points along the curve using a handful of values for 't'.
  353. Then, we calculate the length between those samples via plain euclidean distance;
  354. B(t) concentrates the points around sharp turns, so this should give us a good-enough outline of the curve.
  355. Thus, for a given relative distance ('param'), we can now find an upper and lower value
  356. for the corresponding 't' by searching through those sampled distances.
  357. Finally, we just use linear interpolation to find a better value for the exact 't'.
  358. More info:
  359. http://gamedev.stackexchange.com/questions/105230/points-evenly-spaced-along-a-bezier-curve
  360. http://stackoverflow.com/questions/29438398/cheap-way-of-calculating-cubic-bezier-length
  361. http://steve.hollasch.net/cgindex/curves/cbezarclen.html
  362. https://github.com/retuxx/tinyspline
  363. */
  364. var lenMax, lenMin, tMax, tMin, t;
  365. //Find the two t-s that the current param distance lies between,
  366. //and then interpolate a somewhat accurate value for the exact t:
  367. for(var i = 1; i <= B_parts; i++) {
  368. if(param <= t_distMap[i]) {
  369. tMin = (i-1) / B_parts;
  370. tMax = i / B_parts;
  371. lenMin = t_distMap[i-1];
  372. lenMax = t_distMap[i];
  373. t = (param-lenMin)/(lenMax-lenMin) * (tMax-tMin) + tMin;
  374. break;
  375. }
  376. }
  377. return t;
  378. }
  379. /**
  380. * Creates a vector of length 1 which shows the direction from B to A
  381. */
  382. function createTangent(pointA, pointB) {
  383. return maths.normalize(maths.subtract(pointA, pointB));
  384. }
  385. /*
  386. Simplified versions of what we need from math.js
  387. Optimized for our input, which is only numbers and 1x2 arrays (i.e. [x, y] coordinates).
  388. */
  389. class maths {
  390. //zeros = logAndRun(math.zeros);
  391. static zeros_Xx2x2(x) {
  392. var zs = [];
  393. while(x--) { zs.push([0,0]); }
  394. return zs
  395. }
  396. //multiply = logAndRun(math.multiply);
  397. static mulItems(items, multiplier) {
  398. return items.map(x => x*multiplier);
  399. }
  400. static mulMatrix(m1, m2) {
  401. //https://en.wikipedia.org/wiki/Matrix_multiplication#Matrix_product_.28two_matrices.29
  402. //Simplified to only handle 1-dimensional matrices (i.e. arrays) of equal length:
  403. return m1.reduce((sum,x1,i) => sum + (x1*m2[i]), 0);
  404. }
  405. //Only used to subract to points (or at least arrays):
  406. // subtract = logAndRun(math.subtract);
  407. static subtract(arr1, arr2) {
  408. return arr1.map((x1, i) => x1 - arr2[i]);
  409. }
  410. //add = logAndRun(math.add);
  411. static addArrays(arr1, arr2) {
  412. return arr1.map((x1, i) => x1 + arr2[i]);
  413. }
  414. static addItems(items, addition) {
  415. return items.map(x => x+addition);
  416. }
  417. //var sum = logAndRun(math.sum);
  418. static sum(items) {
  419. return items.reduce((sum,x) => sum + x);
  420. }
  421. //chain = math.chain;
  422. //Only used on two arrays. The dot product is equal to the matrix product in this case:
  423. // dot = logAndRun(math.dot);
  424. static dot(m1, m2) {
  425. return maths.mulMatrix(m1, m2);
  426. }
  427. //https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm
  428. // var norm = logAndRun(math.norm);
  429. static vectorLen(v) {
  430. return Math.hypot(...v);
  431. }
  432. //math.divide = logAndRun(math.divide);
  433. static divItems(items, divisor) {
  434. return items.map(x => x/divisor);
  435. }
  436. //var dotPow = logAndRun(math.dotPow);
  437. static squareItems(items) {
  438. return items.map(x => x*x);
  439. }
  440. static normalize(v) {
  441. return this.divItems(v, this.vectorLen(v));
  442. }
  443. //Math.pow = logAndRun(Math.pow);
  444. }
  445. class bezier {
  446. //Evaluates cubic bezier at t, return point
  447. static q(ctrlPoly, t) {
  448. var tx = 1.0 - t;
  449. var pA = maths.mulItems( ctrlPoly[0], tx * tx * tx ),
  450. pB = maths.mulItems( ctrlPoly[1], 3 * tx * tx * t ),
  451. pC = maths.mulItems( ctrlPoly[2], 3 * tx * t * t ),
  452. pD = maths.mulItems( ctrlPoly[3], t * t * t );
  453. return maths.addArrays(maths.addArrays(pA, pB), maths.addArrays(pC, pD));
  454. }
  455. //Evaluates cubic bezier first derivative at t, return point
  456. static qprime(ctrlPoly, t) {
  457. var tx = 1.0 - t;
  458. var pA = maths.mulItems( maths.subtract(ctrlPoly[1], ctrlPoly[0]), 3 * tx * tx ),
  459. pB = maths.mulItems( maths.subtract(ctrlPoly[2], ctrlPoly[1]), 6 * tx * t ),
  460. pC = maths.mulItems( maths.subtract(ctrlPoly[3], ctrlPoly[2]), 3 * t * t );
  461. return maths.addArrays(maths.addArrays(pA, pB), pC);
  462. }
  463. //Evaluates cubic bezier second derivative at t, return point
  464. static qprimeprime(ctrlPoly, t) {
  465. return maths.addArrays(maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[2], maths.mulItems(ctrlPoly[1], 2)), ctrlPoly[0]), 6 * (1.0 - t) ),
  466. maths.mulItems( maths.addArrays(maths.subtract(ctrlPoly[3], maths.mulItems(ctrlPoly[2], 2)), ctrlPoly[1]), 6 * t ));
  467. }
  468. }
  469. module.exports = fitCurve;
  470. module.exports.fitCubic = fitCubic;
  471. module.exports.createTangent = createTangent;