pulse-js-framework 1.9.2 → 1.10.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/package.json +28 -1
- package/runtime/animation.js +535 -0
- package/runtime/dom-advanced.js +116 -37
- package/runtime/i18n.js +434 -0
- package/runtime/index.js +20 -0
- package/runtime/logger.js +5 -1
- package/runtime/persistence.js +492 -0
- package/runtime/sse.js +393 -0
- package/runtime/sw.js +250 -0
- package/sw/index.js +240 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -110,6 +110,26 @@
|
|
|
110
110
|
"./server/hono": "./server/hono.js",
|
|
111
111
|
"./server/fastify": "./server/fastify.js",
|
|
112
112
|
"./server/utils": "./server/utils.js",
|
|
113
|
+
"./runtime/sse": {
|
|
114
|
+
"types": "./types/sse.d.ts",
|
|
115
|
+
"default": "./runtime/sse.js"
|
|
116
|
+
},
|
|
117
|
+
"./runtime/persistence": {
|
|
118
|
+
"types": "./types/persistence.d.ts",
|
|
119
|
+
"default": "./runtime/persistence.js"
|
|
120
|
+
},
|
|
121
|
+
"./runtime/animation": {
|
|
122
|
+
"types": "./types/animation.d.ts",
|
|
123
|
+
"default": "./runtime/animation.js"
|
|
124
|
+
},
|
|
125
|
+
"./runtime/i18n": {
|
|
126
|
+
"types": "./types/i18n.d.ts",
|
|
127
|
+
"default": "./runtime/i18n.js"
|
|
128
|
+
},
|
|
129
|
+
"./sw": {
|
|
130
|
+
"types": "./types/sw.d.ts",
|
|
131
|
+
"default": "./sw/index.js"
|
|
132
|
+
},
|
|
113
133
|
"./package.json": "./package.json"
|
|
114
134
|
},
|
|
115
135
|
"files": [
|
|
@@ -121,6 +141,7 @@
|
|
|
121
141
|
"loader/",
|
|
122
142
|
"mobile/",
|
|
123
143
|
"server/",
|
|
144
|
+
"sw/",
|
|
124
145
|
"types/",
|
|
125
146
|
"README.md",
|
|
126
147
|
"LICENSE"
|
|
@@ -198,6 +219,12 @@
|
|
|
198
219
|
"test:interceptor-manager": "node test/interceptor-manager.test.js",
|
|
199
220
|
"test:vite-plugin": "node --test test/vite-plugin.test.js",
|
|
200
221
|
"test:memory-cleanup": "node --test test/memory-cleanup.test.js",
|
|
222
|
+
"test:sse": "node --test test/sse.test.js",
|
|
223
|
+
"test:persistence": "node --test test/persistence.test.js",
|
|
224
|
+
"test:i18n": "node --test test/i18n.test.js",
|
|
225
|
+
"test:portal": "node --test test/portal.test.js",
|
|
226
|
+
"test:animation": "node --test test/animation.test.js",
|
|
227
|
+
"test:sw": "node --test test/sw.test.js",
|
|
201
228
|
"test:dev-server": "node --test test/dev-server.test.js",
|
|
202
229
|
"test:ssr-stream": "node --test test/ssr-stream.test.js",
|
|
203
230
|
"test:ssr-mismatch": "node --test test/ssr-mismatch.test.js",
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Animation Module
|
|
3
|
+
* Web Animations API wrapper with Pulse reactivity and reduced motion support.
|
|
4
|
+
*
|
|
5
|
+
* @module pulse-js-framework/runtime/animation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pulse, computed, effect, onCleanup } from './pulse.js';
|
|
9
|
+
import { loggers } from './logger.js';
|
|
10
|
+
import { getAdapter, MockDOMAdapter } from './dom-adapter.js';
|
|
11
|
+
import { prefersReducedMotion } from './a11y.js';
|
|
12
|
+
|
|
13
|
+
const log = loggers.dom;
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// CONFIGURATION
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
const _config = {
|
|
20
|
+
respectReducedMotion: true,
|
|
21
|
+
defaultDuration: 300,
|
|
22
|
+
defaultEasing: 'ease-out',
|
|
23
|
+
disabled: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configure global animation settings
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
* @param {boolean} [options.respectReducedMotion=true] - Respect prefers-reduced-motion
|
|
31
|
+
* @param {number} [options.defaultDuration=300] - Default animation duration (ms)
|
|
32
|
+
* @param {string} [options.defaultEasing='ease-out'] - Default easing function
|
|
33
|
+
* @param {boolean} [options.disabled=false] - Kill switch to disable all animations
|
|
34
|
+
*/
|
|
35
|
+
export function configureAnimations(options = {}) {
|
|
36
|
+
if (options.respectReducedMotion !== undefined) _config.respectReducedMotion = options.respectReducedMotion;
|
|
37
|
+
if (options.defaultDuration !== undefined) _config.defaultDuration = options.defaultDuration;
|
|
38
|
+
if (options.defaultEasing !== undefined) _config.defaultEasing = options.defaultEasing;
|
|
39
|
+
if (options.disabled !== undefined) _config.disabled = options.disabled;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// INTERNAL HELPERS
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
function _isSSR() {
|
|
47
|
+
const adapter = getAdapter();
|
|
48
|
+
return adapter instanceof MockDOMAdapter;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _shouldAnimate() {
|
|
52
|
+
if (_config.disabled) return false;
|
|
53
|
+
if (_isSSR()) return false;
|
|
54
|
+
if (_config.respectReducedMotion && prefersReducedMotion()) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _getEffectiveDuration(duration) {
|
|
59
|
+
if (!_shouldAnimate()) return 0;
|
|
60
|
+
return duration ?? _config.defaultDuration;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// animate()
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Animate an element using the Web Animations API
|
|
69
|
+
*
|
|
70
|
+
* @param {HTMLElement} element - Element to animate
|
|
71
|
+
* @param {Array<Object>|Object} keyframes - Keyframes (WAAPI format)
|
|
72
|
+
* @param {Object} [options] - Animation options
|
|
73
|
+
* @param {number} [options.duration] - Duration in ms (default: configureAnimations default)
|
|
74
|
+
* @param {string} [options.easing] - Easing function
|
|
75
|
+
* @param {string} [options.fill='none'] - Fill mode
|
|
76
|
+
* @param {number} [options.delay=0] - Delay in ms
|
|
77
|
+
* @param {number} [options.iterations=1] - Number of iterations
|
|
78
|
+
* @param {string} [options.direction='normal'] - Direction
|
|
79
|
+
* @returns {Object} AnimationControl with reactive state
|
|
80
|
+
*/
|
|
81
|
+
export function animate(element, keyframes, options = {}) {
|
|
82
|
+
const isPlaying = pulse(false);
|
|
83
|
+
const progress = pulse(0);
|
|
84
|
+
|
|
85
|
+
// SSR or disabled: return no-op control
|
|
86
|
+
if (!_shouldAnimate() || !element || typeof element.animate !== 'function') {
|
|
87
|
+
return _createNoopControl(isPlaying, progress);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const duration = _getEffectiveDuration(options.duration);
|
|
91
|
+
const animationOptions = {
|
|
92
|
+
duration,
|
|
93
|
+
easing: options.easing ?? _config.defaultEasing,
|
|
94
|
+
fill: options.fill ?? 'none',
|
|
95
|
+
delay: options.delay ?? 0,
|
|
96
|
+
iterations: options.iterations ?? 1,
|
|
97
|
+
direction: options.direction ?? 'normal',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let animation = null;
|
|
101
|
+
let rafId = null;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
animation = element.animate(keyframes, animationOptions);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
log.warn('Animation failed:', e.message);
|
|
107
|
+
return _createNoopControl(isPlaying, progress);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Track playing state
|
|
111
|
+
isPlaying.set(true);
|
|
112
|
+
|
|
113
|
+
function _updateProgress() {
|
|
114
|
+
if (!animation) return;
|
|
115
|
+
const timing = animation.effect?.getComputedTiming?.();
|
|
116
|
+
if (timing) {
|
|
117
|
+
progress.set(typeof timing.progress === 'number' ? timing.progress : 0);
|
|
118
|
+
}
|
|
119
|
+
if (animation.playState === 'running') {
|
|
120
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
125
|
+
|
|
126
|
+
const finishedPromise = animation.finished
|
|
127
|
+
.then(() => {
|
|
128
|
+
isPlaying.set(false);
|
|
129
|
+
progress.set(1);
|
|
130
|
+
})
|
|
131
|
+
.catch(() => {
|
|
132
|
+
// Animation was cancelled
|
|
133
|
+
isPlaying.set(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
animation.addEventListener('cancel', () => {
|
|
137
|
+
isPlaying.set(false);
|
|
138
|
+
if (rafId) {
|
|
139
|
+
cancelAnimationFrame(rafId);
|
|
140
|
+
rafId = null;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function _cleanup() {
|
|
145
|
+
if (rafId) {
|
|
146
|
+
cancelAnimationFrame(rafId);
|
|
147
|
+
rafId = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
isPlaying,
|
|
153
|
+
progress,
|
|
154
|
+
finished: finishedPromise,
|
|
155
|
+
|
|
156
|
+
play() {
|
|
157
|
+
if (animation) {
|
|
158
|
+
animation.play();
|
|
159
|
+
isPlaying.set(true);
|
|
160
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
pause() {
|
|
165
|
+
if (animation) {
|
|
166
|
+
animation.pause();
|
|
167
|
+
isPlaying.set(false);
|
|
168
|
+
_cleanup();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
reverse() {
|
|
173
|
+
if (animation) {
|
|
174
|
+
animation.reverse();
|
|
175
|
+
isPlaying.set(true);
|
|
176
|
+
rafId = requestAnimationFrame(_updateProgress);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
cancel() {
|
|
181
|
+
if (animation) {
|
|
182
|
+
animation.cancel();
|
|
183
|
+
isPlaying.set(false);
|
|
184
|
+
progress.set(0);
|
|
185
|
+
_cleanup();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
finish() {
|
|
190
|
+
if (animation) {
|
|
191
|
+
animation.finish();
|
|
192
|
+
isPlaying.set(false);
|
|
193
|
+
progress.set(1);
|
|
194
|
+
_cleanup();
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
dispose() {
|
|
199
|
+
_cleanup();
|
|
200
|
+
if (animation) {
|
|
201
|
+
try { animation.cancel(); } catch { /* already finished */ }
|
|
202
|
+
}
|
|
203
|
+
isPlaying.set(false);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _createNoopControl(isPlaying, progress) {
|
|
209
|
+
return {
|
|
210
|
+
isPlaying,
|
|
211
|
+
progress,
|
|
212
|
+
finished: Promise.resolve(),
|
|
213
|
+
play() {},
|
|
214
|
+
pause() {},
|
|
215
|
+
reverse() {},
|
|
216
|
+
cancel() {},
|
|
217
|
+
finish() { progress.set(1); },
|
|
218
|
+
dispose() {},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// useTransition()
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Reactive enter/leave transition hook
|
|
228
|
+
*
|
|
229
|
+
* @param {Function} condition - Reactive condition function
|
|
230
|
+
* @param {Object} options - Transition options
|
|
231
|
+
* @param {Object} [options.enter] - Enter keyframes
|
|
232
|
+
* @param {Object} [options.leave] - Leave keyframes
|
|
233
|
+
* @param {number} [options.duration] - Duration in ms
|
|
234
|
+
* @param {string} [options.easing] - Easing function
|
|
235
|
+
* @param {Function} [options.onEnter] - Template factory for entering content
|
|
236
|
+
* @param {Function} [options.onLeave] - Template factory for leaving content (optional)
|
|
237
|
+
* @returns {Object} { nodes, isEntering, isLeaving }
|
|
238
|
+
*/
|
|
239
|
+
export function useTransition(condition, options = {}) {
|
|
240
|
+
const dom = getAdapter();
|
|
241
|
+
|
|
242
|
+
const {
|
|
243
|
+
enter = { opacity: [0, 1] },
|
|
244
|
+
leave = { opacity: [1, 0] },
|
|
245
|
+
duration,
|
|
246
|
+
easing,
|
|
247
|
+
onEnter = null,
|
|
248
|
+
onLeave = null,
|
|
249
|
+
} = options;
|
|
250
|
+
|
|
251
|
+
const isEntering = pulse(false);
|
|
252
|
+
const isLeaving = pulse(false);
|
|
253
|
+
|
|
254
|
+
const container = dom.createDocumentFragment();
|
|
255
|
+
const marker = dom.createComment('transition');
|
|
256
|
+
dom.appendChild(container, marker);
|
|
257
|
+
|
|
258
|
+
let currentNodes = [];
|
|
259
|
+
let activeAnimation = null;
|
|
260
|
+
|
|
261
|
+
function _removeCurrentNodes() {
|
|
262
|
+
for (const node of currentNodes) {
|
|
263
|
+
dom.removeNode(node);
|
|
264
|
+
if (node._pulseUnmount) {
|
|
265
|
+
for (const cb of node._pulseUnmount) cb();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
currentNodes = [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function _insertNodes(nodes) {
|
|
272
|
+
const fragment = dom.createDocumentFragment();
|
|
273
|
+
for (const node of nodes) {
|
|
274
|
+
if (dom.isNode(node)) {
|
|
275
|
+
dom.appendChild(fragment, node);
|
|
276
|
+
currentNodes.push(node);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const markerParent = dom.getParentNode(marker);
|
|
280
|
+
if (markerParent) {
|
|
281
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
effect(() => {
|
|
286
|
+
const show = typeof condition === 'function' ? condition() : condition;
|
|
287
|
+
|
|
288
|
+
if (show) {
|
|
289
|
+
// Enter
|
|
290
|
+
if (activeAnimation) {
|
|
291
|
+
activeAnimation.dispose();
|
|
292
|
+
activeAnimation = null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If we have leave content, animate it out first
|
|
296
|
+
if (currentNodes.length > 0 && _shouldAnimate()) {
|
|
297
|
+
isLeaving.set(true);
|
|
298
|
+
const nodesToRemove = [...currentNodes];
|
|
299
|
+
currentNodes = [];
|
|
300
|
+
|
|
301
|
+
// Animate leave on old nodes
|
|
302
|
+
const leaveAnims = nodesToRemove
|
|
303
|
+
.filter(n => typeof n.animate === 'function')
|
|
304
|
+
.map(n => animate(n, leave, { duration, easing }));
|
|
305
|
+
|
|
306
|
+
if (leaveAnims.length > 0) {
|
|
307
|
+
Promise.all(leaveAnims.map(a => a.finished)).then(() => {
|
|
308
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
309
|
+
isLeaving.set(false);
|
|
310
|
+
_enterContent();
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
} else {
|
|
314
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
315
|
+
isLeaving.set(false);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
_removeCurrentNodes();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_enterContent();
|
|
322
|
+
} else {
|
|
323
|
+
// Leave
|
|
324
|
+
if (activeAnimation) {
|
|
325
|
+
activeAnimation.dispose();
|
|
326
|
+
activeAnimation = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (currentNodes.length > 0) {
|
|
330
|
+
isLeaving.set(true);
|
|
331
|
+
const nodesToRemove = [...currentNodes];
|
|
332
|
+
currentNodes = [];
|
|
333
|
+
|
|
334
|
+
if (_shouldAnimate()) {
|
|
335
|
+
const leaveAnims = nodesToRemove
|
|
336
|
+
.filter(n => typeof n.animate === 'function')
|
|
337
|
+
.map(n => animate(n, leave, { duration, easing }));
|
|
338
|
+
|
|
339
|
+
if (leaveAnims.length > 0) {
|
|
340
|
+
Promise.all(leaveAnims.map(a => a.finished)).then(() => {
|
|
341
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
342
|
+
isLeaving.set(false);
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const n of nodesToRemove) dom.removeNode(n);
|
|
349
|
+
isLeaving.set(false);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
function _enterContent() {
|
|
355
|
+
if (!onEnter) return;
|
|
356
|
+
|
|
357
|
+
const result = onEnter();
|
|
358
|
+
if (!result) return;
|
|
359
|
+
|
|
360
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
361
|
+
_insertNodes(nodes);
|
|
362
|
+
|
|
363
|
+
if (_shouldAnimate()) {
|
|
364
|
+
isEntering.set(true);
|
|
365
|
+
const enterAnims = currentNodes
|
|
366
|
+
.filter(n => typeof n.animate === 'function')
|
|
367
|
+
.map(n => animate(n, enter, { duration, easing }));
|
|
368
|
+
|
|
369
|
+
if (enterAnims.length > 0) {
|
|
370
|
+
Promise.all(enterAnims.map(a => a.finished)).then(() => {
|
|
371
|
+
isEntering.set(false);
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
isEntering.set(false);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
onCleanup(() => {
|
|
380
|
+
if (activeAnimation) activeAnimation.dispose();
|
|
381
|
+
_removeCurrentNodes();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
container,
|
|
386
|
+
isEntering,
|
|
387
|
+
isLeaving,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// =============================================================================
|
|
392
|
+
// useSpring()
|
|
393
|
+
// =============================================================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Spring-based animation using damped harmonic oscillator
|
|
397
|
+
*
|
|
398
|
+
* @param {number|Function} target - Target value (or reactive function)
|
|
399
|
+
* @param {Object} [options]
|
|
400
|
+
* @param {number} [options.stiffness=170] - Spring stiffness
|
|
401
|
+
* @param {number} [options.damping=26] - Damping coefficient
|
|
402
|
+
* @param {number} [options.mass=1] - Mass
|
|
403
|
+
* @param {number} [options.precision=0.01] - Settle precision
|
|
404
|
+
* @returns {Object} { value, isAnimating, set, dispose }
|
|
405
|
+
*/
|
|
406
|
+
export function useSpring(target, options = {}) {
|
|
407
|
+
const {
|
|
408
|
+
stiffness = 170,
|
|
409
|
+
damping = 26,
|
|
410
|
+
mass = 1,
|
|
411
|
+
precision = 0.01,
|
|
412
|
+
} = options;
|
|
413
|
+
|
|
414
|
+
const initial = typeof target === 'function' ? target() : target;
|
|
415
|
+
const value = pulse(initial);
|
|
416
|
+
const isAnimating = pulse(false);
|
|
417
|
+
|
|
418
|
+
let currentValue = initial;
|
|
419
|
+
let velocity = 0;
|
|
420
|
+
let targetValue = initial;
|
|
421
|
+
let rafId = null;
|
|
422
|
+
let lastTime = 0;
|
|
423
|
+
|
|
424
|
+
function _step(time) {
|
|
425
|
+
if (!lastTime) {
|
|
426
|
+
lastTime = time;
|
|
427
|
+
rafId = requestAnimationFrame(_step);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const dt = Math.min((time - lastTime) / 1000, 0.064); // Cap at ~16fps minimum
|
|
432
|
+
lastTime = time;
|
|
433
|
+
|
|
434
|
+
// Damped spring: F = -kx - cv
|
|
435
|
+
const displacement = currentValue - targetValue;
|
|
436
|
+
const springForce = -stiffness * displacement;
|
|
437
|
+
const dampingForce = -damping * velocity;
|
|
438
|
+
const acceleration = (springForce + dampingForce) / mass;
|
|
439
|
+
|
|
440
|
+
velocity += acceleration * dt;
|
|
441
|
+
currentValue += velocity * dt;
|
|
442
|
+
|
|
443
|
+
// Check if settled
|
|
444
|
+
if (Math.abs(velocity) < precision && Math.abs(displacement) < precision) {
|
|
445
|
+
currentValue = targetValue;
|
|
446
|
+
velocity = 0;
|
|
447
|
+
value.set(currentValue);
|
|
448
|
+
isAnimating.set(false);
|
|
449
|
+
rafId = null;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
value.set(currentValue);
|
|
454
|
+
rafId = requestAnimationFrame(_step);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function _startAnimation() {
|
|
458
|
+
if (!_shouldAnimate()) {
|
|
459
|
+
currentValue = targetValue;
|
|
460
|
+
value.set(currentValue);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
isAnimating.set(true);
|
|
465
|
+
lastTime = 0;
|
|
466
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
467
|
+
rafId = requestAnimationFrame(_step);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Watch target changes if reactive
|
|
471
|
+
if (typeof target === 'function') {
|
|
472
|
+
effect(() => {
|
|
473
|
+
const newTarget = target();
|
|
474
|
+
if (newTarget !== targetValue) {
|
|
475
|
+
targetValue = newTarget;
|
|
476
|
+
_startAnimation();
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function set(newTarget) {
|
|
482
|
+
targetValue = newTarget;
|
|
483
|
+
_startAnimation();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function dispose() {
|
|
487
|
+
if (rafId) {
|
|
488
|
+
cancelAnimationFrame(rafId);
|
|
489
|
+
rafId = null;
|
|
490
|
+
}
|
|
491
|
+
isAnimating.set(false);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
onCleanup(dispose);
|
|
495
|
+
|
|
496
|
+
return { value, isAnimating, set, dispose };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// stagger()
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Stagger animation across multiple elements
|
|
505
|
+
*
|
|
506
|
+
* @param {Array<HTMLElement>} elements - Elements to animate
|
|
507
|
+
* @param {Array<Object>|Object} keyframes - Keyframes
|
|
508
|
+
* @param {Object} [options]
|
|
509
|
+
* @param {number} [options.duration] - Duration per element
|
|
510
|
+
* @param {number} [options.staggerDelay=50] - Delay between each element
|
|
511
|
+
* @param {string} [options.easing] - Easing function
|
|
512
|
+
* @returns {Array<Object>} Array of AnimationControl objects
|
|
513
|
+
*/
|
|
514
|
+
export function stagger(elements, keyframes, options = {}) {
|
|
515
|
+
const { staggerDelay = 50, ...animOptions } = options;
|
|
516
|
+
|
|
517
|
+
return elements.map((element, index) => {
|
|
518
|
+
return animate(element, keyframes, {
|
|
519
|
+
...animOptions,
|
|
520
|
+
delay: (animOptions.delay ?? 0) + index * staggerDelay,
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// =============================================================================
|
|
526
|
+
// DEFAULT EXPORT
|
|
527
|
+
// =============================================================================
|
|
528
|
+
|
|
529
|
+
export default {
|
|
530
|
+
animate,
|
|
531
|
+
useTransition,
|
|
532
|
+
useSpring,
|
|
533
|
+
stagger,
|
|
534
|
+
configureAnimations,
|
|
535
|
+
};
|