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.
Files changed (134) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +340 -0
  4. package/dist/coupled-system.d.ts +50 -0
  5. package/dist/coupled-system.d.ts.map +1 -0
  6. package/dist/coupled-system.js +269 -0
  7. package/dist/coupled-system.js.map +1 -0
  8. package/dist/coupled.d.ts +3 -0
  9. package/dist/coupled.d.ts.map +1 -0
  10. package/dist/coupled.js +2 -0
  11. package/dist/coupled.js.map +1 -0
  12. package/dist/css-linear.d.ts +20 -0
  13. package/dist/css-linear.d.ts.map +1 -0
  14. package/dist/css-linear.js +95 -0
  15. package/dist/css-linear.js.map +1 -0
  16. package/dist/css.d.ts +3 -0
  17. package/dist/css.d.ts.map +1 -0
  18. package/dist/css.js +2 -0
  19. package/dist/css.js.map +1 -0
  20. package/dist/gsap/active-tracks.d.ts +32 -0
  21. package/dist/gsap/active-tracks.d.ts.map +1 -0
  22. package/dist/gsap/active-tracks.js +177 -0
  23. package/dist/gsap/active-tracks.js.map +1 -0
  24. package/dist/gsap/gsap-time.d.ts +10 -0
  25. package/dist/gsap/gsap-time.d.ts.map +1 -0
  26. package/dist/gsap/gsap-time.js +62 -0
  27. package/dist/gsap/gsap-time.js.map +1 -0
  28. package/dist/gsap/plugin-tween-coordinator.d.ts +30 -0
  29. package/dist/gsap/plugin-tween-coordinator.d.ts.map +1 -0
  30. package/dist/gsap/plugin-tween-coordinator.js +72 -0
  31. package/dist/gsap/plugin-tween-coordinator.js.map +1 -0
  32. package/dist/gsap/plugin.d.ts +84 -0
  33. package/dist/gsap/plugin.d.ts.map +1 -0
  34. package/dist/gsap/plugin.js +552 -0
  35. package/dist/gsap/plugin.js.map +1 -0
  36. package/dist/gsap/spring-to.d.ts +93 -0
  37. package/dist/gsap/spring-to.d.ts.map +1 -0
  38. package/dist/gsap/spring-to.js +569 -0
  39. package/dist/gsap/spring-to.js.map +1 -0
  40. package/dist/gsap/spring-track-policy.d.ts +24 -0
  41. package/dist/gsap/spring-track-policy.d.ts.map +1 -0
  42. package/dist/gsap/spring-track-policy.js +76 -0
  43. package/dist/gsap/spring-track-policy.js.map +1 -0
  44. package/dist/gsap/track-lifecycle.d.ts +16 -0
  45. package/dist/gsap/track-lifecycle.d.ts.map +1 -0
  46. package/dist/gsap/track-lifecycle.js +33 -0
  47. package/dist/gsap/track-lifecycle.js.map +1 -0
  48. package/dist/index.d.ts +28 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +16 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/math.d.ts +9 -0
  53. package/dist/math.d.ts.map +1 -0
  54. package/dist/math.js +38 -0
  55. package/dist/math.js.map +1 -0
  56. package/dist/model.d.ts +14 -0
  57. package/dist/model.d.ts.map +1 -0
  58. package/dist/model.js +38 -0
  59. package/dist/model.js.map +1 -0
  60. package/dist/motion-compatibility.d.ts +18 -0
  61. package/dist/motion-compatibility.d.ts.map +1 -0
  62. package/dist/motion-compatibility.js +83 -0
  63. package/dist/motion-compatibility.js.map +1 -0
  64. package/dist/parameters.d.ts +52 -0
  65. package/dist/parameters.d.ts.map +1 -0
  66. package/dist/parameters.js +124 -0
  67. package/dist/parameters.js.map +1 -0
  68. package/dist/settling.d.ts +12 -0
  69. package/dist/settling.d.ts.map +1 -0
  70. package/dist/settling.js +107 -0
  71. package/dist/settling.js.map +1 -0
  72. package/dist/solver.d.ts +17 -0
  73. package/dist/solver.d.ts.map +1 -0
  74. package/dist/solver.js +190 -0
  75. package/dist/solver.js.map +1 -0
  76. package/dist/spring.d.ts +9 -0
  77. package/dist/spring.d.ts.map +1 -0
  78. package/dist/spring.js +91 -0
  79. package/dist/spring.js.map +1 -0
  80. package/dist/support/additive-spring-value.d.ts +39 -0
  81. package/dist/support/additive-spring-value.d.ts.map +1 -0
  82. package/dist/support/additive-spring-value.js +281 -0
  83. package/dist/support/additive-spring-value.js.map +1 -0
  84. package/dist/support/inertia.d.ts +34 -0
  85. package/dist/support/inertia.d.ts.map +1 -0
  86. package/dist/support/inertia.js +293 -0
  87. package/dist/support/inertia.js.map +1 -0
  88. package/dist/support/keyframes.d.ts +34 -0
  89. package/dist/support/keyframes.d.ts.map +1 -0
  90. package/dist/support/keyframes.js +117 -0
  91. package/dist/support/keyframes.js.map +1 -0
  92. package/dist/support/spring-value.d.ts +38 -0
  93. package/dist/support/spring-value.d.ts.map +1 -0
  94. package/dist/support/spring-value.js +289 -0
  95. package/dist/support/spring-value.js.map +1 -0
  96. package/dist/support/velocity.d.ts +14 -0
  97. package/dist/support/velocity.d.ts.map +1 -0
  98. package/dist/support/velocity.js +75 -0
  99. package/dist/support/velocity.js.map +1 -0
  100. package/dist/types.d.ts +61 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/types.js +2 -0
  103. package/dist/types.js.map +1 -0
  104. package/dist/vector.d.ts +32 -0
  105. package/dist/vector.d.ts.map +1 -0
  106. package/dist/vector.js +121 -0
  107. package/dist/vector.js.map +1 -0
  108. package/package.json +68 -0
  109. package/src/coupled-system.ts +383 -0
  110. package/src/coupled.ts +10 -0
  111. package/src/css-linear.ts +148 -0
  112. package/src/css.ts +6 -0
  113. package/src/gsap/active-tracks.ts +244 -0
  114. package/src/gsap/gsap-time.ts +91 -0
  115. package/src/gsap/plugin-tween-coordinator.ts +112 -0
  116. package/src/gsap/plugin.ts +861 -0
  117. package/src/gsap/spring-to.ts +836 -0
  118. package/src/gsap/spring-track-policy.ts +133 -0
  119. package/src/gsap/track-lifecycle.ts +53 -0
  120. package/src/index.ts +130 -0
  121. package/src/math.ts +46 -0
  122. package/src/model.ts +70 -0
  123. package/src/motion-compatibility.ts +131 -0
  124. package/src/parameters.ts +226 -0
  125. package/src/settling.ts +141 -0
  126. package/src/solver.ts +262 -0
  127. package/src/spring.ts +119 -0
  128. package/src/support/additive-spring-value.ts +343 -0
  129. package/src/support/inertia.ts +367 -0
  130. package/src/support/keyframes.ts +188 -0
  131. package/src/support/spring-value.ts +399 -0
  132. package/src/support/velocity.ts +119 -0
  133. package/src/types.ts +70 -0
  134. package/src/vector.ts +191 -0
@@ -0,0 +1,399 @@
1
+ import { springParameters } from '../parameters.js';
2
+ import { resolveSettlingOptions } from '../settling.js';
3
+ import { createSpring, resolveSpringTimingInput } from '../spring.js';
4
+ import type {
5
+ SpringParameters,
6
+ SpringSettleInput,
7
+ SpringSolution,
8
+ SpringState,
9
+ SpringTimingInput,
10
+ } from '../types.js';
11
+
12
+ export interface FrameDriver {
13
+ /** Monotonic time in seconds. */
14
+ now(): number;
15
+ /** Schedule one frame. The callback receives monotonic time in seconds. */
16
+ schedule(callback: (time: number) => void): () => void;
17
+ }
18
+
19
+ export interface SpringValueOptions {
20
+ settle?: SpringSettleInput;
21
+ timing?: SpringTimingInput;
22
+ }
23
+
24
+ export interface SpringValueSnapshot {
25
+ value: number;
26
+ velocity: number;
27
+ target: number;
28
+ animating: boolean;
29
+ }
30
+
31
+ export interface SpringValueRetargetOptions {
32
+ parameters?: SpringParameters;
33
+ blendDuration?: number;
34
+ }
35
+
36
+ export type SpringValueEvent =
37
+ | 'change'
38
+ | 'logicalComplete'
39
+ | 'settle'
40
+ | 'unsettled';
41
+
42
+ export type SpringValueListener = (snapshot: Readonly<SpringValueSnapshot>) => void;
43
+
44
+ export interface SpringValue {
45
+ get(): number;
46
+ getVelocity(): number;
47
+ getTarget(): number;
48
+ getParameters(): Readonly<SpringParameters>;
49
+ getSnapshot(): Readonly<SpringValueSnapshot>;
50
+ setTarget(target: number, options?: SpringValueRetargetOptions): void;
51
+ setParameters(
52
+ parameters: SpringParameters,
53
+ options?: Pick<SpringValueRetargetOptions, 'blendDuration'>,
54
+ ): void;
55
+ jump(value: number): void;
56
+ stop(): void;
57
+ on(event: SpringValueEvent, listener: SpringValueListener): () => void;
58
+ destroy(): void;
59
+ }
60
+
61
+ interface ActiveSpring {
62
+ solution: SpringSolution;
63
+ blendFrom?: SpringSolution;
64
+ blendDuration: number;
65
+ logicalDuration: number;
66
+ settlingBoundary: number;
67
+ startedAt: number;
68
+ logicalComplete: boolean;
69
+ unsettledNotified: boolean;
70
+ }
71
+
72
+ interface PendingSpringRequest {
73
+ target: number;
74
+ parameters: Readonly<SpringParameters>;
75
+ blendDuration: number;
76
+ }
77
+
78
+ function assertFinite(name: string, value: number): void {
79
+ if (!Number.isFinite(value)) {
80
+ throw new RangeError(`${name} must be a finite number`);
81
+ }
82
+ }
83
+
84
+ export function createSpringValue(
85
+ initialValue: number,
86
+ parameters: SpringParameters,
87
+ driver: FrameDriver,
88
+ options: SpringValueOptions = {},
89
+ ): SpringValue {
90
+ assertFinite('initialValue', initialValue);
91
+ let currentParameters = springParameters.fromPhysics(parameters);
92
+ const settling = resolveSettlingOptions(options.settle);
93
+ const settleInput: SpringSettleInput = {
94
+ position: settling.positionEpsilon,
95
+ velocity: settling.velocityEpsilon,
96
+ maxDuration: settling.maxDuration,
97
+ refinementIterations: settling.refinementIterations,
98
+ };
99
+ const timingInput = resolveSpringTimingInput(options.timing);
100
+ const listeners: Record<SpringValueEvent, Set<SpringValueListener>> = {
101
+ change: new Set(),
102
+ logicalComplete: new Set(),
103
+ settle: new Set(),
104
+ unsettled: new Set(),
105
+ };
106
+
107
+ let value = initialValue;
108
+ let velocity = 0;
109
+ let target = initialValue;
110
+ let pending: PendingSpringRequest | undefined;
111
+ let active: ActiveSpring | undefined;
112
+ let cancelFrame: (() => void) | undefined;
113
+ let destroyed = false;
114
+ let lastFrameTime = Number.NEGATIVE_INFINITY;
115
+ let revision = 0;
116
+
117
+ const snapshot = (): Readonly<SpringValueSnapshot> =>
118
+ Object.freeze({
119
+ value,
120
+ velocity,
121
+ target: pending?.target ?? target,
122
+ animating: active !== undefined || pending !== undefined,
123
+ });
124
+
125
+ const emit = (event: SpringValueEvent): void => {
126
+ if (destroyed || listeners[event].size === 0) return;
127
+ const current = snapshot();
128
+ for (const listener of [...listeners[event]]) listener(current);
129
+ };
130
+
131
+ const write = (nextValue: number, nextVelocity: number): void => {
132
+ assertFinite('value', nextValue);
133
+ assertFinite('velocity', nextVelocity);
134
+ const changed = !Object.is(value, nextValue) || !Object.is(velocity, nextVelocity);
135
+ value = nextValue;
136
+ velocity = nextVelocity;
137
+ if (changed) emit('change');
138
+ };
139
+
140
+ const stateAt = (sampled: ActiveSpring, elapsed: number): SpringState => {
141
+ const next = sampled.solution.stateAt(elapsed);
142
+ if (!sampled.blendFrom || elapsed >= sampled.blendDuration) return next;
143
+
144
+ const previous = sampled.blendFrom.stateAt(elapsed);
145
+ const progress = elapsed / sampled.blendDuration;
146
+ const weight = progress * progress * (3 - 2 * progress);
147
+ const weightVelocity =
148
+ (6 * progress * (1 - progress)) / sampled.blendDuration;
149
+ return {
150
+ position: previous.position + (next.position - previous.position) * weight,
151
+ velocity:
152
+ previous.velocity +
153
+ (next.velocity - previous.velocity) * weight +
154
+ (next.position - previous.position) * weightVelocity,
155
+ };
156
+ };
157
+
158
+ const sampleActive = (time: number): void => {
159
+ if (!active) return;
160
+ const sampled = active;
161
+ const sampledRevision = revision;
162
+ const elapsed = Math.max(0, time - sampled.startedAt);
163
+ const { solution } = sampled;
164
+
165
+ if (solution.timing.settled && elapsed >= sampled.settlingBoundary) {
166
+ active = undefined;
167
+ write(solution.initialState.target, 0);
168
+ if (revision !== sampledRevision) return;
169
+ if (
170
+ !sampled.logicalComplete &&
171
+ elapsed >= sampled.logicalDuration
172
+ ) {
173
+ sampled.logicalComplete = true;
174
+ emit('logicalComplete');
175
+ if (revision !== sampledRevision) return;
176
+ }
177
+ emit('settle');
178
+ return;
179
+ }
180
+
181
+ const state = stateAt(sampled, elapsed);
182
+ write(state.position, state.velocity);
183
+ if (revision !== sampledRevision || active !== sampled) return;
184
+
185
+ if (
186
+ !sampled.logicalComplete &&
187
+ elapsed >= sampled.logicalDuration
188
+ ) {
189
+ sampled.logicalComplete = true;
190
+ emit('logicalComplete');
191
+ if (revision !== sampledRevision || active !== sampled) return;
192
+ }
193
+ if (
194
+ !solution.timing.settled &&
195
+ !sampled.unsettledNotified &&
196
+ elapsed >= sampled.settlingBoundary
197
+ ) {
198
+ sampled.unsettledNotified = true;
199
+ emit('unsettled');
200
+ }
201
+ };
202
+
203
+ const createActiveSpring = (
204
+ time: number,
205
+ request: PendingSpringRequest,
206
+ previousParameters: Readonly<SpringParameters>,
207
+ ): void => {
208
+ target = request.target;
209
+ currentParameters = request.parameters;
210
+ if (Object.is(value, target) && velocity === 0) {
211
+ active = undefined;
212
+ emit('settle');
213
+ return;
214
+ }
215
+
216
+ const solution = createSpring({
217
+ from: value,
218
+ to: target,
219
+ velocity,
220
+ ...currentParameters,
221
+ settle: settleInput,
222
+ ...(timingInput === undefined ? {} : { timing: timingInput }),
223
+ });
224
+ const parametersChanged =
225
+ previousParameters.mass !== currentParameters.mass ||
226
+ previousParameters.stiffness !== currentParameters.stiffness ||
227
+ previousParameters.damping !== currentParameters.damping;
228
+ const blendFrom =
229
+ request.blendDuration > 0 && parametersChanged
230
+ ? createSpring({
231
+ from: value,
232
+ to: target,
233
+ velocity,
234
+ ...previousParameters,
235
+ settle: settleInput,
236
+ ...(timingInput === undefined ? {} : { timing: timingInput }),
237
+ })
238
+ : undefined;
239
+ active = {
240
+ solution,
241
+ ...(blendFrom === undefined ? {} : { blendFrom }),
242
+ blendDuration: blendFrom ? request.blendDuration : 0,
243
+ logicalDuration: Math.max(
244
+ solution.timing.perceptualDuration,
245
+ blendFrom ? request.blendDuration : 0,
246
+ ),
247
+ settlingBoundary: Math.max(
248
+ solution.timing.settlingDuration,
249
+ blendFrom ? request.blendDuration : 0,
250
+ ),
251
+ startedAt: time,
252
+ logicalComplete: false,
253
+ unsettledNotified: false,
254
+ };
255
+ };
256
+
257
+ const validateFrameTime = (time: number): void => {
258
+ assertFinite('frame time', time);
259
+ if (time < lastFrameTime) {
260
+ throw new RangeError('frame time must be monotonic');
261
+ }
262
+ lastFrameTime = time;
263
+ };
264
+
265
+ let requestFrame: () => void;
266
+ const frame = (time: number): void => {
267
+ cancelFrame = undefined;
268
+ if (destroyed) return;
269
+ validateFrameTime(time);
270
+
271
+ if (pending !== undefined) {
272
+ // Sample the interrupted trajectory at the exact retarget time before
273
+ // replacing it, preserving analytical rather than frame-difference velocity.
274
+ sampleActive(time);
275
+ const request = pending;
276
+ const previousParameters = currentParameters;
277
+ pending = undefined;
278
+ createActiveSpring(time, request, previousParameters);
279
+ } else {
280
+ sampleActive(time);
281
+ }
282
+
283
+ if (active || pending !== undefined) requestFrame();
284
+ };
285
+
286
+ requestFrame = (): void => {
287
+ if (destroyed || cancelFrame) return;
288
+ cancelFrame = driver.schedule(frame);
289
+ };
290
+
291
+ const stopAt = (time: number): void => {
292
+ revision += 1;
293
+ validateFrameTime(time);
294
+ sampleActive(time);
295
+ active = undefined;
296
+ pending = undefined;
297
+ cancelFrame?.();
298
+ cancelFrame = undefined;
299
+ write(value, 0);
300
+ target = value;
301
+ };
302
+
303
+ const queueTarget = (
304
+ nextTarget: number,
305
+ retargetOptions: SpringValueRetargetOptions = {},
306
+ ): void => {
307
+ if (destroyed) return;
308
+ assertFinite('target', nextTarget);
309
+ const nextParameters =
310
+ retargetOptions.parameters === undefined
311
+ ? pending?.parameters ?? currentParameters
312
+ : springParameters.fromPhysics(retargetOptions.parameters);
313
+ const blendDuration =
314
+ retargetOptions.blendDuration === undefined
315
+ ? pending?.blendDuration ?? 0
316
+ : retargetOptions.blendDuration;
317
+ assertFinite('blendDuration', blendDuration);
318
+ if (blendDuration < 0) {
319
+ throw new RangeError('blendDuration must be greater than or equal to 0');
320
+ }
321
+ const currentRequestTarget = pending?.target ?? target;
322
+ const currentRequestParameters = pending?.parameters ?? currentParameters;
323
+ const sameParameters =
324
+ currentRequestParameters.mass === nextParameters.mass &&
325
+ currentRequestParameters.stiffness === nextParameters.stiffness &&
326
+ currentRequestParameters.damping === nextParameters.damping;
327
+ if (Object.is(nextTarget, currentRequestTarget) && sameParameters) return;
328
+ revision += 1;
329
+ pending = { target: nextTarget, parameters: nextParameters, blendDuration };
330
+ requestFrame();
331
+ };
332
+
333
+ return Object.freeze({
334
+ get(): number {
335
+ return value;
336
+ },
337
+ getVelocity(): number {
338
+ return velocity;
339
+ },
340
+ getTarget(): number {
341
+ return pending?.target ?? target;
342
+ },
343
+ getParameters(): Readonly<SpringParameters> {
344
+ return pending?.parameters ?? currentParameters;
345
+ },
346
+ getSnapshot(): Readonly<SpringValueSnapshot> {
347
+ return snapshot();
348
+ },
349
+ setTarget(
350
+ nextTarget: number,
351
+ retargetOptions: SpringValueRetargetOptions = {},
352
+ ): void {
353
+ queueTarget(nextTarget, retargetOptions);
354
+ },
355
+ setParameters(
356
+ nextParameters: SpringParameters,
357
+ retargetOptions: Pick<SpringValueRetargetOptions, 'blendDuration'> = {},
358
+ ): void {
359
+ if (destroyed) return;
360
+ queueTarget(pending?.target ?? target, {
361
+ parameters: nextParameters,
362
+ ...(retargetOptions.blendDuration === undefined
363
+ ? {}
364
+ : { blendDuration: retargetOptions.blendDuration }),
365
+ });
366
+ },
367
+ jump(nextValue: number): void {
368
+ if (destroyed) return;
369
+ assertFinite('value', nextValue);
370
+ revision += 1;
371
+ active = undefined;
372
+ pending = undefined;
373
+ target = nextValue;
374
+ cancelFrame?.();
375
+ cancelFrame = undefined;
376
+ write(nextValue, 0);
377
+ },
378
+ stop(): void {
379
+ if (destroyed) return;
380
+ stopAt(driver.now());
381
+ },
382
+ on(event: SpringValueEvent, listener: SpringValueListener): () => void {
383
+ if (destroyed) return (): void => {};
384
+ listeners[event].add(listener);
385
+ let subscribed = true;
386
+ return (): void => {
387
+ if (!subscribed) return;
388
+ subscribed = false;
389
+ listeners[event].delete(listener);
390
+ };
391
+ },
392
+ destroy(): void {
393
+ if (destroyed) return;
394
+ destroyed = true;
395
+ stopAt(driver.now());
396
+ for (const eventListeners of Object.values(listeners)) eventListeners.clear();
397
+ },
398
+ });
399
+ }
@@ -0,0 +1,119 @@
1
+ export interface VelocitySample<TValue> {
2
+ value: TValue;
3
+ /** Monotonic time in seconds. */
4
+ time: number;
5
+ }
6
+
7
+ export interface VelocityFromSamplesOptions {
8
+ /** Samples older than this many seconds resolve to zero velocity. */
9
+ maxSampleAge?: number;
10
+ }
11
+
12
+ function assertFinite(name: string, value: number): void {
13
+ if (!Number.isFinite(value)) {
14
+ throw new RangeError(`${name} must be a finite number`);
15
+ }
16
+ }
17
+
18
+ function validateDeltaTime(
19
+ previousTime: number,
20
+ currentTime: number,
21
+ maxSampleAge: number,
22
+ ): number | undefined {
23
+ assertFinite('previous.time', previousTime);
24
+ assertFinite('current.time', currentTime);
25
+ const deltaTime = currentTime - previousTime;
26
+ if (deltaTime < 0) throw new RangeError('sample times must be monotonic');
27
+ if (deltaTime === 0 || deltaTime > maxSampleAge) return undefined;
28
+ return deltaTime;
29
+ }
30
+
31
+ function resolvedMaxSampleAge(value: number | undefined): number {
32
+ if (value === undefined) return Number.POSITIVE_INFINITY;
33
+ assertFinite('maxSampleAge', value);
34
+ if (value <= 0) throw new RangeError('maxSampleAge must be greater than 0');
35
+ return value;
36
+ }
37
+
38
+ export function velocityFromSamples(
39
+ previous: VelocitySample<number>,
40
+ current: VelocitySample<number>,
41
+ options?: VelocityFromSamplesOptions,
42
+ ): number;
43
+ export function velocityFromSamples(
44
+ previous: VelocitySample<readonly number[]>,
45
+ current: VelocitySample<readonly number[]>,
46
+ options?: VelocityFromSamplesOptions,
47
+ ): readonly number[];
48
+ export function velocityFromSamples(
49
+ previous: VelocitySample<number | readonly number[]>,
50
+ current: VelocitySample<number | readonly number[]>,
51
+ options: VelocityFromSamplesOptions = {},
52
+ ): number | readonly number[] {
53
+ const deltaTime = validateDeltaTime(
54
+ previous.time,
55
+ current.time,
56
+ resolvedMaxSampleAge(options.maxSampleAge),
57
+ );
58
+
59
+ if (typeof previous.value === 'number' && typeof current.value === 'number') {
60
+ assertFinite('previous.value', previous.value);
61
+ assertFinite('current.value', current.value);
62
+ if (deltaTime === undefined) return 0;
63
+ const velocity = (current.value - previous.value) / deltaTime;
64
+ assertFinite('velocity', velocity);
65
+ return velocity;
66
+ }
67
+
68
+ if (Array.isArray(previous.value) && Array.isArray(current.value)) {
69
+ const previousValues = previous.value as readonly number[];
70
+ const currentValues = current.value as readonly number[];
71
+ if (previousValues.length !== currentValues.length) {
72
+ throw new RangeError('sample vectors must have the same number of components');
73
+ }
74
+ return Object.freeze(
75
+ previousValues.map((previousValue, index) => {
76
+ const currentValue = currentValues[index]!;
77
+ assertFinite(`previous.value[${index}]`, previousValue);
78
+ assertFinite(`current.value[${index}]`, currentValue);
79
+ if (deltaTime === undefined) return 0;
80
+ const velocity = (currentValue - previousValue) / deltaTime;
81
+ assertFinite(`velocity[${index}]`, velocity);
82
+ return velocity;
83
+ }),
84
+ );
85
+ }
86
+
87
+ throw new TypeError('sample values must both be numbers or numeric vectors');
88
+ }
89
+
90
+ export function normalizedVelocity(
91
+ velocity: number,
92
+ from: number,
93
+ to: number,
94
+ ): number {
95
+ assertFinite('velocity', velocity);
96
+ assertFinite('from', from);
97
+ assertFinite('to', to);
98
+ const distance = to - from;
99
+ assertFinite('distance', distance);
100
+ if (distance === 0) {
101
+ throw new RangeError('normalized velocity requires a non-zero distance');
102
+ }
103
+ const result = velocity / distance;
104
+ assertFinite('normalizedVelocity', result);
105
+ return result;
106
+ }
107
+
108
+ export function physicalVelocity(
109
+ velocity: number,
110
+ from: number,
111
+ to: number,
112
+ ): number {
113
+ assertFinite('normalizedVelocity', velocity);
114
+ assertFinite('from', from);
115
+ assertFinite('to', to);
116
+ const result = velocity * (to - from);
117
+ assertFinite('physicalVelocity', result);
118
+ return result;
119
+ }
package/src/types.ts ADDED
@@ -0,0 +1,70 @@
1
+ export interface SpringParameters {
2
+ mass: number;
3
+ stiffness: number;
4
+ damping: number;
5
+ }
6
+
7
+ export interface SpringState {
8
+ position: number;
9
+ velocity: number;
10
+ }
11
+
12
+ export interface SpringInitialState extends SpringState {
13
+ target: number;
14
+ }
15
+
16
+ export interface SpringSettlingOptions {
17
+ positionEpsilon: number;
18
+ velocityEpsilon: number;
19
+ maxDuration: number;
20
+ refinementIterations: number;
21
+ }
22
+
23
+ export interface SpringSettleInput {
24
+ position?: number;
25
+ velocity?: number;
26
+ maxDuration?: number;
27
+ refinementIterations?: number;
28
+ }
29
+
30
+ export interface SpringTimingInput {
31
+ perceptualDuration?: number;
32
+ }
33
+
34
+ export interface SpringTiming {
35
+ perceptualDuration: number;
36
+ settlingDuration: number;
37
+ settled: boolean;
38
+ }
39
+
40
+ export type SpringRegime = 'underdamped' | 'critical' | 'overdamped';
41
+
42
+ export interface SpringOptions extends SpringParameters {
43
+ from: number;
44
+ to: number;
45
+ velocity?: number;
46
+ settle?: SpringSettleInput;
47
+ timing?: SpringTimingInput;
48
+ }
49
+
50
+ export interface SettlingResult {
51
+ duration: number;
52
+ iterations: number;
53
+ settled: boolean;
54
+ }
55
+
56
+ export interface SpringSolution {
57
+ readonly dampingRatio: number;
58
+ readonly angularFrequency: number;
59
+ readonly regime: SpringRegime;
60
+ readonly parameters: Readonly<SpringParameters>;
61
+ readonly initialState: Readonly<SpringInitialState>;
62
+ readonly settling: Readonly<SpringSettlingOptions>;
63
+ readonly timing: Readonly<SpringTiming>;
64
+ positionAt(time: number): number;
65
+ velocityAt(time: number): number;
66
+ stateAt(time: number): SpringState;
67
+ getSettlingDuration(): number;
68
+ getSettlingResult(): SettlingResult;
69
+ retarget(target: number, time: number): SpringSolution;
70
+ }