particle-network-bg 0.1.0 → 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 +139 -5
- package/dist/{chunk-A6P5SQOV.mjs → chunk-VMVSPMPB.mjs} +263 -28
- package/dist/index.d.mts +86 -1
- package/dist/index.d.ts +86 -1
- package/dist/index.js +263 -28
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +48 -3
- package/dist/react.d.ts +48 -3
- package/dist/react.js +377 -36
- package/dist/react.mjs +118 -11
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ Run locally:
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
# Vanilla
|
|
18
|
-
cd
|
|
18
|
+
cd demo/vanilla && npm install && npm run dev
|
|
19
19
|
|
|
20
20
|
# React
|
|
21
|
-
cd
|
|
21
|
+
cd demo/react && npm install && npm run dev
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
## Install
|
|
@@ -153,11 +153,13 @@ new ParticleNetwork(canvas, {
|
|
|
153
153
|
color: "#88ccff",
|
|
154
154
|
opacity: 0.6,
|
|
155
155
|
reflectionStrength: 0.85,
|
|
156
|
-
highlightPosition: "top-left",
|
|
156
|
+
highlightPosition: "top-left", // "top-left" | "top" | "top-right" | "center" | "bottom-right"
|
|
157
157
|
highlightColor: "#ffffff",
|
|
158
158
|
shadowStrength: 0.4,
|
|
159
159
|
secondaryReflection: 0.25,
|
|
160
160
|
secondaryHighlightPosition: "bottom-right",
|
|
161
|
+
minRadius: 20, // min size for liquid glass particles (overrides root minRadius)
|
|
162
|
+
maxRadius: 40, // max size for liquid glass particles (overrides root maxRadius)
|
|
161
163
|
},
|
|
162
164
|
});
|
|
163
165
|
```
|
|
@@ -174,7 +176,7 @@ new ParticleNetwork(canvas, {
|
|
|
174
176
|
{ type: "asset", asset: "star", count: 20, liquidGlass: true },
|
|
175
177
|
],
|
|
176
178
|
assets: { star: "https://..." },
|
|
177
|
-
liquidGlass: {
|
|
179
|
+
liquidGlass: { color: "#88ccff", blur: 12, contrast: 25, ... },
|
|
178
180
|
});
|
|
179
181
|
```
|
|
180
182
|
|
|
@@ -289,12 +291,140 @@ new ParticleNetwork(canvas, {
|
|
|
289
291
|
});
|
|
290
292
|
```
|
|
291
293
|
|
|
294
|
+
## Child Particles
|
|
295
|
+
|
|
296
|
+
Attach real UI components (React nodes or DOM elements) as physics particles. Each child particle has an anchor point it springs back to, reacts to the mouse, and can optionally render as a liquid glass blob.
|
|
297
|
+
|
|
298
|
+
### React
|
|
299
|
+
|
|
300
|
+
Use `ChildParticle` or `GlassChildParticle` as children of `ParticleNetworkBg`:
|
|
301
|
+
|
|
302
|
+
```jsx
|
|
303
|
+
import { ParticleNetworkBg, ChildParticle, GlassChildParticle } from "particle-network-bg/react";
|
|
304
|
+
|
|
305
|
+
function App() {
|
|
306
|
+
return (
|
|
307
|
+
<ParticleNetworkBg config={{ particleCount: 60 }} style={{ width: "100%", height: "100vh" }}>
|
|
308
|
+
{/* Normal circle particle with a React child */}
|
|
309
|
+
<ChildParticle id="card-1" x={300} y={200} radius={50}>
|
|
310
|
+
<div style={{ color: "#fff", fontSize: 12 }}>Hello</div>
|
|
311
|
+
</ChildParticle>
|
|
312
|
+
|
|
313
|
+
{/* Liquid glass blob particle */}
|
|
314
|
+
<GlassChildParticle id="clock" x={600} y={400} radius={60}>
|
|
315
|
+
<span>🕐</span>
|
|
316
|
+
</GlassChildParticle>
|
|
317
|
+
|
|
318
|
+
{/* Rectangular child particle */}
|
|
319
|
+
<ChildParticle id="widget" x={900} y={300} width={160} height={80} borderRadius={16}>
|
|
320
|
+
<div>Widget content</div>
|
|
321
|
+
</ChildParticle>
|
|
322
|
+
</ParticleNetworkBg>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### `ChildParticle` / `GlassChildParticle` Props
|
|
328
|
+
|
|
329
|
+
| Prop | Type | Default | Description |
|
|
330
|
+
| ---------------- | --------- | -------- | ------------------------------------------------------------- |
|
|
331
|
+
| `id` | string | required | Unique identifier |
|
|
332
|
+
| `x` | number | required | Anchor X position (px) |
|
|
333
|
+
| `y` | number | required | Anchor Y position (px) |
|
|
334
|
+
| `radius` | number | required | Particle radius (px). Used for physics and as size |
|
|
335
|
+
| `width` | number | — | Rectangular width (px). Set with `height` for rect shape |
|
|
336
|
+
| `height` | number | — | Rectangular height (px) |
|
|
337
|
+
| `borderRadius` | number | — | Border radius (px) for rectangular shapes. Default: fully round |
|
|
338
|
+
| `overflow` | string | `"hidden"` | CSS overflow for the child content container |
|
|
339
|
+
| `anchorForce` | number | `0.05` | Spring force pulling back to anchor (0–1). Lower = more floaty |
|
|
340
|
+
| `mouseInfluence` | number | `0.1` | Mouse influence multiplier (0–1). 0 = ignores mouse |
|
|
341
|
+
| `children` | ReactNode | — | Content to render inside the particle |
|
|
342
|
+
| `style` | CSSProperties | — | Style applied to the inner wrapper div |
|
|
343
|
+
| `className` | string | — | Class applied to the inner wrapper div |
|
|
344
|
+
|
|
345
|
+
### Vanilla JS
|
|
346
|
+
|
|
347
|
+
Use `addChildParticle` / `removeChildParticle` on the `ParticleNetwork` instance directly:
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
const network = new ParticleNetwork(canvas, { particleCount: 60 });
|
|
351
|
+
network.start();
|
|
352
|
+
|
|
353
|
+
// Add a child particle
|
|
354
|
+
network.addChildParticle({
|
|
355
|
+
id: "card-1",
|
|
356
|
+
x: 300,
|
|
357
|
+
y: 200,
|
|
358
|
+
radius: 50,
|
|
359
|
+
anchorForce: 0.05,
|
|
360
|
+
mouseInfluence: 0.1,
|
|
361
|
+
liquidGlass: false,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Update its anchor (e.g. on scroll/resize)
|
|
365
|
+
network.updateChildParticle("card-1", { x: 400, y: 250 });
|
|
366
|
+
|
|
367
|
+
// Get current positions every frame via callback
|
|
368
|
+
network.onChildUpdate = (positions) => {
|
|
369
|
+
const pos = positions.get("card-1");
|
|
370
|
+
// pos.x, pos.y, pos.radius, pos.currentRadius, pos.width, pos.height, pos.rotation
|
|
371
|
+
myDomEl.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Remove
|
|
375
|
+
network.removeChildParticle("card-1");
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Child Particle API
|
|
379
|
+
|
|
380
|
+
| Method / Property | Description |
|
|
381
|
+
| ---------------------------------------------- | -------------------------------------------------------------------- |
|
|
382
|
+
| `addChildParticle(config)` | Register a child particle. Creates the particle with anchor physics. |
|
|
383
|
+
| `removeChildParticle(id)` | Remove a child particle by ID. |
|
|
384
|
+
| `updateChildParticle(id, updates)` | Update anchor position or any config property at runtime. |
|
|
385
|
+
| `getChildParticlePositions()` | Returns a `Map<string, ChildParticlePosition>` with current state. |
|
|
386
|
+
| `onChildUpdate` | Callback fired every frame: `(positions: Map<string, ChildParticlePosition>) => void` |
|
|
387
|
+
| `getChildOverlayElement(id)` | Returns the DOM div overlay for a child particle (for manual use). |
|
|
388
|
+
|
|
389
|
+
### Types
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import type { ChildParticleConfig, ChildParticlePosition } from "particle-network-bg";
|
|
393
|
+
|
|
394
|
+
interface ChildParticleConfig {
|
|
395
|
+
id: string;
|
|
396
|
+
x: number;
|
|
397
|
+
y: number;
|
|
398
|
+
radius: number;
|
|
399
|
+
width?: number;
|
|
400
|
+
height?: number;
|
|
401
|
+
borderRadius?: number;
|
|
402
|
+
overflow?: string;
|
|
403
|
+
anchorForce?: number;
|
|
404
|
+
mouseInfluence?: number;
|
|
405
|
+
liquidGlass?: boolean;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
interface ChildParticlePosition {
|
|
409
|
+
x: number;
|
|
410
|
+
y: number;
|
|
411
|
+
radius: number;
|
|
412
|
+
currentRadius: number;
|
|
413
|
+
width?: number;
|
|
414
|
+
height?: number;
|
|
415
|
+
rotation: number; // blob rotation in radians (liquid glass only, 0 for normal)
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
292
419
|
## API
|
|
293
420
|
|
|
294
421
|
### React (`particle-network-bg/react`)
|
|
295
422
|
|
|
296
|
-
- `ParticleNetworkBg` – Wrapper component. Props: `config`, `style`, `className`
|
|
423
|
+
- `ParticleNetworkBg` – Wrapper component. Props: `config`, `style`, `className`, `children` (for `ChildParticle` / `GlassChildParticle`)
|
|
297
424
|
- `useParticleNetwork(config?)` – Hook that returns a canvas ref
|
|
425
|
+
- `ChildParticle` – React child particle component (see [Child Particles](#child-particles))
|
|
426
|
+
- `GlassChildParticle` – Liquid glass variant of `ChildParticle`
|
|
427
|
+
- `ParticleNetworkContext` – React context exposing the `ParticleNetwork` instance; use `useContext(ParticleNetworkContext)` inside children of `ParticleNetworkBg` for direct instance access
|
|
298
428
|
|
|
299
429
|
### `ParticleNetwork` (vanilla)
|
|
300
430
|
|
|
@@ -313,7 +443,11 @@ import type {
|
|
|
313
443
|
GradientType,
|
|
314
444
|
ConnectionRules,
|
|
315
445
|
LiquidGlassConfig,
|
|
446
|
+
LiquidGlassHighlightPosition,
|
|
316
447
|
ParticleTypeEntry,
|
|
448
|
+
ParticleAssetConfig,
|
|
449
|
+
ChildParticleConfig,
|
|
450
|
+
ChildParticlePosition,
|
|
317
451
|
} from "particle-network-bg";
|
|
318
452
|
```
|
|
319
453
|
|
|
@@ -73,6 +73,11 @@ var ParticleNetwork = class {
|
|
|
73
73
|
this.gradientCenter = { x: 0, y: 0 };
|
|
74
74
|
this.smoothedMouseAngle = 0;
|
|
75
75
|
this.gradientDiv = null;
|
|
76
|
+
this.childParticleConfigs = /* @__PURE__ */ new Map();
|
|
77
|
+
this.overlayContainer = null;
|
|
78
|
+
this.childOverlayElements = /* @__PURE__ */ new Map();
|
|
79
|
+
/** Called every frame with updated child particle positions. */
|
|
80
|
+
this.onChildUpdate = null;
|
|
76
81
|
this.canvas = canvas;
|
|
77
82
|
const ctx = canvas.getContext("2d", { alpha: true });
|
|
78
83
|
if (!ctx) {
|
|
@@ -111,6 +116,58 @@ var ParticleNetwork = class {
|
|
|
111
116
|
this.gradientDiv.style.cssText = "position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:-1;";
|
|
112
117
|
this.canvas.parentElement?.insertBefore(this.gradientDiv, this.canvas);
|
|
113
118
|
}
|
|
119
|
+
ensureOverlayContainer() {
|
|
120
|
+
if (!this.overlayContainer) {
|
|
121
|
+
this.overlayContainer = document.createElement("div");
|
|
122
|
+
this.overlayContainer.style.cssText = "position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:1;overflow:hidden;";
|
|
123
|
+
this.canvas.parentElement?.appendChild(this.overlayContainer);
|
|
124
|
+
}
|
|
125
|
+
return this.overlayContainer;
|
|
126
|
+
}
|
|
127
|
+
/** Get or create the overlay div for a child particle. */
|
|
128
|
+
getChildOverlayElement(id) {
|
|
129
|
+
let el = this.childOverlayElements.get(id);
|
|
130
|
+
if (!el) {
|
|
131
|
+
const container = this.ensureOverlayContainer();
|
|
132
|
+
el = document.createElement("div");
|
|
133
|
+
el.style.cssText = "position:absolute;pointer-events:auto;display:flex;align-items:center;justify-content:center;will-change:transform;";
|
|
134
|
+
el.dataset.childParticleId = id;
|
|
135
|
+
container.appendChild(el);
|
|
136
|
+
this.childOverlayElements.set(id, el);
|
|
137
|
+
}
|
|
138
|
+
return el;
|
|
139
|
+
}
|
|
140
|
+
/** Remove the overlay div for a child particle. */
|
|
141
|
+
removeChildOverlayElement(id) {
|
|
142
|
+
const el = this.childOverlayElements.get(id);
|
|
143
|
+
if (el) {
|
|
144
|
+
el.remove();
|
|
145
|
+
this.childOverlayElements.delete(id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Sync all child overlay divs to their particle positions. Called each frame. */
|
|
149
|
+
updateChildOverlays() {
|
|
150
|
+
for (const particle of this.particles) {
|
|
151
|
+
if (!particle.isChild || !particle.childId) continue;
|
|
152
|
+
const el = this.childOverlayElements.get(particle.childId);
|
|
153
|
+
if (!el) continue;
|
|
154
|
+
const px = particle.smoothX ?? particle.x;
|
|
155
|
+
const py = particle.smoothY ?? particle.y;
|
|
156
|
+
const isRect = particle.width != null && particle.height != null;
|
|
157
|
+
const w = isRect ? particle.width : (particle.currentRadius ?? particle.radius) * 2;
|
|
158
|
+
const h = isRect ? particle.height : (particle.currentRadius ?? particle.radius) * 2;
|
|
159
|
+
el.style.width = w + "px";
|
|
160
|
+
el.style.height = h + "px";
|
|
161
|
+
el.style.transform = `translate(${px - w / 2}px, ${py - h / 2}px)`;
|
|
162
|
+
if (isRect) {
|
|
163
|
+
const br = particle.borderRadius ?? Math.min(w, h) / 2;
|
|
164
|
+
el.style.borderRadius = br + "px";
|
|
165
|
+
} else {
|
|
166
|
+
el.style.borderRadius = "50%";
|
|
167
|
+
}
|
|
168
|
+
el.style.overflow = particle.overflow ?? "hidden";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
114
171
|
validateConfig(config) {
|
|
115
172
|
const numericParams = [
|
|
116
173
|
"particleCount",
|
|
@@ -326,6 +383,10 @@ var ParticleNetwork = class {
|
|
|
326
383
|
this.canvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
|
|
327
384
|
this.gradientDiv?.remove();
|
|
328
385
|
this.gradientDiv = null;
|
|
386
|
+
this.overlayContainer?.remove();
|
|
387
|
+
this.overlayContainer = null;
|
|
388
|
+
this.childOverlayElements.clear();
|
|
389
|
+
this.childParticleConfigs.clear();
|
|
329
390
|
this.stop();
|
|
330
391
|
}
|
|
331
392
|
handleMouseMove(e) {
|
|
@@ -347,6 +408,7 @@ var ParticleNetwork = class {
|
|
|
347
408
|
};
|
|
348
409
|
}
|
|
349
410
|
createParticles() {
|
|
411
|
+
const childParticles = this.particles.filter((p) => p.isChild);
|
|
350
412
|
this.particles = [];
|
|
351
413
|
const total = this.config.particleCount;
|
|
352
414
|
for (let i = 0; i < total; i++) {
|
|
@@ -363,6 +425,7 @@ var ParticleNetwork = class {
|
|
|
363
425
|
});
|
|
364
426
|
}
|
|
365
427
|
this.assignParticleTypes();
|
|
428
|
+
this.particles.push(...childParticles);
|
|
366
429
|
}
|
|
367
430
|
assignParticleTypes() {
|
|
368
431
|
const { particleTypes, particleAssets, particleCount } = this.config;
|
|
@@ -468,7 +531,8 @@ var ParticleNetwork = class {
|
|
|
468
531
|
this.initBlob(p);
|
|
469
532
|
}
|
|
470
533
|
initBlob(p) {
|
|
471
|
-
const
|
|
534
|
+
const isRect = p.width != null && p.height != null;
|
|
535
|
+
const pointCount = isRect ? 28 : 12;
|
|
472
536
|
const modeCount = 3;
|
|
473
537
|
const freqs = [];
|
|
474
538
|
const amps = [];
|
|
@@ -529,16 +593,20 @@ var ParticleNetwork = class {
|
|
|
529
593
|
particle.dz = -Math.abs(particle.dz);
|
|
530
594
|
}
|
|
531
595
|
}
|
|
532
|
-
if (
|
|
533
|
-
this.pulseAngle += this.config.pulseSpeed;
|
|
534
|
-
const pulseScale = Math.sin(this.pulseAngle) * 0.5 + 1;
|
|
535
|
-
particle.currentRadius = particle.radius * pulseScale;
|
|
536
|
-
} else {
|
|
596
|
+
if (particle.isChild) {
|
|
537
597
|
particle.currentRadius = particle.radius;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
598
|
+
} else {
|
|
599
|
+
if (this.config.pulseEnabled) {
|
|
600
|
+
this.pulseAngle += this.config.pulseSpeed;
|
|
601
|
+
const pulseScale = Math.sin(this.pulseAngle) * 0.5 + 1;
|
|
602
|
+
particle.currentRadius = particle.radius * pulseScale;
|
|
603
|
+
} else {
|
|
604
|
+
particle.currentRadius = particle.radius;
|
|
605
|
+
}
|
|
606
|
+
if (this.config.depthEffectEnabled) {
|
|
607
|
+
const depthScale = 0.4 + 0.6 * particle.z;
|
|
608
|
+
particle.currentRadius = (particle.currentRadius ?? particle.radius) * depthScale;
|
|
609
|
+
}
|
|
542
610
|
}
|
|
543
611
|
particle.x += particle.dx;
|
|
544
612
|
particle.y += particle.dy;
|
|
@@ -549,8 +617,9 @@ var ParticleNetwork = class {
|
|
|
549
617
|
if (distance < this.config.mouseRadius) {
|
|
550
618
|
const force = (this.config.mouseRadius - distance) / this.config.mouseRadius;
|
|
551
619
|
const angle = Math.atan2(dy, dx);
|
|
552
|
-
const
|
|
553
|
-
const
|
|
620
|
+
const mouseScale = particle.isChild ? particle.mouseInfluence ?? 0.1 : 1;
|
|
621
|
+
const fx = Math.cos(angle) * force * 0.5 * mouseScale;
|
|
622
|
+
const fy = Math.sin(angle) * force * 0.5 * mouseScale;
|
|
554
623
|
if (particle.mouseAttract) {
|
|
555
624
|
particle.dx += fx;
|
|
556
625
|
particle.dy += fy;
|
|
@@ -560,6 +629,15 @@ var ParticleNetwork = class {
|
|
|
560
629
|
}
|
|
561
630
|
}
|
|
562
631
|
}
|
|
632
|
+
if (particle.isChild && particle.anchorX != null && particle.anchorY != null) {
|
|
633
|
+
const anchorF = particle.anchorForce ?? 0.05;
|
|
634
|
+
const adx = particle.anchorX - particle.x;
|
|
635
|
+
const ady = particle.anchorY - particle.y;
|
|
636
|
+
particle.dx += adx * anchorF;
|
|
637
|
+
particle.dy += ady * anchorF;
|
|
638
|
+
particle.dx *= 0.7;
|
|
639
|
+
particle.dy *= 0.7;
|
|
640
|
+
}
|
|
563
641
|
const minDist = this.config.minParticleDistance ?? 0;
|
|
564
642
|
const minForce = this.config.minParticleForce ?? 0.5;
|
|
565
643
|
if (minDist > 0 && minForce > 0) {
|
|
@@ -573,25 +651,37 @@ var ParticleNetwork = class {
|
|
|
573
651
|
const gap = dist - r1 - r2;
|
|
574
652
|
if (gap < minDist && dist > 1e-3) {
|
|
575
653
|
const strength = (minDist - gap) / minDist * minForce;
|
|
654
|
+
const repulsionScale = particle.isChild ? 0.1 : 1;
|
|
576
655
|
const ux = -dx / dist;
|
|
577
656
|
const uy = -dy / dist;
|
|
578
|
-
particle.dx += ux * strength;
|
|
579
|
-
particle.dy += uy * strength;
|
|
657
|
+
particle.dx += ux * strength * repulsionScale;
|
|
658
|
+
particle.dy += uy * strength * repulsionScale;
|
|
580
659
|
}
|
|
581
660
|
}
|
|
582
661
|
}
|
|
583
|
-
if (particle.
|
|
584
|
-
particle.
|
|
662
|
+
if (!particle.isChild) {
|
|
663
|
+
if (particle.x < 0 || particle.x > this.canvas.width) {
|
|
664
|
+
particle.dx = -particle.dx;
|
|
665
|
+
}
|
|
666
|
+
if (particle.y < 0 || particle.y > this.canvas.height) {
|
|
667
|
+
particle.dy = -particle.dy;
|
|
668
|
+
}
|
|
585
669
|
}
|
|
586
|
-
if (particle.
|
|
587
|
-
|
|
670
|
+
if (!particle.isChild) {
|
|
671
|
+
const speed = Math.sqrt(
|
|
672
|
+
particle.dx * particle.dx + particle.dy * particle.dy
|
|
673
|
+
);
|
|
674
|
+
if (speed > this.config.moveSpeed) {
|
|
675
|
+
particle.dx = particle.dx / speed * this.config.moveSpeed;
|
|
676
|
+
particle.dy = particle.dy / speed * this.config.moveSpeed;
|
|
677
|
+
}
|
|
588
678
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
particle.
|
|
594
|
-
particle.
|
|
679
|
+
if (particle.isChild) {
|
|
680
|
+
const smoothing = 0.12;
|
|
681
|
+
if (particle.smoothX == null) particle.smoothX = particle.x;
|
|
682
|
+
if (particle.smoothY == null) particle.smoothY = particle.y;
|
|
683
|
+
particle.smoothX += (particle.x - particle.smoothX) * smoothing;
|
|
684
|
+
particle.smoothY += (particle.y - particle.smoothY) * smoothing;
|
|
595
685
|
}
|
|
596
686
|
});
|
|
597
687
|
}
|
|
@@ -642,6 +732,46 @@ var ParticleNetwork = class {
|
|
|
642
732
|
}
|
|
643
733
|
this.ctx.closePath();
|
|
644
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Trace a blob path for a rectangular particle using superellipse interpolation.
|
|
737
|
+
* roundness: 0 = sharp rectangle, 1 = ellipse.
|
|
738
|
+
*/
|
|
739
|
+
traceRectBlobPath(cx, cy, halfW, halfH, roundness, blob) {
|
|
740
|
+
const n = blob.pointCount;
|
|
741
|
+
const k = Math.max(0.15, Math.min(1, roundness));
|
|
742
|
+
const sx = blob.mouseStretchX;
|
|
743
|
+
const sy = blob.mouseStretchY;
|
|
744
|
+
const pts = [];
|
|
745
|
+
for (let i = 0; i < n; i++) {
|
|
746
|
+
const angle = i / n * Math.PI * 2;
|
|
747
|
+
const ra = angle + blob.rotation;
|
|
748
|
+
const cosA = Math.cos(angle);
|
|
749
|
+
const sinA = Math.sin(angle);
|
|
750
|
+
const signX = cosA >= 0 ? 1 : -1;
|
|
751
|
+
const signY = sinA >= 0 ? 1 : -1;
|
|
752
|
+
const bx = signX * Math.pow(Math.abs(cosA) + 1e-9, k) * halfW;
|
|
753
|
+
const by = signY * Math.pow(Math.abs(sinA) + 1e-9, k) * halfH;
|
|
754
|
+
let deform = 0;
|
|
755
|
+
for (let m = 0; m < blob.freqs.length; m++) {
|
|
756
|
+
deform += blob.amps[m] * Math.sin(blob.freqs[m] * ra + blob.phases[m]);
|
|
757
|
+
}
|
|
758
|
+
const scale = 1 + deform;
|
|
759
|
+
let px = cx + bx * scale;
|
|
760
|
+
let py = cy + by * scale;
|
|
761
|
+
px += sx * Math.cos(angle) * Math.max(0, Math.cos(angle));
|
|
762
|
+
py += sy * Math.sin(angle) * Math.max(0, Math.sin(angle));
|
|
763
|
+
pts.push({ x: px, y: py });
|
|
764
|
+
}
|
|
765
|
+
this.ctx.beginPath();
|
|
766
|
+
const last = pts[n - 1];
|
|
767
|
+
const first = pts[0];
|
|
768
|
+
this.ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
|
|
769
|
+
for (let i = 0; i < n; i++) {
|
|
770
|
+
const next = pts[(i + 1) % n];
|
|
771
|
+
this.ctx.quadraticCurveTo(pts[i].x, pts[i].y, (pts[i].x + next.x) / 2, (pts[i].y + next.y) / 2);
|
|
772
|
+
}
|
|
773
|
+
this.ctx.closePath();
|
|
774
|
+
}
|
|
645
775
|
updateBlobMouse(particle) {
|
|
646
776
|
const blob = particle.blob;
|
|
647
777
|
if (!blob) return;
|
|
@@ -675,6 +805,7 @@ var ParticleNetwork = class {
|
|
|
675
805
|
const blob = particle.blob;
|
|
676
806
|
const cx = particle.x;
|
|
677
807
|
const cy = particle.y;
|
|
808
|
+
const isRect = particle.width != null && particle.height != null;
|
|
678
809
|
let opacity = (lg.opacity ?? DEFAULT_LIQUID_GLASS.opacity) * this.config.particleOpacity;
|
|
679
810
|
if (this.config.depthEffectEnabled) {
|
|
680
811
|
opacity *= 0.6 + 0.4 * particle.z;
|
|
@@ -691,7 +822,8 @@ var ParticleNetwork = class {
|
|
|
691
822
|
const baseG = parseInt(color.slice(3, 5), 16);
|
|
692
823
|
const baseB = parseInt(color.slice(5, 7), 16);
|
|
693
824
|
const shadowStr = lg.shadowStrength ?? DEFAULT_LIQUID_GLASS.shadowStrength;
|
|
694
|
-
const
|
|
825
|
+
const gradR = isRect ? Math.max(particle.width, particle.height) / 2 : r;
|
|
826
|
+
const hlDist = gradR * 0.35;
|
|
695
827
|
const hlX = cx + Math.cos(blob.hlAngle) * hlDist;
|
|
696
828
|
const hlY = cy + Math.sin(blob.hlAngle) * hlDist;
|
|
697
829
|
const lr = Math.min(255, baseR + Math.round((255 - baseR) * 0.55));
|
|
@@ -704,17 +836,25 @@ var ParticleNetwork = class {
|
|
|
704
836
|
const grad = this.ctx.createRadialGradient(
|
|
705
837
|
hlX,
|
|
706
838
|
hlY,
|
|
707
|
-
|
|
839
|
+
gradR * 0.05,
|
|
708
840
|
cx,
|
|
709
841
|
cy,
|
|
710
|
-
|
|
842
|
+
gradR * 1.05
|
|
711
843
|
);
|
|
712
844
|
grad.addColorStop(0, `rgba(${lr},${lgr},${lb}, ${opacity * 0.95})`);
|
|
713
845
|
grad.addColorStop(0.4, `rgba(${baseR},${baseG},${baseB}, ${opacity * 0.8})`);
|
|
714
846
|
grad.addColorStop(0.8, `rgba(${dr},${dg},${db}, ${opacity * 0.6})`);
|
|
715
847
|
grad.addColorStop(1, `rgba(${dr},${dg},${db}, ${opacity * 0.25})`);
|
|
716
848
|
this.ctx.fillStyle = grad;
|
|
717
|
-
|
|
849
|
+
if (isRect) {
|
|
850
|
+
const halfW = particle.width / 2;
|
|
851
|
+
const halfH = particle.height / 2;
|
|
852
|
+
const minDim = Math.min(halfW, halfH);
|
|
853
|
+
const roundness = particle.borderRadius != null ? Math.min(particle.borderRadius / minDim, 1) : 0.3;
|
|
854
|
+
this.traceRectBlobPath(cx, cy, halfW, halfH, roundness, blob);
|
|
855
|
+
} else {
|
|
856
|
+
this.traceBlobPath(cx, cy, r, blob);
|
|
857
|
+
}
|
|
718
858
|
this.ctx.fill();
|
|
719
859
|
this.ctx.restore();
|
|
720
860
|
}
|
|
@@ -759,6 +899,17 @@ var ParticleNetwork = class {
|
|
|
759
899
|
} else {
|
|
760
900
|
this.ctx.drawImage(img, x, y, size, size);
|
|
761
901
|
}
|
|
902
|
+
} else if (particle.isChild && particle.width != null && particle.height != null) {
|
|
903
|
+
this.ctx.globalAlpha = opacity;
|
|
904
|
+
this.ctx.fillStyle = defaultColor;
|
|
905
|
+
const w = particle.width;
|
|
906
|
+
const h = particle.height;
|
|
907
|
+
const br = particle.borderRadius ?? Math.min(w, h) / 2;
|
|
908
|
+
const x = particle.x - w / 2;
|
|
909
|
+
const y = particle.y - h / 2;
|
|
910
|
+
this.ctx.beginPath();
|
|
911
|
+
this.ctx.roundRect(x, y, w, h, br);
|
|
912
|
+
this.ctx.fill();
|
|
762
913
|
} else {
|
|
763
914
|
this.ctx.globalAlpha = opacity;
|
|
764
915
|
this.ctx.fillStyle = defaultColor;
|
|
@@ -877,6 +1028,84 @@ var ParticleNetwork = class {
|
|
|
877
1028
|
if (!result) return "255,255,255";
|
|
878
1029
|
return result.slice(1).map((n) => parseInt(n, 16)).join(",");
|
|
879
1030
|
}
|
|
1031
|
+
/** Register a child particle. Creates a new particle with anchor behavior. */
|
|
1032
|
+
addChildParticle(config) {
|
|
1033
|
+
if (this.childParticleConfigs.has(config.id)) {
|
|
1034
|
+
this.removeChildParticle(config.id);
|
|
1035
|
+
}
|
|
1036
|
+
this.childParticleConfigs.set(config.id, config);
|
|
1037
|
+
const particle = {
|
|
1038
|
+
x: config.x,
|
|
1039
|
+
y: config.y,
|
|
1040
|
+
dx: 0,
|
|
1041
|
+
dy: 0,
|
|
1042
|
+
radius: config.radius,
|
|
1043
|
+
z: 0.8 + Math.random() * 0.2,
|
|
1044
|
+
dz: (Math.random() - 0.5) * this.config.depthSpeed * 2,
|
|
1045
|
+
isChild: true,
|
|
1046
|
+
childId: config.id,
|
|
1047
|
+
anchorX: config.x,
|
|
1048
|
+
anchorY: config.y,
|
|
1049
|
+
anchorForce: config.anchorForce ?? 0.05,
|
|
1050
|
+
mouseInfluence: config.mouseInfluence ?? 0.1,
|
|
1051
|
+
width: config.width,
|
|
1052
|
+
height: config.height,
|
|
1053
|
+
borderRadius: config.borderRadius,
|
|
1054
|
+
overflow: config.overflow,
|
|
1055
|
+
smoothX: config.x,
|
|
1056
|
+
smoothY: config.y
|
|
1057
|
+
};
|
|
1058
|
+
if (config.liquidGlass) {
|
|
1059
|
+
particle.liquidGlass = true;
|
|
1060
|
+
this.initBlob(particle);
|
|
1061
|
+
}
|
|
1062
|
+
this.particles.push(particle);
|
|
1063
|
+
}
|
|
1064
|
+
/** Remove a child particle by ID. */
|
|
1065
|
+
removeChildParticle(id) {
|
|
1066
|
+
this.childParticleConfigs.delete(id);
|
|
1067
|
+
this.removeChildOverlayElement(id);
|
|
1068
|
+
const idx = this.particles.findIndex((p) => p.childId === id);
|
|
1069
|
+
if (idx !== -1) this.particles.splice(idx, 1);
|
|
1070
|
+
}
|
|
1071
|
+
/** Update a child particle's anchor position and/or config. */
|
|
1072
|
+
updateChildParticle(id, updates) {
|
|
1073
|
+
const config = this.childParticleConfigs.get(id);
|
|
1074
|
+
if (!config) return;
|
|
1075
|
+
Object.assign(config, updates);
|
|
1076
|
+
const particle = this.particles.find((p) => p.childId === id);
|
|
1077
|
+
if (!particle) return;
|
|
1078
|
+
if (updates.x !== void 0) particle.anchorX = updates.x;
|
|
1079
|
+
if (updates.y !== void 0) particle.anchorY = updates.y;
|
|
1080
|
+
if (updates.radius !== void 0) particle.radius = updates.radius;
|
|
1081
|
+
if (updates.anchorForce !== void 0) particle.anchorForce = updates.anchorForce;
|
|
1082
|
+
if (updates.mouseInfluence !== void 0) particle.mouseInfluence = updates.mouseInfluence;
|
|
1083
|
+
if (updates.width !== void 0) particle.width = updates.width;
|
|
1084
|
+
if (updates.height !== void 0) particle.height = updates.height;
|
|
1085
|
+
if (updates.borderRadius !== void 0) particle.borderRadius = updates.borderRadius;
|
|
1086
|
+
if (updates.overflow !== void 0) particle.overflow = updates.overflow;
|
|
1087
|
+
if (updates.liquidGlass !== void 0) {
|
|
1088
|
+
particle.liquidGlass = updates.liquidGlass;
|
|
1089
|
+
if (updates.liquidGlass && !particle.blob) this.initBlob(particle);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/** Get current positions of all child particles. */
|
|
1093
|
+
getChildParticlePositions() {
|
|
1094
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1095
|
+
for (const particle of this.particles) {
|
|
1096
|
+
if (!particle.isChild || !particle.childId) continue;
|
|
1097
|
+
positions.set(particle.childId, {
|
|
1098
|
+
x: particle.smoothX ?? particle.x,
|
|
1099
|
+
y: particle.smoothY ?? particle.y,
|
|
1100
|
+
radius: particle.radius,
|
|
1101
|
+
currentRadius: particle.currentRadius ?? particle.radius,
|
|
1102
|
+
width: particle.width,
|
|
1103
|
+
height: particle.height,
|
|
1104
|
+
rotation: particle.blob?.rotation ?? 0
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return positions;
|
|
1108
|
+
}
|
|
880
1109
|
stop() {
|
|
881
1110
|
this.isRunning = false;
|
|
882
1111
|
if (this.animationId !== null) {
|
|
@@ -895,6 +1124,12 @@ var ParticleNetwork = class {
|
|
|
895
1124
|
this.updateParticles();
|
|
896
1125
|
this.drawParticles();
|
|
897
1126
|
this.drawConnections();
|
|
1127
|
+
if (this.childParticleConfigs.size > 0) {
|
|
1128
|
+
this.updateChildOverlays();
|
|
1129
|
+
if (this.onChildUpdate) {
|
|
1130
|
+
this.onChildUpdate(this.getChildParticlePositions());
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
898
1133
|
if (this.isRunning) {
|
|
899
1134
|
this.animationId = requestAnimationFrame(() => this.animate());
|
|
900
1135
|
}
|