canvasparticles-js 4.3.1 → 4.3.3

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
@@ -337,16 +337,19 @@ By default `particles.ppm` and `particles.max` are used to auto-generate random
337
337
  const canvas = '#my-canvas'
338
338
  const options = {
339
339
  particles: {
340
- generationType: CanvasParticles.generationType.MANUAL, // = 0
340
+ max: 0,
341
341
  rotationSpeed: 0,
342
342
  },
343
343
  }
344
344
  const instance = new CanvasParticles(canvas, options).start()
345
345
 
346
346
  // Create a horizontal line of particles moving down
347
- for (let x = 100; x < 300; x += 4) {
347
+ for (let x = 0; x < instance.width; x += 4) {
348
348
  instance.createParticle(x, 100, 0, 1, 5)
349
349
  }
350
+
351
+ // Keep automatically generated particles and remove manually created ones
352
+ instance.newParticles({ keepAuto: true, keepManual: false })
350
353
  ```
351
354
 
352
355
  ---
package/dist/index.cjs CHANGED
@@ -21,7 +21,7 @@ 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.3.1";
24
+ static version = "4.3.3";
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 */
@@ -45,9 +45,9 @@ class CanvasParticles {
45
45
  if (!instance.options?.animation)
46
46
  return;
47
47
  if ((canvas.inViewbox = entry.isIntersecting))
48
- instance.options.animation?.startOnEnter && instance.start({ auto: true });
48
+ instance.option.animation?.startOnEnter && instance.start({ auto: true });
49
49
  else
50
- instance.options.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
50
+ instance.option.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
51
51
  }
52
52
  }, {
53
53
  rootMargin: '-1px',
@@ -184,7 +184,7 @@ class CanvasParticles {
184
184
  if (this.isAnimating)
185
185
  this.#render();
186
186
  }
187
- /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
187
+ /** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
188
188
  #targetParticleCount() {
189
189
  // Amount of particles to be created
190
190
  let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
@@ -194,19 +194,21 @@ class CanvasParticles {
194
194
  return particleCount | 0;
195
195
  }
196
196
  /** @public Remove existing particles and generate new ones */
197
- newParticles() {
197
+ newParticles({ keepAuto = false, keepManual = true } = {}) {
198
198
  const particleCount = this.#targetParticleCount();
199
- if (this.hasManualParticles) {
200
- this.particles = this.particles.filter((particle) => particle.manual);
199
+ if (this.hasManualParticles && (keepAuto || keepManual)) {
200
+ this.particles = this.particles.filter((particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual));
201
201
  this.hasManualParticles = this.particles.length > 0;
202
202
  }
203
203
  else {
204
204
  this.particles = [];
205
205
  }
206
- for (let i = 0; i < particleCount; i++)
207
- this.#createParticle();
206
+ if (!keepAuto) {
207
+ for (let i = 0; i < particleCount; i++)
208
+ this.#createParticle();
209
+ }
208
210
  }
209
- /** @public Adjust particle array length to match `options.particles.ppm` */
211
+ /** @public Adjust particle array length to match `option.particles.ppm` */
210
212
  matchParticleCount({ updateBounds = false } = {}) {
211
213
  const particleCount = this.#targetParticleCount();
212
214
  if (this.hasManualParticles) {
@@ -215,11 +217,14 @@ class CanvasParticles {
215
217
  // Keep manual particles while pruning automatic particles that exceed `particleCount`
216
218
  // Only count automatic particles towards `particledCount`
217
219
  for (const particle of this.particles) {
220
+ if (particle.isManual) {
221
+ pruned.push(particle);
222
+ continue;
223
+ }
218
224
  if (autoCount >= particleCount)
219
- break;
220
- if (particle.manual)
221
- autoCount++;
225
+ continue;
222
226
  pruned.push(particle);
227
+ autoCount++;
223
228
  }
224
229
  this.particles = pruned;
225
230
  }
@@ -239,27 +244,10 @@ class CanvasParticles {
239
244
  #createParticle() {
240
245
  const posX = prng() * this.width;
241
246
  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);
247
+ this.createParticle(posX, posY, prng() * TWO_PI, (0.5 + prng() * 0.5) * this.option.particles.relSpeed, (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, false);
260
248
  }
261
249
  /** @public Create a new particle with optional parameters */
262
- createParticle(posX, posY, dir, speed, size) {
250
+ createParticle(posX, posY, dir, speed, size, isManual = true) {
263
251
  const particle = {
264
252
  posX, // Logical position in pixels
265
253
  posY, // Logical position in pixels
@@ -274,7 +262,7 @@ class CanvasParticles {
274
262
  size: size, // Ray in pixels of the particle
275
263
  gridPos: { x: 1, y: 1 },
276
264
  isVisible: false,
277
- manual: true,
265
+ isManual,
278
266
  };
279
267
  this.#updateParticleBounds(particle);
280
268
  this.particles.push(particle);
@@ -292,14 +280,12 @@ class CanvasParticles {
292
280
  }
293
281
  /* @public Randomize speed and size of all particles based on current options */
294
282
  updateParticles() {
295
- const particles = this.particles;
296
- const len = particles.length;
297
283
  const relSpeed = this.option.particles.relSpeed;
298
284
  const relSize = this.option.particles.relSize;
299
- for (let i = 0; i < len; i++) {
300
- const particle = particles[i];
285
+ for (const particle of this.particles) {
301
286
  particle.speed = (0.5 + prng() * 0.5) * relSpeed;
302
287
  particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
288
+ this.#updateParticleBounds(particle); // because size changed
303
289
  }
304
290
  }
305
291
  /** @private Apply gravity forces between particles */
@@ -445,7 +431,7 @@ class CanvasParticles {
445
431
  // Visible if either particle is in the center
446
432
  if (particleA.isVisible || particleB.isVisible)
447
433
  return true;
448
- // Not visible if both particles are in the same vertical or horizontal line but outside the center
434
+ // Not visible if both particles are in the same vertical or horizontal line that does not cross the center
449
435
  return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
450
436
  (particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
451
437
  }
@@ -539,7 +525,7 @@ class CanvasParticles {
539
525
  this.ctx.strokeStyle = this.color.hex;
540
526
  this.ctx.lineWidth = 1;
541
527
  this.#renderParticles();
542
- if (this.options.particles.drawLines)
528
+ if (this.option.particles.drawLines)
543
529
  this.#renderConnections();
544
530
  }
545
531
  /** @private Main animation loop that updates and renders the particles */
package/dist/index.d.ts CHANGED
@@ -54,13 +54,16 @@ export default class CanvasParticles {
54
54
  /** @public Resize the canvas and update particles accordingly */
55
55
  resizeCanvas(): void;
56
56
  /** @public Remove existing particles and generate new ones */
57
- newParticles(): void;
58
- /** @public Adjust particle array length to match `options.particles.ppm` */
57
+ newParticles({ keepAuto, keepManual }?: {
58
+ keepAuto?: boolean | undefined;
59
+ keepManual?: boolean | undefined;
60
+ }): void;
61
+ /** @public Adjust particle array length to match `option.particles.ppm` */
59
62
  matchParticleCount({ updateBounds }?: {
60
63
  updateBounds?: boolean;
61
64
  }): void;
62
65
  /** @public Create a new particle with optional parameters */
63
- createParticle(posX: number, posY: number, dir: number, speed: number, size: number): void;
66
+ createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual?: boolean): void;
64
67
  updateParticles(): void;
65
68
  /** @public Start the particle animation if it was not running before */
66
69
  start({ auto }?: {
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ 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.3.1";
22
+ static version = "4.3.3";
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 */
@@ -43,9 +43,9 @@ class CanvasParticles {
43
43
  if (!instance.options?.animation)
44
44
  return;
45
45
  if ((canvas.inViewbox = entry.isIntersecting))
46
- instance.options.animation?.startOnEnter && instance.start({ auto: true });
46
+ instance.option.animation?.startOnEnter && instance.start({ auto: true });
47
47
  else
48
- instance.options.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
48
+ instance.option.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
49
49
  }
50
50
  }, {
51
51
  rootMargin: '-1px',
@@ -182,7 +182,7 @@ class CanvasParticles {
182
182
  if (this.isAnimating)
183
183
  this.#render();
184
184
  }
185
- /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
185
+ /** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
186
186
  #targetParticleCount() {
187
187
  // Amount of particles to be created
188
188
  let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
@@ -192,19 +192,21 @@ class CanvasParticles {
192
192
  return particleCount | 0;
193
193
  }
194
194
  /** @public Remove existing particles and generate new ones */
195
- newParticles() {
195
+ newParticles({ keepAuto = false, keepManual = true } = {}) {
196
196
  const particleCount = this.#targetParticleCount();
197
- if (this.hasManualParticles) {
198
- this.particles = this.particles.filter((particle) => particle.manual);
197
+ if (this.hasManualParticles && (keepAuto || keepManual)) {
198
+ this.particles = this.particles.filter((particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual));
199
199
  this.hasManualParticles = this.particles.length > 0;
200
200
  }
201
201
  else {
202
202
  this.particles = [];
203
203
  }
204
- for (let i = 0; i < particleCount; i++)
205
- this.#createParticle();
204
+ if (!keepAuto) {
205
+ for (let i = 0; i < particleCount; i++)
206
+ this.#createParticle();
207
+ }
206
208
  }
207
- /** @public Adjust particle array length to match `options.particles.ppm` */
209
+ /** @public Adjust particle array length to match `option.particles.ppm` */
208
210
  matchParticleCount({ updateBounds = false } = {}) {
209
211
  const particleCount = this.#targetParticleCount();
210
212
  if (this.hasManualParticles) {
@@ -213,11 +215,14 @@ class CanvasParticles {
213
215
  // Keep manual particles while pruning automatic particles that exceed `particleCount`
214
216
  // Only count automatic particles towards `particledCount`
215
217
  for (const particle of this.particles) {
218
+ if (particle.isManual) {
219
+ pruned.push(particle);
220
+ continue;
221
+ }
216
222
  if (autoCount >= particleCount)
217
- break;
218
- if (particle.manual)
219
- autoCount++;
223
+ continue;
220
224
  pruned.push(particle);
225
+ autoCount++;
221
226
  }
222
227
  this.particles = pruned;
223
228
  }
@@ -237,27 +242,10 @@ class CanvasParticles {
237
242
  #createParticle() {
238
243
  const posX = prng() * this.width;
239
244
  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);
245
+ this.createParticle(posX, posY, prng() * TWO_PI, (0.5 + prng() * 0.5) * this.option.particles.relSpeed, (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, false);
258
246
  }
259
247
  /** @public Create a new particle with optional parameters */
260
- createParticle(posX, posY, dir, speed, size) {
248
+ createParticle(posX, posY, dir, speed, size, isManual = true) {
261
249
  const particle = {
262
250
  posX, // Logical position in pixels
263
251
  posY, // Logical position in pixels
@@ -272,7 +260,7 @@ class CanvasParticles {
272
260
  size: size, // Ray in pixels of the particle
273
261
  gridPos: { x: 1, y: 1 },
274
262
  isVisible: false,
275
- manual: true,
263
+ isManual,
276
264
  };
277
265
  this.#updateParticleBounds(particle);
278
266
  this.particles.push(particle);
@@ -290,14 +278,12 @@ class CanvasParticles {
290
278
  }
291
279
  /* @public Randomize speed and size of all particles based on current options */
292
280
  updateParticles() {
293
- const particles = this.particles;
294
- const len = particles.length;
295
281
  const relSpeed = this.option.particles.relSpeed;
296
282
  const relSize = this.option.particles.relSize;
297
- for (let i = 0; i < len; i++) {
298
- const particle = particles[i];
283
+ for (const particle of this.particles) {
299
284
  particle.speed = (0.5 + prng() * 0.5) * relSpeed;
300
285
  particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
286
+ this.#updateParticleBounds(particle); // because size changed
301
287
  }
302
288
  }
303
289
  /** @private Apply gravity forces between particles */
@@ -443,7 +429,7 @@ class CanvasParticles {
443
429
  // Visible if either particle is in the center
444
430
  if (particleA.isVisible || particleB.isVisible)
445
431
  return true;
446
- // Not visible if both particles are in the same vertical or horizontal line but outside the center
432
+ // Not visible if both particles are in the same vertical or horizontal line that does not cross the center
447
433
  return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
448
434
  (particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
449
435
  }
@@ -537,7 +523,7 @@ class CanvasParticles {
537
523
  this.ctx.strokeStyle = this.color.hex;
538
524
  this.ctx.lineWidth = 1;
539
525
  this.#renderParticles();
540
- if (this.options.particles.drawLines)
526
+ if (this.option.particles.drawLines)
541
527
  this.#renderConnections();
542
528
  }
543
529
  /** @private Main animation loop that updates and renders the particles */
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.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});
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.3";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.option.animation?.startOnEnter&&n.start({auto:!0}):n.option.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({keepAuto:t=!1,keepManual:i=!0}={}){const e=this.#i();if(this.hasManualParticles&&(t||i)?(this.particles=this.particles.filter(e=>t&&!e.isManual||i&&e.isManual),this.hasManualParticles=this.particles.length>0):this.particles=[],!t)for(let t=0;t<e;t++)this.#e()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();if(this.hasManualParticles){const t=[];let e=0;for(const s of this.particles)s.isManual?t.push(s):e>=i||(t.push(s),e++);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;this.createParticle(e,s,i()*t,(.5+.5*i())*this.option.particles.relSpeed,(.5+2*Math.pow(i(),5))*this.option.particles.relSize,!1)}createParticle(t,i,e,s,n,o=!0){const a={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,isManual:o};this.#s(a),this.particles.push(a),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.option.particles.relSpeed,e=this.option.particles.relSize;for(const s of this.particles)s.speed=(.5+.5*i())*t,s.size=(.5+2*Math.pow(i(),5))*e,this.#s(s)}#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,M=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(M+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 x=n.posX+r-l,b=n.posY+c-h;if(!f){const t=d/Math.hypot(x,b);m<t?(n.offX+=(t*x-x-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.option.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,7 +27,7 @@ export interface Particle {
27
27
  bounds: ParticleBounds;
28
28
  gridPos: ParticleGridPos;
29
29
  isVisible: boolean;
30
- manual: boolean;
30
+ isManual: boolean;
31
31
  }
32
32
  export interface ParticleBounds {
33
33
  top: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.3.1",
3
+ "version": "4.3.3",
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.10.4",
33
- "pnpm": "^10.27.0",
32
+ "@types/node": "^24.10.7",
33
+ "pnpm": "^10.28.0",
34
34
  "prettier": "^3.7.4",
35
- "rollup": "^4.54.0",
35
+ "rollup": "^4.55.1",
36
36
  "rollup-plugin-delete": "^3.0.2",
37
37
  "tslib": "^2.8.1",
38
38
  "typescript": "^5.9.3"
package/src/index.ts CHANGED
@@ -59,8 +59,8 @@ export default class CanvasParticles {
59
59
  if (!instance.options?.animation) return
60
60
 
61
61
  if ((canvas.inViewbox = entry.isIntersecting))
62
- instance.options.animation?.startOnEnter && instance.start({ auto: true })
63
- else instance.options.animation?.stopOnLeave && instance.stop({ auto: true, clear: false })
62
+ instance.option.animation?.startOnEnter && instance.start({ auto: true })
63
+ else instance.option.animation?.stopOnLeave && instance.stop({ auto: true, clear: false })
64
64
  }
65
65
  },
66
66
  {
@@ -226,7 +226,7 @@ export default class CanvasParticles {
226
226
  if (this.isAnimating) this.#render()
227
227
  }
228
228
 
229
- /** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
229
+ /** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
230
230
  #targetParticleCount(): number {
231
231
  // Amount of particles to be created
232
232
  let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000)
@@ -237,20 +237,24 @@ export default class CanvasParticles {
237
237
  }
238
238
 
239
239
  /** @public Remove existing particles and generate new ones */
240
- newParticles() {
240
+ newParticles({ keepAuto = false, keepManual = true } = {}) {
241
241
  const particleCount = this.#targetParticleCount()
242
242
 
243
- if (this.hasManualParticles) {
244
- this.particles = this.particles.filter((particle) => particle.manual)
243
+ if (this.hasManualParticles && (keepAuto || keepManual)) {
244
+ this.particles = this.particles.filter(
245
+ (particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual)
246
+ )
245
247
  this.hasManualParticles = this.particles.length > 0
246
248
  } else {
247
249
  this.particles = []
248
250
  }
249
251
 
250
- for (let i = 0; i < particleCount; i++) this.#createParticle()
252
+ if (!keepAuto) {
253
+ for (let i = 0; i < particleCount; i++) this.#createParticle()
254
+ }
251
255
  }
252
256
 
253
- /** @public Adjust particle array length to match `options.particles.ppm` */
257
+ /** @public Adjust particle array length to match `option.particles.ppm` */
254
258
  matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
255
259
  const particleCount = this.#targetParticleCount()
256
260
 
@@ -261,9 +265,14 @@ export default class CanvasParticles {
261
265
  // Keep manual particles while pruning automatic particles that exceed `particleCount`
262
266
  // Only count automatic particles towards `particledCount`
263
267
  for (const particle of this.particles) {
264
- if (autoCount >= particleCount) break
265
- if (particle.manual) autoCount++
268
+ if (particle.isManual) {
269
+ pruned.push(particle)
270
+ continue
271
+ }
272
+
273
+ if (autoCount >= particleCount) continue
266
274
  pruned.push(particle)
275
+ autoCount++
267
276
  }
268
277
  this.particles = pruned
269
278
  } else {
@@ -285,28 +294,18 @@ export default class CanvasParticles {
285
294
  const posX = prng() * this.width
286
295
  const posY = prng() * this.height
287
296
 
288
- const particle: Omit<Particle, 'bounds'> = {
289
- posX, // Logical position in pixels
290
- posY, // Logical position in pixels
291
- x: posX, // Visual position in pixels
292
- y: posY, // Visual position in pixels
293
- velX: 0, // Horizonal speed in pixels per update
294
- velY: 0, // Vertical speed in pixels per update
295
- offX: 0, // Horizontal distance from drawn to logical position in pixels
296
- offY: 0, // Vertical distance from drawn to logical position in pixels
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
300
- gridPos: { x: 1, y: 1 },
301
- isVisible: false,
302
- manual: false,
303
- }
304
- this.#updateParticleBounds(particle)
305
- this.particles.push(particle)
297
+ this.createParticle(
298
+ posX,
299
+ posY,
300
+ prng() * TWO_PI,
301
+ (0.5 + prng() * 0.5) * this.option.particles.relSpeed,
302
+ (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize,
303
+ false
304
+ )
306
305
  }
307
306
 
308
307
  /** @public Create a new particle with optional parameters */
309
- createParticle(posX: number, posY: number, dir: number, speed: number, size: number) {
308
+ createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual = true) {
310
309
  const particle: Omit<Particle, 'bounds'> = {
311
310
  posX, // Logical position in pixels
312
311
  posY, // Logical position in pixels
@@ -321,7 +320,7 @@ export default class CanvasParticles {
321
320
  size: size, // Ray in pixels of the particle
322
321
  gridPos: { x: 1, y: 1 },
323
322
  isVisible: false,
324
- manual: true,
323
+ isManual,
325
324
  }
326
325
  this.#updateParticleBounds(particle)
327
326
  this.particles.push(particle)
@@ -343,15 +342,13 @@ export default class CanvasParticles {
343
342
 
344
343
  /* @public Randomize speed and size of all particles based on current options */
345
344
  updateParticles() {
346
- const particles = this.particles
347
- const len = particles.length
348
345
  const relSpeed = this.option.particles.relSpeed
349
346
  const relSize = this.option.particles.relSize
350
347
 
351
- for (let i = 0; i < len; i++) {
352
- const particle = particles[i]
348
+ for (const particle of this.particles) {
353
349
  particle.speed = (0.5 + prng() * 0.5) * relSpeed
354
350
  particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize
351
+ this.#updateParticleBounds(particle) // because size changed
355
352
  }
356
353
  }
357
354
 
@@ -519,7 +516,7 @@ export default class CanvasParticles {
519
516
  // Visible if either particle is in the center
520
517
  if (particleA.isVisible || particleB.isVisible) return true
521
518
 
522
- // Not visible if both particles are in the same vertical or horizontal line but outside the center
519
+ // Not visible if both particles are in the same vertical or horizontal line that does not cross the center
523
520
  return !(
524
521
  (particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
525
522
  (particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
@@ -629,7 +626,7 @@ export default class CanvasParticles {
629
626
  this.ctx.lineWidth = 1
630
627
 
631
628
  this.#renderParticles()
632
- if (this.options.particles.drawLines) this.#renderConnections()
629
+ if (this.option.particles.drawLines) this.#renderConnections()
633
630
  }
634
631
 
635
632
  /** @private Main animation loop that updates and renders the particles */
@@ -30,7 +30,7 @@ export interface Particle {
30
30
  bounds: ParticleBounds
31
31
  gridPos: ParticleGridPos
32
32
  isVisible: boolean
33
- manual: boolean
33
+ isManual: boolean
34
34
  }
35
35
 
36
36
  export interface ParticleBounds {