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,133 @@
|
|
|
1
|
+
import type { SettlingResult, SpringSolution, SpringState } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const UNSETTLED_POLICIES = [
|
|
4
|
+
'continue',
|
|
5
|
+
'stop',
|
|
6
|
+
'snap',
|
|
7
|
+
'error',
|
|
8
|
+
] as const;
|
|
9
|
+
export type UnsettledPolicy = (typeof UNSETTLED_POLICIES)[number];
|
|
10
|
+
|
|
11
|
+
export interface SpringTrackSampleSource {
|
|
12
|
+
readonly duration: number;
|
|
13
|
+
readonly settling: SettlingResult;
|
|
14
|
+
readonly spring: SpringSolution;
|
|
15
|
+
readonly target: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SpringTrackTiming {
|
|
19
|
+
readonly driverDuration: number;
|
|
20
|
+
readonly finiteDuration: number;
|
|
21
|
+
readonly hasUnsettled: boolean;
|
|
22
|
+
readonly logicalDuration: number;
|
|
23
|
+
readonly requestedLogicalDuration: number;
|
|
24
|
+
readonly unsettledAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const GSAP_TIME_PRECISION = 1e7;
|
|
28
|
+
|
|
29
|
+
export function validateUnsettledPolicy(
|
|
30
|
+
value: unknown,
|
|
31
|
+
context = 'unsettled',
|
|
32
|
+
): UnsettledPolicy {
|
|
33
|
+
if (
|
|
34
|
+
value !== 'continue' &&
|
|
35
|
+
value !== 'stop' &&
|
|
36
|
+
value !== 'snap' &&
|
|
37
|
+
value !== 'error'
|
|
38
|
+
) {
|
|
39
|
+
throw new TypeError(
|
|
40
|
+
`${context} must be one of: ${UNSETTLED_POLICIES.join(', ')}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function gsapSafeDuration(duration: number): number {
|
|
47
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
48
|
+
throw new RangeError(
|
|
49
|
+
'GSAP duration must be a finite number greater than or equal to 0',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const wholeSeconds = Math.floor(duration);
|
|
53
|
+
const fractionalSeconds = duration - wholeSeconds;
|
|
54
|
+
const rounded =
|
|
55
|
+
wholeSeconds +
|
|
56
|
+
Math.ceil(fractionalSeconds * GSAP_TIME_PRECISION) / GSAP_TIME_PRECISION;
|
|
57
|
+
return Number.isFinite(rounded) ? rounded : duration;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function gsapDriverTime(
|
|
61
|
+
elapsed: number,
|
|
62
|
+
finiteDuration: number,
|
|
63
|
+
driverDuration: number = gsapSafeDuration(finiteDuration),
|
|
64
|
+
): number {
|
|
65
|
+
if (!Number.isFinite(elapsed) || elapsed < 0) {
|
|
66
|
+
throw new RangeError(
|
|
67
|
+
'elapsed time must be a finite number greater than or equal to 0',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (finiteDuration === 0 || elapsed >= finiteDuration) return driverDuration;
|
|
71
|
+
return (elapsed / finiteDuration) * driverDuration;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function springElapsedTime(
|
|
75
|
+
driverTime: number,
|
|
76
|
+
finiteDuration: number,
|
|
77
|
+
driverDuration: number = gsapSafeDuration(finiteDuration),
|
|
78
|
+
): number {
|
|
79
|
+
if (!Number.isFinite(driverTime) || driverTime < 0) {
|
|
80
|
+
throw new RangeError(
|
|
81
|
+
'driver time must be a finite number greater than or equal to 0',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (finiteDuration === 0 || driverDuration === 0) return 0;
|
|
85
|
+
if (driverTime >= driverDuration) return finiteDuration;
|
|
86
|
+
return (driverTime / driverDuration) * finiteDuration;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function springTrackState(
|
|
90
|
+
track: SpringTrackSampleSource,
|
|
91
|
+
time: number,
|
|
92
|
+
policy: UnsettledPolicy,
|
|
93
|
+
): SpringState {
|
|
94
|
+
if (track.settling.settled && time >= track.duration) {
|
|
95
|
+
return { position: track.target, velocity: 0 };
|
|
96
|
+
}
|
|
97
|
+
if (!track.settling.settled && time >= track.duration) {
|
|
98
|
+
if (policy === 'snap') return { position: track.target, velocity: 0 };
|
|
99
|
+
if (policy === 'continue') return track.spring.stateAt(time);
|
|
100
|
+
return track.spring.stateAt(track.duration);
|
|
101
|
+
}
|
|
102
|
+
return track.spring.stateAt(time);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function springTrackTiming(
|
|
106
|
+
tracks: readonly SpringTrackSampleSource[],
|
|
107
|
+
policy: UnsettledPolicy,
|
|
108
|
+
): SpringTrackTiming {
|
|
109
|
+
const finiteDuration = Math.max(...tracks.map((track) => track.duration), 0);
|
|
110
|
+
const hasUnsettled = tracks.some((track) => !track.settling.settled);
|
|
111
|
+
const requestedLogicalDuration = Math.max(
|
|
112
|
+
...tracks.map((track) => track.spring.timing.perceptualDuration),
|
|
113
|
+
0,
|
|
114
|
+
);
|
|
115
|
+
const logicalDuration =
|
|
116
|
+
policy === 'continue' && hasUnsettled
|
|
117
|
+
? requestedLogicalDuration
|
|
118
|
+
: Math.min(requestedLogicalDuration, finiteDuration);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
driverDuration: gsapSafeDuration(finiteDuration),
|
|
122
|
+
finiteDuration,
|
|
123
|
+
hasUnsettled,
|
|
124
|
+
logicalDuration,
|
|
125
|
+
requestedLogicalDuration,
|
|
126
|
+
unsettledAt: Math.max(
|
|
127
|
+
...tracks
|
|
128
|
+
.filter((track) => !track.settling.settled)
|
|
129
|
+
.map((track) => track.duration),
|
|
130
|
+
0,
|
|
131
|
+
),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ActiveTrackRegistration } from './active-tracks.js';
|
|
2
|
+
|
|
3
|
+
export interface ActiveTrackTransition {
|
|
4
|
+
readonly isOwner: boolean;
|
|
5
|
+
readonly releasedAtStart: boolean;
|
|
6
|
+
readonly restoredPrevious: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Keeps registry membership aligned with GSAP playback without allowing a
|
|
11
|
+
* discarded forward-running track to reclaim ownership. Earlier registrations
|
|
12
|
+
* may reactivate only when their local clock moves backwards.
|
|
13
|
+
*/
|
|
14
|
+
export function syncActiveTrackRegistration(
|
|
15
|
+
registration: ActiveTrackRegistration,
|
|
16
|
+
time: number,
|
|
17
|
+
lastTime: number,
|
|
18
|
+
options: { retainAtStart?: boolean } = {},
|
|
19
|
+
): ActiveTrackTransition {
|
|
20
|
+
if (
|
|
21
|
+
!options.retainAtStart &&
|
|
22
|
+
time <= 0 &&
|
|
23
|
+
lastTime > 0 &&
|
|
24
|
+
registration.isActive()
|
|
25
|
+
) {
|
|
26
|
+
const restored = registration.release();
|
|
27
|
+
return {
|
|
28
|
+
isOwner: false,
|
|
29
|
+
releasedAtStart: true,
|
|
30
|
+
restoredPrevious: restored,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
time > 0 &&
|
|
36
|
+
!registration.isActive() &&
|
|
37
|
+
(lastTime <= 0 || time < lastTime)
|
|
38
|
+
) {
|
|
39
|
+
registration.activate();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isOwner: registration.isOwner(),
|
|
44
|
+
releasedAtStart: false,
|
|
45
|
+
restoredPrevious: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function retireActiveTrackRegistrations(
|
|
50
|
+
registrations: Iterable<ActiveTrackRegistration | undefined>,
|
|
51
|
+
): void {
|
|
52
|
+
for (const registration of registrations) registration?.retire();
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export { createSpring } from './spring.js';
|
|
2
|
+
export { createSpringModel } from './model.js';
|
|
3
|
+
export { createVectorSpring } from './vector.js';
|
|
4
|
+
export {
|
|
5
|
+
springCharacteristics,
|
|
6
|
+
springParameters,
|
|
7
|
+
springPresets,
|
|
8
|
+
} from './parameters.js';
|
|
9
|
+
export {
|
|
10
|
+
MOTION_COMPATIBILITY_COMMIT,
|
|
11
|
+
MOTION_COMPATIBILITY_VERSION,
|
|
12
|
+
motionSpringParameters,
|
|
13
|
+
} from './motion-compatibility.js';
|
|
14
|
+
export {
|
|
15
|
+
CRITICAL_DAMPING_TOLERANCE,
|
|
16
|
+
angularFrequency,
|
|
17
|
+
classifyDamping,
|
|
18
|
+
dampingRatio,
|
|
19
|
+
} from './math.js';
|
|
20
|
+
export { createAnalyticalSolver, validateSpringParameters } from './solver.js';
|
|
21
|
+
export {
|
|
22
|
+
DEFAULT_SETTLING_OPTIONS,
|
|
23
|
+
getSettlingResult,
|
|
24
|
+
resolveSettlingOptions,
|
|
25
|
+
validateSettlingOptions,
|
|
26
|
+
} from './settling.js';
|
|
27
|
+
export type {
|
|
28
|
+
SettlingResult,
|
|
29
|
+
SpringInitialState,
|
|
30
|
+
SpringOptions,
|
|
31
|
+
SpringParameters,
|
|
32
|
+
SpringRegime,
|
|
33
|
+
SpringSettleInput,
|
|
34
|
+
SpringSettlingOptions,
|
|
35
|
+
SpringSolution,
|
|
36
|
+
SpringState,
|
|
37
|
+
SpringTiming,
|
|
38
|
+
SpringTimingInput,
|
|
39
|
+
} from './types.js';
|
|
40
|
+
export type {
|
|
41
|
+
PerceptualSpringInput,
|
|
42
|
+
ResponseSpringInput,
|
|
43
|
+
SettlingDurationSpringInput,
|
|
44
|
+
SpringCharacteristics,
|
|
45
|
+
SpringParameterConverters,
|
|
46
|
+
SpringPreset,
|
|
47
|
+
SpringPresetOptions,
|
|
48
|
+
SpringPresets,
|
|
49
|
+
VisualSpringInput,
|
|
50
|
+
} from './parameters.js';
|
|
51
|
+
export type {
|
|
52
|
+
MotionDurationSpringInput,
|
|
53
|
+
MotionSpringParameterConverters,
|
|
54
|
+
MotionVisualDurationSpringInput,
|
|
55
|
+
} from './motion-compatibility.js';
|
|
56
|
+
export type { SpringModel } from './model.js';
|
|
57
|
+
export type {
|
|
58
|
+
MutableVectorSpringState,
|
|
59
|
+
VectorSpringOptions,
|
|
60
|
+
VectorSpringSolution,
|
|
61
|
+
VectorSpringState,
|
|
62
|
+
} from './vector.js';
|
|
63
|
+
export {
|
|
64
|
+
createMotionSpringTween,
|
|
65
|
+
registerSpringPlugin,
|
|
66
|
+
} from './gsap/plugin.js';
|
|
67
|
+
export type {
|
|
68
|
+
MotionSpringEffectTweenVars,
|
|
69
|
+
MotionSpringEffectVars,
|
|
70
|
+
MotionSpringVars,
|
|
71
|
+
} from './gsap/plugin.js';
|
|
72
|
+
export { SUPPORTED_PROPERTIES, springTo } from './gsap/spring-to.js';
|
|
73
|
+
export type {
|
|
74
|
+
BuiltInSpringProperty,
|
|
75
|
+
SpringController,
|
|
76
|
+
SpringProperty,
|
|
77
|
+
SpringPropertyAdapter,
|
|
78
|
+
SpringPropertyOptions,
|
|
79
|
+
SpringSolutionMap,
|
|
80
|
+
SpringStateMap,
|
|
81
|
+
SpringTargetValue,
|
|
82
|
+
SpringTargets,
|
|
83
|
+
SpringTweenTarget,
|
|
84
|
+
SpringToSnapshot,
|
|
85
|
+
SpringToVars,
|
|
86
|
+
SpringVelocities,
|
|
87
|
+
UnsettledPolicy,
|
|
88
|
+
} from './gsap/spring-to.js';
|
|
89
|
+
export { createSpringValue } from './support/spring-value.js';
|
|
90
|
+
export { createAdditiveSpringValue } from './support/additive-spring-value.js';
|
|
91
|
+
export { createSpringKeyframes } from './support/keyframes.js';
|
|
92
|
+
export { createInertia, snapToGrid } from './support/inertia.js';
|
|
93
|
+
export {
|
|
94
|
+
normalizedVelocity,
|
|
95
|
+
physicalVelocity,
|
|
96
|
+
velocityFromSamples,
|
|
97
|
+
} from './support/velocity.js';
|
|
98
|
+
export type {
|
|
99
|
+
AdditiveSpringContributionOptions,
|
|
100
|
+
AdditiveSpringEvent,
|
|
101
|
+
AdditiveSpringListener,
|
|
102
|
+
AdditiveSpringOptions,
|
|
103
|
+
AdditiveSpringSnapshot,
|
|
104
|
+
AdditiveSpringValue,
|
|
105
|
+
} from './support/additive-spring-value.js';
|
|
106
|
+
export type {
|
|
107
|
+
SpringKeyframe,
|
|
108
|
+
SpringKeyframeSegment,
|
|
109
|
+
SpringKeyframeSequence,
|
|
110
|
+
SpringKeyframesOptions,
|
|
111
|
+
} from './support/keyframes.js';
|
|
112
|
+
export type {
|
|
113
|
+
InertiaBoundaryTransition,
|
|
114
|
+
InertiaOptions,
|
|
115
|
+
InertiaPhase,
|
|
116
|
+
InertiaSolution,
|
|
117
|
+
} from './support/inertia.js';
|
|
118
|
+
export type {
|
|
119
|
+
FrameDriver,
|
|
120
|
+
SpringValue,
|
|
121
|
+
SpringValueEvent,
|
|
122
|
+
SpringValueListener,
|
|
123
|
+
SpringValueOptions,
|
|
124
|
+
SpringValueRetargetOptions,
|
|
125
|
+
SpringValueSnapshot,
|
|
126
|
+
} from './support/spring-value.js';
|
|
127
|
+
export type {
|
|
128
|
+
VelocityFromSamplesOptions,
|
|
129
|
+
VelocitySample,
|
|
130
|
+
} from './support/velocity.js';
|
package/src/math.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SpringParameters, SpringRegime } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const CRITICAL_DAMPING_TOLERANCE = 1e-7;
|
|
4
|
+
|
|
5
|
+
export function angularFrequency({ mass, stiffness }: SpringParameters): number {
|
|
6
|
+
return Math.sqrt(stiffness) / Math.sqrt(mass);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function dampingDecayRate({ mass, damping }: SpringParameters): number {
|
|
10
|
+
if (damping === 0) return 0;
|
|
11
|
+
|
|
12
|
+
// Divide the larger operand first so a representable c / (2m) is not lost
|
|
13
|
+
// to either an overflowing ratio or an underflowing c / 2 intermediate.
|
|
14
|
+
return damping > mass ? damping / 2 / mass : damping / mass / 2;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function dampingRatio(parameters: SpringParameters): number {
|
|
18
|
+
return dampingDecayRate(parameters) / angularFrequency(parameters);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function classifyDamping(
|
|
22
|
+
ratio: number,
|
|
23
|
+
tolerance: number = CRITICAL_DAMPING_TOLERANCE,
|
|
24
|
+
): SpringRegime {
|
|
25
|
+
assertFinite('dampingRatio', ratio);
|
|
26
|
+
assertFinite('tolerance', tolerance);
|
|
27
|
+
if (ratio < 0) {
|
|
28
|
+
throw new RangeError('dampingRatio must be greater than or equal to 0');
|
|
29
|
+
}
|
|
30
|
+
if (tolerance < 0) {
|
|
31
|
+
throw new RangeError('tolerance must be greater than or equal to 0');
|
|
32
|
+
}
|
|
33
|
+
if (Math.abs(ratio - 1) <= tolerance) return 'critical';
|
|
34
|
+
return ratio < 1 ? 'underdamped' : 'overdamped';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function assertFinite(name: string, value: number): void {
|
|
38
|
+
if (!Number.isFinite(value)) {
|
|
39
|
+
throw new RangeError(`${name} must be a finite number`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function assertNonNegativeTime(time: number): void {
|
|
44
|
+
assertFinite('time', time);
|
|
45
|
+
if (time < 0) throw new RangeError('time must be greater than or equal to 0');
|
|
46
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { assertFinite } from './math.js';
|
|
2
|
+
import { springCharacteristics } from './parameters.js';
|
|
3
|
+
import { getSettlingResult, resolveSettlingOptions } from './settling.js';
|
|
4
|
+
import { createAnalyticalSolver, validateInitialState } from './solver.js';
|
|
5
|
+
import {
|
|
6
|
+
createSpringSolution,
|
|
7
|
+
type SpringSolutionOptions,
|
|
8
|
+
} from './spring.js';
|
|
9
|
+
import type { SpringCharacteristics } from './parameters.js';
|
|
10
|
+
import type {
|
|
11
|
+
SettlingResult,
|
|
12
|
+
SpringInitialState,
|
|
13
|
+
SpringParameters,
|
|
14
|
+
SpringSettleInput,
|
|
15
|
+
SpringSolution,
|
|
16
|
+
SpringState,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
export interface SpringModel {
|
|
20
|
+
readonly parameters: Readonly<SpringParameters>;
|
|
21
|
+
readonly characteristics: Readonly<SpringCharacteristics>;
|
|
22
|
+
force(state: SpringState, target: number): number;
|
|
23
|
+
stateAt(initial: SpringInitialState, time: number): SpringState;
|
|
24
|
+
advance(state: SpringState, target: number, deltaTime: number): SpringState;
|
|
25
|
+
settling(initial: SpringInitialState, options?: SpringSettleInput): SettlingResult;
|
|
26
|
+
solve(initial: SpringInitialState, options?: SpringSolutionOptions): SpringSolution;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createSpringModel(parameters: SpringParameters): SpringModel {
|
|
30
|
+
const characteristics = springCharacteristics(parameters);
|
|
31
|
+
const canonicalParameters: Readonly<SpringParameters> = Object.freeze({
|
|
32
|
+
mass: parameters.mass,
|
|
33
|
+
stiffness: parameters.stiffness,
|
|
34
|
+
damping: parameters.damping,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
parameters: canonicalParameters,
|
|
39
|
+
characteristics,
|
|
40
|
+
force(state: SpringState, target: number): number {
|
|
41
|
+
validateInitialState({ ...state, target });
|
|
42
|
+
const force =
|
|
43
|
+
-canonicalParameters.stiffness * (state.position - target) -
|
|
44
|
+
canonicalParameters.damping * state.velocity;
|
|
45
|
+
assertFinite('force', force);
|
|
46
|
+
return force;
|
|
47
|
+
},
|
|
48
|
+
stateAt(initial: SpringInitialState, time: number): SpringState {
|
|
49
|
+
return createAnalyticalSolver(canonicalParameters, initial).stateAt(time);
|
|
50
|
+
},
|
|
51
|
+
advance(state: SpringState, target: number, deltaTime: number): SpringState {
|
|
52
|
+
return createAnalyticalSolver(canonicalParameters, { ...state, target }).stateAt(
|
|
53
|
+
deltaTime,
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
settling(
|
|
57
|
+
initial: SpringInitialState,
|
|
58
|
+
options?: SpringSettleInput,
|
|
59
|
+
): SettlingResult {
|
|
60
|
+
const solver = createAnalyticalSolver(canonicalParameters, initial);
|
|
61
|
+
return getSettlingResult(solver, resolveSettlingOptions(options));
|
|
62
|
+
},
|
|
63
|
+
solve(
|
|
64
|
+
initial: SpringInitialState,
|
|
65
|
+
options?: SpringSolutionOptions,
|
|
66
|
+
): SpringSolution {
|
|
67
|
+
return createSpringSolution(canonicalParameters, initial, options);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { assertFinite } from './math.js';
|
|
2
|
+
import { springParameters } from './parameters.js';
|
|
3
|
+
import type { SpringParameters } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const MOTION_COMPATIBILITY_VERSION = '13.1.0';
|
|
6
|
+
export const MOTION_COMPATIBILITY_COMMIT =
|
|
7
|
+
'adaf7a4e5368d704ea350669f6ac674fb26ff270';
|
|
8
|
+
|
|
9
|
+
const SAFE_MINIMUM = 0.001;
|
|
10
|
+
const ROOT_ITERATIONS = 12;
|
|
11
|
+
const MIN_DURATION = 0.01;
|
|
12
|
+
const MAX_DURATION = 10;
|
|
13
|
+
const MIN_DAMPING_RATIO = 0.05;
|
|
14
|
+
const MAX_DAMPING_RATIO = 1;
|
|
15
|
+
|
|
16
|
+
export interface MotionDurationSpringInput {
|
|
17
|
+
/** Public compatibility input in seconds. Motion's audited generator stores this in ms. */
|
|
18
|
+
duration: number;
|
|
19
|
+
bounce?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MotionVisualDurationSpringInput {
|
|
23
|
+
visualDuration: number;
|
|
24
|
+
bounce?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MotionSpringParameterConverters {
|
|
28
|
+
fromDuration(input: MotionDurationSpringInput): Readonly<SpringParameters>;
|
|
29
|
+
fromVisualDuration(input: MotionVisualDurationSpringInput): Readonly<SpringParameters>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clamp(minimum: number, maximum: number, value: number): number {
|
|
33
|
+
return Math.min(Math.max(value, minimum), maximum);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assertPositive(name: string, value: number): void {
|
|
37
|
+
assertFinite(name, value);
|
|
38
|
+
if (value <= 0) throw new RangeError(`${name} must be greater than 0`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function approximateRoot(
|
|
42
|
+
envelope: (frequency: number) => number,
|
|
43
|
+
derivative: (frequency: number) => number,
|
|
44
|
+
initialGuess: number,
|
|
45
|
+
): number {
|
|
46
|
+
let result = initialGuess;
|
|
47
|
+
for (let iteration = 1; iteration < ROOT_ITERATIONS; iteration += 1) {
|
|
48
|
+
result -= envelope(result) / derivative(result);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function durationParameters(
|
|
54
|
+
requestedDuration: number,
|
|
55
|
+
requestedBounce: number,
|
|
56
|
+
): Readonly<SpringParameters> {
|
|
57
|
+
assertPositive('duration', requestedDuration);
|
|
58
|
+
assertFinite('bounce', requestedBounce);
|
|
59
|
+
|
|
60
|
+
const duration = clamp(MIN_DURATION, MAX_DURATION, requestedDuration);
|
|
61
|
+
const ratio = clamp(
|
|
62
|
+
MIN_DAMPING_RATIO,
|
|
63
|
+
MAX_DAMPING_RATIO,
|
|
64
|
+
1 - requestedBounce,
|
|
65
|
+
);
|
|
66
|
+
let envelope: (frequency: number) => number;
|
|
67
|
+
let derivative: (frequency: number) => number;
|
|
68
|
+
|
|
69
|
+
if (ratio < 1) {
|
|
70
|
+
envelope = (frequency: number): number => {
|
|
71
|
+
const exponentialDecay = frequency * ratio;
|
|
72
|
+
const dampedFrequency = frequency * Math.sqrt(1 - ratio * ratio);
|
|
73
|
+
return (
|
|
74
|
+
SAFE_MINIMUM -
|
|
75
|
+
(exponentialDecay / dampedFrequency) *
|
|
76
|
+
Math.exp(-exponentialDecay * duration)
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
derivative = (frequency: number): number => {
|
|
80
|
+
const exponentialDecay = frequency * ratio;
|
|
81
|
+
const squaredDecay = ratio * ratio * frequency * frequency * duration;
|
|
82
|
+
const decay = Math.exp(-exponentialDecay * duration);
|
|
83
|
+
const squaredDampedFrequency =
|
|
84
|
+
frequency * frequency * Math.sqrt(1 - ratio * ratio);
|
|
85
|
+
const factor = -envelope(frequency) + SAFE_MINIMUM > 0 ? -1 : 1;
|
|
86
|
+
return (factor * -squaredDecay * decay) / squaredDampedFrequency;
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
envelope = (frequency: number): number =>
|
|
90
|
+
-SAFE_MINIMUM +
|
|
91
|
+
Math.exp(-frequency * duration) * (frequency * duration + 1);
|
|
92
|
+
derivative = (frequency: number): number =>
|
|
93
|
+
Math.exp(-frequency * duration) * -frequency * duration * duration;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const frequency = approximateRoot(envelope, derivative, 5 / duration);
|
|
97
|
+
if (!Number.isFinite(frequency) || frequency <= 0) {
|
|
98
|
+
// This mirrors the audited generator's deterministic fallback, while the
|
|
99
|
+
// compatibility module keeps it explicit and isolated from canonical APIs.
|
|
100
|
+
return springParameters.fromPhysics({ mass: 1, stiffness: 100, damping: 10 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return springParameters.fromPhysics({
|
|
104
|
+
mass: 1,
|
|
105
|
+
stiffness: frequency * frequency,
|
|
106
|
+
damping: ratio * 2 * frequency,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const motionSpringParameters: MotionSpringParameterConverters = Object.freeze({
|
|
111
|
+
fromDuration({
|
|
112
|
+
duration,
|
|
113
|
+
bounce = 0.3,
|
|
114
|
+
}: MotionDurationSpringInput): Readonly<SpringParameters> {
|
|
115
|
+
return durationParameters(duration, bounce);
|
|
116
|
+
},
|
|
117
|
+
fromVisualDuration({
|
|
118
|
+
visualDuration,
|
|
119
|
+
bounce = 0,
|
|
120
|
+
}: MotionVisualDurationSpringInput): Readonly<SpringParameters> {
|
|
121
|
+
assertPositive('visualDuration', visualDuration);
|
|
122
|
+
assertFinite('bounce', bounce);
|
|
123
|
+
const frequency = (2 * Math.PI) / (visualDuration * 1.2);
|
|
124
|
+
const ratio = clamp(MIN_DAMPING_RATIO, MAX_DAMPING_RATIO, 1 - bounce);
|
|
125
|
+
return springParameters.fromPhysics({
|
|
126
|
+
mass: 1,
|
|
127
|
+
stiffness: frequency * frequency,
|
|
128
|
+
damping: 2 * ratio * frequency,
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
});
|