use-scroll-animate 5.1.0 → 5.3.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/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.3.0] - 2026-10-08
11
+
12
+ ### Added
13
+ - **Page-wide pack** (`use-scroll-animate/components/effects`, `registerPageEffects()`, `PAGE_FX`):
14
+ - Transitions — `curtain`, `iris` (closes on the click point), `pixel-dissolve` (`cols` × `rows`), `blinds` (`slats`). Each covers the viewport, awaits `onCovered()` (swap your route / content there), optionally `hold`s, then reveals; `playEffect()` resolves when the page is visible again.
15
+ - Persistent — `velocity-skew` (skews with scroll speed, eases back), `spotlight` (dims everything but a circle at the pointer), `edge-glow` (lights the viewport edge you scroll toward).
16
+ - Showcase: **Page transitions** and **Velocity skew & edge glow** cards.
17
+
18
+ ### Accessibility
19
+ - Transition layers are `aria-hidden`; under reduced motion every transition becomes a 150 ms cross-fade that still calls `onCovered()`. `velocity-skew`, `spotlight` and `edge-glow` don’t run under reduced motion.
20
+
21
+ ## [5.2.0] - 2026-10-08
22
+
23
+ ### Added
24
+ - **Bounce & physics pack** (`use-scroll-animate/components/effects`, `registerPhysicsEffects()`, `PHYSICS_FX`): `bounce-in` (spring overshoot), `rubber-band`, `elastic-hover` (springy lift, persistent), `drop-bounce` (gravity + restitution), `gravity-text` (per-character drop, keeps `aria-label`), `spring-follow` (element springs toward the pointer), `bell-swing` (damped swing from the top).
25
+ - Physics helpers: `solveSpring({ stiffness, damping, mass, steps })` → progress samples + settle time, `springKeyframes(map, spring)`, `bounceKeyframes(restitution, steps)` — physical motion that still runs on the Web Animations API.
26
+ - Showcase: **Bounce & physics**, **Gravity text** and **Elastic hover & spring follow** cards.
27
+
28
+ ### Accessibility
29
+ - Under reduced motion `bounce-in` / `drop-bounce` become a short fade; `rubber-band`, `bell-swing`, `gravity-text`, `elastic-hover` and `spring-follow` don’t move.
30
+
10
31
  ## [5.1.0] - 2026-10-08
11
32
 
12
33
  ### Added
@@ -229,6 +229,396 @@ const CLICK_FX = [
229
229
  },
230
230
  ];
231
231
 
232
+ /**
233
+ * Sample a damped spring from 0 → 1 and return the progress values plus the
234
+ * time (ms) it takes to settle. Pure, deterministic.
235
+ */
236
+ function solveSpring({ stiffness = 180, damping = 12, mass = 1, steps = 40 } = {}) {
237
+ const dt = 1 / 120;
238
+ let x = 0;
239
+ let v = 0;
240
+ const trace = [];
241
+ let t = 0;
242
+ for (; t < 4; t += dt) {
243
+ const a = (-stiffness * (x - 1) - damping * v) / mass;
244
+ v += a * dt;
245
+ x += v * dt;
246
+ trace.push(x);
247
+ if (t > 0.1 && Math.abs(x - 1) < 0.001 && Math.abs(v) < 0.01)
248
+ break;
249
+ }
250
+ const values = Array.from({ length: steps + 1 }, (_, i) => +trace[Math.min(trace.length - 1, Math.round((i / steps) * (trace.length - 1)))].toFixed(4));
251
+ values[0] = 0;
252
+ values[steps] = 1;
253
+ return { values, duration: Math.round(trace.length * dt * 1000) };
254
+ }
255
+ /** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
256
+ function springKeyframes(map, spring) {
257
+ const { values, duration } = solveSpring(spring);
258
+ return { frames: values.map(map), duration };
259
+ }
260
+ /** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
261
+ function bounceKeyframes(bounce = 0.5, steps = 48) {
262
+ // segment durations scale with sqrt(height); heights scale with bounce^2 per hop
263
+ const hops = [1];
264
+ let h = 1;
265
+ while (hops.length < 6 && h > 0.01) {
266
+ h *= bounce * bounce;
267
+ hops.push(h);
268
+ }
269
+ const segT = hops.map((hh, i) => (i === 0 ? Math.sqrt(hh) : 2 * Math.sqrt(hh)));
270
+ const total = segT.reduce((a, b) => a + b, 0);
271
+ return Array.from({ length: steps + 1 }, (_, i) => {
272
+ let t = (i / steps) * total;
273
+ for (let k = 0; k < hops.length; k++) {
274
+ if (t <= segT[k] || k === hops.length - 1) {
275
+ const tt = Math.min(t, segT[k]);
276
+ if (k === 0)
277
+ return +(1 - (tt / segT[0]) ** 2).toFixed(4);
278
+ const half = segT[k] / 2;
279
+ return +(hops[k] * (1 - ((tt - half) / half) ** 2)).toFixed(4);
280
+ }
281
+ t -= segT[k];
282
+ }
283
+ return 0;
284
+ });
285
+ }
286
+ const fade = (el, ctx) => ctx.animate(el, [{ opacity: 0 }, { opacity: 1 }], { duration: 200 });
287
+ const PHYSICS_FX = [
288
+ {
289
+ name: 'bounce-in',
290
+ kind: 'enter',
291
+ description: 'Springs in from small with an overshoot (spring solver; stiffness / damping options).',
292
+ defaults: { stiffness: 220, damping: 11, from: 0.3 },
293
+ run: (el, o, ctx) => {
294
+ if (ctx.reduced)
295
+ return fade(el, ctx);
296
+ const { frames, duration } = springKeyframes((p) => ({ transform: `scale(${(o.from + (1 - o.from) * p).toFixed(4)})`, opacity: Math.min(1, p * 3) }), o);
297
+ return ctx.animate(el, frames, { duration, easing: 'linear' });
298
+ },
299
+ },
300
+ {
301
+ name: 'rubber-band',
302
+ kind: 'attention',
303
+ description: 'Stretches wide, snaps back thin and settles like a rubber band.',
304
+ defaults: { duration: 900, amount: 0.25 },
305
+ run: (el, o, ctx) => {
306
+ if (ctx.reduced)
307
+ return;
308
+ const a = o.amount;
309
+ return ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: `scale(${1 + a},${1 - a})`, offset: 0.3 }, { transform: `scale(${1 - a},${1 + a})`, offset: 0.4 }, { transform: `scale(${1 + a / 2},${1 - a / 2})`, offset: 0.5 }, { transform: `scale(${1 - a / 5},${1 + a / 5})`, offset: 0.65 }, { transform: `scale(${1 + a / 10},${1 - a / 10})`, offset: 0.75 }, { transform: 'scale(1,1)' }], { duration: o.duration });
310
+ },
311
+ },
312
+ {
313
+ name: 'elastic-hover',
314
+ kind: 'hover',
315
+ description: 'Hover lifts with a springy overshoot; leaving springs back (persistent; trigger="load").',
316
+ defaults: { lift: 6, scale: 1.04, stiffness: 260, damping: 10 },
317
+ run: (el, o, ctx) => {
318
+ if (ctx.reduced)
319
+ return;
320
+ const to = (on) => {
321
+ const from = on ? 0 : 1;
322
+ const { frames, duration } = springKeyframes((p) => {
323
+ const k = from + (on ? p : -p);
324
+ return { transform: `translateY(${(-o.lift * k).toFixed(2)}px) scale(${(1 + (o.scale - 1) * k).toFixed(4)})` };
325
+ }, o);
326
+ ctx.animate(el, frames, { duration, fill: 'forwards', easing: 'linear' });
327
+ };
328
+ const enter = () => to(true);
329
+ const leave = () => to(false);
330
+ el.addEventListener('pointerenter', enter);
331
+ el.addEventListener('pointerleave', leave);
332
+ return () => {
333
+ el.removeEventListener('pointerenter', enter);
334
+ el.removeEventListener('pointerleave', leave);
335
+ };
336
+ },
337
+ },
338
+ {
339
+ name: 'drop-bounce',
340
+ kind: 'enter',
341
+ description: 'Falls in under gravity and bounces to rest (`height`, `bounce` restitution 0–0.9).',
342
+ defaults: { height: 120, bounce: 0.5, duration: 1100 },
343
+ run: (el, o, ctx) => {
344
+ if (ctx.reduced)
345
+ return fade(el, ctx);
346
+ const hs = bounceKeyframes(Math.min(0.9, Math.max(0, o.bounce)));
347
+ return ctx.animate(el, hs.map((h, i) => ({ transform: `translateY(${(-h * o.height).toFixed(1)}px)`, opacity: i === 0 ? 0 : 1 })), { duration: o.duration, easing: 'linear' });
348
+ },
349
+ },
350
+ {
351
+ name: 'gravity-text',
352
+ kind: 'text',
353
+ description: 'Each character drops in under gravity with a bounce, staggered (splits the text into aria-hidden spans; the label stays readable).',
354
+ defaults: { height: 60, bounce: 0.45, duration: 900, stagger: 45 },
355
+ run: (el, o, ctx) => {
356
+ if (ctx.reduced)
357
+ return;
358
+ if (!el.dataset.usaSplit) {
359
+ const text = el.textContent || '';
360
+ el.setAttribute('aria-label', text);
361
+ el.textContent = '';
362
+ for (const ch of Array.from(text)) {
363
+ const s = document.createElement('span');
364
+ s.setAttribute('aria-hidden', 'true');
365
+ s.style.display = 'inline-block';
366
+ s.style.whiteSpace = 'pre';
367
+ s.textContent = ch;
368
+ el.appendChild(s);
369
+ }
370
+ el.dataset.usaSplit = '1';
371
+ }
372
+ const hs = bounceKeyframes(o.bounce, 32);
373
+ return all(Array.from(el.children).map((s, i) => ctx.animate(s, hs.map((h) => ({ transform: `translateY(${(-h * o.height).toFixed(1)}px)` })), { duration: o.duration, delay: i * o.stagger, easing: 'linear', fill: 'backwards' })));
374
+ },
375
+ },
376
+ {
377
+ name: 'spring-follow',
378
+ kind: 'cursor',
379
+ description: 'The element springs toward the pointer while it moves over its parent, and back home on leave (persistent).',
380
+ defaults: { stiffness: 0.12, damping: 0.75, range: 40 },
381
+ run: (el, o, ctx) => {
382
+ if (ctx.reduced)
383
+ return;
384
+ const host = el.parentElement || el;
385
+ let tx = 0;
386
+ let ty = 0;
387
+ let x = 0;
388
+ let y = 0;
389
+ let vx = 0;
390
+ let vy = 0;
391
+ let raf = 0;
392
+ const tick = () => {
393
+ vx = (vx + (tx - x) * o.stiffness) * o.damping;
394
+ vy = (vy + (ty - y) * o.stiffness) * o.damping;
395
+ x += vx;
396
+ y += vy;
397
+ el.style.translate = `${x.toFixed(2)}px ${y.toFixed(2)}px`;
398
+ raf = Math.abs(vx) + Math.abs(vy) + Math.abs(tx - x) + Math.abs(ty - y) > 0.05 ? requestAnimationFrame(tick) : 0;
399
+ };
400
+ const kick = () => {
401
+ if (!raf)
402
+ raf = requestAnimationFrame(tick);
403
+ };
404
+ const move = (e) => {
405
+ const r = host.getBoundingClientRect();
406
+ const nx = (e.clientX - r.left) / (r.width || 1) - 0.5;
407
+ const ny = (e.clientY - r.top) / (r.height || 1) - 0.5;
408
+ tx = nx * 2 * o.range;
409
+ ty = ny * 2 * o.range;
410
+ kick();
411
+ };
412
+ const leave = () => {
413
+ tx = ty = 0;
414
+ kick();
415
+ };
416
+ host.addEventListener('pointermove', move);
417
+ host.addEventListener('pointerleave', leave);
418
+ return () => {
419
+ host.removeEventListener('pointermove', move);
420
+ host.removeEventListener('pointerleave', leave);
421
+ cancelAnimationFrame(raf);
422
+ el.style.translate = '';
423
+ };
424
+ },
425
+ },
426
+ {
427
+ name: 'bell-swing',
428
+ kind: 'attention',
429
+ description: 'Swings from its top like a ringing bell with damped oscillation.',
430
+ defaults: { angle: 22, stiffness: 120, damping: 4 },
431
+ run: (el, o, ctx) => {
432
+ if (ctx.reduced)
433
+ return;
434
+ el.style.transformOrigin = 'top center';
435
+ const { frames, duration } = springKeyframes((p) => ({ transform: `rotate(${(o.angle * (1 - p)).toFixed(2)}deg)` }), { stiffness: o.stiffness, damping: o.damping, steps: 48 });
436
+ frames[0] = { transform: 'rotate(0deg)' };
437
+ return ctx.animate(el, frames, { duration: Math.min(duration, 2400), easing: 'linear' });
438
+ },
439
+ },
440
+ ];
441
+
442
+ /** A full-viewport, aria-hidden, pointer-blocking (while covering) transition layer. */
443
+ function screen(css = '') {
444
+ const s = document.createElement('div');
445
+ s.setAttribute('aria-hidden', 'true');
446
+ s.setAttribute('data-usa-page-fx', '');
447
+ s.style.cssText = `position:fixed;inset:0;z-index:2147483001;pointer-events:auto;${css}`;
448
+ document.body.appendChild(s);
449
+ return s;
450
+ }
451
+ const wait = (a) => (a ? a.finished.catch(() => undefined) : Promise.resolve());
452
+ const call = async (fn) => {
453
+ if (typeof fn === 'function')
454
+ await fn();
455
+ };
456
+ /** Reduced-motion version of every transition: a quick cross-fade through the cover colour. */
457
+ async function crossFade(o, ctx) {
458
+ const s = screen(`background:${o.color};opacity:0`);
459
+ await wait(ctx.animate(s, [{ opacity: 0 }, { opacity: 1 }], { duration: 150, fill: 'forwards' }));
460
+ await call(o.onCovered);
461
+ await wait(ctx.animate(s, [{ opacity: 1 }, { opacity: 0 }], { duration: 150, fill: 'forwards' }));
462
+ s.remove();
463
+ }
464
+ /** Build a transition from a cover layer and cover / reveal animations of its parts. */
465
+ function transition(build) {
466
+ return async (el, o, ctx) => {
467
+ if (ctx.reduced)
468
+ return crossFade(o, ctx);
469
+ const s = screen('pointer-events:auto');
470
+ const { parts, cover, delay } = build(s, o, ctx, el);
471
+ const frames = (i) => (typeof cover === 'function' ? cover(i) : cover);
472
+ const half = o.duration / 2;
473
+ await Promise.all(parts.map((p, i) => wait(ctx.animate(p, frames(i), { duration: half, delay: delay?.(i) ?? 0, easing: 'cubic-bezier(0.65, 0, 0.35, 1)', fill: 'both' }))));
474
+ await call(o.onCovered);
475
+ if (o.hold)
476
+ await new Promise((r) => setTimeout(r, o.hold));
477
+ await Promise.all(parts.map((p, i) => wait(ctx.animate(p, [...frames(i)].reverse(), { duration: half, delay: delay?.(i) ?? 0, easing: 'cubic-bezier(0.65, 0, 0.35, 1)', fill: 'both' }))));
478
+ s.remove();
479
+ };
480
+ }
481
+ const part = (s, css) => {
482
+ const p = document.createElement('div');
483
+ p.style.cssText = `position:absolute;${css}`;
484
+ s.appendChild(p);
485
+ return p;
486
+ };
487
+ const TRANSITION_DEFAULTS = { color: '#0f0f1a', duration: 1000, hold: 0, onCovered: undefined };
488
+ const PAGE_FX = [
489
+ {
490
+ name: 'curtain',
491
+ kind: 'page',
492
+ description: 'Two curtain panels close from the sides, call onCovered(), then open.',
493
+ reduced: 'run',
494
+ defaults: TRANSITION_DEFAULTS,
495
+ run: transition((s, o) => ({
496
+ parts: [part(s, `top:0;bottom:0;left:0;width:50.5%;background:${o.color}`), part(s, `top:0;bottom:0;right:0;width:50.5%;background:${o.color}`)],
497
+ cover: (i) => [{ transform: `translateX(${i ? 100 : -100}%)` }, { transform: 'translateX(0)' }],
498
+ })),
499
+ },
500
+ {
501
+ name: 'iris',
502
+ kind: 'page',
503
+ description: 'A circle closes on the click point (or the element’s center), calls onCovered(), then opens.',
504
+ reduced: 'run',
505
+ defaults: TRANSITION_DEFAULTS,
506
+ run: transition((s, o, ctx, el) => {
507
+ const { x, y } = origin(el, ctx);
508
+ const r = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)) + 2;
509
+ // a huge box-shadow ring around a hole: the hole shrinks to 0 to cover
510
+ const p = part(s, `left:${x}px;top:${y}px;width:0;height:0;border-radius:50%;box-shadow:0 0 0 ${Math.ceil(r)}px ${o.color}`);
511
+ s.style.background = 'transparent';
512
+ return { parts: [p], cover: [{ width: `${r * 2}px`, height: `${r * 2}px`, margin: `${-r}px 0 0 ${-r}px` }, { width: '0px', height: '0px', margin: '0px 0 0 0px' }] };
513
+ }),
514
+ },
515
+ {
516
+ name: 'pixel-dissolve',
517
+ kind: 'page',
518
+ description: 'The screen fills with pixels in random order, calls onCovered(), then dissolves (`cols` × `rows`).',
519
+ reduced: 'run',
520
+ defaults: { ...TRANSITION_DEFAULTS, cols: 16, rows: 10 },
521
+ run: transition((s, o) => {
522
+ const n = o.cols * o.rows;
523
+ const order = Array.from({ length: n }, (_, i) => i).sort(() => Math.random() - 0.5);
524
+ const parts = Array.from({ length: n }, (_, i) => part(s, `left:${((i % o.cols) * 100) / o.cols}%;top:${(Math.floor(i / o.cols) * 100) / o.rows}%;width:${100 / o.cols + 0.2}%;height:${100 / o.rows + 0.2}%;background:${o.color};opacity:0`));
525
+ return { parts, cover: [{ opacity: 0 }, { opacity: 1 }], delay: (i) => (order[i] / n) * (o.duration / 2) * 0.6 };
526
+ }),
527
+ },
528
+ {
529
+ name: 'blinds',
530
+ kind: 'page',
531
+ description: 'Horizontal slats rotate shut like venetian blinds, call onCovered(), then open (`slats`).',
532
+ reduced: 'run',
533
+ defaults: { ...TRANSITION_DEFAULTS, slats: 8 },
534
+ run: transition((s, o) => ({
535
+ parts: Array.from({ length: o.slats }, (_, i) => part(s, `left:0;right:0;top:${(i * 100) / o.slats}%;height:${100 / o.slats + 0.3}%;background:${o.color};transform-origin:top`)),
536
+ cover: [{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }],
537
+ delay: (i) => i * 30,
538
+ })),
539
+ },
540
+ {
541
+ name: 'velocity-skew',
542
+ kind: 'scroll',
543
+ description: 'Skews the element with scroll velocity and eases back when scrolling stops (persistent).',
544
+ defaults: { max: 8, factor: 0.25 },
545
+ run: (el, o, ctx) => {
546
+ if (ctx.reduced)
547
+ return;
548
+ let last = scrollY;
549
+ let skew = 0;
550
+ let raf = 0;
551
+ const tick = () => {
552
+ const y = scrollY;
553
+ const target = Math.max(-o.max, Math.min(o.max, (y - last) * o.factor));
554
+ last = y;
555
+ skew += (target - skew) * 0.2;
556
+ el.style.transform = `skewY(${skew.toFixed(3)}deg)`;
557
+ raf = Math.abs(skew) > 0.01 || Math.abs(target) > 0.01 ? requestAnimationFrame(tick) : 0;
558
+ if (!raf)
559
+ el.style.transform = '';
560
+ };
561
+ const onScroll = () => {
562
+ if (!raf)
563
+ raf = requestAnimationFrame(tick);
564
+ };
565
+ addEventListener('scroll', onScroll, { passive: true });
566
+ return () => {
567
+ removeEventListener('scroll', onScroll);
568
+ cancelAnimationFrame(raf);
569
+ el.style.transform = '';
570
+ };
571
+ },
572
+ },
573
+ {
574
+ name: 'spotlight',
575
+ kind: 'cursor',
576
+ description: 'Dims the page except a soft circle that follows the pointer (persistent; `radius`, `dim`).',
577
+ defaults: { radius: 180, dim: 0.72 },
578
+ run: (_el, o, ctx) => {
579
+ if (ctx.reduced)
580
+ return;
581
+ const s = screen(`pointer-events:none;background:radial-gradient(circle ${o.radius}px at var(--x,50%) var(--y,50%),transparent 0,transparent 60%,rgba(0,0,0,${o.dim}) 100%)`);
582
+ const move = (e) => {
583
+ s.style.setProperty('--x', `${e.clientX}px`);
584
+ s.style.setProperty('--y', `${e.clientY}px`);
585
+ };
586
+ addEventListener('pointermove', move, { passive: true });
587
+ return () => {
588
+ removeEventListener('pointermove', move);
589
+ s.remove();
590
+ };
591
+ },
592
+ },
593
+ {
594
+ name: 'edge-glow',
595
+ kind: 'scroll',
596
+ description: 'A soft glow lights the top / bottom edge of the viewport while scrolling in that direction (persistent).',
597
+ defaults: { color: '#7c5cff', size: 90 },
598
+ run: (_el, o, ctx) => {
599
+ if (ctx.reduced)
600
+ return;
601
+ const s = screen('pointer-events:none;opacity:0;transition:opacity .35s ease-out');
602
+ let last = scrollY;
603
+ let t = 0;
604
+ const onScroll = () => {
605
+ const down = scrollY >= last;
606
+ last = scrollY;
607
+ s.style.background = `linear-gradient(${down ? 'to top' : 'to bottom'},${o.color}55,transparent ${o.size}px)`;
608
+ s.style.opacity = '1';
609
+ clearTimeout(t);
610
+ t = setTimeout(() => (s.style.opacity = '0'), 160);
611
+ };
612
+ addEventListener('scroll', onScroll, { passive: true });
613
+ return () => {
614
+ removeEventListener('scroll', onScroll);
615
+ clearTimeout(t);
616
+ s.remove();
617
+ };
618
+ },
619
+ },
620
+ ];
621
+
232
622
  /**
233
623
  * use-scroll-animate/components/effects — the 5.x effect packs, all
234
624
  * registered through `registerEffect()` (5.0) and playable with
@@ -244,11 +634,21 @@ const CLICK_FX = [
244
634
  /** The effect packs by version, in release order. */
245
635
  const EFFECT_PACKS = {
246
636
  'cards-click': [...CARD_FX, ...CLICK_FX],
637
+ physics: PHYSICS_FX,
638
+ page: PAGE_FX,
247
639
  };
248
640
  /** 5.1: card & click effects 2.0. */
249
641
  function registerCardClickEffects() {
250
642
  components_fx.registerEffects(EFFECT_PACKS['cards-click']);
251
643
  }
644
+ /** 5.2: bounce & physics micro-interactions. */
645
+ function registerPhysicsEffects() {
646
+ components_fx.registerEffects(EFFECT_PACKS.physics);
647
+ }
648
+ /** 5.3: page-wide transitions and effects. */
649
+ function registerPageEffects() {
650
+ components_fx.registerEffects(EFFECT_PACKS.page);
651
+ }
252
652
  /** Register the built-ins and every pack (idempotent). */
253
653
  function registerAllEffects() {
254
654
  components_fx.registerBuiltinEffects();
@@ -259,7 +659,14 @@ function registerAllEffects() {
259
659
  exports.CARD_FX = CARD_FX;
260
660
  exports.CLICK_FX = CLICK_FX;
261
661
  exports.EFFECT_PACKS = EFFECT_PACKS;
662
+ exports.PAGE_FX = PAGE_FX;
663
+ exports.PHYSICS_FX = PHYSICS_FX;
664
+ exports.bounceKeyframes = bounceKeyframes;
262
665
  exports.fxLayer = fxLayer;
263
666
  exports.registerAllEffects = registerAllEffects;
264
667
  exports.registerCardClickEffects = registerCardClickEffects;
668
+ exports.registerPageEffects = registerPageEffects;
669
+ exports.registerPhysicsEffects = registerPhysicsEffects;
670
+ exports.solveSpring = solveSpring;
671
+ exports.springKeyframes = springKeyframes;
265
672
  //# sourceMappingURL=effects.cjs.map