curvy 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/axis.cjs +9 -320
- package/dist/axis.d.cts +1 -1
- package/dist/axis.d.mts +1 -1
- package/dist/axis.d.ts +1 -1
- package/dist/axis.mjs +3 -314
- package/dist/convert.d.cts +1 -1
- package/dist/convert.d.mts +1 -1
- package/dist/convert.d.ts +1 -1
- package/dist/curve.cjs +209 -36
- package/dist/curve.d.cts +5 -3
- package/dist/curve.d.mts +5 -3
- package/dist/curve.d.ts +5 -3
- package/dist/curve.mjs +209 -37
- package/dist/shared/curvy.7876e09e.mjs +323 -0
- package/dist/shared/curvy.db103082.cjs +335 -0
- package/dist/shared/{curvy.0f1371d7.d.cts → curvy.df73166c.d.cts} +3 -3
- package/dist/shared/{curvy.0f1371d7.d.mts → curvy.df73166c.d.mts} +3 -3
- package/dist/shared/{curvy.0f1371d7.d.ts → curvy.df73166c.d.ts} +3 -3
- package/dist/splines.d.cts +1 -1
- package/dist/splines.d.mts +1 -1
- package/dist/splines.d.ts +1 -1
- package/package.json +4 -2
package/dist/curve.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import invariant from 'tiny-invariant';
|
|
2
|
-
import { createCurveAxis } from './
|
|
2
|
+
import { c as computeCubicAntiderivative, a as createCurveAxis } from './shared/curvy.7876e09e.mjs';
|
|
3
3
|
import { r as remap, a as round } from './shared/curvy.9cd8b44c.mjs';
|
|
4
4
|
import { splines } from './splines.mjs';
|
|
5
5
|
|
|
@@ -61,42 +61,193 @@ function createIntervalTree(boundaries) {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function collectPointsOfInterest(axis) {
|
|
65
|
+
const samples = new Set(Array.from({ length: 33 }, (_, i) => i / 32));
|
|
66
|
+
const segmentToNormalized = 1 / axis.segments.length;
|
|
67
|
+
for (const [index, segment] of axis.segments.entries()) {
|
|
68
|
+
samples.add(index * segmentToNormalized);
|
|
69
|
+
samples.add((index + 1) * segmentToNormalized);
|
|
70
|
+
for (const t of segment.extrema) {
|
|
71
|
+
samples.add((index + t) * segmentToNormalized);
|
|
72
|
+
}
|
|
73
|
+
for (const root of segment.roots) {
|
|
74
|
+
samples.add((index + root) * segmentToNormalized);
|
|
75
|
+
}
|
|
76
|
+
for (const root of segment.derivative.roots) {
|
|
77
|
+
samples.add((index + root) * segmentToNormalized);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Array.from(samples).sort((a, b) => a - b);
|
|
81
|
+
}
|
|
82
|
+
function takeLinearIntegral(x1, x2, y1, y2) {
|
|
83
|
+
const m = (y2 - y1) / (x2 - x1);
|
|
84
|
+
const b = y1 - m * x1;
|
|
85
|
+
const s1 = x1 ** 2 * m / 2 + b * x1;
|
|
86
|
+
const s2 = x2 ** 2 * m / 2 + b * x2;
|
|
87
|
+
return Math.abs(s2 - s1);
|
|
88
|
+
}
|
|
89
|
+
function takePolynomialIntegral(coefficients, start, end) {
|
|
90
|
+
let s1 = 0;
|
|
91
|
+
let s2 = 0;
|
|
92
|
+
for (let i = 0; i < coefficients.length; i++) {
|
|
93
|
+
const power = coefficients.length - i;
|
|
94
|
+
s1 += coefficients[i] * start ** power / power;
|
|
95
|
+
s2 += coefficients[i] * end ** power / power;
|
|
96
|
+
}
|
|
97
|
+
return Math.abs(s2 - s1);
|
|
98
|
+
}
|
|
99
|
+
function* createAxisSampler(key, axis) {
|
|
100
|
+
const queue = collectPointsOfInterest(axis);
|
|
101
|
+
let totalLength = 0;
|
|
102
|
+
const lengthLookup = /* @__PURE__ */ new Map();
|
|
103
|
+
while (queue.length > 0) {
|
|
104
|
+
const current = queue[0];
|
|
105
|
+
const next = queue[1];
|
|
106
|
+
if (next === void 0) {
|
|
107
|
+
yield current;
|
|
108
|
+
lengthLookup.set(totalLength, current);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
const segment = axis.segments[Math.floor(current * axis.segments.length)];
|
|
112
|
+
const currentPosition = axis.solvePosition(current);
|
|
113
|
+
const nextPosition = axis.solvePosition(next);
|
|
114
|
+
const currentVelocity = axis.solveVelocity(current);
|
|
115
|
+
const nextVelocity = axis.solveVelocity(next);
|
|
116
|
+
const lutPositionIntegral = takeLinearIntegral(
|
|
117
|
+
current,
|
|
118
|
+
next,
|
|
119
|
+
currentPosition,
|
|
120
|
+
nextPosition
|
|
121
|
+
);
|
|
122
|
+
const curvePositionIntegral = takePolynomialIntegral(
|
|
123
|
+
computeCubicAntiderivative(segment.coefficients, 0),
|
|
124
|
+
current,
|
|
125
|
+
next
|
|
126
|
+
);
|
|
127
|
+
const lutVelocityIntegral = takeLinearIntegral(
|
|
128
|
+
current,
|
|
129
|
+
next,
|
|
130
|
+
currentVelocity,
|
|
131
|
+
nextVelocity
|
|
132
|
+
);
|
|
133
|
+
const curveVelocityIntegral = takePolynomialIntegral(
|
|
134
|
+
segment.derivative.coefficients,
|
|
135
|
+
current,
|
|
136
|
+
next
|
|
137
|
+
);
|
|
138
|
+
const error = Math.max(
|
|
139
|
+
Math.abs(lutPositionIntegral - curvePositionIntegral),
|
|
140
|
+
Math.abs(lutVelocityIntegral - curveVelocityIntegral)
|
|
141
|
+
);
|
|
142
|
+
if (error > 1) {
|
|
143
|
+
queue.splice(1, 0, (current + next) / 2);
|
|
144
|
+
} else {
|
|
145
|
+
yield current;
|
|
146
|
+
lengthLookup.set(totalLength, current);
|
|
147
|
+
totalLength += Math.hypot(current - next, currentPosition - nextPosition);
|
|
148
|
+
queue.shift();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return [
|
|
152
|
+
key,
|
|
153
|
+
{
|
|
154
|
+
length: totalLength,
|
|
155
|
+
lookup: lengthLookup
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
}
|
|
64
159
|
function distance(a, b) {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
160
|
+
const deltas = [];
|
|
161
|
+
for (const key of Object.keys(a)) {
|
|
162
|
+
deltas.push(a[key] - b[key]);
|
|
163
|
+
}
|
|
67
164
|
return Math.hypot(...deltas);
|
|
68
165
|
}
|
|
69
|
-
function
|
|
70
|
-
const
|
|
71
|
-
|
|
166
|
+
function createSampleBuffer() {
|
|
167
|
+
const values = /* @__PURE__ */ new Map();
|
|
168
|
+
return {
|
|
169
|
+
get size() {
|
|
170
|
+
return values.size;
|
|
171
|
+
},
|
|
172
|
+
add(key, value) {
|
|
173
|
+
if (!values.has(key)) {
|
|
174
|
+
values.set(key, /* @__PURE__ */ new Set());
|
|
175
|
+
}
|
|
176
|
+
values.get(key)?.add(value);
|
|
177
|
+
},
|
|
178
|
+
get(key) {
|
|
179
|
+
return values.get(key);
|
|
180
|
+
},
|
|
181
|
+
remove(key, value) {
|
|
182
|
+
values.get(key)?.delete(value);
|
|
183
|
+
if (values.get(key)?.size === 0) {
|
|
184
|
+
values.delete(key);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
keys() {
|
|
188
|
+
return Array.from(values.keys());
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function createLengthLookup({
|
|
193
|
+
length: totalLength,
|
|
194
|
+
lookup
|
|
195
|
+
}) {
|
|
196
|
+
const { search } = createIntervalTree(Array.from(lookup.keys()));
|
|
197
|
+
return (normalized) => {
|
|
198
|
+
invariant(normalized >= 0 && normalized <= 1, "Length out of bounds");
|
|
199
|
+
const length = normalized * totalLength;
|
|
200
|
+
const result = search(length);
|
|
201
|
+
invariant(result, "Length not found");
|
|
202
|
+
return remap(
|
|
203
|
+
length,
|
|
204
|
+
result[0],
|
|
205
|
+
result[1],
|
|
206
|
+
lookup.get(result[0]),
|
|
207
|
+
lookup.get(result[1])
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function createCurveLengthLookups(axes, positionAt) {
|
|
72
212
|
const lengthLookup = /* @__PURE__ */ new Map();
|
|
213
|
+
const axesLookups = {};
|
|
214
|
+
const sampleBuffer = createSampleBuffer();
|
|
215
|
+
for (const [key, axis] of Object.entries(axes)) {
|
|
216
|
+
const sampler = createAxisSampler(key, axis);
|
|
217
|
+
const first = sampler.next();
|
|
218
|
+
if (first.done) {
|
|
219
|
+
axesLookups[first.value[0]] = createLengthLookup(first.value[1]);
|
|
220
|
+
} else {
|
|
221
|
+
sampleBuffer.add(first.value, sampler);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
73
224
|
let lastPoint = positionAt(0);
|
|
74
225
|
let length = 0;
|
|
75
226
|
lengthLookup.set(0, 0);
|
|
76
|
-
|
|
227
|
+
while (sampleBuffer.size > 0) {
|
|
228
|
+
const t = Math.min(...sampleBuffer.keys());
|
|
77
229
|
const point = positionAt(t);
|
|
78
230
|
length = length + distance(lastPoint, point);
|
|
79
231
|
lengthLookup.set(length, t);
|
|
80
232
|
lastPoint = point;
|
|
233
|
+
const samplers = sampleBuffer.get(t);
|
|
234
|
+
for (const sampler of samplers) {
|
|
235
|
+
const next = sampler.next();
|
|
236
|
+
if (next.done) {
|
|
237
|
+
axesLookups[next.value[0]] = createLengthLookup(next.value[1]);
|
|
238
|
+
} else {
|
|
239
|
+
sampleBuffer.add(next.value, sampler);
|
|
240
|
+
}
|
|
241
|
+
sampleBuffer.remove(t, sampler);
|
|
242
|
+
}
|
|
81
243
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
invariant(normalized >= 0 && normalized <= 1, "Length out of bounds");
|
|
86
|
-
const length2 = normalized * totalLength;
|
|
87
|
-
const result = search(length2);
|
|
88
|
-
invariant(result, "Length not found");
|
|
89
|
-
return remap(
|
|
90
|
-
length2,
|
|
91
|
-
result[0],
|
|
92
|
-
result[1],
|
|
93
|
-
lengthLookup.get(result[0]),
|
|
94
|
-
lengthLookup.get(result[1])
|
|
95
|
-
);
|
|
244
|
+
return {
|
|
245
|
+
curve: createLengthLookup({ length, lookup: lengthLookup }),
|
|
246
|
+
axes: axesLookups
|
|
96
247
|
};
|
|
97
248
|
}
|
|
98
249
|
|
|
99
|
-
function
|
|
250
|
+
function convertPointsToAxes(points, spline) {
|
|
100
251
|
const axisKeys = Object.keys(points[0]);
|
|
101
252
|
const axisPoints = {};
|
|
102
253
|
for (const key of axisKeys) {
|
|
@@ -112,6 +263,10 @@ function createCurve(points, spline) {
|
|
|
112
263
|
for (const key of axisKeys) {
|
|
113
264
|
axes[key] = createCurveAxis(spline.chunkCoefficients(axisPoints[key]));
|
|
114
265
|
}
|
|
266
|
+
return axes;
|
|
267
|
+
}
|
|
268
|
+
function createCurve(axes) {
|
|
269
|
+
const axisKeys = Object.keys(axes);
|
|
115
270
|
const solveT = (t, precision = 12) => {
|
|
116
271
|
const rounded = round(t);
|
|
117
272
|
invariant(
|
|
@@ -124,7 +279,7 @@ function createCurve(points, spline) {
|
|
|
124
279
|
}
|
|
125
280
|
return position;
|
|
126
281
|
};
|
|
127
|
-
const
|
|
282
|
+
const lengthLookups = createCurveLengthLookups(axes, solveT);
|
|
128
283
|
const positionAt = (input, normalize = 1, precision = 12) => {
|
|
129
284
|
invariant(
|
|
130
285
|
input >= 0 && input <= 1,
|
|
@@ -132,7 +287,23 @@ function createCurve(points, spline) {
|
|
|
132
287
|
);
|
|
133
288
|
let t = input;
|
|
134
289
|
if (normalize > 0) {
|
|
135
|
-
const denormalizedT =
|
|
290
|
+
const denormalizedT = lengthLookups.curve(input);
|
|
291
|
+
if (normalize === 1) {
|
|
292
|
+
t = denormalizedT;
|
|
293
|
+
} else {
|
|
294
|
+
t = (1 - normalize) * input + normalize * denormalizedT;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return solveT(t, precision);
|
|
298
|
+
};
|
|
299
|
+
const solveAlong = (axis, input, normalize = 1, precision = 12) => {
|
|
300
|
+
invariant(
|
|
301
|
+
input >= 0 && input <= 1,
|
|
302
|
+
`Input must be between 0 and 1, got ${input}`
|
|
303
|
+
);
|
|
304
|
+
let t = input;
|
|
305
|
+
if (normalize > 0) {
|
|
306
|
+
const denormalizedT = lengthLookups.axes[axis](input);
|
|
136
307
|
if (normalize === 1) {
|
|
137
308
|
t = denormalizedT;
|
|
138
309
|
} else {
|
|
@@ -149,34 +320,35 @@ function createCurve(points, spline) {
|
|
|
149
320
|
typeof t === "number",
|
|
150
321
|
`Could not solve for ${axis} = ${position}`
|
|
151
322
|
);
|
|
152
|
-
|
|
153
|
-
for (const key of axisKeys) {
|
|
154
|
-
if (key !== axis) {
|
|
155
|
-
result[key] = round(axes[key].solvePosition(t), precision);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return result;
|
|
323
|
+
return positionAt(t, 0, precision);
|
|
159
324
|
};
|
|
160
325
|
return {
|
|
161
326
|
axes,
|
|
162
327
|
positionAt,
|
|
328
|
+
solveAlong,
|
|
163
329
|
solveWhere
|
|
164
330
|
};
|
|
165
331
|
}
|
|
166
332
|
function createBasisCurve(points, triplicateEndpoints = true) {
|
|
167
|
-
return createCurve(
|
|
333
|
+
return createCurve(
|
|
334
|
+
convertPointsToAxes(points, splines.basis(triplicateEndpoints))
|
|
335
|
+
);
|
|
168
336
|
}
|
|
169
337
|
function createBezierCurve(points) {
|
|
170
|
-
return createCurve(points, splines.bezier);
|
|
338
|
+
return createCurve(convertPointsToAxes(points, splines.bezier));
|
|
171
339
|
}
|
|
172
340
|
function createCardinalCurve(points, tension = 0.5, duplicateEndpoints = true) {
|
|
173
|
-
return createCurve(
|
|
341
|
+
return createCurve(
|
|
342
|
+
convertPointsToAxes(points, splines.cardinal(tension, duplicateEndpoints))
|
|
343
|
+
);
|
|
174
344
|
}
|
|
175
345
|
function createCatmullRomCurve(points, duplicateEndpoints = true) {
|
|
176
|
-
return createCurve(
|
|
346
|
+
return createCurve(
|
|
347
|
+
convertPointsToAxes(points, splines.catmullRom(duplicateEndpoints))
|
|
348
|
+
);
|
|
177
349
|
}
|
|
178
350
|
function createHermiteCurve(points) {
|
|
179
|
-
return createCurve(points, splines.hermite);
|
|
351
|
+
return createCurve(convertPointsToAxes(points, splines.hermite));
|
|
180
352
|
}
|
|
181
353
|
|
|
182
|
-
export { createBasisCurve, createBezierCurve, createCardinalCurve, createCatmullRomCurve, createHermiteCurve };
|
|
354
|
+
export { createBasisCurve, createBezierCurve, createCardinalCurve, createCatmullRomCurve, createCurve, createHermiteCurve };
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import invariant from 'tiny-invariant';
|
|
2
|
+
import { splines } from '../splines.mjs';
|
|
3
|
+
|
|
4
|
+
const UNIT_INTERVAL = [0, 1];
|
|
5
|
+
function createLinearPolynomial(coefficients, domain = UNIT_INTERVAL) {
|
|
6
|
+
const [a, b] = coefficients;
|
|
7
|
+
invariant(domain[0] <= domain[1], "invalid domain");
|
|
8
|
+
let root = a === 0 ? null : b === 0 ? 0 : -b / a;
|
|
9
|
+
if (root !== null && (root < domain[0] || root > domain[1])) {
|
|
10
|
+
root = null;
|
|
11
|
+
}
|
|
12
|
+
const range = a >= 0 ? [a * domain[0] + b, a * domain[1] + b] : [a * domain[1] + b, a * domain[0] + b];
|
|
13
|
+
const solve = (x) => {
|
|
14
|
+
invariant(x >= domain[0] && x <= domain[1], "x out of domain");
|
|
15
|
+
return a * x + b;
|
|
16
|
+
};
|
|
17
|
+
const solveInverse = (y) => {
|
|
18
|
+
invariant(y >= range[0] && y <= range[1], "y out of range");
|
|
19
|
+
return a === 0 ? 0 : (y - b) / a;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
domain,
|
|
23
|
+
range,
|
|
24
|
+
monotonicity: a === 0 ? "constant" : a > 0 ? "increasing" : "decreasing",
|
|
25
|
+
coefficients,
|
|
26
|
+
root,
|
|
27
|
+
solve,
|
|
28
|
+
solveInverse
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createQuadraticRootSolver(coefficients, defaultDomain, range) {
|
|
32
|
+
const [a, b, c] = coefficients;
|
|
33
|
+
if (a !== 0) {
|
|
34
|
+
return function solveInverse(y, domain = defaultDomain) {
|
|
35
|
+
invariant(domain[0] <= domain[1], "invalid domain");
|
|
36
|
+
invariant(y >= range[0] && y <= range[1], "y out of range");
|
|
37
|
+
const discriminant = b ** 2 - 4 * a * (c - y);
|
|
38
|
+
if (discriminant < 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
if (discriminant === 0) {
|
|
42
|
+
const root = -b / (2 * a);
|
|
43
|
+
return root < domain[0] || root > domain[1] ? [] : [root];
|
|
44
|
+
}
|
|
45
|
+
const sqrtDiscriminant = Math.sqrt(discriminant);
|
|
46
|
+
const denominator = 2 * a;
|
|
47
|
+
const left = (-b - sqrtDiscriminant) / denominator;
|
|
48
|
+
const right = (-b + sqrtDiscriminant) / denominator;
|
|
49
|
+
const leftInDomain = left >= domain[0] && left <= domain[1];
|
|
50
|
+
const rightInDomain = right >= domain[0] && right <= domain[1];
|
|
51
|
+
if (leftInDomain && rightInDomain) {
|
|
52
|
+
return left < right ? [left, right] : [right, left];
|
|
53
|
+
}
|
|
54
|
+
if (leftInDomain) {
|
|
55
|
+
return [left];
|
|
56
|
+
}
|
|
57
|
+
if (rightInDomain) {
|
|
58
|
+
return [right];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (b !== 0) {
|
|
64
|
+
return function solveInverse(y, domain = defaultDomain) {
|
|
65
|
+
invariant(domain[0] <= domain[1], "invalid domain");
|
|
66
|
+
invariant(y >= range[0] && y <= range[1], "y out of range");
|
|
67
|
+
const root = -(c - y) / b;
|
|
68
|
+
return root < domain[0] || root > domain[1] ? [] : [root];
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return function solveInverse() {
|
|
72
|
+
return [];
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function createQuadraticPolynomial(coefficients, domain = UNIT_INTERVAL) {
|
|
76
|
+
const [a, b, c] = coefficients;
|
|
77
|
+
invariant(domain[0] <= domain[1], "invalid domain");
|
|
78
|
+
const derivative = createLinearPolynomial([2 * a, b], domain);
|
|
79
|
+
const extreme = derivative.root;
|
|
80
|
+
const solve = (x) => {
|
|
81
|
+
invariant(x >= domain[0] && x <= domain[1], "x out of domain");
|
|
82
|
+
return a * x ** 2 + b * x + c;
|
|
83
|
+
};
|
|
84
|
+
const rangeCandidates = extreme === null ? [solve(domain[0]), solve(domain[1])] : [solve(domain[0]), solve(extreme), solve(domain[1])];
|
|
85
|
+
const range = [
|
|
86
|
+
Math.min(...rangeCandidates),
|
|
87
|
+
Math.max(...rangeCandidates)
|
|
88
|
+
];
|
|
89
|
+
const solveInverse = createQuadraticRootSolver(coefficients, domain, range);
|
|
90
|
+
const roots = range[0] > 0 || range[1] < 0 ? [] : solveInverse(0);
|
|
91
|
+
return {
|
|
92
|
+
domain,
|
|
93
|
+
range,
|
|
94
|
+
coefficients,
|
|
95
|
+
monotonicity: a !== 0 ? "none" : b === 0 ? "constant" : b > 0 ? "increasing" : "decreasing",
|
|
96
|
+
extreme,
|
|
97
|
+
roots,
|
|
98
|
+
derivative,
|
|
99
|
+
solve,
|
|
100
|
+
solveInverse
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const TWO_THIRDS_PI = 2 * Math.PI / 3;
|
|
104
|
+
function createCubicRootSolver(coefficients, defaultDomain, range) {
|
|
105
|
+
if (coefficients[0] === 0) {
|
|
106
|
+
return createQuadraticRootSolver(
|
|
107
|
+
[coefficients[1], coefficients[2], coefficients[3]],
|
|
108
|
+
defaultDomain,
|
|
109
|
+
range
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const oneOverA = 1 / coefficients[0];
|
|
113
|
+
const a0 = coefficients[1] * oneOverA;
|
|
114
|
+
const a1 = coefficients[2] * oneOverA;
|
|
115
|
+
const a2OverThree = a0 / 3;
|
|
116
|
+
const q = a1 / 3 - a0 ** 2 / 9;
|
|
117
|
+
const r0 = a1 * a0 / 6 - a0 ** 3 / 27;
|
|
118
|
+
return function solveInverse(y, domain = defaultDomain) {
|
|
119
|
+
invariant(domain[0] <= domain[1], "invalid domain");
|
|
120
|
+
invariant(y >= range[0] && y <= range[1], "y out of range");
|
|
121
|
+
const a02 = (coefficients[3] - y) * oneOverA;
|
|
122
|
+
const r = r0 - a02 * 0.5;
|
|
123
|
+
const discriminant = q ** 3 + r ** 2;
|
|
124
|
+
if (discriminant > 0) {
|
|
125
|
+
const a = Math.cbrt(Math.abs(r) + Math.sqrt(discriminant));
|
|
126
|
+
const root = (r < 0 ? q / a - a : a - q / a) - a2OverThree;
|
|
127
|
+
return root < domain[0] || root > domain[1] ? [] : [root];
|
|
128
|
+
}
|
|
129
|
+
const theta = Math.acos(r / Math.sqrt((-q) ** 3)) / 3;
|
|
130
|
+
const twoRootQ = 2 * Math.sqrt(-q);
|
|
131
|
+
const root1 = twoRootQ * Math.cos(theta - TWO_THIRDS_PI) - a2OverThree;
|
|
132
|
+
const root2 = twoRootQ * Math.cos(theta) - a2OverThree;
|
|
133
|
+
const root3 = twoRootQ * Math.cos(theta + TWO_THIRDS_PI) - a2OverThree;
|
|
134
|
+
const candidates = [root1, root2, root3];
|
|
135
|
+
const roots = [];
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
if (candidate >= domain[0] && candidate <= domain[1] && !roots.includes(candidate)) {
|
|
138
|
+
roots.push(candidate);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
roots.sort((a, b) => a - b);
|
|
142
|
+
return roots;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function getMonotonicity(extrema, derivative, domain) {
|
|
146
|
+
if (derivative.coefficients[0] === 0 && derivative.coefficients[1] === 0 && derivative.coefficients[2] === 0) {
|
|
147
|
+
return "constant";
|
|
148
|
+
}
|
|
149
|
+
let hasInnerExtrema = false;
|
|
150
|
+
for (const extreme of extrema) {
|
|
151
|
+
if (extreme > domain[0] && extreme > domain[1]) {
|
|
152
|
+
hasInnerExtrema = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (hasInnerExtrema) {
|
|
157
|
+
return "none";
|
|
158
|
+
}
|
|
159
|
+
let test = derivative.solve(domain[0]);
|
|
160
|
+
if (test === 0) {
|
|
161
|
+
test = derivative.solve((domain[0] + domain[1]) * 0.5);
|
|
162
|
+
}
|
|
163
|
+
return Math.sign(test) > 0 ? "increasing" : "decreasing";
|
|
164
|
+
}
|
|
165
|
+
function createCubicPolynomial(coefficients, domain = UNIT_INTERVAL) {
|
|
166
|
+
invariant(domain[0] <= domain[1], "invalid domain");
|
|
167
|
+
const [a, b, c, d] = coefficients;
|
|
168
|
+
const derivative = createQuadraticPolynomial([3 * a, 2 * b, c], domain);
|
|
169
|
+
const extrema = derivative.roots.length === 1 && derivative.coefficients[0] !== 0 ? [] : derivative.roots;
|
|
170
|
+
const solve = (x) => {
|
|
171
|
+
invariant(x >= domain[0] && x <= domain[1], "x out of domain");
|
|
172
|
+
return a * x ** 3 + b * x ** 2 + c * x + d;
|
|
173
|
+
};
|
|
174
|
+
const rangeCandidates = [
|
|
175
|
+
solve(domain[0]),
|
|
176
|
+
...extrema.map((extreme) => solve(extreme)),
|
|
177
|
+
solve(domain[1])
|
|
178
|
+
];
|
|
179
|
+
const range = [
|
|
180
|
+
Math.min(...rangeCandidates),
|
|
181
|
+
Math.max(...rangeCandidates)
|
|
182
|
+
];
|
|
183
|
+
const solveInverse = createCubicRootSolver(coefficients, domain, range);
|
|
184
|
+
const roots = range[0] > 0 || range[1] < 0 ? [] : solveInverse(0, domain);
|
|
185
|
+
const monotonicity = getMonotonicity(extrema, derivative, domain);
|
|
186
|
+
return {
|
|
187
|
+
domain,
|
|
188
|
+
range,
|
|
189
|
+
monotonicity,
|
|
190
|
+
coefficients,
|
|
191
|
+
extrema,
|
|
192
|
+
roots,
|
|
193
|
+
derivative,
|
|
194
|
+
solve,
|
|
195
|
+
solveInverse
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function computeCubicAntiderivative(coefficients, integrationConstant) {
|
|
199
|
+
return [
|
|
200
|
+
coefficients[0] / 4,
|
|
201
|
+
coefficients[1] / 3,
|
|
202
|
+
coefficients[2] / 2,
|
|
203
|
+
coefficients[3],
|
|
204
|
+
integrationConstant
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getCurveExtrema(lookup) {
|
|
209
|
+
const extremaLookup = new Map(lookup);
|
|
210
|
+
const extremaCandidates = Array.from(extremaLookup.keys());
|
|
211
|
+
for (const [index, t] of extremaCandidates.entries()) {
|
|
212
|
+
if (index === 0 || index === extremaCandidates.length - 1) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const previous = extremaLookup.get(extremaCandidates[index - 1]);
|
|
216
|
+
const current = extremaLookup.get(t);
|
|
217
|
+
const next = extremaLookup.get(extremaCandidates[index + 1]);
|
|
218
|
+
if (previous <= current && current < next || previous >= current && current > next) {
|
|
219
|
+
extremaLookup.delete(t);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const extrema = Array.from(extremaLookup.keys());
|
|
223
|
+
return extrema;
|
|
224
|
+
}
|
|
225
|
+
function createCurveAxis(coefficients) {
|
|
226
|
+
invariant(coefficients.length > 0, "Curve must have at least one segment");
|
|
227
|
+
const segments = [];
|
|
228
|
+
const extremaLookup = /* @__PURE__ */ new Map();
|
|
229
|
+
let monotonicity = "none";
|
|
230
|
+
const normalizedToCurve = coefficients.length;
|
|
231
|
+
const curveToNormalized = 1 / coefficients.length;
|
|
232
|
+
for (let i = 0; i < coefficients.length; i++) {
|
|
233
|
+
const segment = createCubicPolynomial(coefficients[i], UNIT_INTERVAL);
|
|
234
|
+
segments.push(segment);
|
|
235
|
+
extremaLookup.set(i * curveToNormalized, segment.solve(0));
|
|
236
|
+
for (const extreme of segment.extrema) {
|
|
237
|
+
extremaLookup.set(
|
|
238
|
+
(i + extreme) * curveToNormalized,
|
|
239
|
+
segment.solve(extreme)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (i === 0) {
|
|
243
|
+
monotonicity = segment.monotonicity;
|
|
244
|
+
} else if (monotonicity !== segment.monotonicity) {
|
|
245
|
+
monotonicity = "none";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
extremaLookup.set(1, segments[segments.length - 1].solve(1));
|
|
249
|
+
const extrema = getCurveExtrema(extremaLookup);
|
|
250
|
+
const range = [
|
|
251
|
+
Math.min(...extremaLookup.values()),
|
|
252
|
+
Math.max(...extremaLookup.values())
|
|
253
|
+
];
|
|
254
|
+
const getSegmentAndInput = (t) => {
|
|
255
|
+
invariant(t >= 0 && t <= 1, "t out of range");
|
|
256
|
+
if (t === 1) {
|
|
257
|
+
return { segment: segments[segments.length - 1], input: 1 };
|
|
258
|
+
}
|
|
259
|
+
const curveT = t * normalizedToCurve;
|
|
260
|
+
const index = Math.floor(curveT);
|
|
261
|
+
const segment = segments[index];
|
|
262
|
+
const input = curveT - index;
|
|
263
|
+
return { segment, input };
|
|
264
|
+
};
|
|
265
|
+
const solvePosition = (t) => {
|
|
266
|
+
const { segment, input } = getSegmentAndInput(t);
|
|
267
|
+
return segment.solve(input);
|
|
268
|
+
};
|
|
269
|
+
const solveVelocity = (t) => {
|
|
270
|
+
const { segment, input } = getSegmentAndInput(t);
|
|
271
|
+
return segment.derivative.solve(input);
|
|
272
|
+
};
|
|
273
|
+
const solveT = (position, domain) => {
|
|
274
|
+
invariant(
|
|
275
|
+
position >= range[0] && position <= range[1],
|
|
276
|
+
"position out of range"
|
|
277
|
+
);
|
|
278
|
+
invariant(domain[0] <= domain[1], "invalid domain");
|
|
279
|
+
const result = [];
|
|
280
|
+
for (const [index, segment] of segments.entries()) {
|
|
281
|
+
if (position >= segment.range[0] && position <= segment.range[1]) {
|
|
282
|
+
const t = segment.solveInverse(position, domain);
|
|
283
|
+
for (const tValue of t) {
|
|
284
|
+
result.push((index + tValue) * curveToNormalized);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
domain: UNIT_INTERVAL,
|
|
292
|
+
range,
|
|
293
|
+
monotonicity,
|
|
294
|
+
extrema,
|
|
295
|
+
segments,
|
|
296
|
+
solvePosition,
|
|
297
|
+
solveVelocity,
|
|
298
|
+
solveT
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function createBasisAxis(parameters, triplicateEndpoints = true) {
|
|
302
|
+
return createCurveAxis(
|
|
303
|
+
splines.basis(triplicateEndpoints).chunkCoefficients(parameters)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
function createBezierAxis(parameters) {
|
|
307
|
+
return createCurveAxis(splines.bezier.chunkCoefficients(parameters));
|
|
308
|
+
}
|
|
309
|
+
function createCardinalAxis(parameters, a = 0.5, duplicateEndpoints = true) {
|
|
310
|
+
return createCurveAxis(
|
|
311
|
+
splines.cardinal(a, duplicateEndpoints).chunkCoefficients(parameters)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
function createCatmullRomAxis(parameters, duplicateEndpoints = true) {
|
|
315
|
+
return createCurveAxis(
|
|
316
|
+
splines.catmullRom(duplicateEndpoints).chunkCoefficients(parameters)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
function createHermiteAxis(parameters) {
|
|
320
|
+
return createCurveAxis(splines.hermite.chunkCoefficients(parameters));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export { createCurveAxis as a, createBasisAxis as b, computeCubicAntiderivative as c, createBezierAxis as d, createCardinalAxis as e, createCatmullRomAxis as f, createHermiteAxis as g };
|