jsBezier-0.6.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /**
  2. * jsBezier-0.6
  3. *
  4. * Copyright (c) 2010 - 2013 Simon Porritt (simon.porritt@gmail.com)
  5. *
  6. * licensed under the MIT license.
  7. *
  8. * a set of Bezier curve functions that deal with Beziers, used by jsPlumb, and perhaps useful for other people. These functions work with Bezier
  9. * curves of arbitrary degree.
  10. *
  11. * - functions are all in the 'jsBezier' namespace.
  12. *
  13. * - all input points should be in the format {x:.., y:..}. all output points are in this format too.
  14. *
  15. * - all input curves should be in the format [ {x:.., y:..}, {x:.., y:..}, {x:.., y:..}, {x:.., y:..} ]
  16. *
  17. * - 'location' as used as an input here refers to a decimal in the range 0-1 inclusive, which indicates a point some proportion along the length
  18. * of the curve. location as output has the same format and meaning.
  19. *
  20. *
  21. * Function List:
  22. * --------------
  23. *
  24. * distanceFromCurve(point, curve)
  25. *
  26. * Calculates the distance that the given point lies from the given Bezier. Note that it is computed relative to the center of the Bezier,
  27. * so if you have stroked the curve with a wide pen you may wish to take that into account! The distance returned is relative to the values
  28. * of the curve and the point - it will most likely be pixels.
  29. *
  30. * gradientAtPoint(curve, location)
  31. *
  32. * Calculates the gradient to the curve at the given location, as a decimal between 0 and 1 inclusive.
  33. *
  34. * gradientAtPointAlongCurveFrom (curve, location)
  35. *
  36. * Calculates the gradient at the point on the given curve that is 'distance' units from location.
  37. *
  38. * nearestPointOnCurve(point, curve)
  39. *
  40. * Calculates the nearest point to the given point on the given curve. The return value of this is a JS object literal, containing both the
  41. *point's coordinates and also the 'location' of the point (see above), for example: { point:{x:551,y:150}, location:0.263365 }.
  42. *
  43. * pointOnCurve(curve, location)
  44. *
  45. * Calculates the coordinates of the point on the given Bezier curve at the given location.
  46. *
  47. * pointAlongCurveFrom(curve, location, distance)
  48. *
  49. * Calculates the coordinates of the point on the given curve that is 'distance' units from location. 'distance' should be in the same coordinate
  50. * space as that used to construct the Bezier curve. For an HTML Canvas usage, for example, distance would be a measure of pixels.
  51. *
  52. * locationAlongCurveFrom(curve, location, distance)
  53. *
  54. * Calculates the location on the given curve that is 'distance' units from location. 'distance' should be in the same coordinate
  55. * space as that used to construct the Bezier curve. For an HTML Canvas usage, for example, distance would be a measure of pixels.
  56. *
  57. * perpendicularToCurveAt(curve, location, length, distance)
  58. *
  59. * Calculates the perpendicular to the given curve at the given location. length is the length of the line you wish for (it will be centered
  60. * on the point at 'location'). distance is optional, and allows you to specify a point along the path from the given location as the center of
  61. * the perpendicular returned. The return value of this is an array of two points: [ {x:...,y:...}, {x:...,y:...} ].
  62. *
  63. *
  64. */
  65. (function() {
  66. if(typeof Math.sgn == "undefined") {
  67. Math.sgn = function(x) { return x == 0 ? 0 : x > 0 ? 1 :-1; };
  68. }
  69. var Vectors = {
  70. subtract : function(v1, v2) { return {x:v1.x - v2.x, y:v1.y - v2.y }; },
  71. dotProduct : function(v1, v2) { return (v1.x * v2.x) + (v1.y * v2.y); },
  72. square : function(v) { return Math.sqrt((v.x * v.x) + (v.y * v.y)); },
  73. scale : function(v, s) { return {x:v.x * s, y:v.y * s }; }
  74. },
  75. maxRecursion = 64,
  76. flatnessTolerance = Math.pow(2.0,-maxRecursion-1);
  77. /**
  78. * Calculates the distance that the point lies from the curve.
  79. *
  80. * @param point a point in the form {x:567, y:3342}
  81. * @param curve a Bezier curve in the form [{x:..., y:...}, {x:..., y:...}, {x:..., y:...}, {x:..., y:...}]. note that this is currently
  82. * hardcoded to assume cubiz beziers, but would be better off supporting any degree.
  83. * @return a JS object literal containing location and distance, for example: {location:0.35, distance:10}. Location is analogous to the location
  84. * argument you pass to the pointOnPath function: it is a ratio of distance travelled along the curve. Distance is the distance in pixels from
  85. * the point to the curve.
  86. */
  87. var _distanceFromCurve = function(point, curve) {
  88. var candidates = [],
  89. w = _convertToBezier(point, curve),
  90. degree = curve.length - 1, higherDegree = (2 * degree) - 1,
  91. numSolutions = _findRoots(w, higherDegree, candidates, 0),
  92. v = Vectors.subtract(point, curve[0]), dist = Vectors.square(v), t = 0.0;
  93. for (var i = 0; i < numSolutions; i++) {
  94. v = Vectors.subtract(point, _bezier(curve, degree, candidates[i], null, null));
  95. var newDist = Vectors.square(v);
  96. if (newDist < dist) {
  97. dist = newDist;
  98. t = candidates[i];
  99. }
  100. }
  101. v = Vectors.subtract(point, curve[degree]);
  102. newDist = Vectors.square(v);
  103. if (newDist < dist) {
  104. dist = newDist;
  105. t = 1.0;
  106. }
  107. return {location:t, distance:dist};
  108. };
  109. /**
  110. * finds the nearest point on the curve to the given point.
  111. */
  112. var _nearestPointOnCurve = function(point, curve) {
  113. var td = _distanceFromCurve(point, curve);
  114. return {point:_bezier(curve, curve.length - 1, td.location, null, null), location:td.location};
  115. };
  116. var _convertToBezier = function(point, curve) {
  117. var degree = curve.length - 1, higherDegree = (2 * degree) - 1,
  118. c = [], d = [], cdTable = [], w = [],
  119. z = [ [1.0, 0.6, 0.3, 0.1], [0.4, 0.6, 0.6, 0.4], [0.1, 0.3, 0.6, 1.0] ];
  120. for (var i = 0; i <= degree; i++) c[i] = Vectors.subtract(curve[i], point);
  121. for (var i = 0; i <= degree - 1; i++) {
  122. d[i] = Vectors.subtract(curve[i+1], curve[i]);
  123. d[i] = Vectors.scale(d[i], 3.0);
  124. }
  125. for (var row = 0; row <= degree - 1; row++) {
  126. for (var column = 0; column <= degree; column++) {
  127. if (!cdTable[row]) cdTable[row] = [];
  128. cdTable[row][column] = Vectors.dotProduct(d[row], c[column]);
  129. }
  130. }
  131. for (i = 0; i <= higherDegree; i++) {
  132. if (!w[i]) w[i] = [];
  133. w[i].y = 0.0;
  134. w[i].x = parseFloat(i) / higherDegree;
  135. }
  136. var n = degree, m = degree-1;
  137. for (var k = 0; k <= n + m; k++) {
  138. var lb = Math.max(0, k - m),
  139. ub = Math.min(k, n);
  140. for (i = lb; i <= ub; i++) {
  141. j = k - i;
  142. w[i+j].y += cdTable[j][i] * z[j][i];
  143. }
  144. }
  145. return w;
  146. };
  147. /**
  148. * counts how many roots there are.
  149. */
  150. var _findRoots = function(w, degree, t, depth) {
  151. var left = [], right = [],
  152. left_count, right_count,
  153. left_t = [], right_t = [];
  154. switch (_getCrossingCount(w, degree)) {
  155. case 0 : {
  156. return 0;
  157. }
  158. case 1 : {
  159. if (depth >= maxRecursion) {
  160. t[0] = (w[0].x + w[degree].x) / 2.0;
  161. return 1;
  162. }
  163. if (_isFlatEnough(w, degree)) {
  164. t[0] = _computeXIntercept(w, degree);
  165. return 1;
  166. }
  167. break;
  168. }
  169. }
  170. _bezier(w, degree, 0.5, left, right);
  171. left_count = _findRoots(left, degree, left_t, depth+1);
  172. right_count = _findRoots(right, degree, right_t, depth+1);
  173. for (var i = 0; i < left_count; i++) t[i] = left_t[i];
  174. for (var i = 0; i < right_count; i++) t[i+left_count] = right_t[i];
  175. return (left_count+right_count);
  176. };
  177. var _getCrossingCount = function(curve, degree) {
  178. var n_crossings = 0, sign, old_sign;
  179. sign = old_sign = Math.sgn(curve[0].y);
  180. for (var i = 1; i <= degree; i++) {
  181. sign = Math.sgn(curve[i].y);
  182. if (sign != old_sign) n_crossings++;
  183. old_sign = sign;
  184. }
  185. return n_crossings;
  186. };
  187. var _isFlatEnough = function(curve, degree) {
  188. var error,
  189. intercept_1, intercept_2, left_intercept, right_intercept,
  190. a, b, c, det, dInv, a1, b1, c1, a2, b2, c2;
  191. a = curve[0].y - curve[degree].y;
  192. b = curve[degree].x - curve[0].x;
  193. c = curve[0].x * curve[degree].y - curve[degree].x * curve[0].y;
  194. var max_distance_above = max_distance_below = 0.0;
  195. for (var i = 1; i < degree; i++) {
  196. var value = a * curve[i].x + b * curve[i].y + c;
  197. if (value > max_distance_above)
  198. max_distance_above = value;
  199. else if (value < max_distance_below)
  200. max_distance_below = value;
  201. }
  202. a1 = 0.0; b1 = 1.0; c1 = 0.0; a2 = a; b2 = b;
  203. c2 = c - max_distance_above;
  204. det = a1 * b2 - a2 * b1;
  205. dInv = 1.0/det;
  206. intercept_1 = (b1 * c2 - b2 * c1) * dInv;
  207. a2 = a; b2 = b; c2 = c - max_distance_below;
  208. det = a1 * b2 - a2 * b1;
  209. dInv = 1.0/det;
  210. intercept_2 = (b1 * c2 - b2 * c1) * dInv;
  211. left_intercept = Math.min(intercept_1, intercept_2);
  212. right_intercept = Math.max(intercept_1, intercept_2);
  213. error = right_intercept - left_intercept;
  214. return (error < flatnessTolerance)? 1 : 0;
  215. };
  216. var _computeXIntercept = function(curve, degree) {
  217. var XLK = 1.0, YLK = 0.0,
  218. XNM = curve[degree].x - curve[0].x, YNM = curve[degree].y - curve[0].y,
  219. XMK = curve[0].x - 0.0, YMK = curve[0].y - 0.0,
  220. det = XNM*YLK - YNM*XLK, detInv = 1.0/det,
  221. S = (XNM*YMK - YNM*XMK) * detInv;
  222. return 0.0 + XLK * S;
  223. };
  224. var _bezier = function(curve, degree, t, left, right) {
  225. var temp = [[]];
  226. for (var j =0; j <= degree; j++) temp[0][j] = curve[j];
  227. for (var i = 1; i <= degree; i++) {
  228. for (var j =0 ; j <= degree - i; j++) {
  229. if (!temp[i]) temp[i] = [];
  230. if (!temp[i][j]) temp[i][j] = {};
  231. temp[i][j].x = (1.0 - t) * temp[i-1][j].x + t * temp[i-1][j+1].x;
  232. temp[i][j].y = (1.0 - t) * temp[i-1][j].y + t * temp[i-1][j+1].y;
  233. }
  234. }
  235. if (left != null)
  236. for (j = 0; j <= degree; j++) left[j] = temp[j][0];
  237. if (right != null)
  238. for (j = 0; j <= degree; j++) right[j] = temp[degree-j][j];
  239. return (temp[degree][0]);
  240. };
  241. var _curveFunctionCache = {};
  242. var _getCurveFunctions = function(order) {
  243. var fns = _curveFunctionCache[order];
  244. if (!fns) {
  245. fns = [];
  246. var f_term = function() { return function(t) { return Math.pow(t, order); }; },
  247. l_term = function() { return function(t) { return Math.pow((1-t), order); }; },
  248. c_term = function(c) { return function(t) { return c; }; },
  249. t_term = function() { return function(t) { return t; }; },
  250. one_minus_t_term = function() { return function(t) { return 1-t; }; },
  251. _termFunc = function(terms) {
  252. return function(t) {
  253. var p = 1;
  254. for (var i = 0; i < terms.length; i++) p = p * terms[i](t);
  255. return p;
  256. };
  257. };
  258. fns.push(new f_term()); // first is t to the power of the curve order
  259. for (var i = 1; i < order; i++) {
  260. var terms = [new c_term(order)];
  261. for (var j = 0 ; j < (order - i); j++) terms.push(new t_term());
  262. for (var j = 0 ; j < i; j++) terms.push(new one_minus_t_term());
  263. fns.push(new _termFunc(terms));
  264. }
  265. fns.push(new l_term()); // last is (1-t) to the power of the curve order
  266. _curveFunctionCache[order] = fns;
  267. }
  268. return fns;
  269. };
  270. /**
  271. * calculates a point on the curve, for a Bezier of arbitrary order.
  272. * @param curve an array of control points, eg [{x:10,y:20}, {x:50,y:50}, {x:100,y:100}, {x:120,y:100}]. For a cubic bezier this should have four points.
  273. * @param location a decimal indicating the distance along the curve the point should be located at. this is the distance along the curve as it travels, taking the way it bends into account. should be a number from 0 to 1, inclusive.
  274. */
  275. var _pointOnPath = function(curve, location) {
  276. var cc = _getCurveFunctions(curve.length - 1),
  277. _x = 0, _y = 0;
  278. for (var i = 0; i < curve.length ; i++) {
  279. _x = _x + (curve[i].x * cc[i](location));
  280. _y = _y + (curve[i].y * cc[i](location));
  281. }
  282. return {x:_x, y:_y};
  283. };
  284. var _dist = function(p1,p2) {
  285. return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
  286. };
  287. var _isPoint = function(curve) {
  288. return curve[0].x == curve[1].x && curve[0].y == curve[1].y;
  289. };
  290. /**
  291. * finds the point that is 'distance' along the path from 'location'. this method returns both the x,y location of the point and also
  292. * its 'location' (proportion of travel along the path); the method below - _pointAlongPathFrom - calls this method and just returns the
  293. * point.
  294. */
  295. var _pointAlongPath = function(curve, location, distance) {
  296. if (_isPoint(curve)) {
  297. return {
  298. point:curve[0],
  299. location:location
  300. };
  301. }
  302. var prev = _pointOnPath(curve, location),
  303. tally = 0,
  304. curLoc = location,
  305. direction = distance > 0 ? 1 : -1,
  306. cur = null;
  307. while (tally < Math.abs(distance)) {
  308. curLoc += (0.005 * direction);
  309. cur = _pointOnPath(curve, curLoc);
  310. tally += _dist(cur, prev);
  311. prev = cur;
  312. }
  313. return {point:cur, location:curLoc};
  314. };
  315. var _length = function(curve) {
  316. if (_isPoint(curve)) return 0;
  317. var prev = _pointOnPath(curve, 0),
  318. tally = 0,
  319. curLoc = 0,
  320. direction = 1,
  321. cur = null;
  322. while (curLoc < 1) {
  323. curLoc += (0.005 * direction);
  324. cur = _pointOnPath(curve, curLoc);
  325. tally += _dist(cur, prev);
  326. prev = cur;
  327. }
  328. return tally;
  329. };
  330. /**
  331. * finds the point that is 'distance' along the path from 'location'.
  332. */
  333. var _pointAlongPathFrom = function(curve, location, distance) {
  334. return _pointAlongPath(curve, location, distance).point;
  335. };
  336. /**
  337. * finds the location that is 'distance' along the path from 'location'.
  338. */
  339. var _locationAlongPathFrom = function(curve, location, distance) {
  340. return _pointAlongPath(curve, location, distance).location;
  341. };
  342. /**
  343. * returns the gradient of the curve at the given location, which is a decimal between 0 and 1 inclusive.
  344. *
  345. * thanks // http://bimixual.org/AnimationLibrary/beziertangents.html
  346. */
  347. var _gradientAtPoint = function(curve, location) {
  348. var p1 = _pointOnPath(curve, location),
  349. p2 = _pointOnPath(curve.slice(0, curve.length - 1), location),
  350. dy = p2.y - p1.y, dx = p2.x - p1.x;
  351. return dy == 0 ? Infinity : Math.atan(dy / dx);
  352. };
  353. /**
  354. returns the gradient of the curve at the point which is 'distance' from the given location.
  355. if this point is greater than location 1, the gradient at location 1 is returned.
  356. if this point is less than location 0, the gradient at location 0 is returned.
  357. */
  358. var _gradientAtPointAlongPathFrom = function(curve, location, distance) {
  359. var p = _pointAlongPath(curve, location, distance);
  360. if (p.location > 1) p.location = 1;
  361. if (p.location < 0) p.location = 0;
  362. return _gradientAtPoint(curve, p.location);
  363. };
  364. /**
  365. * calculates a line that is 'length' pixels long, perpendicular to, and centered on, the path at 'distance' pixels from the given location.
  366. * if distance is not supplied, the perpendicular for the given location is computed (ie. we set distance to zero).
  367. */
  368. var _perpendicularToPathAt = function(curve, location, length, distance) {
  369. distance = distance == null ? 0 : distance;
  370. var p = _pointAlongPath(curve, location, distance),
  371. m = _gradientAtPoint(curve, p.location),
  372. _theta2 = Math.atan(-1 / m),
  373. y = length / 2 * Math.sin(_theta2),
  374. x = length / 2 * Math.cos(_theta2);
  375. return [{x:p.point.x + x, y:p.point.y + y}, {x:p.point.x - x, y:p.point.y - y}];
  376. };
  377. var jsBezier = window.jsBezier = {
  378. distanceFromCurve : _distanceFromCurve,
  379. gradientAtPoint : _gradientAtPoint,
  380. gradientAtPointAlongCurveFrom : _gradientAtPointAlongPathFrom,
  381. nearestPointOnCurve : _nearestPointOnCurve,
  382. pointOnCurve : _pointOnPath,
  383. pointAlongCurveFrom : _pointAlongPathFrom,
  384. perpendicularToCurveAt : _perpendicularToPathAt,
  385. locationAlongCurveFrom:_locationAlongPathFrom,
  386. getLength:_length
  387. };
  388. })();