@humanspeak/svelte-motion 0.1.2 → 0.1.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/README.md +95 -9
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/utils/animation.d.ts +1 -1
- package/dist/utils/animation.js +1 -1
- package/dist/utils/hover.d.ts +1 -1
- package/dist/utils/spring.d.ts +38 -0
- package/dist/utils/spring.js +157 -0
- package/dist/utils/time.d.ts +14 -0
- package/dist/utils/time.js +68 -0
- package/dist/utils/transform.d.ts +42 -0
- package/dist/utils/transform.js +129 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,15 +74,16 @@ This package carefully selects its dependencies to provide a robust and maintain
|
|
|
74
74
|
|
|
75
75
|
### Examples
|
|
76
76
|
|
|
77
|
-
| Motion | Demo / Route
|
|
78
|
-
| -------------------------------------------------------------------------------------------------------- |
|
|
79
|
-
| [React - Enter Animation](https://examples.motion.dev/react/enter-animation) | `/tests/motion/enter-animation`
|
|
80
|
-
| [HTML Content (0→100 counter)](https://examples.motion.dev/react/html-content) | `/tests/motion/html-content`
|
|
81
|
-
| [Aspect Ratio](https://examples.motion.dev/react/aspect-ratio) | `/tests/motion/aspect-ratio`
|
|
82
|
-
| [Hover + Tap (whileHover + whileTap)](https://motion.dev/docs/react?platform=react#hover-tap-animation) | `/tests/motion/hover-and-tap`
|
|
83
|
-
| [Random - Shiny Button](https://www.youtube.com/watch?v=jcpLprT5F0I) by [@verse\_](https://x.com/verse_) | `/tests/random/shiny-button`
|
|
84
|
-
| [Fancy Like Button](https://github.com/DRlFTER/fancyLikeButton) | `/tests/random/fancy-like-button`
|
|
85
|
-
| [Keyframes (square → circle → square; scale 1→2→1)](https://motion.dev/docs/react-animation#keyframes) | `/tests/motion/keyframes`
|
|
77
|
+
| Motion | Demo / Route | REPL |
|
|
78
|
+
| -------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
79
|
+
| [React - Enter Animation](https://examples.motion.dev/react/enter-animation) | `/tests/motion/enter-animation` | [View Example](https://svelte.dev/playground/7f60c347729f4ea48b1a4590c9dedc02?version=5.38.10) |
|
|
80
|
+
| [HTML Content (0→100 counter)](https://examples.motion.dev/react/html-content) | `/tests/motion/html-content` | [View Example](https://svelte.dev/playground/31cd72df4a3242b4b4589501a25e774f?version=5.38.10) |
|
|
81
|
+
| [Aspect Ratio](https://examples.motion.dev/react/aspect-ratio) | `/tests/motion/aspect-ratio` | [View Example](https://svelte.dev/playground/1bf60e745fae44f5becb4c830fde9b6e?version=5.38.10) |
|
|
82
|
+
| [Hover + Tap (whileHover + whileTap)](https://motion.dev/docs/react?platform=react#hover-tap-animation) | `/tests/motion/hover-and-tap` | [View Example](https://svelte.dev/playground/674c7d58f2c740baa4886b01340a97ea?version=5.38.10) |
|
|
83
|
+
| [Random - Shiny Button](https://www.youtube.com/watch?v=jcpLprT5F0I) by [@verse\_](https://x.com/verse_) | `/tests/random/shiny-button` | [View Example](https://svelte.dev/playground/96f9e0bf624f4396adaf06c519147450?version=5.38.10) |
|
|
84
|
+
| [Fancy Like Button](https://github.com/DRlFTER/fancyLikeButton) | `/tests/random/fancy-like-button` | [View Example](https://svelte.dev/playground/c34b7e53d41c48b0ab1eaf21ca120c6e?version=5.38.10) |
|
|
85
|
+
| [Keyframes (square → circle → square; scale 1→2→1)](https://motion.dev/docs/react-animation#keyframes) | `/tests/motion/keyframes` | [View Example](https://svelte.dev/playground/05595ce0db124c1cbbe4e74fda68d717?version=5.38.10) |
|
|
86
|
+
| [Animated Border Gradient (conic-gradient rotate)](https://www.youtube.com/watch?v=OgQI1-9T6ZA) | `/tests/random/animated-border-gradient` | [View Example](https://svelte.dev/playground/6983a61b4c35441b8aa72a971de01a23?version=5.38.10) |
|
|
86
87
|
|
|
87
88
|
## Interactions
|
|
88
89
|
|
|
@@ -149,6 +150,91 @@ Notes:
|
|
|
149
150
|
- Transform properties like `scale`/`rotate` are composed into a single `transform` style during SSR.
|
|
150
151
|
- When `initial` is empty, the first keyframe from `animate` is used to seed SSR styles.
|
|
151
152
|
|
|
153
|
+
## Utilities
|
|
154
|
+
|
|
155
|
+
### useTime(id?)
|
|
156
|
+
|
|
157
|
+
- Returns a Svelte readable store that updates once per animation frame with elapsed milliseconds since creation.
|
|
158
|
+
- If you pass an `id`, calls with the same id return a shared timeline (kept in sync across components).
|
|
159
|
+
- SSR-safe: Returns a static `0` store when `window` is not available.
|
|
160
|
+
|
|
161
|
+
```svelte
|
|
162
|
+
<script lang="ts">
|
|
163
|
+
import { motion, useTime } from '$lib'
|
|
164
|
+
import { derived } from 'svelte/store'
|
|
165
|
+
|
|
166
|
+
const time = useTime('global') // shared
|
|
167
|
+
const rotate = derived(time, (t) => ((t % 4000) / 4000) * 360)
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
<motion.div style={`rotate: ${$rotate}deg`} />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### useSpring
|
|
174
|
+
|
|
175
|
+
`useSpring` creates a readable store that animates to its latest target with a spring. You can either control it directly with `set`/`jump`, or have it follow another readable (like a time-derived value).
|
|
176
|
+
|
|
177
|
+
```svelte
|
|
178
|
+
<script lang="ts">
|
|
179
|
+
import { useTime, useTransform, useSpring } from '$lib'
|
|
180
|
+
|
|
181
|
+
// Track another readable
|
|
182
|
+
const time = useTime()
|
|
183
|
+
const blurTarget = useTransform(() => {
|
|
184
|
+
const phase = ($time % 2000) / 2000
|
|
185
|
+
return 4 * (0.5 + 0.5 * Math.sin(phase * Math.PI * 2)) // 0..4
|
|
186
|
+
}, [time])
|
|
187
|
+
const blur = useSpring(blurTarget, { stiffness: 300 })
|
|
188
|
+
|
|
189
|
+
// Or direct control
|
|
190
|
+
const x = useSpring(0, { stiffness: 300 })
|
|
191
|
+
// x.set(100) // animates to 100
|
|
192
|
+
// x.jump(0) // jumps without animation
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<div style={`filter: blur(${$blur}px)`} />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- Accepts number or unit string (e.g., `"100vh"`) or a readable source.
|
|
199
|
+
- Returns a readable with `{ set, jump }` methods when used in the browser; SSR-safe on the server.
|
|
200
|
+
- Reference: Motion useSpring docs [motion.dev](https://motion.dev/docs/react-use-spring?platform=react).
|
|
201
|
+
|
|
202
|
+
### useTransform
|
|
203
|
+
|
|
204
|
+
`useTransform` creates a derived readable. It supports:
|
|
205
|
+
|
|
206
|
+
- Range mapping: map a numeric source across input/output ranges with optional `{ clamp, ease, mixer }`.
|
|
207
|
+
- Function form: compute from one or more dependencies.
|
|
208
|
+
|
|
209
|
+
Range mapping example:
|
|
210
|
+
|
|
211
|
+
```svelte
|
|
212
|
+
<script lang="ts">
|
|
213
|
+
import { useTime, useTransform } from '$lib'
|
|
214
|
+
const time = useTime()
|
|
215
|
+
// Map 0..4000ms to 0..360deg, unclamped to allow wrap-around
|
|
216
|
+
const rotate = useTransform(time, [0, 4000], [0, 360], { clamp: false })
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<div style={`rotate: ${$rotate}deg`} />
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Function form example:
|
|
223
|
+
|
|
224
|
+
```svelte
|
|
225
|
+
<script lang="ts">
|
|
226
|
+
import { useTransform } from '$lib'
|
|
227
|
+
// Given stores a and b, compute their sum
|
|
228
|
+
const add = (a: number, b: number) => a + b
|
|
229
|
+
// deps are stores; body can access them via $ syntax
|
|
230
|
+
const total = useTransform(() => add($a, $b), [a, b])
|
|
231
|
+
</script>
|
|
232
|
+
|
|
233
|
+
<span>{$total}</span>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- Reference: Motion useTransform docs [motion.dev](https://motion.dev/docs/react-use-transform?platform=react).
|
|
237
|
+
|
|
152
238
|
## Access the underlying element (bind:ref)
|
|
153
239
|
|
|
154
240
|
You can bind a ref to access the underlying DOM element rendered by a motion component:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import MotionConfig from './components/MotionConfig.svelte';
|
|
2
|
-
import type { MotionComponents } from './html/index
|
|
2
|
+
import type { MotionComponents } from './html/index';
|
|
3
3
|
export declare const motion: MotionComponents;
|
|
4
4
|
export { animate, hover } from 'motion';
|
|
5
|
-
export type { MotionAnimate, MotionInitial, MotionTransition, MotionWhileTap } from './types
|
|
5
|
+
export type { MotionAnimate, MotionInitial, MotionTransition, MotionWhileTap } from './types';
|
|
6
|
+
export { useSpring } from './utils/spring';
|
|
7
|
+
export { useTime } from './utils/time';
|
|
8
|
+
export { useTransform } from './utils/transform';
|
|
6
9
|
export { MotionConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import MotionConfig from './components/MotionConfig.svelte';
|
|
2
|
-
import * as html from './html/index
|
|
2
|
+
import * as html from './html/index';
|
|
3
3
|
// Create the motion object with all components
|
|
4
4
|
export const motion = Object.fromEntries(Object.entries(html).map(([key, component]) => [key.toLowerCase(), component]));
|
|
5
5
|
// Export all types
|
|
6
6
|
export { animate, hover } from 'motion';
|
|
7
|
+
export { useSpring } from './utils/spring';
|
|
8
|
+
export { useTime } from './utils/time';
|
|
9
|
+
export { useTransform } from './utils/transform';
|
|
7
10
|
export { MotionConfig };
|
package/dist/utils/animation.js
CHANGED
package/dist/utils/hover.d.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Spring configuration options.
|
|
4
|
+
*
|
|
5
|
+
* This is a minimal subset modeled after Motion's spring transition options.
|
|
6
|
+
* Values are tuned for sensible defaults, not parity.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} SpringOptions
|
|
9
|
+
* @property {number=} stiffness Spring stiffness (higher = snappier). Default 170.
|
|
10
|
+
* @property {number=} damping Spring damping (higher = less oscillation). Default 26.
|
|
11
|
+
* @property {number=} mass Mass of the object. Default 1.
|
|
12
|
+
* @property {number=} restDelta Threshold for absolute position delta to stop. Default 0.01.
|
|
13
|
+
* @property {number=} restSpeed Threshold for velocity magnitude to stop. Default 0.01.
|
|
14
|
+
*/
|
|
15
|
+
export type SpringOptions = {
|
|
16
|
+
stiffness?: number;
|
|
17
|
+
damping?: number;
|
|
18
|
+
mass?: number;
|
|
19
|
+
restDelta?: number;
|
|
20
|
+
restSpeed?: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Creates a spring-animated readable store. The store exposes `set` to
|
|
24
|
+
* animate towards a target, or `jump` to immediately set the value without
|
|
25
|
+
* animation. When constructed with another readable store, the spring
|
|
26
|
+
* automatically follows it.
|
|
27
|
+
*
|
|
28
|
+
* This is SSR-safe: On the server it returns a static store and no timers run.
|
|
29
|
+
*
|
|
30
|
+
* @template T
|
|
31
|
+
* @param {number|string|Readable<number|string>} source Initial value or a source store to follow.
|
|
32
|
+
* @param {SpringOptions=} options Spring configuration.
|
|
33
|
+
* @returns {Readable<number|string> & { set: (v: number|string) => void; jump: (v: number|string) => void; }}
|
|
34
|
+
*/
|
|
35
|
+
export declare const useSpring: (source: number | string | Readable<number | string>, options?: SpringOptions) => Readable<number | string> & {
|
|
36
|
+
set: (v: number | string) => void;
|
|
37
|
+
jump: (v: number | string) => void;
|
|
38
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readable, writable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Parses a number or unit string into numeric value and unit.
|
|
4
|
+
* @param {number|string} v The input value.
|
|
5
|
+
* @returns {UnitValue} Parsed value and unit.
|
|
6
|
+
* @private
|
|
7
|
+
*/
|
|
8
|
+
const parseUnit = (v) => {
|
|
9
|
+
if (typeof v === 'number')
|
|
10
|
+
return { value: v, unit: '' };
|
|
11
|
+
const match = String(v).match(/^(-?\d*\.?\d+)(.*)$/);
|
|
12
|
+
if (!match || !match[1])
|
|
13
|
+
return { value: 0, unit: '' };
|
|
14
|
+
const parsed = Number.parseFloat(match[1]);
|
|
15
|
+
if (!Number.isFinite(parsed))
|
|
16
|
+
return { value: 0, unit: '' };
|
|
17
|
+
const unit = match[2] ?? '';
|
|
18
|
+
return { value: parsed, unit };
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Formats a numeric value with a unit.
|
|
22
|
+
* @param {number} n Numeric value.
|
|
23
|
+
* @param {string} unit Unit suffix.
|
|
24
|
+
* @returns {number|string} Number or string with unit.
|
|
25
|
+
* @private
|
|
26
|
+
*/
|
|
27
|
+
const formatUnit = (n, unit) => (unit ? `${n}${unit}` : n);
|
|
28
|
+
/**
|
|
29
|
+
* Creates a spring-animated readable store. The store exposes `set` to
|
|
30
|
+
* animate towards a target, or `jump` to immediately set the value without
|
|
31
|
+
* animation. When constructed with another readable store, the spring
|
|
32
|
+
* automatically follows it.
|
|
33
|
+
*
|
|
34
|
+
* This is SSR-safe: On the server it returns a static store and no timers run.
|
|
35
|
+
*
|
|
36
|
+
* @template T
|
|
37
|
+
* @param {number|string|Readable<number|string>} source Initial value or a source store to follow.
|
|
38
|
+
* @param {SpringOptions=} options Spring configuration.
|
|
39
|
+
* @returns {Readable<number|string> & { set: (v: number|string) => void; jump: (v: number|string) => void; }}
|
|
40
|
+
*/
|
|
41
|
+
export const useSpring = (source, options = {}) => {
|
|
42
|
+
if (typeof window === 'undefined') {
|
|
43
|
+
// Derive best-effort initial value for SSR to avoid hydration mismatch
|
|
44
|
+
let initial = 0;
|
|
45
|
+
if (typeof source === 'number' || typeof source === 'string') {
|
|
46
|
+
initial = source;
|
|
47
|
+
}
|
|
48
|
+
else if (source && typeof source === 'object') {
|
|
49
|
+
const anySource = source;
|
|
50
|
+
if (typeof anySource.get === 'function') {
|
|
51
|
+
const v = anySource.get();
|
|
52
|
+
if (typeof v === 'number' || typeof v === 'string')
|
|
53
|
+
initial = v;
|
|
54
|
+
}
|
|
55
|
+
else if (typeof anySource.value === 'number' || typeof anySource.value === 'string') {
|
|
56
|
+
initial = anySource.value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const store = readable(initial, () => { });
|
|
60
|
+
store.set = () => { };
|
|
61
|
+
store.jump = () => { };
|
|
62
|
+
return store;
|
|
63
|
+
}
|
|
64
|
+
const { stiffness = 170, damping = 26, mass = 1, restDelta = 0.01, restSpeed = 0.01 } = options;
|
|
65
|
+
const state = {
|
|
66
|
+
current: parseUnit(typeof source === 'object' ? 0 : source),
|
|
67
|
+
target: parseUnit(typeof source === 'object' ? 0 : source)
|
|
68
|
+
};
|
|
69
|
+
const unit = state.current.unit || state.target.unit;
|
|
70
|
+
const store = writable(formatUnit(state.current.value, unit));
|
|
71
|
+
let raf = 0;
|
|
72
|
+
let lastTime = 0;
|
|
73
|
+
let velocity = 0;
|
|
74
|
+
const step = (t) => {
|
|
75
|
+
if (!lastTime)
|
|
76
|
+
lastTime = t;
|
|
77
|
+
// Clamp dt to a safe range to avoid instability across large time gaps
|
|
78
|
+
const dt = Math.min(0.1, Math.max(0.001, (t - lastTime) / 1000));
|
|
79
|
+
lastTime = t;
|
|
80
|
+
const displacement = state.current.value - state.target.value;
|
|
81
|
+
// Spring force based on Hooke's Law: F = -k x; damping force: -c v
|
|
82
|
+
const spring = -stiffness * displacement;
|
|
83
|
+
const damper = -damping * velocity;
|
|
84
|
+
const accel = (spring + damper) / mass;
|
|
85
|
+
velocity += accel * dt;
|
|
86
|
+
state.current.value += velocity * dt;
|
|
87
|
+
const isNoVelocity = Math.abs(velocity) <= restSpeed;
|
|
88
|
+
const isNoDisplacement = Math.abs(state.current.value - state.target.value) <= restDelta;
|
|
89
|
+
const done = isNoVelocity && isNoDisplacement;
|
|
90
|
+
if (done) {
|
|
91
|
+
state.current.value = state.target.value;
|
|
92
|
+
store.set(formatUnit(state.current.value, unit));
|
|
93
|
+
raf = 0;
|
|
94
|
+
lastTime = 0;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
store.set(formatUnit(state.current.value, unit));
|
|
98
|
+
raf = requestAnimationFrame(step);
|
|
99
|
+
};
|
|
100
|
+
const start = () => {
|
|
101
|
+
if (raf)
|
|
102
|
+
return;
|
|
103
|
+
raf = requestAnimationFrame(step);
|
|
104
|
+
};
|
|
105
|
+
const api = {
|
|
106
|
+
set: (v) => {
|
|
107
|
+
state.target = parseUnit(v);
|
|
108
|
+
start();
|
|
109
|
+
},
|
|
110
|
+
jump: (v) => {
|
|
111
|
+
state.current = parseUnit(v);
|
|
112
|
+
state.target = parseUnit(v);
|
|
113
|
+
velocity = 0;
|
|
114
|
+
store.set(formatUnit(state.current.value, state.current.unit || state.target.unit));
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
// If following another store, subscribe and forward values to set()
|
|
118
|
+
if (typeof source === 'object' && 'subscribe' in source) {
|
|
119
|
+
let followSource = true;
|
|
120
|
+
const unsub = source.subscribe((v) => api.set(v));
|
|
121
|
+
const wrapped = readable(formatUnit(state.current.value, unit), (set) => {
|
|
122
|
+
const sub = store.subscribe(set);
|
|
123
|
+
return () => {
|
|
124
|
+
sub();
|
|
125
|
+
unsub();
|
|
126
|
+
followSource = false;
|
|
127
|
+
if (raf)
|
|
128
|
+
cancelAnimationFrame(raf);
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
wrapped.set = (v) => {
|
|
132
|
+
if (followSource)
|
|
133
|
+
unsub();
|
|
134
|
+
followSource = false;
|
|
135
|
+
api.set(v);
|
|
136
|
+
};
|
|
137
|
+
wrapped.jump = (v) => {
|
|
138
|
+
if (followSource)
|
|
139
|
+
unsub();
|
|
140
|
+
followSource = false;
|
|
141
|
+
api.jump(v);
|
|
142
|
+
};
|
|
143
|
+
return wrapped;
|
|
144
|
+
}
|
|
145
|
+
// Standard readable wrapping internal writable
|
|
146
|
+
const wrapped = readable(formatUnit(state.current.value, unit), (set) => {
|
|
147
|
+
const sub = store.subscribe(set);
|
|
148
|
+
return () => {
|
|
149
|
+
sub();
|
|
150
|
+
if (raf)
|
|
151
|
+
cancelAnimationFrame(raf);
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
wrapped.set = api.set;
|
|
155
|
+
wrapped.jump = api.jump;
|
|
156
|
+
return wrapped;
|
|
157
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a time store that ticks once per animation frame.
|
|
4
|
+
*
|
|
5
|
+
* - Without an `id`, returns a fresh timeline per call.
|
|
6
|
+
* - With an `id`, callers sharing the same id receive the same store/timeline,
|
|
7
|
+
* ensuring synchronized reads across components.
|
|
8
|
+
* - SSR-safe: Returns a static 0-valued store when `window` is unavailable.
|
|
9
|
+
*
|
|
10
|
+
* @param {string=} id Optional timeline identifier for sharing across calls.
|
|
11
|
+
* @returns {Readable<number>} A readable store of elapsed milliseconds.
|
|
12
|
+
* @see https://motion.dev/docs/react-use-time?platform=react
|
|
13
|
+
*/
|
|
14
|
+
export declare const useTime: (id?: string) => Readable<number>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readable } from 'svelte/store';
|
|
2
|
+
const SSR_ZERO = readable(0, () => { });
|
|
3
|
+
const sharedStores = new Map();
|
|
4
|
+
// Clear shared timelines on HMR dispose to avoid stale entries across hot reloads
|
|
5
|
+
if (import.meta &&
|
|
6
|
+
import.meta.hot) {
|
|
7
|
+
;
|
|
8
|
+
import.meta.hot.dispose(() => {
|
|
9
|
+
sharedStores.clear();
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new time store that updates once per animation frame.
|
|
14
|
+
*
|
|
15
|
+
* The store value represents elapsed milliseconds since the store was created.
|
|
16
|
+
* In SSR environments (no `window`), a static 0-valued store is returned.
|
|
17
|
+
*
|
|
18
|
+
* @returns {Readable<number>} A readable store of elapsed milliseconds.
|
|
19
|
+
* @see https://motion.dev/docs/react-use-time?platform=react
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
const createTimeStore = () => {
|
|
23
|
+
if (typeof window === 'undefined')
|
|
24
|
+
return SSR_ZERO;
|
|
25
|
+
return readable(0, (set) => {
|
|
26
|
+
const start = performance.now();
|
|
27
|
+
let raf = 0;
|
|
28
|
+
/* c8 ignore start */
|
|
29
|
+
const loop = (t) => {
|
|
30
|
+
set(t - start);
|
|
31
|
+
raf = requestAnimationFrame(loop);
|
|
32
|
+
};
|
|
33
|
+
/* c8 ignore stop */
|
|
34
|
+
raf = requestAnimationFrame(loop);
|
|
35
|
+
return () => cancelAnimationFrame(raf);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Returns a time store that ticks once per animation frame.
|
|
40
|
+
*
|
|
41
|
+
* - Without an `id`, returns a fresh timeline per call.
|
|
42
|
+
* - With an `id`, callers sharing the same id receive the same store/timeline,
|
|
43
|
+
* ensuring synchronized reads across components.
|
|
44
|
+
* - SSR-safe: Returns a static 0-valued store when `window` is unavailable.
|
|
45
|
+
*
|
|
46
|
+
* @param {string=} id Optional timeline identifier for sharing across calls.
|
|
47
|
+
* @returns {Readable<number>} A readable store of elapsed milliseconds.
|
|
48
|
+
* @see https://motion.dev/docs/react-use-time?platform=react
|
|
49
|
+
*/
|
|
50
|
+
export const useTime = (id) => {
|
|
51
|
+
if (!id)
|
|
52
|
+
return createTimeStore();
|
|
53
|
+
if (typeof window === 'undefined')
|
|
54
|
+
return SSR_ZERO;
|
|
55
|
+
const existing = sharedStores.get(id);
|
|
56
|
+
if (existing)
|
|
57
|
+
return existing;
|
|
58
|
+
const base = createTimeStore();
|
|
59
|
+
const store = readable(0, (set) => {
|
|
60
|
+
const unsub = base.subscribe(set);
|
|
61
|
+
return () => {
|
|
62
|
+
unsub();
|
|
63
|
+
sharedStores.delete(id);
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
sharedStores.set(id, store);
|
|
67
|
+
return store;
|
|
68
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Options for range-mapping transform.
|
|
4
|
+
*
|
|
5
|
+
* - clamp: If true, clamps the input to the active segment bounds.
|
|
6
|
+
* - ease: A single easing function or one per segment to shape interpolation.
|
|
7
|
+
* - mixer: Custom mixer factory to interpolate non-numeric outputs.
|
|
8
|
+
*
|
|
9
|
+
* @see https://motion.dev/docs/react-use-transform?platform=react
|
|
10
|
+
*/
|
|
11
|
+
export type TransformOptions = {
|
|
12
|
+
clamp?: boolean;
|
|
13
|
+
ease?: ((t: number) => number) | Array<(t: number) => number>;
|
|
14
|
+
mixer?: (from: unknown, to: unknown) => (t: number) => unknown;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Clamps a numeric value between two bounds, irrespective of their order.
|
|
18
|
+
*
|
|
19
|
+
* @param val Current value.
|
|
20
|
+
* @param a First bound.
|
|
21
|
+
* @param b Second bound.
|
|
22
|
+
* @returns Value clamped to [min(a,b), max(a,b)].
|
|
23
|
+
*/
|
|
24
|
+
export declare const clampBidirectional: (val: number, a: number, b: number) => number;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a derived Svelte store that transforms values.
|
|
27
|
+
*
|
|
28
|
+
* Two supported forms (API parity with Motion's useTransform):
|
|
29
|
+
* - Mapping form: Map a numeric source across input/output ranges.
|
|
30
|
+
* Example: `useTransform(src, [0, 100], [0, 1], { clamp: true })`
|
|
31
|
+
* - Function form: Recompute from a function based on dependency stores.
|
|
32
|
+
* Example: `useTransform(() => compute(), [depA, depB])`
|
|
33
|
+
*
|
|
34
|
+
* @template T
|
|
35
|
+
* @param {Readable<number>|(() => T)} sourceOrCompute Numeric source store (mapping form), or compute function (function form).
|
|
36
|
+
* @param {number[]|Readable<unknown>[]} inputOrDeps Input stops (mapping) or dependency stores (function form).
|
|
37
|
+
* @param {T[]=} output Output stops (mapping form only). Must match input length.
|
|
38
|
+
* @param {TransformOptions=} options Mapping options (mapping form only).
|
|
39
|
+
* @returns {Readable<T>} A derived Svelte readable store.
|
|
40
|
+
* @see https://motion.dev/docs/react-use-transform?platform=react
|
|
41
|
+
*/
|
|
42
|
+
export declare const useTransform: <T = number>(sourceOrCompute: Readable<number> | (() => T), inputOrDeps: number[] | Readable<unknown>[], output?: T[], options?: TransformOptions) => Readable<T>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { derived, readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a linear mixer function for numeric values.
|
|
4
|
+
*
|
|
5
|
+
* @param from Starting numeric value.
|
|
6
|
+
* @param to Ending numeric value.
|
|
7
|
+
* @returns Function that linearly interpolates between from→to for progress t∈[0,1].
|
|
8
|
+
* @private
|
|
9
|
+
*/
|
|
10
|
+
const linearMix = (from, to) => (t) => from + (to - from) * t;
|
|
11
|
+
/**
|
|
12
|
+
* Clamps a numeric value between two bounds, irrespective of their order.
|
|
13
|
+
*
|
|
14
|
+
* @param val Current value.
|
|
15
|
+
* @param a First bound.
|
|
16
|
+
* @param b Second bound.
|
|
17
|
+
* @returns Value clamped to [min(a,b), max(a,b)].
|
|
18
|
+
*/
|
|
19
|
+
export const clampBidirectional = (val, a, b) => {
|
|
20
|
+
const lower = a < b ? a : b;
|
|
21
|
+
const upper = a < b ? b : a;
|
|
22
|
+
return Math.min(Math.max(val, lower), upper);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Finds the segment index i such that x lies between input[i] and input[i+1].
|
|
26
|
+
* Handles both ascending and descending input ranges.
|
|
27
|
+
*
|
|
28
|
+
* @param input Monotonic list of input stops.
|
|
29
|
+
* @param x Current input value.
|
|
30
|
+
* @returns Segment index in range [0, input.length - 2].
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
const findSegment = (input, x) => {
|
|
34
|
+
if (input.length < 2)
|
|
35
|
+
return 0;
|
|
36
|
+
const first = input[0];
|
|
37
|
+
const second = input[1];
|
|
38
|
+
const ascending = second > first;
|
|
39
|
+
if (ascending) {
|
|
40
|
+
if (x <= first)
|
|
41
|
+
return 0;
|
|
42
|
+
for (let i = 1; i < input.length; i++) {
|
|
43
|
+
const curr = input[i];
|
|
44
|
+
if (x <= curr)
|
|
45
|
+
return i - 1;
|
|
46
|
+
}
|
|
47
|
+
return input.length - 2;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (x >= first)
|
|
51
|
+
return 0;
|
|
52
|
+
for (let i = 1; i < input.length; i++) {
|
|
53
|
+
const curr = input[i];
|
|
54
|
+
if (x >= curr)
|
|
55
|
+
return i - 1;
|
|
56
|
+
}
|
|
57
|
+
return input.length - 2;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Creates a derived Svelte store that transforms values.
|
|
62
|
+
*
|
|
63
|
+
* Two supported forms (API parity with Motion's useTransform):
|
|
64
|
+
* - Mapping form: Map a numeric source across input/output ranges.
|
|
65
|
+
* Example: `useTransform(src, [0, 100], [0, 1], { clamp: true })`
|
|
66
|
+
* - Function form: Recompute from a function based on dependency stores.
|
|
67
|
+
* Example: `useTransform(() => compute(), [depA, depB])`
|
|
68
|
+
*
|
|
69
|
+
* @template T
|
|
70
|
+
* @param {Readable<number>|(() => T)} sourceOrCompute Numeric source store (mapping form), or compute function (function form).
|
|
71
|
+
* @param {number[]|Readable<unknown>[]} inputOrDeps Input stops (mapping) or dependency stores (function form).
|
|
72
|
+
* @param {T[]=} output Output stops (mapping form only). Must match input length.
|
|
73
|
+
* @param {TransformOptions=} options Mapping options (mapping form only).
|
|
74
|
+
* @returns {Readable<T>} A derived Svelte readable store.
|
|
75
|
+
* @see https://motion.dev/docs/react-use-transform?platform=react
|
|
76
|
+
*/
|
|
77
|
+
export const useTransform = (sourceOrCompute, inputOrDeps, output, options = {}) => {
|
|
78
|
+
// Function form: (compute, deps)
|
|
79
|
+
if (typeof sourceOrCompute === 'function') {
|
|
80
|
+
const compute = sourceOrCompute;
|
|
81
|
+
const deps = inputOrDeps;
|
|
82
|
+
if (!deps || deps.length === 0)
|
|
83
|
+
return readable(compute());
|
|
84
|
+
return derived(deps, () => compute());
|
|
85
|
+
}
|
|
86
|
+
// Mapping form: (source, input, output, options)
|
|
87
|
+
const source = sourceOrCompute;
|
|
88
|
+
const input = inputOrDeps;
|
|
89
|
+
const out = (output ?? []);
|
|
90
|
+
const { clamp = true, ease, mixer } = options;
|
|
91
|
+
if (input.length !== out.length) {
|
|
92
|
+
throw new Error(`useTransform: input and output arrays must be the same length (input: ${input.length}, output: ${out.length})`);
|
|
93
|
+
}
|
|
94
|
+
const easings = Array.isArray(ease)
|
|
95
|
+
? ease
|
|
96
|
+
: ease
|
|
97
|
+
? new Array(Math.max(0, out.length - 1)).fill(ease)
|
|
98
|
+
: [];
|
|
99
|
+
return derived(source, (x) => {
|
|
100
|
+
if (input.length === 0)
|
|
101
|
+
return out[0];
|
|
102
|
+
if (input.length === 1)
|
|
103
|
+
return out[0];
|
|
104
|
+
const seg = findSegment(input, x);
|
|
105
|
+
const i0 = input[seg];
|
|
106
|
+
const i1 = input[seg + 1];
|
|
107
|
+
const o0 = out[seg];
|
|
108
|
+
const o1 = out[seg + 1];
|
|
109
|
+
// Runtime validation to avoid non-null assertions
|
|
110
|
+
if (i0 === undefined || i1 === undefined || o0 === undefined || o1 === undefined) {
|
|
111
|
+
console.warn('useTransform: Invalid segment bounds', {
|
|
112
|
+
seg,
|
|
113
|
+
inputLength: input.length,
|
|
114
|
+
outputLength: out.length
|
|
115
|
+
});
|
|
116
|
+
return out[0];
|
|
117
|
+
}
|
|
118
|
+
const localClamp = clamp ? clampBidirectional : (val) => val;
|
|
119
|
+
const progress = i0 === i1 ? 0 : (localClamp(x, i0, i1) - i0) / (i1 - i0);
|
|
120
|
+
const e = easings[seg];
|
|
121
|
+
const p = e ? e(progress) : progress;
|
|
122
|
+
const mix = mixer
|
|
123
|
+
? mixer(o0, o1)
|
|
124
|
+
: typeof o0 === 'number' && typeof o1 === 'number'
|
|
125
|
+
? linearMix(o0, o1)
|
|
126
|
+
: (_t) => (p < 0.5 ? o0 : o1);
|
|
127
|
+
return mix(p);
|
|
128
|
+
});
|
|
129
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A lightweight animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|