Skip to content

Commit d72d966

Browse files
committed
Math curves library
1 parent c6b562d commit d72d966

5 files changed

Lines changed: 551 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Curve } from "./Curve";
2+
import { type Vec3, createVec3Float64 } from "../vector";
3+
import { b3 } from "./index";
4+
5+
/**
6+
* A Curve along which a 3D position can be animated.
7+
*
8+
* A CubicBezierCurve is defined by four control points.
9+
*/
10+
class CubicBezierCurve extends Curve {
11+
protected _v0: Vec3 = createVec3Float64();
12+
protected _v1: Vec3 = createVec3Float64();
13+
protected _v2: Vec3 = createVec3Float64();
14+
protected _v3: Vec3 = createVec3Float64();
15+
16+
/**
17+
* @param cfg Configs
18+
* @param cfg.v0 The starting point.
19+
* @param cfg.v1 The first control point.
20+
* @param cfg.v2 The second control point.
21+
* @param cfg.v3 The ending point.
22+
* @param cfg.t Current position on this CubicBezierCurve, in range between 0..1.
23+
*/
24+
constructor(cfg: { v0?: Vec3; v1?: Vec3; v2?: Vec3; v3?: Vec3; t?: number } = {}) {
25+
super(cfg);
26+
27+
this.v0 = cfg.v0 ?? createVec3Float64();
28+
this.v1 = cfg.v1 ?? createVec3Float64();
29+
this.v2 = cfg.v2 ?? createVec3Float64();
30+
this.v3 = cfg.v3 ?? createVec3Float64();
31+
this.t = cfg.t ?? 0;
32+
}
33+
34+
/**
35+
* Starting point.
36+
*/
37+
set v0(value: Vec3) {
38+
this._v0 = value || createVec3Float64();
39+
this._updateArcLengths();
40+
}
41+
42+
get v0(): Vec3 {
43+
return this._v0;
44+
}
45+
46+
/**
47+
* First control point.
48+
*/
49+
set v1(value: Vec3) {
50+
this._v1 = value || createVec3Float64();
51+
this._updateArcLengths();
52+
}
53+
54+
get v1(): Vec3 {
55+
return this._v1;
56+
}
57+
58+
/**
59+
* Second control point.
60+
*/
61+
set v2(value: Vec3) {
62+
this._v2 = value || createVec3Float64();
63+
this._updateArcLengths();
64+
}
65+
66+
get v2(): Vec3 {
67+
return this._v2;
68+
}
69+
70+
/**
71+
* End point.
72+
*/
73+
set v3(value: Vec3) {
74+
this._v3 = value || createVec3Float64();
75+
this._updateArcLengths();
76+
}
77+
78+
get v3(): Vec3 {
79+
return this._v3;
80+
}
81+
82+
/**
83+
* Point on the curve at the current t.
84+
*/
85+
get point(): Vec3 {
86+
return this.getPoint(this._t);
87+
}
88+
89+
/**
90+
* Returns point on this CubicBezierCurve at the given position.
91+
*/
92+
getPoint(t: number): Vec3 {
93+
const vector = createVec3Float64();
94+
95+
vector[0] = b3(t, this._v0[0], this._v1[0], this._v2[0], this._v3[0]);
96+
vector[1] = b3(t, this._v0[1], this._v1[1], this._v2[1], this._v3[1]);
97+
vector[2] = b3(t, this._v0[2], this._v1[2], this._v2[2], this._v3[2]);
98+
99+
return vector;
100+
}
101+
102+
/**
103+
*
104+
*/
105+
getJSON(): { v0: Vec3; v1: Vec3; v2: Vec3; v3: Vec3; t: number } {
106+
return {
107+
v0: this._v0,
108+
v1: this._v1,
109+
v2: this._v2,
110+
v3: this._v3,
111+
t: this._t
112+
};
113+
}
114+
}
115+
116+
export { CubicBezierCurve };
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import {subVec3, type Vec3, createVec3Float64, normalizeVec3, lenVec3} from "../vector";
2+
3+
/**
4+
* Base class for parametric 3D curves.
5+
*
6+
* Curves are sampled over `t` in the range `[0..1]` and provide helpers for
7+
* point, tangent, arc-length, and uniform-distance sampling.
8+
*/
9+
abstract class Curve {
10+
protected _t: number = 0;
11+
protected __arcLengthDivisions?: number;
12+
protected cacheArcLengths?: number[];
13+
protected needsUpdate?: boolean;
14+
15+
/**
16+
* Creates a curve.
17+
*
18+
* @param cfg Configuration options
19+
* @param cfg.t Initial curve parameter in the range `[0..1]`
20+
*/
21+
constructor(cfg: { t?: number } = {}) {
22+
this.t = cfg.t ?? 0;
23+
}
24+
25+
/**
26+
* Current curve parameter.
27+
*
28+
* Clamped to the range `[0..1]`.
29+
*/
30+
set t(value: number) {
31+
value = value || 0;
32+
this._t = value < 0.0 ? 0.0 : value > 1.0 ? 1.0 : value;
33+
}
34+
35+
get t(): number {
36+
return this._t;
37+
}
38+
39+
/**
40+
* Normalized tangent at the current {@link t}.
41+
*/
42+
get tangent(): Vec3 {
43+
return this.getTangent(this._t);
44+
}
45+
46+
/**
47+
* Approximate arc length of the curve.
48+
*
49+
* Computed from cached sampled lengths.
50+
*/
51+
get length(): number {
52+
const lengths = this._getLengths();
53+
return lengths[lengths.length - 1];
54+
}
55+
56+
/**
57+
* Returns the normalized tangent at parameter `t`.
58+
*
59+
* Uses a small finite difference around `t`.
60+
*
61+
* @param t Curve parameter in the range `[0..1]`. Defaults to the current {@link t}.
62+
* @returns Normalized tangent vector
63+
*/
64+
getTangent(t?: number): Vec3 {
65+
const delta = 0.0001;
66+
67+
if (t === undefined) {
68+
t = this._t;
69+
}
70+
71+
let t1 = t - delta;
72+
let t2 = t + delta;
73+
74+
if (t1 < 0) {
75+
t1 = 0;
76+
}
77+
78+
if (t2 > 1) {
79+
t2 = 1;
80+
}
81+
82+
const pt1 = this.getPoint(t1);
83+
const pt2 = this.getPoint(t2);
84+
const vec = subVec3(pt2, pt1, createVec3Float64());
85+
return normalizeVec3(vec, createVec3Float64());
86+
}
87+
88+
/**
89+
* Returns a point using normalized arc-length parameterization.
90+
*
91+
* Unlike {@link getPoint}, `u` maps to distance along the curve rather than
92+
* directly to the curve parameter.
93+
*
94+
* @param u Normalized distance along the curve in the range `[0..1]`
95+
* @returns Point on the curve
96+
*/
97+
getPointAt(u: number): Vec3 {
98+
const t = this.getUToTMapping(u);
99+
return this.getPoint(t);
100+
}
101+
102+
/**
103+
* Samples points at evenly spaced parameter intervals.
104+
*
105+
* @param divisions Number of intervals to divide `[0..1]` into
106+
* @returns Sampled points, including both endpoints
107+
*/
108+
getPoints(divisions: number = 5): Vec3[] {
109+
const pts: Vec3[] = [];
110+
111+
for (let d = 0; d <= divisions; d++) {
112+
pts.push(this.getPoint(d / divisions));
113+
}
114+
115+
return pts;
116+
}
117+
118+
/**
119+
* Returns cumulative sampled arc lengths for the curve.
120+
*
121+
* The returned array starts at `0` and ends at the total sampled length.
122+
* Results are cached until invalidated.
123+
*
124+
* @param divisions Number of sampling divisions used to approximate arc length
125+
* @returns Cumulative arc lengths
126+
*/
127+
protected _getLengths(divisions?: number): number[] {
128+
if (!divisions) {
129+
divisions = this.__arcLengthDivisions ? this.__arcLengthDivisions : 200;
130+
}
131+
132+
if (
133+
this.cacheArcLengths &&
134+
this.cacheArcLengths.length === divisions + 1 &&
135+
!this.needsUpdate
136+
) {
137+
return this.cacheArcLengths;
138+
}
139+
140+
this.needsUpdate = false;
141+
142+
const cache: number[] = [];
143+
let current: Vec3;
144+
let last = this.getPoint(0);
145+
let sum = 0;
146+
147+
cache.push(0);
148+
149+
for (let p = 1; p <= divisions; p++) {
150+
current = this.getPoint(p / divisions);
151+
sum += lenVec3(subVec3(current, last, createVec3Float64()));
152+
cache.push(sum);
153+
last = current;
154+
}
155+
156+
this.cacheArcLengths = cache;
157+
return cache;
158+
}
159+
160+
/**
161+
* Invalidates cached arc-length data and rebuilds it.
162+
*/
163+
protected _updateArcLengths(): void {
164+
this.needsUpdate = true;
165+
this._getLengths();
166+
}
167+
168+
/**
169+
* Maps normalized arc-length parameter `u` to curve parameter `t`.
170+
*
171+
* This is useful when you want points spaced by distance along the curve
172+
* instead of by raw parameter value.
173+
*
174+
* @param u Normalized distance along the curve in the range `[0..1]`
175+
* @param distance Absolute distance along the curve. When provided, overrides `u`.
176+
* @returns Curve parameter in the range `[0..1]`
177+
*/
178+
getUToTMapping(u: number, distance?: number): number {
179+
const arcLengths = this._getLengths();
180+
let i = 0;
181+
const il = arcLengths.length;
182+
let t: number;
183+
let targetArcLength: number;
184+
185+
if (distance) {
186+
targetArcLength = distance;
187+
} else {
188+
targetArcLength = u * arcLengths[il - 1];
189+
}
190+
191+
let low = 0;
192+
let high = il - 1;
193+
let comparison: number;
194+
195+
while (low <= high) {
196+
i = Math.floor(low + (high - low) / 2);
197+
comparison = arcLengths[i] - targetArcLength;
198+
199+
if (comparison < 0) {
200+
low = i + 1;
201+
} else if (comparison > 0) {
202+
high = i - 1;
203+
} else {
204+
high = i;
205+
break;
206+
}
207+
}
208+
209+
i = high;
210+
211+
if (arcLengths[i] === targetArcLength) {
212+
t = i / (il - 1);
213+
return t;
214+
}
215+
216+
const lengthBefore = arcLengths[i];
217+
const lengthAfter = arcLengths[i + 1];
218+
const segmentLength = lengthAfter - lengthBefore;
219+
const segmentFraction = (targetArcLength - lengthBefore) / segmentLength;
220+
221+
t = (i + segmentFraction) / (il - 1);
222+
return t;
223+
}
224+
225+
/**
226+
* Samples the curve at parameter `t`.
227+
*
228+
* @param t Curve parameter in the range `[0..1]`
229+
* @returns Point on the curve
230+
*/
231+
abstract getPoint(t: number): Vec3;
232+
}
233+
234+
export { Curve };

0 commit comments

Comments
 (0)