use-scroll-animate 5.0.0 → 5.2.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 +24 -0
- package/dist/components/effects.cjs +485 -0
- package/dist/components/effects.cjs.map +1 -0
- package/dist/components/effects.d.cts +102 -0
- package/dist/components/effects.d.ts +102 -0
- package/dist/components/effects.js +473 -0
- package/dist/components/effects.js.map +1 -0
- package/dist/components/fx.cjs +14 -10
- package/dist/components/fx.cjs.map +1 -1
- package/dist/components/fx.js +14 -10
- package/dist/components/fx.js.map +1 -1
- package/dist/components/lite.cjs +14 -10
- package/dist/components/lite.cjs.map +1 -1
- package/dist/components/lite.js +14 -10
- package/dist/components/lite.js.map +1 -1
- package/dist/components.umd.js +2 -2
- package/dist/components.umd.js.map +1 -1
- package/docs/ROADMAP.md +2 -2
- package/docs/components.md +30 -0
- package/package.json +11 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
type MotionSensitivity = 'full' | 'gentle' | 'minimal' | 'static';
|
|
2
|
+
type Cleanup = () => void;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 5.0 — unified plugin-style effect registration. Every effect (built-in or
|
|
6
|
+
* yours) is a plain object registered once and played the same way:
|
|
7
|
+
* `playEffect(el, name)`, `bindEffect(el, name, { trigger })` or
|
|
8
|
+
* `<usa-fx effect="name" trigger="click">`. Effects get a context that
|
|
9
|
+
* already applies reduced motion, motion sensitivity, intensity and the
|
|
10
|
+
* animation budget.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare const EFFECT_KINDS: readonly ["enter", "exit", "attention", "click", "hover", "card", "loop", "page", "background", "text", "cursor", "scroll"];
|
|
14
|
+
type EffectKind = (typeof EFFECT_KINDS)[number];
|
|
15
|
+
interface EffectContext {
|
|
16
|
+
/** Reduced motion applies (OS setting, `minimal` / `static` sensitivity). */
|
|
17
|
+
readonly reduced: boolean;
|
|
18
|
+
readonly sensitivity: MotionSensitivity;
|
|
19
|
+
/** The triggering event (pointer position for click effects), if any. */
|
|
20
|
+
readonly event?: Event;
|
|
21
|
+
/** `el.animate()` with the library's motion rules (may return `null`). */
|
|
22
|
+
animate(el: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions): Animation | null;
|
|
23
|
+
/** Register teardown for long-running effects (loops, listeners). */
|
|
24
|
+
onCleanup(fn: Cleanup): void;
|
|
25
|
+
}
|
|
26
|
+
interface EffectDefinition<O extends Record<string, unknown> = Record<string, any>> {
|
|
27
|
+
/** Unique, kebab-case. */
|
|
28
|
+
name: string;
|
|
29
|
+
kind: EffectKind;
|
|
30
|
+
/** One line for docs and the gallery. */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Option defaults (merged under the caller's options). */
|
|
33
|
+
defaults?: Partial<O>;
|
|
34
|
+
/**
|
|
35
|
+
* Under reduced motion: `'skip'` (do nothing — default for loop, background
|
|
36
|
+
* and cursor effects) or `'run'` (run with `ctx.reduced === true`, the
|
|
37
|
+
* effect degrades itself — default for everything else).
|
|
38
|
+
*/
|
|
39
|
+
reduced?: 'skip' | 'run';
|
|
40
|
+
/** Play the effect. Return an Animation / Promise to be awaited, or a cleanup. */
|
|
41
|
+
run(el: HTMLElement, options: O, ctx: EffectContext): void | Cleanup | Animation | null | Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 5.1 — card & click effects 2.0, registered through `registerEffect()`.
|
|
46
|
+
* Card: `holo` (holographic foil following the pointer), `glare-sweep`,
|
|
47
|
+
* `book-open`, `card-fan`, `topple`, `float-tilt`. Click: `shockwave`,
|
|
48
|
+
* `ink-splash`, `star-burst`, `jelly-press`, `ring-ripple`, `emoji-rain`.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
declare const CARD_FX: EffectDefinition[];
|
|
52
|
+
declare const CLICK_FX: EffectDefinition[];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 5.2 — bounce & physics micro-interactions, registered through
|
|
56
|
+
* `registerEffect()`. Keyframes come from a damped-spring / gravity solver
|
|
57
|
+
* (`springKeyframes()`, `bounceKeyframes()`), so the motion is physical but
|
|
58
|
+
* still runs on the Web Animations API (compositor, reduced-motion aware).
|
|
59
|
+
* Effects: `bounce-in`, `rubber-band`, `elastic-hover`, `drop-bounce`,
|
|
60
|
+
* `gravity-text`, `spring-follow`, `bell-swing`.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
interface SpringOptions {
|
|
64
|
+
stiffness?: number;
|
|
65
|
+
damping?: number;
|
|
66
|
+
mass?: number;
|
|
67
|
+
/** Samples (keyframes). */
|
|
68
|
+
steps?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sample a damped spring from 0 → 1 and return the progress values plus the
|
|
72
|
+
* time (ms) it takes to settle. Pure, deterministic.
|
|
73
|
+
*/
|
|
74
|
+
declare function solveSpring({ stiffness, damping, mass, steps }?: SpringOptions): {
|
|
75
|
+
values: number[];
|
|
76
|
+
duration: number;
|
|
77
|
+
};
|
|
78
|
+
/** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
|
|
79
|
+
declare function springKeyframes(map: (p: number) => Keyframe, spring?: SpringOptions): {
|
|
80
|
+
frames: Keyframe[];
|
|
81
|
+
duration: number;
|
|
82
|
+
};
|
|
83
|
+
/** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
|
|
84
|
+
declare function bounceKeyframes(bounce?: number, steps?: number): number[];
|
|
85
|
+
declare const PHYSICS_FX: EffectDefinition[];
|
|
86
|
+
|
|
87
|
+
/** Helpers shared by the 5.x effect packs. */
|
|
88
|
+
|
|
89
|
+
/** A fixed, pointer-transparent, aria-hidden layer for transient particles. */
|
|
90
|
+
declare function fxLayer(): HTMLElement;
|
|
91
|
+
|
|
92
|
+
/** The effect packs by version, in release order. */
|
|
93
|
+
declare const EFFECT_PACKS: Record<string, EffectDefinition[]>;
|
|
94
|
+
/** 5.1: card & click effects 2.0. */
|
|
95
|
+
declare function registerCardClickEffects(): void;
|
|
96
|
+
/** 5.2: bounce & physics micro-interactions. */
|
|
97
|
+
declare function registerPhysicsEffects(): void;
|
|
98
|
+
/** Register the built-ins and every pack (idempotent). */
|
|
99
|
+
declare function registerAllEffects(): void;
|
|
100
|
+
|
|
101
|
+
export { CARD_FX, CLICK_FX, EFFECT_PACKS, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPhysicsEffects, solveSpring, springKeyframes };
|
|
102
|
+
export type { SpringOptions };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
type MotionSensitivity = 'full' | 'gentle' | 'minimal' | 'static';
|
|
2
|
+
type Cleanup = () => void;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 5.0 — unified plugin-style effect registration. Every effect (built-in or
|
|
6
|
+
* yours) is a plain object registered once and played the same way:
|
|
7
|
+
* `playEffect(el, name)`, `bindEffect(el, name, { trigger })` or
|
|
8
|
+
* `<usa-fx effect="name" trigger="click">`. Effects get a context that
|
|
9
|
+
* already applies reduced motion, motion sensitivity, intensity and the
|
|
10
|
+
* animation budget.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare const EFFECT_KINDS: readonly ["enter", "exit", "attention", "click", "hover", "card", "loop", "page", "background", "text", "cursor", "scroll"];
|
|
14
|
+
type EffectKind = (typeof EFFECT_KINDS)[number];
|
|
15
|
+
interface EffectContext {
|
|
16
|
+
/** Reduced motion applies (OS setting, `minimal` / `static` sensitivity). */
|
|
17
|
+
readonly reduced: boolean;
|
|
18
|
+
readonly sensitivity: MotionSensitivity;
|
|
19
|
+
/** The triggering event (pointer position for click effects), if any. */
|
|
20
|
+
readonly event?: Event;
|
|
21
|
+
/** `el.animate()` with the library's motion rules (may return `null`). */
|
|
22
|
+
animate(el: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions): Animation | null;
|
|
23
|
+
/** Register teardown for long-running effects (loops, listeners). */
|
|
24
|
+
onCleanup(fn: Cleanup): void;
|
|
25
|
+
}
|
|
26
|
+
interface EffectDefinition<O extends Record<string, unknown> = Record<string, any>> {
|
|
27
|
+
/** Unique, kebab-case. */
|
|
28
|
+
name: string;
|
|
29
|
+
kind: EffectKind;
|
|
30
|
+
/** One line for docs and the gallery. */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Option defaults (merged under the caller's options). */
|
|
33
|
+
defaults?: Partial<O>;
|
|
34
|
+
/**
|
|
35
|
+
* Under reduced motion: `'skip'` (do nothing — default for loop, background
|
|
36
|
+
* and cursor effects) or `'run'` (run with `ctx.reduced === true`, the
|
|
37
|
+
* effect degrades itself — default for everything else).
|
|
38
|
+
*/
|
|
39
|
+
reduced?: 'skip' | 'run';
|
|
40
|
+
/** Play the effect. Return an Animation / Promise to be awaited, or a cleanup. */
|
|
41
|
+
run(el: HTMLElement, options: O, ctx: EffectContext): void | Cleanup | Animation | null | Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 5.1 — card & click effects 2.0, registered through `registerEffect()`.
|
|
46
|
+
* Card: `holo` (holographic foil following the pointer), `glare-sweep`,
|
|
47
|
+
* `book-open`, `card-fan`, `topple`, `float-tilt`. Click: `shockwave`,
|
|
48
|
+
* `ink-splash`, `star-burst`, `jelly-press`, `ring-ripple`, `emoji-rain`.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
declare const CARD_FX: EffectDefinition[];
|
|
52
|
+
declare const CLICK_FX: EffectDefinition[];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 5.2 — bounce & physics micro-interactions, registered through
|
|
56
|
+
* `registerEffect()`. Keyframes come from a damped-spring / gravity solver
|
|
57
|
+
* (`springKeyframes()`, `bounceKeyframes()`), so the motion is physical but
|
|
58
|
+
* still runs on the Web Animations API (compositor, reduced-motion aware).
|
|
59
|
+
* Effects: `bounce-in`, `rubber-band`, `elastic-hover`, `drop-bounce`,
|
|
60
|
+
* `gravity-text`, `spring-follow`, `bell-swing`.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
interface SpringOptions {
|
|
64
|
+
stiffness?: number;
|
|
65
|
+
damping?: number;
|
|
66
|
+
mass?: number;
|
|
67
|
+
/** Samples (keyframes). */
|
|
68
|
+
steps?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sample a damped spring from 0 → 1 and return the progress values plus the
|
|
72
|
+
* time (ms) it takes to settle. Pure, deterministic.
|
|
73
|
+
*/
|
|
74
|
+
declare function solveSpring({ stiffness, damping, mass, steps }?: SpringOptions): {
|
|
75
|
+
values: number[];
|
|
76
|
+
duration: number;
|
|
77
|
+
};
|
|
78
|
+
/** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
|
|
79
|
+
declare function springKeyframes(map: (p: number) => Keyframe, spring?: SpringOptions): {
|
|
80
|
+
frames: Keyframe[];
|
|
81
|
+
duration: number;
|
|
82
|
+
};
|
|
83
|
+
/** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
|
|
84
|
+
declare function bounceKeyframes(bounce?: number, steps?: number): number[];
|
|
85
|
+
declare const PHYSICS_FX: EffectDefinition[];
|
|
86
|
+
|
|
87
|
+
/** Helpers shared by the 5.x effect packs. */
|
|
88
|
+
|
|
89
|
+
/** A fixed, pointer-transparent, aria-hidden layer for transient particles. */
|
|
90
|
+
declare function fxLayer(): HTMLElement;
|
|
91
|
+
|
|
92
|
+
/** The effect packs by version, in release order. */
|
|
93
|
+
declare const EFFECT_PACKS: Record<string, EffectDefinition[]>;
|
|
94
|
+
/** 5.1: card & click effects 2.0. */
|
|
95
|
+
declare function registerCardClickEffects(): void;
|
|
96
|
+
/** 5.2: bounce & physics micro-interactions. */
|
|
97
|
+
declare function registerPhysicsEffects(): void;
|
|
98
|
+
/** Register the built-ins and every pack (idempotent). */
|
|
99
|
+
declare function registerAllEffects(): void;
|
|
100
|
+
|
|
101
|
+
export { CARD_FX, CLICK_FX, EFFECT_PACKS, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPhysicsEffects, solveSpring, springKeyframes };
|
|
102
|
+
export type { SpringOptions };
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { registerBuiltinEffects, registerEffects } from './fx.js';
|
|
2
|
+
import '../chunks/base-C3Sw9sAO.js';
|
|
3
|
+
import '../chunks/core-DN3hHbHh.js';
|
|
4
|
+
import './tokens.js';
|
|
5
|
+
import '../chunks/fx-ChjxrMBo.js';
|
|
6
|
+
|
|
7
|
+
let layer = null;
|
|
8
|
+
/** A fixed, pointer-transparent, aria-hidden layer for transient particles. */
|
|
9
|
+
function fxLayer() {
|
|
10
|
+
if (layer?.isConnected)
|
|
11
|
+
return layer;
|
|
12
|
+
layer = document.createElement('div');
|
|
13
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
14
|
+
layer.setAttribute('data-usa-fx-layer', '');
|
|
15
|
+
layer.style.cssText = 'position:fixed;inset:0;pointer-events:none;overflow:hidden;z-index:2147483000;contain:strict';
|
|
16
|
+
document.body.appendChild(layer);
|
|
17
|
+
return layer;
|
|
18
|
+
}
|
|
19
|
+
/** Client point of the triggering pointer event, or the element's center. */
|
|
20
|
+
function origin(el, ctx) {
|
|
21
|
+
const e = ctx.event;
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
return e && typeof e.clientX === 'number' && (e.clientX || e.clientY) ? { x: e.clientX, y: e.clientY } : { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
24
|
+
}
|
|
25
|
+
/** Spawn an absolutely positioned node in the fx layer at (x, y); removed when its animation ends. */
|
|
26
|
+
function spawn(x, y, css, ctx, frames, opts, text = '') {
|
|
27
|
+
const n = document.createElement('span');
|
|
28
|
+
n.style.cssText = `position:absolute;left:${x}px;top:${y}px;${css}`;
|
|
29
|
+
if (text)
|
|
30
|
+
n.textContent = text;
|
|
31
|
+
fxLayer().appendChild(n);
|
|
32
|
+
const a = ctx.animate(n, frames, { fill: 'forwards', ...opts });
|
|
33
|
+
const rm = () => n.remove();
|
|
34
|
+
if (a)
|
|
35
|
+
a.finished.then(rm, rm);
|
|
36
|
+
else
|
|
37
|
+
rm();
|
|
38
|
+
return a;
|
|
39
|
+
}
|
|
40
|
+
/** An overlay child covering `el` (makes `el` a positioning context). Returns it and a remover. */
|
|
41
|
+
function overlay(el, css) {
|
|
42
|
+
const o = document.createElement('span');
|
|
43
|
+
o.setAttribute('aria-hidden', 'true');
|
|
44
|
+
o.style.cssText = `position:absolute;inset:0;pointer-events:none;border-radius:inherit;${css}`;
|
|
45
|
+
if (getComputedStyle(el).position === 'static')
|
|
46
|
+
el.style.position = 'relative';
|
|
47
|
+
el.appendChild(o);
|
|
48
|
+
return [o, () => o.remove()];
|
|
49
|
+
}
|
|
50
|
+
const PALETTE = ['#7c5cff', '#ff5c8a', '#22d3ee', '#facc15', '#34d399'];
|
|
51
|
+
const rand = (a, b) => a + Math.random() * (b - a);
|
|
52
|
+
/** Wait for all animations (ignoring nulls). */
|
|
53
|
+
const all = (anims) => Promise.all(anims.filter(Boolean).map((a) => a.finished.catch(() => undefined)));
|
|
54
|
+
|
|
55
|
+
const CARD_FX = [
|
|
56
|
+
{
|
|
57
|
+
name: 'holo',
|
|
58
|
+
kind: 'card',
|
|
59
|
+
description: 'Holographic foil: a rainbow sheen and sparkle that follow the pointer (persistent; bind with trigger="load").',
|
|
60
|
+
reduced: 'run',
|
|
61
|
+
defaults: { strength: 0.55 },
|
|
62
|
+
run: (el, o, ctx) => {
|
|
63
|
+
const [layer, remove] = overlay(el, `mix-blend-mode:color-dodge;opacity:${o.strength};background:linear-gradient(115deg,transparent 20%,#ff8bd855 35%,#8bf3ff55 45%,#fff58b55 55%,transparent 70%),repeating-linear-gradient(55deg,#ffffff10 0 2px,transparent 2px 6px);background-size:250% 250%,100% 100%;background-position:50% 50%;transition:background-position .2s ease-out`);
|
|
64
|
+
if (ctx.reduced)
|
|
65
|
+
return remove;
|
|
66
|
+
const move = (e) => {
|
|
67
|
+
const r = el.getBoundingClientRect();
|
|
68
|
+
const x = ((e.clientX - r.left) / (r.width || 1)) * 100;
|
|
69
|
+
const y = ((e.clientY - r.top) / (r.height || 1)) * 100;
|
|
70
|
+
layer.style.backgroundPosition = `${x}% ${y}%, 0 0`;
|
|
71
|
+
el.style.transform = `perspective(700px) rotateY(${(x - 50) / 8}deg) rotateX(${(50 - y) / 8}deg)`;
|
|
72
|
+
};
|
|
73
|
+
const leave = () => {
|
|
74
|
+
layer.style.backgroundPosition = '50% 50%, 0 0';
|
|
75
|
+
el.style.transform = '';
|
|
76
|
+
};
|
|
77
|
+
el.addEventListener('pointermove', move);
|
|
78
|
+
el.addEventListener('pointerleave', leave);
|
|
79
|
+
return () => {
|
|
80
|
+
el.removeEventListener('pointermove', move);
|
|
81
|
+
el.removeEventListener('pointerleave', leave);
|
|
82
|
+
leave();
|
|
83
|
+
remove();
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'glare-sweep',
|
|
89
|
+
kind: 'card',
|
|
90
|
+
description: 'A diagonal light streak sweeps across the card once.',
|
|
91
|
+
defaults: { duration: 900, color: 'rgba(255,255,255,.55)' },
|
|
92
|
+
run: (el, o, ctx) => {
|
|
93
|
+
if (ctx.reduced)
|
|
94
|
+
return;
|
|
95
|
+
const [g, remove] = overlay(el, `overflow:hidden;background:linear-gradient(105deg,transparent 35%,${o.color} 50%,transparent 65%);background-size:250% 100%`);
|
|
96
|
+
const a = ctx.animate(g, [{ backgroundPosition: '150% 0' }, { backgroundPosition: '-50% 0' }], { duration: o.duration, easing: 'ease-in-out' });
|
|
97
|
+
if (a)
|
|
98
|
+
a.finished.then(remove, remove);
|
|
99
|
+
else
|
|
100
|
+
remove();
|
|
101
|
+
return a;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'book-open',
|
|
106
|
+
kind: 'card',
|
|
107
|
+
description: 'The card opens like a book cover around its left edge, peeks and closes.',
|
|
108
|
+
defaults: { angle: 35, duration: 1100 },
|
|
109
|
+
run: (el, o, ctx) => {
|
|
110
|
+
el.style.transformOrigin = 'left center';
|
|
111
|
+
return ctx.animate(el, [{ transform: 'perspective(900px) rotateY(0)' }, { transform: `perspective(900px) rotateY(-${o.angle}deg)`, offset: 0.45 }, { transform: 'perspective(900px) rotateY(0)' }], { duration: o.duration, easing: 'cubic-bezier(0.34, 1.3, 0.64, 1)' });
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'card-fan',
|
|
116
|
+
kind: 'card',
|
|
117
|
+
description: 'The element’s children fan out like a hand of cards, then gather.',
|
|
118
|
+
defaults: { spread: 14, duration: 900 },
|
|
119
|
+
run: (el, o, ctx) => {
|
|
120
|
+
const kids = Array.from(el.children);
|
|
121
|
+
const mid = (kids.length - 1) / 2;
|
|
122
|
+
return all(kids.map((k, i) => ctx.animate(k, [{ transform: 'none' }, { transform: `translateX(${(i - mid) * o.spread}px) rotate(${(i - mid) * (o.spread / 2)}deg)`, offset: 0.5 }, { transform: 'none' }], { duration: o.duration, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' })));
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'topple',
|
|
127
|
+
kind: 'card',
|
|
128
|
+
description: 'Tips backwards on its bottom edge and springs upright.',
|
|
129
|
+
defaults: { duration: 1000 },
|
|
130
|
+
run: (el, o, ctx) => {
|
|
131
|
+
el.style.transformOrigin = 'bottom center';
|
|
132
|
+
return ctx.animate(el, [{ transform: 'perspective(800px) rotateX(0)' }, { transform: 'perspective(800px) rotateX(55deg)', offset: 0.35 }, { transform: 'perspective(800px) rotateX(-12deg)', offset: 0.6 }, { transform: 'perspective(800px) rotateX(5deg)', offset: 0.8 }, { transform: 'perspective(800px) rotateX(0)' }], { duration: o.duration, easing: 'ease-out' });
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'float-tilt',
|
|
137
|
+
kind: 'loop',
|
|
138
|
+
description: 'Idle floating with a slow 3D sway (loop; stops under reduced motion).',
|
|
139
|
+
defaults: { duration: 4200, distance: 8 },
|
|
140
|
+
run: (el, o, ctx) => {
|
|
141
|
+
const a = ctx.animate(el, [{ transform: 'translateY(0) rotateX(0) rotateY(0)' }, { transform: `translateY(-${o.distance}px) rotateX(4deg) rotateY(-5deg)` }, { transform: 'translateY(0) rotateX(0) rotateY(0)' }], { duration: o.duration, iterations: Infinity, easing: 'ease-in-out' });
|
|
142
|
+
return () => a?.cancel();
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
const CLICK_FX = [
|
|
147
|
+
{
|
|
148
|
+
name: 'shockwave',
|
|
149
|
+
kind: 'click',
|
|
150
|
+
description: 'Two expanding rings from the click point.',
|
|
151
|
+
defaults: { color: '#7c5cff', size: 160, duration: 700 },
|
|
152
|
+
run: (el, o, ctx) => {
|
|
153
|
+
if (ctx.reduced)
|
|
154
|
+
return;
|
|
155
|
+
const { x, y } = origin(el, ctx);
|
|
156
|
+
return all([0, 120].map((delay) => spawn(x, y, `width:${o.size}px;height:${o.size}px;margin:${-o.size / 2}px 0 0 ${-o.size / 2}px;border:3px solid ${o.color};border-radius:50%`, ctx, [{ transform: 'scale(0)', opacity: 0.9 }, { transform: 'scale(1)', opacity: 0 }], { duration: o.duration, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' })));
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'ink-splash',
|
|
161
|
+
kind: 'click',
|
|
162
|
+
description: 'Ink blobs splatter from the click point and fade.',
|
|
163
|
+
defaults: { count: 9, colors: PALETTE, duration: 800 },
|
|
164
|
+
run: (el, o, ctx) => {
|
|
165
|
+
if (ctx.reduced)
|
|
166
|
+
return;
|
|
167
|
+
const { x, y } = origin(el, ctx);
|
|
168
|
+
return all(Array.from({ length: o.count }, (_, i) => {
|
|
169
|
+
const s = rand(6, 22);
|
|
170
|
+
const ang = rand(0, Math.PI * 2);
|
|
171
|
+
const d = rand(20, 70);
|
|
172
|
+
return spawn(x, y, `width:${s}px;height:${s}px;margin:${-s / 2}px 0 0 ${-s / 2}px;background:${o.colors[i % o.colors.length]};border-radius:${rand(40, 50)}% ${rand(50, 60)}% ${rand(40, 60)}% ${rand(45, 55)}%`, ctx, [{ transform: 'translate(0,0) scale(.3)', opacity: 1 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d}px) scale(1)`, opacity: 0.9, offset: 0.5 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d + 8}px) scale(1.1)`, opacity: 0 }], { duration: o.duration, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' });
|
|
173
|
+
}));
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'star-burst',
|
|
178
|
+
kind: 'click',
|
|
179
|
+
description: 'Spinning stars burst outward.',
|
|
180
|
+
defaults: { count: 10, colors: ['#facc15', '#fb923c', '#f472b6'], duration: 900 },
|
|
181
|
+
run: (el, o, ctx) => {
|
|
182
|
+
if (ctx.reduced)
|
|
183
|
+
return;
|
|
184
|
+
const { x, y } = origin(el, ctx);
|
|
185
|
+
return all(Array.from({ length: o.count }, (_, i) => {
|
|
186
|
+
const ang = (i / o.count) * Math.PI * 2;
|
|
187
|
+
const d = rand(40, 80);
|
|
188
|
+
const s = rand(12, 22);
|
|
189
|
+
return spawn(x, y, `font-size:${s}px;line-height:1;margin:${-s / 2}px 0 0 ${-s / 2}px;color:${o.colors[i % o.colors.length]}`, ctx, [{ transform: 'translate(0,0) rotate(0) scale(.2)', opacity: 1 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d}px) rotate(${rand(180, 360)}deg) scale(1)`, opacity: 0 }], { duration: o.duration, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' }, '★');
|
|
190
|
+
}));
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'jelly-press',
|
|
195
|
+
kind: 'click',
|
|
196
|
+
description: 'The pressed element squishes like jelly and wobbles back.',
|
|
197
|
+
defaults: { duration: 650 },
|
|
198
|
+
run: (el, o, ctx) => ctx.reduced ? ctx.animate(el, [{ opacity: 1 }, { opacity: 0.7 }, { opacity: 1 }], { duration: 250 }) : ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: 'scale(1.15,.85)', offset: 0.2 }, { transform: 'scale(.9,1.1)', offset: 0.4 }, { transform: 'scale(1.05,.95)', offset: 0.6 }, { transform: 'scale(.98,1.02)', offset: 0.8 }, { transform: 'scale(1,1)' }], { duration: o.duration, easing: 'ease-out' }),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'ring-ripple',
|
|
202
|
+
kind: 'click',
|
|
203
|
+
description: 'Concentric outlined rings ripple out like water.',
|
|
204
|
+
defaults: { rings: 3, color: '#22d3ee', size: 120, duration: 1000 },
|
|
205
|
+
run: (el, o, ctx) => {
|
|
206
|
+
if (ctx.reduced)
|
|
207
|
+
return;
|
|
208
|
+
const { x, y } = origin(el, ctx);
|
|
209
|
+
return all(Array.from({ length: o.rings }, (_, i) => spawn(x, y, `width:${o.size}px;height:${o.size}px;margin:${-o.size / 2}px 0 0 ${-o.size / 2}px;border:2px solid ${o.color};border-radius:50%`, ctx, [{ transform: 'scale(.1)', opacity: 0.8 }, { transform: 'scale(1)', opacity: 0 }], { duration: o.duration, delay: i * 160, easing: 'ease-out' })));
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'emoji-rain',
|
|
214
|
+
kind: 'click',
|
|
215
|
+
description: 'Emoji pop up and fall around the click (pass `emoji: "🎉✨💜"`).',
|
|
216
|
+
defaults: { emoji: '🎉✨💜🔥', count: 14, duration: 1300 },
|
|
217
|
+
run: (el, o, ctx) => {
|
|
218
|
+
if (ctx.reduced)
|
|
219
|
+
return;
|
|
220
|
+
const { x, y } = origin(el, ctx);
|
|
221
|
+
const chars = Array.from(String(o.emoji));
|
|
222
|
+
return all(Array.from({ length: o.count }, (_, i) => {
|
|
223
|
+
const dx = rand(-90, 90);
|
|
224
|
+
return spawn(x, y, `font-size:${rand(16, 28)}px;line-height:1`, ctx, [{ transform: 'translate(0,0) scale(.4)', opacity: 0 }, { transform: `translate(${dx * 0.6}px,${rand(-90, -50)}px) scale(1)`, opacity: 1, offset: 0.35 }, { transform: `translate(${dx}px,${rand(40, 90)}px) rotate(${rand(-60, 60)}deg) scale(.9)`, opacity: 0 }], { duration: o.duration, delay: i * 25, easing: 'cubic-bezier(0.33, 0, 0.67, 1)' }, chars[i % chars.length]);
|
|
225
|
+
}));
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sample a damped spring from 0 → 1 and return the progress values plus the
|
|
232
|
+
* time (ms) it takes to settle. Pure, deterministic.
|
|
233
|
+
*/
|
|
234
|
+
function solveSpring({ stiffness = 180, damping = 12, mass = 1, steps = 40 } = {}) {
|
|
235
|
+
const dt = 1 / 120;
|
|
236
|
+
let x = 0;
|
|
237
|
+
let v = 0;
|
|
238
|
+
const trace = [];
|
|
239
|
+
let t = 0;
|
|
240
|
+
for (; t < 4; t += dt) {
|
|
241
|
+
const a = (-stiffness * (x - 1) - damping * v) / mass;
|
|
242
|
+
v += a * dt;
|
|
243
|
+
x += v * dt;
|
|
244
|
+
trace.push(x);
|
|
245
|
+
if (t > 0.1 && Math.abs(x - 1) < 0.001 && Math.abs(v) < 0.01)
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
const values = Array.from({ length: steps + 1 }, (_, i) => +trace[Math.min(trace.length - 1, Math.round((i / steps) * (trace.length - 1)))].toFixed(4));
|
|
249
|
+
values[0] = 0;
|
|
250
|
+
values[steps] = 1;
|
|
251
|
+
return { values, duration: Math.round(trace.length * dt * 1000) };
|
|
252
|
+
}
|
|
253
|
+
/** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
|
|
254
|
+
function springKeyframes(map, spring) {
|
|
255
|
+
const { values, duration } = solveSpring(spring);
|
|
256
|
+
return { frames: values.map(map), duration };
|
|
257
|
+
}
|
|
258
|
+
/** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
|
|
259
|
+
function bounceKeyframes(bounce = 0.5, steps = 48) {
|
|
260
|
+
// segment durations scale with sqrt(height); heights scale with bounce^2 per hop
|
|
261
|
+
const hops = [1];
|
|
262
|
+
let h = 1;
|
|
263
|
+
while (hops.length < 6 && h > 0.01) {
|
|
264
|
+
h *= bounce * bounce;
|
|
265
|
+
hops.push(h);
|
|
266
|
+
}
|
|
267
|
+
const segT = hops.map((hh, i) => (i === 0 ? Math.sqrt(hh) : 2 * Math.sqrt(hh)));
|
|
268
|
+
const total = segT.reduce((a, b) => a + b, 0);
|
|
269
|
+
return Array.from({ length: steps + 1 }, (_, i) => {
|
|
270
|
+
let t = (i / steps) * total;
|
|
271
|
+
for (let k = 0; k < hops.length; k++) {
|
|
272
|
+
if (t <= segT[k] || k === hops.length - 1) {
|
|
273
|
+
const tt = Math.min(t, segT[k]);
|
|
274
|
+
if (k === 0)
|
|
275
|
+
return +(1 - (tt / segT[0]) ** 2).toFixed(4);
|
|
276
|
+
const half = segT[k] / 2;
|
|
277
|
+
return +(hops[k] * (1 - ((tt - half) / half) ** 2)).toFixed(4);
|
|
278
|
+
}
|
|
279
|
+
t -= segT[k];
|
|
280
|
+
}
|
|
281
|
+
return 0;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const fade = (el, ctx) => ctx.animate(el, [{ opacity: 0 }, { opacity: 1 }], { duration: 200 });
|
|
285
|
+
const PHYSICS_FX = [
|
|
286
|
+
{
|
|
287
|
+
name: 'bounce-in',
|
|
288
|
+
kind: 'enter',
|
|
289
|
+
description: 'Springs in from small with an overshoot (spring solver; stiffness / damping options).',
|
|
290
|
+
defaults: { stiffness: 220, damping: 11, from: 0.3 },
|
|
291
|
+
run: (el, o, ctx) => {
|
|
292
|
+
if (ctx.reduced)
|
|
293
|
+
return fade(el, ctx);
|
|
294
|
+
const { frames, duration } = springKeyframes((p) => ({ transform: `scale(${(o.from + (1 - o.from) * p).toFixed(4)})`, opacity: Math.min(1, p * 3) }), o);
|
|
295
|
+
return ctx.animate(el, frames, { duration, easing: 'linear' });
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'rubber-band',
|
|
300
|
+
kind: 'attention',
|
|
301
|
+
description: 'Stretches wide, snaps back thin and settles like a rubber band.',
|
|
302
|
+
defaults: { duration: 900, amount: 0.25 },
|
|
303
|
+
run: (el, o, ctx) => {
|
|
304
|
+
if (ctx.reduced)
|
|
305
|
+
return;
|
|
306
|
+
const a = o.amount;
|
|
307
|
+
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 });
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'elastic-hover',
|
|
312
|
+
kind: 'hover',
|
|
313
|
+
description: 'Hover lifts with a springy overshoot; leaving springs back (persistent; trigger="load").',
|
|
314
|
+
defaults: { lift: 6, scale: 1.04, stiffness: 260, damping: 10 },
|
|
315
|
+
run: (el, o, ctx) => {
|
|
316
|
+
if (ctx.reduced)
|
|
317
|
+
return;
|
|
318
|
+
const to = (on) => {
|
|
319
|
+
const from = on ? 0 : 1;
|
|
320
|
+
const { frames, duration } = springKeyframes((p) => {
|
|
321
|
+
const k = from + (on ? p : -p);
|
|
322
|
+
return { transform: `translateY(${(-o.lift * k).toFixed(2)}px) scale(${(1 + (o.scale - 1) * k).toFixed(4)})` };
|
|
323
|
+
}, o);
|
|
324
|
+
ctx.animate(el, frames, { duration, fill: 'forwards', easing: 'linear' });
|
|
325
|
+
};
|
|
326
|
+
const enter = () => to(true);
|
|
327
|
+
const leave = () => to(false);
|
|
328
|
+
el.addEventListener('pointerenter', enter);
|
|
329
|
+
el.addEventListener('pointerleave', leave);
|
|
330
|
+
return () => {
|
|
331
|
+
el.removeEventListener('pointerenter', enter);
|
|
332
|
+
el.removeEventListener('pointerleave', leave);
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'drop-bounce',
|
|
338
|
+
kind: 'enter',
|
|
339
|
+
description: 'Falls in under gravity and bounces to rest (`height`, `bounce` restitution 0–0.9).',
|
|
340
|
+
defaults: { height: 120, bounce: 0.5, duration: 1100 },
|
|
341
|
+
run: (el, o, ctx) => {
|
|
342
|
+
if (ctx.reduced)
|
|
343
|
+
return fade(el, ctx);
|
|
344
|
+
const hs = bounceKeyframes(Math.min(0.9, Math.max(0, o.bounce)));
|
|
345
|
+
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' });
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: 'gravity-text',
|
|
350
|
+
kind: 'text',
|
|
351
|
+
description: 'Each character drops in under gravity with a bounce, staggered (splits the text into aria-hidden spans; the label stays readable).',
|
|
352
|
+
defaults: { height: 60, bounce: 0.45, duration: 900, stagger: 45 },
|
|
353
|
+
run: (el, o, ctx) => {
|
|
354
|
+
if (ctx.reduced)
|
|
355
|
+
return;
|
|
356
|
+
if (!el.dataset.usaSplit) {
|
|
357
|
+
const text = el.textContent || '';
|
|
358
|
+
el.setAttribute('aria-label', text);
|
|
359
|
+
el.textContent = '';
|
|
360
|
+
for (const ch of Array.from(text)) {
|
|
361
|
+
const s = document.createElement('span');
|
|
362
|
+
s.setAttribute('aria-hidden', 'true');
|
|
363
|
+
s.style.display = 'inline-block';
|
|
364
|
+
s.style.whiteSpace = 'pre';
|
|
365
|
+
s.textContent = ch;
|
|
366
|
+
el.appendChild(s);
|
|
367
|
+
}
|
|
368
|
+
el.dataset.usaSplit = '1';
|
|
369
|
+
}
|
|
370
|
+
const hs = bounceKeyframes(o.bounce, 32);
|
|
371
|
+
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' })));
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'spring-follow',
|
|
376
|
+
kind: 'cursor',
|
|
377
|
+
description: 'The element springs toward the pointer while it moves over its parent, and back home on leave (persistent).',
|
|
378
|
+
defaults: { stiffness: 0.12, damping: 0.75, range: 40 },
|
|
379
|
+
run: (el, o, ctx) => {
|
|
380
|
+
if (ctx.reduced)
|
|
381
|
+
return;
|
|
382
|
+
const host = el.parentElement || el;
|
|
383
|
+
let tx = 0;
|
|
384
|
+
let ty = 0;
|
|
385
|
+
let x = 0;
|
|
386
|
+
let y = 0;
|
|
387
|
+
let vx = 0;
|
|
388
|
+
let vy = 0;
|
|
389
|
+
let raf = 0;
|
|
390
|
+
const tick = () => {
|
|
391
|
+
vx = (vx + (tx - x) * o.stiffness) * o.damping;
|
|
392
|
+
vy = (vy + (ty - y) * o.stiffness) * o.damping;
|
|
393
|
+
x += vx;
|
|
394
|
+
y += vy;
|
|
395
|
+
el.style.translate = `${x.toFixed(2)}px ${y.toFixed(2)}px`;
|
|
396
|
+
raf = Math.abs(vx) + Math.abs(vy) + Math.abs(tx - x) + Math.abs(ty - y) > 0.05 ? requestAnimationFrame(tick) : 0;
|
|
397
|
+
};
|
|
398
|
+
const kick = () => {
|
|
399
|
+
if (!raf)
|
|
400
|
+
raf = requestAnimationFrame(tick);
|
|
401
|
+
};
|
|
402
|
+
const move = (e) => {
|
|
403
|
+
const r = host.getBoundingClientRect();
|
|
404
|
+
const nx = (e.clientX - r.left) / (r.width || 1) - 0.5;
|
|
405
|
+
const ny = (e.clientY - r.top) / (r.height || 1) - 0.5;
|
|
406
|
+
tx = nx * 2 * o.range;
|
|
407
|
+
ty = ny * 2 * o.range;
|
|
408
|
+
kick();
|
|
409
|
+
};
|
|
410
|
+
const leave = () => {
|
|
411
|
+
tx = ty = 0;
|
|
412
|
+
kick();
|
|
413
|
+
};
|
|
414
|
+
host.addEventListener('pointermove', move);
|
|
415
|
+
host.addEventListener('pointerleave', leave);
|
|
416
|
+
return () => {
|
|
417
|
+
host.removeEventListener('pointermove', move);
|
|
418
|
+
host.removeEventListener('pointerleave', leave);
|
|
419
|
+
cancelAnimationFrame(raf);
|
|
420
|
+
el.style.translate = '';
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: 'bell-swing',
|
|
426
|
+
kind: 'attention',
|
|
427
|
+
description: 'Swings from its top like a ringing bell with damped oscillation.',
|
|
428
|
+
defaults: { angle: 22, stiffness: 120, damping: 4 },
|
|
429
|
+
run: (el, o, ctx) => {
|
|
430
|
+
if (ctx.reduced)
|
|
431
|
+
return;
|
|
432
|
+
el.style.transformOrigin = 'top center';
|
|
433
|
+
const { frames, duration } = springKeyframes((p) => ({ transform: `rotate(${(o.angle * (1 - p)).toFixed(2)}deg)` }), { stiffness: o.stiffness, damping: o.damping, steps: 48 });
|
|
434
|
+
frames[0] = { transform: 'rotate(0deg)' };
|
|
435
|
+
return ctx.animate(el, frames, { duration: Math.min(duration, 2400), easing: 'linear' });
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* use-scroll-animate/components/effects — the 5.x effect packs, all
|
|
442
|
+
* registered through `registerEffect()` (5.0) and playable with
|
|
443
|
+
* `playEffect()`, `bindEffect()` or `<usa-fx>`. Kept out of
|
|
444
|
+
* `use-scroll-animate/components` / `components/lite` so their size budgets
|
|
445
|
+
* hold; the UMD bundle registers everything.
|
|
446
|
+
*
|
|
447
|
+
* ```ts
|
|
448
|
+
* import { registerAllEffects } from 'use-scroll-animate/components/effects';
|
|
449
|
+
* registerAllEffects();
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
/** The effect packs by version, in release order. */
|
|
453
|
+
const EFFECT_PACKS = {
|
|
454
|
+
'cards-click': [...CARD_FX, ...CLICK_FX],
|
|
455
|
+
physics: PHYSICS_FX,
|
|
456
|
+
};
|
|
457
|
+
/** 5.1: card & click effects 2.0. */
|
|
458
|
+
function registerCardClickEffects() {
|
|
459
|
+
registerEffects(EFFECT_PACKS['cards-click']);
|
|
460
|
+
}
|
|
461
|
+
/** 5.2: bounce & physics micro-interactions. */
|
|
462
|
+
function registerPhysicsEffects() {
|
|
463
|
+
registerEffects(EFFECT_PACKS.physics);
|
|
464
|
+
}
|
|
465
|
+
/** Register the built-ins and every pack (idempotent). */
|
|
466
|
+
function registerAllEffects() {
|
|
467
|
+
registerBuiltinEffects();
|
|
468
|
+
for (const defs of Object.values(EFFECT_PACKS))
|
|
469
|
+
registerEffects(defs);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export { CARD_FX, CLICK_FX, EFFECT_PACKS, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPhysicsEffects, solveSpring, springKeyframes };
|
|
473
|
+
//# sourceMappingURL=effects.js.map
|