@rdme/curve-2d
Version:
2d curves in typescript
129 lines (111 loc) • 3.9 kB
text/typescript
import type { Bezier2d } from "src/bezier2d";
import type { Catmull2d } from "src/catmull2d";
import type { Line2d } from "src/line2d";
import { lerp } from "src/util";
import { Vec2d, Vec2dLike } from "src/vec2d";
const { min, abs } = Math;
export abstract class CurveBase2d {
protected _length?: number;
protected _lut?: Vec2d[];
protected _percentFn?: (percent: number) => number;
public abstract getSegment(tStart: number, tEnd: number): CurveBase2d;
public abstract get(t: number): Vec2d;
public abstract clone(): this;
public abstract reverse(): this;
public abstract scale(factor: number, origin?: Vec2dLike): this;
public abstract rotate(angle: number, origin?: Vec2dLike): this;
public abstract translate(vector: Vec2dLike): this;
public invalidate() {
this._length = this._lut = this._percentFn = undefined;
}
public defaultPrecision(): number {
return 100;
}
public length(precision?: number): number {
if (this._length !== undefined) return this._length;
const lut = this.getLut(precision);
let len = 0;
for (let i = 0; i < lut.length - 1; ++i) len += lut[i].distance(lut[i + 1]);
return (this._length = len);
}
public getLut(
precision: number = this.defaultPrecision(),
cache = true
): Vec2d[] {
if (!precision) return [this.get(0), this.get(1)];
if (this._lut?.length === precision + 1) return this._lut;
const step = 1 / precision;
const lut = Array.from({ length: precision + 1 }, (_, i) =>
this.get(min(1, i * step))
);
if (cache) {
this._lut = lut;
this._length = this._percentFn = undefined;
}
return lut;
}
public split(t: number): [CurveBase2d, CurveBase2d] {
return [this.getSegment(0, t), this.getSegment(t, 1)];
}
public getPercentageFn(precision?: number): (percent: number) => number {
const lut = this.getLut(precision);
if (this._percentFn) return this._percentFn;
const percentLookup = CurveBase2d._getCumulativeLut(lut);
return (this._percentFn = (n) => {
if (n >= 0.999) return 1;
if (n <= 0.001) return 0;
const [[first, firstT], [second, secondT]] = percentLookup.sort(
([a], [b]) => {
return abs(a - n) - abs(b - n);
}
);
const dFirst = abs(n - first);
const dSecond = abs(n - second);
return lerp(firstT, secondT, dFirst / (dFirst + dSecond));
});
}
public getByPercent(percent: number, precision?: number): Vec2d {
return this.get(this.getPercentageFn(precision)(percent));
}
protected _applyReverse(curve: this): this {
curve._lut?.reverse();
if (curve._percentFn) {
const fn = curve._percentFn;
if ("__reverse" in fn) curve._percentFn = (fn as any).__reverse;
else {
curve._percentFn = (t) => 1 - fn(1 - t);
(curve._percentFn as any).__reverse = fn;
}
}
return curve;
}
protected _applyClone(curve: this): this {
curve._lut = this._lut?.map((point) => new Vec2d(point));
curve._length = this._length;
curve._percentFn = this._percentFn;
return curve;
}
protected static _getCumulativeLut(lut: Vec2d[]) {
const percentLookup: [number, number][] = Array(lut.length);
const length = lut.length;
const step = 1 / (lut.length - 1);
let len = 0;
for (let i = 0; i < lut.length - 1; ++i) len += lut[i].distance(lut[i + 1]);
let last: Vec2d = lut[0],
lastDistance = 0;
percentLookup[0] = [0, 0];
for (let i = 1; i < length; ++i) {
percentLookup[i] = [
(lastDistance += last.distance(lut[i])) / len,
i * step,
];
last = lut[i];
}
return percentLookup;
}
}
export interface CurveBase2d {
catmull(precision?: number, cache?: boolean): Catmull2d;
beziers(precision?: number, cache?: boolean): Bezier2d[];
tangent(t: number, threshold?: number): Line2d;
}