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
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
|
+
}
|