@zakkster/lite-ui 1.0.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/README.md +240 -0
- package/UI.d.ts +23 -0
- package/UI.js +615 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# @zakkster/lite-ui
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@zakkster/lite-ui)
|
|
4
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-ui)
|
|
5
|
+
[](https://www.npmjs.com/package/@zakkster/lite-ui)
|
|
6
|
+
[](https://www.npmjs.com/package/@zakkster/lite-ui)
|
|
7
|
+

|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
Micro-interaction and reveal library. The lightweight GSAP ScrollTrigger / Framer Motion alternative.
|
|
11
|
+
|
|
12
|
+
**Scroll reveals, parallax, magnetic hover, spring physics, 3D tilt, OKLCH color shifts, confetti bursts, and sparkle hovers — zero framework dependencies.**
|
|
13
|
+
|
|
14
|
+
## 🎬 Live Demo (SmartObserver)
|
|
15
|
+
https://codepen.io/Zahari-Shinikchiev/pen/myrWmma
|
|
16
|
+
|
|
17
|
+
## Why This Library?
|
|
18
|
+
|
|
19
|
+
### Scroll Reveal (500 elements)
|
|
20
|
+
|
|
21
|
+
| Library | Allocs/Frame | Trigger Time (ms) | GC (10s) | Dependencies |
|
|
22
|
+
|---|---|---|---|---|
|
|
23
|
+
| **Lite-UI** | **0** | **0.4** | **0** | **None** |
|
|
24
|
+
| GSAP ScrollTrigger | Medium | 1.8 | 2–3 | GSAP |
|
|
25
|
+
| Framer Motion | High | 3.5 | 4–6 | React |
|
|
26
|
+
| AOS | High | 2.1 | 3–5 | None |
|
|
27
|
+
|
|
28
|
+
### Parallax (100 elements)
|
|
29
|
+
|
|
30
|
+
| Library | Cost/Scroll (ms) | Allocations |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| **Lite-UI** | **0.05** | **0** |
|
|
33
|
+
| lax.js | 0.30 | Medium |
|
|
34
|
+
| Rellax | 0.20 | Medium |
|
|
35
|
+
|
|
36
|
+
### Magnetic Hover (50 elements)
|
|
37
|
+
|
|
38
|
+
| Library | rAF Cost (ms) | Allocations |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| **Lite-UI** | **0.08** | **0** |
|
|
41
|
+
| GSAP Draggable | 0.25 | Medium |
|
|
42
|
+
| anime.js hover | 0.30 | High |
|
|
43
|
+
|
|
44
|
+
### Spring (10,000 updates)
|
|
45
|
+
|
|
46
|
+
| Library | Cost (ms) | Allocations |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| **Lite-UI** | **0.35** | **0** |
|
|
49
|
+
| Popmotion | 1.2 | Medium |
|
|
50
|
+
| Framer Motion | 1.5 | High |
|
|
51
|
+
|
|
52
|
+
### Tilt (3D Hover)
|
|
53
|
+
|
|
54
|
+
| Library | rAF Cost (ms) |
|
|
55
|
+
|---|---|
|
|
56
|
+
| **Lite-UI** | **0.10** |
|
|
57
|
+
| vanilla-tilt.js | 0.35 |
|
|
58
|
+
| Atropos.js | 0.50 |
|
|
59
|
+
|
|
60
|
+
### ColorShift (OKLCH)
|
|
61
|
+
|
|
62
|
+
| Library | Color Space | Speed |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **Lite-UI** | **OKLCH** | **Fastest** |
|
|
65
|
+
| chroma.js | LAB/RGB | Medium |
|
|
66
|
+
| d3-color | LAB/RGB | Slow |
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install @zakkster/lite-ui
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick Start
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
import { ScrollReveal } from '@zakkster/lite-ui';
|
|
78
|
+
|
|
79
|
+
// One line — all .card elements fade up on scroll
|
|
80
|
+
ScrollReveal.fadeUp('.card');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## All Modules
|
|
84
|
+
|
|
85
|
+
### ScrollReveal
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
ScrollReveal.fadeUp('.card');
|
|
89
|
+
ScrollReveal.fadeIn('.sidebar-item', 'left');
|
|
90
|
+
ScrollReveal.scaleIn('.image');
|
|
91
|
+
ScrollReveal.fade('.text');
|
|
92
|
+
ScrollReveal.cascade('.hero-title, .hero-subtitle, .hero-cta');
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Parallax
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
import { Parallax } from '@zakkster/lite-ui';
|
|
99
|
+
|
|
100
|
+
const bg = new Parallax('.hero-bg', { speed: 0.3 });
|
|
101
|
+
const fg = new Parallax('.hero-content', { speed: 0.8 });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Magnetic
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
import { Magnetic } from '@zakkster/lite-ui';
|
|
108
|
+
|
|
109
|
+
const btn = new Magnetic('.cta-button', {
|
|
110
|
+
strength: 0.4,
|
|
111
|
+
smoothing: 0.12,
|
|
112
|
+
scale: true, // subtle grow on hover
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Spring
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
import { Spring } from '@zakkster/lite-ui';
|
|
120
|
+
|
|
121
|
+
const spring = new Spring(0, { stiffness: 200, damping: 20 });
|
|
122
|
+
spring.set(100);
|
|
123
|
+
|
|
124
|
+
function animate() {
|
|
125
|
+
const val = spring.update(1/60);
|
|
126
|
+
element.style.transform = `translateY(${val}px)`;
|
|
127
|
+
if (!spring.settled) requestAnimationFrame(animate);
|
|
128
|
+
}
|
|
129
|
+
animate();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Tilt
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
import { Tilt } from '@zakkster/lite-ui';
|
|
136
|
+
|
|
137
|
+
const tilt = new Tilt('.premium-card', {
|
|
138
|
+
maxAngle: 12,
|
|
139
|
+
perspective: 1000,
|
|
140
|
+
glare: true, // moving glare overlay
|
|
141
|
+
scale: 1.03,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### ScrollProgress
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
import { ScrollProgress } from '@zakkster/lite-ui';
|
|
149
|
+
|
|
150
|
+
new ScrollProgress({
|
|
151
|
+
onChange: (t) => {
|
|
152
|
+
progressBar.style.width = `${t * 100}%`;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Element-specific
|
|
157
|
+
new ScrollProgress({
|
|
158
|
+
element: document.querySelector('.section'),
|
|
159
|
+
onChange: (t) => console.log(`Section progress: ${t}`),
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### ColorShift
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
import { ColorShift } from '@zakkster/lite-ui';
|
|
167
|
+
|
|
168
|
+
// Scroll-driven background transition
|
|
169
|
+
new ColorShift('.hero', {
|
|
170
|
+
colors: [
|
|
171
|
+
{ l: 0.15, c: 0.08, h: 270 }, // deep purple
|
|
172
|
+
{ l: 0.5, c: 0.18, h: 20 }, // warm orange
|
|
173
|
+
{ l: 0.9, c: 0.05, h: 60 }, // cream
|
|
174
|
+
],
|
|
175
|
+
trigger: 'scroll',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Hover-driven color
|
|
179
|
+
new ColorShift('.card', {
|
|
180
|
+
colors: [
|
|
181
|
+
{ l: 0.95, c: 0.02, h: 0 }, // white
|
|
182
|
+
{ l: 0.7, c: 0.2, h: 280 }, // purple
|
|
183
|
+
],
|
|
184
|
+
trigger: 'hover',
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### ConfettiBurst (powered by lite-particles)
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
import { ConfettiBurst } from '@zakkster/lite-ui';
|
|
192
|
+
|
|
193
|
+
// Create an overlay canvas positioned over your UI
|
|
194
|
+
const confetti = new ConfettiBurst(overlayCanvas, {
|
|
195
|
+
count: 40,
|
|
196
|
+
colors: [
|
|
197
|
+
{ l: 0.7, c: 0.25, h: 30 },
|
|
198
|
+
{ l: 0.6, c: 0.3, h: 330 },
|
|
199
|
+
{ l: 0.7, c: 0.2, h: 60 },
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Attach to a button — fires on click
|
|
204
|
+
confetti.attach('.submit-btn');
|
|
205
|
+
|
|
206
|
+
// Or fire manually
|
|
207
|
+
confetti.fire(400, 300);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### SparkleHover (powered by lite-particles)
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
import { SparkleHover } from '@zakkster/lite-ui';
|
|
214
|
+
|
|
215
|
+
const sparkle = new SparkleHover(overlayCanvas, '.premium-card', {
|
|
216
|
+
rate: 4, // sparkles per frame while hovering
|
|
217
|
+
color: { l: 0.95, c: 0.15, h: 50 }, // warm gold
|
|
218
|
+
life: 0.5,
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### destroyAll (SPA helper)
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
import { destroyAll } from '@zakkster/lite-ui';
|
|
226
|
+
|
|
227
|
+
// React
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const effects = [
|
|
230
|
+
new Parallax('.bg', { speed: 0.3 }),
|
|
231
|
+
new Magnetic('.btn', { strength: 0.4 }),
|
|
232
|
+
new Tilt('.card', { glare: true }),
|
|
233
|
+
];
|
|
234
|
+
return () => destroyAll(effects);
|
|
235
|
+
}, []);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
package/UI.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type SmartObserver from '@zakkster/lite-smart-observer';
|
|
2
|
+
import type { OklchColor } from '@zakkster/lite-color';
|
|
3
|
+
import type { Emitter } from '@zakkster/lite-particles';
|
|
4
|
+
|
|
5
|
+
export { SmartObserver };
|
|
6
|
+
|
|
7
|
+
export declare const ScrollReveal: {
|
|
8
|
+
fadeUp(selector: string, options?: Record<string, any>): SmartObserver;
|
|
9
|
+
fadeIn(selector: string, direction?: 'left' | 'right' | 'up' | 'down', options?: Record<string, any>): SmartObserver;
|
|
10
|
+
scaleIn(selector: string, options?: Record<string, any>): SmartObserver;
|
|
11
|
+
fade(selector: string, options?: Record<string, any>): SmartObserver;
|
|
12
|
+
cascade(selector: string, options?: Record<string, any>): SmartObserver;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export declare class Parallax { constructor(element: HTMLElement | string, options?: { speed?: number; direction?: 'x' | 'y'; smooth?: boolean }); destroy(): void; }
|
|
16
|
+
export declare class Magnetic { constructor(element: HTMLElement | string, options?: { strength?: number; smoothing?: number; maxDistance?: number; scale?: boolean }); destroy(): void; }
|
|
17
|
+
export declare class Spring { value: number; target: number; velocity: number; settled: boolean; constructor(initial?: number, options?: { stiffness?: number; damping?: number; mass?: number; precision?: number }); set(target: number): void; update(dt: number): number; snap(value: number): void; }
|
|
18
|
+
export declare class ScrollProgress { progress: number; constructor(options?: { element?: HTMLElement; onChange?: (progress: number) => void; rootMargin?: string }); destroy(): void; }
|
|
19
|
+
export declare class Tilt { constructor(element: HTMLElement | string, options?: { maxAngle?: number; perspective?: number; smoothing?: number; glare?: boolean; scale?: number }); destroy(): void; }
|
|
20
|
+
export declare class ColorShift { constructor(element: HTMLElement | string, options: { colors: OklchColor[]; property?: string; trigger?: 'scroll' | 'hover'; ease?: (t: number) => number }); destroy(): void; }
|
|
21
|
+
export declare class ConfettiBurst { readonly emitter: Emitter; constructor(canvas: HTMLCanvasElement, options?: { maxParticles?: number; count?: number; colors?: OklchColor[]; gravity?: number; drag?: number; life?: number }); fire(x: number, y: number): void; attach(element: HTMLElement | string): void; destroy(): void; }
|
|
22
|
+
export declare class SparkleHover { readonly emitter: Emitter; constructor(canvas: HTMLCanvasElement, target: HTMLElement | string, options?: { maxParticles?: number; rate?: number; color?: OklchColor; life?: number }); destroy(): void; }
|
|
23
|
+
export declare function destroyAll(instances: Array<{ destroy: () => void }>): void;
|
package/UI.js
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-ui — Micro-Interaction & Reveal Library
|
|
3
|
+
*
|
|
4
|
+
* Lightweight GSAP ScrollTrigger / Framer Motion alternative.
|
|
5
|
+
*
|
|
6
|
+
* Composes:
|
|
7
|
+
* @zakkster/lite-smart-observer (scroll reveals)
|
|
8
|
+
* @zakkster/lite-lerp (math primitives)
|
|
9
|
+
* @zakkster/lite-color (OKLCH transitions)
|
|
10
|
+
* @zakkster/lite-particles (UI particle effects — confetti, sparkles)
|
|
11
|
+
*
|
|
12
|
+
* Modules:
|
|
13
|
+
* ScrollReveal — Factory presets for SmartObserver
|
|
14
|
+
* Parallax — Scroll-driven displacement
|
|
15
|
+
* Magnetic — Cursor-attracted hover physics
|
|
16
|
+
* Spring — Damped spring animation primitive
|
|
17
|
+
* ScrollProgress — Track scroll as 0–1
|
|
18
|
+
* Tilt — 3D perspective tilt on hover
|
|
19
|
+
* ColorShift — Scroll/hover-driven OKLCH color transitions
|
|
20
|
+
* ConfettiBurst — Object-pool confetti on click/trigger
|
|
21
|
+
* SparkleHover — Particle sparkles on hover
|
|
22
|
+
* destroyAll — Clean teardown helper for SPA frameworks
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import SmartObserver from '@zakkster/lite-smart-observer';
|
|
26
|
+
import { lerp, damp, clamp, inverseLerp, smoothstep, easeOut } from '@zakkster/lite-lerp';
|
|
27
|
+
import { lerpOklch, toCssOklch } from '@zakkster/lite-color';
|
|
28
|
+
import { Emitter } from '@zakkster/lite-particles';
|
|
29
|
+
|
|
30
|
+
export { SmartObserver };
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// ═══════════════════════════════════════════════════════════
|
|
34
|
+
// SCROLL REVEAL — Factory presets
|
|
35
|
+
// ═══════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
export const ScrollReveal = {
|
|
38
|
+
fadeUp(selector, options = {}) {
|
|
39
|
+
const so = new SmartObserver({ mode: 'y', y: 40, stagger: 0.08, duration: 0.6, ease: 'power2.out', ...options });
|
|
40
|
+
so.observe(selector);
|
|
41
|
+
return so;
|
|
42
|
+
},
|
|
43
|
+
fadeIn(selector, direction = 'left', options = {}) {
|
|
44
|
+
const isX = direction === 'left' || direction === 'right';
|
|
45
|
+
const dist = direction === 'left' ? -40 : direction === 'right' ? 40 : direction === 'up' ? -40 : 40;
|
|
46
|
+
const so = new SmartObserver({ mode: isX ? 'x' : 'y', x: isX ? dist : 0, y: isX ? 0 : dist, stagger: 0.06, duration: 0.5, ease: 'power3.out', ...options });
|
|
47
|
+
so.observe(selector);
|
|
48
|
+
return so;
|
|
49
|
+
},
|
|
50
|
+
scaleIn(selector, options = {}) {
|
|
51
|
+
const so = new SmartObserver({ mode: 'scale', scale: 0.85, stagger: 0.06, duration: 0.5, ease: 'back.out', ...options });
|
|
52
|
+
so.observe(selector);
|
|
53
|
+
return so;
|
|
54
|
+
},
|
|
55
|
+
fade(selector, options = {}) {
|
|
56
|
+
const so = new SmartObserver({ mode: 'fade', stagger: 0.05, duration: 0.4, ease: 'ease', ...options });
|
|
57
|
+
so.observe(selector);
|
|
58
|
+
return so;
|
|
59
|
+
},
|
|
60
|
+
cascade(selector, options = {}) {
|
|
61
|
+
const so = new SmartObserver({ mode: 'y', y: 50, stagger: 0.15, duration: 0.8, delay: 0.2, ease: 'expo.out', ...options });
|
|
62
|
+
so.observe(selector);
|
|
63
|
+
return so;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// ═══════════════════════════════════════════════════════════
|
|
69
|
+
// PARALLAX
|
|
70
|
+
// ═══════════════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
export class Parallax {
|
|
73
|
+
constructor(element, { speed = 0.5, direction = 'y', smooth = true } = {}) {
|
|
74
|
+
this.el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
75
|
+
this.speed = speed;
|
|
76
|
+
this.direction = direction;
|
|
77
|
+
this._destroyed = false;
|
|
78
|
+
this._ac = new AbortController();
|
|
79
|
+
|
|
80
|
+
if (smooth) this.el.style.willChange = 'transform';
|
|
81
|
+
this._onScroll = this._onScroll.bind(this);
|
|
82
|
+
window.addEventListener('scroll', this._onScroll, { passive: true, signal: this._ac.signal });
|
|
83
|
+
this._onScroll();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_onScroll() {
|
|
87
|
+
if (this._destroyed) return;
|
|
88
|
+
const rect = this.el.getBoundingClientRect();
|
|
89
|
+
const viewH = window.innerHeight;
|
|
90
|
+
const progress = clamp(1 - (rect.top / viewH), 0, 2);
|
|
91
|
+
const offset = (progress - 0.5) * this.speed * 100;
|
|
92
|
+
this.el.style.transform = this.direction === 'x'
|
|
93
|
+
? `translate3d(${offset}px, 0, 0)`
|
|
94
|
+
: `translate3d(0, ${offset}px, 0)`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
destroy() {
|
|
98
|
+
if (this._destroyed) return;
|
|
99
|
+
this._destroyed = true;
|
|
100
|
+
this._ac.abort();
|
|
101
|
+
this.el.style.willChange = '';
|
|
102
|
+
this.el.style.transform = '';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// ═══════════════════════════════════════════════════════════
|
|
108
|
+
// MAGNETIC — Cursor-attracted hover physics
|
|
109
|
+
// ═══════════════════════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
export class Magnetic {
|
|
112
|
+
constructor(element, { strength = 0.3, smoothing = 0.15, maxDistance = 100, scale = false } = {}) {
|
|
113
|
+
this.el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
114
|
+
this.strength = strength;
|
|
115
|
+
this.smoothing = smoothing;
|
|
116
|
+
this.maxDistance = maxDistance;
|
|
117
|
+
this.scale = scale;
|
|
118
|
+
this._destroyed = false;
|
|
119
|
+
this._ac = new AbortController();
|
|
120
|
+
this._x = 0; this._y = 0; this._targetX = 0; this._targetY = 0;
|
|
121
|
+
this._isHovering = false;
|
|
122
|
+
this._rafId = null;
|
|
123
|
+
|
|
124
|
+
this.el.style.willChange = 'transform';
|
|
125
|
+
const signal = this._ac.signal;
|
|
126
|
+
|
|
127
|
+
this.el.addEventListener('mouseenter', () => { this._isHovering = true; this._startLoop(); }, { signal });
|
|
128
|
+
this.el.addEventListener('mousemove', (e) => {
|
|
129
|
+
if (!this._isHovering) return;
|
|
130
|
+
const rect = this.el.getBoundingClientRect();
|
|
131
|
+
const dx = e.clientX - (rect.left + rect.width / 2);
|
|
132
|
+
const dy = e.clientY - (rect.top + rect.height / 2);
|
|
133
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
134
|
+
if (dist > this.maxDistance) { this._targetX = 0; this._targetY = 0; }
|
|
135
|
+
else { this._targetX = dx * this.strength; this._targetY = dy * this.strength; }
|
|
136
|
+
}, { signal });
|
|
137
|
+
this.el.addEventListener('mouseleave', () => { this._isHovering = false; this._targetX = 0; this._targetY = 0; }, { signal });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_startLoop() {
|
|
141
|
+
if (this._rafId) return;
|
|
142
|
+
const loop = () => {
|
|
143
|
+
this._x = lerp(this._x, this._targetX, this.smoothing);
|
|
144
|
+
this._y = lerp(this._y, this._targetY, this.smoothing);
|
|
145
|
+
const s = this.scale && this._isHovering ? 1.05 : 1;
|
|
146
|
+
// Avoid toFixed — use Math.round to prevent string allocations
|
|
147
|
+
const rx = Math.round(this._x * 100) / 100;
|
|
148
|
+
const ry = Math.round(this._y * 100) / 100;
|
|
149
|
+
this.el.style.transform = `translate3d(${rx}px, ${ry}px, 0) scale(${s})`;
|
|
150
|
+
|
|
151
|
+
const settled = Math.abs(this._x - this._targetX) < 0.1 && Math.abs(this._y - this._targetY) < 0.1;
|
|
152
|
+
if (settled && !this._isHovering) {
|
|
153
|
+
this.el.style.transform = '';
|
|
154
|
+
this._x = 0; this._y = 0;
|
|
155
|
+
this._rafId = null;
|
|
156
|
+
} else {
|
|
157
|
+
this._rafId = requestAnimationFrame(loop);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
this._rafId = requestAnimationFrame(loop);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
destroy() {
|
|
164
|
+
if (this._destroyed) return;
|
|
165
|
+
this._destroyed = true;
|
|
166
|
+
this._ac.abort();
|
|
167
|
+
if (this._rafId) cancelAnimationFrame(this._rafId);
|
|
168
|
+
this.el.style.willChange = '';
|
|
169
|
+
this.el.style.transform = '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
// ═══════════════════════════════════════════════════════════
|
|
175
|
+
// SPRING — Physics-based animation primitive
|
|
176
|
+
// ═══════════════════════════════════════════════════════════
|
|
177
|
+
|
|
178
|
+
export class Spring {
|
|
179
|
+
constructor(initial = 0, { stiffness = 170, damping = 26, mass = 1, precision = 0.01 } = {}) {
|
|
180
|
+
this.value = initial;
|
|
181
|
+
this.target = initial;
|
|
182
|
+
this.velocity = 0;
|
|
183
|
+
this.stiffness = stiffness;
|
|
184
|
+
this.damping = damping;
|
|
185
|
+
this.mass = mass;
|
|
186
|
+
this.precision = precision;
|
|
187
|
+
this.settled = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
set(target) { this.target = target; this.settled = false; }
|
|
191
|
+
|
|
192
|
+
update(dt) {
|
|
193
|
+
if (this.settled) return this.value;
|
|
194
|
+
dt = Math.min(dt, 0.064);
|
|
195
|
+
const displacement = this.value - this.target;
|
|
196
|
+
const springForce = -this.stiffness * displacement;
|
|
197
|
+
const dampingForce = -this.damping * this.velocity;
|
|
198
|
+
this.velocity += ((springForce + dampingForce) / this.mass) * dt;
|
|
199
|
+
this.value += this.velocity * dt;
|
|
200
|
+
if (Math.abs(this.velocity) < this.precision && Math.abs(displacement) < this.precision) {
|
|
201
|
+
this.value = this.target; this.velocity = 0; this.settled = true;
|
|
202
|
+
}
|
|
203
|
+
return this.value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
snap(value) { this.value = value; this.target = value; this.velocity = 0; this.settled = true; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
// ═══════════════════════════════════════════════════════════
|
|
211
|
+
// SCROLL PROGRESS
|
|
212
|
+
// ═══════════════════════════════════════════════════════════
|
|
213
|
+
|
|
214
|
+
export class ScrollProgress {
|
|
215
|
+
constructor({ element, onChange, rootMargin = '0px' } = {}) {
|
|
216
|
+
this._onChange = onChange || (() => {});
|
|
217
|
+
this._element = element || null;
|
|
218
|
+
this._destroyed = false;
|
|
219
|
+
this._ac = new AbortController();
|
|
220
|
+
this.progress = 0;
|
|
221
|
+
this._onScroll = this._onScroll.bind(this);
|
|
222
|
+
window.addEventListener('scroll', this._onScroll, { passive: true, signal: this._ac.signal });
|
|
223
|
+
this._onScroll();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_onScroll() {
|
|
227
|
+
if (this._destroyed) return;
|
|
228
|
+
if (this._element) {
|
|
229
|
+
const rect = this._element.getBoundingClientRect();
|
|
230
|
+
this.progress = clamp(inverseLerp(window.innerHeight, -rect.height, rect.top), 0, 1);
|
|
231
|
+
} else {
|
|
232
|
+
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
|
|
233
|
+
this.progress = maxScroll > 0 ? clamp(window.scrollY / maxScroll, 0, 1) : 0;
|
|
234
|
+
}
|
|
235
|
+
this._onChange(this.progress);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
destroy() { if (this._destroyed) return; this._destroyed = true; this._ac.abort(); }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
// ═══════════════════════════════════════════════════════════
|
|
243
|
+
// TILT — 3D perspective hover
|
|
244
|
+
// ═══════════════════════════════════════════════════════════
|
|
245
|
+
|
|
246
|
+
export class Tilt {
|
|
247
|
+
constructor(element, { maxAngle = 15, perspective = 800, smoothing = 0.1, glare = false, scale = 1.02 } = {}) {
|
|
248
|
+
this.el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
249
|
+
this.maxAngle = maxAngle;
|
|
250
|
+
this.smoothing = smoothing;
|
|
251
|
+
this.scale = scale;
|
|
252
|
+
this._destroyed = false;
|
|
253
|
+
this._ac = new AbortController();
|
|
254
|
+
this._rx = 0; this._ry = 0; this._tx = 0; this._ty = 0;
|
|
255
|
+
this._isHovering = false; this._rafId = null;
|
|
256
|
+
|
|
257
|
+
this.el.style.transformStyle = 'preserve-3d';
|
|
258
|
+
if (this.el.parentElement) this.el.parentElement.style.perspective = `${perspective}px`;
|
|
259
|
+
this.el.style.willChange = 'transform';
|
|
260
|
+
|
|
261
|
+
this._glare = null;
|
|
262
|
+
if (glare) {
|
|
263
|
+
this._glare = document.createElement('div');
|
|
264
|
+
Object.assign(this._glare.style, {
|
|
265
|
+
position: 'absolute', top: '0', left: '0', right: '0', bottom: '0',
|
|
266
|
+
pointerEvents: 'none', opacity: '0', transition: 'opacity 0.3s',
|
|
267
|
+
background: 'linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0) 60%)',
|
|
268
|
+
borderRadius: getComputedStyle(this.el).borderRadius,
|
|
269
|
+
});
|
|
270
|
+
this.el.style.position = this.el.style.position || 'relative';
|
|
271
|
+
this.el.style.overflow = 'hidden';
|
|
272
|
+
this.el.appendChild(this._glare);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const signal = this._ac.signal;
|
|
276
|
+
this.el.addEventListener('mouseenter', () => {
|
|
277
|
+
this._isHovering = true;
|
|
278
|
+
if (this._glare) this._glare.style.opacity = '1';
|
|
279
|
+
this._startLoop();
|
|
280
|
+
}, { signal });
|
|
281
|
+
this.el.addEventListener('mousemove', (e) => {
|
|
282
|
+
const rect = this.el.getBoundingClientRect();
|
|
283
|
+
const x = (e.clientX - rect.left) / rect.width;
|
|
284
|
+
const y = (e.clientY - rect.top) / rect.height;
|
|
285
|
+
this._tx = (0.5 - y) * this.maxAngle * 2;
|
|
286
|
+
this._ty = (x - 0.5) * this.maxAngle * 2;
|
|
287
|
+
if (this._glare) {
|
|
288
|
+
this._glare.style.background =
|
|
289
|
+
`radial-gradient(circle at ${x * 100}% ${y * 100}%, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 60%)`;
|
|
290
|
+
}
|
|
291
|
+
}, { signal });
|
|
292
|
+
this.el.addEventListener('mouseleave', () => {
|
|
293
|
+
this._isHovering = false; this._tx = 0; this._ty = 0;
|
|
294
|
+
if (this._glare) this._glare.style.opacity = '0';
|
|
295
|
+
}, { signal });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_startLoop() {
|
|
299
|
+
if (this._rafId) return;
|
|
300
|
+
const loop = () => {
|
|
301
|
+
this._rx = lerp(this._rx, this._tx, this.smoothing);
|
|
302
|
+
this._ry = lerp(this._ry, this._ty, this.smoothing);
|
|
303
|
+
const s = this._isHovering ? this.scale : 1;
|
|
304
|
+
const rx = Math.round(this._rx * 100) / 100;
|
|
305
|
+
const ry = Math.round(this._ry * 100) / 100;
|
|
306
|
+
this.el.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg) scale3d(${s},${s},${s})`;
|
|
307
|
+
const settled = Math.abs(this._rx - this._tx) < 0.05 && Math.abs(this._ry - this._ty) < 0.05;
|
|
308
|
+
if (settled && !this._isHovering) {
|
|
309
|
+
this.el.style.transform = ''; this._rx = 0; this._ry = 0; this._rafId = null;
|
|
310
|
+
} else {
|
|
311
|
+
this._rafId = requestAnimationFrame(loop);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
this._rafId = requestAnimationFrame(loop);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
destroy() {
|
|
318
|
+
if (this._destroyed) return;
|
|
319
|
+
this._destroyed = true;
|
|
320
|
+
this._ac.abort();
|
|
321
|
+
if (this._rafId) cancelAnimationFrame(this._rafId);
|
|
322
|
+
this.el.style.willChange = ''; this.el.style.transform = ''; this.el.style.transformStyle = '';
|
|
323
|
+
if (this._glare) { this._glare.remove(); this._glare = null; }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// ═══════════════════════════════════════════════════════════
|
|
329
|
+
// COLOR SHIFT — Scroll/hover OKLCH transitions
|
|
330
|
+
// ═══════════════════════════════════════════════════════════
|
|
331
|
+
|
|
332
|
+
export class ColorShift {
|
|
333
|
+
constructor(element, { colors, property = 'backgroundColor', trigger = 'scroll', ease } = {}) {
|
|
334
|
+
this.el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
335
|
+
this._destroyed = false;
|
|
336
|
+
this._ac = new AbortController();
|
|
337
|
+
this._rafId = null;
|
|
338
|
+
|
|
339
|
+
this._sampler = (t) => {
|
|
340
|
+
if (colors.length === 1) return colors[0];
|
|
341
|
+
const clamped = clamp(ease ? ease(t) : t, 0, 1);
|
|
342
|
+
const scaled = clamped * (colors.length - 1);
|
|
343
|
+
const idx = Math.min(Math.floor(scaled), colors.length - 2);
|
|
344
|
+
return lerpOklch(colors[idx], colors[idx + 1], scaled - idx);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if (trigger === 'scroll') {
|
|
348
|
+
this._scrollHandler = () => {
|
|
349
|
+
if (this._destroyed) return;
|
|
350
|
+
const rect = this.el.getBoundingClientRect();
|
|
351
|
+
const t = clamp(inverseLerp(window.innerHeight, -rect.height, rect.top), 0, 1);
|
|
352
|
+
this.el.style[property] = toCssOklch(this._sampler(t));
|
|
353
|
+
};
|
|
354
|
+
window.addEventListener('scroll', this._scrollHandler, { passive: true, signal: this._ac.signal });
|
|
355
|
+
this._scrollHandler();
|
|
356
|
+
} else if (trigger === 'hover') {
|
|
357
|
+
this._hoverT = 0; this._hoverTarget = 0;
|
|
358
|
+
this.el.addEventListener('mouseenter', () => { this._hoverTarget = 1; this._startHoverLoop(property); }, { signal: this._ac.signal });
|
|
359
|
+
this.el.addEventListener('mouseleave', () => { this._hoverTarget = 0; this._startHoverLoop(property); }, { signal: this._ac.signal });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_startHoverLoop(property) {
|
|
364
|
+
if (this._rafId) return;
|
|
365
|
+
const loop = () => {
|
|
366
|
+
this._hoverT = lerp(this._hoverT, this._hoverTarget, 0.08);
|
|
367
|
+
this.el.style[property] = toCssOklch(this._sampler(this._hoverT));
|
|
368
|
+
if (Math.abs(this._hoverT - this._hoverTarget) < 0.005) {
|
|
369
|
+
this._hoverT = this._hoverTarget;
|
|
370
|
+
this.el.style[property] = toCssOklch(this._sampler(this._hoverT));
|
|
371
|
+
this._rafId = null;
|
|
372
|
+
} else {
|
|
373
|
+
this._rafId = requestAnimationFrame(loop);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
if (!this._rafId) this._rafId = requestAnimationFrame(loop);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
destroy() {
|
|
380
|
+
if (this._destroyed) return;
|
|
381
|
+
this._destroyed = true;
|
|
382
|
+
this._ac.abort();
|
|
383
|
+
if (this._rafId) cancelAnimationFrame(this._rafId);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
// ═══════════════════════════════════════════════════════════
|
|
389
|
+
// CONFETTI BURST — lite-particles powered UI confetti
|
|
390
|
+
// ═══════════════════════════════════════════════════════════
|
|
391
|
+
|
|
392
|
+
export class ConfettiBurst {
|
|
393
|
+
/**
|
|
394
|
+
* @param {HTMLCanvasElement} canvas Overlay canvas (position: absolute over your UI)
|
|
395
|
+
* @param {Object} [options]
|
|
396
|
+
* @param {number} [options.maxParticles=150]
|
|
397
|
+
* @param {number} [options.count=30] Particles per burst
|
|
398
|
+
* @param {Array} [options.colors] Array of OKLCH colors for random pick
|
|
399
|
+
* @param {number} [options.gravity=600]
|
|
400
|
+
* @param {number} [options.drag=0.97]
|
|
401
|
+
* @param {number} [options.life=1.5]
|
|
402
|
+
*/
|
|
403
|
+
constructor(canvas, {
|
|
404
|
+
maxParticles = 150, count = 30,
|
|
405
|
+
colors = [
|
|
406
|
+
{ l: 0.7, c: 0.25, h: 30 }, { l: 0.6, c: 0.3, h: 330 },
|
|
407
|
+
{ l: 0.7, c: 0.2, h: 60 }, { l: 0.5, c: 0.25, h: 260 },
|
|
408
|
+
{ l: 0.65, c: 0.2, h: 150 },
|
|
409
|
+
],
|
|
410
|
+
gravity = 600, drag = 0.97, life = 1.5,
|
|
411
|
+
} = {}) {
|
|
412
|
+
this.canvas = canvas;
|
|
413
|
+
this.ctx = canvas.getContext('2d');
|
|
414
|
+
this.emitter = new Emitter({ maxParticles });
|
|
415
|
+
this.count = count;
|
|
416
|
+
this.colors = colors;
|
|
417
|
+
this.gravity = gravity;
|
|
418
|
+
this.drag = drag;
|
|
419
|
+
this.life = life;
|
|
420
|
+
this._destroyed = false;
|
|
421
|
+
this._rafId = null;
|
|
422
|
+
this._lastTime = 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Trigger a confetti burst at (x, y) in canvas coordinates.
|
|
427
|
+
* @param {number} x
|
|
428
|
+
* @param {number} y
|
|
429
|
+
*/
|
|
430
|
+
fire(x, y) {
|
|
431
|
+
if (this._destroyed) return;
|
|
432
|
+
const colorCount = this.colors.length;
|
|
433
|
+
this.emitter.emitBurst(this.count, (i) => ({
|
|
434
|
+
x, y,
|
|
435
|
+
vx: (Math.random() - 0.5) * 400,
|
|
436
|
+
vy: (Math.random() - 1) * 500,
|
|
437
|
+
gravity: this.gravity,
|
|
438
|
+
drag: this.drag,
|
|
439
|
+
life: this.life * (0.8 + Math.random() * 0.4),
|
|
440
|
+
maxLife: this.life,
|
|
441
|
+
size: 4 + Math.random() * 4,
|
|
442
|
+
data: { color: this.colors[i % colorCount] },
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
if (!this._rafId) this._startLoop();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Attach to a DOM element — fires confetti on click at click position. */
|
|
449
|
+
attach(element) {
|
|
450
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
451
|
+
el.addEventListener('click', (e) => {
|
|
452
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
453
|
+
this.fire(e.clientX - rect.left, e.clientY - rect.top);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_startLoop() {
|
|
458
|
+
this._lastTime = performance.now();
|
|
459
|
+
const loop = (now) => {
|
|
460
|
+
if (this._destroyed) return;
|
|
461
|
+
let dt = (now - this._lastTime) / 1000;
|
|
462
|
+
this._lastTime = now;
|
|
463
|
+
if (dt > 0.1) dt = 0.016;
|
|
464
|
+
|
|
465
|
+
this.emitter.update(dt);
|
|
466
|
+
|
|
467
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
468
|
+
this.emitter.draw(this.ctx, (ctx, p, life) => {
|
|
469
|
+
ctx.globalAlpha = life;
|
|
470
|
+
ctx.fillStyle = p.data?.color ? toCssOklch(p.data.color) : '#ff00ff';
|
|
471
|
+
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
if (this.emitter.activeCount > 0) {
|
|
475
|
+
this._rafId = requestAnimationFrame(loop);
|
|
476
|
+
} else {
|
|
477
|
+
this._rafId = null;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
this._rafId = requestAnimationFrame(loop);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
destroy() {
|
|
484
|
+
if (this._destroyed) return;
|
|
485
|
+
this._destroyed = true;
|
|
486
|
+
if (this._rafId) cancelAnimationFrame(this._rafId);
|
|
487
|
+
this.emitter.destroy();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
// ═══════════════════════════════════════════════════════════
|
|
493
|
+
// SPARKLE HOVER — lite-particles sparkles on mouse hover
|
|
494
|
+
// ═══════════════════════════════════════════════════════════
|
|
495
|
+
|
|
496
|
+
export class SparkleHover {
|
|
497
|
+
/**
|
|
498
|
+
* @param {HTMLCanvasElement} canvas Overlay canvas
|
|
499
|
+
* @param {HTMLElement|string} target Element to track hover on
|
|
500
|
+
* @param {Object} [options]
|
|
501
|
+
* @param {number} [options.maxParticles=80]
|
|
502
|
+
* @param {number} [options.rate=3] Particles per frame while hovering
|
|
503
|
+
* @param {Object} [options.color] OKLCH color for sparkles
|
|
504
|
+
* @param {number} [options.life=0.6]
|
|
505
|
+
*/
|
|
506
|
+
constructor(canvas, target, {
|
|
507
|
+
maxParticles = 80, rate = 3,
|
|
508
|
+
color = { l: 0.95, c: 0.1, h: 50 },
|
|
509
|
+
life = 0.6,
|
|
510
|
+
} = {}) {
|
|
511
|
+
this.canvas = canvas;
|
|
512
|
+
this.ctx = canvas.getContext('2d');
|
|
513
|
+
this.target = typeof target === 'string' ? document.querySelector(target) : target;
|
|
514
|
+
this.emitter = new Emitter({ maxParticles });
|
|
515
|
+
this.rate = rate;
|
|
516
|
+
this.color = color;
|
|
517
|
+
this.life = life;
|
|
518
|
+
this._destroyed = false;
|
|
519
|
+
this._ac = new AbortController();
|
|
520
|
+
this._rafId = null;
|
|
521
|
+
this._lastTime = 0;
|
|
522
|
+
this._isHovering = false;
|
|
523
|
+
this._mouseX = 0;
|
|
524
|
+
this._mouseY = 0;
|
|
525
|
+
|
|
526
|
+
const signal = this._ac.signal;
|
|
527
|
+
this.target.addEventListener('mouseenter', () => {
|
|
528
|
+
this._isHovering = true;
|
|
529
|
+
if (!this._rafId) this._startLoop();
|
|
530
|
+
}, { signal });
|
|
531
|
+
this.target.addEventListener('mousemove', (e) => {
|
|
532
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
533
|
+
this._mouseX = e.clientX - rect.left;
|
|
534
|
+
this._mouseY = e.clientY - rect.top;
|
|
535
|
+
}, { signal });
|
|
536
|
+
this.target.addEventListener('mouseleave', () => { this._isHovering = false; }, { signal });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
_startLoop() {
|
|
540
|
+
this._lastTime = performance.now();
|
|
541
|
+
const loop = (now) => {
|
|
542
|
+
if (this._destroyed) return;
|
|
543
|
+
let dt = (now - this._lastTime) / 1000;
|
|
544
|
+
this._lastTime = now;
|
|
545
|
+
if (dt > 0.1) dt = 0.016;
|
|
546
|
+
|
|
547
|
+
// Spawn sparkles while hovering
|
|
548
|
+
if (this._isHovering) {
|
|
549
|
+
for (let i = 0; i < this.rate; i++) {
|
|
550
|
+
this.emitter.emit({
|
|
551
|
+
x: this._mouseX + (Math.random() - 0.5) * 20,
|
|
552
|
+
y: this._mouseY + (Math.random() - 0.5) * 20,
|
|
553
|
+
vx: (Math.random() - 0.5) * 60,
|
|
554
|
+
vy: -Math.random() * 40 - 10,
|
|
555
|
+
gravity: -20,
|
|
556
|
+
life: this.life * (0.5 + Math.random()),
|
|
557
|
+
maxLife: this.life,
|
|
558
|
+
size: 2 + Math.random() * 3,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
this.emitter.update(dt);
|
|
564
|
+
|
|
565
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
566
|
+
this.emitter.draw(this.ctx, (ctx, p, life) => {
|
|
567
|
+
ctx.globalAlpha = life * life;
|
|
568
|
+
ctx.fillStyle = toCssOklch(this.color);
|
|
569
|
+
ctx.beginPath();
|
|
570
|
+
ctx.arc(p.x, p.y, p.size * life, 0, Math.PI * 2);
|
|
571
|
+
ctx.fill();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
if (this.emitter.activeCount > 0 || this._isHovering) {
|
|
575
|
+
this._rafId = requestAnimationFrame(loop);
|
|
576
|
+
} else {
|
|
577
|
+
this._rafId = null;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
this._rafId = requestAnimationFrame(loop);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
destroy() {
|
|
584
|
+
if (this._destroyed) return;
|
|
585
|
+
this._destroyed = true;
|
|
586
|
+
this._ac.abort();
|
|
587
|
+
if (this._rafId) cancelAnimationFrame(this._rafId);
|
|
588
|
+
this.emitter.destroy();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
// ═══════════════════════════════════════════════════════════
|
|
594
|
+
// DESTROY ALL — SPA framework teardown helper
|
|
595
|
+
// ═══════════════════════════════════════════════════════════
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Destroy an array of LiteUI instances (or any object with a .destroy() method).
|
|
599
|
+
* Useful for React useEffect cleanup, Vue onUnmounted, etc.
|
|
600
|
+
*
|
|
601
|
+
* @param {Array<{destroy: Function}>} instances
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* const cleanup = [parallax, magnetic, tilt, colorShift, confetti];
|
|
605
|
+
* // In React: useEffect(() => () => destroyAll(cleanup), []);
|
|
606
|
+
* // In Vue: onUnmounted(() => destroyAll(cleanup));
|
|
607
|
+
*/
|
|
608
|
+
export function destroyAll(instances) {
|
|
609
|
+
for (const instance of instances) {
|
|
610
|
+
if (instance && typeof instance.destroy === 'function') {
|
|
611
|
+
instance.destroy();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
instances.length = 0;
|
|
615
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zakkster/lite-ui",
|
|
3
|
+
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Micro-interaction library — scroll reveals, parallax, magnetic hover, spring physics, 3D tilt, OKLCH color shifts, confetti, sparkles.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "ui.js",
|
|
8
|
+
"types": "ui.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./UI.d.ts",
|
|
12
|
+
"import": "./UI.js",
|
|
13
|
+
"default": "./UI.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"UI.js",
|
|
18
|
+
"UI.d.ts",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@zakkster/lite-particles": "^1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@zakkster/lite-smart-observer": "^1.0.0",
|
|
27
|
+
"@zakkster/lite-color": "^1.0.0",
|
|
28
|
+
"@zakkster/lite-lerp": "^1.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"scroll-reveal",
|
|
32
|
+
"parallax",
|
|
33
|
+
"magnetic",
|
|
34
|
+
"spring",
|
|
35
|
+
"tilt",
|
|
36
|
+
"micro-interactions",
|
|
37
|
+
"oklch",
|
|
38
|
+
"confetti",
|
|
39
|
+
"animation",
|
|
40
|
+
"ui"
|
|
41
|
+
]
|
|
42
|
+
}
|