coily 0.13.1 → 0.15.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/dist/css.d.ts ADDED
@@ -0,0 +1,175 @@
1
+ import { r as SpringState, t as SpringDefinition } from "./config-OOMMRdK8.js";
2
+ //#region src/css.d.ts
3
+ interface LinearEasingOptions {
4
+ /**
5
+ * Animation range the duration is tuned for, in value units. The easing
6
+ * shape is identical regardless; only the settle time scales with it.
7
+ * The keyframe helpers (`springToWaapi`, `springToCss`,
8
+ * `springToTransition`) default it to the largest move among their
9
+ * specs instead.
10
+ * @default 1
11
+ */
12
+ readonly displacement?: number;
13
+ /**
14
+ * Max output error of the piecewise-linear approximation, in progress
15
+ * units (0..1). Smaller keeps more stops.
16
+ * @default 0.0025
17
+ */
18
+ readonly maxError?: number;
19
+ /**
20
+ * Hard cap on generated duration, in ms, for near-undamped configs.
21
+ * @default 10000
22
+ */
23
+ readonly maxDuration?: number;
24
+ }
25
+ interface EasingBase {
26
+ /** A CSS `linear()` easing string. */
27
+ readonly easing: string;
28
+ /** Duration to pair the easing with, in ms. */
29
+ readonly duration: number;
30
+ /** Number of stops in the easing (diagnostic). */
31
+ readonly stops: number;
32
+ /** Worst reconstruction error vs the dense trajectory (diagnostic). */
33
+ readonly maxError: number;
34
+ }
35
+ /** The easing of a spring that comes to rest: play it once over `duration`. */
36
+ interface SettlingEasing extends EasingBase {
37
+ readonly mode: 'settle';
38
+ }
39
+ /**
40
+ * The easing of a spring that never rests — an undamped oscillation,
41
+ * expressed as a seamless loop. Animate the property between its two
42
+ * swing extremes (`from` and `2 * target - from`, target at the 50%
43
+ * midpoint) with `animation-iteration-count: infinite` and
44
+ * `animation-direction: alternate`. `duration` is one half period.
45
+ */
46
+ interface LoopingEasing extends EasingBase {
47
+ readonly mode: 'loop';
48
+ readonly direction: 'alternate';
49
+ readonly iterations: 'infinite';
50
+ }
51
+ type LinearEasing = SettlingEasing | LoopingEasing;
52
+ /**
53
+ * Turns a resolved spring config into a CSS `linear()` easing and the
54
+ * duration in ms to pair it with. A spring that settles returns a
55
+ * `'settle'` easing. A pure undamped config (`dampingRatio: 0` with
56
+ * passthrough `arrival`) has no friction and never settles, so it
57
+ * returns a `'loop'` easing for an infinite alternating animation.
58
+ */
59
+ declare function springToLinear(config: SpringDefinition, options?: LinearEasingOptions): LinearEasing;
60
+ /**
61
+ * Like `springToLinear`, but from a spring caught mid-flight rather than at
62
+ * rest: `state` carries both the displacement from the target (`position`)
63
+ * and the current `velocity`, so the easing continues the motion smoothly
64
+ * instead of restarting. Use it to regenerate an animation when its target
65
+ * changes — the interruption carries momentum instead of stalling. The
66
+ * from-rest shape trick doesn't apply here (the curve now depends on the
67
+ * velocity), so this is computed per interruption, not cached per config.
68
+ *
69
+ * Always a settling easing; `position` must be nonzero.
70
+ */
71
+ declare function springFromState(config: SpringDefinition, state: SpringState, options?: LinearEasingOptions): LinearEasing;
72
+ /**
73
+ * Evolves a spring `state` forward by `elapsedMs`, returning the new
74
+ * displacement and velocity. Pair it with a running `element.animate` — read
75
+ * the animation's `currentTime`, evolve the state it started from, and you
76
+ * have the exact live velocity the Web Animations API never exposes, ready
77
+ * to hand to `springFromState` on interruption.
78
+ */
79
+ declare function springStateAt(config: SpringDefinition, state: SpringState, elapsedMs: number): SpringState;
80
+ /** What to animate: a CSS property from one numeric value to another. */
81
+ interface PropertySpec {
82
+ /** The CSS property, e.g. `'opacity'`, `'transform'`, `'translate'`. */
83
+ readonly property: string;
84
+ /** Start value. */
85
+ readonly from: number;
86
+ /** Target value the spring is tuned toward. */
87
+ readonly to: number;
88
+ /**
89
+ * Unit appended to numbers, e.g. `'px'`, `'%'`, `'deg'`.
90
+ * @default ''
91
+ */
92
+ readonly unit?: string;
93
+ /**
94
+ * Formats an interpolated number into a property value, overriding
95
+ * `unit` — needed for wrapped values like `(v) => `translateX(${v}px)``.
96
+ */
97
+ readonly format?: (value: number) => string;
98
+ /**
99
+ * The property's current velocity, in its units per second. Set it to
100
+ * regenerate an interrupted animation from its live motion so it carries
101
+ * momentum instead of restarting from rest. Single-property only — a
102
+ * from-state easing depends on that property's velocity and can't be
103
+ * shared across properties.
104
+ */
105
+ readonly velocity?: number;
106
+ }
107
+ interface CssOptions extends LinearEasingOptions {
108
+ /**
109
+ * Name for the generated `@keyframes` rule.
110
+ * @default 'coily'
111
+ */
112
+ readonly name?: string;
113
+ }
114
+ /**
115
+ * Builds arguments for `element.animate(keyframes, options)`: two keyframes
116
+ * carrying the endpoint values, and options carrying the spring's `linear()`
117
+ * easing and duration. Pass an array of specs to drive several properties
118
+ * from one spring. A settling spring fills forwards; an undamped one repeats
119
+ * infinitely in alternating directions.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const { keyframes, options } = springToWaapi(config, {
124
+ * property: 'translate',
125
+ * from: 0,
126
+ * to: 300,
127
+ * unit: 'px',
128
+ * })
129
+ * element.animate(keyframes, options)
130
+ * ```
131
+ */
132
+ declare function springToWaapi(config: SpringDefinition, spec: PropertySpec | PropertySpec[], options?: LinearEasingOptions): {
133
+ keyframes: Keyframe[];
134
+ options: KeyframeAnimationOptions;
135
+ };
136
+ /**
137
+ * Builds CSS text for the animation: a `@keyframes` rule and an `animation`
138
+ * shorthand value, driving one property or several from one spring. The
139
+ * `name` option (default `'coily'`) names the `@keyframes` rule and is
140
+ * referenced by the `animation` value.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * const { keyframes, animation } = springToCss(config, {
145
+ * property: 'translate',
146
+ * from: 0,
147
+ * to: 300,
148
+ * unit: 'px',
149
+ * })
150
+ * const style = document.createElement('style')
151
+ * style.textContent = keyframes
152
+ * document.head.append(style)
153
+ * element.style.animation = animation
154
+ * ```
155
+ */
156
+ declare function springToCss(config: SpringDefinition, spec: PropertySpec | PropertySpec[], options?: CssOptions): {
157
+ keyframes: string;
158
+ animation: string;
159
+ };
160
+ /**
161
+ * Builds a CSS `transition` value that springs a property change: set it on
162
+ * the element, then change the property (on `:hover`, a toggled class, a
163
+ * state attribute) to animate. Pass several specs for one `transition`
164
+ * across properties.
165
+ *
166
+ * Only settling springs — a transition can't loop. It also can't carry
167
+ * momentum: a change interrupted mid-flight restarts the spring from rest
168
+ * instead of reversing through its current velocity, so it stalls at the
169
+ * turn. For motion that's reversed often, drive a live `Spring` instead. The
170
+ * specs' `from`/`to` only tune the duration; the actual values live in your
171
+ * CSS rules.
172
+ */
173
+ declare function springToTransition(config: SpringDefinition, spec: PropertySpec | PropertySpec[], options?: LinearEasingOptions): string;
174
+ //#endregion
175
+ export { CssOptions, LinearEasing, LinearEasingOptions, LoopingEasing, PropertySpec, SettlingEasing, springFromState, springStateAt, springToCss, springToLinear, springToTransition, springToWaapi };
package/dist/css.js ADDED
@@ -0,0 +1,294 @@
1
+ import { i as State, n as OverdampedSolver, o as invariant, r as UnderdampedSolver, t as CriticallyDampedSolver } from "./solver-CVXOTDkZ.js";
2
+ //#region src/css.ts
3
+ function pickSolver(config, state) {
4
+ const solver = config.dampingRatio < 1 ? new UnderdampedSolver(state) : config.dampingRatio === 1 ? new CriticallyDampedSolver(state) : new OverdampedSolver(state);
5
+ solver.configure(config);
6
+ return solver;
7
+ }
8
+ function walk(config, displacement, spanSec, scale, stopAtRest, velocity = 0) {
9
+ const state = new State(config, displacement, velocity);
10
+ const solver = pickSolver(config, state);
11
+ const period = 2 * Math.PI / config.naturalFrequency;
12
+ const dt = Math.max(1e-4, Math.min(period / 48, spanSec / 160));
13
+ const points = [{
14
+ x: 0,
15
+ y: 0
16
+ }];
17
+ let t = 0;
18
+ while (spanSec - t > 1e-9) {
19
+ const step = Math.min(dt, spanSec - t);
20
+ solver.tick(step);
21
+ t += step;
22
+ points.push({
23
+ x: t / spanSec,
24
+ y: (1 - state.position / displacement) * scale
25
+ });
26
+ if (stopAtRest && state.isResting) break;
27
+ }
28
+ return points;
29
+ }
30
+ function buildEasing(points, maxError) {
31
+ const simplified = simplify(points, maxError);
32
+ return {
33
+ easing: format(simplified),
34
+ stops: simplified.length,
35
+ maxError: reconstructionError(points, simplified)
36
+ };
37
+ }
38
+ function buildSettle(config, displacement, durationMs, maxError, velocity = 0) {
39
+ const points = walk(config, displacement, durationMs / 1e3, 1, true, velocity);
40
+ const last = points[points.length - 1];
41
+ if (1 - last.x > 1e-6) points.push({
42
+ x: 1,
43
+ y: 1
44
+ });
45
+ else {
46
+ last.x = 1;
47
+ last.y = 1;
48
+ }
49
+ return {
50
+ mode: "settle",
51
+ duration: durationMs,
52
+ ...buildEasing(points, maxError)
53
+ };
54
+ }
55
+ function buildLoop(config, displacement, maxError) {
56
+ const halfSec = Math.PI / config.naturalFrequency;
57
+ const points = walk(config, displacement, halfSec, .5, false);
58
+ const last = points[points.length - 1];
59
+ last.x = 1;
60
+ last.y = 1;
61
+ return {
62
+ mode: "loop",
63
+ duration: halfSec * 1e3,
64
+ direction: "alternate",
65
+ iterations: "infinite",
66
+ ...buildEasing(points, maxError)
67
+ };
68
+ }
69
+ function simplify(points, epsilon) {
70
+ const keep = Array.from({ length: points.length }, () => false);
71
+ keep[0] = true;
72
+ keep[points.length - 1] = true;
73
+ const stack = [[0, points.length - 1]];
74
+ while (stack.length > 0) {
75
+ const [lo, hi] = stack.pop();
76
+ const a = points[lo];
77
+ const b = points[hi];
78
+ const dx = b.x - a.x;
79
+ let worst = -1;
80
+ let worstIndex = -1;
81
+ for (let i = lo + 1; i < hi; i += 1) {
82
+ const p = points[i];
83
+ const f = dx === 0 ? 0 : (p.x - a.x) / dx;
84
+ const interpolated = a.y + f * (b.y - a.y);
85
+ const distance = Math.abs(p.y - interpolated);
86
+ if (distance > worst) {
87
+ worst = distance;
88
+ worstIndex = i;
89
+ }
90
+ }
91
+ if (worst > epsilon && worstIndex !== -1) {
92
+ keep[worstIndex] = true;
93
+ stack.push([lo, worstIndex], [worstIndex, hi]);
94
+ }
95
+ }
96
+ return points.filter((_, i) => keep[i]);
97
+ }
98
+ function trim(value, places) {
99
+ return Number(value.toFixed(places)).toString();
100
+ }
101
+ function format(points) {
102
+ return `linear(${points.map((point, i) => {
103
+ const output = trim(point.y, 5);
104
+ if (i === 0 || i === points.length - 1) return output;
105
+ return `${output} ${trim(point.x * 100, 2)}%`;
106
+ }).join(", ")})`;
107
+ }
108
+ function reconstructionError(dense, simplified) {
109
+ let worst = 0;
110
+ let j = 0;
111
+ for (const point of dense) {
112
+ while (j < simplified.length - 2 && simplified[j + 1].x < point.x) j += 1;
113
+ const a = simplified[j];
114
+ const b = simplified[j + 1];
115
+ const dx = b.x - a.x;
116
+ const f = dx === 0 ? 0 : (point.x - a.x) / dx;
117
+ worst = Math.max(worst, Math.abs(point.y - (a.y + f * (b.y - a.y))));
118
+ }
119
+ return worst;
120
+ }
121
+ /**
122
+ * Turns a resolved spring config into a CSS `linear()` easing and the
123
+ * duration in ms to pair it with. A spring that settles returns a
124
+ * `'settle'` easing. A pure undamped config (`dampingRatio: 0` with
125
+ * passthrough `arrival`) has no friction and never settles, so it
126
+ * returns a `'loop'` easing for an infinite alternating animation.
127
+ */
128
+ function springToLinear(config, options = {}) {
129
+ const displacement = options.displacement ?? 1;
130
+ const maxError = options.maxError ?? .0025;
131
+ const maxDuration = options.maxDuration ?? 1e4;
132
+ if (config.dampingRatio === 0 && config.arrival === 1) return buildLoop(config, displacement, maxError);
133
+ const settleTime = config.computeTimeRemaining({
134
+ position: displacement,
135
+ velocity: 0
136
+ });
137
+ return buildSettle(config, displacement, Math.min(settleTime, maxDuration), maxError);
138
+ }
139
+ /**
140
+ * Like `springToLinear`, but from a spring caught mid-flight rather than at
141
+ * rest: `state` carries both the displacement from the target (`position`)
142
+ * and the current `velocity`, so the easing continues the motion smoothly
143
+ * instead of restarting. Use it to regenerate an animation when its target
144
+ * changes — the interruption carries momentum instead of stalling. The
145
+ * from-rest shape trick doesn't apply here (the curve now depends on the
146
+ * velocity), so this is computed per interruption, not cached per config.
147
+ *
148
+ * Always a settling easing; `position` must be nonzero.
149
+ */
150
+ function springFromState(config, state, options = {}) {
151
+ const maxError = options.maxError ?? .0025;
152
+ const maxDuration = options.maxDuration ?? 1e4;
153
+ invariant(Math.abs(state.position) > 1e-9, "springFromState needs a nonzero position (the value and target must differ)");
154
+ const settleTime = config.computeTimeRemaining(state);
155
+ return buildSettle(config, state.position, Math.min(settleTime, maxDuration), maxError, state.velocity);
156
+ }
157
+ /**
158
+ * Evolves a spring `state` forward by `elapsedMs`, returning the new
159
+ * displacement and velocity. Pair it with a running `element.animate` — read
160
+ * the animation's `currentTime`, evolve the state it started from, and you
161
+ * have the exact live velocity the Web Animations API never exposes, ready
162
+ * to hand to `springFromState` on interruption.
163
+ */
164
+ function springStateAt(config, state, elapsedMs) {
165
+ if (elapsedMs <= 0) return {
166
+ position: state.position,
167
+ velocity: state.velocity
168
+ };
169
+ const live = new State(config, state.position, state.velocity);
170
+ pickSolver(config, live).tick(elapsedMs / 1e3);
171
+ return {
172
+ position: live.position,
173
+ velocity: live.velocity
174
+ };
175
+ }
176
+ function resolveSpecs(config, specs, options) {
177
+ const withVelocity = specs.some((spec) => (spec.velocity ?? 0) !== 0);
178
+ invariant(!withVelocity || specs.length === 1, "velocity handoff is single-property: a from-state easing depends on that property, so run one animation per property");
179
+ const first = specs[0];
180
+ const easing = withVelocity ? springFromState(config, {
181
+ position: first.from - first.to,
182
+ velocity: first.velocity
183
+ }, options) : springToLinear(config, {
184
+ ...options,
185
+ displacement: options.displacement ?? (Math.max(...specs.map((s) => Math.abs(s.to - s.from))) || 1)
186
+ });
187
+ const from = {};
188
+ const to = {};
189
+ for (const spec of specs) {
190
+ const format = spec.format ?? ((value) => `${Number(value.toFixed(3))}${spec.unit ?? ""}`);
191
+ const far = easing.mode === "loop" ? 2 * spec.to - spec.from : spec.to;
192
+ const start = format(spec.from);
193
+ const end = format(far);
194
+ from[spec.property] = from[spec.property] ? `${from[spec.property]} ${start}` : start;
195
+ to[spec.property] = to[spec.property] ? `${to[spec.property]} ${end}` : end;
196
+ }
197
+ return {
198
+ easing,
199
+ from,
200
+ to
201
+ };
202
+ }
203
+ function asArray(spec) {
204
+ return Array.isArray(spec) ? spec : [spec];
205
+ }
206
+ /**
207
+ * Builds arguments for `element.animate(keyframes, options)`: two keyframes
208
+ * carrying the endpoint values, and options carrying the spring's `linear()`
209
+ * easing and duration. Pass an array of specs to drive several properties
210
+ * from one spring. A settling spring fills forwards; an undamped one repeats
211
+ * infinitely in alternating directions.
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * const { keyframes, options } = springToWaapi(config, {
216
+ * property: 'translate',
217
+ * from: 0,
218
+ * to: 300,
219
+ * unit: 'px',
220
+ * })
221
+ * element.animate(keyframes, options)
222
+ * ```
223
+ */
224
+ function springToWaapi(config, spec, options = {}) {
225
+ const { easing, from, to } = resolveSpecs(config, asArray(spec), options);
226
+ const keyframes = [from, to];
227
+ const shared = {
228
+ duration: easing.duration,
229
+ easing: easing.easing
230
+ };
231
+ return {
232
+ keyframes,
233
+ options: easing.mode === "loop" ? {
234
+ ...shared,
235
+ iterations: Number.POSITIVE_INFINITY,
236
+ direction: "alternate"
237
+ } : {
238
+ ...shared,
239
+ fill: "forwards"
240
+ }
241
+ };
242
+ }
243
+ /**
244
+ * Builds CSS text for the animation: a `@keyframes` rule and an `animation`
245
+ * shorthand value, driving one property or several from one spring. The
246
+ * `name` option (default `'coily'`) names the `@keyframes` rule and is
247
+ * referenced by the `animation` value.
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * const { keyframes, animation } = springToCss(config, {
252
+ * property: 'translate',
253
+ * from: 0,
254
+ * to: 300,
255
+ * unit: 'px',
256
+ * })
257
+ * const style = document.createElement('style')
258
+ * style.textContent = keyframes
259
+ * document.head.append(style)
260
+ * element.style.animation = animation
261
+ * ```
262
+ */
263
+ function springToCss(config, spec, options = {}) {
264
+ const { easing, from, to } = resolveSpecs(config, asArray(spec), options);
265
+ const name = options.name ?? "coily";
266
+ const ms = `${Number(easing.duration.toFixed(1))}ms`;
267
+ const declare = (values) => Object.entries(values).map(([property, value]) => `${property}: ${value};`).join(" ");
268
+ return {
269
+ keyframes: `@keyframes ${name} {\n from { ${declare(from)} }\n to { ${declare(to)} }\n}`,
270
+ animation: easing.mode === "loop" ? `${ms} ${easing.easing} infinite alternate ${name}` : `${ms} ${easing.easing} forwards ${name}`
271
+ };
272
+ }
273
+ /**
274
+ * Builds a CSS `transition` value that springs a property change: set it on
275
+ * the element, then change the property (on `:hover`, a toggled class, a
276
+ * state attribute) to animate. Pass several specs for one `transition`
277
+ * across properties.
278
+ *
279
+ * Only settling springs — a transition can't loop. It also can't carry
280
+ * momentum: a change interrupted mid-flight restarts the spring from rest
281
+ * instead of reversing through its current velocity, so it stalls at the
282
+ * turn. For motion that's reversed often, drive a live `Spring` instead. The
283
+ * specs' `from`/`to` only tune the duration; the actual values live in your
284
+ * CSS rules.
285
+ */
286
+ function springToTransition(config, spec, options = {}) {
287
+ const specs = asArray(spec);
288
+ const { easing } = resolveSpecs(config, specs, options);
289
+ invariant(easing.mode === "settle", "springToTransition requires a settling spring; an undamped (looping) config has no single end state to transition to");
290
+ const ms = `${Number(easing.duration.toFixed(1))}ms`;
291
+ return specs.map((s) => `${s.property} ${ms} ${easing.easing}`).join(", ");
292
+ }
293
+ //#endregion
294
+ export { springFromState, springStateAt, springToCss, springToLinear, springToTransition, springToWaapi };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import { _ as SpringOptions, a as ConfigShape, c as Shape, d as SpringObjectWithOffset, f as Spring, g as SpringConfig, h as SpringWithOffset, i as TickerOptions, l as SpringObject, m as SpringTarget, n as SpringSystemOptions, o as PartialShape, p as SpringPosition, r as createSpringSystem, s as ReadonlyShape, t as SpringSystem, u as SpringObjectTarget, v as SpringState, y as defineSpring } from "./system-86fsXJN_.js";
2
- export { type ConfigShape, type PartialShape, type ReadonlyShape, type Shape, type Spring, type SpringConfig, type SpringObject, type SpringObjectTarget, type SpringObjectWithOffset, type SpringOptions, type SpringPosition, type SpringState, type SpringSystem, type SpringSystemOptions, type SpringTarget, type SpringWithOffset, type TickerOptions, createSpringSystem, defineSpring };
1
+ import { i as defineSpring, n as SpringDefinitionOptions, r as SpringState, t as SpringDefinition } from "./config-OOMMRdK8.js";
2
+ import { _ as accelerationOf, a as createSpringSystem, b as SpringSourceSymbol, c as ConfigShape, g as KinematicSource, h as SpringTarget, i as SpringSystemOptions, m as Spring, n as SpringOptions, o as CompositeSpring, p as Purpose, r as SpringSystem, s as CompositeSpringTarget, t as CompositeSpringOptions, u as PurposeShape, v as velocityOf, x as mapSpring, y as SpringSource } from "./system-Du_fXXmO.js";
3
+ export { type CompositeSpring, type CompositeSpringOptions, type CompositeSpringTarget, type ConfigShape, type KinematicSource, type Purpose, type PurposeShape, type Spring, type SpringDefinition, type SpringDefinitionOptions, type SpringOptions, type SpringSource, SpringSourceSymbol, type SpringState, type SpringSystem, type SpringSystemOptions, type SpringTarget, accelerationOf, createSpringSystem, defineSpring, mapSpring, velocityOf };
package/dist/index.js CHANGED
@@ -1,2 +1,91 @@
1
- import { r as defineSpring, t as createSpringSystem } from "./system-Csho09XW.js";
2
- export { createSpringSystem, defineSpring };
1
+ import { a as registerRecipe, i as mapSpring, n as SpringSourceSymbol, r as isSpringSource, s as defineSpring, t as createSpringSystem } from "./system-Br_cPnaN.js";
2
+ import { o as invariant } from "./solver-CVXOTDkZ.js";
3
+ //#region src/kinematic-source.ts
4
+ function isKinematicSource(value) {
5
+ if (!isSpringSource(value)) return false;
6
+ const api = value[SpringSourceSymbol];
7
+ return "velocity" in api && "acceleration" in api;
8
+ }
9
+ const NOT_KINEMATIC = "expected a source in motion (a Spring or CompositeSpring); a value derived with mapSpring has neither velocity nor acceleration";
10
+ /**
11
+ * Derives a source from another's velocity — the rate its value changes,
12
+ * in value units per second. `velocityOf(motion)` is a source that reads
13
+ * `motion`'s current velocity, so speed-driven effects fall out of the
14
+ * same follow and `mapSpring` machinery as everything else: a spring can
15
+ * follow it (`blur.target = velocityOf(motion)`) and a map can shape it
16
+ * (`mapSpring(velocityOf(motion), (v) => 1 + Math.abs(v) * 0.001)` for
17
+ * squash and stretch). A scalar source yields a scalar velocity a spring
18
+ * can follow; a `CompositeSpring` or shape yields a velocity of the same
19
+ * shape, mapped to a scalar the way the composite itself is.
20
+ *
21
+ * The result is a stateless view, not a spring: it holds no
22
+ * subscriptions and needs no disposal, and reads report the source's
23
+ * current velocity on the fly. It updates when the source does and is
24
+ * released with it — followers detach then, keeping their current
25
+ * target, as they would from a disposed spring.
26
+ *
27
+ * Only a source in motion qualifies — a `Spring` or a `CompositeSpring`.
28
+ * A value derived with `mapSpring` has no velocity (its rate of change
29
+ * would need the derivative of the map), so it is rejected at the type
30
+ * level and at runtime; take `velocityOf` of the spring the map reads
31
+ * instead.
32
+ */
33
+ function velocityOf(source) {
34
+ invariant(isKinematicSource(source), NOT_KINEMATIC);
35
+ const api = source[SpringSourceSymbol];
36
+ const velocity = Object.freeze({ [SpringSourceSymbol]: Object.freeze({
37
+ get value() {
38
+ return api.velocity;
39
+ },
40
+ onUpdate: (callback) => api.onUpdate(callback),
41
+ onDispose: (callback) => api.onDispose(callback)
42
+ }) });
43
+ registerRecipe(velocity, {
44
+ sources: [source],
45
+ read: () => api.velocity,
46
+ fns: []
47
+ });
48
+ return velocity;
49
+ }
50
+ /**
51
+ * Derives a source from another's acceleration — the rate its velocity
52
+ * changes, in value units per second squared. `accelerationOf(motion)`
53
+ * spikes when `motion` is pushed hardest and passes through zero at the
54
+ * peak of a swing, so force-driven effects fall out of the same follow
55
+ * and `mapSpring` machinery: a spring can follow it, and a map can turn
56
+ * it into an impact flash or a jelly wobble
57
+ * (`mapSpring(accelerationOf(motion), (a) => Math.min(1, Math.abs(a) * 1e-4))`).
58
+ * A scalar source yields a scalar acceleration; a `CompositeSpring` or
59
+ * shape yields an acceleration of the same shape, mapped to a scalar the
60
+ * way the composite itself is.
61
+ *
62
+ * The result is a stateless view, not a spring: it holds no
63
+ * subscriptions and needs no disposal, and reads report the source's
64
+ * current acceleration on the fly. It updates when the source does and
65
+ * is released with it, followers detaching then as from a disposed
66
+ * spring.
67
+ *
68
+ * Only a source in motion qualifies — a `Spring` or a `CompositeSpring`.
69
+ * A value derived with `mapSpring` has no acceleration, so it is rejected
70
+ * at the type level and at runtime; take `accelerationOf` of the spring
71
+ * the map reads instead.
72
+ */
73
+ function accelerationOf(source) {
74
+ invariant(isKinematicSource(source), NOT_KINEMATIC);
75
+ const api = source[SpringSourceSymbol];
76
+ const acceleration = Object.freeze({ [SpringSourceSymbol]: Object.freeze({
77
+ get value() {
78
+ return api.acceleration;
79
+ },
80
+ onUpdate: (callback) => api.onUpdate(callback),
81
+ onDispose: (callback) => api.onDispose(callback)
82
+ }) });
83
+ registerRecipe(acceleration, {
84
+ sources: [source],
85
+ read: () => api.acceleration,
86
+ fns: []
87
+ });
88
+ return acceleration;
89
+ }
90
+ //#endregion
91
+ export { SpringSourceSymbol, accelerationOf, createSpringSystem, defineSpring, mapSpring, velocityOf };
package/dist/nuxt.d.ts CHANGED
@@ -1,12 +1,58 @@
1
1
  import { NuxtModule } from "@nuxt/schema";
2
2
  //#region src/vue/nuxt.d.ts
3
+ /**
4
+ * Options for the `coily` key in `nuxt.config`, passed to the app-wide
5
+ * spring system the module creates.
6
+ */
3
7
  interface CoilyModuleOptions {
8
+ /**
9
+ * Log active motion counts to the console whenever they change.
10
+ * @default false
11
+ */
4
12
  debug?: boolean;
13
+ /**
14
+ * Frame-rate cap. 0 means uncapped: one tick per displayed frame.
15
+ * Capped ticks land on whole display frames and receive the true
16
+ * elapsed time.
17
+ * @default 0
18
+ */
5
19
  fps?: number;
20
+ /**
21
+ * Frame gap in milliseconds above which the gap is treated as lag and
22
+ * replaced with `adjustedLag`, so springs don't teleport when frames
23
+ * resume. 0 disables lag clamping.
24
+ * @default 500
25
+ */
6
26
  lagThreshold?: number;
27
+ /**
28
+ * The elapsed milliseconds a lagging frame is replaced with. Clamped
29
+ * to at most `lagThreshold`.
30
+ * @default 33
31
+ */
7
32
  adjustedLag?: number;
33
+ /**
34
+ * How far a spring may trail a source it follows within one frame, in
35
+ * the value's own units. Frames whose motion would exceed it split
36
+ * into internal sub-steps; smaller is tighter and costs more solver
37
+ * work.
38
+ * @default 0.1
39
+ */
40
+ couplingTolerance?: number;
41
+ /**
42
+ * When springs snap to their targets instead of animating: `'user'`
43
+ * follows the OS prefers-reduced-motion setting, including live
44
+ * changes; `'always'` and `'never'` force one behavior.
45
+ * @default 'user'
46
+ */
8
47
  reducedMotion?: 'user' | 'always' | 'never';
9
48
  }
49
+ /**
50
+ * The Nuxt module: creates an app-wide spring system (started on the
51
+ * client), auto-imports the composables and helpers `useSpring`,
52
+ * `useSpringSystem`, `useSpringPool`, `defineSpring`, `mapSpring`,
53
+ * `velocityOf`, and `accelerationOf`, and registers the `SpringValue`
54
+ * component. Types are left out — they would be noise in most apps.
55
+ */
10
56
  declare const coilyModule: NuxtModule;
11
57
  //#endregion
12
58
  export { CoilyModuleOptions, coilyModule as default };
package/dist/nuxt.js CHANGED
@@ -6,6 +6,7 @@ function getPluginTemplate(options) {
6
6
  if (options.fps !== void 0) systemOptions.fps = options.fps;
7
7
  if (options.lagThreshold !== void 0) systemOptions.lagThreshold = options.lagThreshold;
8
8
  if (options.adjustedLag !== void 0) systemOptions.adjustedLag = options.adjustedLag;
9
+ if (options.couplingTolerance !== void 0) systemOptions.couplingTolerance = options.couplingTolerance;
9
10
  if (options.reducedMotion) systemOptions.reducedMotion = options.reducedMotion;
10
11
  return `import { createSpringSystem } from 'coily'
11
12
  import { provideSpringSystem } from 'coily/vue'
@@ -21,6 +22,13 @@ export default defineNuxtPlugin((nuxtApp) => {
21
22
  })
22
23
  })`;
23
24
  }
25
+ /**
26
+ * The Nuxt module: creates an app-wide spring system (started on the
27
+ * client), auto-imports the composables and helpers `useSpring`,
28
+ * `useSpringSystem`, `useSpringPool`, `defineSpring`, `mapSpring`,
29
+ * `velocityOf`, and `accelerationOf`, and registers the `SpringValue`
30
+ * component. Types are left out — they would be noise in most apps.
31
+ */
24
32
  const coilyModule = defineNuxtModule({
25
33
  meta: {
26
34
  name: "coily",
@@ -48,28 +56,20 @@ const coilyModule = defineNuxtModule({
48
56
  name: "useSpringPool"
49
57
  },
50
58
  {
51
- from: "coily/vue",
52
- name: "SpringRef",
53
- type: true
54
- },
55
- {
56
- from: "coily/vue",
57
- name: "SpringObjectRef",
58
- type: true
59
+ from: "coily",
60
+ name: "defineSpring"
59
61
  },
60
62
  {
61
- from: "coily/vue",
62
- name: "SpringPool",
63
- type: true
63
+ from: "coily",
64
+ name: "mapSpring"
64
65
  },
65
66
  {
66
67
  from: "coily",
67
- name: "defineSpring"
68
+ name: "velocityOf"
68
69
  },
69
70
  {
70
71
  from: "coily",
71
- name: "SpringConfig",
72
- type: true
72
+ name: "accelerationOf"
73
73
  }
74
74
  ]);
75
75
  addComponent({