UNPKG

@rdme/curve-2d

Version:

2d curves in typescript

129 lines (111 loc) 3.9 kB
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; }