canvasparticles-js 4.2.4 → 4.3.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,6 +15,7 @@ Creating a fun and interactive background. Colors, interaction and gravity can b
15
15
  [Implementation](#implementation)<br>
16
16
  [Class Instantiation](#class-instantiation)<br>
17
17
  [Options](#options)<br>
18
+ [Manually creating particles](#manually-creating-particles)<br>
18
19
  [One Pager Example](#one-pager-example)
19
20
 
20
21
  ---
@@ -212,18 +213,18 @@ Your screen resolution and refresh rate will directly impact perfomance!
212
213
 
213
214
  ### `particles`
214
215
 
215
- | Option | Type | Default | Description |
216
- | ------------------------------ | --------- | ---------- | ------------------------------------------------------------------------------------------------- |
217
- | `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
218
- | `particles.ppm` | `integer` | `100` | Particles per million pixels. _Heavily impacts performance_ |
219
- | `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
220
- | `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
221
- | `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). _Heavily impacts performance_ |
222
- | `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
223
- | `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
224
- | `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
225
- | `particles.regenerateOnResize` | `boolean` | `false` | Regenerate all particles when the canvas resizes. |
226
- | `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
216
+ | Option | Type | Default | Description |
217
+ | --------------------------- | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- |
218
+ | `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `0 = OFF`, `1 = NEW`, `2 = MATCH` |
219
+ | `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
220
+ | `particles.ppm` | `integer` | `100` | Particles per million pixels. _Heavily impacts performance_ |
221
+ | `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
222
+ | `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
223
+ | `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). _Heavily impacts performance_ |
224
+ | `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
225
+ | `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
226
+ | `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
227
+ | `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
227
228
 
228
229
  ---
229
230
 
@@ -273,13 +274,32 @@ instance.option.particles.ppm = 100
273
274
  instance.option.particles.max = 300
274
275
  ```
275
276
 
276
- The changes are only applied when one of the following methods is called.
277
+ The changes are only applied when one of the following methods is called:
277
278
 
278
279
  ```js
279
280
  instance.newParticles() // Remove all particles and create the correct amount of new ones
280
281
  instance.matchParticleCount() // Add or remove some particles to match the count
281
282
  ```
282
283
 
284
+ ### Changing particle properties
285
+
286
+ After updating the following options, the particles are **not automatically updated**:
287
+
288
+ - `particles.relSize`
289
+ - `particles.relSpeed`
290
+
291
+ ```js
292
+ // Note: the backing field is called `option` not `options`!
293
+ instance.option.particles.relSize = 2
294
+ instance.option.particles.relSpeed = 3
295
+ ```
296
+
297
+ The changes are only applied when the following method is called:
298
+
299
+ ```js
300
+ instance.updateParticles() // Updates the particle.speed and particle.size properties without regenerating any particles
301
+ ```
302
+
283
303
  #### Modifying object properties
284
304
 
285
305
  **All** other options can be updated by only modifying the `option` internal field properties, with changes taking effect immediately.
@@ -297,12 +317,40 @@ instance.option.gravity.repulsive = 1
297
317
 
298
318
  To reinitialize all options, pass a new options object to the `options` setter.
299
319
 
320
+ > Existing particles their properties will not be updated automatically. [Changing particle properties](#changing-particle-properties)
321
+
300
322
  ```js
301
323
  instance.options = { ... }
302
324
  ```
303
325
 
304
326
  ---
305
327
 
328
+ ## Manually creating particles
329
+
330
+ ```ts
331
+ createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number)
332
+ ```
333
+
334
+ By default `particles.ppm` and `particles.max` are used to auto-generate random particles. This might destroy manually created particles. To fix this, set `particles.generationType` to `MANUAL (0)`.
335
+
336
+ ```js
337
+ const canvas = '#my-canvas'
338
+ const options = {
339
+ particles: {
340
+ generationType: CanvasParticles.generationType.MANUAL, // = 0
341
+ rotationSpeed: 0,
342
+ },
343
+ }
344
+ const instance = new CanvasParticles(canvas, options).start()
345
+
346
+ // Create a horizontal line of particles moving down
347
+ for (let x = 100; x < 300; x += 4) {
348
+ instance.createParticle(x, 100, 0, 1, 5)
349
+ }
350
+ ```
351
+
352
+ ---
353
+
306
354
  ## One Pager Example
307
355
 
308
356
  ```html
package/dist/index.cjs CHANGED
@@ -21,14 +21,20 @@ function Mulberry32(seed) {
21
21
  // Spectral test: /demo/mulberry32.html
22
22
  const prng = Mulberry32(Math.random() * 2 ** 32).next;
23
23
  class CanvasParticles {
24
- static version = "4.2.4";
24
+ static version = "4.3.0";
25
25
  static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
26
26
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
27
27
  /** Defines mouse interaction types with the particles */
28
28
  static interactionType = Object.freeze({
29
29
  NONE: 0, // No mouse interaction
30
30
  SHIFT: 1, // Visual displacement only
31
- MOVE: 2, // Actual particle movement
31
+ MOVE: 2, // Actual particle movement (default)
32
+ });
33
+ /** Defines how the particles are auto-generated */
34
+ static generationType = Object.freeze({
35
+ MANUAL: 0, // Never auto-generate particles
36
+ NEW: 1, // Generate particles from scratch
37
+ MATCH: 2, // Add or remove particles to match new count (default)
32
38
  });
33
39
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
34
40
  static canvasIntersectionObserver = new IntersectionObserver((entries) => {
@@ -81,7 +87,6 @@ class CanvasParticles {
81
87
  isAnimating = false;
82
88
  lastAnimationFrame = 0;
83
89
  particles = [];
84
- particleCount = 0;
85
90
  clientX = Infinity;
86
91
  clientY = Infinity;
87
92
  mouseX = Infinity;
@@ -168,35 +173,39 @@ class CanvasParticles {
168
173
  this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
169
174
  this.offX = (width - this.width) / 2;
170
175
  this.offY = (height - this.height) / 2;
171
- if (this.option.particles.regenerateOnResize || this.particles.length === 0)
172
- this.newParticles();
173
- else
174
- this.matchParticleCount({ updateBounds: true });
176
+ const generationType = this.option.particles.generationType;
177
+ if (generationType !== CanvasParticles.generationType.MANUAL) {
178
+ if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
179
+ this.newParticles();
180
+ else if (generationType === CanvasParticles.generationType.MATCH)
181
+ this.matchParticleCount({ updateBounds: true });
182
+ }
175
183
  if (this.isAnimating)
176
184
  this.#render();
177
185
  }
178
186
  /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
179
- #updateParticleCount() {
187
+ #targetParticleCount() {
180
188
  // Amount of particles to be created
181
- const particleCount = ((this.option.particles.ppm * this.width * this.height) / 1_000_000) | 0;
182
- this.particleCount = Math.min(this.option.particles.max, particleCount);
183
- if (!isFinite(this.particleCount))
189
+ let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
190
+ particleCount = Math.min(this.option.particles.max, particleCount);
191
+ if (!isFinite(particleCount))
184
192
  throw new RangeError('particleCount must be finite');
193
+ return particleCount | 0;
185
194
  }
186
195
  /** @public Remove existing particles and generate new ones */
187
196
  newParticles() {
188
- this.#updateParticleCount();
197
+ const particleCount = this.#targetParticleCount();
189
198
  this.particles = [];
190
- for (let i = 0; i < this.particleCount; i++)
199
+ for (let i = 0; i < particleCount; i++)
191
200
  this.createParticle();
192
201
  }
193
202
  /** @public Adjust particle array length to match `options.particles.ppm` */
194
203
  matchParticleCount({ updateBounds = false } = {}) {
195
- this.#updateParticleCount();
196
- this.particles = this.particles.slice(0, this.particleCount);
204
+ const particleCount = this.#targetParticleCount();
205
+ this.particles = this.particles.slice(0, particleCount);
197
206
  if (updateBounds)
198
207
  this.particles.forEach((particle) => this.#updateParticleBounds(particle));
199
- while (this.particleCount > this.particles.length)
208
+ while (particleCount > this.particles.length)
200
209
  this.createParticle();
201
210
  }
202
211
  /** @public Create a new particle with optional parameters */
@@ -212,9 +221,9 @@ class CanvasParticles {
212
221
  velY: 0, // Vertical speed in pixels per update
213
222
  offX: 0, // Horizontal distance from drawn to logical position in pixels
214
223
  offY: 0, // Vertical distance from drawn to logical position in pixels
215
- dir: dir || prng() * TWO_PI, // Direction in radians
216
- speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
217
- size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
224
+ dir: dir ?? prng() * TWO_PI, // Direction in radians
225
+ speed: speed ?? (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
226
+ size: size ?? (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
218
227
  gridPos: { x: 1, y: 1 },
219
228
  isVisible: false,
220
229
  };
@@ -231,14 +240,26 @@ class CanvasParticles {
231
240
  left: -particle.size,
232
241
  };
233
242
  }
243
+ /* @public Randomize speed and size of all particles based on current options */
244
+ updateParticles() {
245
+ const particles = this.particles;
246
+ const len = particles.length;
247
+ const relSpeed = this.option.particles.relSpeed;
248
+ const relSize = this.option.particles.relSize;
249
+ for (let i = 0; i < len; i++) {
250
+ const particle = particles[i];
251
+ particle.speed = (0.5 + prng() * 0.5) * relSpeed;
252
+ particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
253
+ }
254
+ }
234
255
  /** @private Apply gravity forces between particles */
235
256
  #updateGravity(step) {
236
257
  const isRepulsiveEnabled = this.option.gravity.repulsive > 0;
237
258
  const isPullingEnabled = this.option.gravity.pulling > 0;
238
259
  if (!isRepulsiveEnabled && !isPullingEnabled)
239
260
  return;
240
- const len = this.particleCount;
241
261
  const particles = this.particles;
262
+ const len = particles.length;
242
263
  const connectDist = this.option.particles.connectDist;
243
264
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
244
265
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
@@ -285,8 +306,8 @@ class CanvasParticles {
285
306
  }
286
307
  /** @private Update positions, directions, and visibility of all particles */
287
308
  #updateParticles(step) {
288
- const len = this.particleCount;
289
309
  const particles = this.particles;
310
+ const len = particles.length;
290
311
  const width = this.width;
291
312
  const height = this.height;
292
313
  const offX = this.offX;
@@ -380,8 +401,8 @@ class CanvasParticles {
380
401
  }
381
402
  /** @private Draw the particles on the canvas */
382
403
  #renderParticles() {
383
- const len = this.particleCount;
384
404
  const particles = this.particles;
405
+ const len = particles.length;
385
406
  const ctx = this.ctx;
386
407
  for (let i = 0; i < len; i++) {
387
408
  const particle = particles[i];
@@ -403,8 +424,8 @@ class CanvasParticles {
403
424
  }
404
425
  /** @private Draw lines between particles if they are close enough */
405
426
  #renderConnections() {
406
- const len = this.particleCount;
407
427
  const particles = this.particles;
428
+ const len = particles.length;
408
429
  const ctx = this.ctx;
409
430
  const maxDist = this.option.particles.connectDist;
410
431
  const maxDistSq = maxDist ** 2;
@@ -538,7 +559,7 @@ class CanvasParticles {
538
559
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
539
560
  },
540
561
  particles: {
541
- regenerateOnResize: !!options.particles?.regenerateOnResize,
562
+ generationType: ~~pno('particles.generationType', options.particles?.generationType, CanvasParticles.generationType.MATCH, { min: 0, max: 2 }),
542
563
  drawLines: !!(options.particles?.drawLines ?? true),
543
564
  color: options.particles?.color ?? 'black',
544
565
  ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
package/dist/index.d.ts CHANGED
@@ -11,6 +11,12 @@ export default class CanvasParticles {
11
11
  SHIFT: 1;
12
12
  MOVE: 2;
13
13
  }>;
14
+ /** Defines how the particles are auto-generated */
15
+ static readonly generationType: Readonly<{
16
+ MANUAL: 0;
17
+ NEW: 1;
18
+ MATCH: 2;
19
+ }>;
14
20
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
15
21
  static readonly canvasIntersectionObserver: IntersectionObserver;
16
22
  static readonly canvasResizeObserver: ResizeObserver;
@@ -23,7 +29,6 @@ export default class CanvasParticles {
23
29
  isAnimating: boolean;
24
30
  private lastAnimationFrame;
25
31
  particles: Particle[];
26
- particleCount: number;
27
32
  private clientX;
28
33
  private clientY;
29
34
  mouseX: number;
@@ -55,6 +60,7 @@ export default class CanvasParticles {
55
60
  }): void;
56
61
  /** @public Create a new particle with optional parameters */
57
62
  createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number): void;
63
+ updateParticles(): void;
58
64
  /** @public Start the particle animation if it was not running before */
59
65
  start({ auto }?: {
60
66
  auto?: boolean;
package/dist/index.mjs CHANGED
@@ -19,14 +19,20 @@ function Mulberry32(seed) {
19
19
  // Spectral test: /demo/mulberry32.html
20
20
  const prng = Mulberry32(Math.random() * 2 ** 32).next;
21
21
  class CanvasParticles {
22
- static version = "4.2.4";
22
+ static version = "4.3.0";
23
23
  static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
24
24
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
25
25
  /** Defines mouse interaction types with the particles */
26
26
  static interactionType = Object.freeze({
27
27
  NONE: 0, // No mouse interaction
28
28
  SHIFT: 1, // Visual displacement only
29
- MOVE: 2, // Actual particle movement
29
+ MOVE: 2, // Actual particle movement (default)
30
+ });
31
+ /** Defines how the particles are auto-generated */
32
+ static generationType = Object.freeze({
33
+ MANUAL: 0, // Never auto-generate particles
34
+ NEW: 1, // Generate particles from scratch
35
+ MATCH: 2, // Add or remove particles to match new count (default)
30
36
  });
31
37
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
32
38
  static canvasIntersectionObserver = new IntersectionObserver((entries) => {
@@ -79,7 +85,6 @@ class CanvasParticles {
79
85
  isAnimating = false;
80
86
  lastAnimationFrame = 0;
81
87
  particles = [];
82
- particleCount = 0;
83
88
  clientX = Infinity;
84
89
  clientY = Infinity;
85
90
  mouseX = Infinity;
@@ -166,35 +171,39 @@ class CanvasParticles {
166
171
  this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
167
172
  this.offX = (width - this.width) / 2;
168
173
  this.offY = (height - this.height) / 2;
169
- if (this.option.particles.regenerateOnResize || this.particles.length === 0)
170
- this.newParticles();
171
- else
172
- this.matchParticleCount({ updateBounds: true });
174
+ const generationType = this.option.particles.generationType;
175
+ if (generationType !== CanvasParticles.generationType.MANUAL) {
176
+ if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
177
+ this.newParticles();
178
+ else if (generationType === CanvasParticles.generationType.MATCH)
179
+ this.matchParticleCount({ updateBounds: true });
180
+ }
173
181
  if (this.isAnimating)
174
182
  this.#render();
175
183
  }
176
184
  /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
177
- #updateParticleCount() {
185
+ #targetParticleCount() {
178
186
  // Amount of particles to be created
179
- const particleCount = ((this.option.particles.ppm * this.width * this.height) / 1_000_000) | 0;
180
- this.particleCount = Math.min(this.option.particles.max, particleCount);
181
- if (!isFinite(this.particleCount))
187
+ let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
188
+ particleCount = Math.min(this.option.particles.max, particleCount);
189
+ if (!isFinite(particleCount))
182
190
  throw new RangeError('particleCount must be finite');
191
+ return particleCount | 0;
183
192
  }
184
193
  /** @public Remove existing particles and generate new ones */
185
194
  newParticles() {
186
- this.#updateParticleCount();
195
+ const particleCount = this.#targetParticleCount();
187
196
  this.particles = [];
188
- for (let i = 0; i < this.particleCount; i++)
197
+ for (let i = 0; i < particleCount; i++)
189
198
  this.createParticle();
190
199
  }
191
200
  /** @public Adjust particle array length to match `options.particles.ppm` */
192
201
  matchParticleCount({ updateBounds = false } = {}) {
193
- this.#updateParticleCount();
194
- this.particles = this.particles.slice(0, this.particleCount);
202
+ const particleCount = this.#targetParticleCount();
203
+ this.particles = this.particles.slice(0, particleCount);
195
204
  if (updateBounds)
196
205
  this.particles.forEach((particle) => this.#updateParticleBounds(particle));
197
- while (this.particleCount > this.particles.length)
206
+ while (particleCount > this.particles.length)
198
207
  this.createParticle();
199
208
  }
200
209
  /** @public Create a new particle with optional parameters */
@@ -210,9 +219,9 @@ class CanvasParticles {
210
219
  velY: 0, // Vertical speed in pixels per update
211
220
  offX: 0, // Horizontal distance from drawn to logical position in pixels
212
221
  offY: 0, // Vertical distance from drawn to logical position in pixels
213
- dir: dir || prng() * TWO_PI, // Direction in radians
214
- speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
215
- size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
222
+ dir: dir ?? prng() * TWO_PI, // Direction in radians
223
+ speed: speed ?? (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
224
+ size: size ?? (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
216
225
  gridPos: { x: 1, y: 1 },
217
226
  isVisible: false,
218
227
  };
@@ -229,14 +238,26 @@ class CanvasParticles {
229
238
  left: -particle.size,
230
239
  };
231
240
  }
241
+ /* @public Randomize speed and size of all particles based on current options */
242
+ updateParticles() {
243
+ const particles = this.particles;
244
+ const len = particles.length;
245
+ const relSpeed = this.option.particles.relSpeed;
246
+ const relSize = this.option.particles.relSize;
247
+ for (let i = 0; i < len; i++) {
248
+ const particle = particles[i];
249
+ particle.speed = (0.5 + prng() * 0.5) * relSpeed;
250
+ particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
251
+ }
252
+ }
232
253
  /** @private Apply gravity forces between particles */
233
254
  #updateGravity(step) {
234
255
  const isRepulsiveEnabled = this.option.gravity.repulsive > 0;
235
256
  const isPullingEnabled = this.option.gravity.pulling > 0;
236
257
  if (!isRepulsiveEnabled && !isPullingEnabled)
237
258
  return;
238
- const len = this.particleCount;
239
259
  const particles = this.particles;
260
+ const len = particles.length;
240
261
  const connectDist = this.option.particles.connectDist;
241
262
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
242
263
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
@@ -283,8 +304,8 @@ class CanvasParticles {
283
304
  }
284
305
  /** @private Update positions, directions, and visibility of all particles */
285
306
  #updateParticles(step) {
286
- const len = this.particleCount;
287
307
  const particles = this.particles;
308
+ const len = particles.length;
288
309
  const width = this.width;
289
310
  const height = this.height;
290
311
  const offX = this.offX;
@@ -378,8 +399,8 @@ class CanvasParticles {
378
399
  }
379
400
  /** @private Draw the particles on the canvas */
380
401
  #renderParticles() {
381
- const len = this.particleCount;
382
402
  const particles = this.particles;
403
+ const len = particles.length;
383
404
  const ctx = this.ctx;
384
405
  for (let i = 0; i < len; i++) {
385
406
  const particle = particles[i];
@@ -401,8 +422,8 @@ class CanvasParticles {
401
422
  }
402
423
  /** @private Draw lines between particles if they are close enough */
403
424
  #renderConnections() {
404
- const len = this.particleCount;
405
425
  const particles = this.particles;
426
+ const len = particles.length;
406
427
  const ctx = this.ctx;
407
428
  const maxDist = this.option.particles.connectDist;
408
429
  const maxDistSq = maxDist ** 2;
@@ -536,7 +557,7 @@ class CanvasParticles {
536
557
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
537
558
  },
538
559
  particles: {
539
- regenerateOnResize: !!options.particles?.regenerateOnResize,
560
+ generationType: ~~pno('particles.generationType', options.particles?.generationType, CanvasParticles.generationType.MATCH, { min: 0, max: 2 }),
540
561
  drawLines: !!(options.particles?.drawLines ?? true),
541
562
  color: options.particles?.color ?? 'black',
542
563
  ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.2.4";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static defaultIfNaN=(t,i)=>isNaN(+t)?i:+t;static parseNumericOption=(t,i,s,n)=>{if(null==i)return s;const{min:o=-1/0,max:a=1/0}=n??{};return isFinite(o)&&i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):isFinite(a)&&i>a&&console.warn(new RangeError(`option.${t} was clamped to ${a} as ${i} is too high`)),e.defaultIfNaN(Math.min(Math.max(i??s,o),a),s)};canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];particleCount=0;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("particleCount must be finite")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#e(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particleCount,n=this.particles,o=this.option.particles.connectDist,a=o*this.option.gravity.repulsive*t,r=o*this.option.gravity.pulling*t,c=(o/2)**2,h=o**2/256;for(let t=0;t<s;t++){const i=n[t];for(let o=t+1;o<s;o++){const t=n[o],s=i.posX-t.posX,l=i.posY-t.posY,p=s*s+l*l;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-l,-s),d=1/(p+h);const f=Math.cos(u),v=Math.sin(u);if(p<c){m=d*a;const e=f*m,s=v*m;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;m=d*r;const g=f*m,y=v*m;i.velX+=g,i.velY+=y,t.velX-=g,t.velY-=y}}}#n(i){const s=this.particleCount,n=this.particles,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,m=this.option.mouse.distRatio,f=this.option.mouse.interactionType===e.interactionType.NONE,v=this.option.mouse.interactionType===e.interactionType.MOVE,g=1-Math.pow(.75,i);for(let e=0;e<s;e++){const s=n[e];s.dir+=2*(Math.random()-.5)*p*i,s.dir%=t;const y=Math.sin(s.dir)*s.speed,x=Math.cos(s.dir)*s.speed;s.posX+=(y+s.velX)*i,s.posY+=(x+s.velY)*i,s.posX%=o,s.posX<0&&(s.posX+=o),s.posY%=a,s.posY<0&&(s.posY+=a),s.velX*=Math.pow(u,i),s.velY*=Math.pow(u,i);const b=s.posX+r-h,M=s.posY+c-l;if(!f){const t=d/Math.hypot(b,M);m<t?(s.offX+=(t*b-b-s.offX)*g,s.offY+=(t*M-M-s.offY)*g):(s.offX-=s.offX*g,s.offY-=s.offY*g)}s.x=s.posX+s.offX,s.y=s.posY+s.offY,v&&(s.posX=s.x,s.posY=s.y),s.x+=r,s.y+=c,this.#o(s),s.isVisible=1===s.gridPos.x&&1===s.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particleCount,e=this.particles,s=this.ctx;for(let n=0;n<i;n++){const i=e[n];i.isVisible&&(i.size>1?(s.beginPath(),s.arc(i.x,i.y,i.size,0,t),s.fill(),s.closePath()):s.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}}#c(){const t=this.particleCount,i=this.particles,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<t;s++){const p=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!a&&!this.#a(p,t))continue;const s=p.x-t.x,m=p.y-t.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=h/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(t.x,t.y),e.stroke()):l.push([p.x,p.y,t.x,t.y]),(u+=f)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.options.particles.drawLines&&this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#s(i),this.#n(i),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:~~i("mouse.interactionType",t.mouse?.interactionType,e.interactionType.MOVE,{min:0,max:2}),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3,{min:0}),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3,{min:0})},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,drawLines:!!(t.particles?.drawLines??1),color:t.particles?.color??"black",ppm:~~i("particles.ppm",t.particles?.ppm,100),max:Math.round(i("particles.max",t.particles?.max,1/0,{min:0})),maxWork:Math.round(i("particles.maxWork",t.particles?.maxWork,1/0,{min:0})),connectDist:~~i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:0}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0,{min:0}),pulling:i("gravity.pulling",t.gravity?.pulling,0,{min:0}),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){const i=e.parseNumericOption("mouse.connectDistMult",t,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*i}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
1
+ !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.3.0";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({MANUAL:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static defaultIfNaN=(t,i)=>isNaN(+t)?i:+t;static parseNumericOption=(t,i,s,n)=>{if(null==i)return s;const{min:o=-1/0,max:a=1/0}=n??{};return isFinite(o)&&i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):isFinite(a)&&i>a&&console.warn(new RangeError(`option.${t} was clamped to ${a} as ${i} is too high`)),e.defaultIfNaN(Math.min(Math.max(i??s,o),a),s)};canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2;const s=this.option.particles.generationType;s!==e.generationType.MANUAL&&(s===e.generationType.NEW||0===this.particles.length?this.newParticles():s===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#t()}#i(){let t=Math.round(this.option.particles.ppm*this.width*this.height/1e6);if(t=Math.min(this.option.particles.max,t),!isFinite(t))throw new RangeError("particleCount must be finite");return 0|t}newParticles(){const t=this.#i();this.particles=[];for(let i=0;i<t;i++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();for(this.particles=this.particles.slice(0,i),t&&this.particles.forEach(t=>this.#e(t));i>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n??i()*t,speed:o??(.5+.5*i())*this.option.particles.relSpeed,size:a??(.5+2*Math.pow(i(),5))*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}updateParticles(){const t=this.particles,e=t.length,s=this.option.particles.relSpeed,n=this.option.particles.relSize;for(let o=0;o<e;o++){const e=t[o];e.speed=(.5+.5*i())*s,e.size=(.5+2*Math.pow(i(),5))*n}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particles,n=s.length,o=this.option.particles.connectDist,a=o*this.option.gravity.repulsive*t,r=o*this.option.gravity.pulling*t,c=(o/2)**2,h=o**2/256;for(let t=0;t<n;t++){const i=s[t];for(let o=t+1;o<n;o++){const t=s[o],n=i.posX-t.posX,l=i.posY-t.posY,p=n*n+l*l;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-l,-n),d=1/(p+h);const f=Math.cos(u),g=Math.sin(u);if(p<c){m=d*a;const e=f*m,s=g*m;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;m=d*r;const v=f*m,y=g*m;i.velX+=v,i.velY+=y,t.velX-=v,t.velY-=y}}}#n(i){const s=this.particles,n=s.length,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,m=this.option.mouse.distRatio,f=this.option.mouse.interactionType===e.interactionType.NONE,g=this.option.mouse.interactionType===e.interactionType.MOVE,v=1-Math.pow(.75,i);for(let e=0;e<n;e++){const n=s[e];n.dir+=2*(Math.random()-.5)*p*i,n.dir%=t;const y=Math.sin(n.dir)*n.speed,x=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(x+n.velY)*i,n.posX%=o,n.posX<0&&(n.posX+=o),n.posY%=a,n.posY<0&&(n.posY+=a),n.velX*=Math.pow(u,i),n.velY*=Math.pow(u,i);const M=n.posX+r-h,b=n.posY+c-l;if(!f){const t=d/Math.hypot(M,b);m<t?(n.offX+=(t*M-M-n.offX)*v,n.offY+=(t*b-b-n.offY)*v):(n.offX-=n.offX*v,n.offY-=n.offY*v)}n.x=n.posX+n.offX,n.y=n.posY+n.offY,g&&(n.posX=n.x,n.posY=n.y),n.x+=r,n.y+=c,this.#o(n),n.isVisible=1===n.gridPos.x&&1===n.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particles,e=i.length,s=this.ctx;for(let n=0;n<e;n++){const e=i[n];e.isVisible&&(e.size>1?(s.beginPath(),s.arc(e.x,e.y,e.size,0,t),s.fill(),s.closePath()):s.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}}#c(){const t=this.particles,i=t.length,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<i;s++){const p=t[s];let u=0;for(let d=s+1;d<i;d++){const i=t[d];if(!a&&!this.#a(p,i))continue;const s=p.x-i.x,m=p.y-i.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=h/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(i.x,i.y),e.stroke()):l.push([p.x,p.y,i.x,i.y]),(u+=f)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.options.particles.drawLines&&this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#s(i),this.#n(i),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:~~i("mouse.interactionType",t.mouse?.interactionType,e.interactionType.MOVE,{min:0,max:2}),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3,{min:0}),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3,{min:0})},particles:{generationType:~~i("particles.generationType",t.particles?.generationType,e.generationType.MATCH,{min:0,max:2}),drawLines:!!(t.particles?.drawLines??1),color:t.particles?.color??"black",ppm:~~i("particles.ppm",t.particles?.ppm,100),max:Math.round(i("particles.max",t.particles?.max,1/0,{min:0})),maxWork:Math.round(i("particles.maxWork",t.particles?.maxWork,1/0,{min:0})),connectDist:~~i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:0}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0,{min:0}),pulling:i("gravity.pulling",t.gravity?.pulling,0,{min:0}),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){const i=e.parseNumericOption("mouse.connectDistMult",t,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*i}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
@@ -5,13 +5,13 @@ export interface CanvasParticlesOptions {
5
5
  stopOnLeave: boolean;
6
6
  };
7
7
  mouse: {
8
- interactionType: number;
8
+ interactionType: 0 | 1 | 2;
9
9
  connectDistMult: number;
10
10
  connectDist: number;
11
11
  distRatio: number;
12
12
  };
13
13
  particles: {
14
- regenerateOnResize: boolean;
14
+ generationType: 0 | 1 | 2;
15
15
  drawLines: boolean;
16
16
  color: string | CanvasGradient | CanvasPattern;
17
17
  ppm: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.2.4",
3
+ "version": "4.3.0",
4
4
  "description": "In an HTML canvas, a bunch of interactive particles connected with lines when they approach each other.",
5
5
  "author": "Khoeckman",
6
6
  "license": "MIT",
@@ -29,10 +29,10 @@
29
29
  "@rollup/plugin-replace": "^6.0.3",
30
30
  "@rollup/plugin-terser": "^0.4.4",
31
31
  "@rollup/plugin-typescript": "^12.3.0",
32
- "@types/node": "^24.9.2",
33
- "prettier": "^3.6.2",
34
- "rollup": "^4.52.5",
35
- "rollup-plugin-delete": "^3.0.1",
32
+ "@types/node": "^24.10.4",
33
+ "prettier": "^3.7.4",
34
+ "rollup": "^4.54.0",
35
+ "rollup-plugin-delete": "^3.0.2",
36
36
  "tslib": "^2.8.1",
37
37
  "typescript": "^5.9.3"
38
38
  },
package/src/index.ts CHANGED
@@ -38,7 +38,14 @@ export default class CanvasParticles {
38
38
  static readonly interactionType = Object.freeze({
39
39
  NONE: 0, // No mouse interaction
40
40
  SHIFT: 1, // Visual displacement only
41
- MOVE: 2, // Actual particle movement
41
+ MOVE: 2, // Actual particle movement (default)
42
+ })
43
+
44
+ /** Defines how the particles are auto-generated */
45
+ static readonly generationType = Object.freeze({
46
+ MANUAL: 0, // Never auto-generate particles
47
+ NEW: 1, // Generate particles from scratch
48
+ MATCH: 2, // Add or remove particles to match new count (default)
42
49
  })
43
50
 
44
51
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
@@ -110,8 +117,6 @@ export default class CanvasParticles {
110
117
  private lastAnimationFrame: number = 0
111
118
 
112
119
  particles: Particle[] = []
113
- particleCount: number = 0
114
-
115
120
  private clientX: number = Infinity
116
121
  private clientY: number = Infinity
117
122
  mouseX: number = Infinity
@@ -210,36 +215,42 @@ export default class CanvasParticles {
210
215
  this.offX = (width - this.width) / 2
211
216
  this.offY = (height - this.height) / 2
212
217
 
213
- if (this.option.particles.regenerateOnResize || this.particles.length === 0) this.newParticles()
214
- else this.matchParticleCount({ updateBounds: true })
218
+ const generationType = this.option.particles.generationType
219
+
220
+ if (generationType !== CanvasParticles.generationType.MANUAL) {
221
+ if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0) this.newParticles()
222
+ else if (generationType === CanvasParticles.generationType.MATCH) this.matchParticleCount({ updateBounds: true })
223
+ }
215
224
 
216
225
  if (this.isAnimating) this.#render()
217
226
  }
218
227
 
219
228
  /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
220
- #updateParticleCount() {
229
+ #targetParticleCount(): number {
221
230
  // Amount of particles to be created
222
- const particleCount = ((this.option.particles.ppm * this.width * this.height) / 1_000_000) | 0
223
- this.particleCount = Math.min(this.option.particles.max, particleCount)
231
+ let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000)
232
+ particleCount = Math.min(this.option.particles.max, particleCount)
224
233
 
225
- if (!isFinite(this.particleCount)) throw new RangeError('particleCount must be finite')
234
+ if (!isFinite(particleCount)) throw new RangeError('particleCount must be finite')
235
+ return particleCount | 0
226
236
  }
227
237
 
228
238
  /** @public Remove existing particles and generate new ones */
229
239
  newParticles() {
230
- this.#updateParticleCount()
240
+ const particleCount = this.#targetParticleCount()
231
241
 
232
242
  this.particles = []
233
- for (let i = 0; i < this.particleCount; i++) this.createParticle()
243
+ for (let i = 0; i < particleCount; i++) this.createParticle()
234
244
  }
235
245
 
236
246
  /** @public Adjust particle array length to match `options.particles.ppm` */
237
247
  matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
238
- this.#updateParticleCount()
248
+ const particleCount = this.#targetParticleCount()
239
249
 
240
- this.particles = this.particles.slice(0, this.particleCount)
250
+ this.particles = this.particles.slice(0, particleCount)
241
251
  if (updateBounds) this.particles.forEach((particle) => this.#updateParticleBounds(particle))
242
- while (this.particleCount > this.particles.length) this.createParticle()
252
+
253
+ while (particleCount > this.particles.length) this.createParticle()
243
254
  }
244
255
 
245
256
  /** @public Create a new particle with optional parameters */
@@ -256,9 +267,9 @@ export default class CanvasParticles {
256
267
  velY: 0, // Vertical speed in pixels per update
257
268
  offX: 0, // Horizontal distance from drawn to logical position in pixels
258
269
  offY: 0, // Vertical distance from drawn to logical position in pixels
259
- dir: dir || prng() * TWO_PI, // Direction in radians
260
- speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
261
- size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
270
+ dir: dir ?? prng() * TWO_PI, // Direction in radians
271
+ speed: speed ?? (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
272
+ size: size ?? (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
262
273
  gridPos: { x: 1, y: 1 },
263
274
  isVisible: false,
264
275
  }
@@ -277,6 +288,20 @@ export default class CanvasParticles {
277
288
  }
278
289
  }
279
290
 
291
+ /* @public Randomize speed and size of all particles based on current options */
292
+ updateParticles() {
293
+ const particles = this.particles
294
+ const len = particles.length
295
+ const relSpeed = this.option.particles.relSpeed
296
+ const relSize = this.option.particles.relSize
297
+
298
+ for (let i = 0; i < len; i++) {
299
+ const particle = particles[i]
300
+ particle.speed = (0.5 + prng() * 0.5) * relSpeed
301
+ particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize
302
+ }
303
+ }
304
+
280
305
  /** @private Apply gravity forces between particles */
281
306
  #updateGravity(step: number) {
282
307
  const isRepulsiveEnabled = this.option.gravity.repulsive > 0
@@ -284,8 +309,8 @@ export default class CanvasParticles {
284
309
 
285
310
  if (!isRepulsiveEnabled && !isPullingEnabled) return
286
311
 
287
- const len = this.particleCount
288
312
  const particles = this.particles
313
+ const len = particles.length
289
314
  const connectDist = this.option.particles.connectDist
290
315
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step
291
316
  const gravPullingMult = connectDist * this.option.gravity.pulling * step
@@ -340,8 +365,8 @@ export default class CanvasParticles {
340
365
 
341
366
  /** @private Update positions, directions, and visibility of all particles */
342
367
  #updateParticles(step: number) {
343
- const len = this.particleCount
344
368
  const particles = this.particles
369
+ const len = particles.length
345
370
  const width = this.width
346
371
  const height = this.height
347
372
  const offX = this.offX
@@ -450,8 +475,8 @@ export default class CanvasParticles {
450
475
 
451
476
  /** @private Draw the particles on the canvas */
452
477
  #renderParticles() {
453
- const len = this.particleCount
454
478
  const particles = this.particles
479
+ const len = particles.length
455
480
  const ctx = this.ctx
456
481
 
457
482
  for (let i = 0; i < len; i++) {
@@ -475,8 +500,8 @@ export default class CanvasParticles {
475
500
 
476
501
  /** @private Draw lines between particles if they are close enough */
477
502
  #renderConnections() {
478
- const len = this.particleCount
479
503
  const particles = this.particles
504
+ const len = particles.length
480
505
  const ctx = this.ctx
481
506
  const maxDist = this.option.particles.connectDist
482
507
  const maxDistSq = maxDist ** 2
@@ -632,13 +657,18 @@ export default class CanvasParticles {
632
657
  options.mouse?.interactionType,
633
658
  CanvasParticles.interactionType.MOVE,
634
659
  { min: 0, max: 2 }
635
- ),
660
+ ) as 0 | 1 | 2,
636
661
  connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
637
662
  connectDist: 1 /* post processed */,
638
663
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
639
664
  },
640
665
  particles: {
641
- regenerateOnResize: !!options.particles?.regenerateOnResize,
666
+ generationType: ~~pno(
667
+ 'particles.generationType',
668
+ options.particles?.generationType,
669
+ CanvasParticles.generationType.MATCH,
670
+ { min: 0, max: 2 }
671
+ ) as 0 | 1 | 2,
642
672
  drawLines: !!(options.particles?.drawLines ?? true),
643
673
  color: options.particles?.color ?? 'black',
644
674
  ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
@@ -7,14 +7,14 @@ export interface CanvasParticlesOptions {
7
7
  }
8
8
 
9
9
  mouse: {
10
- interactionType: number
10
+ interactionType: 0 | 1 | 2 /* see CanvasParticles.interactionType */
11
11
  connectDistMult: number
12
12
  connectDist: number /* post processed */
13
13
  distRatio: number
14
14
  }
15
15
 
16
16
  particles: {
17
- regenerateOnResize: boolean
17
+ generationType: 0 | 1 | 2 /* see CanvasParticles.generationType */
18
18
  drawLines: boolean
19
19
  color: string | CanvasGradient | CanvasPattern
20
20
  ppm: number