particle-network-bg 0.0.1

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 ADDED
@@ -0,0 +1,198 @@
1
+ # particle-network-bg
2
+
3
+ Interactive particle network animation for backgrounds. Typed, zero dependencies (core), works with vanilla JS, React, or any framework.
4
+
5
+ > **Note:** The demos are in the [`demo/`](./demo) directory.
6
+
7
+ ## Demo
8
+
9
+ Live demos (after enabling [GitHub Pages](https://docs.github.com/en/pages)):
10
+
11
+ - **[Vanilla JS](https://aliotadi.github.io/particle-network-bg/vanilla/)**
12
+ - **[React](https://aliotadi.github.io/particle-network-bg/react/)**
13
+
14
+ Run locally:
15
+
16
+ ```bash
17
+ # Vanilla
18
+ cd lib/demo/vanilla && npm install && npm run dev
19
+
20
+ # React
21
+ cd lib/demo/react && npm install && npm run dev
22
+ ```
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install particle-network-bg
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Vanilla JS
33
+
34
+ ```js
35
+ import { ParticleNetwork } from "particle-network-bg";
36
+
37
+ const canvas = document.getElementById("canvas");
38
+ const network = new ParticleNetwork(canvas, {
39
+ particleCount: 100,
40
+ particleColor: "#000000",
41
+ lineColor: "#000000",
42
+ backgroundColor: "#ffffff",
43
+ });
44
+
45
+ network.start();
46
+
47
+ // Later: cleanup
48
+ network.cleanup();
49
+ ```
50
+
51
+ ### React – Component
52
+
53
+ ```jsx
54
+ import { ParticleNetworkBg } from "particle-network-bg/react";
55
+
56
+ function App() {
57
+ return (
58
+ <ParticleNetworkBg
59
+ config={{
60
+ particleCount: 80,
61
+ gradientEnabled: true,
62
+ gradientColors: ["#667eea", "#764ba2"],
63
+ }}
64
+ style={{ width: "100%", height: "100vh" }}
65
+ />
66
+ );
67
+ }
68
+ ```
69
+
70
+ ### React – Hook
71
+
72
+ ```jsx
73
+ import { useParticleNetwork } from "particle-network-bg/react";
74
+
75
+ function App() {
76
+ const canvasRef = useParticleNetwork({
77
+ particleCount: 80,
78
+ gradientEnabled: true,
79
+ gradientColors: ["#667eea", "#764ba2"],
80
+ });
81
+
82
+ return (
83
+ <canvas
84
+ ref={canvasRef}
85
+ style={{ display: "block", width: "100%", height: "100vh" }}
86
+ />
87
+ );
88
+ }
89
+ ```
90
+
91
+ > **Note:** Config is applied on mount. The canvas resizes to the window.
92
+
93
+ ## Configuration
94
+
95
+ | Option | Type | Default | Description |
96
+ | ------------------------ | ---------------------- | ------------------------ | -------------------------------------------------------- |
97
+ | `particleCount` | number | 100 | Number of particles |
98
+ | `minRadius` | number | 2 | Min particle size (px) |
99
+ | `maxRadius` | number | 6 | Max particle size (px) |
100
+ | `particleColor` | string | `#000000` | Particle color (hex) |
101
+ | `lineColor` | string | `#000000` | Connection line color (hex) |
102
+ | `lineWidth` | number | 1 | Line width |
103
+ | `lineOpacity` | number | 0.2 | Line opacity (0–1) |
104
+ | `maxDistance` | number | 150 | Max connection distance (px) |
105
+ | `moveSpeed` | number | 1 | Particle movement speed |
106
+ | `backgroundColor` | string | `#ffffff` | Background color (hex) |
107
+ | `backgroundOpacity` | number | 1 | Background opacity (0–1) |
108
+ | `particleOpacity` | number | 1 | Particle opacity (0–1) |
109
+ | `mouseRadius` | number | 200 | Mouse interaction radius |
110
+ | `mouseInteraction` | boolean | true | Enable mouse repel effect |
111
+ | `pulseEnabled` | boolean | true | Enable pulse animation |
112
+ | `pulseSpeed` | number | 0 | Pulse speed |
113
+ | `depthEffectEnabled` | boolean | true | 3D depth effect |
114
+ | `depthSpeed` | number | 0.02 | Depth animation speed |
115
+ | `gradientEnabled` | boolean | false | Use gradient background |
116
+ | `gradientType` | `"linear" \| "radial"` | `"linear"` | Gradient type |
117
+ | `gradientColors` | string[] | `["#667eea", "#764ba2"]` | Gradient colors (hex) |
118
+ | `gradientStops` | number[] | auto | Color stops (0–1), optional |
119
+ | `gradientSpeed` | number | 0.02 | Gradient animation speed |
120
+ | `gradientMouseReaction` | boolean | true | **Radial only:** Gradient center follows mouse |
121
+ | `gradientMouseInfluence` | number | 0.5 | **Radial only:** Mouse influence strength (0–1) |
122
+ | `gradientAngle` | number | 0 | **Linear spin mode:** Initial rotation angle (degrees) |
123
+ | `gradientRadius` | number | 1 | **Radial:** Gradient radius multiplier |
124
+ | `gradientSpin` | boolean | false | **Linear:** `true` = rotate, `false` = flow continuously |
125
+ | `gradientFlowAngle` | number | 45 | **Linear flow mode:** Direction of color flow (degrees) |
126
+ | `gradientOrbitRadius` | number | 0.3 | **Radial:** Orbit radius for center movement (0–1) |
127
+
128
+ ## Gradient Examples
129
+
130
+ ### Linear Gradient (Continuous Flow)
131
+
132
+ ```js
133
+ new ParticleNetwork(canvas, {
134
+ gradientEnabled: true,
135
+ gradientType: "linear",
136
+ gradientColors: ["#667eea", "#764ba2", "#f093fb"],
137
+ gradientSpeed: 0.02,
138
+ gradientSpin: false, // Flow mode (colors slide continuously)
139
+ gradientFlowAngle: 45, // Flow direction in degrees
140
+ });
141
+ ```
142
+
143
+ ### Linear Gradient (Spin/Rotate)
144
+
145
+ ```js
146
+ new ParticleNetwork(canvas, {
147
+ gradientEnabled: true,
148
+ gradientType: "linear",
149
+ gradientColors: ["#667eea", "#764ba2"],
150
+ gradientSpeed: 0.01,
151
+ gradientSpin: true, // Rotate the gradient angle
152
+ gradientAngle: 0, // Starting angle
153
+ });
154
+ ```
155
+
156
+ ### Radial Gradient (Mouse Interactive)
157
+
158
+ ```js
159
+ new ParticleNetwork(canvas, {
160
+ gradientEnabled: true,
161
+ gradientType: "radial",
162
+ gradientColors: ["#667eea", "#764ba2", "#f093fb"],
163
+ gradientSpeed: 0.02,
164
+ gradientMouseReaction: true, // Center follows mouse
165
+ gradientMouseInfluence: 0.5, // 0 = no effect, 1 = full follow
166
+ gradientOrbitRadius: 0.3, // Orbit radius when no mouse
167
+ });
168
+ ```
169
+
170
+ ## API
171
+
172
+ ### React (`particle-network-bg/react`)
173
+
174
+ - `ParticleNetworkBg` – Wrapper component. Props: `config`, `style`, `className`
175
+ - `useParticleNetwork(config?)` – Hook that returns a canvas ref
176
+
177
+ ### `ParticleNetwork` (vanilla)
178
+
179
+ - `start()` – Start animation
180
+ - `stop()` – Stop animation
181
+ - `cleanup()` – Remove listeners and stop (call on unmount)
182
+ - `updateConfig(property, value)` – Update a config value at runtime
183
+ - `reset(defaults)` – Reset to new defaults
184
+
185
+ ### Types
186
+
187
+ ```ts
188
+ import type {
189
+ ParticleNetworkConfig,
190
+ Particle,
191
+ GradientType,
192
+ } from "particle-network-bg";
193
+ ```
194
+
195
+ ## License
196
+
197
+ MIT
198
+
@@ -0,0 +1,449 @@
1
+ // src/index.ts
2
+ var DEFAULT_CONFIG = {
3
+ particleCount: 100,
4
+ minRadius: 2,
5
+ maxRadius: 6,
6
+ particleColor: "#000000",
7
+ lineColor: "#000000",
8
+ lineWidth: 1,
9
+ lineOpacity: 0.2,
10
+ maxDistance: 150,
11
+ moveSpeed: 1,
12
+ backgroundColor: "#ffffff",
13
+ backgroundOpacity: 1,
14
+ particleOpacity: 1,
15
+ mouseRadius: 200,
16
+ mouseInteraction: true,
17
+ pulseEnabled: true,
18
+ pulseSpeed: 0,
19
+ depthEffectEnabled: true,
20
+ depthSpeed: 0.02,
21
+ gradientEnabled: false,
22
+ gradientType: "linear",
23
+ gradientColors: ["#667eea", "#764ba2"],
24
+ gradientSpeed: 0.02,
25
+ gradientMouseReaction: true,
26
+ gradientMouseInfluence: 0.5,
27
+ gradientAngle: 0,
28
+ gradientRadius: 1,
29
+ gradientSpin: false,
30
+ gradientFlowAngle: 45,
31
+ gradientOrbitRadius: 0.3
32
+ };
33
+ var HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
34
+ var ParticleNetwork = class {
35
+ constructor(canvas, config = {}) {
36
+ this.particles = [];
37
+ this.animationId = null;
38
+ this.isRunning = false;
39
+ this.mousePosition = null;
40
+ this.pulseAngle = 0;
41
+ this.gradientAngle = 0;
42
+ this.gradientFlowOffset = 0;
43
+ this.gradientCenter = { x: 0, y: 0 };
44
+ this.smoothedMouseAngle = 0;
45
+ this.canvas = canvas;
46
+ const ctx = canvas.getContext("2d", { alpha: true });
47
+ if (!ctx) {
48
+ throw new Error("Could not initialize canvas context");
49
+ }
50
+ this.ctx = ctx;
51
+ this.config = this.validateConfig({ ...DEFAULT_CONFIG, ...config });
52
+ this.boundHandleResize = this.handleResize.bind(this);
53
+ this.boundHandleMouseMove = this.handleMouseMove.bind(this);
54
+ this.boundHandleMouseLeave = this.handleMouseLeave.bind(this);
55
+ this.handleResize();
56
+ this.createParticles();
57
+ this.setupEventListeners();
58
+ }
59
+ validateConfig(config) {
60
+ const numericParams = [
61
+ "particleCount",
62
+ "minRadius",
63
+ "maxRadius",
64
+ "lineWidth",
65
+ "lineOpacity",
66
+ "maxDistance",
67
+ "moveSpeed",
68
+ "backgroundOpacity",
69
+ "particleOpacity",
70
+ "mouseRadius",
71
+ "pulseSpeed",
72
+ "depthSpeed",
73
+ "gradientSpeed",
74
+ "gradientMouseInfluence",
75
+ "gradientAngle",
76
+ "gradientRadius",
77
+ "gradientFlowAngle",
78
+ "gradientOrbitRadius"
79
+ ];
80
+ for (const param of numericParams) {
81
+ const val = config[param];
82
+ if (typeof val !== "number" || isNaN(val)) {
83
+ throw new Error(`Invalid ${param}: must be a number`);
84
+ }
85
+ }
86
+ const booleanParams = [
87
+ "mouseInteraction",
88
+ "pulseEnabled",
89
+ "depthEffectEnabled",
90
+ "gradientEnabled",
91
+ "gradientMouseReaction",
92
+ "gradientSpin"
93
+ ];
94
+ for (const param of booleanParams) {
95
+ if (typeof config[param] !== "boolean") {
96
+ throw new Error(`Invalid ${param}: must be a boolean`);
97
+ }
98
+ }
99
+ const colorParams = [
100
+ "backgroundColor",
101
+ "particleColor",
102
+ "lineColor"
103
+ ];
104
+ for (const param of colorParams) {
105
+ if (!HEX_COLOR_REGEX.test(config[param])) {
106
+ throw new Error(`Invalid ${param}: must be a valid hex color`);
107
+ }
108
+ }
109
+ if (config.gradientEnabled) {
110
+ if (!Array.isArray(config.gradientColors) || config.gradientColors.length < 2) {
111
+ throw new Error(
112
+ "gradientColors must be an array of at least 2 hex colors"
113
+ );
114
+ }
115
+ for (const c of config.gradientColors) {
116
+ if (!HEX_COLOR_REGEX.test(c)) {
117
+ throw new Error(`Invalid gradient color: ${c} must be valid hex`);
118
+ }
119
+ }
120
+ if (config.gradientType !== "linear" && config.gradientType !== "radial") {
121
+ throw new Error("gradientType must be 'linear' or 'radial'");
122
+ }
123
+ if (config.gradientStops) {
124
+ if (config.gradientStops.length !== config.gradientColors.length) {
125
+ throw new Error(
126
+ "gradientStops length must match gradientColors length"
127
+ );
128
+ }
129
+ for (let i = 0; i < config.gradientStops.length; i++) {
130
+ const s = config.gradientStops[i];
131
+ if (typeof s !== "number" || s < 0 || s > 1) {
132
+ throw new Error(`gradientStops[${i}] must be a number 0-1`);
133
+ }
134
+ if (i > 0 && s <= config.gradientStops[i - 1]) {
135
+ throw new Error("gradientStops must be strictly increasing");
136
+ }
137
+ }
138
+ }
139
+ }
140
+ return config;
141
+ }
142
+ setupEventListeners() {
143
+ window.addEventListener("resize", this.boundHandleResize);
144
+ this.canvas.addEventListener("mousemove", this.boundHandleMouseMove);
145
+ this.canvas.addEventListener("mouseleave", this.boundHandleMouseLeave);
146
+ }
147
+ cleanup() {
148
+ window.removeEventListener("resize", this.boundHandleResize);
149
+ this.canvas.removeEventListener("mousemove", this.boundHandleMouseMove);
150
+ this.canvas.removeEventListener("mouseleave", this.boundHandleMouseLeave);
151
+ this.stop();
152
+ }
153
+ handleMouseMove(e) {
154
+ const rect = this.canvas.getBoundingClientRect();
155
+ this.mousePosition = {
156
+ x: e.clientX - rect.left,
157
+ y: e.clientY - rect.top
158
+ };
159
+ }
160
+ handleMouseLeave() {
161
+ this.mousePosition = null;
162
+ }
163
+ handleResize() {
164
+ this.canvas.width = window.innerWidth;
165
+ this.canvas.height = window.innerHeight;
166
+ this.gradientCenter = {
167
+ x: this.canvas.width / 2,
168
+ y: this.canvas.height / 2
169
+ };
170
+ }
171
+ createParticles() {
172
+ this.particles = [];
173
+ for (let i = 0; i < this.config.particleCount; i++) {
174
+ const sizeRange = this.config.maxRadius - this.config.minRadius;
175
+ const randomSize = Math.random() * sizeRange + this.config.minRadius;
176
+ this.particles.push({
177
+ x: Math.random() * this.canvas.width,
178
+ y: Math.random() * this.canvas.height,
179
+ dx: (Math.random() - 0.5) * this.config.moveSpeed,
180
+ dy: (Math.random() - 0.5) * this.config.moveSpeed,
181
+ radius: randomSize,
182
+ z: Math.random(),
183
+ dz: (Math.random() - 0.5) * this.config.depthSpeed * 2
184
+ });
185
+ }
186
+ }
187
+ updateParticles() {
188
+ this.particles.forEach((particle) => {
189
+ if (this.config.depthEffectEnabled) {
190
+ particle.z += particle.dz;
191
+ if (particle.z <= 0) {
192
+ particle.z = 0;
193
+ particle.dz = Math.abs(particle.dz);
194
+ } else if (particle.z >= 1) {
195
+ particle.z = 1;
196
+ particle.dz = -Math.abs(particle.dz);
197
+ }
198
+ }
199
+ if (this.config.pulseEnabled) {
200
+ this.pulseAngle += this.config.pulseSpeed;
201
+ const pulseScale = Math.sin(this.pulseAngle) * 0.5 + 1;
202
+ particle.currentRadius = particle.radius * pulseScale;
203
+ } else {
204
+ particle.currentRadius = particle.radius;
205
+ }
206
+ if (this.config.depthEffectEnabled) {
207
+ const depthScale = 0.4 + 0.6 * particle.z;
208
+ particle.currentRadius = (particle.currentRadius ?? particle.radius) * depthScale;
209
+ }
210
+ particle.x += particle.dx;
211
+ particle.y += particle.dy;
212
+ if (this.config.mouseInteraction && this.mousePosition) {
213
+ const dx = this.mousePosition.x - particle.x;
214
+ const dy = this.mousePosition.y - particle.y;
215
+ const distance = Math.sqrt(dx * dx + dy * dy);
216
+ if (distance < this.config.mouseRadius) {
217
+ const force = (this.config.mouseRadius - distance) / this.config.mouseRadius;
218
+ const angle = Math.atan2(dy, dx);
219
+ const repelX = Math.cos(angle) * force * 0.5;
220
+ const repelY = Math.sin(angle) * force * 0.5;
221
+ particle.dx -= repelX;
222
+ particle.dy -= repelY;
223
+ }
224
+ }
225
+ if (particle.x < 0 || particle.x > this.canvas.width) {
226
+ particle.dx = -particle.dx;
227
+ }
228
+ if (particle.y < 0 || particle.y > this.canvas.height) {
229
+ particle.dy = -particle.dy;
230
+ }
231
+ const speed = Math.sqrt(
232
+ particle.dx * particle.dx + particle.dy * particle.dy
233
+ );
234
+ if (speed > this.config.moveSpeed) {
235
+ particle.dx = particle.dx / speed * this.config.moveSpeed;
236
+ particle.dy = particle.dy / speed * this.config.moveSpeed;
237
+ }
238
+ });
239
+ }
240
+ drawParticles() {
241
+ this.ctx.fillStyle = this.config.particleColor;
242
+ this.particles.forEach((particle) => {
243
+ let opacity = this.config.particleOpacity;
244
+ if (this.config.depthEffectEnabled) {
245
+ opacity *= 0.6 + 0.4 * particle.z;
246
+ }
247
+ this.ctx.globalAlpha = opacity;
248
+ this.ctx.beginPath();
249
+ this.ctx.arc(
250
+ particle.x,
251
+ particle.y,
252
+ particle.currentRadius ?? particle.radius,
253
+ 0,
254
+ Math.PI * 2
255
+ );
256
+ this.ctx.fill();
257
+ });
258
+ this.ctx.globalAlpha = 1;
259
+ }
260
+ drawConnections() {
261
+ for (let i = 0; i < this.particles.length; i++) {
262
+ for (let j = i + 1; j < this.particles.length; j++) {
263
+ const dx = this.particles[i].x - this.particles[j].x;
264
+ const dy = this.particles[i].y - this.particles[j].y;
265
+ const distance = Math.sqrt(dx * dx + dy * dy);
266
+ if (distance < this.config.maxDistance) {
267
+ const opacity = 1 - distance / this.config.maxDistance;
268
+ const color = this.hexToRgb(this.config.lineColor);
269
+ this.ctx.beginPath();
270
+ this.ctx.strokeStyle = `rgba(${color}, ${opacity * this.config.lineOpacity})`;
271
+ this.ctx.lineWidth = this.config.lineWidth;
272
+ this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
273
+ this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
274
+ this.ctx.stroke();
275
+ }
276
+ }
277
+ }
278
+ }
279
+ drawBackground() {
280
+ this.ctx.globalAlpha = this.config.backgroundOpacity;
281
+ if (this.config.gradientEnabled) {
282
+ const w = this.canvas.width;
283
+ const h = this.canvas.height;
284
+ const colors = this.config.gradientColors;
285
+ const stops = this.config.gradientStops ?? colors.map((_, i) => i / (colors.length - 1));
286
+ if (this.config.gradientType === "linear") {
287
+ const baseAngleRad = this.config.gradientFlowAngle * Math.PI / 180;
288
+ const diag = Math.sqrt(w * w + h * h);
289
+ let angle;
290
+ let offsetX = 0;
291
+ let offsetY = 0;
292
+ if (this.config.gradientSpin) {
293
+ this.gradientAngle += this.config.gradientSpeed;
294
+ angle = this.gradientAngle;
295
+ } else {
296
+ this.gradientFlowOffset = (this.gradientFlowOffset + this.config.gradientSpeed) % (Math.PI * 2);
297
+ const t = this.gradientFlowOffset / (Math.PI * 2);
298
+ offsetX = Math.cos(baseAngleRad) * t * diag;
299
+ offsetY = Math.sin(baseAngleRad) * t * diag;
300
+ angle = baseAngleRad;
301
+ }
302
+ const cos = Math.cos(angle);
303
+ const sin = Math.sin(angle);
304
+ const lineLen = this.config.gradientSpin ? diag / 2 : diag * 1.5;
305
+ const cx = w / 2 + offsetX;
306
+ const cy = h / 2 + offsetY;
307
+ const x1 = cx - cos * lineLen;
308
+ const y1 = cy - sin * lineLen;
309
+ const x2 = cx + cos * lineLen;
310
+ const y2 = cy + sin * lineLen;
311
+ const gradient = this.ctx.createLinearGradient(x1, y1, x2, y2);
312
+ if (this.config.gradientSpin) {
313
+ colors.forEach((c, i) => gradient.addColorStop(stops[i], c));
314
+ } else {
315
+ const extColors = [...colors, colors[0]];
316
+ for (let rep = 0; rep < 3; rep++) {
317
+ extColors.forEach((c, i) => {
318
+ gradient.addColorStop((rep + i / (extColors.length - 1)) / 3, c);
319
+ });
320
+ }
321
+ }
322
+ this.ctx.fillStyle = gradient;
323
+ } else {
324
+ const cx = w / 2;
325
+ const cy = h / 2;
326
+ const orbitR = Math.min(w, h) * this.config.gradientOrbitRadius;
327
+ let targetX;
328
+ let targetY;
329
+ this.gradientAngle += this.config.gradientSpeed;
330
+ targetX = cx + Math.cos(this.gradientAngle) * orbitR;
331
+ targetY = cy + Math.sin(this.gradientAngle) * orbitR;
332
+ if (this.config.gradientMouseReaction && this.mousePosition) {
333
+ const influence = this.config.gradientMouseInfluence;
334
+ targetX = targetX + (this.mousePosition.x - targetX) * influence;
335
+ targetY = targetY + (this.mousePosition.y - targetY) * influence;
336
+ }
337
+ const lerpFactor = 0.03;
338
+ this.gradientCenter.x += (targetX - this.gradientCenter.x) * lerpFactor;
339
+ this.gradientCenter.y += (targetY - this.gradientCenter.y) * lerpFactor;
340
+ this.gradientFlowOffset = (this.gradientFlowOffset + this.config.gradientSpeed) % (Math.PI * 2);
341
+ const t = this.gradientFlowOffset / (Math.PI * 2);
342
+ const r = Math.max(w, h) * this.config.gradientRadius * 2;
343
+ const gradient = this.ctx.createRadialGradient(
344
+ this.gradientCenter.x,
345
+ this.gradientCenter.y,
346
+ 0,
347
+ this.gradientCenter.x,
348
+ this.gradientCenter.y,
349
+ r
350
+ );
351
+ const extColors = [...colors, colors[0]];
352
+ const entries = [];
353
+ for (let rep = 0; rep < 3; rep++) {
354
+ extColors.forEach((c, i) => {
355
+ const base = (rep + i / (extColors.length - 1)) / 3;
356
+ entries.push({ stop: (base + t) % 1, color: c });
357
+ });
358
+ }
359
+ entries.sort((a, b) => a.stop - b.stop);
360
+ if (entries[0].stop > 1e-3) {
361
+ entries.unshift({ stop: 0, color: entries[entries.length - 1].color });
362
+ }
363
+ if (entries[entries.length - 1].stop < 0.999) {
364
+ entries.push({ stop: 1, color: entries[0].color });
365
+ }
366
+ entries.forEach((e) => gradient.addColorStop(e.stop, e.color));
367
+ this.ctx.fillStyle = gradient;
368
+ }
369
+ } else {
370
+ this.ctx.fillStyle = this.config.backgroundColor;
371
+ }
372
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
373
+ this.ctx.globalAlpha = 1;
374
+ }
375
+ hexToRgb(hex) {
376
+ hex = hex.replace(/^#/, "");
377
+ if (hex.length === 3) {
378
+ hex = hex.split("").map((c) => c + c).join("");
379
+ }
380
+ const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
381
+ if (!result) return "255,255,255";
382
+ return result.slice(1).map((n) => parseInt(n, 16)).join(",");
383
+ }
384
+ stop() {
385
+ this.isRunning = false;
386
+ if (this.animationId !== null) {
387
+ cancelAnimationFrame(this.animationId);
388
+ this.animationId = null;
389
+ }
390
+ }
391
+ start() {
392
+ if (!this.isRunning) {
393
+ this.isRunning = true;
394
+ this.animate();
395
+ }
396
+ }
397
+ animate() {
398
+ this.drawBackground();
399
+ this.updateParticles();
400
+ this.drawParticles();
401
+ this.drawConnections();
402
+ if (this.isRunning) {
403
+ this.animationId = requestAnimationFrame(() => this.animate());
404
+ }
405
+ }
406
+ updateConfig(property, value) {
407
+ this.config[property] = value;
408
+ if (property === "particleCount") {
409
+ this.createParticles();
410
+ }
411
+ if (property === "moveSpeed") {
412
+ this.particles.forEach((particle) => {
413
+ const currentSpeed = Math.sqrt(
414
+ particle.dx * particle.dx + particle.dy * particle.dy
415
+ );
416
+ if (currentSpeed > 0) {
417
+ particle.dx = particle.dx / currentSpeed * value;
418
+ particle.dy = particle.dy / currentSpeed * value;
419
+ }
420
+ });
421
+ }
422
+ if (property === "minRadius" || property === "maxRadius") {
423
+ this.particles.forEach((particle) => {
424
+ const sizeRange = this.config.maxRadius - this.config.minRadius;
425
+ const randomSize = Math.random() * sizeRange + this.config.minRadius;
426
+ particle.radius = randomSize;
427
+ });
428
+ }
429
+ if (property === "depthSpeed") {
430
+ this.particles.forEach((particle) => {
431
+ const sign = particle.dz >= 0 ? 1 : -1;
432
+ particle.dz = sign * (Math.random() * 0.5 + 0.5) * value * 2;
433
+ });
434
+ }
435
+ }
436
+ reset(defaults) {
437
+ this.config = this.validateConfig({
438
+ ...DEFAULT_CONFIG,
439
+ ...defaults
440
+ });
441
+ this.createParticles();
442
+ this.stop();
443
+ this.start();
444
+ }
445
+ };
446
+
447
+ export {
448
+ ParticleNetwork
449
+ };