PolynomialBezier.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. function floatEqual(a, b) {
  2. return Math.abs(a - b) * 100000 <= Math.min(Math.abs(a), Math.abs(b));
  3. }
  4. function floatZero(f) {
  5. return Math.abs(f) <= 0.00001;
  6. }
  7. function lerp(p0, p1, amount) {
  8. return p0 * (1 - amount) + p1 * amount;
  9. }
  10. function lerpPoint(p0, p1, amount) {
  11. return [lerp(p0[0], p1[0], amount), lerp(p0[1], p1[1], amount)];
  12. }
  13. function quadRoots(a, b, c) {
  14. // no root
  15. if (a === 0) return [];
  16. var s = b * b - 4 * a * c;
  17. // Complex roots
  18. if (s < 0) return [];
  19. var singleRoot = -b / (2 * a);
  20. // 1 root
  21. if (s === 0) return [singleRoot];
  22. var delta = Math.sqrt(s) / (2 * a);
  23. // 2 roots
  24. return [singleRoot - delta, singleRoot + delta];
  25. }
  26. function polynomialCoefficients(p0, p1, p2, p3) {
  27. return [
  28. -p0 + 3 * p1 - 3 * p2 + p3,
  29. 3 * p0 - 6 * p1 + 3 * p2,
  30. -3 * p0 + 3 * p1,
  31. p0,
  32. ];
  33. }
  34. function singlePoint(p) {
  35. return new PolynomialBezier(p, p, p, p, false);
  36. }
  37. function PolynomialBezier(p0, p1, p2, p3, linearize) {
  38. if (linearize && pointEqual(p0, p1)) {
  39. p1 = lerpPoint(p0, p3, 1 / 3);
  40. }
  41. if (linearize && pointEqual(p2, p3)) {
  42. p2 = lerpPoint(p0, p3, 2 / 3);
  43. }
  44. var coeffx = polynomialCoefficients(p0[0], p1[0], p2[0], p3[0]);
  45. var coeffy = polynomialCoefficients(p0[1], p1[1], p2[1], p3[1]);
  46. this.a = [coeffx[0], coeffy[0]];
  47. this.b = [coeffx[1], coeffy[1]];
  48. this.c = [coeffx[2], coeffy[2]];
  49. this.d = [coeffx[3], coeffy[3]];
  50. this.points = [p0, p1, p2, p3];
  51. }
  52. PolynomialBezier.prototype.point = function (t) {
  53. return [
  54. (((this.a[0] * t) + this.b[0]) * t + this.c[0]) * t + this.d[0],
  55. (((this.a[1] * t) + this.b[1]) * t + this.c[1]) * t + this.d[1],
  56. ];
  57. };
  58. PolynomialBezier.prototype.derivative = function (t) {
  59. return [
  60. (3 * t * this.a[0] + 2 * this.b[0]) * t + this.c[0],
  61. (3 * t * this.a[1] + 2 * this.b[1]) * t + this.c[1],
  62. ];
  63. };
  64. PolynomialBezier.prototype.tangentAngle = function (t) {
  65. var p = this.derivative(t);
  66. return Math.atan2(p[1], p[0]);
  67. };
  68. PolynomialBezier.prototype.normalAngle = function (t) {
  69. var p = this.derivative(t);
  70. return Math.atan2(p[0], p[1]);
  71. };
  72. PolynomialBezier.prototype.inflectionPoints = function () {
  73. var denom = this.a[1] * this.b[0] - this.a[0] * this.b[1];
  74. if (floatZero(denom)) return [];
  75. var tcusp = (-0.5 * (this.a[1] * this.c[0] - this.a[0] * this.c[1])) / denom;
  76. var square = tcusp * tcusp - ((1 / 3) * (this.b[1] * this.c[0] - this.b[0] * this.c[1])) / denom;
  77. if (square < 0) return [];
  78. var root = Math.sqrt(square);
  79. if (floatZero(root)) {
  80. if (root > 0 && root < 1) return [tcusp];
  81. return [];
  82. }
  83. return [tcusp - root, tcusp + root].filter(function (r) { return r > 0 && r < 1; });
  84. };
  85. PolynomialBezier.prototype.split = function (t) {
  86. if (t <= 0) return [singlePoint(this.points[0]), this];
  87. if (t >= 1) return [this, singlePoint(this.points[this.points.length - 1])];
  88. var p10 = lerpPoint(this.points[0], this.points[1], t);
  89. var p11 = lerpPoint(this.points[1], this.points[2], t);
  90. var p12 = lerpPoint(this.points[2], this.points[3], t);
  91. var p20 = lerpPoint(p10, p11, t);
  92. var p21 = lerpPoint(p11, p12, t);
  93. var p3 = lerpPoint(p20, p21, t);
  94. return [
  95. new PolynomialBezier(this.points[0], p10, p20, p3, true),
  96. new PolynomialBezier(p3, p21, p12, this.points[3], true),
  97. ];
  98. };
  99. function extrema(bez, comp) {
  100. var min = bez.points[0][comp];
  101. var max = bez.points[bez.points.length - 1][comp];
  102. if (min > max) {
  103. var e = max;
  104. max = min;
  105. min = e;
  106. }
  107. // Derivative roots to find min/max
  108. var f = quadRoots(3 * bez.a[comp], 2 * bez.b[comp], bez.c[comp]);
  109. for (var i = 0; i < f.length; i += 1) {
  110. if (f[i] > 0 && f[i] < 1) {
  111. var val = bez.point(f[i])[comp];
  112. if (val < min) min = val;
  113. else if (val > max) max = val;
  114. }
  115. }
  116. return {
  117. min: min,
  118. max: max,
  119. };
  120. }
  121. PolynomialBezier.prototype.bounds = function () {
  122. return {
  123. x: extrema(this, 0),
  124. y: extrema(this, 1),
  125. };
  126. };
  127. PolynomialBezier.prototype.boundingBox = function () {
  128. var bounds = this.bounds();
  129. return {
  130. left: bounds.x.min,
  131. right: bounds.x.max,
  132. top: bounds.y.min,
  133. bottom: bounds.y.max,
  134. width: bounds.x.max - bounds.x.min,
  135. height: bounds.y.max - bounds.y.min,
  136. cx: (bounds.x.max + bounds.x.min) / 2,
  137. cy: (bounds.y.max + bounds.y.min) / 2,
  138. };
  139. };
  140. function intersectData(bez, t1, t2) {
  141. var box = bez.boundingBox();
  142. return {
  143. cx: box.cx,
  144. cy: box.cy,
  145. width: box.width,
  146. height: box.height,
  147. bez: bez,
  148. t: (t1 + t2) / 2,
  149. t1: t1,
  150. t2: t2,
  151. };
  152. }
  153. function splitData(data) {
  154. var split = data.bez.split(0.5);
  155. return [
  156. intersectData(split[0], data.t1, data.t),
  157. intersectData(split[1], data.t, data.t2),
  158. ];
  159. }
  160. function boxIntersect(b1, b2) {
  161. return Math.abs(b1.cx - b2.cx) * 2 < b1.width + b2.width
  162. && Math.abs(b1.cy - b2.cy) * 2 < b1.height + b2.height;
  163. }
  164. function intersectsImpl(d1, d2, depth, tolerance, intersections, maxRecursion) {
  165. if (!boxIntersect(d1, d2)) return;
  166. if (depth >= maxRecursion || (d1.width <= tolerance && d1.height <= tolerance && d2.width <= tolerance && d2.height <= tolerance)) {
  167. intersections.push([d1.t, d2.t]);
  168. return;
  169. }
  170. var d1s = splitData(d1);
  171. var d2s = splitData(d2);
  172. intersectsImpl(d1s[0], d2s[0], depth + 1, tolerance, intersections, maxRecursion);
  173. intersectsImpl(d1s[0], d2s[1], depth + 1, tolerance, intersections, maxRecursion);
  174. intersectsImpl(d1s[1], d2s[0], depth + 1, tolerance, intersections, maxRecursion);
  175. intersectsImpl(d1s[1], d2s[1], depth + 1, tolerance, intersections, maxRecursion);
  176. }
  177. PolynomialBezier.prototype.intersections = function (other, tolerance, maxRecursion) {
  178. if (tolerance === undefined) tolerance = 2;
  179. if (maxRecursion === undefined) maxRecursion = 7;
  180. var intersections = [];
  181. intersectsImpl(intersectData(this, 0, 1), intersectData(other, 0, 1), 0, tolerance, intersections, maxRecursion);
  182. return intersections;
  183. };
  184. PolynomialBezier.shapeSegment = function (shapePath, index) {
  185. var nextIndex = (index + 1) % shapePath.length();
  186. return new PolynomialBezier(shapePath.v[index], shapePath.o[index], shapePath.i[nextIndex], shapePath.v[nextIndex], true);
  187. };
  188. PolynomialBezier.shapeSegmentInverted = function (shapePath, index) {
  189. var nextIndex = (index + 1) % shapePath.length();
  190. return new PolynomialBezier(shapePath.v[nextIndex], shapePath.i[nextIndex], shapePath.o[index], shapePath.v[index], true);
  191. };
  192. function crossProduct(a, b) {
  193. return [
  194. a[1] * b[2] - a[2] * b[1],
  195. a[2] * b[0] - a[0] * b[2],
  196. a[0] * b[1] - a[1] * b[0],
  197. ];
  198. }
  199. function lineIntersection(start1, end1, start2, end2) {
  200. var v1 = [start1[0], start1[1], 1];
  201. var v2 = [end1[0], end1[1], 1];
  202. var v3 = [start2[0], start2[1], 1];
  203. var v4 = [end2[0], end2[1], 1];
  204. var r = crossProduct(
  205. crossProduct(v1, v2),
  206. crossProduct(v3, v4)
  207. );
  208. if (floatZero(r[2])) return null;
  209. return [r[0] / r[2], r[1] / r[2]];
  210. }
  211. function polarOffset(p, angle, length) {
  212. return [
  213. p[0] + Math.cos(angle) * length,
  214. p[1] - Math.sin(angle) * length,
  215. ];
  216. }
  217. function pointDistance(p1, p2) {
  218. return Math.hypot(p1[0] - p2[0], p1[1] - p2[1]);
  219. }
  220. function pointEqual(p1, p2) {
  221. return floatEqual(p1[0], p2[0]) && floatEqual(p1[1], p2[1]);
  222. }
  223. export {
  224. PolynomialBezier,
  225. lineIntersection,
  226. polarOffset,
  227. pointDistance,
  228. pointEqual,
  229. floatEqual,
  230. };