canvasparticles-js 4.2.4 → 4.3.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 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.1";
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) => {
@@ -67,10 +73,10 @@ class CanvasParticles {
67
73
  if (value == undefined)
68
74
  return defaultValue;
69
75
  const { min = -Infinity, max = Infinity } = clamp ?? {};
70
- if (isFinite(min) && value < min) {
76
+ if (value < min) {
71
77
  console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
72
78
  }
73
- else if (isFinite(max) && value > max) {
79
+ else if (value > max) {
74
80
  console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
75
81
  }
76
82
  return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
@@ -81,7 +87,7 @@ class CanvasParticles {
81
87
  isAnimating = false;
82
88
  lastAnimationFrame = 0;
83
89
  particles = [];
84
- particleCount = 0;
90
+ hasManualParticles = false; // set to true once @public createParticle() is used
85
91
  clientX = Infinity;
86
92
  clientY = Infinity;
87
93
  mouseX = Infinity;
@@ -168,41 +174,92 @@ class CanvasParticles {
168
174
  this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
169
175
  this.offX = (width - this.width) / 2;
170
176
  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 });
177
+ const generationType = this.option.particles.generationType;
178
+ if (generationType !== CanvasParticles.generationType.MANUAL) {
179
+ if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
180
+ this.newParticles();
181
+ else if (generationType === CanvasParticles.generationType.MATCH)
182
+ this.matchParticleCount({ updateBounds: true });
183
+ }
175
184
  if (this.isAnimating)
176
185
  this.#render();
177
186
  }
178
187
  /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
179
- #updateParticleCount() {
188
+ #targetParticleCount() {
180
189
  // 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))
190
+ let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
191
+ particleCount = Math.min(this.option.particles.max, particleCount);
192
+ if (!isFinite(particleCount))
184
193
  throw new RangeError('particleCount must be finite');
194
+ return particleCount | 0;
185
195
  }
186
196
  /** @public Remove existing particles and generate new ones */
187
197
  newParticles() {
188
- this.#updateParticleCount();
189
- this.particles = [];
190
- for (let i = 0; i < this.particleCount; i++)
191
- this.createParticle();
198
+ const particleCount = this.#targetParticleCount();
199
+ if (this.hasManualParticles) {
200
+ this.particles = this.particles.filter((particle) => particle.manual);
201
+ this.hasManualParticles = this.particles.length > 0;
202
+ }
203
+ else {
204
+ this.particles = [];
205
+ }
206
+ for (let i = 0; i < particleCount; i++)
207
+ this.#createParticle();
192
208
  }
193
209
  /** @public Adjust particle array length to match `options.particles.ppm` */
194
210
  matchParticleCount({ updateBounds = false } = {}) {
195
- this.#updateParticleCount();
196
- this.particles = this.particles.slice(0, this.particleCount);
197
- if (updateBounds)
198
- this.particles.forEach((particle) => this.#updateParticleBounds(particle));
199
- while (this.particleCount > this.particles.length)
200
- this.createParticle();
211
+ const particleCount = this.#targetParticleCount();
212
+ if (this.hasManualParticles) {
213
+ const pruned = [];
214
+ let autoCount = 0;
215
+ // Keep manual particles while pruning automatic particles that exceed `particleCount`
216
+ // Only count automatic particles towards `particledCount`
217
+ for (const particle of this.particles) {
218
+ if (autoCount >= particleCount)
219
+ break;
220
+ if (particle.manual)
221
+ autoCount++;
222
+ pruned.push(particle);
223
+ }
224
+ this.particles = pruned;
225
+ }
226
+ else {
227
+ this.particles = this.particles.slice(0, particleCount);
228
+ }
229
+ // Only necessary after resize
230
+ if (updateBounds) {
231
+ for (const particle of this.particles) {
232
+ this.#updateParticleBounds(particle);
233
+ }
234
+ }
235
+ for (let i = this.particles.length; i < particleCount; i++)
236
+ this.#createParticle();
237
+ }
238
+ /** @private Create a random new particle */
239
+ #createParticle() {
240
+ const posX = prng() * this.width;
241
+ const posY = prng() * this.height;
242
+ const particle = {
243
+ posX, // Logical position in pixels
244
+ posY, // Logical position in pixels
245
+ x: posX, // Visual position in pixels
246
+ y: posY, // Visual position in pixels
247
+ velX: 0, // Horizonal speed in pixels per update
248
+ velY: 0, // Vertical speed in pixels per update
249
+ offX: 0, // Horizontal distance from drawn to logical position in pixels
250
+ offY: 0, // Vertical distance from drawn to logical position in pixels
251
+ dir: prng() * TWO_PI, // Direction in radians
252
+ speed: (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
253
+ size: (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
254
+ gridPos: { x: 1, y: 1 },
255
+ isVisible: false,
256
+ manual: false,
257
+ };
258
+ this.#updateParticleBounds(particle);
259
+ this.particles.push(particle);
201
260
  }
202
261
  /** @public Create a new particle with optional parameters */
203
262
  createParticle(posX, posY, dir, speed, size) {
204
- posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
205
- posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
206
263
  const particle = {
207
264
  posX, // Logical position in pixels
208
265
  posY, // Logical position in pixels
@@ -212,14 +269,16 @@ class CanvasParticles {
212
269
  velY: 0, // Vertical speed in pixels per update
213
270
  offX: 0, // Horizontal distance from drawn to logical position in pixels
214
271
  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
272
+ dir: dir, // Direction in radians
273
+ speed: speed, // Velocity in pixels per update
274
+ size: size, // Ray in pixels of the particle
218
275
  gridPos: { x: 1, y: 1 },
219
276
  isVisible: false,
277
+ manual: true,
220
278
  };
221
279
  this.#updateParticleBounds(particle);
222
280
  this.particles.push(particle);
281
+ this.hasManualParticles = true;
223
282
  }
224
283
  /** @private Update the visible bounds of a particle */
225
284
  #updateParticleBounds(particle) {
@@ -231,14 +290,26 @@ class CanvasParticles {
231
290
  left: -particle.size,
232
291
  };
233
292
  }
293
+ /* @public Randomize speed and size of all particles based on current options */
294
+ updateParticles() {
295
+ const particles = this.particles;
296
+ const len = particles.length;
297
+ const relSpeed = this.option.particles.relSpeed;
298
+ const relSize = this.option.particles.relSize;
299
+ for (let i = 0; i < len; i++) {
300
+ const particle = particles[i];
301
+ particle.speed = (0.5 + prng() * 0.5) * relSpeed;
302
+ particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
303
+ }
304
+ }
234
305
  /** @private Apply gravity forces between particles */
235
306
  #updateGravity(step) {
236
307
  const isRepulsiveEnabled = this.option.gravity.repulsive > 0;
237
308
  const isPullingEnabled = this.option.gravity.pulling > 0;
238
309
  if (!isRepulsiveEnabled && !isPullingEnabled)
239
310
  return;
240
- const len = this.particleCount;
241
311
  const particles = this.particles;
312
+ const len = particles.length;
242
313
  const connectDist = this.option.particles.connectDist;
243
314
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
244
315
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
@@ -285,8 +356,8 @@ class CanvasParticles {
285
356
  }
286
357
  /** @private Update positions, directions, and visibility of all particles */
287
358
  #updateParticles(step) {
288
- const len = this.particleCount;
289
359
  const particles = this.particles;
360
+ const len = particles.length;
290
361
  const width = this.width;
291
362
  const height = this.height;
292
363
  const offX = this.offX;
@@ -380,8 +451,8 @@ class CanvasParticles {
380
451
  }
381
452
  /** @private Draw the particles on the canvas */
382
453
  #renderParticles() {
383
- const len = this.particleCount;
384
454
  const particles = this.particles;
455
+ const len = particles.length;
385
456
  const ctx = this.ctx;
386
457
  for (let i = 0; i < len; i++) {
387
458
  const particle = particles[i];
@@ -403,8 +474,8 @@ class CanvasParticles {
403
474
  }
404
475
  /** @private Draw lines between particles if they are close enough */
405
476
  #renderConnections() {
406
- const len = this.particleCount;
407
477
  const particles = this.particles;
478
+ const len = particles.length;
408
479
  const ctx = this.ctx;
409
480
  const maxDist = this.option.particles.connectDist;
410
481
  const maxDistSq = maxDist ** 2;
@@ -538,7 +609,7 @@ class CanvasParticles {
538
609
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
539
610
  },
540
611
  particles: {
541
- regenerateOnResize: !!options.particles?.regenerateOnResize,
612
+ generationType: ~~pno('particles.generationType', options.particles?.generationType, CanvasParticles.generationType.MATCH, { min: 0, max: 2 }),
542
613
  drawLines: !!(options.particles?.drawLines ?? true),
543
614
  color: options.particles?.color ?? 'black',
544
615
  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,7 @@ export default class CanvasParticles {
23
29
  isAnimating: boolean;
24
30
  private lastAnimationFrame;
25
31
  particles: Particle[];
26
- particleCount: number;
32
+ hasManualParticles: boolean;
27
33
  private clientX;
28
34
  private clientY;
29
35
  mouseX: number;
@@ -54,7 +60,8 @@ export default class CanvasParticles {
54
60
  updateBounds?: boolean;
55
61
  }): void;
56
62
  /** @public Create a new particle with optional parameters */
57
- createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number): void;
63
+ createParticle(posX: number, posY: number, dir: number, speed: number, size: number): void;
64
+ updateParticles(): void;
58
65
  /** @public Start the particle animation if it was not running before */
59
66
  start({ auto }?: {
60
67
  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.1";
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) => {
@@ -65,10 +71,10 @@ class CanvasParticles {
65
71
  if (value == undefined)
66
72
  return defaultValue;
67
73
  const { min = -Infinity, max = Infinity } = clamp ?? {};
68
- if (isFinite(min) && value < min) {
74
+ if (value < min) {
69
75
  console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
70
76
  }
71
- else if (isFinite(max) && value > max) {
77
+ else if (value > max) {
72
78
  console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
73
79
  }
74
80
  return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
@@ -79,7 +85,7 @@ class CanvasParticles {
79
85
  isAnimating = false;
80
86
  lastAnimationFrame = 0;
81
87
  particles = [];
82
- particleCount = 0;
88
+ hasManualParticles = false; // set to true once @public createParticle() is used
83
89
  clientX = Infinity;
84
90
  clientY = Infinity;
85
91
  mouseX = Infinity;
@@ -166,41 +172,92 @@ class CanvasParticles {
166
172
  this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
167
173
  this.offX = (width - this.width) / 2;
168
174
  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 });
175
+ const generationType = this.option.particles.generationType;
176
+ if (generationType !== CanvasParticles.generationType.MANUAL) {
177
+ if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
178
+ this.newParticles();
179
+ else if (generationType === CanvasParticles.generationType.MATCH)
180
+ this.matchParticleCount({ updateBounds: true });
181
+ }
173
182
  if (this.isAnimating)
174
183
  this.#render();
175
184
  }
176
185
  /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
177
- #updateParticleCount() {
186
+ #targetParticleCount() {
178
187
  // 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))
188
+ let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
189
+ particleCount = Math.min(this.option.particles.max, particleCount);
190
+ if (!isFinite(particleCount))
182
191
  throw new RangeError('particleCount must be finite');
192
+ return particleCount | 0;
183
193
  }
184
194
  /** @public Remove existing particles and generate new ones */
185
195
  newParticles() {
186
- this.#updateParticleCount();
187
- this.particles = [];
188
- for (let i = 0; i < this.particleCount; i++)
189
- this.createParticle();
196
+ const particleCount = this.#targetParticleCount();
197
+ if (this.hasManualParticles) {
198
+ this.particles = this.particles.filter((particle) => particle.manual);
199
+ this.hasManualParticles = this.particles.length > 0;
200
+ }
201
+ else {
202
+ this.particles = [];
203
+ }
204
+ for (let i = 0; i < particleCount; i++)
205
+ this.#createParticle();
190
206
  }
191
207
  /** @public Adjust particle array length to match `options.particles.ppm` */
192
208
  matchParticleCount({ updateBounds = false } = {}) {
193
- this.#updateParticleCount();
194
- this.particles = this.particles.slice(0, this.particleCount);
195
- if (updateBounds)
196
- this.particles.forEach((particle) => this.#updateParticleBounds(particle));
197
- while (this.particleCount > this.particles.length)
198
- this.createParticle();
209
+ const particleCount = this.#targetParticleCount();
210
+ if (this.hasManualParticles) {
211
+ const pruned = [];
212
+ let autoCount = 0;
213
+ // Keep manual particles while pruning automatic particles that exceed `particleCount`
214
+ // Only count automatic particles towards `particledCount`
215
+ for (const particle of this.particles) {
216
+ if (autoCount >= particleCount)
217
+ break;
218
+ if (particle.manual)
219
+ autoCount++;
220
+ pruned.push(particle);
221
+ }
222
+ this.particles = pruned;
223
+ }
224
+ else {
225
+ this.particles = this.particles.slice(0, particleCount);
226
+ }
227
+ // Only necessary after resize
228
+ if (updateBounds) {
229
+ for (const particle of this.particles) {
230
+ this.#updateParticleBounds(particle);
231
+ }
232
+ }
233
+ for (let i = this.particles.length; i < particleCount; i++)
234
+ this.#createParticle();
235
+ }
236
+ /** @private Create a random new particle */
237
+ #createParticle() {
238
+ const posX = prng() * this.width;
239
+ const posY = prng() * this.height;
240
+ const particle = {
241
+ posX, // Logical position in pixels
242
+ posY, // Logical position in pixels
243
+ x: posX, // Visual position in pixels
244
+ y: posY, // Visual position in pixels
245
+ velX: 0, // Horizonal speed in pixels per update
246
+ velY: 0, // Vertical speed in pixels per update
247
+ offX: 0, // Horizontal distance from drawn to logical position in pixels
248
+ offY: 0, // Vertical distance from drawn to logical position in pixels
249
+ dir: prng() * TWO_PI, // Direction in radians
250
+ speed: (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
251
+ size: (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
252
+ gridPos: { x: 1, y: 1 },
253
+ isVisible: false,
254
+ manual: false,
255
+ };
256
+ this.#updateParticleBounds(particle);
257
+ this.particles.push(particle);
199
258
  }
200
259
  /** @public Create a new particle with optional parameters */
201
260
  createParticle(posX, posY, dir, speed, size) {
202
- posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
203
- posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
204
261
  const particle = {
205
262
  posX, // Logical position in pixels
206
263
  posY, // Logical position in pixels
@@ -210,14 +267,16 @@ class CanvasParticles {
210
267
  velY: 0, // Vertical speed in pixels per update
211
268
  offX: 0, // Horizontal distance from drawn to logical position in pixels
212
269
  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
270
+ dir: dir, // Direction in radians
271
+ speed: speed, // Velocity in pixels per update
272
+ size: size, // Ray in pixels of the particle
216
273
  gridPos: { x: 1, y: 1 },
217
274
  isVisible: false,
275
+ manual: true,
218
276
  };
219
277
  this.#updateParticleBounds(particle);
220
278
  this.particles.push(particle);
279
+ this.hasManualParticles = true;
221
280
  }
222
281
  /** @private Update the visible bounds of a particle */
223
282
  #updateParticleBounds(particle) {
@@ -229,14 +288,26 @@ class CanvasParticles {
229
288
  left: -particle.size,
230
289
  };
231
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
+ for (let i = 0; i < len; i++) {
298
+ const particle = particles[i];
299
+ particle.speed = (0.5 + prng() * 0.5) * relSpeed;
300
+ particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
301
+ }
302
+ }
232
303
  /** @private Apply gravity forces between particles */
233
304
  #updateGravity(step) {
234
305
  const isRepulsiveEnabled = this.option.gravity.repulsive > 0;
235
306
  const isPullingEnabled = this.option.gravity.pulling > 0;
236
307
  if (!isRepulsiveEnabled && !isPullingEnabled)
237
308
  return;
238
- const len = this.particleCount;
239
309
  const particles = this.particles;
310
+ const len = particles.length;
240
311
  const connectDist = this.option.particles.connectDist;
241
312
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
242
313
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
@@ -283,8 +354,8 @@ class CanvasParticles {
283
354
  }
284
355
  /** @private Update positions, directions, and visibility of all particles */
285
356
  #updateParticles(step) {
286
- const len = this.particleCount;
287
357
  const particles = this.particles;
358
+ const len = particles.length;
288
359
  const width = this.width;
289
360
  const height = this.height;
290
361
  const offX = this.offX;
@@ -378,8 +449,8 @@ class CanvasParticles {
378
449
  }
379
450
  /** @private Draw the particles on the canvas */
380
451
  #renderParticles() {
381
- const len = this.particleCount;
382
452
  const particles = this.particles;
453
+ const len = particles.length;
383
454
  const ctx = this.ctx;
384
455
  for (let i = 0; i < len; i++) {
385
456
  const particle = particles[i];
@@ -401,8 +472,8 @@ class CanvasParticles {
401
472
  }
402
473
  /** @private Draw lines between particles if they are close enough */
403
474
  #renderConnections() {
404
- const len = this.particleCount;
405
475
  const particles = this.particles;
476
+ const len = particles.length;
406
477
  const ctx = this.ctx;
407
478
  const maxDist = this.option.particles.connectDist;
408
479
  const maxDistSq = maxDist ** 2;
@@ -536,7 +607,7 @@ class CanvasParticles {
536
607
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
537
608
  },
538
609
  particles: {
539
- regenerateOnResize: !!options.particles?.regenerateOnResize,
610
+ generationType: ~~pno('particles.generationType', options.particles?.generationType, CanvasParticles.generationType.MATCH, { min: 0, max: 2 }),
540
611
  drawLines: !!(options.particles?.drawLines ?? true),
541
612
  color: options.particles?.color ?? 'black',
542
613
  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.1";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 i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):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=[];hasManualParticles=!1;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.hasManualParticles?(this.particles=this.particles.filter(t=>t.manual),this.hasManualParticles=this.particles.length>0):this.particles=[];for(let i=0;i<t;i++)this.#e()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();if(this.hasManualParticles){const t=[];let e=0;for(const s of this.particles){if(e>=i)break;s.manual&&e++,t.push(s)}this.particles=t}else this.particles=this.particles.slice(0,i);if(t)for(const t of this.particles)this.#s(t);for(let t=this.particles.length;t<i;t++)this.#e()}#e(){const e=i()*this.width,s=i()*this.height,n={posX:e,posY:s,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:i()*t,speed:(.5+.5*i())*this.option.particles.relSpeed,size:(.5+2*Math.pow(i(),5))*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1,manual:!1};this.#s(n),this.particles.push(n)}createParticle(t,i,e,s,n){const o={posX:t,posY:i,x:t,y:i,velX:0,velY:0,offX:0,offY:0,dir:e,speed:s,size:n,gridPos:{x:1,y:1},isVisible:!1,manual:!0};this.#s(o),this.particles.push(o),this.hasManualParticles=!0}#s(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}}#n(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,l=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,h=i.posY-t.posY,p=n*n+h*h;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-h,-n),d=1/(p+l);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}}}#o(i){const s=this.particles,n=s.length,o=this.width,a=this.height,r=this.offX,c=this.offY,l=this.mouseX,h=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-l,b=n.posY+c-h;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.#a(n),n.isVisible=1===n.gridPos.x&&1===n.gridPos.y}}#a(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)}#r(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)}#c(){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))}}#l(){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,l=this.color.alpha*s,h=[];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.#r(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=l/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(i.x,i.y),e.stroke()):h.push([p.x,p.y,i.x,i.y]),(u+=f)>=r))break}}if(h.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<h.length;t++){const i=h[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.#c(),this.options.particles.drawLines&&this.#l()}#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.#n(i),this.#o(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});
@@ -27,6 +27,7 @@ export interface Particle {
27
27
  bounds: ParticleBounds;
28
28
  gridPos: ParticleGridPos;
29
29
  isVisible: boolean;
30
+ manual: boolean;
30
31
  }
31
32
  export interface ParticleBounds {
32
33
  top: number;
@@ -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.1",
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,11 @@
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
+ "pnpm": "^10.27.0",
34
+ "prettier": "^3.7.4",
35
+ "rollup": "^4.54.0",
36
+ "rollup-plugin-delete": "^3.0.2",
36
37
  "tslib": "^2.8.1",
37
38
  "typescript": "^5.9.3"
38
39
  },
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 */
@@ -93,9 +100,9 @@ export default class CanvasParticles {
93
100
 
94
101
  const { min = -Infinity, max = Infinity } = clamp ?? {}
95
102
 
96
- if (isFinite(min) && value < min) {
103
+ if (value < min) {
97
104
  console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
98
- } else if (isFinite(max) && value > max) {
105
+ } else if (value > max) {
99
106
  console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
100
107
  }
101
108
 
@@ -110,8 +117,7 @@ export default class CanvasParticles {
110
117
  private lastAnimationFrame: number = 0
111
118
 
112
119
  particles: Particle[] = []
113
- particleCount: number = 0
114
-
120
+ hasManualParticles = false // set to true once @public createParticle() is used
115
121
  private clientX: number = Infinity
116
122
  private clientY: number = Infinity
117
123
  mouseX: number = Infinity
@@ -210,42 +216,74 @@ export default class CanvasParticles {
210
216
  this.offX = (width - this.width) / 2
211
217
  this.offY = (height - this.height) / 2
212
218
 
213
- if (this.option.particles.regenerateOnResize || this.particles.length === 0) this.newParticles()
214
- else this.matchParticleCount({ updateBounds: true })
219
+ const generationType = this.option.particles.generationType
220
+
221
+ if (generationType !== CanvasParticles.generationType.MANUAL) {
222
+ if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0) this.newParticles()
223
+ else if (generationType === CanvasParticles.generationType.MATCH) this.matchParticleCount({ updateBounds: true })
224
+ }
215
225
 
216
226
  if (this.isAnimating) this.#render()
217
227
  }
218
228
 
219
229
  /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
220
- #updateParticleCount() {
230
+ #targetParticleCount(): number {
221
231
  // 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)
232
+ let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000)
233
+ particleCount = Math.min(this.option.particles.max, particleCount)
224
234
 
225
- if (!isFinite(this.particleCount)) throw new RangeError('particleCount must be finite')
235
+ if (!isFinite(particleCount)) throw new RangeError('particleCount must be finite')
236
+ return particleCount | 0
226
237
  }
227
238
 
228
239
  /** @public Remove existing particles and generate new ones */
229
240
  newParticles() {
230
- this.#updateParticleCount()
241
+ const particleCount = this.#targetParticleCount()
231
242
 
232
- this.particles = []
233
- for (let i = 0; i < this.particleCount; i++) this.createParticle()
243
+ if (this.hasManualParticles) {
244
+ this.particles = this.particles.filter((particle) => particle.manual)
245
+ this.hasManualParticles = this.particles.length > 0
246
+ } else {
247
+ this.particles = []
248
+ }
249
+
250
+ for (let i = 0; i < particleCount; i++) this.#createParticle()
234
251
  }
235
252
 
236
253
  /** @public Adjust particle array length to match `options.particles.ppm` */
237
254
  matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
238
- this.#updateParticleCount()
255
+ const particleCount = this.#targetParticleCount()
256
+
257
+ if (this.hasManualParticles) {
258
+ const pruned: Particle[] = []
259
+ let autoCount = 0
260
+
261
+ // Keep manual particles while pruning automatic particles that exceed `particleCount`
262
+ // Only count automatic particles towards `particledCount`
263
+ for (const particle of this.particles) {
264
+ if (autoCount >= particleCount) break
265
+ if (particle.manual) autoCount++
266
+ pruned.push(particle)
267
+ }
268
+ this.particles = pruned
269
+ } else {
270
+ this.particles = this.particles.slice(0, particleCount)
271
+ }
239
272
 
240
- this.particles = this.particles.slice(0, this.particleCount)
241
- if (updateBounds) this.particles.forEach((particle) => this.#updateParticleBounds(particle))
242
- while (this.particleCount > this.particles.length) this.createParticle()
273
+ // Only necessary after resize
274
+ if (updateBounds) {
275
+ for (const particle of this.particles) {
276
+ this.#updateParticleBounds(particle)
277
+ }
278
+ }
279
+
280
+ for (let i = this.particles.length; i < particleCount; i++) this.#createParticle()
243
281
  }
244
282
 
245
- /** @public Create a new particle with optional parameters */
246
- createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number) {
247
- posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width
248
- posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height
283
+ /** @private Create a random new particle */
284
+ #createParticle() {
285
+ const posX = prng() * this.width
286
+ const posY = prng() * this.height
249
287
 
250
288
  const particle: Omit<Particle, 'bounds'> = {
251
289
  posX, // Logical position in pixels
@@ -256,18 +294,44 @@ export default class CanvasParticles {
256
294
  velY: 0, // Vertical speed in pixels per update
257
295
  offX: 0, // Horizontal distance from drawn to logical position in pixels
258
296
  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
297
+ dir: prng() * TWO_PI, // Direction in radians
298
+ speed: (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
299
+ size: (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
262
300
  gridPos: { x: 1, y: 1 },
263
301
  isVisible: false,
302
+ manual: false,
264
303
  }
265
304
  this.#updateParticleBounds(particle)
266
- this.particles.push(particle as Particle)
305
+ this.particles.push(particle)
306
+ }
307
+
308
+ /** @public Create a new particle with optional parameters */
309
+ createParticle(posX: number, posY: number, dir: number, speed: number, size: number) {
310
+ const particle: Omit<Particle, 'bounds'> = {
311
+ posX, // Logical position in pixels
312
+ posY, // Logical position in pixels
313
+ x: posX, // Visual position in pixels
314
+ y: posY, // Visual position in pixels
315
+ velX: 0, // Horizonal speed in pixels per update
316
+ velY: 0, // Vertical speed in pixels per update
317
+ offX: 0, // Horizontal distance from drawn to logical position in pixels
318
+ offY: 0, // Vertical distance from drawn to logical position in pixels
319
+ dir: dir, // Direction in radians
320
+ speed: speed, // Velocity in pixels per update
321
+ size: size, // Ray in pixels of the particle
322
+ gridPos: { x: 1, y: 1 },
323
+ isVisible: false,
324
+ manual: true,
325
+ }
326
+ this.#updateParticleBounds(particle)
327
+ this.particles.push(particle)
328
+ this.hasManualParticles = true
267
329
  }
268
330
 
269
331
  /** @private Update the visible bounds of a particle */
270
- #updateParticleBounds(particle: Omit<Particle, 'bounds'> & Partial<Pick<Particle, 'bounds'>>) {
332
+ #updateParticleBounds(
333
+ particle: Omit<Particle, 'bounds'> & Partial<Pick<Particle, 'bounds'>>
334
+ ): asserts particle is Particle {
271
335
  // The particle is considered visible within these bounds
272
336
  particle.bounds = {
273
337
  top: -particle.size,
@@ -277,6 +341,20 @@ export default class CanvasParticles {
277
341
  }
278
342
  }
279
343
 
344
+ /* @public Randomize speed and size of all particles based on current options */
345
+ updateParticles() {
346
+ const particles = this.particles
347
+ const len = particles.length
348
+ const relSpeed = this.option.particles.relSpeed
349
+ const relSize = this.option.particles.relSize
350
+
351
+ for (let i = 0; i < len; i++) {
352
+ const particle = particles[i]
353
+ particle.speed = (0.5 + prng() * 0.5) * relSpeed
354
+ particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize
355
+ }
356
+ }
357
+
280
358
  /** @private Apply gravity forces between particles */
281
359
  #updateGravity(step: number) {
282
360
  const isRepulsiveEnabled = this.option.gravity.repulsive > 0
@@ -284,8 +362,8 @@ export default class CanvasParticles {
284
362
 
285
363
  if (!isRepulsiveEnabled && !isPullingEnabled) return
286
364
 
287
- const len = this.particleCount
288
365
  const particles = this.particles
366
+ const len = particles.length
289
367
  const connectDist = this.option.particles.connectDist
290
368
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step
291
369
  const gravPullingMult = connectDist * this.option.gravity.pulling * step
@@ -340,8 +418,8 @@ export default class CanvasParticles {
340
418
 
341
419
  /** @private Update positions, directions, and visibility of all particles */
342
420
  #updateParticles(step: number) {
343
- const len = this.particleCount
344
421
  const particles = this.particles
422
+ const len = particles.length
345
423
  const width = this.width
346
424
  const height = this.height
347
425
  const offX = this.offX
@@ -450,8 +528,8 @@ export default class CanvasParticles {
450
528
 
451
529
  /** @private Draw the particles on the canvas */
452
530
  #renderParticles() {
453
- const len = this.particleCount
454
531
  const particles = this.particles
532
+ const len = particles.length
455
533
  const ctx = this.ctx
456
534
 
457
535
  for (let i = 0; i < len; i++) {
@@ -475,8 +553,8 @@ export default class CanvasParticles {
475
553
 
476
554
  /** @private Draw lines between particles if they are close enough */
477
555
  #renderConnections() {
478
- const len = this.particleCount
479
556
  const particles = this.particles
557
+ const len = particles.length
480
558
  const ctx = this.ctx
481
559
  const maxDist = this.option.particles.connectDist
482
560
  const maxDistSq = maxDist ** 2
@@ -632,13 +710,18 @@ export default class CanvasParticles {
632
710
  options.mouse?.interactionType,
633
711
  CanvasParticles.interactionType.MOVE,
634
712
  { min: 0, max: 2 }
635
- ),
713
+ ) as 0 | 1 | 2,
636
714
  connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
637
715
  connectDist: 1 /* post processed */,
638
716
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
639
717
  },
640
718
  particles: {
641
- regenerateOnResize: !!options.particles?.regenerateOnResize,
719
+ generationType: ~~pno(
720
+ 'particles.generationType',
721
+ options.particles?.generationType,
722
+ CanvasParticles.generationType.MATCH,
723
+ { min: 0, max: 2 }
724
+ ) as 0 | 1 | 2,
642
725
  drawLines: !!(options.particles?.drawLines ?? true),
643
726
  color: options.particles?.color ?? 'black',
644
727
  ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
@@ -30,6 +30,7 @@ export interface Particle {
30
30
  bounds: ParticleBounds
31
31
  gridPos: ParticleGridPos
32
32
  isVisible: boolean
33
+ manual: boolean
33
34
  }
34
35
 
35
36
  export interface ParticleBounds {
@@ -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