coily 0.14.0 → 0.16.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.js ADDED
@@ -0,0 +1,318 @@
1
+ import { i as State, n as OverdampedSolver, o as invariant, r as UnderdampedSolver, t as CriticallyDampedSolver } from "./solver-CAXuxdKj.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 spring config into a CSS `linear()` easing and the duration in
123
+ * ms to pair it with. A settling config returns a `'settle'` easing. A
124
+ * pure undamped config (`dampingRatio: 0` with passthrough `arrival`)
125
+ * never settles and returns a `'loop'` easing for an infinite alternating
126
+ * animation. Any other config that never settles, such as
127
+ * `dampingRatio: 0` with `arrival: -1`, returns a `'settle'` easing cut
128
+ * off at `maxDuration`.
129
+ */
130
+ function springToLinear(config, options = {}) {
131
+ const displacement = options.displacement ?? 1;
132
+ const maxError = options.maxError ?? .0025;
133
+ const maxDuration = options.maxDuration ?? 1e4;
134
+ if (config.dampingRatio === 0 && config.arrival === 1) return buildLoop(config, displacement, maxError);
135
+ const settleTime = config.computeTimeRemaining({
136
+ position: displacement,
137
+ velocity: 0
138
+ });
139
+ return buildSettle(config, displacement, Math.min(settleTime, maxDuration), maxError);
140
+ }
141
+ /**
142
+ * Like `springToLinear`, but for a spring caught mid-flight. `state`
143
+ * carries the displacement from the target (`position`) and the current
144
+ * `velocity`, so the easing continues the motion instead of restarting it.
145
+ * Use it to regenerate an animation when its target changes. The curve
146
+ * depends on the velocity, so compute it per interruption instead of
147
+ * caching it per config.
148
+ *
149
+ * The result is always a `'settle'` easing, timed from `position`, so
150
+ * `options.displacement` is ignored. A config that never settles is cut
151
+ * off at `maxDuration`. Throws when `position` is 0 (within 1e-9), even if
152
+ * `velocity` is not.
153
+ */
154
+ function springFromState(config, state, options = {}) {
155
+ const maxError = options.maxError ?? .0025;
156
+ const maxDuration = options.maxDuration ?? 1e4;
157
+ invariant(Math.abs(state.position) > 1e-9, "springFromState needs a nonzero position (the value and target must differ)");
158
+ const settleTime = config.computeTimeRemaining(state);
159
+ return buildSettle(config, state.position, Math.min(settleTime, maxDuration), maxError, state.velocity);
160
+ }
161
+ /**
162
+ * Advances a spring `state` by `elapsedMs` ms and returns the new
163
+ * displacement and velocity. `elapsedMs <= 0` returns a copy of `state`.
164
+ *
165
+ * The Web Animations API doesn't expose velocity, so to interrupt a running
166
+ * `element.animate`, advance the state it started from by its `currentTime`
167
+ * and hand the result to `springFromState`.
168
+ */
169
+ function springStateAt(config, state, elapsedMs) {
170
+ if (elapsedMs <= 0) return {
171
+ position: state.position,
172
+ velocity: state.velocity
173
+ };
174
+ const live = new State(config, state.position, state.velocity);
175
+ pickSolver(config, live).tick(elapsedMs / 1e3);
176
+ return {
177
+ position: live.position,
178
+ velocity: live.velocity
179
+ };
180
+ }
181
+ function resolveSpecs(config, specs, options) {
182
+ const withVelocity = specs.some((spec) => (spec.velocity ?? 0) !== 0);
183
+ invariant(!withVelocity || specs.length === 1, "velocity handoff is single-property: a from-state easing depends on that property, so run one animation per property");
184
+ const first = specs[0];
185
+ const easing = withVelocity ? springFromState(config, {
186
+ position: first.from - first.to,
187
+ velocity: first.velocity
188
+ }, options) : springToLinear(config, {
189
+ ...options,
190
+ displacement: options.displacement ?? (Math.max(...specs.map((s) => Math.abs(s.to - s.from))) || 1)
191
+ });
192
+ const from = {};
193
+ const to = {};
194
+ for (const spec of specs) {
195
+ const format = spec.format ?? ((value) => `${Number(value.toFixed(3))}${spec.unit ?? ""}`);
196
+ const far = easing.mode === "loop" ? 2 * spec.to - spec.from : spec.to;
197
+ const start = format(spec.from);
198
+ const end = format(far);
199
+ from[spec.property] = from[spec.property] ? `${from[spec.property]} ${start}` : start;
200
+ to[spec.property] = to[spec.property] ? `${to[spec.property]} ${end}` : end;
201
+ }
202
+ return {
203
+ easing,
204
+ from,
205
+ to
206
+ };
207
+ }
208
+ function asArray(spec) {
209
+ return Array.isArray(spec) ? spec : [spec];
210
+ }
211
+ /**
212
+ * Builds arguments for `element.animate(keyframes, options)`: two keyframes
213
+ * carrying the endpoint values, and options carrying the spring's `linear()`
214
+ * easing and duration. Pass an array of specs to drive several properties
215
+ * from one spring. A settling spring fills forwards. An undamped one repeats
216
+ * infinitely in alternating directions.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * const { keyframes, options } = springToWaapi(config, {
221
+ * property: 'translate',
222
+ * from: 0,
223
+ * to: 300,
224
+ * unit: 'px',
225
+ * })
226
+ * element.animate(keyframes, options)
227
+ * ```
228
+ */
229
+ function springToWaapi(config, spec, options = {}) {
230
+ const { easing, from, to } = resolveSpecs(config, asArray(spec), options);
231
+ const keyframes = [from, to];
232
+ const shared = {
233
+ duration: easing.duration,
234
+ easing: easing.easing
235
+ };
236
+ return {
237
+ keyframes,
238
+ options: easing.mode === "loop" ? {
239
+ ...shared,
240
+ iterations: Number.POSITIVE_INFINITY,
241
+ direction: "alternate"
242
+ } : {
243
+ ...shared,
244
+ fill: "forwards"
245
+ }
246
+ };
247
+ }
248
+ /**
249
+ * Builds CSS text for the animation: a `@keyframes` rule named by `name`
250
+ * (default `'coily'`) and an `animation` shorthand value that references
251
+ * it, driving one property or several from one spring. A settling spring
252
+ * fills forwards. An undamped one repeats infinitely in alternating
253
+ * directions.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * const { keyframes, animation } = springToCss(config, {
258
+ * property: 'translate',
259
+ * from: 0,
260
+ * to: 300,
261
+ * unit: 'px',
262
+ * })
263
+ * const style = document.createElement('style')
264
+ * style.textContent = keyframes
265
+ * document.head.append(style)
266
+ * element.style.animation = animation
267
+ * ```
268
+ */
269
+ function springToCss(config, spec, options = {}) {
270
+ const { easing, from, to } = resolveSpecs(config, asArray(spec), options);
271
+ const name = options.name ?? "coily";
272
+ const ms = `${Number(easing.duration.toFixed(1))}ms`;
273
+ const declare = (values) => Object.entries(values).map(([property, value]) => `${property}: ${value};`).join(" ");
274
+ return {
275
+ keyframes: `@keyframes ${name} {\n from { ${declare(from)} }\n to { ${declare(to)} }\n}`,
276
+ animation: easing.mode === "loop" ? `${ms} ${easing.easing} infinite alternate ${name}` : `${ms} ${easing.easing} forwards ${name}`
277
+ };
278
+ }
279
+ /**
280
+ * Builds a CSS `transition` value that animates a property change with the
281
+ * spring. Set it on the element, then change the property, for example on
282
+ * `:hover`, with a toggled class, or with a state attribute. Pass several
283
+ * specs to build one `transition` for several properties.
284
+ *
285
+ * The values come from your CSS rules. Each spec's `from` and `to` only
286
+ * size the move, which tunes the duration. An undamped (looping) config
287
+ * throws, because a transition cannot loop.
288
+ *
289
+ * A transition starts from rest unless its spec carries a `velocity`, in
290
+ * the property's units per second, such as a drag's speed at release. The
291
+ * velocity reshapes the curve and the duration, so build the transition at
292
+ * the handoff. A spec with a velocity must be the only spec in the call.
293
+ *
294
+ * ```ts
295
+ * // Release a dragged card back to 0 at the drag's speed.
296
+ * card.style.transition = springToTransition(config, {
297
+ * property: 'translate',
298
+ * from: dragX,
299
+ * to: 0,
300
+ * velocity: releaseVelocity,
301
+ * })
302
+ * card.style.translate = '0px'
303
+ * ```
304
+ *
305
+ * The browser carries no momentum between transitions, so a change that
306
+ * interrupts one mid-flight stalls at the turn. To keep the momentum, read
307
+ * the velocity with `springStateAt` and rebuild the transition with it.
308
+ * For motion that reverses often, drive a live `Spring` instead.
309
+ */
310
+ function springToTransition(config, spec, options = {}) {
311
+ const specs = asArray(spec);
312
+ const { easing } = resolveSpecs(config, specs, options);
313
+ invariant(easing.mode === "settle", "springToTransition requires a settling spring; an undamped (looping) config has no single end state to transition to");
314
+ const ms = `${Number(easing.duration.toFixed(1))}ms`;
315
+ return specs.map((s) => `${s.property} ${ms} ${easing.easing}`).join(", ");
316
+ }
317
+ //#endregion
318
+ export { springFromState, springStateAt, springToCss, springToLinear, springToTransition, springToWaapi };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import { C as SpringDefinitionOptions, S as SpringDefinition, T as defineSpring, _ 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, w as SpringState, x as mapSpring, y as SpringSource } from "./system-Ci-R4RaI.js";
2
- 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 };
1
+ import { i as defineSpring, n as SpringDefinitionOptions, r as SpringState, t as SpringDefinition } from "./config-B8M2xp26.js";
2
+ import { A as SpringSourceSymbol, C as accelerationOf, D as SourceValues, E as SourceShape, O as SpringSource, S as TargetedSourceApi, T as velocityOf, _ as Spring, a as createSpringSystem, b as KinematicSourceApi, c as CompositeSpring, d as PartialShape, f as PurposeShape, g as Purpose, h as TargetShape, i as SpringSystemOptions, j as mapSpring, k as SpringSourceApi, l as CompositeSpringTarget, m as Shape, n as SpringOptions, o as Ramp, p as ReadonlyShape, r as SpringSystem, s as RampOptions, t as CompositeSpringOptions, u as ConfigShape, v as SpringTarget, w as targetOf, x as TargetedSource, y as KinematicSource } from "./system-C7PbI4L0.js";
3
+ export { type CompositeSpring, type CompositeSpringOptions, type CompositeSpringTarget, type ConfigShape, type KinematicSource, type KinematicSourceApi, type PartialShape, type Purpose, type PurposeShape, type Ramp, type RampOptions, type ReadonlyShape, type Shape, type SourceShape, type SourceValues, type Spring, SpringDefinition, type SpringDefinitionOptions, type SpringOptions, type SpringSource, type SpringSourceApi, SpringSourceSymbol, type SpringState, type SpringSystem, type SpringSystemOptions, type SpringTarget, type TargetShape, type TargetedSource, type TargetedSourceApi, accelerationOf, createSpringSystem, defineSpring, mapSpring, targetOf, velocityOf };
package/dist/index.js CHANGED
@@ -1,78 +1,124 @@
1
- import { i as mapSpring, n as SpringSourceSymbol, o as defineSpring, r as isSpringSource, s as invariant, t as createSpringSystem } from "./system-BXxAHM2a.js";
1
+ import { a as SpringSourceSymbol, c as registerRecipe, i as defineSpring, o as isSpringSource, r as SpringDefinition, s as mapSpring, t as createSpringSystem } from "./system-WejqGkfv.js";
2
+ import { o as invariant } from "./solver-CAXuxdKj.js";
2
3
  //#region src/kinematic-source.ts
3
4
  function isKinematicSource(value) {
4
5
  if (!isSpringSource(value)) return false;
5
6
  const api = value[SpringSourceSymbol];
6
7
  return "velocity" in api && "acceleration" in api;
7
8
  }
8
- const NOT_KINEMATIC = "expected a source in motion (a Spring or CompositeSpring); a value derived with mapSpring has neither velocity nor acceleration";
9
+ function deriveView(source, read) {
10
+ const api = source[SpringSourceSymbol];
11
+ const view = Object.freeze({ [SpringSourceSymbol]: Object.freeze({
12
+ get value() {
13
+ return read();
14
+ },
15
+ onUpdate: (callback) => api.onUpdate(callback),
16
+ onDispose: (callback) => api.onDispose(callback)
17
+ }) });
18
+ registerRecipe(view, {
19
+ sources: [source],
20
+ read,
21
+ fns: []
22
+ });
23
+ return view;
24
+ }
25
+ const NOT_KINEMATIC = "expected a source in motion (a Spring, CompositeSpring, or Ramp); a derived source, such as a mapSpring result, has neither velocity nor acceleration";
9
26
  /**
10
- * Derives a source from another's velocity — the rate its value changes,
11
- * in value units per second. `velocityOf(motion)` is a source that reads
12
- * `motion`'s current velocity, so speed-driven effects fall out of the
13
- * same follow and `mapSpring` machinery as everything else: a spring can
14
- * follow it (`blur.target = velocityOf(motion)`) and a map can shape it
27
+ * Derives a source that reads another source's velocity: the rate its
28
+ * value changes, in value units per second. Speed-driven effects then
29
+ * use the same tools as everything else. A spring can follow it
30
+ * (`blur.target = velocityOf(motion)`), and a map can shape it
15
31
  * (`mapSpring(velocityOf(motion), (v) => 1 + Math.abs(v) * 0.001)` for
16
- * squash and stretch). A scalar source yields a scalar velocity a spring
17
- * can follow; a `CompositeSpring` or shape yields a velocity of the same
18
- * shape, mapped to a scalar the way the composite itself is.
32
+ * squash and stretch). A scalar source yields a scalar velocity. A
33
+ * `CompositeSpring` yields a velocity of its own shape, which you map to
34
+ * a scalar as you would its value.
19
35
  *
20
- * The result is a stateless view, not a spring: it holds no
21
- * subscriptions and needs no disposal, and reads report the source's
22
- * current velocity on the fly. It updates when the source does and is
23
- * released with it — followers detach then, keeping their current
24
- * target, as they would from a disposed spring.
36
+ * The result is a stateless view that holds no subscriptions and needs
37
+ * no disposal. Each read reports the source's current velocity. It
38
+ * updates when the source does and is released with it, and followers
39
+ * then detach and keep their current target.
25
40
  *
26
- * Only a source in motion qualifies — a `Spring` or a `CompositeSpring`.
27
- * A value derived with `mapSpring` has no velocity (its rate of change
28
- * would need the derivative of the map), so it is rejected at the type
29
- * level and at runtime; take `velocityOf` of the spring the map reads
30
- * instead.
41
+ * Only a `Spring`, a `CompositeSpring`, a `Ramp`, or a Vue ref that wraps
42
+ * one qualifies. A `mapSpring` result has no velocity, and `velocityOf`
43
+ * rejects it at the type level and at runtime. Take `velocityOf` of the
44
+ * spring the map reads instead.
31
45
  */
32
46
  function velocityOf(source) {
33
47
  invariant(isKinematicSource(source), NOT_KINEMATIC);
34
48
  const api = source[SpringSourceSymbol];
35
- return Object.freeze({ [SpringSourceSymbol]: Object.freeze({
36
- get value() {
37
- return api.velocity;
38
- },
39
- onUpdate: (callback) => api.onUpdate(callback),
40
- onDispose: (callback) => api.onDispose(callback)
41
- }) });
49
+ return deriveView(source, () => api.velocity);
42
50
  }
43
51
  /**
44
- * Derives a source from another's acceleration — the rate its velocity
45
- * changes, in value units per second squared. `accelerationOf(motion)`
46
- * spikes when `motion` is pushed hardest and passes through zero at the
47
- * peak of a swing, so force-driven effects fall out of the same follow
48
- * and `mapSpring` machinery: a spring can follow it, and a map can turn
49
- * it into an impact flash or a jelly wobble
52
+ * Derives a source that reads another source's acceleration: the rate
53
+ * its velocity changes, in value units per second squared. On a spring it
54
+ * is largest just after a retarget and near the far end of each swing,
55
+ * and passes through zero where a swing moves fastest. A spring can
56
+ * follow it, and a map can turn it into an impact flash or a jelly wobble
50
57
  * (`mapSpring(accelerationOf(motion), (a) => Math.min(1, Math.abs(a) * 1e-4))`).
51
- * A scalar source yields a scalar acceleration; a `CompositeSpring` or
52
- * shape yields an acceleration of the same shape, mapped to a scalar the
53
- * way the composite itself is.
58
+ * A scalar source yields a scalar acceleration. A `CompositeSpring`
59
+ * yields an acceleration of its own shape, which you map to a scalar as
60
+ * you would its value. A `Ramp`'s acceleration is always 0.
54
61
  *
55
- * The result is a stateless view, not a spring: it holds no
56
- * subscriptions and needs no disposal, and reads report the source's
57
- * current acceleration on the fly. It updates when the source does and
58
- * is released with it, followers detaching then as from a disposed
59
- * spring.
62
+ * The result is a stateless view that holds no subscriptions and needs
63
+ * no disposal. Each read reports the source's current acceleration. It
64
+ * updates when the source does and is released with it, and followers
65
+ * then detach and keep their current target.
60
66
  *
61
- * Only a source in motion qualifies — a `Spring` or a `CompositeSpring`.
62
- * A value derived with `mapSpring` has no acceleration, so it is rejected
63
- * at the type level and at runtime; take `accelerationOf` of the spring
64
- * the map reads instead.
67
+ * Only a `Spring`, a `CompositeSpring`, a `Ramp`, or a Vue ref that wraps
68
+ * one qualifies. A `mapSpring` result has no acceleration, and
69
+ * `accelerationOf` rejects it at the type level and at runtime. Take
70
+ * `accelerationOf` of the spring the map reads instead.
65
71
  */
66
72
  function accelerationOf(source) {
67
73
  invariant(isKinematicSource(source), NOT_KINEMATIC);
68
74
  const api = source[SpringSourceSymbol];
69
- return Object.freeze({ [SpringSourceSymbol]: Object.freeze({
70
- get value() {
71
- return api.acceleration;
72
- },
73
- onUpdate: (callback) => api.onUpdate(callback),
74
- onDispose: (callback) => api.onDispose(callback)
75
- }) });
75
+ return deriveView(source, () => api.acceleration);
76
+ }
77
+ function isTargetedSource(value) {
78
+ return isKinematicSource(value) && "target" in value[SpringSourceSymbol];
79
+ }
80
+ const NO_TARGET = "expected a source with a target (a Spring, CompositeSpring, or Ramp); a derived source, such as a mapSpring result, has no target";
81
+ /**
82
+ * Derives a source that reads a spring's or ramp's target: where it is
83
+ * heading, not where it is. Pair it with the source in a `mapSpring`
84
+ * shape to measure against the destination, such as remaining travel,
85
+ * progress, or a lead that anticipates the move. A `Spring` or a bounded
86
+ * `Ramp` yields a scalar target that a spring can follow. An endless
87
+ * `Ramp` yields `Infinity` or `-Infinity`, and a spring that follows it
88
+ * throws. A `CompositeSpring` yields a target of its own shape, which you
89
+ * map to a scalar as you would its value.
90
+ *
91
+ * ```ts
92
+ * const system = createSpringSystem()
93
+ * const body = system.createSpring(0)
94
+ * // The eyes look ahead by the body's remaining travel, capped at 20.
95
+ * const lead = mapSpring({ value: body, target: targetOf(body) }, ({ value, target }) =>
96
+ * Math.max(-20, Math.min(20, target - value)),
97
+ * )
98
+ * const eyes = system.createSpring(lead)
99
+ *
100
+ * body.target = 100
101
+ * system.advance(100)
102
+ * eyes.target // 20: the body still has more than 20 to go
103
+ * ```
104
+ *
105
+ * The result is a stateless view that holds no subscriptions and needs
106
+ * no disposal. Each read reports the spring's current target. It updates
107
+ * when the spring does, but assigning `target` emits nothing by itself,
108
+ * so followers pick up a new target on the next frame, when the spring
109
+ * starts toward it. Under reduced motion, a `'motion'` spring jumps to
110
+ * its new target and notifies followers at once. The view is released
111
+ * with the spring, and followers then detach and keep their current
112
+ * target.
113
+ *
114
+ * Only a `Spring`, a `CompositeSpring`, a `Ramp`, or a Vue ref that wraps
115
+ * one has a target. `targetOf` rejects a `mapSpring` result at the type
116
+ * level and at runtime.
117
+ */
118
+ function targetOf(spring) {
119
+ invariant(isTargetedSource(spring), NO_TARGET);
120
+ const api = spring[SpringSourceSymbol];
121
+ return deriveView(spring, () => api.target);
76
122
  }
77
123
  //#endregion
78
- export { SpringSourceSymbol, accelerationOf, createSpringSystem, defineSpring, mapSpring, velocityOf };
124
+ export { SpringDefinition, SpringSourceSymbol, accelerationOf, createSpringSystem, defineSpring, mapSpring, targetOf, velocityOf };
package/dist/nuxt.d.ts CHANGED
@@ -11,39 +11,48 @@ interface CoilyModuleOptions {
11
11
  */
12
12
  debug?: boolean;
13
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.
14
+ * Frame-rate cap. 0 means uncapped: one tick per displayed frame. A
15
+ * capped tick lands on a whole display frame and receives the true
16
+ * elapsed time in ms.
17
17
  * @default 0
18
18
  */
19
19
  fps?: number;
20
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.
21
+ * The frame gap in ms above which the system treats the gap as lag and
22
+ * replaces it with `adjustedLag`, so springs do not jump when frames
23
+ * resume. 0 turns off lag clamping.
24
24
  * @default 500
25
25
  */
26
26
  lagThreshold?: number;
27
27
  /**
28
- * The elapsed milliseconds a lagging frame is replaced with. Clamped
29
- * to at most `lagThreshold`.
28
+ * The elapsed time in ms that replaces a lagging frame. A frame uses
29
+ * at most `lagThreshold` ms of it.
30
30
  * @default 33
31
31
  */
32
32
  adjustedLag?: number;
33
33
  /**
34
- * When springs snap to their targets instead of animating: `'user'`
34
+ * How far a spring can trail a source it follows within one frame, in
35
+ * the value's own units. Frames whose motion would exceed it run as
36
+ * sub-steps, so a smaller value follows more closely at more cost.
37
+ * @default 0.1
38
+ */
39
+ couplingTolerance?: number;
40
+ /**
41
+ * When springs snap to their targets instead of animating. `'user'`
35
42
  * follows the OS prefers-reduced-motion setting, including live
36
- * changes; `'always'` and `'never'` force one behavior.
43
+ * changes. `'always'` and `'never'` force one behavior.
37
44
  * @default 'user'
38
45
  */
39
46
  reducedMotion?: 'user' | 'always' | 'never';
40
47
  }
41
48
  /**
42
- * The Nuxt module: creates an app-wide spring system (started on the
43
- * client), auto-imports the composables and helpers `useSpring`,
44
- * `useSpringSystem`, `useSpringPool`, `defineSpring`, `mapSpring`,
45
- * `velocityOf`, and `accelerationOf`, and registers the `SpringValue`
46
- * component. Types are left out — they would be noise in most apps.
49
+ * The Nuxt module. It creates one spring system for the app and starts
50
+ * it on the client only, so server rendering shows springs at their
51
+ * initial values. The module auto-imports
52
+ * `useSpring`, `useRamp`, `useSpringSystem`, `useSpringPool`,
53
+ * `defineSpring`, `mapSpring`, `velocityOf`, `accelerationOf`, and
54
+ * `targetOf`, and registers the `SpringValue` component. It does not
55
+ * auto-import types.
47
56
  */
48
57
  declare const coilyModule: NuxtModule;
49
58
  //#endregion
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'
@@ -22,11 +23,13 @@ export default defineNuxtPlugin((nuxtApp) => {
22
23
  })`;
23
24
  }
24
25
  /**
25
- * The Nuxt module: creates an app-wide spring system (started on the
26
- * client), auto-imports the composables and helpers `useSpring`,
27
- * `useSpringSystem`, `useSpringPool`, `defineSpring`, `mapSpring`,
28
- * `velocityOf`, and `accelerationOf`, and registers the `SpringValue`
29
- * component. Types are left out — they would be noise in most apps.
26
+ * The Nuxt module. It creates one spring system for the app and starts
27
+ * it on the client only, so server rendering shows springs at their
28
+ * initial values. The module auto-imports
29
+ * `useSpring`, `useRamp`, `useSpringSystem`, `useSpringPool`,
30
+ * `defineSpring`, `mapSpring`, `velocityOf`, `accelerationOf`, and
31
+ * `targetOf`, and registers the `SpringValue` component. It does not
32
+ * auto-import types.
30
33
  */
31
34
  const coilyModule = defineNuxtModule({
32
35
  meta: {
@@ -46,6 +49,10 @@ const coilyModule = defineNuxtModule({
46
49
  from: "coily/vue",
47
50
  name: "useSpring"
48
51
  },
52
+ {
53
+ from: "coily/vue",
54
+ name: "useRamp"
55
+ },
49
56
  {
50
57
  from: "coily/vue",
51
58
  name: "useSpringSystem"
@@ -69,6 +76,10 @@ const coilyModule = defineNuxtModule({
69
76
  {
70
77
  from: "coily",
71
78
  name: "accelerationOf"
79
+ },
80
+ {
81
+ from: "coily",
82
+ name: "targetOf"
72
83
  }
73
84
  ]);
74
85
  addComponent({