Skip to content

Commit

Permalink
feat(🧮): cubicBezierYForX() (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Sep 23, 2020
1 parent a71273b commit 4dc53bb
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 0 deletions.
91 changes: 91 additions & 0 deletions src/Math.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Vector } from "./Vectors";

export const bin = (value: boolean): 0 | 1 => {
"worklet";
return value ? 1 : 0;
Expand Down Expand Up @@ -66,3 +68,92 @@ export const round = (value: number, precision = 0) => {
const p = Math.pow(10, precision);
return Math.round(value * p) / p;
};

// https://stackoverflow.com/questions/27176423/function-to-solve-cubic-equation-analytically
const cuberoot = (x: number) => {
"worklet";
const y = Math.pow(Math.abs(x), 1 / 3);
return x < 0 ? -y : y;
};

const solveCubic = (a: number, b: number, c: number, d: number) => {
"worklet";
if (Math.abs(a) < 1e-8) {
// Quadratic case, ax^2+bx+c=0
a = b;
b = c;
c = d;
if (Math.abs(a) < 1e-8) {
// Linear case, ax+b=0
a = b;
b = c;
if (Math.abs(a) < 1e-8) {
// Degenerate case
return [];
}
return [-b / a];
}

const D = b * b - 4 * a * c;
if (Math.abs(D) < 1e-8) {
return [-b / (2 * a)];
} else if (D > 0) {
return [(-b + Math.sqrt(D)) / (2 * a), (-b - Math.sqrt(D)) / (2 * a)];
}
return [];
}

// Convert to depressed cubic t^3+pt+q = 0 (subst x = t - b/3a)
const p = (3 * a * c - b * b) / (3 * a * a);
const q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a);
let roots;

if (Math.abs(p) < 1e-8) {
// p = 0 -> t^3 = -q -> t = -q^1/3
roots = [cuberoot(-q)];
} else if (Math.abs(q) < 1e-8) {
// q = 0 -> t^3 + pt = 0 -> t(t^2+p)=0
roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []);
} else {
const D = (q * q) / 4 + (p * p * p) / 27;
if (Math.abs(D) < 1e-8) {
// D = 0 -> two roots
roots = [(-1.5 * q) / p, (3 * q) / p];
} else if (D > 0) {
// Only one real root
const u = cuberoot(-q / 2 - Math.sqrt(D));
roots = [u - p / (3 * u)];
} else {
// D < 0, three roots, but needs to use complex numbers/trigonometric solution
const u = 2 * Math.sqrt(-p / 3);
const t = Math.acos((3 * q) / p / u) / 3; // D < 0 implies p < 0 and acos argument in [-1..1]
const k = (2 * Math.PI) / 3;
roots = [u * Math.cos(t), u * Math.cos(t - k), u * Math.cos(t - 2 * k)];
}
}

// Convert back from depressed cubic
for (let i = 0; i < roots.length; i++) {
roots[i] -= b / (3 * a);
}

return roots;
};

export const cubicBezierYForX = (
x: number,
a: Vector,
b: Vector,
c: Vector,
d: Vector
) => {
"worklet";
const pa = -a.x + 3 * b.x - 3 * c.x + d.x;
const pb = 3 * a.x - 6 * b.x + 3 * c.x;
const pc = -3 * a.x + 3 * b.x;
const pd = a.x - x;
const [t] = solveCubic(pa, pb, pc, pd).filter(
(root) => root >= 0 && root <= 1
);
return cubicBezier(t, a.y, b.y, c.y, d.y);
};
17 changes: 17 additions & 0 deletions src/Vectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,20 @@ export const useVector = (
const y = useSharedValue(y1 ?? x1);
return { x, y };
};

type Create = {
(): Vector<0>;
<T extends Animated.Adaptable<number>>(x: T, y?: T): Vector<T>;
};

const create: Create = <T extends Animated.Adaptable<number>>(x?: T, y?: T) => {
"worklet";
return {
x: x ?? 0,
y: y ?? x ?? 0,
};
};

export const vec = {
create,
};
22 changes: 22 additions & 0 deletions src/__tests__/Math.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
between,
toRad,
toDeg,
cubicBezierYForX,
} from "../Math";
import { vec } from "../Vectors";

test("bin()", () => {
expect(bin(true)).toBe(1);
Expand Down Expand Up @@ -58,3 +60,23 @@ test("toRad()", () => {
expect(toDeg(Math.PI)).toBe(180);
expect(toDeg(Math.PI / 4)).toBe(45);
});

test("cubicBezierYForX()", () => {
const x = 198;
const y = 94;
const a = vec.create(20, 250);
const b = vec.create(30, 20);
const c = vec.create(203, 221);
const d = vec.create(220, 20);
expect(Math.round(cubicBezierYForX(x, a, b, c, d))).toBe(y);
});

test("cubicBezierYForX2()", () => {
const x = 116;
const y = 139;
const a = vec.create(59, 218);
const b = vec.create(131, 39);
const c = vec.create(204, 223);
const d = vec.create(227, 89);
expect(Math.round(cubicBezierYForX(x, a, b, c, d))).toBe(y);
});

0 comments on commit 4dc53bb

Please sign in to comment.