curve.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
  2. import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
  3. import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
  4. import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
  5. /**
  6. *
  7. * @param a
  8. * @param b
  9. * @param c
  10. * @param d
  11. * @returns
  12. */
  13. export function curve<Point extends GlobalPoint | LocalPoint>(
  14. a: Point,
  15. b: Point,
  16. c: Point,
  17. d: Point,
  18. ) {
  19. return [a, b, c, d] as Curve<Point>;
  20. }
  21. function gradient(
  22. f: (t: number, s: number) => number,
  23. t0: number,
  24. s0: number,
  25. delta: number = 1e-6,
  26. ): number[] {
  27. return [
  28. (f(t0 + delta, s0) - f(t0 - delta, s0)) / (2 * delta),
  29. (f(t0, s0 + delta) - f(t0, s0 - delta)) / (2 * delta),
  30. ];
  31. }
  32. function solve(
  33. f: (t: number, s: number) => [number, number],
  34. t0: number,
  35. s0: number,
  36. tolerance: number = 1e-3,
  37. iterLimit: number = 10,
  38. ): number[] | null {
  39. let error = Infinity;
  40. let iter = 0;
  41. while (error >= tolerance) {
  42. if (iter >= iterLimit) {
  43. return null;
  44. }
  45. const y0 = f(t0, s0);
  46. const jacobian = [
  47. gradient((t, s) => f(t, s)[0], t0, s0),
  48. gradient((t, s) => f(t, s)[1], t0, s0),
  49. ];
  50. const b = [[-y0[0]], [-y0[1]]];
  51. const det =
  52. jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
  53. if (det === 0) {
  54. return null;
  55. }
  56. const iJ = [
  57. [jacobian[1][1] / det, -jacobian[0][1] / det],
  58. [-jacobian[1][0] / det, jacobian[0][0] / det],
  59. ];
  60. const h = [
  61. [iJ[0][0] * b[0][0] + iJ[0][1] * b[1][0]],
  62. [iJ[1][0] * b[0][0] + iJ[1][1] * b[1][0]],
  63. ];
  64. t0 = t0 + h[0][0];
  65. s0 = s0 + h[1][0];
  66. const [tErr, sErr] = f(t0, s0);
  67. error = Math.max(Math.abs(tErr), Math.abs(sErr));
  68. iter += 1;
  69. }
  70. return [t0, s0];
  71. }
  72. export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
  73. c: Curve<Point>,
  74. t: number,
  75. ) =>
  76. pointFrom<Point>(
  77. (1 - t) ** 3 * c[0][0] +
  78. 3 * (1 - t) ** 2 * t * c[1][0] +
  79. 3 * (1 - t) * t ** 2 * c[2][0] +
  80. t ** 3 * c[3][0],
  81. (1 - t) ** 3 * c[0][1] +
  82. 3 * (1 - t) ** 2 * t * c[1][1] +
  83. 3 * (1 - t) * t ** 2 * c[2][1] +
  84. t ** 3 * c[3][1],
  85. );
  86. /**
  87. * Computes the intersection between a cubic spline and a line segment.
  88. */
  89. export function curveIntersectLineSegment<
  90. Point extends GlobalPoint | LocalPoint,
  91. >(c: Curve<Point>, l: LineSegment<Point>): Point[] {
  92. const line = (s: number) =>
  93. pointFrom<Point>(
  94. l[0][0] + s * (l[1][0] - l[0][0]),
  95. l[0][1] + s * (l[1][1] - l[0][1]),
  96. );
  97. const initial_guesses: [number, number][] = [
  98. [0.5, 0],
  99. [0.2, 0],
  100. [0.8, 0],
  101. ];
  102. const calculate = ([t0, s0]: [number, number]) => {
  103. const solution = solve(
  104. (t: number, s: number) => {
  105. const bezier_point = bezierEquation(c, t);
  106. const line_point = line(s);
  107. return [
  108. bezier_point[0] - line_point[0],
  109. bezier_point[1] - line_point[1],
  110. ];
  111. },
  112. t0,
  113. s0,
  114. );
  115. if (!solution) {
  116. return null;
  117. }
  118. const [t, s] = solution;
  119. if (t < 0 || t > 1 || s < 0 || s > 1) {
  120. return null;
  121. }
  122. return bezierEquation(c, t);
  123. };
  124. let solution = calculate(initial_guesses[0]);
  125. if (solution) {
  126. return [solution];
  127. }
  128. solution = calculate(initial_guesses[1]);
  129. if (solution) {
  130. return [solution];
  131. }
  132. solution = calculate(initial_guesses[2]);
  133. if (solution) {
  134. return [solution];
  135. }
  136. return [];
  137. }
  138. /**
  139. * Finds the closest point on the Bezier curve from another point
  140. *
  141. * @param x
  142. * @param y
  143. * @param P0
  144. * @param P1
  145. * @param P2
  146. * @param P3
  147. * @param tolerance
  148. * @param maxLevel
  149. * @returns
  150. */
  151. export function curveClosestPoint<Point extends GlobalPoint | LocalPoint>(
  152. c: Curve<Point>,
  153. p: Point,
  154. tolerance: number = 1e-3,
  155. ): Point | null {
  156. const localMinimum = (
  157. min: number,
  158. max: number,
  159. f: (t: number) => number,
  160. e: number = tolerance,
  161. ) => {
  162. let m = min;
  163. let n = max;
  164. let k;
  165. while (n - m > e) {
  166. k = (n + m) / 2;
  167. if (f(k - e) < f(k + e)) {
  168. n = k;
  169. } else {
  170. m = k;
  171. }
  172. }
  173. return k;
  174. };
  175. const maxSteps = 30;
  176. let closestStep = 0;
  177. for (let min = Infinity, step = 0; step < maxSteps; step++) {
  178. const d = pointDistance(p, bezierEquation(c, step / maxSteps));
  179. if (d < min) {
  180. min = d;
  181. closestStep = step;
  182. }
  183. }
  184. const t0 = Math.max((closestStep - 1) / maxSteps, 0);
  185. const t1 = Math.min((closestStep + 1) / maxSteps, 1);
  186. const solution = localMinimum(t0, t1, (t) =>
  187. pointDistance(p, bezierEquation(c, t)),
  188. );
  189. if (!solution) {
  190. return null;
  191. }
  192. return bezierEquation(c, solution);
  193. }
  194. /**
  195. * Determines the distance between a point and the closest point on the
  196. * Bezier curve.
  197. *
  198. * @param c The curve to test
  199. * @param p The point to measure from
  200. */
  201. export function curvePointDistance<Point extends GlobalPoint | LocalPoint>(
  202. c: Curve<Point>,
  203. p: Point,
  204. ) {
  205. const closest = curveClosestPoint(c, p);
  206. if (!closest) {
  207. return 0;
  208. }
  209. return pointDistance(p, closest);
  210. }
  211. /**
  212. * Determines if the parameter is a Curve
  213. */
  214. export function isCurve<P extends GlobalPoint | LocalPoint>(
  215. v: unknown,
  216. ): v is Curve<P> {
  217. return (
  218. Array.isArray(v) &&
  219. v.length === 4 &&
  220. isPoint(v[0]) &&
  221. isPoint(v[1]) &&
  222. isPoint(v[2]) &&
  223. isPoint(v[3])
  224. );
  225. }
  226. export function curveTangent<Point extends GlobalPoint | LocalPoint>(
  227. [p0, p1, p2, p3]: Curve<Point>,
  228. t: number,
  229. ) {
  230. return vector(
  231. -3 * (1 - t) * (1 - t) * p0[0] +
  232. 3 * (1 - t) * (1 - t) * p1[0] -
  233. 6 * t * (1 - t) * p1[0] -
  234. 3 * t * t * p2[0] +
  235. 6 * t * (1 - t) * p2[0] +
  236. 3 * t * t * p3[0],
  237. -3 * (1 - t) * (1 - t) * p0[1] +
  238. 3 * (1 - t) * (1 - t) * p1[1] -
  239. 6 * t * (1 - t) * p1[1] -
  240. 3 * t * t * p2[1] +
  241. 6 * t * (1 - t) * p2[1] +
  242. 3 * t * t * p3[1],
  243. );
  244. }
  245. export function curveCatmullRomQuadraticApproxPoints(
  246. points: GlobalPoint[],
  247. tension = 0.5,
  248. ) {
  249. if (points.length < 2) {
  250. return;
  251. }
  252. const pointSets: [GlobalPoint, GlobalPoint][] = [];
  253. for (let i = 0; i < points.length - 1; i++) {
  254. const p0 = points[i - 1 < 0 ? 0 : i - 1];
  255. const p1 = points[i];
  256. const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
  257. const cpX = p1[0] + ((p2[0] - p0[0]) * tension) / 2;
  258. const cpY = p1[1] + ((p2[1] - p0[1]) * tension) / 2;
  259. pointSets.push([
  260. pointFrom<GlobalPoint>(cpX, cpY),
  261. pointFrom<GlobalPoint>(p2[0], p2[1]),
  262. ]);
  263. }
  264. return pointSets;
  265. }
  266. export function curveCatmullRomCubicApproxPoints<
  267. Point extends GlobalPoint | LocalPoint,
  268. >(points: Point[], tension = 0.5) {
  269. if (points.length < 2) {
  270. return;
  271. }
  272. const pointSets: Curve<Point>[] = [];
  273. for (let i = 0; i < points.length - 1; i++) {
  274. const p0 = points[i - 1 < 0 ? 0 : i - 1];
  275. const p1 = points[i];
  276. const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
  277. const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2];
  278. const tangent1 = [(p2[0] - p0[0]) * tension, (p2[1] - p0[1]) * tension];
  279. const tangent2 = [(p3[0] - p1[0]) * tension, (p3[1] - p1[1]) * tension];
  280. const cp1x = p1[0] + tangent1[0] / 3;
  281. const cp1y = p1[1] + tangent1[1] / 3;
  282. const cp2x = p2[0] - tangent2[0] / 3;
  283. const cp2y = p2[1] - tangent2[1] / 3;
  284. pointSets.push(
  285. curve(
  286. pointFrom(p1[0], p1[1]),
  287. pointFrom(cp1x, cp1y),
  288. pointFrom(cp2x, cp2y),
  289. pointFrom(p2[0], p2[1]),
  290. ),
  291. );
  292. }
  293. return pointSets;
  294. }
  295. export function curveOffsetPoints(
  296. [p0, p1, p2, p3]: Curve<GlobalPoint>,
  297. offset: number,
  298. steps = 50,
  299. ) {
  300. const offsetPoints = [];
  301. for (let i = 0; i <= steps; i++) {
  302. const t = i / steps;
  303. const c = curve(p0, p1, p2, p3);
  304. const point = bezierEquation(c, t);
  305. const tangent = vectorNormalize(curveTangent(c, t));
  306. const normal = vectorNormal(tangent);
  307. offsetPoints.push(pointFromVector(vectorScale(normal, offset), point));
  308. }
  309. return offsetPoints;
  310. }
  311. export function offsetPointsForQuadraticBezier(
  312. p0: GlobalPoint,
  313. p1: GlobalPoint,
  314. p2: GlobalPoint,
  315. offsetDist: number,
  316. steps = 50,
  317. ) {
  318. const offsetPoints = [];
  319. for (let i = 0; i <= steps; i++) {
  320. const t = i / steps;
  321. const t1 = 1 - t;
  322. const point = pointFrom<GlobalPoint>(
  323. t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0],
  324. t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1],
  325. );
  326. const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]);
  327. const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]);
  328. const tangent = vectorNormalize(vector(tangentX, tangentY));
  329. const normal = vectorNormal(tangent);
  330. offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
  331. }
  332. return offsetPoints;
  333. }
  334. /**
  335. * Implementation based on Legendre-Gauss quadrature for more accurate arc
  336. * length calculation.
  337. *
  338. * Reference: https://pomax.github.io/bezierinfo/#arclength
  339. *
  340. * @param c The curve to calculate the length of
  341. * @returns The approximated length of the curve
  342. */
  343. export function curveLength<P extends GlobalPoint | LocalPoint>(
  344. c: Curve<P>,
  345. ): number {
  346. const z2 = 0.5;
  347. let sum = 0;
  348. for (let i = 0; i < 24; i++) {
  349. const t = z2 * LegendreGaussN24TValues[i] + z2;
  350. const derivativeVector = curveTangent(c, t);
  351. const magnitude = Math.sqrt(
  352. derivativeVector[0] * derivativeVector[0] +
  353. derivativeVector[1] * derivativeVector[1],
  354. );
  355. sum += LegendreGaussN24CValues[i] * magnitude;
  356. }
  357. return z2 * sum;
  358. }
  359. /**
  360. * Calculates the curve length from t=0 to t=parameter using the same
  361. * Legendre-Gauss quadrature method used in curveLength
  362. *
  363. * @param c The curve to calculate the partial length for
  364. * @param t The parameter value (0 to 1) to calculate length up to
  365. * @returns The length of the curve from beginning to parameter t
  366. */
  367. export function curveLengthAtParameter<P extends GlobalPoint | LocalPoint>(
  368. c: Curve<P>,
  369. t: number,
  370. ): number {
  371. if (t <= 0) {
  372. return 0;
  373. }
  374. if (t >= 1) {
  375. return curveLength(c);
  376. }
  377. // Scale and shift the integration interval from [0,t] to [-1,1]
  378. // which is what the Legendre-Gauss quadrature expects
  379. const z1 = t / 2;
  380. const z2 = t / 2;
  381. let sum = 0;
  382. for (let i = 0; i < 24; i++) {
  383. const parameter = z1 * LegendreGaussN24TValues[i] + z2;
  384. const derivativeVector = curveTangent(c, parameter);
  385. const magnitude = Math.sqrt(
  386. derivativeVector[0] * derivativeVector[0] +
  387. derivativeVector[1] * derivativeVector[1],
  388. );
  389. sum += LegendreGaussN24CValues[i] * magnitude;
  390. }
  391. return z1 * sum; // Scale the result back to the original interval
  392. }
  393. /**
  394. * Calculates the point at a specific percentage of a curve's total length
  395. * using binary search for improved efficiency and accuracy.
  396. *
  397. * @param c The curve to calculate point on
  398. * @param percent A value between 0 and 1 representing the percentage of the curve's length
  399. * @returns The point at the specified percentage of curve length
  400. */
  401. export function curvePointAtLength<P extends GlobalPoint | LocalPoint>(
  402. c: Curve<P>,
  403. percent: number,
  404. ): P {
  405. if (percent <= 0) {
  406. return bezierEquation(c, 0);
  407. }
  408. if (percent >= 1) {
  409. return bezierEquation(c, 1);
  410. }
  411. const totalLength = curveLength(c);
  412. const targetLength = totalLength * percent;
  413. // Binary search to find parameter t where length at t equals target length
  414. let tMin = 0;
  415. let tMax = 1;
  416. let t = percent; // Start with a reasonable guess (t = percent)
  417. let currentLength = 0;
  418. // Tolerance for length comparison and iteration limit to avoid infinite loops
  419. const tolerance = totalLength * 0.0001;
  420. const maxIterations = 20;
  421. for (let iteration = 0; iteration < maxIterations; iteration++) {
  422. currentLength = curveLengthAtParameter(c, t);
  423. const error = Math.abs(currentLength - targetLength);
  424. if (error < tolerance) {
  425. break;
  426. }
  427. if (currentLength < targetLength) {
  428. tMin = t;
  429. } else {
  430. tMax = t;
  431. }
  432. t = (tMin + tMax) / 2;
  433. }
  434. return bezierEquation(c, t);
  435. }