/src/path/PathFitter.js

http://github.com/paperjs/paper.js · JavaScript · 282 lines · 198 code · 15 blank · 69 comment · 32 complexity · 64972d73dd87197802479b2676e2e4cb MD5 · raw file

  1. /*
  2. * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
  3. * http://paperjs.org/
  4. *
  5. * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey
  6. * http://juerglehni.com/ & https://puckey.studio/
  7. *
  8. * Distributed under the MIT license. See LICENSE file for details.
  9. *
  10. * All rights reserved.
  11. */
  12. // An Algorithm for Automatically Fitting Digitized Curves
  13. // by Philip J. Schneider
  14. // from "Graphics Gems", Academic Press, 1990
  15. // Modifications and optimizations of original algorithm by Jürg Lehni.
  16. /**
  17. * @name PathFitter
  18. * @class
  19. * @private
  20. */
  21. var PathFitter = Base.extend({
  22. initialize: function(path) {
  23. var points = this.points = [],
  24. segments = path._segments,
  25. closed = path._closed;
  26. // Copy over points from path and filter out adjacent duplicates.
  27. for (var i = 0, prev, l = segments.length; i < l; i++) {
  28. var point = segments[i].point;
  29. if (!prev || !prev.equals(point)) {
  30. points.push(prev = point.clone());
  31. }
  32. }
  33. // We need to duplicate the first and last segment when simplifying a
  34. // closed path.
  35. if (closed) {
  36. points.unshift(points[points.length - 1]);
  37. points.push(points[1]); // The point previously at index 0 is now 1.
  38. }
  39. this.closed = closed;
  40. },
  41. fit: function(error) {
  42. var points = this.points,
  43. length = points.length,
  44. segments = null;
  45. if (length > 0) {
  46. // To support reducing paths with multiple points in the same place
  47. // to one segment:
  48. segments = [new Segment(points[0])];
  49. if (length > 1) {
  50. this.fitCubic(segments, error, 0, length - 1,
  51. // Left Tangent
  52. points[1].subtract(points[0]),
  53. // Right Tangent
  54. points[length - 2].subtract(points[length - 1]));
  55. // Remove the duplicated segments for closed paths again.
  56. if (this.closed) {
  57. segments.shift();
  58. segments.pop();
  59. }
  60. }
  61. }
  62. return segments;
  63. },
  64. // Fit a Bezier curve to a (sub)set of digitized points
  65. fitCubic: function(segments, error, first, last, tan1, tan2) {
  66. var points = this.points;
  67. // Use heuristic if region only has two points in it
  68. if (last - first === 1) {
  69. var pt1 = points[first],
  70. pt2 = points[last],
  71. dist = pt1.getDistance(pt2) / 3;
  72. this.addCurve(segments, [pt1, pt1.add(tan1.normalize(dist)),
  73. pt2.add(tan2.normalize(dist)), pt2]);
  74. return;
  75. }
  76. // Parameterize points, and attempt to fit curve
  77. var uPrime = this.chordLengthParameterize(first, last),
  78. maxError = Math.max(error, error * error),
  79. split,
  80. parametersInOrder = true;
  81. // Try 4 iterations
  82. for (var i = 0; i <= 4; i++) {
  83. var curve = this.generateBezier(first, last, uPrime, tan1, tan2);
  84. // Find max deviation of points to fitted curve
  85. var max = this.findMaxError(first, last, curve, uPrime);
  86. if (max.error < error && parametersInOrder) {
  87. this.addCurve(segments, curve);
  88. return;
  89. }
  90. split = max.index;
  91. // If error not too large, try reparameterization and iteration
  92. if (max.error >= maxError)
  93. break;
  94. parametersInOrder = this.reparameterize(first, last, uPrime, curve);
  95. maxError = max.error;
  96. }
  97. // Fitting failed -- split at max error point and fit recursively
  98. var tanCenter = points[split - 1].subtract(points[split + 1]);
  99. this.fitCubic(segments, error, first, split, tan1, tanCenter);
  100. this.fitCubic(segments, error, split, last, tanCenter.negate(), tan2);
  101. },
  102. addCurve: function(segments, curve) {
  103. var prev = segments[segments.length - 1];
  104. prev.setHandleOut(curve[1].subtract(curve[0]));
  105. segments.push(new Segment(curve[3], curve[2].subtract(curve[3])));
  106. },
  107. // Use least-squares method to find Bezier control points for region.
  108. generateBezier: function(first, last, uPrime, tan1, tan2) {
  109. var epsilon = /*#=*/Numerical.EPSILON,
  110. abs = Math.abs,
  111. points = this.points,
  112. pt1 = points[first],
  113. pt2 = points[last],
  114. // Create the C and X matrices
  115. C = [[0, 0], [0, 0]],
  116. X = [0, 0];
  117. for (var i = 0, l = last - first + 1; i < l; i++) {
  118. var u = uPrime[i],
  119. t = 1 - u,
  120. b = 3 * u * t,
  121. b0 = t * t * t,
  122. b1 = b * t,
  123. b2 = b * u,
  124. b3 = u * u * u,
  125. a1 = tan1.normalize(b1),
  126. a2 = tan2.normalize(b2),
  127. tmp = points[first + i]
  128. .subtract(pt1.multiply(b0 + b1))
  129. .subtract(pt2.multiply(b2 + b3));
  130. C[0][0] += a1.dot(a1);
  131. C[0][1] += a1.dot(a2);
  132. // C[1][0] += a1.dot(a2);
  133. C[1][0] = C[0][1];
  134. C[1][1] += a2.dot(a2);
  135. X[0] += a1.dot(tmp);
  136. X[1] += a2.dot(tmp);
  137. }
  138. // Compute the determinants of C and X
  139. var detC0C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1],
  140. alpha1,
  141. alpha2;
  142. if (abs(detC0C1) > epsilon) {
  143. // Kramer's rule
  144. var detC0X = C[0][0] * X[1] - C[1][0] * X[0],
  145. detXC1 = X[0] * C[1][1] - X[1] * C[0][1];
  146. // Derive alpha values
  147. alpha1 = detXC1 / detC0C1;
  148. alpha2 = detC0X / detC0C1;
  149. } else {
  150. // Matrix is under-determined, try assuming alpha1 == alpha2
  151. var c0 = C[0][0] + C[0][1],
  152. c1 = C[1][0] + C[1][1];
  153. alpha1 = alpha2 = abs(c0) > epsilon ? X[0] / c0
  154. : abs(c1) > epsilon ? X[1] / c1
  155. : 0;
  156. }
  157. // If alpha negative, use the Wu/Barsky heuristic (see text)
  158. // (if alpha is 0, you get coincident control points that lead to
  159. // divide by zero in any subsequent NewtonRaphsonRootFind() call.
  160. var segLength = pt2.getDistance(pt1),
  161. eps = epsilon * segLength,
  162. handle1,
  163. handle2;
  164. if (alpha1 < eps || alpha2 < eps) {
  165. // fall back on standard (probably inaccurate) formula,
  166. // and subdivide further if needed.
  167. alpha1 = alpha2 = segLength / 3;
  168. } else {
  169. // Check if the found control points are in the right order when
  170. // projected onto the line through pt1 and pt2.
  171. var line = pt2.subtract(pt1);
  172. // Control points 1 and 2 are positioned an alpha distance out
  173. // on the tangent vectors, left and right, respectively
  174. handle1 = tan1.normalize(alpha1);
  175. handle2 = tan2.normalize(alpha2);
  176. if (handle1.dot(line) - handle2.dot(line) > segLength * segLength) {
  177. // Fall back to the Wu/Barsky heuristic above.
  178. alpha1 = alpha2 = segLength / 3;
  179. handle1 = handle2 = null; // Force recalculation
  180. }
  181. }
  182. // First and last control points of the Bezier curve are
  183. // positioned exactly at the first and last data points
  184. return [pt1,
  185. pt1.add(handle1 || tan1.normalize(alpha1)),
  186. pt2.add(handle2 || tan2.normalize(alpha2)),
  187. pt2];
  188. },
  189. // Given set of points and their parameterization, try to find
  190. // a better parameterization.
  191. reparameterize: function(first, last, u, curve) {
  192. for (var i = first; i <= last; i++) {
  193. u[i - first] = this.findRoot(curve, this.points[i], u[i - first]);
  194. }
  195. // Detect if the new parameterization has reordered the points.
  196. // In that case, we would fit the points of the path in the wrong order.
  197. for (var i = 1, l = u.length; i < l; i++) {
  198. if (u[i] <= u[i - 1])
  199. return false;
  200. }
  201. return true;
  202. },
  203. // Use Newton-Raphson iteration to find better root.
  204. findRoot: function(curve, point, u) {
  205. var curve1 = [],
  206. curve2 = [];
  207. // Generate control vertices for Q'
  208. for (var i = 0; i <= 2; i++) {
  209. curve1[i] = curve[i + 1].subtract(curve[i]).multiply(3);
  210. }
  211. // Generate control vertices for Q''
  212. for (var i = 0; i <= 1; i++) {
  213. curve2[i] = curve1[i + 1].subtract(curve1[i]).multiply(2);
  214. }
  215. // Compute Q(u), Q'(u) and Q''(u)
  216. var pt = this.evaluate(3, curve, u),
  217. pt1 = this.evaluate(2, curve1, u),
  218. pt2 = this.evaluate(1, curve2, u),
  219. diff = pt.subtract(point),
  220. df = pt1.dot(pt1) + diff.dot(pt2);
  221. // u = u - f(u) / f'(u)
  222. return Numerical.isMachineZero(df) ? u : u - diff.dot(pt1) / df;
  223. },
  224. // Evaluate a bezier curve at a particular parameter value
  225. evaluate: function(degree, curve, t) {
  226. // Copy array
  227. var tmp = curve.slice();
  228. // Triangle computation
  229. for (var i = 1; i <= degree; i++) {
  230. for (var j = 0; j <= degree - i; j++) {
  231. tmp[j] = tmp[j].multiply(1 - t).add(tmp[j + 1].multiply(t));
  232. }
  233. }
  234. return tmp[0];
  235. },
  236. // Assign parameter values to digitized points
  237. // using relative distances between points.
  238. chordLengthParameterize: function(first, last) {
  239. var u = [0];
  240. for (var i = first + 1; i <= last; i++) {
  241. u[i - first] = u[i - first - 1]
  242. + this.points[i].getDistance(this.points[i - 1]);
  243. }
  244. for (var i = 1, m = last - first; i <= m; i++) {
  245. u[i] /= u[m];
  246. }
  247. return u;
  248. },
  249. // Find the maximum squared distance of digitized points to fitted curve.
  250. findMaxError: function(first, last, curve, u) {
  251. var index = Math.floor((last - first + 1) / 2),
  252. maxDist = 0;
  253. for (var i = first + 1; i < last; i++) {
  254. var P = this.evaluate(3, curve, u[i - first]);
  255. var v = P.subtract(this.points[i]);
  256. var dist = v.x * v.x + v.y * v.y; // squared
  257. if (dist >= maxDist) {
  258. maxDist = dist;
  259. index = i;
  260. }
  261. }
  262. return {
  263. error: maxDist,
  264. index: index
  265. };
  266. }
  267. });