tensum 0.1.0
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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/dist/coupled-system.d.ts +50 -0
- package/dist/coupled-system.d.ts.map +1 -0
- package/dist/coupled-system.js +269 -0
- package/dist/coupled-system.js.map +1 -0
- package/dist/coupled.d.ts +3 -0
- package/dist/coupled.d.ts.map +1 -0
- package/dist/coupled.js +2 -0
- package/dist/coupled.js.map +1 -0
- package/dist/css-linear.d.ts +20 -0
- package/dist/css-linear.d.ts.map +1 -0
- package/dist/css-linear.js +95 -0
- package/dist/css-linear.js.map +1 -0
- package/dist/css.d.ts +3 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +2 -0
- package/dist/css.js.map +1 -0
- package/dist/gsap/active-tracks.d.ts +32 -0
- package/dist/gsap/active-tracks.d.ts.map +1 -0
- package/dist/gsap/active-tracks.js +177 -0
- package/dist/gsap/active-tracks.js.map +1 -0
- package/dist/gsap/gsap-time.d.ts +10 -0
- package/dist/gsap/gsap-time.d.ts.map +1 -0
- package/dist/gsap/gsap-time.js +62 -0
- package/dist/gsap/gsap-time.js.map +1 -0
- package/dist/gsap/plugin-tween-coordinator.d.ts +30 -0
- package/dist/gsap/plugin-tween-coordinator.d.ts.map +1 -0
- package/dist/gsap/plugin-tween-coordinator.js +72 -0
- package/dist/gsap/plugin-tween-coordinator.js.map +1 -0
- package/dist/gsap/plugin.d.ts +84 -0
- package/dist/gsap/plugin.d.ts.map +1 -0
- package/dist/gsap/plugin.js +552 -0
- package/dist/gsap/plugin.js.map +1 -0
- package/dist/gsap/spring-to.d.ts +93 -0
- package/dist/gsap/spring-to.d.ts.map +1 -0
- package/dist/gsap/spring-to.js +569 -0
- package/dist/gsap/spring-to.js.map +1 -0
- package/dist/gsap/spring-track-policy.d.ts +24 -0
- package/dist/gsap/spring-track-policy.d.ts.map +1 -0
- package/dist/gsap/spring-track-policy.js +76 -0
- package/dist/gsap/spring-track-policy.js.map +1 -0
- package/dist/gsap/track-lifecycle.d.ts +16 -0
- package/dist/gsap/track-lifecycle.d.ts.map +1 -0
- package/dist/gsap/track-lifecycle.js +33 -0
- package/dist/gsap/track-lifecycle.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +9 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +38 -0
- package/dist/math.js.map +1 -0
- package/dist/model.d.ts +14 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +38 -0
- package/dist/model.js.map +1 -0
- package/dist/motion-compatibility.d.ts +18 -0
- package/dist/motion-compatibility.d.ts.map +1 -0
- package/dist/motion-compatibility.js +83 -0
- package/dist/motion-compatibility.js.map +1 -0
- package/dist/parameters.d.ts +52 -0
- package/dist/parameters.d.ts.map +1 -0
- package/dist/parameters.js +124 -0
- package/dist/parameters.js.map +1 -0
- package/dist/settling.d.ts +12 -0
- package/dist/settling.d.ts.map +1 -0
- package/dist/settling.js +107 -0
- package/dist/settling.js.map +1 -0
- package/dist/solver.d.ts +17 -0
- package/dist/solver.d.ts.map +1 -0
- package/dist/solver.js +190 -0
- package/dist/solver.js.map +1 -0
- package/dist/spring.d.ts +9 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +91 -0
- package/dist/spring.js.map +1 -0
- package/dist/support/additive-spring-value.d.ts +39 -0
- package/dist/support/additive-spring-value.d.ts.map +1 -0
- package/dist/support/additive-spring-value.js +281 -0
- package/dist/support/additive-spring-value.js.map +1 -0
- package/dist/support/inertia.d.ts +34 -0
- package/dist/support/inertia.d.ts.map +1 -0
- package/dist/support/inertia.js +293 -0
- package/dist/support/inertia.js.map +1 -0
- package/dist/support/keyframes.d.ts +34 -0
- package/dist/support/keyframes.d.ts.map +1 -0
- package/dist/support/keyframes.js +117 -0
- package/dist/support/keyframes.js.map +1 -0
- package/dist/support/spring-value.d.ts +38 -0
- package/dist/support/spring-value.d.ts.map +1 -0
- package/dist/support/spring-value.js +289 -0
- package/dist/support/spring-value.js.map +1 -0
- package/dist/support/velocity.d.ts +14 -0
- package/dist/support/velocity.d.ts.map +1 -0
- package/dist/support/velocity.js +75 -0
- package/dist/support/velocity.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vector.d.ts +32 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +121 -0
- package/dist/vector.js.map +1 -0
- package/package.json +68 -0
- package/src/coupled-system.ts +383 -0
- package/src/coupled.ts +10 -0
- package/src/css-linear.ts +148 -0
- package/src/css.ts +6 -0
- package/src/gsap/active-tracks.ts +244 -0
- package/src/gsap/gsap-time.ts +91 -0
- package/src/gsap/plugin-tween-coordinator.ts +112 -0
- package/src/gsap/plugin.ts +861 -0
- package/src/gsap/spring-to.ts +836 -0
- package/src/gsap/spring-track-policy.ts +133 -0
- package/src/gsap/track-lifecycle.ts +53 -0
- package/src/index.ts +130 -0
- package/src/math.ts +46 -0
- package/src/model.ts +70 -0
- package/src/motion-compatibility.ts +131 -0
- package/src/parameters.ts +226 -0
- package/src/settling.ts +141 -0
- package/src/solver.ts +262 -0
- package/src/spring.ts +119 -0
- package/src/support/additive-spring-value.ts +343 -0
- package/src/support/inertia.ts +367 -0
- package/src/support/keyframes.ts +188 -0
- package/src/support/spring-value.ts +399 -0
- package/src/support/velocity.ts +119 -0
- package/src/types.ts +70 -0
- package/src/vector.ts +191 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { resolveSettlingOptions } from '../settling.js';
|
|
2
|
+
import { createSpring } from '../spring.js';
|
|
3
|
+
import { validateSpringParameters } from '../solver.js';
|
|
4
|
+
import type {
|
|
5
|
+
SettlingResult,
|
|
6
|
+
SpringParameters,
|
|
7
|
+
SpringSettleInput,
|
|
8
|
+
SpringSolution,
|
|
9
|
+
SpringState,
|
|
10
|
+
} from '../types.js';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BOUNDARY_SPRING: Readonly<SpringParameters> = Object.freeze({
|
|
13
|
+
mass: 1,
|
|
14
|
+
stiffness: 500,
|
|
15
|
+
damping: 40,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export interface InertiaOptions {
|
|
19
|
+
from: number;
|
|
20
|
+
velocity: number;
|
|
21
|
+
timeConstant?: number;
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
modifyTarget?: (naturalTarget: number) => number;
|
|
25
|
+
boundarySpring?: SpringParameters;
|
|
26
|
+
settle?: SpringSettleInput;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InertiaBoundaryTransition {
|
|
30
|
+
readonly time: number;
|
|
31
|
+
readonly bound: number;
|
|
32
|
+
readonly incomingVelocity: number;
|
|
33
|
+
readonly spring: SpringSolution;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type InertiaPhase = 'decay' | 'spring' | 'complete';
|
|
37
|
+
|
|
38
|
+
export interface InertiaSolution {
|
|
39
|
+
readonly initialState: Readonly<SpringState>;
|
|
40
|
+
readonly naturalTarget: number;
|
|
41
|
+
readonly target: number;
|
|
42
|
+
readonly duration: number;
|
|
43
|
+
readonly settled: boolean;
|
|
44
|
+
readonly boundary?: Readonly<InertiaBoundaryTransition>;
|
|
45
|
+
stateAt(time: number): SpringState;
|
|
46
|
+
positionAt(time: number): number;
|
|
47
|
+
velocityAt(time: number): number;
|
|
48
|
+
phaseAt(time: number): InertiaPhase;
|
|
49
|
+
getSettlingResult(): SettlingResult;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertFinite(name: string, value: number): void {
|
|
53
|
+
if (!Number.isFinite(value)) {
|
|
54
|
+
throw new RangeError(`${name} must be a finite number`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertPositive(name: string, value: number): void {
|
|
59
|
+
assertFinite(name, value);
|
|
60
|
+
if (value <= 0) throw new RangeError(`${name} must be greater than 0`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function finiteState(
|
|
64
|
+
position: number,
|
|
65
|
+
velocity: number,
|
|
66
|
+
suffix = '',
|
|
67
|
+
): SpringState {
|
|
68
|
+
assertFinite(`position${suffix}`, position);
|
|
69
|
+
assertFinite(`velocity${suffix}`, velocity);
|
|
70
|
+
return { position, velocity };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function snapToGrid(size: number, origin = 0): (target: number) => number {
|
|
74
|
+
assertPositive('grid size', size);
|
|
75
|
+
assertFinite('grid origin', origin);
|
|
76
|
+
return (target: number): number => {
|
|
77
|
+
assertFinite('target', target);
|
|
78
|
+
const result = origin + Math.round((target - origin) / size) * size;
|
|
79
|
+
assertFinite('snapped target', result);
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createInertia(options: InertiaOptions): InertiaSolution {
|
|
85
|
+
assertFinite('from', options.from);
|
|
86
|
+
assertFinite('velocity', options.velocity);
|
|
87
|
+
const timeConstant =
|
|
88
|
+
options.timeConstant === undefined ? 0.7 : options.timeConstant;
|
|
89
|
+
assertPositive('timeConstant', timeConstant);
|
|
90
|
+
if (options.min !== undefined) assertFinite('min', options.min);
|
|
91
|
+
if (options.max !== undefined) assertFinite('max', options.max);
|
|
92
|
+
if (
|
|
93
|
+
options.min !== undefined &&
|
|
94
|
+
options.max !== undefined &&
|
|
95
|
+
options.min > options.max
|
|
96
|
+
) {
|
|
97
|
+
throw new RangeError('min must be less than or equal to max');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const settling = resolveSettlingOptions(options.settle);
|
|
101
|
+
if (options.boundarySpring !== undefined) {
|
|
102
|
+
validateSpringParameters(options.boundarySpring);
|
|
103
|
+
}
|
|
104
|
+
const naturalTarget = options.from + options.velocity * timeConstant;
|
|
105
|
+
assertFinite('naturalTarget', naturalTarget);
|
|
106
|
+
const target =
|
|
107
|
+
options.modifyTarget === undefined
|
|
108
|
+
? naturalTarget
|
|
109
|
+
: options.modifyTarget(naturalTarget);
|
|
110
|
+
assertFinite('modified target', target);
|
|
111
|
+
const displacement = options.from - target;
|
|
112
|
+
assertFinite('displacement', displacement);
|
|
113
|
+
const linearCoefficient = options.velocity + displacement / timeConstant;
|
|
114
|
+
assertFinite('linear coefficient', linearCoefficient);
|
|
115
|
+
|
|
116
|
+
const decayStateAt = (time: number): SpringState => {
|
|
117
|
+
if (time === 0) return { position: options.from, velocity: options.velocity };
|
|
118
|
+
const decay = Math.exp(-time / timeConstant);
|
|
119
|
+
if (decay === 0) return { position: target, velocity: 0 };
|
|
120
|
+
const timeDecay = time * decay;
|
|
121
|
+
return finiteState(
|
|
122
|
+
target + decay * displacement + timeDecay * linearCoefficient,
|
|
123
|
+
decay * options.velocity -
|
|
124
|
+
(timeDecay / timeConstant) * linearCoefficient,
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const decayBoundsAt = (time: number): SpringState => {
|
|
129
|
+
const decay = Math.exp(-time / timeConstant);
|
|
130
|
+
if (decay === 0) return { position: 0, velocity: 0 };
|
|
131
|
+
const timeDecay = time * decay;
|
|
132
|
+
return finiteState(
|
|
133
|
+
decay * Math.abs(displacement) +
|
|
134
|
+
timeDecay * Math.abs(linearCoefficient),
|
|
135
|
+
decay * Math.abs(options.velocity) +
|
|
136
|
+
(timeDecay / timeConstant) * Math.abs(linearCoefficient),
|
|
137
|
+
' bound',
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const boundsMeet = (time: number): boolean => {
|
|
142
|
+
const bounds = decayBoundsAt(time);
|
|
143
|
+
return (
|
|
144
|
+
bounds.position <= settling.positionEpsilon &&
|
|
145
|
+
bounds.velocity <= settling.velocityEpsilon
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
let decayIterations = 0;
|
|
150
|
+
let decayResult: SettlingResult;
|
|
151
|
+
if (displacement === 0 && options.velocity === 0) {
|
|
152
|
+
decayResult = { duration: 0, iterations: 0, settled: true };
|
|
153
|
+
} else {
|
|
154
|
+
const absoluteLinearCoefficient = Math.abs(linearCoefficient);
|
|
155
|
+
const positionMonotonicAfter =
|
|
156
|
+
absoluteLinearCoefficient === 0
|
|
157
|
+
? 0
|
|
158
|
+
: Math.max(
|
|
159
|
+
0,
|
|
160
|
+
timeConstant - Math.abs(displacement) / absoluteLinearCoefficient,
|
|
161
|
+
);
|
|
162
|
+
const velocityMonotonicAfter =
|
|
163
|
+
absoluteLinearCoefficient === 0
|
|
164
|
+
? 0
|
|
165
|
+
: Math.max(
|
|
166
|
+
0,
|
|
167
|
+
timeConstant -
|
|
168
|
+
(Math.abs(options.velocity) * timeConstant) /
|
|
169
|
+
absoluteLinearCoefficient,
|
|
170
|
+
);
|
|
171
|
+
const boundsMonotonicAfter = Math.max(
|
|
172
|
+
positionMonotonicAfter,
|
|
173
|
+
velocityMonotonicAfter,
|
|
174
|
+
);
|
|
175
|
+
if (boundsMonotonicAfter > settling.maxDuration) {
|
|
176
|
+
decayResult = {
|
|
177
|
+
duration: settling.maxDuration,
|
|
178
|
+
iterations: 0,
|
|
179
|
+
settled: false,
|
|
180
|
+
};
|
|
181
|
+
} else if (boundsMeet(0) && boundsMonotonicAfter === 0) {
|
|
182
|
+
decayResult = { duration: 0, iterations: 0, settled: true };
|
|
183
|
+
} else {
|
|
184
|
+
let lower = boundsMonotonicAfter;
|
|
185
|
+
let upper = Math.max(lower, Math.min(settling.maxDuration, 1 / 120));
|
|
186
|
+
if (boundsMeet(lower)) {
|
|
187
|
+
decayResult = { duration: lower, iterations: 0, settled: true };
|
|
188
|
+
} else {
|
|
189
|
+
while (upper < settling.maxDuration && !boundsMeet(upper)) {
|
|
190
|
+
lower = upper;
|
|
191
|
+
upper = Math.min(settling.maxDuration, upper * 2);
|
|
192
|
+
decayIterations += 1;
|
|
193
|
+
}
|
|
194
|
+
if (!boundsMeet(upper)) {
|
|
195
|
+
decayResult = {
|
|
196
|
+
duration: settling.maxDuration,
|
|
197
|
+
iterations: decayIterations,
|
|
198
|
+
settled: false,
|
|
199
|
+
};
|
|
200
|
+
} else {
|
|
201
|
+
for (
|
|
202
|
+
let iteration = 0;
|
|
203
|
+
iteration < settling.refinementIterations;
|
|
204
|
+
iteration += 1
|
|
205
|
+
) {
|
|
206
|
+
const midpoint = lower + (upper - lower) / 2;
|
|
207
|
+
if (midpoint === lower || midpoint === upper) break;
|
|
208
|
+
if (boundsMeet(midpoint)) upper = midpoint;
|
|
209
|
+
else lower = midpoint;
|
|
210
|
+
decayIterations += 1;
|
|
211
|
+
}
|
|
212
|
+
decayResult = {
|
|
213
|
+
duration: upper,
|
|
214
|
+
iterations: decayIterations,
|
|
215
|
+
settled: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const outsideAtStart =
|
|
223
|
+
options.min !== undefined && options.from < options.min
|
|
224
|
+
? options.min
|
|
225
|
+
: options.max !== undefined && options.from > options.max
|
|
226
|
+
? options.max
|
|
227
|
+
: options.min !== undefined &&
|
|
228
|
+
options.from === options.min &&
|
|
229
|
+
options.velocity < 0
|
|
230
|
+
? options.min
|
|
231
|
+
: options.max !== undefined &&
|
|
232
|
+
options.from === options.max &&
|
|
233
|
+
options.velocity > 0
|
|
234
|
+
? options.max
|
|
235
|
+
: undefined;
|
|
236
|
+
|
|
237
|
+
const findBoundary = (): { time: number; bound: number } | undefined => {
|
|
238
|
+
if (outsideAtStart !== undefined) return { time: 0, bound: outsideAtStart };
|
|
239
|
+
if (options.min === undefined && options.max === undefined) return undefined;
|
|
240
|
+
const extremumTime =
|
|
241
|
+
linearCoefficient === 0
|
|
242
|
+
? undefined
|
|
243
|
+
: (options.velocity * timeConstant) / linearCoefficient;
|
|
244
|
+
const boundaries = [
|
|
245
|
+
0,
|
|
246
|
+
...(extremumTime !== undefined &&
|
|
247
|
+
extremumTime > 0 &&
|
|
248
|
+
extremumTime < decayResult.duration
|
|
249
|
+
? [extremumTime]
|
|
250
|
+
: []),
|
|
251
|
+
decayResult.duration,
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
for (let index = 1; index < boundaries.length; index += 1) {
|
|
255
|
+
const startTime = boundaries[index - 1]!;
|
|
256
|
+
const endTime = boundaries[index]!;
|
|
257
|
+
const startPosition = decayStateAt(startTime).position;
|
|
258
|
+
const endPosition = decayStateAt(endTime).position;
|
|
259
|
+
const increasing = endPosition >= startPosition;
|
|
260
|
+
const bound = increasing
|
|
261
|
+
? options.max !== undefined &&
|
|
262
|
+
startPosition <= options.max &&
|
|
263
|
+
endPosition >= options.max
|
|
264
|
+
? options.max
|
|
265
|
+
: undefined
|
|
266
|
+
: options.min !== undefined &&
|
|
267
|
+
startPosition >= options.min &&
|
|
268
|
+
endPosition <= options.min
|
|
269
|
+
? options.min
|
|
270
|
+
: undefined;
|
|
271
|
+
if (bound === undefined) continue;
|
|
272
|
+
|
|
273
|
+
let lower = startTime;
|
|
274
|
+
let upper = endTime;
|
|
275
|
+
for (let iteration = 0; iteration < 48; iteration += 1) {
|
|
276
|
+
const midpoint = lower + (upper - lower) / 2;
|
|
277
|
+
const position = decayStateAt(midpoint).position;
|
|
278
|
+
const crossed = increasing ? position >= bound : position <= bound;
|
|
279
|
+
if (crossed) upper = midpoint;
|
|
280
|
+
else lower = midpoint;
|
|
281
|
+
}
|
|
282
|
+
return { time: upper, bound };
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const hit = findBoundary();
|
|
288
|
+
let boundary: Readonly<InertiaBoundaryTransition> | undefined;
|
|
289
|
+
let duration = decayResult.duration;
|
|
290
|
+
let settled = decayResult.settled;
|
|
291
|
+
let iterations = decayResult.iterations;
|
|
292
|
+
|
|
293
|
+
if (hit) {
|
|
294
|
+
const hitState =
|
|
295
|
+
hit.time === 0
|
|
296
|
+
? { position: options.from, velocity: options.velocity }
|
|
297
|
+
: decayStateAt(hit.time);
|
|
298
|
+
const spring = createSpring({
|
|
299
|
+
from: hitState.position,
|
|
300
|
+
to: hit.bound,
|
|
301
|
+
velocity: hitState.velocity,
|
|
302
|
+
...(options.boundarySpring ?? DEFAULT_BOUNDARY_SPRING),
|
|
303
|
+
settle: {
|
|
304
|
+
position: settling.positionEpsilon,
|
|
305
|
+
velocity: settling.velocityEpsilon,
|
|
306
|
+
maxDuration: settling.maxDuration,
|
|
307
|
+
refinementIterations: settling.refinementIterations,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
const springResult = spring.getSettlingResult();
|
|
311
|
+
boundary = Object.freeze({
|
|
312
|
+
time: hit.time,
|
|
313
|
+
bound: hit.bound,
|
|
314
|
+
incomingVelocity: hitState.velocity,
|
|
315
|
+
spring,
|
|
316
|
+
});
|
|
317
|
+
duration = hit.time + springResult.duration;
|
|
318
|
+
assertFinite('duration', duration);
|
|
319
|
+
settled = springResult.settled;
|
|
320
|
+
iterations += springResult.iterations;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const stateAt = (time: number): SpringState => {
|
|
324
|
+
assertFinite('time', time);
|
|
325
|
+
if (time < 0) throw new RangeError('time must be greater than or equal to 0');
|
|
326
|
+
if (settled && time >= duration) {
|
|
327
|
+
return { position: boundary?.bound ?? target, velocity: 0 };
|
|
328
|
+
}
|
|
329
|
+
if (boundary && time >= boundary.time) {
|
|
330
|
+
return boundary.spring.stateAt(
|
|
331
|
+
Math.min(time - boundary.time, boundary.spring.getSettlingDuration()),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return decayStateAt(Math.min(time, decayResult.duration));
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const phaseAt = (time: number): InertiaPhase => {
|
|
338
|
+
assertFinite('time', time);
|
|
339
|
+
if (time < 0) throw new RangeError('time must be greater than or equal to 0');
|
|
340
|
+
if (settled && time >= duration) return 'complete';
|
|
341
|
+
if (boundary && time >= boundary.time) return 'spring';
|
|
342
|
+
return 'decay';
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return Object.freeze({
|
|
346
|
+
initialState: Object.freeze({
|
|
347
|
+
position: options.from,
|
|
348
|
+
velocity: options.velocity,
|
|
349
|
+
}),
|
|
350
|
+
naturalTarget,
|
|
351
|
+
target: boundary?.bound ?? target,
|
|
352
|
+
duration,
|
|
353
|
+
settled,
|
|
354
|
+
...(boundary === undefined ? {} : { boundary }),
|
|
355
|
+
stateAt,
|
|
356
|
+
positionAt(time: number): number {
|
|
357
|
+
return stateAt(time).position;
|
|
358
|
+
},
|
|
359
|
+
velocityAt(time: number): number {
|
|
360
|
+
return stateAt(time).velocity;
|
|
361
|
+
},
|
|
362
|
+
phaseAt,
|
|
363
|
+
getSettlingResult(): SettlingResult {
|
|
364
|
+
return { duration, iterations, settled };
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createSpring } from '../spring.js';
|
|
2
|
+
import type {
|
|
3
|
+
SettlingResult,
|
|
4
|
+
SpringParameters,
|
|
5
|
+
SpringSettleInput,
|
|
6
|
+
SpringSolution,
|
|
7
|
+
SpringState,
|
|
8
|
+
SpringTiming,
|
|
9
|
+
SpringTimingInput,
|
|
10
|
+
} from '../types.js';
|
|
11
|
+
|
|
12
|
+
export interface SpringKeyframe {
|
|
13
|
+
value: number;
|
|
14
|
+
startVelocity?: number;
|
|
15
|
+
parameters?: Partial<SpringParameters>;
|
|
16
|
+
settle?: SpringSettleInput;
|
|
17
|
+
timing?: SpringTimingInput;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SpringKeyframesOptions {
|
|
21
|
+
from: number;
|
|
22
|
+
keyframes: readonly SpringKeyframe[];
|
|
23
|
+
parameters: SpringParameters;
|
|
24
|
+
velocity?: number;
|
|
25
|
+
settle?: SpringSettleInput;
|
|
26
|
+
timing?: SpringTimingInput;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SpringKeyframeSegment {
|
|
30
|
+
readonly index: number;
|
|
31
|
+
readonly startTime: number;
|
|
32
|
+
readonly endTime: number;
|
|
33
|
+
readonly solution: SpringSolution;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SpringKeyframeSequence {
|
|
37
|
+
readonly duration: number;
|
|
38
|
+
readonly settled: boolean;
|
|
39
|
+
readonly timing: Readonly<SpringTiming>;
|
|
40
|
+
readonly segments: readonly Readonly<SpringKeyframeSegment>[];
|
|
41
|
+
positionAt(time: number): number;
|
|
42
|
+
velocityAt(time: number): number;
|
|
43
|
+
stateAt(time: number): SpringState;
|
|
44
|
+
getSettlingResult(): SettlingResult;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertFinite(name: string, value: number): void {
|
|
48
|
+
if (!Number.isFinite(value)) {
|
|
49
|
+
throw new RangeError(`${name} must be a finite number`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertOptionalObject(name: string, value: unknown): void {
|
|
54
|
+
if (
|
|
55
|
+
value !== undefined &&
|
|
56
|
+
(typeof value !== 'object' || value === null || Array.isArray(value))
|
|
57
|
+
) {
|
|
58
|
+
throw new TypeError(`${name} must be an object when provided`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createSpringKeyframes(
|
|
63
|
+
options: SpringKeyframesOptions,
|
|
64
|
+
): SpringKeyframeSequence {
|
|
65
|
+
assertFinite('from', options.from);
|
|
66
|
+
if (options.keyframes.length === 0) {
|
|
67
|
+
throw new RangeError('spring keyframes require at least one destination');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const segments: SpringKeyframeSegment[] = [];
|
|
71
|
+
let from = options.from;
|
|
72
|
+
let startTime = 0;
|
|
73
|
+
let perceptualDuration = 0;
|
|
74
|
+
|
|
75
|
+
if (options.velocity !== undefined) {
|
|
76
|
+
assertFinite('velocity', options.velocity);
|
|
77
|
+
}
|
|
78
|
+
assertOptionalObject('settle', options.settle);
|
|
79
|
+
assertOptionalObject('timing', options.timing);
|
|
80
|
+
|
|
81
|
+
for (let index = 0; index < options.keyframes.length; index += 1) {
|
|
82
|
+
const keyframe = options.keyframes[index]!;
|
|
83
|
+
assertFinite(`keyframes[${index}].value`, keyframe.value);
|
|
84
|
+
assertOptionalObject(`keyframes[${index}].parameters`, keyframe.parameters);
|
|
85
|
+
assertOptionalObject(`keyframes[${index}].settle`, keyframe.settle);
|
|
86
|
+
assertOptionalObject(`keyframes[${index}].timing`, keyframe.timing);
|
|
87
|
+
const frameVelocity =
|
|
88
|
+
keyframe.startVelocity === undefined
|
|
89
|
+
? index === 0
|
|
90
|
+
? options.velocity === undefined
|
|
91
|
+
? 0
|
|
92
|
+
: options.velocity
|
|
93
|
+
: 0
|
|
94
|
+
: keyframe.startVelocity;
|
|
95
|
+
assertFinite(`keyframes[${index}].startVelocity`, frameVelocity);
|
|
96
|
+
const parameters: SpringParameters = {
|
|
97
|
+
mass:
|
|
98
|
+
keyframe.parameters?.mass === undefined
|
|
99
|
+
? options.parameters.mass
|
|
100
|
+
: keyframe.parameters.mass,
|
|
101
|
+
stiffness:
|
|
102
|
+
keyframe.parameters?.stiffness === undefined
|
|
103
|
+
? options.parameters.stiffness
|
|
104
|
+
: keyframe.parameters.stiffness,
|
|
105
|
+
damping:
|
|
106
|
+
keyframe.parameters?.damping === undefined
|
|
107
|
+
? options.parameters.damping
|
|
108
|
+
: keyframe.parameters.damping,
|
|
109
|
+
};
|
|
110
|
+
const settle =
|
|
111
|
+
options.settle || keyframe.settle
|
|
112
|
+
? { ...options.settle, ...keyframe.settle }
|
|
113
|
+
: undefined;
|
|
114
|
+
const timing =
|
|
115
|
+
keyframe.timing === undefined ? options.timing : keyframe.timing;
|
|
116
|
+
const solution = createSpring({
|
|
117
|
+
from,
|
|
118
|
+
to: keyframe.value,
|
|
119
|
+
velocity: frameVelocity,
|
|
120
|
+
...parameters,
|
|
121
|
+
...(settle === undefined ? {} : { settle }),
|
|
122
|
+
...(timing === undefined ? {} : { timing }),
|
|
123
|
+
});
|
|
124
|
+
const result = solution.getSettlingResult();
|
|
125
|
+
if (!result.settled && index < options.keyframes.length - 1) {
|
|
126
|
+
throw new RangeError(
|
|
127
|
+
`keyframes[${index}] is unsettled and cannot define the next segment start`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const endTime = startTime + result.duration;
|
|
131
|
+
assertFinite('duration', endTime);
|
|
132
|
+
segments.push(Object.freeze({ index, startTime, endTime, solution }));
|
|
133
|
+
perceptualDuration += solution.timing.perceptualDuration;
|
|
134
|
+
assertFinite('perceptualDuration', perceptualDuration);
|
|
135
|
+
startTime = endTime;
|
|
136
|
+
from = keyframe.value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const frozenSegments = Object.freeze(segments);
|
|
140
|
+
const duration = frozenSegments.at(-1)!.endTime;
|
|
141
|
+
const settled = frozenSegments.every((segment) => segment.solution.timing.settled);
|
|
142
|
+
const timing: Readonly<SpringTiming> = Object.freeze({
|
|
143
|
+
perceptualDuration,
|
|
144
|
+
settlingDuration: duration,
|
|
145
|
+
settled,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const stateAt = (time: number): SpringState => {
|
|
149
|
+
assertFinite('time', time);
|
|
150
|
+
if (time < 0) throw new RangeError('time must be greater than or equal to 0');
|
|
151
|
+
const segment =
|
|
152
|
+
frozenSegments.find((candidate) => time < candidate.endTime) ??
|
|
153
|
+
frozenSegments.at(-1)!;
|
|
154
|
+
const localTime = Math.min(
|
|
155
|
+
Math.max(0, time - segment.startTime),
|
|
156
|
+
segment.solution.getSettlingDuration(),
|
|
157
|
+
);
|
|
158
|
+
if (time >= duration && segment.solution.timing.settled) {
|
|
159
|
+
return { position: segment.solution.initialState.target, velocity: 0 };
|
|
160
|
+
}
|
|
161
|
+
return segment.solution.stateAt(localTime);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return Object.freeze({
|
|
165
|
+
duration,
|
|
166
|
+
settled,
|
|
167
|
+
timing,
|
|
168
|
+
segments: frozenSegments,
|
|
169
|
+
positionAt(time: number): number {
|
|
170
|
+
return stateAt(time).position;
|
|
171
|
+
},
|
|
172
|
+
velocityAt(time: number): number {
|
|
173
|
+
return stateAt(time).velocity;
|
|
174
|
+
},
|
|
175
|
+
stateAt,
|
|
176
|
+
getSettlingResult(): SettlingResult {
|
|
177
|
+
return {
|
|
178
|
+
duration,
|
|
179
|
+
iterations: frozenSegments.reduce(
|
|
180
|
+
(total, segment) =>
|
|
181
|
+
total + segment.solution.getSettlingResult().iterations,
|
|
182
|
+
0,
|
|
183
|
+
),
|
|
184
|
+
settled,
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|