curve.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { point, pointRotateRads } from "./point";
  2. import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
  3. /**
  4. *
  5. * @param a
  6. * @param b
  7. * @param c
  8. * @param d
  9. * @returns
  10. */
  11. export function curve<Point extends GlobalPoint | LocalPoint>(
  12. a: Point,
  13. b: Point,
  14. c: Point,
  15. d: Point,
  16. ) {
  17. return [a, b, c, d] as Curve<Point>;
  18. }
  19. export const curveRotate = <Point extends LocalPoint | GlobalPoint>(
  20. curve: Curve<Point>,
  21. angle: Radians,
  22. origin: Point,
  23. ) => {
  24. return curve.map((p) => pointRotateRads(p, origin, angle));
  25. };
  26. /**
  27. *
  28. * @param pointsIn
  29. * @param curveTightness
  30. * @returns
  31. */
  32. export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
  33. pointsIn: readonly Point[],
  34. curveTightness = 0,
  35. ): Point[] {
  36. const len = pointsIn.length;
  37. if (len < 3) {
  38. throw new Error("A curve must have at least three points.");
  39. }
  40. const out: Point[] = [];
  41. if (len === 3) {
  42. out.push(
  43. point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
  44. point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
  45. point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
  46. point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
  47. );
  48. } else {
  49. const points: Point[] = [];
  50. points.push(pointsIn[0], pointsIn[0]);
  51. for (let i = 1; i < pointsIn.length; i++) {
  52. points.push(pointsIn[i]);
  53. if (i === pointsIn.length - 1) {
  54. points.push(pointsIn[i]);
  55. }
  56. }
  57. const b: Point[] = [];
  58. const s = 1 - curveTightness;
  59. out.push(point(points[0][0], points[0][1]));
  60. for (let i = 1; i + 2 < points.length; i++) {
  61. const cachedVertArray = points[i];
  62. b[0] = point(cachedVertArray[0], cachedVertArray[1]);
  63. b[1] = point(
  64. cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
  65. cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
  66. );
  67. b[2] = point(
  68. points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
  69. points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
  70. );
  71. b[3] = point(points[i + 1][0], points[i + 1][1]);
  72. out.push(b[1], b[2], b[3]);
  73. }
  74. }
  75. return out;
  76. }
  77. /**
  78. *
  79. * @param t
  80. * @param controlPoints
  81. * @returns
  82. */
  83. export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
  84. t: number,
  85. controlPoints: Curve<Point>,
  86. ): Point => {
  87. const [p0, p1, p2, p3] = controlPoints;
  88. const x =
  89. Math.pow(1 - t, 3) * p0[0] +
  90. 3 * Math.pow(1 - t, 2) * t * p1[0] +
  91. 3 * (1 - t) * Math.pow(t, 2) * p2[0] +
  92. Math.pow(t, 3) * p3[0];
  93. const y =
  94. Math.pow(1 - t, 3) * p0[1] +
  95. 3 * Math.pow(1 - t, 2) * t * p1[1] +
  96. 3 * (1 - t) * Math.pow(t, 2) * p2[1] +
  97. Math.pow(t, 3) * p3[1];
  98. return point(x, y);
  99. };
  100. /**
  101. *
  102. * @param point
  103. * @param controlPoints
  104. * @returns
  105. */
  106. export const cubicBezierDistance = <Point extends LocalPoint | GlobalPoint>(
  107. point: Point,
  108. controlPoints: Curve<Point>,
  109. ) => {
  110. // Calculate the closest point on the Bezier curve to the given point
  111. const t = findClosestParameter(point, controlPoints);
  112. // Calculate the coordinates of the closest point on the curve
  113. const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
  114. // Calculate the distance between the given point and the closest point on the curve
  115. const distance = Math.sqrt(
  116. (point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
  117. );
  118. return distance;
  119. };
  120. const solveCubic = (a: number, b: number, c: number, d: number) => {
  121. // This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
  122. const roots: number[] = [];
  123. const discriminant =
  124. 18 * a * b * c * d -
  125. 4 * Math.pow(b, 3) * d +
  126. Math.pow(b, 2) * Math.pow(c, 2) -
  127. 4 * a * Math.pow(c, 3) -
  128. 27 * Math.pow(a, 2) * Math.pow(d, 2);
  129. if (discriminant >= 0) {
  130. const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
  131. const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
  132. const root1 = (-b - C - D) / (3 * a);
  133. const root2 = (-b + (C + D) / 2) / (3 * a);
  134. const root3 = (-b + (C + D) / 2) / (3 * a);
  135. roots.push(root1, root2, root3);
  136. } else {
  137. const realPart = -b / (3 * a);
  138. const root1 =
  139. 2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
  140. const root2 =
  141. 2 *
  142. Math.sqrt(-b / (3 * a)) *
  143. Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
  144. const root3 =
  145. 2 *
  146. Math.sqrt(-b / (3 * a)) *
  147. Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
  148. roots.push(root1, root2, root3);
  149. }
  150. return roots;
  151. };
  152. const findClosestParameter = <Point extends LocalPoint | GlobalPoint>(
  153. point: Point,
  154. controlPoints: Curve<Point>,
  155. ) => {
  156. // This function finds the parameter t that minimizes the distance between the point
  157. // and any point on the cubic Bezier curve.
  158. const [p0, p1, p2, p3] = controlPoints;
  159. // Use the direct formula to find the parameter t
  160. const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
  161. const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
  162. const c = 3 * p1[0] - 3 * p0[0];
  163. const d = p0[0] - point[0];
  164. const rootsX = solveCubic(a, b, c, d);
  165. // Do the same for the y-coordinate
  166. const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
  167. const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
  168. const g = 3 * p1[1] - 3 * p0[1];
  169. const h = p0[1] - point[1];
  170. const rootsY = solveCubic(e, f, g, h);
  171. // Select the real root that is between 0 and 1 (inclusive)
  172. const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
  173. const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
  174. if (validRootsX.length === 0 || validRootsY.length === 0) {
  175. // No valid roots found, use the midpoint as a fallback
  176. return 0.5;
  177. }
  178. // Choose the parameter t that minimizes the distance
  179. let minDistance = Infinity;
  180. let closestT = 0;
  181. for (const rootX of validRootsX) {
  182. for (const rootY of validRootsY) {
  183. const distance = Math.sqrt(
  184. (rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
  185. );
  186. if (distance < minDistance) {
  187. minDistance = distance;
  188. closestT = (rootX + rootY) / 2; // Use the average for a smoother result
  189. }
  190. }
  191. }
  192. return closestT;
  193. };