ngx-astro-background-beams 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/package.json +31 -0
- package/src/BackgroundBeams.astro +505 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ngx-astro-background-beams",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Background beams effect for Astro. Port of @omnedia/ngx-background-beams without Angular runtime.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/BackgroundBeams.astro",
|
|
9
|
+
"./BackgroundBeams.astro": "./src/BackgroundBeams.astro"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"astro": ">=5.0.0"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"astro",
|
|
19
|
+
"astro-component",
|
|
20
|
+
"background",
|
|
21
|
+
"beams",
|
|
22
|
+
"effect",
|
|
23
|
+
"animation",
|
|
24
|
+
"canvas",
|
|
25
|
+
"svgl"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/whitedotjsx/ngx-ui-to-astro.git"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Exactly 3 colors for the beam gradient. */
|
|
4
|
+
gradientColorValues?: [string, string, string];
|
|
5
|
+
/** Color of the path lines. */
|
|
6
|
+
pathColor?: string;
|
|
7
|
+
/** Number of animated paths to generate. */
|
|
8
|
+
pathQuantity?: number;
|
|
9
|
+
/**
|
|
10
|
+
* false (default): 1:1 with NGXUI — animation restarts from 0 on every re-enter.
|
|
11
|
+
* true: animation pauses when leaving the viewport and resumes in place.
|
|
12
|
+
*/
|
|
13
|
+
resume?: boolean;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
gradientColorValues = ['#18CCFC', '#6344F5', '#AE48FF'],
|
|
19
|
+
pathColor = '#00000025',
|
|
20
|
+
pathQuantity = 50,
|
|
21
|
+
resume = false,
|
|
22
|
+
class: className = '',
|
|
23
|
+
} = Astro.props;
|
|
24
|
+
|
|
25
|
+
if (gradientColorValues.length !== 3) {
|
|
26
|
+
throw new Error('om-background-beams: gradient colors need to be exactly 3 values.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const uid = Math.random().toString(36).slice(2, 10);
|
|
30
|
+
const radialId = `paint0_radial_${uid}`;
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
class:list={['om-background-beams', className]}
|
|
35
|
+
data-om-beams
|
|
36
|
+
data-uid={uid}
|
|
37
|
+
data-colors={JSON.stringify(gradientColorValues)}
|
|
38
|
+
data-path-color={pathColor}
|
|
39
|
+
data-path-qty={pathQuantity}
|
|
40
|
+
data-resume={resume}
|
|
41
|
+
>
|
|
42
|
+
<div class="om-background-beams-background">
|
|
43
|
+
<div class="custom-svg-wrapper">
|
|
44
|
+
<svg
|
|
45
|
+
class="custom-svg"
|
|
46
|
+
width="100%"
|
|
47
|
+
height="100%"
|
|
48
|
+
viewBox="0 0 696 316"
|
|
49
|
+
fill="none"
|
|
50
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
51
|
+
>
|
|
52
|
+
<path
|
|
53
|
+
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875"
|
|
54
|
+
stroke={`url(#${radialId})`}
|
|
55
|
+
stroke-opacity="0.05"
|
|
56
|
+
stroke-width="0.5"
|
|
57
|
+
></path>
|
|
58
|
+
<defs>
|
|
59
|
+
<radialGradient
|
|
60
|
+
id={radialId}
|
|
61
|
+
cx="0"
|
|
62
|
+
cy="0"
|
|
63
|
+
r="1"
|
|
64
|
+
gradientUnits="userSpaceOnUse"
|
|
65
|
+
gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)"
|
|
66
|
+
>
|
|
67
|
+
<stop offset="0.0666667" stop-color="#d1d5db"></stop>
|
|
68
|
+
<stop offset="0.243243" stop-color="#d1d5db"></stop>
|
|
69
|
+
<stop offset="0.43594" stop-color="white" stop-opacity="0"></stop>
|
|
70
|
+
</radialGradient>
|
|
71
|
+
</defs>
|
|
72
|
+
</svg>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<slot />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.om-background-beams {
|
|
80
|
+
position: relative;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
width: 100%;
|
|
83
|
+
height: 100%;
|
|
84
|
+
|
|
85
|
+
.om-background-beams-background {
|
|
86
|
+
position: absolute;
|
|
87
|
+
width: 150%;
|
|
88
|
+
height: 120%;
|
|
89
|
+
top: 0;
|
|
90
|
+
left: 0;
|
|
91
|
+
pointer-events: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.custom-svg-wrapper {
|
|
95
|
+
position: absolute;
|
|
96
|
+
inset: 0;
|
|
97
|
+
display: flex;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
align-items: center;
|
|
100
|
+
mask-size: 40px;
|
|
101
|
+
mask-repeat: no-repeat;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.custom-svg {
|
|
105
|
+
position: absolute;
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
z-index: 0;
|
|
109
|
+
pointer-events: none;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
113
|
+
|
|
114
|
+
<script>
|
|
115
|
+
(() => {
|
|
116
|
+
const els = document.querySelectorAll<HTMLElement>('[data-om-beams]');
|
|
117
|
+
if (!els.length) return;
|
|
118
|
+
|
|
119
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
120
|
+
|
|
121
|
+
const firstPath = {
|
|
122
|
+
mX1: -380, mY1: -189, cX1: -380, cY1: -189, cX2: -312, cY2: 216,
|
|
123
|
+
cX3: 152, cY3: 343, cX4: 616, cY4: 470, cX5: 684, cY5: 875,
|
|
124
|
+
};
|
|
125
|
+
const lastPath = {
|
|
126
|
+
mX1: -37, mY1: -581, cX1: -37, cY1: -581, cX2: 31, cY2: -176,
|
|
127
|
+
cX3: 495, cY3: -49, cX4: 959, cY4: 78, cX5: 1027, cY5: 483,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
els.forEach((el) => {
|
|
131
|
+
const uid = el.dataset.uid!;
|
|
132
|
+
const colors = JSON.parse(el.dataset.colors!) as string[];
|
|
133
|
+
const pathColor = el.dataset.pathColor ?? '#00000025';
|
|
134
|
+
const pathQuantity = Number(el.dataset.pathQty) || 50;
|
|
135
|
+
let resumeMode = el.dataset.resume === 'true';
|
|
136
|
+
|
|
137
|
+
const svg = el.querySelector<SVGSVGElement>('svg.custom-svg')!;
|
|
138
|
+
const defs = svg.querySelector('defs')!;
|
|
139
|
+
|
|
140
|
+
// ---- Estado compartido por ambos motores ----
|
|
141
|
+
const st: {
|
|
142
|
+
inViewport: boolean;
|
|
143
|
+
x1: string[];
|
|
144
|
+
x2: string[];
|
|
145
|
+
y1: string[];
|
|
146
|
+
y2: string[];
|
|
147
|
+
delays: number[];
|
|
148
|
+
durations: number[];
|
|
149
|
+
} = {
|
|
150
|
+
inViewport: false,
|
|
151
|
+
x1: [], x2: [], y1: [], y2: [], delays: [], durations: [],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const rafIds: number[] = [];
|
|
155
|
+
const timerIds: ReturnType<typeof setTimeout>[] = [];
|
|
156
|
+
|
|
157
|
+
function clearTimers(): void {
|
|
158
|
+
timerIds.splice(0).forEach((t) => clearTimeout(t));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function clearRaf(): void {
|
|
162
|
+
rafIds.splice(0).forEach((id) => cancelAnimationFrame(id));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function setTimer(fn: () => void, ms: number): void {
|
|
166
|
+
timerIds.push(setTimeout(fn, ms));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function generatePaths(): void {
|
|
170
|
+
el.querySelectorAll('[data-om-dyn]').forEach((n) => n.remove());
|
|
171
|
+
|
|
172
|
+
st.x1 = [];
|
|
173
|
+
st.x2 = [];
|
|
174
|
+
st.y1 = [];
|
|
175
|
+
st.y2 = [];
|
|
176
|
+
st.delays = [];
|
|
177
|
+
st.durations = [];
|
|
178
|
+
|
|
179
|
+
// El haz de curvas es una traslación pura de la misma "S" a lo largo
|
|
180
|
+
// del vector (343,-392). Con t ∈ [0,1] el área visible queda sin
|
|
181
|
+
// cobertura en la esquina inferior-izquierda (y una banda vacía).
|
|
182
|
+
// Extendemos el rango a [-0.2, 1] manteniendo el paso original, lo
|
|
183
|
+
// que desplaza curvas a esa esquina sin cambiar la densidad del haz.
|
|
184
|
+
const T_MIN = -0.2;
|
|
185
|
+
const tStep = pathQuantity > 1 ? 1 / (pathQuantity - 1) : 1;
|
|
186
|
+
const count = pathQuantity > 1 ? Math.round((1 - T_MIN) / tStep) + 1 : 1;
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < count; i++) {
|
|
189
|
+
const t = count > 1 ? T_MIN + i * tStep : 0;
|
|
190
|
+
|
|
191
|
+
const mX1 = firstPath.mX1 + t * (lastPath.mX1 - firstPath.mX1);
|
|
192
|
+
const mY1 = firstPath.mY1 + t * (lastPath.mY1 - firstPath.mY1);
|
|
193
|
+
const cX1 = firstPath.cX1 + t * (lastPath.cX1 - firstPath.cX1);
|
|
194
|
+
const cY1 = firstPath.cY1 + t * (lastPath.cY1 - firstPath.cY1);
|
|
195
|
+
const cX2 = firstPath.cX2 + t * (lastPath.cX2 - firstPath.cX2);
|
|
196
|
+
const cY2 = firstPath.cY2 + t * (lastPath.cY2 - firstPath.cY2);
|
|
197
|
+
const cX3 = firstPath.cX3 + t * (lastPath.cX3 - firstPath.cX3);
|
|
198
|
+
const cY3 = firstPath.cY3 + t * (lastPath.cY3 - firstPath.cY3);
|
|
199
|
+
const cX4 = firstPath.cX4 + t * (lastPath.cX4 - firstPath.cX4);
|
|
200
|
+
const cY4 = firstPath.cY4 + t * (lastPath.cY4 - firstPath.cY4);
|
|
201
|
+
const cX5 = firstPath.cX5 + t * (lastPath.cX5 - firstPath.cX5);
|
|
202
|
+
const cY5 = firstPath.cY5 + t * (lastPath.cY5 - firstPath.cY5);
|
|
203
|
+
|
|
204
|
+
const d =
|
|
205
|
+
`M${mX1} ${mY1}` +
|
|
206
|
+
`C${cX1} ${cY1} ${cX2} ${cY2} ${cX3} ${cY3}` +
|
|
207
|
+
`C${cX4} ${cY4} ${cX5} ${cY5} ${cX5} ${cY5}`;
|
|
208
|
+
|
|
209
|
+
const p1 = document.createElementNS(SVG_NS, 'path');
|
|
210
|
+
p1.setAttribute('d', d);
|
|
211
|
+
p1.setAttribute('stroke', pathColor);
|
|
212
|
+
p1.setAttribute('stroke-opacity', '1');
|
|
213
|
+
p1.setAttribute('stroke-width', '0.5');
|
|
214
|
+
p1.setAttribute('data-om-dyn', '1');
|
|
215
|
+
p1.setAttribute('data-om-dyn-type', 'color');
|
|
216
|
+
|
|
217
|
+
const p2 = document.createElementNS(SVG_NS, 'path');
|
|
218
|
+
p2.setAttribute('d', d);
|
|
219
|
+
p2.setAttribute('stroke', `url(#lg-${uid}-${i})`);
|
|
220
|
+
p2.setAttribute('stroke-opacity', '1');
|
|
221
|
+
p2.setAttribute('stroke-width', '0.5');
|
|
222
|
+
p2.setAttribute('data-om-dyn', '1');
|
|
223
|
+
p2.setAttribute('data-om-dyn-type', 'gradient');
|
|
224
|
+
p2.setAttribute('data-om-index', String(i));
|
|
225
|
+
|
|
226
|
+
svg.appendChild(p1);
|
|
227
|
+
svg.appendChild(p2);
|
|
228
|
+
|
|
229
|
+
const lg = document.createElementNS(SVG_NS, 'linearGradient');
|
|
230
|
+
lg.setAttribute('id', `lg-${uid}-${i}`);
|
|
231
|
+
lg.setAttribute('data-om-dyn', '1');
|
|
232
|
+
// Inicializa el gradiente vacío (0%): sin esto SVG los hace
|
|
233
|
+
// x1=0%,y1=0%,x2=100%,y2=0% → línea completa "llena" hasta
|
|
234
|
+
// que vene el delay. Igual que NGXUI, que bindea 0% desde el inicio.
|
|
235
|
+
lg.setAttribute('x1', '0%');
|
|
236
|
+
lg.setAttribute('x2', '0%');
|
|
237
|
+
lg.setAttribute('y1', '0%');
|
|
238
|
+
lg.setAttribute('y2', '0%');
|
|
239
|
+
lg.innerHTML =
|
|
240
|
+
`<stop stop-color="${colors[0]}" stop-opacity="0"></stop>` +
|
|
241
|
+
`<stop stop-color="${colors[0]}"></stop>` +
|
|
242
|
+
`<stop offset="32.5%" stop-color="${colors[1]}"></stop>` +
|
|
243
|
+
`<stop offset="100%" stop-color="${colors[2]}" stop-opacity="0"></stop>`;
|
|
244
|
+
defs.appendChild(lg);
|
|
245
|
+
|
|
246
|
+
st.x1.push('0%');
|
|
247
|
+
st.x2.push('0%');
|
|
248
|
+
st.y1.push('0%');
|
|
249
|
+
st.y2.push('0%');
|
|
250
|
+
st.delays.push(Math.random() * 10);
|
|
251
|
+
st.durations.push(Math.random() * 10 + 10);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function commit(index: number): void {
|
|
256
|
+
const g = document.getElementById(`lg-${uid}-${index}`);
|
|
257
|
+
if (!g) return;
|
|
258
|
+
g.setAttribute('x1', st.x1[index]);
|
|
259
|
+
g.setAttribute('x2', st.x2[index]);
|
|
260
|
+
g.setAttribute('y1', st.y1[index]);
|
|
261
|
+
g.setAttribute('y2', st.y2[index]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- Motor A: 1:1 con NGXUI (restart desde 0) ----
|
|
265
|
+
|
|
266
|
+
function animateGradient(index: number, duration: number): void {
|
|
267
|
+
if (rafIds[index]) cancelAnimationFrame(rafIds[index]);
|
|
268
|
+
|
|
269
|
+
const startTime = performance.now();
|
|
270
|
+
const randomY2Value = 93 + Math.random() * 8;
|
|
271
|
+
|
|
272
|
+
const animate = (currentTime: number) => {
|
|
273
|
+
if (!st.inViewport) return;
|
|
274
|
+
|
|
275
|
+
const elapsed = currentTime - startTime;
|
|
276
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
277
|
+
|
|
278
|
+
st.x1[index] = `${progress * 100}%`;
|
|
279
|
+
st.x2[index] = `${progress * 95}%`;
|
|
280
|
+
st.y1[index] = `${progress * 100}%`;
|
|
281
|
+
st.y2[index] = `${progress * randomY2Value}%`;
|
|
282
|
+
|
|
283
|
+
commit(index);
|
|
284
|
+
|
|
285
|
+
if (progress < 1) {
|
|
286
|
+
rafIds[index] = requestAnimationFrame(animate);
|
|
287
|
+
} else {
|
|
288
|
+
restartAnimation(index, duration);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
rafIds[index] = requestAnimationFrame(animate);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function restartAnimation(index: number, duration: number): void {
|
|
296
|
+
setTimer(() => {
|
|
297
|
+
st.x1[index] = '0%';
|
|
298
|
+
st.x2[index] = '0%';
|
|
299
|
+
st.y1[index] = '0%';
|
|
300
|
+
st.y2[index] = '0%';
|
|
301
|
+
commit(index);
|
|
302
|
+
animateGradient(index, duration);
|
|
303
|
+
}, Math.random() * 10000);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function startAnimations(): void {
|
|
307
|
+
for (let i = 0; i < st.delays.length; i++) {
|
|
308
|
+
setTimer(() => animateGradient(i, st.durations[i] * 1000), st.delays[i] * 1000);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---- Motor B: pausa/reanuda en el sitio ----
|
|
313
|
+
|
|
314
|
+
interface Phase {
|
|
315
|
+
state: 0 | 1 | 2; // 0 delay, 1 animate, 2 wait
|
|
316
|
+
start: number;
|
|
317
|
+
dur: number;
|
|
318
|
+
y2v: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let phases: Phase[] = [];
|
|
322
|
+
let engineRunning = false;
|
|
323
|
+
let engineRaf = 0;
|
|
324
|
+
let pausedAt = 0;
|
|
325
|
+
let hasEntered = false;
|
|
326
|
+
|
|
327
|
+
function initPhases(now: number): void {
|
|
328
|
+
phases = [];
|
|
329
|
+
for (let i = 0; i < st.delays.length; i++) {
|
|
330
|
+
phases.push({ state: 0, start: now, dur: st.delays[i] * 1000, y2v: 0 });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Continúa la animación desde el progreso actual del motor A.
|
|
335
|
+
function adoptCurrentProgress(now: number): void {
|
|
336
|
+
phases = [];
|
|
337
|
+
for (let i = 0; i < st.delays.length; i++) {
|
|
338
|
+
const x1v = parseFloat(st.x1[i]) || 0; // 0..100
|
|
339
|
+
const progress = x1v / 100;
|
|
340
|
+
if (progress <= 0) {
|
|
341
|
+
phases.push({ state: 0, start: now, dur: st.delays[i] * 1000, y2v: 0 });
|
|
342
|
+
} else {
|
|
343
|
+
const y2p = parseFloat(st.y2[i]) || 0;
|
|
344
|
+
const y2v = progress > 0.001 ? Math.max(0.001, y2p / progress) : 93 + Math.random() * 8;
|
|
345
|
+
phases.push({
|
|
346
|
+
state: 1,
|
|
347
|
+
start: now - progress * (st.durations[i] * 1000),
|
|
348
|
+
dur: st.durations[i] * 1000,
|
|
349
|
+
y2v,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
hasEntered = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function tick(now: number): void {
|
|
357
|
+
if (!engineRunning) return;
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < st.delays.length; i++) {
|
|
360
|
+
const ph = phases[i];
|
|
361
|
+
let elapsed = now - ph.start;
|
|
362
|
+
|
|
363
|
+
if (ph.state === 0 || ph.state === 2) {
|
|
364
|
+
if (elapsed < ph.dur) continue;
|
|
365
|
+
// Fase de espera terminada: reinicia a 0% y arranca la animación.
|
|
366
|
+
st.x1[i] = '0%';
|
|
367
|
+
st.x2[i] = '0%';
|
|
368
|
+
st.y1[i] = '0%';
|
|
369
|
+
st.y2[i] = '0%';
|
|
370
|
+
commit(i);
|
|
371
|
+
ph.state = 1;
|
|
372
|
+
ph.start = now;
|
|
373
|
+
ph.dur = st.durations[i] * 1000;
|
|
374
|
+
ph.y2v = 93 + Math.random() * 8;
|
|
375
|
+
elapsed = 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const progress = Math.min(elapsed / ph.dur, 1);
|
|
379
|
+
|
|
380
|
+
st.x1[i] = `${progress * 100}%`;
|
|
381
|
+
st.x2[i] = `${progress * 95}%`;
|
|
382
|
+
st.y1[i] = `${progress * 100}%`;
|
|
383
|
+
st.y2[i] = `${progress * ph.y2v}%`;
|
|
384
|
+
commit(i);
|
|
385
|
+
|
|
386
|
+
if (progress >= 1) {
|
|
387
|
+
// Mantiene el estado "relleno" durante la espera (como el original).
|
|
388
|
+
ph.state = 2;
|
|
389
|
+
ph.start = now;
|
|
390
|
+
ph.dur = Math.random() * 10000;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
engineRaf = requestAnimationFrame(tick);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function playEngine(): void {
|
|
398
|
+
if (engineRunning) return;
|
|
399
|
+
const now = performance.now();
|
|
400
|
+
if (!hasEntered) {
|
|
401
|
+
initPhases(now);
|
|
402
|
+
hasEntered = true;
|
|
403
|
+
} else if (pausedAt > 0) {
|
|
404
|
+
const delta = now - pausedAt;
|
|
405
|
+
phases.forEach((p) => (p.start += delta));
|
|
406
|
+
pausedAt = 0;
|
|
407
|
+
}
|
|
408
|
+
engineRunning = true;
|
|
409
|
+
engineRaf = requestAnimationFrame(tick);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function stopEngine(): void {
|
|
413
|
+
if (!engineRunning) return;
|
|
414
|
+
engineRunning = false;
|
|
415
|
+
cancelAnimationFrame(engineRaf);
|
|
416
|
+
pausedAt = performance.now();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
generatePaths();
|
|
420
|
+
|
|
421
|
+
let currentlyVisible = false;
|
|
422
|
+
let observerTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
423
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
424
|
+
clearTimeout(observerTimeout);
|
|
425
|
+
observerTimeout = setTimeout(() => {
|
|
426
|
+
currentlyVisible = entry.isIntersecting;
|
|
427
|
+
if (entry.isIntersecting) {
|
|
428
|
+
if (resumeMode) {
|
|
429
|
+
playEngine();
|
|
430
|
+
} else {
|
|
431
|
+
st.inViewport = true;
|
|
432
|
+
clearTimers();
|
|
433
|
+
clearRaf();
|
|
434
|
+
startAnimations();
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
if (resumeMode) {
|
|
438
|
+
stopEngine();
|
|
439
|
+
} else {
|
|
440
|
+
st.inViewport = false;
|
|
441
|
+
clearTimers();
|
|
442
|
+
clearRaf();
|
|
443
|
+
generatePaths();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}, 100);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
observer.observe(el);
|
|
450
|
+
|
|
451
|
+
const attrObserver = new MutationObserver((muts) => {
|
|
452
|
+
for (const m of muts) {
|
|
453
|
+
if (m.attributeName === 'data-colors') {
|
|
454
|
+
let colors: string[];
|
|
455
|
+
try {
|
|
456
|
+
colors = JSON.parse(el.dataset.colors!);
|
|
457
|
+
} catch {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (let i = 0; i < st.delays.length; i++) {
|
|
461
|
+
const lg = document.getElementById(`lg-${uid}-${i}`);
|
|
462
|
+
if (!lg) continue;
|
|
463
|
+
const stops = lg.children;
|
|
464
|
+
stops[0]?.setAttribute('stop-color', colors[0]);
|
|
465
|
+
stops[1]?.setAttribute('stop-color', colors[0]);
|
|
466
|
+
stops[2]?.setAttribute('stop-color', colors[1]);
|
|
467
|
+
stops[3]?.setAttribute('stop-color', colors[2]);
|
|
468
|
+
}
|
|
469
|
+
} else if (m.attributeName === 'data-path-color') {
|
|
470
|
+
const c = el.dataset.pathColor ?? '#00000025';
|
|
471
|
+
el.querySelectorAll('path[data-om-dyn-type="color"]').forEach((p) =>
|
|
472
|
+
p.setAttribute('stroke', c),
|
|
473
|
+
);
|
|
474
|
+
} else if (m.attributeName === 'data-resume') {
|
|
475
|
+
// Cambio de modo en caliente (solo demo/Color lab).
|
|
476
|
+
const wantResume = el.dataset.resume === 'true';
|
|
477
|
+
if (wantResume === resumeMode) return;
|
|
478
|
+
resumeMode = wantResume;
|
|
479
|
+
|
|
480
|
+
clearTimers();
|
|
481
|
+
clearRaf();
|
|
482
|
+
|
|
483
|
+
if (resumeMode) {
|
|
484
|
+
// A → B: adoptar el progreso actual y pausar/reanudar desde ahí.
|
|
485
|
+
st.inViewport = false;
|
|
486
|
+
if (engineRunning) stopEngine();
|
|
487
|
+
adoptCurrentProgress(performance.now());
|
|
488
|
+
if (!currentlyVisible) pausedAt = performance.now();
|
|
489
|
+
if (currentlyVisible) playEngine();
|
|
490
|
+
} else {
|
|
491
|
+
// B → A: volver al comportamiento 1:1 (reinicia desde 0 al entrar).
|
|
492
|
+
stopEngine();
|
|
493
|
+
st.inViewport = true;
|
|
494
|
+
startAnimations();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
attrObserver.observe(el, {
|
|
500
|
+
attributes: true,
|
|
501
|
+
attributeFilter: ['data-colors', 'data-path-color', 'data-resume'],
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
})();
|
|
505
|
+
</script>
|