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 CHANGED
@@ -15,10 +15,10 @@ Run locally:
15
15
 
16
16
  ```bash
17
17
  # Vanilla
18
- cd lib/demo/vanilla && npm install && npm run dev
18
+ cd demo/vanilla && npm install && npm run dev
19
19
 
20
20
  # React
21
- cd lib/demo/react && npm install && npm run dev
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: { mergeDistance: 40, color: "#88ccff", ... },
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 pointCount = 12;
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 (this.config.pulseEnabled) {
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
- if (this.config.depthEffectEnabled) {
540
- const depthScale = 0.4 + 0.6 * particle.z;
541
- particle.currentRadius = (particle.currentRadius ?? particle.radius) * depthScale;
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 fx = Math.cos(angle) * force * 0.5;
553
- const fy = Math.sin(angle) * force * 0.5;
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.x < 0 || particle.x > this.canvas.width) {
584
- particle.dx = -particle.dx;
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.y < 0 || particle.y > this.canvas.height) {
587
- particle.dy = -particle.dy;
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
- const speed = Math.sqrt(
590
- particle.dx * particle.dx + particle.dy * particle.dy
591
- );
592
- if (speed > this.config.moveSpeed) {
593
- particle.dx = particle.dx / speed * this.config.moveSpeed;
594
- particle.dy = particle.dy / speed * this.config.moveSpeed;
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 hlDist = r * 0.35;
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
- r * 0.05,
839
+ gradR * 0.05,
708
840
  cx,
709
841
  cy,
710
- r * 1.05
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
- this.traceBlobPath(cx, cy, r, blob);
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
  }