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
package/src/spring.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { assertFinite } from './math.js';
2
+ import { getSettlingResult, resolveSettlingOptions } from './settling.js';
3
+ import { createAnalyticalSolver } from './solver.js';
4
+ import type {
5
+ SpringInitialState,
6
+ SpringOptions,
7
+ SpringParameters,
8
+ SpringSettleInput,
9
+ SpringSolution,
10
+ SpringState,
11
+ SpringTiming,
12
+ SpringTimingInput,
13
+ } from './types.js';
14
+
15
+ export interface SpringSolutionOptions {
16
+ settle?: SpringSettleInput;
17
+ timing?: SpringTimingInput;
18
+ }
19
+
20
+ export function resolveSpringTimingInput(
21
+ input: SpringTimingInput | undefined,
22
+ ): Readonly<SpringTimingInput> | undefined {
23
+ if (input === undefined) return undefined;
24
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
25
+ throw new TypeError('timing must be an object when provided');
26
+ }
27
+ if (input.perceptualDuration === undefined) return Object.freeze({});
28
+ assertFinite('perceptualDuration', input.perceptualDuration);
29
+ if (input.perceptualDuration < 0) {
30
+ throw new RangeError('perceptualDuration must be greater than or equal to 0');
31
+ }
32
+ return Object.freeze({ perceptualDuration: input.perceptualDuration });
33
+ }
34
+
35
+ export function createSpringSolution(
36
+ parameters: SpringParameters,
37
+ initialState: SpringInitialState,
38
+ options: SpringSolutionOptions = {},
39
+ ): SpringSolution {
40
+ const resolvedTiming = resolveSpringTimingInput(options.timing);
41
+ const settling = resolveSettlingOptions(options.settle);
42
+ const solver = createAnalyticalSolver(parameters, initialState);
43
+ const settlingResult = getSettlingResult(solver, settling);
44
+ const requestedPerceptualDuration = resolvedTiming?.perceptualDuration;
45
+ const perceptualDuration =
46
+ requestedPerceptualDuration === undefined
47
+ ? (2 * Math.PI) / solver.angularFrequency
48
+ : requestedPerceptualDuration;
49
+ assertFinite('perceptualDuration', perceptualDuration);
50
+ if (perceptualDuration < 0) {
51
+ throw new RangeError('perceptualDuration must be greater than or equal to 0');
52
+ }
53
+ const timing: SpringTiming = {
54
+ perceptualDuration,
55
+ settlingDuration: settlingResult.duration,
56
+ settled: settlingResult.settled,
57
+ };
58
+
59
+ const stateAt = (time: number): SpringState => solver.stateAt(time);
60
+
61
+ return Object.freeze({
62
+ dampingRatio: solver.dampingRatio,
63
+ angularFrequency: solver.angularFrequency,
64
+ regime: solver.regime,
65
+ parameters: Object.freeze({ ...parameters }),
66
+ initialState: Object.freeze({ ...initialState }),
67
+ settling,
68
+ timing: Object.freeze(timing),
69
+ positionAt(time: number) {
70
+ return stateAt(time).position;
71
+ },
72
+ velocityAt(time: number) {
73
+ return stateAt(time).velocity;
74
+ },
75
+ stateAt,
76
+ getSettlingDuration() {
77
+ return settlingResult.duration;
78
+ },
79
+ getSettlingResult() {
80
+ return { ...settlingResult };
81
+ },
82
+ retarget(target: number, time: number) {
83
+ assertFinite('target', target);
84
+ const current = stateAt(time);
85
+ return createSpringSolution(parameters, {
86
+ position: current.position,
87
+ velocity: current.velocity,
88
+ target,
89
+ }, {
90
+ settle: {
91
+ position: settling.positionEpsilon,
92
+ velocity: settling.velocityEpsilon,
93
+ maxDuration: settling.maxDuration,
94
+ refinementIterations: settling.refinementIterations,
95
+ },
96
+ timing: { perceptualDuration: timing.perceptualDuration },
97
+ });
98
+ },
99
+ });
100
+ }
101
+
102
+ export function createSpring(options: SpringOptions): SpringSolution {
103
+ return createSpringSolution(
104
+ {
105
+ mass: options.mass,
106
+ stiffness: options.stiffness,
107
+ damping: options.damping,
108
+ },
109
+ {
110
+ position: options.from,
111
+ velocity: options.velocity === undefined ? 0 : options.velocity,
112
+ target: options.to,
113
+ },
114
+ {
115
+ ...(options.settle === undefined ? {} : { settle: options.settle }),
116
+ ...(options.timing === undefined ? {} : { timing: options.timing }),
117
+ },
118
+ );
119
+ }
@@ -0,0 +1,343 @@
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
+ import type { FrameDriver } from './spring-value.js';
12
+
13
+ export interface AdditiveSpringOptions {
14
+ settle?: SpringSettleInput;
15
+ timing?: SpringTimingInput;
16
+ }
17
+
18
+ export interface AdditiveSpringContributionOptions {
19
+ parameters?: SpringParameters;
20
+ settle?: SpringSettleInput;
21
+ timing?: SpringTimingInput;
22
+ velocity?: number;
23
+ }
24
+
25
+ export interface AdditiveSpringSnapshot {
26
+ value: number;
27
+ velocity: number;
28
+ target: number;
29
+ animating: boolean;
30
+ contributions: number;
31
+ }
32
+
33
+ export type AdditiveSpringEvent = 'change' | 'settle' | 'unsettled';
34
+ export type AdditiveSpringListener = (
35
+ snapshot: Readonly<AdditiveSpringSnapshot>,
36
+ ) => void;
37
+
38
+ export interface AdditiveSpringValue {
39
+ get(): number;
40
+ getVelocity(): number;
41
+ getTarget(): number;
42
+ getSnapshot(): Readonly<AdditiveSpringSnapshot>;
43
+ animateBy(delta: number, options?: AdditiveSpringContributionOptions): number;
44
+ cancel(contribution: number): void;
45
+ jump(value: number): void;
46
+ stop(): void;
47
+ on(event: AdditiveSpringEvent, listener: AdditiveSpringListener): () => void;
48
+ destroy(): void;
49
+ }
50
+
51
+ interface ActiveContribution {
52
+ solution: SpringSolution;
53
+ startedAt: number;
54
+ unsettledNotified: boolean;
55
+ }
56
+
57
+ function assertFinite(name: string, value: number): void {
58
+ if (!Number.isFinite(value)) {
59
+ throw new RangeError(`${name} must be a finite number`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Composes overlapping spring effects as independent additive contributions.
65
+ * Use createSpringValue when each new target should replace the previous one.
66
+ */
67
+ export function createAdditiveSpringValue(
68
+ initialValue: number,
69
+ parameters: SpringParameters,
70
+ driver: FrameDriver,
71
+ options: AdditiveSpringOptions = {},
72
+ ): AdditiveSpringValue {
73
+ assertFinite('initialValue', initialValue);
74
+ const defaultParameters = springParameters.fromPhysics(parameters);
75
+ const defaultSettling = resolveSettlingOptions(options.settle);
76
+ const defaultTiming = resolveSpringTimingInput(options.timing);
77
+ const listeners: Record<AdditiveSpringEvent, Set<AdditiveSpringListener>> = {
78
+ change: new Set(),
79
+ settle: new Set(),
80
+ unsettled: new Set(),
81
+ };
82
+ const contributions = new Map<number, ActiveContribution>();
83
+
84
+ let base = initialValue;
85
+ let value = initialValue;
86
+ let velocity = 0;
87
+ let nextId = 1;
88
+ let cancelFrame: (() => void) | undefined;
89
+ let destroyed = false;
90
+ let lastTime = Number.NEGATIVE_INFINITY;
91
+ let revision = 0;
92
+
93
+ const aggregateTarget = (): number => {
94
+ let aggregate = base;
95
+ for (const contribution of contributions.values()) {
96
+ aggregate += contribution.solution.initialState.target;
97
+ assertFinite('target', aggregate);
98
+ }
99
+ return aggregate;
100
+ };
101
+
102
+ const snapshot = (): Readonly<AdditiveSpringSnapshot> =>
103
+ Object.freeze({
104
+ value,
105
+ velocity,
106
+ target: aggregateTarget(),
107
+ animating: contributions.size > 0,
108
+ contributions: contributions.size,
109
+ });
110
+
111
+ const emit = (event: AdditiveSpringEvent): void => {
112
+ if (destroyed || listeners[event].size === 0) return;
113
+ const current = snapshot();
114
+ for (const listener of [...listeners[event]]) listener(current);
115
+ };
116
+
117
+ const write = (nextValue: number, nextVelocity: number): void => {
118
+ assertFinite('value', nextValue);
119
+ assertFinite('velocity', nextVelocity);
120
+ const changed =
121
+ !Object.is(value, nextValue) || !Object.is(velocity, nextVelocity);
122
+ value = nextValue;
123
+ velocity = nextVelocity;
124
+ if (changed) emit('change');
125
+ };
126
+
127
+ const validateTime = (time: number): void => {
128
+ assertFinite('frame time', time);
129
+ if (time < lastTime) throw new RangeError('frame time must be monotonic');
130
+ lastTime = time;
131
+ };
132
+
133
+ const sample = (time: number): void => {
134
+ validateTime(time);
135
+ if (contributions.size === 0) return;
136
+ const sampledRevision = revision;
137
+
138
+ let positionSum = 0;
139
+ let velocitySum = 0;
140
+ let nextBase = base;
141
+ const removedIds: number[] = [];
142
+ let removed = false;
143
+ let becameUnsettled = false;
144
+ for (const [id, contribution] of contributions) {
145
+ const elapsed = Math.max(0, time - contribution.startedAt);
146
+ const result = contribution.solution.getSettlingResult();
147
+ if (result.settled && elapsed >= result.duration) {
148
+ nextBase += contribution.solution.initialState.target;
149
+ assertFinite('base', nextBase);
150
+ removedIds.push(id);
151
+ removed = true;
152
+ continue;
153
+ }
154
+
155
+ const state = contribution.solution.stateAt(elapsed);
156
+ positionSum += state.position;
157
+ velocitySum += state.velocity;
158
+ assertFinite('position sum', positionSum);
159
+ assertFinite('velocity sum', velocitySum);
160
+ if (
161
+ !result.settled &&
162
+ !contribution.unsettledNotified &&
163
+ elapsed >= result.duration
164
+ ) {
165
+ contribution.unsettledNotified = true;
166
+ becameUnsettled = true;
167
+ }
168
+ }
169
+
170
+ const nextValue = nextBase + positionSum;
171
+ assertFinite('value', nextValue);
172
+ base = nextBase;
173
+ for (const id of removedIds) contributions.delete(id);
174
+ write(nextValue, velocitySum);
175
+ if (revision !== sampledRevision) return;
176
+ if (becameUnsettled) {
177
+ emit('unsettled');
178
+ if (revision !== sampledRevision) return;
179
+ }
180
+ if (removed && contributions.size === 0) emit('settle');
181
+ };
182
+
183
+ let requestFrame: () => void;
184
+ const frame = (time: number): void => {
185
+ cancelFrame = undefined;
186
+ if (destroyed) return;
187
+ sample(time);
188
+ if (contributions.size > 0) requestFrame();
189
+ };
190
+
191
+ requestFrame = (): void => {
192
+ if (destroyed || cancelFrame || contributions.size === 0) return;
193
+ cancelFrame = driver.schedule(frame);
194
+ };
195
+
196
+ const sampleNow = (): number => {
197
+ const time = driver.now();
198
+ sample(time);
199
+ return time;
200
+ };
201
+
202
+ const clearAtCurrentValue = (): void => {
203
+ base = value;
204
+ contributions.clear();
205
+ cancelFrame?.();
206
+ cancelFrame = undefined;
207
+ write(base, 0);
208
+ };
209
+
210
+ return Object.freeze({
211
+ get(): number {
212
+ return value;
213
+ },
214
+ getVelocity(): number {
215
+ return velocity;
216
+ },
217
+ getTarget(): number {
218
+ return snapshot().target;
219
+ },
220
+ getSnapshot: snapshot,
221
+ animateBy(
222
+ delta: number,
223
+ contributionOptions: AdditiveSpringContributionOptions = {},
224
+ ): number {
225
+ if (destroyed) throw new Error('additive spring value has been destroyed');
226
+ assertFinite('delta', delta);
227
+ const initialVelocity =
228
+ contributionOptions.velocity === undefined
229
+ ? 0
230
+ : contributionOptions.velocity;
231
+ assertFinite('velocity', initialVelocity);
232
+ const contributionParameters = springParameters.fromPhysics(
233
+ contributionOptions.parameters === undefined
234
+ ? defaultParameters
235
+ : contributionOptions.parameters,
236
+ );
237
+ if (contributionOptions.settle !== undefined) {
238
+ resolveSettlingOptions(contributionOptions.settle);
239
+ }
240
+ const settle = {
241
+ position: defaultSettling.positionEpsilon,
242
+ velocity: defaultSettling.velocityEpsilon,
243
+ maxDuration: defaultSettling.maxDuration,
244
+ refinementIterations: defaultSettling.refinementIterations,
245
+ ...contributionOptions.settle,
246
+ };
247
+ const timing =
248
+ contributionOptions.timing === undefined
249
+ ? defaultTiming
250
+ : resolveSpringTimingInput(contributionOptions.timing);
251
+ const time = sampleNow();
252
+ assertFinite('target', aggregateTarget() + delta);
253
+ const solution = createSpring({
254
+ from: 0,
255
+ to: delta,
256
+ velocity: initialVelocity,
257
+ ...contributionParameters,
258
+ settle,
259
+ ...(timing === undefined ? {} : { timing }),
260
+ });
261
+ const id = nextId;
262
+ nextId += 1;
263
+ revision += 1;
264
+
265
+ if (delta === 0 && initialVelocity === 0) {
266
+ if (contributions.size === 0) emit('settle');
267
+ return id;
268
+ }
269
+ contributions.set(id, {
270
+ solution,
271
+ startedAt: time,
272
+ unsettledNotified: false,
273
+ });
274
+ requestFrame();
275
+ return id;
276
+ },
277
+ cancel(contributionId: number): void {
278
+ if (destroyed) return;
279
+ if (!Number.isInteger(contributionId) || contributionId <= 0) {
280
+ throw new RangeError('contribution must be a positive integer');
281
+ }
282
+ const time = sampleNow();
283
+ const contribution = contributions.get(contributionId);
284
+ if (!contribution) return;
285
+ revision += 1;
286
+ const currentRevision = revision;
287
+ const state: SpringState = contribution.solution.stateAt(
288
+ Math.max(0, time - contribution.startedAt),
289
+ );
290
+ const nextBase = base + state.position;
291
+ const nextVelocity = velocity - state.velocity;
292
+ assertFinite('base', nextBase);
293
+ assertFinite('velocity', nextVelocity);
294
+ base = nextBase;
295
+ contributions.delete(contributionId);
296
+ write(value, nextVelocity);
297
+ if (revision !== currentRevision) return;
298
+ if (contributions.size === 0) {
299
+ cancelFrame?.();
300
+ cancelFrame = undefined;
301
+ emit('settle');
302
+ }
303
+ },
304
+ jump(nextValue: number): void {
305
+ if (destroyed) return;
306
+ assertFinite('value', nextValue);
307
+ const changed = !Object.is(value, nextValue) || velocity !== 0;
308
+ revision += 1;
309
+ const currentRevision = revision;
310
+ base = nextValue;
311
+ contributions.clear();
312
+ cancelFrame?.();
313
+ cancelFrame = undefined;
314
+ write(nextValue, 0);
315
+ if (revision !== currentRevision) return;
316
+ if (changed) emit('settle');
317
+ },
318
+ stop(): void {
319
+ if (destroyed || contributions.size === 0) return;
320
+ sampleNow();
321
+ if (contributions.size === 0) return;
322
+ revision += 1;
323
+ const currentRevision = revision;
324
+ clearAtCurrentValue();
325
+ if (revision !== currentRevision) return;
326
+ emit('settle');
327
+ },
328
+ on(event: AdditiveSpringEvent, listener: AdditiveSpringListener): () => void {
329
+ if (destroyed) return () => undefined;
330
+ listeners[event].add(listener);
331
+ return () => listeners[event].delete(listener);
332
+ },
333
+ destroy(): void {
334
+ if (destroyed) return;
335
+ revision += 1;
336
+ destroyed = true;
337
+ clearAtCurrentValue();
338
+ for (const eventListeners of Object.values(listeners)) {
339
+ eventListeners.clear();
340
+ }
341
+ },
342
+ });
343
+ }