aboutsummaryrefslogtreecommitdiffstats
path: root/packages/math/curve.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/math/curve.ts')
-rw-r--r--packages/math/curve.ts282
1 files changed, 282 insertions, 0 deletions
diff --git a/packages/math/curve.ts b/packages/math/curve.ts
new file mode 100644
index 0000000..9b275ce
--- /dev/null
+++ b/packages/math/curve.ts
@@ -0,0 +1,282 @@
+import type { Bounds } from "../excalidraw/element/bounds";
+import { isPoint, pointDistance, pointFrom } from "./point";
+import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
+import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
+
+/**
+ *
+ * @param a
+ * @param b
+ * @param c
+ * @param d
+ * @returns
+ */
+export function curve<Point extends GlobalPoint | LocalPoint>(
+ a: Point,
+ b: Point,
+ c: Point,
+ d: Point,
+) {
+ return [a, b, c, d] as Curve<Point>;
+}
+
+function gradient(
+ f: (t: number, s: number) => number,
+ t0: number,
+ s0: number,
+ delta: number = 1e-6,
+): number[] {
+ return [
+ (f(t0 + delta, s0) - f(t0 - delta, s0)) / (2 * delta),
+ (f(t0, s0 + delta) - f(t0, s0 - delta)) / (2 * delta),
+ ];
+}
+
+function solve(
+ f: (t: number, s: number) => [number, number],
+ t0: number,
+ s0: number,
+ tolerance: number = 1e-3,
+ iterLimit: number = 10,
+): number[] | null {
+ let error = Infinity;
+ let iter = 0;
+
+ while (error >= tolerance) {
+ if (iter >= iterLimit) {
+ return null;
+ }
+
+ const y0 = f(t0, s0);
+ const jacobian = [
+ gradient((t, s) => f(t, s)[0], t0, s0),
+ gradient((t, s) => f(t, s)[1], t0, s0),
+ ];
+ const b = [[-y0[0]], [-y0[1]]];
+ const det =
+ jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
+
+ if (det === 0) {
+ return null;
+ }
+
+ const iJ = [
+ [jacobian[1][1] / det, -jacobian[0][1] / det],
+ [-jacobian[1][0] / det, jacobian[0][0] / det],
+ ];
+ const h = [
+ [iJ[0][0] * b[0][0] + iJ[0][1] * b[1][0]],
+ [iJ[1][0] * b[0][0] + iJ[1][1] * b[1][0]],
+ ];
+
+ t0 = t0 + h[0][0];
+ s0 = s0 + h[1][0];
+
+ const [tErr, sErr] = f(t0, s0);
+ error = Math.max(Math.abs(tErr), Math.abs(sErr));
+ iter += 1;
+ }
+
+ return [t0, s0];
+}
+
+const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
+ c: Curve<Point>,
+ t: number,
+) =>
+ pointFrom<Point>(
+ (1 - t) ** 3 * c[0][0] +
+ 3 * (1 - t) ** 2 * t * c[1][0] +
+ 3 * (1 - t) * t ** 2 * c[2][0] +
+ t ** 3 * c[3][0],
+ (1 - t) ** 3 * c[0][1] +
+ 3 * (1 - t) ** 2 * t * c[1][1] +
+ 3 * (1 - t) * t ** 2 * c[2][1] +
+ t ** 3 * c[3][1],
+ );
+
+/**
+ * Computes the intersection between a cubic spline and a line segment.
+ */
+export function curveIntersectLineSegment<
+ Point extends GlobalPoint | LocalPoint,
+>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
+ // Optimize by doing a cheap bounding box check first
+ const bounds = curveBounds(c);
+ if (
+ rectangleIntersectLineSegment(
+ rectangle(
+ pointFrom(bounds[0], bounds[1]),
+ pointFrom(bounds[2], bounds[3]),
+ ),
+ l,
+ ).length === 0
+ ) {
+ return [];
+ }
+
+ const line = (s: number) =>
+ pointFrom<Point>(
+ l[0][0] + s * (l[1][0] - l[0][0]),
+ l[0][1] + s * (l[1][1] - l[0][1]),
+ );
+
+ const initial_guesses: [number, number][] = [
+ [0.5, 0],
+ [0.2, 0],
+ [0.8, 0],
+ ];
+
+ const calculate = ([t0, s0]: [number, number]) => {
+ const solution = solve(
+ (t: number, s: number) => {
+ const bezier_point = bezierEquation(c, t);
+ const line_point = line(s);
+
+ return [
+ bezier_point[0] - line_point[0],
+ bezier_point[1] - line_point[1],
+ ];
+ },
+ t0,
+ s0,
+ );
+
+ if (!solution) {
+ return null;
+ }
+
+ const [t, s] = solution;
+
+ if (t < 0 || t > 1 || s < 0 || s > 1) {
+ return null;
+ }
+
+ return bezierEquation(c, t);
+ };
+
+ let solution = calculate(initial_guesses[0]);
+ if (solution) {
+ return [solution];
+ }
+
+ solution = calculate(initial_guesses[1]);
+ if (solution) {
+ return [solution];
+ }
+
+ solution = calculate(initial_guesses[2]);
+ if (solution) {
+ return [solution];
+ }
+
+ return [];
+}
+
+/**
+ * Finds the closest point on the Bezier curve from another point
+ *
+ * @param x
+ * @param y
+ * @param P0
+ * @param P1
+ * @param P2
+ * @param P3
+ * @param tolerance
+ * @param maxLevel
+ * @returns
+ */
+export function curveClosestPoint<Point extends GlobalPoint | LocalPoint>(
+ c: Curve<Point>,
+ p: Point,
+ tolerance: number = 1e-3,
+): Point | null {
+ const localMinimum = (
+ min: number,
+ max: number,
+ f: (t: number) => number,
+ e: number = tolerance,
+ ) => {
+ let m = min;
+ let n = max;
+ let k;
+
+ while (n - m > e) {
+ k = (n + m) / 2;
+ if (f(k - e) < f(k + e)) {
+ n = k;
+ } else {
+ m = k;
+ }
+ }
+
+ return k;
+ };
+
+ const maxSteps = 30;
+ let closestStep = 0;
+ for (let min = Infinity, step = 0; step < maxSteps; step++) {
+ const d = pointDistance(p, bezierEquation(c, step / maxSteps));
+ if (d < min) {
+ min = d;
+ closestStep = step;
+ }
+ }
+
+ const t0 = Math.max((closestStep - 1) / maxSteps, 0);
+ const t1 = Math.min((closestStep + 1) / maxSteps, 1);
+ const solution = localMinimum(t0, t1, (t) =>
+ pointDistance(p, bezierEquation(c, t)),
+ );
+
+ if (!solution) {
+ return null;
+ }
+
+ return bezierEquation(c, solution);
+}
+
+/**
+ * Determines the distance between a point and the closest point on the
+ * Bezier curve.
+ *
+ * @param c The curve to test
+ * @param p The point to measure from
+ */
+export function curvePointDistance<Point extends GlobalPoint | LocalPoint>(
+ c: Curve<Point>,
+ p: Point,
+) {
+ const closest = curveClosestPoint(c, p);
+
+ if (!closest) {
+ return 0;
+ }
+
+ return pointDistance(p, closest);
+}
+
+/**
+ * Determines if the parameter is a Curve
+ */
+export function isCurve<P extends GlobalPoint | LocalPoint>(
+ v: unknown,
+): v is Curve<P> {
+ return (
+ Array.isArray(v) &&
+ v.length === 4 &&
+ isPoint(v[0]) &&
+ isPoint(v[1]) &&
+ isPoint(v[2]) &&
+ isPoint(v[3])
+ );
+}
+
+function curveBounds<Point extends GlobalPoint | LocalPoint>(
+ c: Curve<Point>,
+): Bounds {
+ const [P0, P1, P2, P3] = c;
+ const x = [P0[0], P1[0], P2[0], P3[0]];
+ const y = [P0[1], P1[1], P2[1], P3[1]];
+ return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)];
+}