@aphrody/m3-motion 3.3.1 → 3.3.3
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/dist/components/M3Collapse.d.ts.map +1 -1
- package/dist/components/M3ContainerTransform.d.ts +6 -6
- package/dist/components/M3ContainerTransform.d.ts.map +1 -1
- package/dist/components/M3Fade.d.ts.map +1 -1
- package/dist/components/M3FadeThrough.d.ts.map +1 -1
- package/dist/components/M3ListStagger.d.ts.map +1 -1
- package/dist/components/M3SharedAxis.d.ts.map +1 -1
- package/dist/components/M3Transition.d.ts.map +1 -1
- package/dist/easings.d.ts +1 -1
- package/dist/hooks/useM3Animate.d.ts.map +1 -1
- package/dist/hooks/useM3ReducedMotion.d.ts +6 -0
- package/dist/hooks/useM3ReducedMotion.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +398 -338
- package/dist/index.js.map +21 -1
- package/package.json +12 -13
- package/src/components/M3Collapse.tsx +82 -0
- package/src/components/M3ContainerTransform.tsx +187 -0
- package/src/components/M3Fade.tsx +68 -0
- package/src/components/M3FadeThrough.tsx +80 -0
- package/src/components/M3ListStagger.tsx +117 -0
- package/src/components/M3SharedAxis.tsx +115 -0
- package/src/components/M3Transition.tsx +207 -0
- package/src/easings.ts +62 -0
- package/src/hooks/useM3Animate.ts +93 -0
- package/src/hooks/useM3ReducedMotion.ts +43 -0
- package/src/index.ts +12 -0
- package/src/spring-interpolation.ts +390 -0
- package/src/springs.ts +54 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Physical spring interpolation for the Web platform (M3 Expressive, Phase 3.1).
|
|
5
|
+
*
|
|
6
|
+
* The React components in this package drive springs through `motion/react`,
|
|
7
|
+
* which runs a per-frame numerical integrator. Lit components from
|
|
8
|
+
* `@material/web` live inside a Shadow DOM and have no access to Motion, so
|
|
9
|
+
* they can only animate through CSS transitions (a single `cubic-bezier`) or
|
|
10
|
+
* the Web Animations API (`element.animate(keyframes, options)`).
|
|
11
|
+
*
|
|
12
|
+
* This module is the bridge: it turns the same physical spring parameters
|
|
13
|
+
* (`stiffness` / `damping` / `mass`) used by {@link m3Springs} into:
|
|
14
|
+
*
|
|
15
|
+
* 1. a settle-time estimate in milliseconds ({@link springDurationMs}),
|
|
16
|
+
* 2. a single `cubic-bezier(...)` CSS easing approximation
|
|
17
|
+
* ({@link springToCssEasing}), and
|
|
18
|
+
* 3. an exact sampling of the analytical damped-spring solution as WAAPI
|
|
19
|
+
* keyframes ({@link springToWaapiKeyframes}), which — unlike a
|
|
20
|
+
* cubic-bezier — preserves overshoot/bounce.
|
|
21
|
+
*
|
|
22
|
+
* All functions are pure, deterministic and SSR-safe: there is no DOM access
|
|
23
|
+
* at module load or call time. `Keyframe` is referenced only as a DOM lib
|
|
24
|
+
* type. The maths handle the three damping regimes (under-, critically- and
|
|
25
|
+
* over-damped) without dividing by zero.
|
|
26
|
+
*
|
|
27
|
+
* Physical model (mass-spring-damper, m x'' + c x' + k x = 0):
|
|
28
|
+
* - natural angular frequency w0 = sqrt(k / m)
|
|
29
|
+
* - damping ratio zeta = c / (2 * sqrt(k * m))
|
|
30
|
+
* - damped angular frequency wd = w0 * sqrt(1 - zeta^2) (under-damped)
|
|
31
|
+
*
|
|
32
|
+
* Here `k` = stiffness, `c` = damping, `m` = mass.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Physical parameters of a mass-spring-damper system. */
|
|
36
|
+
export interface SpringParams {
|
|
37
|
+
/** Spring stiffness `k` (> 0). Higher = snappier. */
|
|
38
|
+
stiffness: number;
|
|
39
|
+
/** Damping coefficient `c` (>= 0). Higher = less / no bounce. */
|
|
40
|
+
damping: number;
|
|
41
|
+
/** Mass `m` (> 0). Defaults to 1. Higher = slower, heavier feel. */
|
|
42
|
+
mass?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One sample of the analytical spring solution, progress-normalised. */
|
|
46
|
+
export interface SpringSample {
|
|
47
|
+
/** Timeline position in [0, 1]. */
|
|
48
|
+
offset: number;
|
|
49
|
+
/**
|
|
50
|
+
* Animated value at this offset. With `from`/`to` defaults (0 -> 1) this
|
|
51
|
+
* is the normalised progress and may exceed 1 (overshoot) for
|
|
52
|
+
* under-damped springs — that is the whole point of WAAPI sampling.
|
|
53
|
+
*/
|
|
54
|
+
value: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Result bundle for WAAPI consumption. */
|
|
58
|
+
export interface SpringWaapi {
|
|
59
|
+
/** Progress-normalised samples (always present, easy to remap). */
|
|
60
|
+
samples: SpringSample[];
|
|
61
|
+
/**
|
|
62
|
+
* Ready-to-use `Keyframe[]`. Empty `{ offset }`-only objects unless a
|
|
63
|
+
* property mapper is supplied via {@link springToWaapiKeyframes} options;
|
|
64
|
+
* use {@link springKeyframesForProperty} to project `samples` onto a CSS
|
|
65
|
+
* property such as `transform` or `opacity`.
|
|
66
|
+
*/
|
|
67
|
+
keyframes: Keyframe[];
|
|
68
|
+
/** WAAPI `KeyframeEffectOptions`-compatible timing. */
|
|
69
|
+
options: { duration: number; easing: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const MIN_DURATION_MS = 50;
|
|
73
|
+
const MAX_DURATION_MS = 2000;
|
|
74
|
+
/** Envelope threshold for "settled": within 0.5% of the rest position. */
|
|
75
|
+
const SETTLE_THRESHOLD = 0.005;
|
|
76
|
+
const DEFAULT_MASS = 1;
|
|
77
|
+
const DEFAULT_STEPS = 60;
|
|
78
|
+
|
|
79
|
+
/** Clamp helper (deterministic, NaN-safe via the final fallback). */
|
|
80
|
+
function clamp(value: number, min: number, max: number): number {
|
|
81
|
+
if (value < min) return min;
|
|
82
|
+
if (value > max) return max;
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Normalise raw params: enforce a positive mass/stiffness and a non-negative
|
|
88
|
+
* damping so the downstream maths never produce NaN/Infinity.
|
|
89
|
+
*/
|
|
90
|
+
function normalize(params: SpringParams): { k: number; c: number; m: number } {
|
|
91
|
+
const m = params.mass != null && params.mass > 0 ? params.mass : DEFAULT_MASS;
|
|
92
|
+
const k = params.stiffness > 0 ? params.stiffness : 1;
|
|
93
|
+
const c = params.damping >= 0 ? params.damping : 0;
|
|
94
|
+
return { k, c, m };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Natural angular frequency `w0 = sqrt(k / m)`. */
|
|
98
|
+
function naturalFrequency(k: number, m: number): number {
|
|
99
|
+
return Math.sqrt(k / m);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Damping ratio `zeta = c / (2 * sqrt(k * m))`. */
|
|
103
|
+
function dampingRatio(k: number, c: number, m: number): number {
|
|
104
|
+
const denom = 2 * Math.sqrt(k * m);
|
|
105
|
+
return denom > 0 ? c / denom : 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Estimate the spring settle time in milliseconds.
|
|
110
|
+
*
|
|
111
|
+
* The motion decays under the exponential envelope `exp(-zeta * w0 * t)`. The
|
|
112
|
+
* system is considered settled once that envelope drops below
|
|
113
|
+
* {@link SETTLE_THRESHOLD} (0.5%):
|
|
114
|
+
*
|
|
115
|
+
* exp(-zeta * w0 * t) = threshold
|
|
116
|
+
* => t = -ln(threshold) / (zeta * w0)
|
|
117
|
+
*
|
|
118
|
+
* For over-damped systems (`zeta > 1`) the slow real root dominates the decay,
|
|
119
|
+
* so we use the smaller-magnitude pole `s = w0 * (zeta - sqrt(zeta^2 - 1))`
|
|
120
|
+
* instead of `zeta * w0`, which would otherwise badly under-estimate the tail.
|
|
121
|
+
*
|
|
122
|
+
* The result is clamped to a sensible UI range
|
|
123
|
+
* [{@link MIN_DURATION_MS}, {@link MAX_DURATION_MS}].
|
|
124
|
+
*/
|
|
125
|
+
export function springDurationMs(params: SpringParams): number {
|
|
126
|
+
const { k, c, m } = normalize(params);
|
|
127
|
+
const w0 = naturalFrequency(k, m);
|
|
128
|
+
const zeta = dampingRatio(k, c, m);
|
|
129
|
+
|
|
130
|
+
// Effective decay rate (1/s). For under/critically-damped this is zeta*w0;
|
|
131
|
+
// for over-damped the dominant (slowest) pole governs the tail.
|
|
132
|
+
let decayRate: number;
|
|
133
|
+
if (zeta > 1) {
|
|
134
|
+
const root = Math.sqrt(zeta * zeta - 1);
|
|
135
|
+
decayRate = w0 * (zeta - root);
|
|
136
|
+
} else {
|
|
137
|
+
decayRate = zeta * w0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!(decayRate > 0) || !Number.isFinite(decayRate)) {
|
|
141
|
+
// Undamped or degenerate: cap at the maximum.
|
|
142
|
+
return MAX_DURATION_MS;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const settleSeconds = -Math.log(SETTLE_THRESHOLD) / decayRate;
|
|
146
|
+
const settleMs = settleSeconds * 1000;
|
|
147
|
+
return clamp(settleMs, MIN_DURATION_MS, MAX_DURATION_MS);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Approximate a spring with a single CSS `cubic-bezier(x1,y1,x2,y2)` easing.
|
|
152
|
+
*
|
|
153
|
+
* LIMITATION: a cubic-bezier easing is monotonic in time and its output is
|
|
154
|
+
* conceptually clamped to [0, 1] by the CSS engine for most properties, so it
|
|
155
|
+
* **cannot represent overshoot/bounce**. For bouncy (under-damped) spatial
|
|
156
|
+
* springs we therefore fall back to an expressive M3 decelerate curve
|
|
157
|
+
* (`cubic-bezier(0.05, 0.7, 0.1, 1)` = `emphasizedDecelerate`) which reads as
|
|
158
|
+
* a lively settle without the impossible overshoot. Use
|
|
159
|
+
* {@link springToWaapiKeyframes} when real bounce is required.
|
|
160
|
+
*
|
|
161
|
+
* Control points are derived from the damping ratio `zeta`:
|
|
162
|
+
* - low zeta (bouncy) -> sharper attack, snappier feel
|
|
163
|
+
* - high zeta (smooth) -> gentle ease-out, closer to standard decelerate
|
|
164
|
+
*
|
|
165
|
+
* The mapping is deterministic and the control points are bounded to valid
|
|
166
|
+
* cubic-bezier ranges (x in [0, 1]; y unbounded by spec but kept in [0, 1]).
|
|
167
|
+
*/
|
|
168
|
+
export function springToCssEasing(params: SpringParams): string {
|
|
169
|
+
const { k, c, m } = normalize(params);
|
|
170
|
+
const zeta = dampingRatio(k, c, m);
|
|
171
|
+
|
|
172
|
+
// Bouncy springs (visibly under-damped) overshoot, which a cubic-bezier
|
|
173
|
+
// cannot express -> fall back to an expressive M3 decelerate.
|
|
174
|
+
if (zeta < 0.85) {
|
|
175
|
+
const [x1, y1, x2, y2] = [0.05, 0.7, 0.1, 1.0];
|
|
176
|
+
return `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// For near-critical / over-damped springs we synthesise a decelerate
|
|
180
|
+
// curve whose attack sharpens as zeta drops toward 1. Map zeta in
|
|
181
|
+
// [0.85, ~2+] onto an interpolation factor t in [1, 0] (clamped), where
|
|
182
|
+
// t=1 is the sharpest valid attack and t=0 a soft standard ease-out.
|
|
183
|
+
const t = clamp((zeta - 0.85) / (1.6 - 0.85), 0, 1);
|
|
184
|
+
|
|
185
|
+
// Endpoints of the interpolation:
|
|
186
|
+
// sharp (t = 0): emphasized-ish decelerate, vivid attack
|
|
187
|
+
// smooth (t = 1): standard decelerate, gentle
|
|
188
|
+
const x1 = lerp(0.1, 0.0, t);
|
|
189
|
+
const y1 = lerp(0.65, 0.0, t);
|
|
190
|
+
const x2 = lerp(0.2, 0.0, t);
|
|
191
|
+
const y2 = 1.0;
|
|
192
|
+
|
|
193
|
+
return `cubic-bezier(${round(x1)}, ${round(y1)}, ${round(x2)}, ${round(y2)})`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function lerp(a: number, b: number, t: number): number {
|
|
197
|
+
return a + (b - a) * t;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function round(n: number): number {
|
|
201
|
+
return Math.round(n * 1000) / 1000;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Evaluate the analytical position of a unit-step damped spring at time `t`
|
|
206
|
+
* (seconds), starting at rest at 0 and settling to 1.
|
|
207
|
+
*
|
|
208
|
+
* Closed-form solutions of `m x'' + c x' + k x = k` with `x(0)=0`, `x'(0)=0`:
|
|
209
|
+
*
|
|
210
|
+
* - Under-damped (zeta < 1):
|
|
211
|
+
* x(t) = 1 - e^{-zeta w0 t} ( cos(wd t) + (zeta w0 / wd) sin(wd t) )
|
|
212
|
+
* with wd = w0 sqrt(1 - zeta^2)
|
|
213
|
+
* - Critically damped (zeta == 1):
|
|
214
|
+
* x(t) = 1 - e^{-w0 t} (1 + w0 t)
|
|
215
|
+
* - Over-damped (zeta > 1):
|
|
216
|
+
* with poles s1,2 = -w0 (zeta -/+ sqrt(zeta^2 - 1)),
|
|
217
|
+
* x(t) = 1 - ( s2 e^{s1 t} - s1 e^{s2 t} ) / (s2 - s1)
|
|
218
|
+
*
|
|
219
|
+
* The under-damped branch is the only one that can return values > 1
|
|
220
|
+
* (overshoot). The `wd` divide is guarded: it is only used when
|
|
221
|
+
* `1 - zeta^2 > 0`, so there is no division by zero at the regime boundaries.
|
|
222
|
+
*/
|
|
223
|
+
function springPosition(t: number, w0: number, zeta: number): number {
|
|
224
|
+
if (t <= 0) return 0;
|
|
225
|
+
|
|
226
|
+
if (zeta < 1 - 1e-6) {
|
|
227
|
+
// Under-damped.
|
|
228
|
+
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
229
|
+
const envelope = Math.exp(-zeta * w0 * t);
|
|
230
|
+
const osc = Math.cos(wd * t) + ((zeta * w0) / wd) * Math.sin(wd * t);
|
|
231
|
+
return 1 - envelope * osc;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (zeta <= 1 + 1e-6) {
|
|
235
|
+
// Critically damped (treat the boundary band as critical for stability).
|
|
236
|
+
const envelope = Math.exp(-w0 * t);
|
|
237
|
+
return 1 - envelope * (1 + w0 * t);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Over-damped: two distinct real poles.
|
|
241
|
+
const root = Math.sqrt(zeta * zeta - 1);
|
|
242
|
+
const s1 = -w0 * (zeta - root);
|
|
243
|
+
const s2 = -w0 * (zeta + root);
|
|
244
|
+
const e1 = Math.exp(s1 * t);
|
|
245
|
+
const e2 = Math.exp(s2 * t);
|
|
246
|
+
return 1 - (s2 * e1 - s1 * e2) / (s2 - s1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Sample the analytical damped-spring solution into WAAPI-ready data.
|
|
251
|
+
*
|
|
252
|
+
* Returns {@link SpringWaapi} containing:
|
|
253
|
+
* - `samples`: `{ offset, value }[]` of the progress curve (value in `from`
|
|
254
|
+
* -> `to` units; may overshoot past `to` for under-damped springs),
|
|
255
|
+
* - `keyframes`: `Keyframe[]` carrying only `offset` unless `opts.property`
|
|
256
|
+
* is given (see below),
|
|
257
|
+
* - `options`: `{ duration, easing: "linear" }` — easing is `linear`
|
|
258
|
+
* because the spring shape is fully baked into the per-frame samples.
|
|
259
|
+
*
|
|
260
|
+
* Options:
|
|
261
|
+
* - `steps` : number of samples (default {@link DEFAULT_STEPS}, min 2).
|
|
262
|
+
* - `from`/`to` : start/end values (defaults 0 -> 1).
|
|
263
|
+
* - `property` + `unit`/`format` : project samples onto a CSS property so the
|
|
264
|
+
* returned `keyframes` are directly consumable by `element.animate`.
|
|
265
|
+
*
|
|
266
|
+
* Examples:
|
|
267
|
+
* springToWaapiKeyframes(p, { property: "transform", format: (v) => `translateX(${v}px)`, from: 100, to: 0 })
|
|
268
|
+
* springToWaapiKeyframes(p, { property: "opacity" })
|
|
269
|
+
*
|
|
270
|
+
* The first sample is exactly `{ offset: 0, value: from }`; the last is
|
|
271
|
+
* `{ offset: 1, value: to }` (snapped to the rest position so the animation
|
|
272
|
+
* always finishes precisely on target, even though the analytical value at the
|
|
273
|
+
* truncated settle time may differ by the sub-threshold tail).
|
|
274
|
+
*/
|
|
275
|
+
export function springToWaapiKeyframes(
|
|
276
|
+
params: SpringParams,
|
|
277
|
+
opts?: {
|
|
278
|
+
steps?: number;
|
|
279
|
+
from?: number;
|
|
280
|
+
to?: number;
|
|
281
|
+
/** CSS property name to emit on each keyframe (e.g. "transform"). */
|
|
282
|
+
property?: string;
|
|
283
|
+
/** Format a numeric value into the property string (overrides `unit`). */
|
|
284
|
+
format?: (value: number) => string;
|
|
285
|
+
/** Unit suffix when `format` is absent (e.g. "px", "%"). */
|
|
286
|
+
unit?: string;
|
|
287
|
+
},
|
|
288
|
+
): SpringWaapi {
|
|
289
|
+
const { k, c, m } = normalize(params);
|
|
290
|
+
const w0 = naturalFrequency(k, m);
|
|
291
|
+
const zeta = dampingRatio(k, c, m);
|
|
292
|
+
|
|
293
|
+
const steps = Math.max(2, Math.floor(opts?.steps ?? DEFAULT_STEPS));
|
|
294
|
+
const from = opts?.from ?? 0;
|
|
295
|
+
const to = opts?.to ?? 1;
|
|
296
|
+
const span = to - from;
|
|
297
|
+
|
|
298
|
+
const duration = springDurationMs(params);
|
|
299
|
+
const durationSeconds = duration / 1000;
|
|
300
|
+
|
|
301
|
+
const samples: SpringSample[] = [];
|
|
302
|
+
for (let i = 0; i < steps; i++) {
|
|
303
|
+
const offset = i / (steps - 1);
|
|
304
|
+
let value: number;
|
|
305
|
+
if (i === 0) {
|
|
306
|
+
value = from;
|
|
307
|
+
} else if (i === steps - 1) {
|
|
308
|
+
// Snap the final frame exactly to the target.
|
|
309
|
+
value = to;
|
|
310
|
+
} else {
|
|
311
|
+
const t = offset * durationSeconds;
|
|
312
|
+
const progress = springPosition(t, w0, zeta);
|
|
313
|
+
value = from + span * progress;
|
|
314
|
+
}
|
|
315
|
+
samples.push({ offset, value });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const keyframes = opts?.property
|
|
319
|
+
? buildKeyframes(samples, opts.property, opts.format, opts.unit)
|
|
320
|
+
: samples.map((s) => ({ offset: s.offset }) as Keyframe);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
samples,
|
|
324
|
+
keyframes,
|
|
325
|
+
// Easing is linear: the spring's shape is encoded in the samples
|
|
326
|
+
// themselves, so the browser must interpolate them at constant rate.
|
|
327
|
+
options: { duration, easing: "linear" },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Project spring {@link SpringSample}s onto a CSS property, returning
|
|
333
|
+
* `Keyframe[]` ready for `element.animate`.
|
|
334
|
+
*
|
|
335
|
+
* @param samples output of {@link springToWaapiKeyframes}.
|
|
336
|
+
* @param property CSS property name, e.g. "transform" or "opacity".
|
|
337
|
+
* @param format optional formatter (value -> CSS string); overrides `unit`.
|
|
338
|
+
* @param unit unit suffix used when no `format` is given (default "").
|
|
339
|
+
*/
|
|
340
|
+
export function springKeyframesForProperty(
|
|
341
|
+
samples: readonly SpringSample[],
|
|
342
|
+
property: string,
|
|
343
|
+
format?: (value: number) => string,
|
|
344
|
+
unit?: string,
|
|
345
|
+
): Keyframe[] {
|
|
346
|
+
return buildKeyframes(samples, property, format, unit);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildKeyframes(
|
|
350
|
+
samples: readonly SpringSample[],
|
|
351
|
+
property: string,
|
|
352
|
+
format?: (value: number) => string,
|
|
353
|
+
unit?: string,
|
|
354
|
+
): Keyframe[] {
|
|
355
|
+
const suffix = unit ?? "";
|
|
356
|
+
return samples.map((s) => {
|
|
357
|
+
const css = format ? format(s.value) : `${s.value}${suffix}`;
|
|
358
|
+
// `Keyframe` indexes string property names, so a dynamic key is valid.
|
|
359
|
+
return { offset: s.offset, [property]: css } as Keyframe;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Spring preset shape as authored in {@link m3Springs} (a Motion `Transition`
|
|
365
|
+
* augmented with the physical fields). Kept minimal so we depend only on the
|
|
366
|
+
* numeric fields, not on the full Motion type.
|
|
367
|
+
*/
|
|
368
|
+
export interface SpringPresetLike {
|
|
369
|
+
stiffness?: number;
|
|
370
|
+
damping?: number;
|
|
371
|
+
mass?: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Extract {@link SpringParams} from an {@link m3Springs} entry (or any
|
|
376
|
+
* compatible preset object) for reuse on the Lit / WAAPI side.
|
|
377
|
+
*
|
|
378
|
+
* Missing fields fall back to the `default.spatial` M3 preset values so the
|
|
379
|
+
* result is always a usable, finite spring.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* springFromPreset(m3Springs.default.spatial) // { stiffness: 220, damping: 17, mass: 1 }
|
|
383
|
+
*/
|
|
384
|
+
export function springFromPreset(preset: SpringPresetLike): SpringParams {
|
|
385
|
+
return {
|
|
386
|
+
stiffness: preset.stiffness != null && preset.stiffness > 0 ? preset.stiffness : 220,
|
|
387
|
+
damping: preset.damping != null && preset.damping >= 0 ? preset.damping : 17,
|
|
388
|
+
mass: preset.mass != null && preset.mass > 0 ? preset.mass : DEFAULT_MASS,
|
|
389
|
+
};
|
|
390
|
+
}
|
package/src/springs.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Transition } from "motion/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Material Design 3 Spring Transition Presets.
|
|
5
|
+
* Driven by physical stiffness, damping, and mass.
|
|
6
|
+
*
|
|
7
|
+
* Category Guide:
|
|
8
|
+
* - Spatial: Used for movement (x/y positions, width/height, scale, borders). Includes overshoot (bounce).
|
|
9
|
+
* - Effects: Used for color changes, opacities, and gradients. Critically damped (no overshoot).
|
|
10
|
+
*/
|
|
11
|
+
export const m3Springs = {
|
|
12
|
+
fast: {
|
|
13
|
+
spatial: {
|
|
14
|
+
type: "spring",
|
|
15
|
+
stiffness: 400,
|
|
16
|
+
damping: 24,
|
|
17
|
+
mass: 0.8,
|
|
18
|
+
} as Transition,
|
|
19
|
+
effects: {
|
|
20
|
+
type: "spring",
|
|
21
|
+
stiffness: 350,
|
|
22
|
+
damping: 37,
|
|
23
|
+
mass: 0.8,
|
|
24
|
+
} as Transition,
|
|
25
|
+
},
|
|
26
|
+
default: {
|
|
27
|
+
spatial: {
|
|
28
|
+
type: "spring",
|
|
29
|
+
stiffness: 220,
|
|
30
|
+
damping: 17,
|
|
31
|
+
mass: 1.0,
|
|
32
|
+
} as Transition,
|
|
33
|
+
effects: {
|
|
34
|
+
type: "spring",
|
|
35
|
+
stiffness: 180,
|
|
36
|
+
damping: 26,
|
|
37
|
+
mass: 1.0,
|
|
38
|
+
} as Transition,
|
|
39
|
+
},
|
|
40
|
+
slow: {
|
|
41
|
+
spatial: {
|
|
42
|
+
type: "spring",
|
|
43
|
+
stiffness: 80,
|
|
44
|
+
damping: 12,
|
|
45
|
+
mass: 1.2,
|
|
46
|
+
} as Transition,
|
|
47
|
+
effects: {
|
|
48
|
+
type: "spring",
|
|
49
|
+
stiffness: 70,
|
|
50
|
+
damping: 16,
|
|
51
|
+
mass: 1.2,
|
|
52
|
+
} as Transition,
|
|
53
|
+
},
|
|
54
|
+
} as const;
|