nativecorejs 0.1.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 +22 -0
- package/dist/components/builtinRegistry.d.ts +2 -0
- package/dist/components/builtinRegistry.js +72 -0
- package/dist/components/index.d.ts +59 -0
- package/dist/components/index.js +59 -0
- package/dist/components/loading-spinner.d.ts +5 -0
- package/dist/components/loading-spinner.js +48 -0
- package/dist/components/nc-a.d.ts +45 -0
- package/dist/components/nc-a.js +290 -0
- package/dist/components/nc-accordion.d.ts +36 -0
- package/dist/components/nc-accordion.js +186 -0
- package/dist/components/nc-alert.d.ts +11 -0
- package/dist/components/nc-alert.js +127 -0
- package/dist/components/nc-animation.d.ts +117 -0
- package/dist/components/nc-animation.js +1053 -0
- package/dist/components/nc-autocomplete.d.ts +41 -0
- package/dist/components/nc-autocomplete.js +275 -0
- package/dist/components/nc-avatar-group.d.ts +7 -0
- package/dist/components/nc-avatar-group.js +85 -0
- package/dist/components/nc-avatar.d.ts +9 -0
- package/dist/components/nc-avatar.js +127 -0
- package/dist/components/nc-badge.d.ts +7 -0
- package/dist/components/nc-badge.js +63 -0
- package/dist/components/nc-bottom-nav.d.ts +53 -0
- package/dist/components/nc-bottom-nav.js +198 -0
- package/dist/components/nc-breadcrumb.d.ts +10 -0
- package/dist/components/nc-breadcrumb.js +71 -0
- package/dist/components/nc-button.d.ts +38 -0
- package/dist/components/nc-button.js +293 -0
- package/dist/components/nc-card.d.ts +11 -0
- package/dist/components/nc-card.js +74 -0
- package/dist/components/nc-checkbox.d.ts +16 -0
- package/dist/components/nc-checkbox.js +194 -0
- package/dist/components/nc-chip.d.ts +8 -0
- package/dist/components/nc-chip.js +89 -0
- package/dist/components/nc-code.d.ts +37 -0
- package/dist/components/nc-code.js +315 -0
- package/dist/components/nc-collapsible.d.ts +33 -0
- package/dist/components/nc-collapsible.js +148 -0
- package/dist/components/nc-color-picker.d.ts +33 -0
- package/dist/components/nc-color-picker.js +265 -0
- package/dist/components/nc-copy-button.d.ts +10 -0
- package/dist/components/nc-copy-button.js +94 -0
- package/dist/components/nc-date-picker.d.ts +41 -0
- package/dist/components/nc-date-picker.js +443 -0
- package/dist/components/nc-div.d.ts +53 -0
- package/dist/components/nc-div.js +270 -0
- package/dist/components/nc-divider.d.ts +7 -0
- package/dist/components/nc-divider.js +57 -0
- package/dist/components/nc-drawer.d.ts +40 -0
- package/dist/components/nc-drawer.js +217 -0
- package/dist/components/nc-dropdown.d.ts +41 -0
- package/dist/components/nc-dropdown.js +170 -0
- package/dist/components/nc-empty-state.d.ts +5 -0
- package/dist/components/nc-empty-state.js +76 -0
- package/dist/components/nc-file-upload.d.ts +40 -0
- package/dist/components/nc-file-upload.js +336 -0
- package/dist/components/nc-form.d.ts +70 -0
- package/dist/components/nc-form.js +273 -0
- package/dist/components/nc-image.d.ts +10 -0
- package/dist/components/nc-image.js +139 -0
- package/dist/components/nc-input.d.ts +25 -0
- package/dist/components/nc-input.js +302 -0
- package/dist/components/nc-kbd.d.ts +5 -0
- package/dist/components/nc-kbd.js +34 -0
- package/dist/components/nc-menu-item.d.ts +43 -0
- package/dist/components/nc-menu-item.js +182 -0
- package/dist/components/nc-menu.d.ts +76 -0
- package/dist/components/nc-menu.js +360 -0
- package/dist/components/nc-modal.d.ts +51 -0
- package/dist/components/nc-modal.js +231 -0
- package/dist/components/nc-nav-item.d.ts +35 -0
- package/dist/components/nc-nav-item.js +142 -0
- package/dist/components/nc-number-input.d.ts +22 -0
- package/dist/components/nc-number-input.js +270 -0
- package/dist/components/nc-otp-input.d.ts +41 -0
- package/dist/components/nc-otp-input.js +227 -0
- package/dist/components/nc-pagination.d.ts +28 -0
- package/dist/components/nc-pagination.js +171 -0
- package/dist/components/nc-popover.d.ts +58 -0
- package/dist/components/nc-popover.js +301 -0
- package/dist/components/nc-progress-circular.d.ts +7 -0
- package/dist/components/nc-progress-circular.js +67 -0
- package/dist/components/nc-progress.d.ts +7 -0
- package/dist/components/nc-progress.js +109 -0
- package/dist/components/nc-radio.d.ts +13 -0
- package/dist/components/nc-radio.js +169 -0
- package/dist/components/nc-rating.d.ts +19 -0
- package/dist/components/nc-rating.js +187 -0
- package/dist/components/nc-rich-text.d.ts +43 -0
- package/dist/components/nc-rich-text.js +310 -0
- package/dist/components/nc-scroll-top.d.ts +28 -0
- package/dist/components/nc-scroll-top.js +103 -0
- package/dist/components/nc-select.d.ts +51 -0
- package/dist/components/nc-select.js +425 -0
- package/dist/components/nc-skeleton.d.ts +7 -0
- package/dist/components/nc-skeleton.js +90 -0
- package/dist/components/nc-slider.d.ts +41 -0
- package/dist/components/nc-slider.js +268 -0
- package/dist/components/nc-snackbar.d.ts +51 -0
- package/dist/components/nc-snackbar.js +200 -0
- package/dist/components/nc-splash.d.ts +25 -0
- package/dist/components/nc-splash.js +296 -0
- package/dist/components/nc-stepper.d.ts +50 -0
- package/dist/components/nc-stepper.js +236 -0
- package/dist/components/nc-switch.d.ts +14 -0
- package/dist/components/nc-switch.js +194 -0
- package/dist/components/nc-tab-item.d.ts +39 -0
- package/dist/components/nc-tab-item.js +127 -0
- package/dist/components/nc-table.d.ts +44 -0
- package/dist/components/nc-table.js +265 -0
- package/dist/components/nc-tabs.d.ts +79 -0
- package/dist/components/nc-tabs.js +519 -0
- package/dist/components/nc-tag-input.d.ts +49 -0
- package/dist/components/nc-tag-input.js +268 -0
- package/dist/components/nc-textarea.d.ts +15 -0
- package/dist/components/nc-textarea.js +164 -0
- package/dist/components/nc-time-picker.d.ts +51 -0
- package/dist/components/nc-time-picker.js +452 -0
- package/dist/components/nc-timeline.d.ts +53 -0
- package/dist/components/nc-timeline.js +171 -0
- package/dist/components/nc-tooltip.d.ts +27 -0
- package/dist/components/nc-tooltip.js +135 -0
- package/dist/core/component.d.ts +33 -0
- package/dist/core/component.js +208 -0
- package/dist/core/gpu-animation.d.ts +141 -0
- package/dist/core/gpu-animation.js +474 -0
- package/dist/core/lazyComponents.d.ts +13 -0
- package/dist/core/lazyComponents.js +73 -0
- package/dist/core/router.d.ts +55 -0
- package/dist/core/router.js +424 -0
- package/dist/core/state.d.ts +18 -0
- package/dist/core/state.js +153 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/utils/cacheBuster.d.ts +9 -0
- package/dist/utils/cacheBuster.js +12 -0
- package/dist/utils/dom.d.ts +16 -0
- package/dist/utils/dom.js +70 -0
- package/dist/utils/events.d.ts +20 -0
- package/dist/utils/events.js +80 -0
- package/dist/utils/templates.d.ts +2 -0
- package/dist/utils/templates.js +2 -0
- package/package.json +53 -0
- package/src/styles/base.css +40 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcAnimation Component
|
|
3
|
+
*
|
|
4
|
+
* A declarative animation wrapper that intelligently selects the most
|
|
5
|
+
* GPU-efficient execution path for each animation type:
|
|
6
|
+
*
|
|
7
|
+
* - Simple CSS keyframes → runs entirely on the compositor thread (no JS)
|
|
8
|
+
* - Enter / exit → Web Animations API via gpu-animation.ts
|
|
9
|
+
* - Scroll-reveal → IntersectionObserver + Web Animations API
|
|
10
|
+
* - Continuous / looping → CSS animation (compositor-only)
|
|
11
|
+
* - Particle effects → WebGL (vertex shader) with canvas2d fallback
|
|
12
|
+
*
|
|
13
|
+
* Attributes:
|
|
14
|
+
* name - Animation preset name (see list below). Required.
|
|
15
|
+
* trigger - When to run: 'mount'|'visible'|'hover'|'click'|'manual' (default: 'mount')
|
|
16
|
+
* duration - Ms integer (default: varies per preset)
|
|
17
|
+
* delay - Ms delay before animation starts (default: 0)
|
|
18
|
+
* easing - CSS easing / cubic-bezier string (default: preset-specific)
|
|
19
|
+
* iterations - Number or 'infinite' (default: 1)
|
|
20
|
+
* distance - Slide/offset distance in px (default: 40)
|
|
21
|
+
* threshold - IntersectionObserver threshold 0-1 (default: 0.15, trigger=visible only)
|
|
22
|
+
* fill - Web Animations fill mode: 'forwards'|'backwards'|'both'|'none' (default: 'forwards')
|
|
23
|
+
* reverse - boolean - play animation backwards
|
|
24
|
+
* no-gpu-hint - boolean - skip will-change + contain hints (use for very tiny elements)
|
|
25
|
+
*
|
|
26
|
+
* Particle-specific attributes (all optional):
|
|
27
|
+
* origin-x - Horizontal start position as 0-1 fraction of screen width (default: 0.5 = center)
|
|
28
|
+
* origin-y - Vertical start position as 0-1 fraction of screen height (default: 0.5)
|
|
29
|
+
* Special shortcuts: 'top'=0, 'bottom'=1, 'left'=0, 'right'=1, 'center'=0.5
|
|
30
|
+
* target-x - End-point X fraction (used by 'converge'/'ripple' presets)
|
|
31
|
+
* target-y - End-point Y fraction
|
|
32
|
+
* count - Override particle count (integer)
|
|
33
|
+
* spread - Override spread/speed scale 0-2 (default: 1)
|
|
34
|
+
*
|
|
35
|
+
* Animation preset names:
|
|
36
|
+
* Enter/exit: fade-in | fade-out | slide-up | slide-down | slide-left | slide-right
|
|
37
|
+
* scale-in | scale-out | zoom-in | zoom-out | flip-x | flip-y
|
|
38
|
+
* Attention: pulse | shake | bounce | rubber-band | swing | jello | tada | heartbeat
|
|
39
|
+
* Continuous: spin | ping | float | glow
|
|
40
|
+
* Particles: confetti | sparkles | bubbles | snow | firework |
|
|
41
|
+
* electricity | fire | explosion | ripple
|
|
42
|
+
*
|
|
43
|
+
* Events:
|
|
44
|
+
* start - CustomEvent - animation begins
|
|
45
|
+
* finish - CustomEvent - animation ends (not fired for infinite)
|
|
46
|
+
* cancel - CustomEvent - animation was cancelled
|
|
47
|
+
*
|
|
48
|
+
* Methods (call on the element):
|
|
49
|
+
* el.play() - play / replay
|
|
50
|
+
* el.pause() - pause (Web Animations API animations only)
|
|
51
|
+
* el.cancel() - cancel and reset
|
|
52
|
+
*
|
|
53
|
+
* Slots:
|
|
54
|
+
* default - the content to animate
|
|
55
|
+
*
|
|
56
|
+
* Usage:
|
|
57
|
+
* <nc-animation name="fade-in" trigger="visible">
|
|
58
|
+
* <nc-card>...</nc-card>
|
|
59
|
+
* </nc-animation>
|
|
60
|
+
*
|
|
61
|
+
* <nc-animation name="slide-up" trigger="visible" delay="150">
|
|
62
|
+
* <p>Staggered paragraph</p>
|
|
63
|
+
* </nc-animation>
|
|
64
|
+
*
|
|
65
|
+
* <nc-animation name="pulse" trigger="hover" iterations="infinite">
|
|
66
|
+
* <nc-button>Hover me</nc-button>
|
|
67
|
+
* </nc-animation>
|
|
68
|
+
*
|
|
69
|
+
* <nc-animation name="confetti" trigger="click">
|
|
70
|
+
* <nc-button variant="success">Celebrate</nc-button>
|
|
71
|
+
* </nc-animation>
|
|
72
|
+
*/
|
|
73
|
+
import { Component, defineComponent } from '../core/component.js';
|
|
74
|
+
import { animate, prepareForAnimation, cleanupAnimation, fadeIn, fadeOut, slideIn, scaleIn, createAnimationLoop, } from '../core/gpu-animation.js';
|
|
75
|
+
// ── Preset registry ───────────────────────────────────────────────────────────
|
|
76
|
+
const PRESETS = {
|
|
77
|
+
// ── Enter / exit (WAAPI - GPU transform + opacity) ──────────────────────
|
|
78
|
+
'fade-in': {
|
|
79
|
+
path: 'waapi', duration: 400,
|
|
80
|
+
run: (el, opts) => fadeIn(el, opts.duration),
|
|
81
|
+
},
|
|
82
|
+
'fade-out': {
|
|
83
|
+
path: 'waapi', duration: 400,
|
|
84
|
+
run: (el, opts) => fadeOut(el, opts.duration),
|
|
85
|
+
},
|
|
86
|
+
'slide-up': {
|
|
87
|
+
path: 'waapi', duration: 450,
|
|
88
|
+
run: (el, opts) => slideIn(el, 'up', opts.distance, opts.duration),
|
|
89
|
+
},
|
|
90
|
+
'slide-down': {
|
|
91
|
+
path: 'waapi', duration: 450,
|
|
92
|
+
run: (el, opts) => slideIn(el, 'down', opts.distance, opts.duration),
|
|
93
|
+
},
|
|
94
|
+
'slide-left': {
|
|
95
|
+
path: 'waapi', duration: 450,
|
|
96
|
+
run: (el, opts) => slideIn(el, 'left', opts.distance, opts.duration),
|
|
97
|
+
},
|
|
98
|
+
'slide-right': {
|
|
99
|
+
path: 'waapi', duration: 450,
|
|
100
|
+
run: (el, opts) => slideIn(el, 'right', opts.distance, opts.duration),
|
|
101
|
+
},
|
|
102
|
+
'scale-in': {
|
|
103
|
+
path: 'waapi', duration: 350,
|
|
104
|
+
run: (el, opts) => scaleIn(el, opts.duration),
|
|
105
|
+
},
|
|
106
|
+
'scale-out': {
|
|
107
|
+
path: 'waapi', duration: 350,
|
|
108
|
+
run: (el, opts) => animate(el, [
|
|
109
|
+
{ transform: 'scale3d(1,1,1)', opacity: '1' },
|
|
110
|
+
{ transform: 'scale3d(0.8,0.8,1)', opacity: '0' },
|
|
111
|
+
], opts),
|
|
112
|
+
},
|
|
113
|
+
'zoom-in': {
|
|
114
|
+
path: 'waapi', duration: 400,
|
|
115
|
+
run: (el, opts) => animate(el, [
|
|
116
|
+
{ transform: 'scale3d(0.5,0.5,1)', opacity: '0' },
|
|
117
|
+
{ transform: 'scale3d(1,1,1)', opacity: '1' },
|
|
118
|
+
], { ...opts, easing: 'cubic-bezier(0.34,1.56,0.64,1)' }),
|
|
119
|
+
},
|
|
120
|
+
'zoom-out': {
|
|
121
|
+
path: 'waapi', duration: 400,
|
|
122
|
+
run: (el, opts) => animate(el, [
|
|
123
|
+
{ transform: 'scale3d(1,1,1)', opacity: '1' },
|
|
124
|
+
{ transform: 'scale3d(0.5,0.5,1)', opacity: '0' },
|
|
125
|
+
], opts),
|
|
126
|
+
},
|
|
127
|
+
'flip-x': {
|
|
128
|
+
path: 'waapi', duration: 500,
|
|
129
|
+
run: (el, opts) => {
|
|
130
|
+
prepareForAnimation(el, ['transform', 'opacity']);
|
|
131
|
+
return animate(el, [
|
|
132
|
+
{ transform: 'perspective(400px) rotateX(90deg)', opacity: '0' },
|
|
133
|
+
{ transform: 'perspective(400px) rotateX(-10deg)', opacity: '1', offset: 0.6 },
|
|
134
|
+
{ transform: 'perspective(400px) rotateX(0deg)', opacity: '1' },
|
|
135
|
+
], opts);
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
'flip-y': {
|
|
139
|
+
path: 'waapi', duration: 500,
|
|
140
|
+
run: (el, opts) => {
|
|
141
|
+
prepareForAnimation(el, ['transform', 'opacity']);
|
|
142
|
+
return animate(el, [
|
|
143
|
+
{ transform: 'perspective(400px) rotateY(90deg)', opacity: '0' },
|
|
144
|
+
{ transform: 'perspective(400px) rotateY(-10deg)', opacity: '1', offset: 0.6 },
|
|
145
|
+
{ transform: 'perspective(400px) rotateY(0deg)', opacity: '1' },
|
|
146
|
+
], opts);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
// ── Attention seekers (WAAPI - brief GPU motion) ─────────────────────────
|
|
150
|
+
'pulse': {
|
|
151
|
+
path: 'waapi', duration: 600,
|
|
152
|
+
run: (el, opts) => {
|
|
153
|
+
prepareForAnimation(el, ['transform']);
|
|
154
|
+
return animate(el, [
|
|
155
|
+
{ transform: 'scale3d(1,1,1)' },
|
|
156
|
+
{ transform: 'scale3d(1.05,1.05,1)', offset: 0.5 },
|
|
157
|
+
{ transform: 'scale3d(1,1,1)' },
|
|
158
|
+
], opts);
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
'shake': {
|
|
162
|
+
path: 'waapi', duration: 500,
|
|
163
|
+
run: (el, opts) => {
|
|
164
|
+
prepareForAnimation(el, ['transform']);
|
|
165
|
+
return animate(el, [
|
|
166
|
+
{ transform: 'translate3d(0,0,0)' },
|
|
167
|
+
{ transform: 'translate3d(-8px,0,0)', offset: 0.1 },
|
|
168
|
+
{ transform: 'translate3d(8px,0,0)', offset: 0.3 },
|
|
169
|
+
{ transform: 'translate3d(-8px,0,0)', offset: 0.5 },
|
|
170
|
+
{ transform: 'translate3d(8px,0,0)', offset: 0.7 },
|
|
171
|
+
{ transform: 'translate3d(-4px,0,0)', offset: 0.9 },
|
|
172
|
+
{ transform: 'translate3d(0,0,0)' },
|
|
173
|
+
], { ...opts, easing: 'ease-in-out' });
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
'bounce': {
|
|
177
|
+
path: 'waapi', duration: 800,
|
|
178
|
+
run: (el, opts) => {
|
|
179
|
+
prepareForAnimation(el, ['transform']);
|
|
180
|
+
return animate(el, [
|
|
181
|
+
{ transform: 'translate3d(0,0,0)', animationTimingFunction: 'cubic-bezier(0.8,0,1,1)' },
|
|
182
|
+
{ transform: 'translate3d(0,-30px,0)', offset: 0.4, animationTimingFunction: 'cubic-bezier(0,0,0.2,1)' },
|
|
183
|
+
{ transform: 'translate3d(0,0,0)', offset: 0.6, animationTimingFunction: 'cubic-bezier(0.8,0,1,1)' },
|
|
184
|
+
{ transform: 'translate3d(0,-15px,0)', offset: 0.8, animationTimingFunction: 'cubic-bezier(0,0,0.2,1)' },
|
|
185
|
+
{ transform: 'translate3d(0,0,0)' },
|
|
186
|
+
], { ...opts, easing: 'linear' });
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
'rubber-band': {
|
|
190
|
+
path: 'waapi', duration: 700,
|
|
191
|
+
run: (el, opts) => {
|
|
192
|
+
prepareForAnimation(el, ['transform']);
|
|
193
|
+
return animate(el, [
|
|
194
|
+
{ transform: 'scale3d(1,1,1)' },
|
|
195
|
+
{ transform: 'scale3d(1.25,0.75,1)', offset: 0.3 },
|
|
196
|
+
{ transform: 'scale3d(0.75,1.25,1)', offset: 0.5 },
|
|
197
|
+
{ transform: 'scale3d(1.15,0.85,1)', offset: 0.65 },
|
|
198
|
+
{ transform: 'scale3d(0.95,1.05,1)', offset: 0.75 },
|
|
199
|
+
{ transform: 'scale3d(1.05,0.95,1)', offset: 0.9 },
|
|
200
|
+
{ transform: 'scale3d(1,1,1)' },
|
|
201
|
+
], { ...opts, easing: 'ease-in-out' });
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
'swing': {
|
|
205
|
+
path: 'waapi', duration: 700,
|
|
206
|
+
run: (el, opts) => {
|
|
207
|
+
prepareForAnimation(el, ['transform']);
|
|
208
|
+
return animate(el, [
|
|
209
|
+
{ transform: 'rotate3d(0,0,1,0deg)' },
|
|
210
|
+
{ transform: 'rotate3d(0,0,1,15deg)', offset: 0.2 },
|
|
211
|
+
{ transform: 'rotate3d(0,0,1,-10deg)', offset: 0.4 },
|
|
212
|
+
{ transform: 'rotate3d(0,0,1,5deg)', offset: 0.6 },
|
|
213
|
+
{ transform: 'rotate3d(0,0,1,-5deg)', offset: 0.8 },
|
|
214
|
+
{ transform: 'rotate3d(0,0,1,0deg)' },
|
|
215
|
+
], { ...opts, easing: 'ease-in-out' });
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
'jello': {
|
|
219
|
+
path: 'waapi', duration: 900,
|
|
220
|
+
run: (el, opts) => {
|
|
221
|
+
prepareForAnimation(el, ['transform']);
|
|
222
|
+
return animate(el, [
|
|
223
|
+
{ transform: 'skewX(0) skewY(0)' },
|
|
224
|
+
{ transform: 'skewX(-12.5deg) skewY(-12.5deg)', offset: 0.11 },
|
|
225
|
+
{ transform: 'skewX(6.25deg) skewY(6.25deg)', offset: 0.22 },
|
|
226
|
+
{ transform: 'skewX(-3.125deg) skewY(-3.125deg)', offset: 0.33 },
|
|
227
|
+
{ transform: 'skewX(1.5625deg) skewY(1.5625deg)', offset: 0.44 },
|
|
228
|
+
{ transform: 'skewX(-0.78125deg) skewY(-0.78125deg)', offset: 0.55 },
|
|
229
|
+
{ transform: 'skewX(0.390625deg) skewY(0.390625deg)', offset: 0.66 },
|
|
230
|
+
{ transform: 'skewX(-0.1953125deg) skewY(-0.1953125deg)', offset: 0.77 },
|
|
231
|
+
{ transform: 'skewX(0) skewY(0)' },
|
|
232
|
+
], { ...opts, easing: 'ease-in-out' });
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
'tada': {
|
|
236
|
+
path: 'waapi', duration: 800,
|
|
237
|
+
run: (el, opts) => {
|
|
238
|
+
prepareForAnimation(el, ['transform']);
|
|
239
|
+
return animate(el, [
|
|
240
|
+
{ transform: 'scale3d(1,1,1) rotate3d(0,0,1,0deg)' },
|
|
241
|
+
{ transform: 'scale3d(0.9,0.9,0.9) rotate3d(0,0,1,-3deg)', offset: 0.1 },
|
|
242
|
+
{ transform: 'scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg)', offset: 0.3 },
|
|
243
|
+
{ transform: 'scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg)', offset: 0.5 },
|
|
244
|
+
{ transform: 'scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg)', offset: 0.7 },
|
|
245
|
+
{ transform: 'scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg)', offset: 0.8 },
|
|
246
|
+
{ transform: 'scale3d(1,1,1) rotate3d(0,0,1,0deg)' },
|
|
247
|
+
], { ...opts, easing: 'ease-in-out' });
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
'heartbeat': {
|
|
251
|
+
path: 'waapi', duration: 600,
|
|
252
|
+
run: (el, opts) => {
|
|
253
|
+
prepareForAnimation(el, ['transform']);
|
|
254
|
+
return animate(el, [
|
|
255
|
+
{ transform: 'scale3d(1,1,1)' },
|
|
256
|
+
{ transform: 'scale3d(1.15,1.15,1)', offset: 0.14 },
|
|
257
|
+
{ transform: 'scale3d(1,1,1)', offset: 0.28 },
|
|
258
|
+
{ transform: 'scale3d(1.15,1.15,1)', offset: 0.42 },
|
|
259
|
+
{ transform: 'scale3d(1,1,1)' },
|
|
260
|
+
], { ...opts, easing: 'ease-in-out' });
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
// ── Continuous / looping (CSS - compositor-only, zero rAF cost) ──────────
|
|
264
|
+
'spin': {
|
|
265
|
+
path: 'css', duration: 1000,
|
|
266
|
+
keyframes: `from { transform: rotate(0deg); } to { transform: rotate(360deg); }`,
|
|
267
|
+
},
|
|
268
|
+
'ping': {
|
|
269
|
+
path: 'css', duration: 1000,
|
|
270
|
+
keyframes: `
|
|
271
|
+
0% { transform: scale3d(1,1,1); opacity: 1; }
|
|
272
|
+
75%, 100% { transform: scale3d(2,2,1); opacity: 0; }
|
|
273
|
+
`,
|
|
274
|
+
},
|
|
275
|
+
'float': {
|
|
276
|
+
path: 'css', duration: 3000,
|
|
277
|
+
easing: 'ease-in-out',
|
|
278
|
+
keyframes: `
|
|
279
|
+
0%, 100% { transform: translate3d(0,0,0); }
|
|
280
|
+
50% { transform: translate3d(0,-12px,0); }
|
|
281
|
+
`,
|
|
282
|
+
},
|
|
283
|
+
'glow': {
|
|
284
|
+
path: 'css', duration: 2000,
|
|
285
|
+
easing: 'ease-in-out',
|
|
286
|
+
keyframes: `
|
|
287
|
+
0%, 100% { filter: brightness(1) drop-shadow(0 0 0px currentColor); }
|
|
288
|
+
50% { filter: brightness(1.2) drop-shadow(0 0 8px currentColor); }
|
|
289
|
+
`,
|
|
290
|
+
},
|
|
291
|
+
// ── Particle system (WebGL → canvas2d fallback) ───────────────────────────
|
|
292
|
+
'confetti': {
|
|
293
|
+
path: 'particle', duration: 3000,
|
|
294
|
+
particle: {
|
|
295
|
+
count: 120,
|
|
296
|
+
colors: ['#f43f5e', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899'],
|
|
297
|
+
size: { min: 4, max: 10 },
|
|
298
|
+
speed: { min: 60, max: 160 },
|
|
299
|
+
type: 'shower',
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
'sparkles': {
|
|
303
|
+
path: 'particle', duration: 2000,
|
|
304
|
+
particle: {
|
|
305
|
+
count: 60,
|
|
306
|
+
colors: ['#fcd34d', '#fbbf24', '#fde68a', '#fff'],
|
|
307
|
+
size: { min: 2, max: 6 },
|
|
308
|
+
speed: { min: 40, max: 100 },
|
|
309
|
+
type: 'burst',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
'bubbles': {
|
|
313
|
+
path: 'particle', duration: 4000,
|
|
314
|
+
particle: {
|
|
315
|
+
count: 50,
|
|
316
|
+
colors: ['#93c5fd', '#bfdbfe', '#dbeafe'],
|
|
317
|
+
size: { min: 6, max: 18 },
|
|
318
|
+
speed: { min: 20, max: 60 },
|
|
319
|
+
type: 'float',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
'snow': {
|
|
323
|
+
path: 'particle', duration: 5000,
|
|
324
|
+
particle: {
|
|
325
|
+
count: 80,
|
|
326
|
+
colors: ['#e0f2fe', '#bae6fd', '#fff'],
|
|
327
|
+
size: { min: 3, max: 8 },
|
|
328
|
+
speed: { min: 20, max: 50 },
|
|
329
|
+
type: 'shower',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
'firework': {
|
|
333
|
+
path: 'particle', duration: 3000,
|
|
334
|
+
particle: {
|
|
335
|
+
count: 120,
|
|
336
|
+
colors: ['#f43f5e', '#f59e0b', '#fcd34d', '#34d399', '#60a5fa', '#c084fc', '#fff'],
|
|
337
|
+
size: { min: 2, max: 5 },
|
|
338
|
+
speed: { min: 280, max: 460 },
|
|
339
|
+
type: 'firework',
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
// ── New presets ───────────────────────────────────────────────────────────
|
|
343
|
+
'electricity': {
|
|
344
|
+
path: 'particle', duration: 2000,
|
|
345
|
+
particle: {
|
|
346
|
+
count: 5, // number of simultaneous bolts
|
|
347
|
+
colors: ['#a5f3fc', '#e0f2fe', '#7dd3fc', '#ffffff'],
|
|
348
|
+
size: { min: 1, max: 2 },
|
|
349
|
+
speed: { min: 0, max: 0 },
|
|
350
|
+
type: 'electricity',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
'fire': {
|
|
354
|
+
path: 'particle', duration: 3000,
|
|
355
|
+
particle: {
|
|
356
|
+
count: 120,
|
|
357
|
+
colors: ['#ef4444', '#f97316', '#fbbf24', '#fde68a'],
|
|
358
|
+
size: { min: 8, max: 22 },
|
|
359
|
+
speed: { min: 80, max: 180 },
|
|
360
|
+
type: 'fire',
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
'explosion': {
|
|
364
|
+
path: 'particle', duration: 2000,
|
|
365
|
+
particle: {
|
|
366
|
+
count: 110,
|
|
367
|
+
colors: ['#ef4444', '#f97316', '#fbbf24', '#fde68a', '#fff'],
|
|
368
|
+
size: { min: 4, max: 16 },
|
|
369
|
+
speed: { min: 90, max: 360 },
|
|
370
|
+
type: 'explosion',
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
'ripple': {
|
|
374
|
+
path: 'particle', duration: 2500,
|
|
375
|
+
particle: {
|
|
376
|
+
count: 5, // number of concentric rings
|
|
377
|
+
colors: ['#3b82f6', '#60a5fa', '#93c5fd', '#bfdbfe'],
|
|
378
|
+
size: { min: 2, max: 4 },
|
|
379
|
+
speed: { min: 0, max: 0 },
|
|
380
|
+
type: 'ripple',
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
385
|
+
// Global set - keyframes injected into document.head, deduplicated across all instances
|
|
386
|
+
const _injectedKeyframes = new Set();
|
|
387
|
+
export class NcAnimation extends Component {
|
|
388
|
+
static useShadowDOM = true;
|
|
389
|
+
static get observedAttributes() {
|
|
390
|
+
return ['name', 'trigger', 'duration', 'delay', 'easing', 'iterations',
|
|
391
|
+
'distance', 'threshold', 'fill', 'reverse', 'no-gpu-hint',
|
|
392
|
+
'origin-x', 'origin-y', 'target-x', 'target-y', 'count', 'spread'];
|
|
393
|
+
}
|
|
394
|
+
// active Web Animation handle - lets us pause / cancel
|
|
395
|
+
_waAnimation = null;
|
|
396
|
+
// particle loop + webgl handles
|
|
397
|
+
_particleLoop = null;
|
|
398
|
+
_particleWebGL = null;
|
|
399
|
+
_canvas = null;
|
|
400
|
+
// IntersectionObserver for trigger=visible
|
|
401
|
+
_io = null;
|
|
402
|
+
// tear-down refs
|
|
403
|
+
_hoverOff = null;
|
|
404
|
+
_clickOff = null;
|
|
405
|
+
// css animation class injected into shadow
|
|
406
|
+
_cssAnimName = '';
|
|
407
|
+
// track whether IntersectionObserver already fired
|
|
408
|
+
_visibleFired = false;
|
|
409
|
+
template() {
|
|
410
|
+
return `
|
|
411
|
+
<style>
|
|
412
|
+
:host {
|
|
413
|
+
display: contents;
|
|
414
|
+
}
|
|
415
|
+
.wrap {
|
|
416
|
+
display: contents;
|
|
417
|
+
}
|
|
418
|
+
.canvas-layer {
|
|
419
|
+
position: absolute;
|
|
420
|
+
inset: 0;
|
|
421
|
+
pointer-events: none;
|
|
422
|
+
z-index: 10;
|
|
423
|
+
}
|
|
424
|
+
/* Will be extended dynamically per CSS preset */
|
|
425
|
+
</style>
|
|
426
|
+
<div class="wrap"><slot></slot></div>
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
onMount() {
|
|
430
|
+
this._setup();
|
|
431
|
+
}
|
|
432
|
+
onUnmount() {
|
|
433
|
+
this._teardown();
|
|
434
|
+
}
|
|
435
|
+
// ── Public methods ────────────────────────────────────────────────────────
|
|
436
|
+
play() { this._run(); }
|
|
437
|
+
pause() {
|
|
438
|
+
if (this._waAnimation && this._waAnimation.playState === 'running') {
|
|
439
|
+
this._waAnimation.pause();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
cancel() {
|
|
443
|
+
this._waAnimation?.cancel();
|
|
444
|
+
this._waAnimation = null;
|
|
445
|
+
this._stopParticles();
|
|
446
|
+
const target = this._target();
|
|
447
|
+
if (target) {
|
|
448
|
+
cleanupAnimation(target);
|
|
449
|
+
target.style.cssText = target.style.cssText.replace(/animation[^;]*;?/g, '');
|
|
450
|
+
}
|
|
451
|
+
this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }));
|
|
452
|
+
}
|
|
453
|
+
// ── Setup ─────────────────────────────────────────────────────────────────
|
|
454
|
+
_setup() {
|
|
455
|
+
const trigger = this._attr('trigger', 'mount');
|
|
456
|
+
switch (trigger) {
|
|
457
|
+
case 'mount':
|
|
458
|
+
this._scheduleRun();
|
|
459
|
+
break;
|
|
460
|
+
case 'visible':
|
|
461
|
+
this._setupVisibleTrigger();
|
|
462
|
+
break;
|
|
463
|
+
case 'hover':
|
|
464
|
+
this._setupHoverTrigger();
|
|
465
|
+
break;
|
|
466
|
+
case 'click':
|
|
467
|
+
this._setupClickTrigger();
|
|
468
|
+
break;
|
|
469
|
+
// 'manual' - nothing; caller calls .play()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
_teardown() {
|
|
473
|
+
this._io?.disconnect();
|
|
474
|
+
this._io = null;
|
|
475
|
+
this._hoverOff?.();
|
|
476
|
+
this._hoverOff = null;
|
|
477
|
+
this._clickOff?.();
|
|
478
|
+
this._clickOff = null;
|
|
479
|
+
this._waAnimation?.cancel();
|
|
480
|
+
this._waAnimation = null;
|
|
481
|
+
this._stopParticles();
|
|
482
|
+
const target = this._target();
|
|
483
|
+
if (target)
|
|
484
|
+
cleanupAnimation(target);
|
|
485
|
+
}
|
|
486
|
+
_scheduleRun() {
|
|
487
|
+
const delay = this._numAttr('delay', 0);
|
|
488
|
+
if (delay > 0) {
|
|
489
|
+
setTimeout(() => this._run(), delay);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// defer one tick so the slot is painted
|
|
493
|
+
requestAnimationFrame(() => this._run());
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
_setupVisibleTrigger() {
|
|
497
|
+
const threshold = parseFloat(this.getAttribute('threshold') || '0.15');
|
|
498
|
+
this._io = new IntersectionObserver((entries) => {
|
|
499
|
+
for (const entry of entries) {
|
|
500
|
+
if (entry.isIntersecting && !this._visibleFired) {
|
|
501
|
+
this._visibleFired = true;
|
|
502
|
+
this._scheduleRun();
|
|
503
|
+
this._io?.disconnect();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}, { threshold });
|
|
507
|
+
this._io.observe(this);
|
|
508
|
+
}
|
|
509
|
+
_setupHoverTrigger() {
|
|
510
|
+
const onEnter = () => { this._visibleFired = false; this._run(); };
|
|
511
|
+
this.addEventListener('mouseenter', onEnter);
|
|
512
|
+
this._hoverOff = () => this.removeEventListener('mouseenter', onEnter);
|
|
513
|
+
}
|
|
514
|
+
_setupClickTrigger() {
|
|
515
|
+
const onClick = () => { this._visibleFired = false; this._run(); };
|
|
516
|
+
this.addEventListener('click', onClick);
|
|
517
|
+
this._clickOff = () => this.removeEventListener('click', onClick);
|
|
518
|
+
}
|
|
519
|
+
// ── Core run dispatcher ───────────────────────────────────────────────────
|
|
520
|
+
_run() {
|
|
521
|
+
const name = this._attr('name', 'fade-in');
|
|
522
|
+
const preset = PRESETS[name];
|
|
523
|
+
if (!preset) {
|
|
524
|
+
console.warn(`[nc-animation] Unknown preset: "${name}"`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
this.dispatchEvent(new CustomEvent('start', { bubbles: true, composed: true }));
|
|
528
|
+
switch (preset.path) {
|
|
529
|
+
case 'waapi':
|
|
530
|
+
this._runWAAPI(preset);
|
|
531
|
+
break;
|
|
532
|
+
case 'css':
|
|
533
|
+
this._runCSS(preset, name);
|
|
534
|
+
break;
|
|
535
|
+
case 'particle':
|
|
536
|
+
this._runParticles(preset);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// ── WAAPI path ────────────────────────────────────────────────────────────
|
|
541
|
+
_runWAAPI(preset) {
|
|
542
|
+
const target = this._target();
|
|
543
|
+
if (!target || !preset.run)
|
|
544
|
+
return;
|
|
545
|
+
const noHint = this.hasAttribute('no-gpu-hint');
|
|
546
|
+
if (!noHint)
|
|
547
|
+
prepareForAnimation(target, ['transform', 'opacity']);
|
|
548
|
+
const opts = {
|
|
549
|
+
duration: this._numAttr('duration', preset.duration),
|
|
550
|
+
delay: this._numAttr('delay', 0),
|
|
551
|
+
easing: this.getAttribute('easing') ?? preset.easing ?? 'cubic-bezier(0.4,0,0.2,1)',
|
|
552
|
+
fill: this.getAttribute('fill') ?? 'forwards',
|
|
553
|
+
iterations: this._iterAttr(),
|
|
554
|
+
distance: this._numAttr('distance', 40),
|
|
555
|
+
};
|
|
556
|
+
// Capture the animation handle so pause/cancel work
|
|
557
|
+
// We monkey-patch by running and grabbing the animation from the element
|
|
558
|
+
const before = target.getAnimations().length;
|
|
559
|
+
preset.run(target, opts).then(() => {
|
|
560
|
+
if (!noHint && opts.iterations === 1)
|
|
561
|
+
cleanupAnimation(target);
|
|
562
|
+
this.dispatchEvent(new CustomEvent('finish', { bubbles: true, composed: true }));
|
|
563
|
+
});
|
|
564
|
+
// Grab the newest animation handle
|
|
565
|
+
requestAnimationFrame(() => {
|
|
566
|
+
const anims = target.getAnimations();
|
|
567
|
+
if (anims.length > before) {
|
|
568
|
+
this._waAnimation = anims[anims.length - 1];
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
// ── CSS compositor path ───────────────────────────────────────────────────
|
|
573
|
+
_runCSS(preset, name) {
|
|
574
|
+
const target = this._target();
|
|
575
|
+
if (!target || !preset.keyframes)
|
|
576
|
+
return;
|
|
577
|
+
const animName = `nc-anim-${name}`;
|
|
578
|
+
// Inject keyframes into document.head (NOT shadow root) - slotted targets
|
|
579
|
+
// are light DOM elements; shadow-scoped @keyframes are invisible to them.
|
|
580
|
+
if (!_injectedKeyframes.has(animName)) {
|
|
581
|
+
const style = document.createElement('style');
|
|
582
|
+
style.id = `kf-${animName}`;
|
|
583
|
+
style.textContent = `@keyframes ${animName} { ${preset.keyframes} }`;
|
|
584
|
+
document.head.appendChild(style);
|
|
585
|
+
_injectedKeyframes.add(animName);
|
|
586
|
+
}
|
|
587
|
+
const duration = this._numAttr('duration', preset.duration);
|
|
588
|
+
const delay = this._numAttr('delay', 0);
|
|
589
|
+
const easing = this.getAttribute('easing') ?? preset.easing ?? 'ease-in-out';
|
|
590
|
+
const iterations = this.getAttribute('iterations') === 'infinite' ? 'infinite' : this._iterAttr();
|
|
591
|
+
const fill = this.getAttribute('fill') ?? (iterations === 'infinite' ? 'none' : 'forwards');
|
|
592
|
+
const direction = this.hasAttribute('reverse') ? 'reverse' : 'normal';
|
|
593
|
+
target.style.willChange = 'transform, opacity';
|
|
594
|
+
target.style.animation = `${animName} ${duration}ms ${easing} ${delay}ms ${iterations} ${fill} ${direction}`;
|
|
595
|
+
if (iterations !== 'infinite') {
|
|
596
|
+
target.addEventListener('animationend', () => {
|
|
597
|
+
cleanupAnimation(target);
|
|
598
|
+
this.dispatchEvent(new CustomEvent('finish', { bubbles: true, composed: true }));
|
|
599
|
+
}, { once: true });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// ── Particle path ─────────────────────────────────────────────────────────
|
|
603
|
+
/** Resolve 'top'|'bottom'|'left'|'right'|'center'|number-string → 0-1 float */
|
|
604
|
+
_resolvePos(raw, fallback) {
|
|
605
|
+
if (!raw)
|
|
606
|
+
return fallback;
|
|
607
|
+
const aliases = { top: 0, bottom: 1, left: 0, right: 1, center: 0.5 };
|
|
608
|
+
if (raw in aliases)
|
|
609
|
+
return aliases[raw];
|
|
610
|
+
const n = parseFloat(raw);
|
|
611
|
+
return isNaN(n) ? fallback : Math.max(0, Math.min(1, n));
|
|
612
|
+
}
|
|
613
|
+
_runParticles(preset) {
|
|
614
|
+
if (!preset.particle)
|
|
615
|
+
return;
|
|
616
|
+
this._stopParticles();
|
|
617
|
+
// Full-viewport canvas - particles live in screen space, not element space
|
|
618
|
+
const canvas = document.createElement('canvas');
|
|
619
|
+
canvas.width = window.innerWidth;
|
|
620
|
+
canvas.height = window.innerHeight;
|
|
621
|
+
canvas.style.cssText =
|
|
622
|
+
'position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:9999;';
|
|
623
|
+
document.body.appendChild(canvas);
|
|
624
|
+
this._canvas = canvas;
|
|
625
|
+
// Resolve origin from attributes (default center of screen)
|
|
626
|
+
const ox = this._resolvePos(this.getAttribute('origin-x'), 0.5);
|
|
627
|
+
const oy = this._resolvePos(this.getAttribute('origin-y'), 0.5);
|
|
628
|
+
const tx = this._resolvePos(this.getAttribute('target-x'), 0.5);
|
|
629
|
+
const ty = this._resolvePos(this.getAttribute('target-y'), 0.5);
|
|
630
|
+
// Allow attribute overrides on count and spread
|
|
631
|
+
const countOverride = this.getAttribute('count');
|
|
632
|
+
const spreadScale = parseFloat(this.getAttribute('spread') ?? '1');
|
|
633
|
+
const cfg = {
|
|
634
|
+
...preset.particle,
|
|
635
|
+
count: countOverride ? parseInt(countOverride, 10) : preset.particle.count,
|
|
636
|
+
speed: preset.particle.speed
|
|
637
|
+
? { min: preset.particle.speed.min * spreadScale, max: preset.particle.speed.max * spreadScale }
|
|
638
|
+
: { min: 40, max: 120 },
|
|
639
|
+
};
|
|
640
|
+
// Always use canvas2d - the WebGL system from gpu-animation doesn't
|
|
641
|
+
// support per-preset behaviors (electricity, fire, ripple etc.)
|
|
642
|
+
this._runCanvas2D(canvas, cfg, ox, oy, tx, ty, preset);
|
|
643
|
+
const duration = this._numAttr('duration', preset.duration);
|
|
644
|
+
setTimeout(() => {
|
|
645
|
+
this._stopParticles();
|
|
646
|
+
this.dispatchEvent(new CustomEvent('finish', { bubbles: true, composed: true }));
|
|
647
|
+
}, duration);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Full canvas2d particle engine.
|
|
651
|
+
* origin / target are 0-1 fractions of canvas dimensions.
|
|
652
|
+
* Each preset type gets its own spawn + update behaviour.
|
|
653
|
+
*/
|
|
654
|
+
_runCanvas2D(canvas, config, ox, oy, tx, ty, preset) {
|
|
655
|
+
const ctx = canvas.getContext('2d');
|
|
656
|
+
if (!ctx)
|
|
657
|
+
return;
|
|
658
|
+
const W = canvas.width;
|
|
659
|
+
const H = canvas.height;
|
|
660
|
+
const originX = ox * W;
|
|
661
|
+
const originY = oy * H;
|
|
662
|
+
const targetX = tx * W;
|
|
663
|
+
const targetY = ty * H;
|
|
664
|
+
const { count, colors = ['#667eea'], size = { min: 3, max: 8 }, speed = { min: 40, max: 120 }, type = 'burst' } = config;
|
|
665
|
+
const rnd = (min, max) => min + Math.random() * (max - min);
|
|
666
|
+
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
667
|
+
const hex2rgb = (hex) => {
|
|
668
|
+
const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
669
|
+
return r ? [parseInt(r[1], 16), parseInt(r[2], 16), parseInt(r[3], 16)] : [255, 255, 255];
|
|
670
|
+
};
|
|
671
|
+
const particles = [];
|
|
672
|
+
// ── Spawn logic per type ─────────────────────────────────────────────
|
|
673
|
+
const spawnParticle = (i) => {
|
|
674
|
+
const t = count > 1 ? i / (count - 1) : 0.5;
|
|
675
|
+
switch (type) {
|
|
676
|
+
case 'shower': {
|
|
677
|
+
// rain from origin-x spread across top edge
|
|
678
|
+
const spread = W * 0.6;
|
|
679
|
+
return {
|
|
680
|
+
x: originX + (Math.random() - 0.5) * spread,
|
|
681
|
+
y: originY * H < H * 0.3 ? -10 : originY * H,
|
|
682
|
+
vx: rnd(-30, 30),
|
|
683
|
+
vy: speed.min + Math.random() * (speed.max - speed.min),
|
|
684
|
+
ax: 0, ay: 60,
|
|
685
|
+
size: rnd(size.min, size.max),
|
|
686
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
687
|
+
rot: Math.random() * Math.PI * 2,
|
|
688
|
+
rotV: rnd(-3, 3),
|
|
689
|
+
shape: 'rect',
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
case 'burst': {
|
|
693
|
+
const angle = t * Math.PI * 2;
|
|
694
|
+
const spd = rnd(speed.min, speed.max);
|
|
695
|
+
return {
|
|
696
|
+
x: originX, y: originY,
|
|
697
|
+
vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd,
|
|
698
|
+
ax: 0, ay: 20,
|
|
699
|
+
size: rnd(size.min, size.max),
|
|
700
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
701
|
+
rot: 0, rotV: rnd(-5, 5), shape: 'circle',
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
case 'explode': {
|
|
705
|
+
const angle = Math.random() * Math.PI * 2;
|
|
706
|
+
const spd = rnd(speed.min, speed.max);
|
|
707
|
+
return {
|
|
708
|
+
x: originX, y: originY,
|
|
709
|
+
vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd,
|
|
710
|
+
ax: 0, ay: 120,
|
|
711
|
+
size: rnd(size.min, size.max),
|
|
712
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
713
|
+
rot: Math.random() * Math.PI * 2,
|
|
714
|
+
rotV: rnd(-8, 8), shape: 'rect',
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
case 'float': {
|
|
718
|
+
return {
|
|
719
|
+
x: originX + (Math.random() - 0.5) * W * 0.8,
|
|
720
|
+
y: H + rnd(0, H * 0.2),
|
|
721
|
+
vx: rnd(-20, 20),
|
|
722
|
+
vy: -(speed.min + Math.random() * (speed.max - speed.min)),
|
|
723
|
+
ax: Math.sin(i) * 5, ay: 0,
|
|
724
|
+
size: rnd(size.min, size.max),
|
|
725
|
+
color: pick(colors), alpha: 0.7, life: 1, maxLife: 1,
|
|
726
|
+
shape: 'circle',
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
case 'spiral': {
|
|
730
|
+
const a = t * Math.PI * 8;
|
|
731
|
+
const r = t * Math.min(W, H) * 0.4;
|
|
732
|
+
const spd = rnd(speed.min, speed.max);
|
|
733
|
+
return {
|
|
734
|
+
x: originX + Math.cos(a) * r,
|
|
735
|
+
y: originY + Math.sin(a) * r,
|
|
736
|
+
vx: Math.cos(a + Math.PI / 2) * spd,
|
|
737
|
+
vy: Math.sin(a + Math.PI / 2) * spd,
|
|
738
|
+
ax: 0, ay: 0,
|
|
739
|
+
size: rnd(size.min, size.max),
|
|
740
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
741
|
+
shape: 'circle',
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
case 'converge': {
|
|
745
|
+
const angle = Math.random() * Math.PI * 2;
|
|
746
|
+
const dist = rnd(Math.min(W, H) * 0.3, Math.min(W, H) * 0.6);
|
|
747
|
+
const sx = targetX + Math.cos(angle) * dist;
|
|
748
|
+
const sy = targetY + Math.sin(angle) * dist;
|
|
749
|
+
const spd = rnd(speed.min, speed.max);
|
|
750
|
+
const dx = targetX - sx, dy = targetY - sy;
|
|
751
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
752
|
+
return {
|
|
753
|
+
x: sx, y: sy,
|
|
754
|
+
vx: (dx / len) * spd, vy: (dy / len) * spd,
|
|
755
|
+
ax: 0, ay: 0,
|
|
756
|
+
size: rnd(size.min, size.max),
|
|
757
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
758
|
+
shape: 'circle',
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
case 'firework': {
|
|
762
|
+
// Particles burst from random cluster positions across upper screen
|
|
763
|
+
const clusterSize = 20;
|
|
764
|
+
const clusterIdx = Math.floor(i / clusterSize);
|
|
765
|
+
const totalClusters = Math.max(1, Math.ceil(count / clusterSize));
|
|
766
|
+
// Spread bursts evenly across horizontal, upper 55% of screen
|
|
767
|
+
const burstX = W * (0.1 + (clusterIdx / Math.max(totalClusters - 1, 1)) * 0.8);
|
|
768
|
+
const burstY = H * (0.1 + Math.random() * 0.35);
|
|
769
|
+
const angle = ((i % clusterSize) / clusterSize) * Math.PI * 2 + (Math.random() - 0.5) * 0.4;
|
|
770
|
+
const spd = rnd(speed.min, speed.max);
|
|
771
|
+
return {
|
|
772
|
+
x: burstX, y: burstY,
|
|
773
|
+
vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd,
|
|
774
|
+
ax: 0, ay: 80,
|
|
775
|
+
size: rnd(size.min, size.max),
|
|
776
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
777
|
+
shape: 'circle',
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
case 'explosion': {
|
|
781
|
+
// First 3 are expanding shockwave rings, rest are debris
|
|
782
|
+
if (i < 4) {
|
|
783
|
+
return {
|
|
784
|
+
x: originX, y: originY,
|
|
785
|
+
vx: 0, vy: 0, ax: 0, ay: 0,
|
|
786
|
+
size: 3 + i * 2,
|
|
787
|
+
color: pick(['#fbbf24', '#fde68a', '#fff']),
|
|
788
|
+
alpha: 0.85, life: 1, maxLife: 1,
|
|
789
|
+
wave: i * 0.12,
|
|
790
|
+
shape: 'arc',
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
const angle = Math.random() * Math.PI * 2;
|
|
794
|
+
const spd = rnd(speed.min, speed.max);
|
|
795
|
+
return {
|
|
796
|
+
x: originX + (Math.random() - 0.5) * 20,
|
|
797
|
+
y: originY + (Math.random() - 0.5) * 20,
|
|
798
|
+
vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd - 40,
|
|
799
|
+
ax: 0, ay: 200,
|
|
800
|
+
size: rnd(size.min, size.max),
|
|
801
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
802
|
+
shape: 'circle',
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
case 'fire': {
|
|
806
|
+
return {
|
|
807
|
+
x: originX + (Math.random() - 0.5) * size.max * 6,
|
|
808
|
+
y: originY,
|
|
809
|
+
vx: rnd(-20, 20),
|
|
810
|
+
vy: -(speed.min + Math.random() * (speed.max - speed.min)),
|
|
811
|
+
ax: rnd(-8, 8), ay: -10,
|
|
812
|
+
size: rnd(size.min, size.max),
|
|
813
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
814
|
+
alpha: 0.9, life: 1, maxLife: 1,
|
|
815
|
+
shape: 'circle',
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
case 'ripple': {
|
|
819
|
+
return {
|
|
820
|
+
x: originX, y: originY,
|
|
821
|
+
vx: 0, vy: 0, ax: 0, ay: 0,
|
|
822
|
+
size: 0,
|
|
823
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
824
|
+
wave: t * 0.5, // stagger rings by index
|
|
825
|
+
shape: 'arc',
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
default: {
|
|
829
|
+
const angle = Math.random() * Math.PI * 2;
|
|
830
|
+
const spd = rnd(speed.min, speed.max);
|
|
831
|
+
return {
|
|
832
|
+
x: originX, y: originY,
|
|
833
|
+
vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd,
|
|
834
|
+
ax: 0, ay: 30,
|
|
835
|
+
size: rnd(size.min, size.max),
|
|
836
|
+
color: pick(colors), alpha: 1, life: 1, maxLife: 1,
|
|
837
|
+
shape: 'circle',
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
// Stagger spawning for shower/fire so screen doesn't flash all at once
|
|
843
|
+
const isStaggered = type === 'shower' || type === 'fire' || type === 'float' || type === 'ripple';
|
|
844
|
+
if (isStaggered) {
|
|
845
|
+
// Spawn first 20% immediately, rest over first 40% of duration
|
|
846
|
+
const duration = this._numAttr('duration', preset.duration);
|
|
847
|
+
for (let i = 0; i < count; i++) {
|
|
848
|
+
const delay = (i / count) * duration * 0.5;
|
|
849
|
+
setTimeout(() => {
|
|
850
|
+
if (!this._canvas)
|
|
851
|
+
return;
|
|
852
|
+
particles.push(spawnParticle(i));
|
|
853
|
+
}, delay);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
for (let i = 0; i < count; i++)
|
|
858
|
+
particles.push(spawnParticle(i));
|
|
859
|
+
}
|
|
860
|
+
// ── Per-particle draw ─────────────────────────────────────────────────
|
|
861
|
+
const drawParticle = (p) => {
|
|
862
|
+
ctx.globalAlpha = Math.max(0, p.life) * (p.alpha ?? 1);
|
|
863
|
+
ctx.fillStyle = p.color;
|
|
864
|
+
ctx.strokeStyle = p.color;
|
|
865
|
+
if (p.shape === 'rect' && p.rot !== undefined) {
|
|
866
|
+
ctx.save();
|
|
867
|
+
ctx.translate(p.x, p.y);
|
|
868
|
+
ctx.rotate(p.rot);
|
|
869
|
+
ctx.fillRect(-p.size / 2, -p.size * 1.5, p.size, p.size * 3);
|
|
870
|
+
ctx.restore();
|
|
871
|
+
}
|
|
872
|
+
else if (p.shape === 'line' && p.trail && p.trail.length > 1) {
|
|
873
|
+
// Electricity bolt - draw jagged line segments from trail
|
|
874
|
+
ctx.lineWidth = p.size;
|
|
875
|
+
ctx.shadowColor = p.color;
|
|
876
|
+
ctx.shadowBlur = 12;
|
|
877
|
+
ctx.beginPath();
|
|
878
|
+
ctx.moveTo(p.trail[0][0], p.trail[0][1]);
|
|
879
|
+
for (let k = 1; k < p.trail.length; k++) {
|
|
880
|
+
ctx.lineTo(p.trail[k][0], p.trail[k][1]);
|
|
881
|
+
}
|
|
882
|
+
ctx.stroke();
|
|
883
|
+
ctx.shadowBlur = 0;
|
|
884
|
+
}
|
|
885
|
+
else if (p.shape === 'arc') {
|
|
886
|
+
// Ripple ring OR explosion shockwave
|
|
887
|
+
const maxR = type === 'explosion'
|
|
888
|
+
? Math.max(W, H) * 0.35
|
|
889
|
+
: Math.max(W, H) * 0.45;
|
|
890
|
+
const radius = (1 - p.life) * maxR + (p.size ?? 0);
|
|
891
|
+
if (radius > 0) {
|
|
892
|
+
ctx.lineWidth = type === 'explosion' ? Math.max(1, p.size * p.life * 2) : (p.size > 0 ? p.size : 2);
|
|
893
|
+
ctx.beginPath();
|
|
894
|
+
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
|
|
895
|
+
ctx.stroke();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
// Circle - fire, firework, explosion debris each get glow
|
|
900
|
+
if (type === 'fire') {
|
|
901
|
+
const [r, g, b] = hex2rgb(p.color);
|
|
902
|
+
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size);
|
|
903
|
+
grad.addColorStop(0, `rgba(${r},${g},${b},${p.life})`);
|
|
904
|
+
grad.addColorStop(0.6, `rgba(${r},${g},${b},${p.life * 0.5})`);
|
|
905
|
+
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
|
|
906
|
+
ctx.fillStyle = grad;
|
|
907
|
+
}
|
|
908
|
+
else if (type === 'firework') {
|
|
909
|
+
// Glowing spark - shrinks as it fades
|
|
910
|
+
ctx.shadowColor = p.color;
|
|
911
|
+
ctx.shadowBlur = p.size * 4;
|
|
912
|
+
const drawR = Math.max(0.5, p.size * p.life);
|
|
913
|
+
ctx.beginPath();
|
|
914
|
+
ctx.arc(p.x, p.y, drawR, 0, Math.PI * 2);
|
|
915
|
+
ctx.fill();
|
|
916
|
+
ctx.shadowBlur = 0;
|
|
917
|
+
ctx.globalAlpha = 1;
|
|
918
|
+
return; // skip the generic arc below
|
|
919
|
+
}
|
|
920
|
+
else if (type === 'explosion') {
|
|
921
|
+
const [r, g, b] = hex2rgb(p.color);
|
|
922
|
+
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size);
|
|
923
|
+
grad.addColorStop(0, `rgba(255,255,255,${p.life})`);
|
|
924
|
+
grad.addColorStop(0.35, `rgba(${r},${g},${b},${p.life})`);
|
|
925
|
+
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
|
|
926
|
+
ctx.fillStyle = grad;
|
|
927
|
+
ctx.shadowColor = p.color;
|
|
928
|
+
ctx.shadowBlur = p.size * 2;
|
|
929
|
+
}
|
|
930
|
+
ctx.beginPath();
|
|
931
|
+
ctx.arc(p.x, p.y, Math.max(0, p.size), 0, Math.PI * 2);
|
|
932
|
+
ctx.fill();
|
|
933
|
+
}
|
|
934
|
+
ctx.globalAlpha = 1;
|
|
935
|
+
ctx.shadowBlur = 0;
|
|
936
|
+
};
|
|
937
|
+
const maxElecBolts = 5;
|
|
938
|
+
let elecTimer = 0;
|
|
939
|
+
const elecInterval = 0.06; // seconds between new bolt sets
|
|
940
|
+
const loop = createAnimationLoop((dt) => {
|
|
941
|
+
ctx.clearRect(0, 0, W, H);
|
|
942
|
+
// ── Electricity: regenerate jagged bolt paths each interval ────
|
|
943
|
+
if (type === 'electricity') {
|
|
944
|
+
elecTimer += dt;
|
|
945
|
+
if (elecTimer >= elecInterval) {
|
|
946
|
+
elecTimer = 0;
|
|
947
|
+
// Re-generate all bolt trails from origin -> target
|
|
948
|
+
const activeBolts = Math.min(maxElecBolts, particles.length);
|
|
949
|
+
for (let b = 0; b < activeBolts; b++) {
|
|
950
|
+
const p = particles[b];
|
|
951
|
+
if (!p)
|
|
952
|
+
continue;
|
|
953
|
+
const steps = 12 + Math.floor(Math.random() * 8);
|
|
954
|
+
const trail = [[originX, originY]];
|
|
955
|
+
for (let s = 1; s < steps; s++) {
|
|
956
|
+
const t2 = s / steps;
|
|
957
|
+
const mx = originX + (targetX - originX) * t2;
|
|
958
|
+
const my = originY + (targetY - originY) * t2;
|
|
959
|
+
const jitter = (1 - Math.abs(t2 - 0.5) * 2) * 80;
|
|
960
|
+
trail.push([
|
|
961
|
+
mx + (Math.random() - 0.5) * jitter,
|
|
962
|
+
my + (Math.random() - 0.5) * jitter,
|
|
963
|
+
]);
|
|
964
|
+
}
|
|
965
|
+
trail.push([targetX, targetY]);
|
|
966
|
+
p.trail = trail;
|
|
967
|
+
p.life = 1;
|
|
968
|
+
p.alpha = 0.6 + Math.random() * 0.4;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
// Fade between regenerations
|
|
973
|
+
for (const p of particles)
|
|
974
|
+
p.life = Math.max(0, p.life - dt * 6);
|
|
975
|
+
}
|
|
976
|
+
for (const p of particles)
|
|
977
|
+
drawParticle(p);
|
|
978
|
+
return true; // electricity loops until duration expires
|
|
979
|
+
}
|
|
980
|
+
// ── All other types ───────────────────────────────────────────
|
|
981
|
+
let alive = 0;
|
|
982
|
+
const fadeRate = type === 'ripple' ? 0.3 : type === 'fire' ? 0.7 :
|
|
983
|
+
type === 'float' ? 0.15 :
|
|
984
|
+
type === 'firework' ? 0.42 :
|
|
985
|
+
type === 'explosion' ? 0.65 : 0.35;
|
|
986
|
+
for (const p of particles) {
|
|
987
|
+
p.vx += p.ax * dt;
|
|
988
|
+
p.vy += p.ay * dt;
|
|
989
|
+
p.x += p.vx * dt;
|
|
990
|
+
p.y += p.vy * dt;
|
|
991
|
+
if (p.rot !== undefined && p.rotV !== undefined)
|
|
992
|
+
p.rot += p.rotV * dt;
|
|
993
|
+
// Ripple: shrink size for growing ring effect is handled in draw
|
|
994
|
+
if (type !== 'ripple') {
|
|
995
|
+
p.life = Math.max(0, p.life - dt * fadeRate);
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
// stagger rings with wave offset
|
|
999
|
+
p.life = Math.max(0, p.life - dt * (0.35 + (p.wave ?? 0) * 0.1));
|
|
1000
|
+
}
|
|
1001
|
+
if (p.life <= 0.01)
|
|
1002
|
+
continue;
|
|
1003
|
+
alive++;
|
|
1004
|
+
drawParticle(p);
|
|
1005
|
+
}
|
|
1006
|
+
return alive > 0 || particles.length < count;
|
|
1007
|
+
});
|
|
1008
|
+
this._particleLoop = loop;
|
|
1009
|
+
loop.start();
|
|
1010
|
+
}
|
|
1011
|
+
_stopParticles() {
|
|
1012
|
+
this._particleWebGL?.stop();
|
|
1013
|
+
this._particleWebGL = null;
|
|
1014
|
+
this._particleLoop?.stop();
|
|
1015
|
+
this._particleLoop = null;
|
|
1016
|
+
if (this._canvas) {
|
|
1017
|
+
this._canvas.remove();
|
|
1018
|
+
this._canvas = null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
1022
|
+
/** First painted element assigned to the default slot. */
|
|
1023
|
+
_target() {
|
|
1024
|
+
const slot = this.shadowRoot?.querySelector('slot');
|
|
1025
|
+
if (!slot)
|
|
1026
|
+
return null;
|
|
1027
|
+
const nodes = slot.assignedElements({ flatten: true });
|
|
1028
|
+
return nodes[0] ?? null;
|
|
1029
|
+
}
|
|
1030
|
+
_attr(name, fallback) {
|
|
1031
|
+
return this.getAttribute(name) ?? fallback;
|
|
1032
|
+
}
|
|
1033
|
+
_numAttr(name, fallback) {
|
|
1034
|
+
const v = parseInt(this.getAttribute(name) ?? '', 10);
|
|
1035
|
+
return isNaN(v) ? fallback : v;
|
|
1036
|
+
}
|
|
1037
|
+
_iterAttr() {
|
|
1038
|
+
const raw = this.getAttribute('iterations');
|
|
1039
|
+
if (!raw || raw === 'infinite')
|
|
1040
|
+
return 1;
|
|
1041
|
+
const n = parseInt(raw, 10);
|
|
1042
|
+
return isNaN(n) ? 1 : n;
|
|
1043
|
+
}
|
|
1044
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1045
|
+
if (oldValue === newValue || !this._mounted)
|
|
1046
|
+
return;
|
|
1047
|
+
// Re-setup on any attribute change (restarts trigger logic)
|
|
1048
|
+
this._teardown();
|
|
1049
|
+
this._visibleFired = false;
|
|
1050
|
+
this._setup();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
defineComponent('nc-animation', NcAnimation);
|