canvasparticles-js 4.4.10 → 4.5.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/dist/index.cjs CHANGED
@@ -1,5 +1,22 @@
1
1
  'use strict';
2
2
 
3
+ /** Helper functions for options parsing */
4
+ function defaultIfNaN(value, defaultValue) {
5
+ return isNaN(+value) ? defaultValue : +value;
6
+ }
7
+ function parseNumericOption(name, value, defaultValue, clamp) {
8
+ if (value == undefined)
9
+ return defaultValue;
10
+ const { min = -Infinity, max = Infinity } = clamp ?? {};
11
+ if (value < min) {
12
+ console.warn(`option.${name} was clamped to ${min} as ${value} is too low`);
13
+ }
14
+ else if (value > max) {
15
+ console.warn(`option.${name} was clamped to ${max} as ${value} is too high`);
16
+ }
17
+ return defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
18
+ }
19
+
3
20
  // Copyright (c) 2022–2026 Kyle Hoeckman, MIT License
4
21
  // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
5
22
  const TWO_PI = 2 * Math.PI;
@@ -22,7 +39,7 @@ function Mulberry32(seed) {
22
39
  const prng = Mulberry32(Math.random() * 4294967296).next;
23
40
  class CanvasParticles {
24
41
  /** Version of the library, injected via Rollup replace plugin. */
25
- static version = "4.4.10";
42
+ static version = "4.5.1";
26
43
  static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
27
44
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
28
45
  /** Defines mouse interaction types with the particles */
@@ -37,7 +54,7 @@ class CanvasParticles {
37
54
  NEW: 1, // Generate all particles from scratch
38
55
  MATCH: 2, // Add or remove some particles to match the new count (default)
39
56
  });
40
- /** Observes canvas elements entering or leaving the viewport to start/stop animation */
57
+ /** Observes when canvas elements enter or leave the viewport to start/stop animation */
41
58
  static canvasIntersectionObserver = new IntersectionObserver((entries) => {
42
59
  for (const entry of entries) {
43
60
  const canvas = entry.target;
@@ -52,6 +69,7 @@ class CanvasParticles {
52
69
  }, {
53
70
  rootMargin: '-1px',
54
71
  });
72
+ /** Observes when canvas elements change size */
55
73
  static canvasResizeObserver = new ResizeObserver((entries) => {
56
74
  // Seperate for loops is very important to prevent huge forced reflow overhead
57
75
  // First read all canvas rects at once
@@ -67,22 +85,7 @@ class CanvasParticles {
67
85
  canvas.instance.#resizeCanvas(dpr);
68
86
  }
69
87
  });
70
- /** Helper functions for options parsing */
71
- static defaultIfNaN(value, defaultValue) {
72
- return isNaN(+value) ? defaultValue : +value;
73
- }
74
- static parseNumericOption(name, value, defaultValue, clamp) {
75
- if (value == undefined)
76
- return defaultValue;
77
- const { min = -Infinity, max = Infinity } = clamp ?? {};
78
- if (value < min) {
79
- console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
80
- }
81
- else if (value > max) {
82
- console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
83
- }
84
- return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
85
- }
88
+ static instances = new Set();
86
89
  canvas;
87
90
  ctx;
88
91
  enableAnimating = false;
@@ -127,15 +130,9 @@ class CanvasParticles {
127
130
  throw new Error('failed to get 2D context from canvas');
128
131
  this.ctx = ctx;
129
132
  this.options = options; // Uses setter
133
+ CanvasParticles.instances.add(this);
130
134
  CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
131
135
  CanvasParticles.canvasResizeObserver.observe(this.canvas);
132
- // Setup event handlers
133
- this.resizeCanvas = this.resizeCanvas.bind(this);
134
- this.handleMouseMove = this.handleMouseMove.bind(this);
135
- this.handleScroll = this.handleScroll.bind(this);
136
- // this.resizeCanvas()
137
- window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
138
- window.addEventListener('scroll', this.handleScroll, { passive: true });
139
136
  }
140
137
  updateCanvasRect() {
141
138
  const { top, left, width, height } = this.canvas.getBoundingClientRect();
@@ -223,17 +220,17 @@ class CanvasParticles {
223
220
  if (this.hasManualParticles) {
224
221
  const pruned = [];
225
222
  let autoCount = 0;
226
- // Keep manual particles while pruning automatic particles that exceed `particleCount`
227
- // Only count automatic particles towards `particledCount`
228
223
  for (const particle of this.particles) {
224
+ // Keep manual particles
229
225
  if (particle.isManual) {
230
226
  pruned.push(particle);
231
227
  continue;
232
228
  }
233
- if (autoCount >= particleCount)
234
- continue;
235
- pruned.push(particle);
236
- autoCount++;
229
+ // Only keep `autoCount` amount of automatic particles
230
+ if (autoCount < particleCount) {
231
+ pruned.push(particle);
232
+ autoCount++;
233
+ }
237
234
  }
238
235
  this.particles = pruned;
239
236
  }
@@ -243,7 +240,7 @@ class CanvasParticles {
243
240
  // Only necessary after resize
244
241
  if (updateBounds) {
245
242
  for (const particle of this.particles) {
246
- this.#updateParticleBounds(particle);
243
+ this.#updateParticleUpperBounds(particle);
247
244
  }
248
245
  }
249
246
  for (let i = this.particles.length; i < particleCount; i++)
@@ -272,21 +269,26 @@ class CanvasParticles {
272
269
  gridPos: { x: 1, y: 1 },
273
270
  isVisible: false,
274
271
  isManual,
272
+ bounds: {
273
+ top: -size,
274
+ right: this.canvas.width + size,
275
+ bottom: this.canvas.height + size,
276
+ left: -size,
277
+ },
275
278
  };
276
- this.#updateParticleBounds(particle);
277
279
  this.particles.push(particle);
278
280
  this.hasManualParticles = true;
279
281
  }
280
- /** Update the visible bounds of a particle */
281
- #updateParticleBounds(particle // Make bounds optional on particle
282
- ) {
283
- // The particle is considered visible within these bounds
284
- particle.bounds = {
285
- top: -particle.size,
286
- right: this.canvas.width + particle.size,
287
- bottom: this.canvas.height + particle.size,
288
- left: -particle.size,
289
- };
282
+ /** Updates the particle's bounding box, including size padding, for visibility checks. */
283
+ #updateParticleBounds(particle) {
284
+ particle.bounds.top = -particle.size;
285
+ particle.bounds.right = this.canvas.width + particle.size;
286
+ particle.bounds.bottom = this.canvas.height + particle.size;
287
+ particle.bounds.left = -particle.size;
288
+ }
289
+ #updateParticleUpperBounds(particle) {
290
+ particle.bounds.right = this.canvas.width + particle.size;
291
+ particle.bounds.bottom = this.canvas.height + particle.size;
290
292
  }
291
293
  /* Randomize speed and size of all particles based on current options */
292
294
  updateParticles() {
@@ -744,16 +746,15 @@ class CanvasParticles {
744
746
  /** Gracefully destroy the instance and remove the canvas element */
745
747
  destroy() {
746
748
  this.stop();
749
+ CanvasParticles.instances.delete(this);
747
750
  CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
748
751
  CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
749
- window.removeEventListener('mousemove', this.handleMouseMove);
750
- window.removeEventListener('scroll', this.handleScroll);
751
752
  this.canvas?.remove();
752
753
  Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
753
754
  }
754
755
  /** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
755
756
  set options(options) {
756
- const pno = CanvasParticles.parseNumericOption;
757
+ const pno = parseNumericOption;
757
758
  // Format and parse all options
758
759
  this.option = {
759
760
  background: options.background ?? false,
@@ -763,7 +764,6 @@ class CanvasParticles {
763
764
  },
764
765
  mouse: {
765
766
  interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
766
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
767
767
  connectDist: 1 /* post processed */,
768
768
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
769
769
  },
@@ -791,7 +791,7 @@ class CanvasParticles {
791
791
  },
792
792
  };
793
793
  this.setBackground(this.option.background);
794
- this.setMouseConnectDistMult(this.option.mouse.connectDistMult);
794
+ this.setMouseConnectDistMult(options.mouse?.connectDistMult);
795
795
  this.setParticleColor(this.option.particles.color);
796
796
  }
797
797
  get options() {
@@ -807,7 +807,7 @@ class CanvasParticles {
807
807
  }
808
808
  /** Transform the distance multiplier (float) to absolute distance (px) */
809
809
  setMouseConnectDistMult(connectDistMult) {
810
- const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
810
+ const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
811
811
  this.option.mouse.connectDist = this.option.particles.connectDist * mult;
812
812
  }
813
813
  /** Format particle color and opacity */
@@ -834,5 +834,16 @@ class CanvasParticles {
834
834
  }
835
835
  }
836
836
  }
837
+ // Global event listeners that handle all instances at once
838
+ window.addEventListener('mousemove', (e) => {
839
+ for (const instance of CanvasParticles.instances) {
840
+ instance.handleMouseMove(e);
841
+ }
842
+ }, { passive: true });
843
+ window.addEventListener('scroll', () => {
844
+ for (const instance of CanvasParticles.instances) {
845
+ instance.handleScroll();
846
+ }
847
+ }, { passive: true });
837
848
 
838
849
  module.exports = CanvasParticles;
package/dist/index.d.ts CHANGED
@@ -18,12 +18,11 @@ export default class CanvasParticles {
18
18
  NEW: 1;
19
19
  MATCH: 2;
20
20
  }>;
21
- /** Observes canvas elements entering or leaving the viewport to start/stop animation */
21
+ /** Observes when canvas elements enter or leave the viewport to start/stop animation */
22
22
  static readonly canvasIntersectionObserver: IntersectionObserver;
23
+ /** Observes when canvas elements change size */
23
24
  static readonly canvasResizeObserver: ResizeObserver;
24
- /** Helper functions for options parsing */
25
- private static defaultIfNaN;
26
- private static parseNumericOption;
25
+ static instances: Set<CanvasParticles>;
27
26
  canvas: CanvasParticlesCanvas;
28
27
  private ctx;
29
28
  enableAnimating: boolean;
@@ -83,7 +82,7 @@ export default class CanvasParticles {
83
82
  /** Sets the canvas background */
84
83
  setBackground(background: CanvasParticlesOptionsInput['background']): void;
85
84
  /** Transform the distance multiplier (float) to absolute distance (px) */
86
- setMouseConnectDistMult(connectDistMult: number): void;
85
+ setMouseConnectDistMult(connectDistMult: number | undefined): void;
87
86
  /** Format particle color and opacity */
88
87
  setParticleColor(color: string | CanvasGradient | CanvasPattern): void;
89
88
  }
package/dist/index.mjs CHANGED
@@ -1,3 +1,20 @@
1
+ /** Helper functions for options parsing */
2
+ function defaultIfNaN(value, defaultValue) {
3
+ return isNaN(+value) ? defaultValue : +value;
4
+ }
5
+ function parseNumericOption(name, value, defaultValue, clamp) {
6
+ if (value == undefined)
7
+ return defaultValue;
8
+ const { min = -Infinity, max = Infinity } = clamp ?? {};
9
+ if (value < min) {
10
+ console.warn(`option.${name} was clamped to ${min} as ${value} is too low`);
11
+ }
12
+ else if (value > max) {
13
+ console.warn(`option.${name} was clamped to ${max} as ${value} is too high`);
14
+ }
15
+ return defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
16
+ }
17
+
1
18
  // Copyright (c) 2022–2026 Kyle Hoeckman, MIT License
2
19
  // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
3
20
  const TWO_PI = 2 * Math.PI;
@@ -20,7 +37,7 @@ function Mulberry32(seed) {
20
37
  const prng = Mulberry32(Math.random() * 4294967296).next;
21
38
  class CanvasParticles {
22
39
  /** Version of the library, injected via Rollup replace plugin. */
23
- static version = "4.4.10";
40
+ static version = "4.5.1";
24
41
  static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
25
42
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
26
43
  /** Defines mouse interaction types with the particles */
@@ -35,7 +52,7 @@ class CanvasParticles {
35
52
  NEW: 1, // Generate all particles from scratch
36
53
  MATCH: 2, // Add or remove some particles to match the new count (default)
37
54
  });
38
- /** Observes canvas elements entering or leaving the viewport to start/stop animation */
55
+ /** Observes when canvas elements enter or leave the viewport to start/stop animation */
39
56
  static canvasIntersectionObserver = new IntersectionObserver((entries) => {
40
57
  for (const entry of entries) {
41
58
  const canvas = entry.target;
@@ -50,6 +67,7 @@ class CanvasParticles {
50
67
  }, {
51
68
  rootMargin: '-1px',
52
69
  });
70
+ /** Observes when canvas elements change size */
53
71
  static canvasResizeObserver = new ResizeObserver((entries) => {
54
72
  // Seperate for loops is very important to prevent huge forced reflow overhead
55
73
  // First read all canvas rects at once
@@ -65,22 +83,7 @@ class CanvasParticles {
65
83
  canvas.instance.#resizeCanvas(dpr);
66
84
  }
67
85
  });
68
- /** Helper functions for options parsing */
69
- static defaultIfNaN(value, defaultValue) {
70
- return isNaN(+value) ? defaultValue : +value;
71
- }
72
- static parseNumericOption(name, value, defaultValue, clamp) {
73
- if (value == undefined)
74
- return defaultValue;
75
- const { min = -Infinity, max = Infinity } = clamp ?? {};
76
- if (value < min) {
77
- console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
78
- }
79
- else if (value > max) {
80
- console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
81
- }
82
- return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
83
- }
86
+ static instances = new Set();
84
87
  canvas;
85
88
  ctx;
86
89
  enableAnimating = false;
@@ -125,15 +128,9 @@ class CanvasParticles {
125
128
  throw new Error('failed to get 2D context from canvas');
126
129
  this.ctx = ctx;
127
130
  this.options = options; // Uses setter
131
+ CanvasParticles.instances.add(this);
128
132
  CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
129
133
  CanvasParticles.canvasResizeObserver.observe(this.canvas);
130
- // Setup event handlers
131
- this.resizeCanvas = this.resizeCanvas.bind(this);
132
- this.handleMouseMove = this.handleMouseMove.bind(this);
133
- this.handleScroll = this.handleScroll.bind(this);
134
- // this.resizeCanvas()
135
- window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
136
- window.addEventListener('scroll', this.handleScroll, { passive: true });
137
134
  }
138
135
  updateCanvasRect() {
139
136
  const { top, left, width, height } = this.canvas.getBoundingClientRect();
@@ -221,17 +218,17 @@ class CanvasParticles {
221
218
  if (this.hasManualParticles) {
222
219
  const pruned = [];
223
220
  let autoCount = 0;
224
- // Keep manual particles while pruning automatic particles that exceed `particleCount`
225
- // Only count automatic particles towards `particledCount`
226
221
  for (const particle of this.particles) {
222
+ // Keep manual particles
227
223
  if (particle.isManual) {
228
224
  pruned.push(particle);
229
225
  continue;
230
226
  }
231
- if (autoCount >= particleCount)
232
- continue;
233
- pruned.push(particle);
234
- autoCount++;
227
+ // Only keep `autoCount` amount of automatic particles
228
+ if (autoCount < particleCount) {
229
+ pruned.push(particle);
230
+ autoCount++;
231
+ }
235
232
  }
236
233
  this.particles = pruned;
237
234
  }
@@ -241,7 +238,7 @@ class CanvasParticles {
241
238
  // Only necessary after resize
242
239
  if (updateBounds) {
243
240
  for (const particle of this.particles) {
244
- this.#updateParticleBounds(particle);
241
+ this.#updateParticleUpperBounds(particle);
245
242
  }
246
243
  }
247
244
  for (let i = this.particles.length; i < particleCount; i++)
@@ -270,21 +267,26 @@ class CanvasParticles {
270
267
  gridPos: { x: 1, y: 1 },
271
268
  isVisible: false,
272
269
  isManual,
270
+ bounds: {
271
+ top: -size,
272
+ right: this.canvas.width + size,
273
+ bottom: this.canvas.height + size,
274
+ left: -size,
275
+ },
273
276
  };
274
- this.#updateParticleBounds(particle);
275
277
  this.particles.push(particle);
276
278
  this.hasManualParticles = true;
277
279
  }
278
- /** Update the visible bounds of a particle */
279
- #updateParticleBounds(particle // Make bounds optional on particle
280
- ) {
281
- // The particle is considered visible within these bounds
282
- particle.bounds = {
283
- top: -particle.size,
284
- right: this.canvas.width + particle.size,
285
- bottom: this.canvas.height + particle.size,
286
- left: -particle.size,
287
- };
280
+ /** Updates the particle's bounding box, including size padding, for visibility checks. */
281
+ #updateParticleBounds(particle) {
282
+ particle.bounds.top = -particle.size;
283
+ particle.bounds.right = this.canvas.width + particle.size;
284
+ particle.bounds.bottom = this.canvas.height + particle.size;
285
+ particle.bounds.left = -particle.size;
286
+ }
287
+ #updateParticleUpperBounds(particle) {
288
+ particle.bounds.right = this.canvas.width + particle.size;
289
+ particle.bounds.bottom = this.canvas.height + particle.size;
288
290
  }
289
291
  /* Randomize speed and size of all particles based on current options */
290
292
  updateParticles() {
@@ -742,16 +744,15 @@ class CanvasParticles {
742
744
  /** Gracefully destroy the instance and remove the canvas element */
743
745
  destroy() {
744
746
  this.stop();
747
+ CanvasParticles.instances.delete(this);
745
748
  CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
746
749
  CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
747
- window.removeEventListener('mousemove', this.handleMouseMove);
748
- window.removeEventListener('scroll', this.handleScroll);
749
750
  this.canvas?.remove();
750
751
  Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
751
752
  }
752
753
  /** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
753
754
  set options(options) {
754
- const pno = CanvasParticles.parseNumericOption;
755
+ const pno = parseNumericOption;
755
756
  // Format and parse all options
756
757
  this.option = {
757
758
  background: options.background ?? false,
@@ -761,7 +762,6 @@ class CanvasParticles {
761
762
  },
762
763
  mouse: {
763
764
  interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
764
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
765
765
  connectDist: 1 /* post processed */,
766
766
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
767
767
  },
@@ -789,7 +789,7 @@ class CanvasParticles {
789
789
  },
790
790
  };
791
791
  this.setBackground(this.option.background);
792
- this.setMouseConnectDistMult(this.option.mouse.connectDistMult);
792
+ this.setMouseConnectDistMult(options.mouse?.connectDistMult);
793
793
  this.setParticleColor(this.option.particles.color);
794
794
  }
795
795
  get options() {
@@ -805,7 +805,7 @@ class CanvasParticles {
805
805
  }
806
806
  /** Transform the distance multiplier (float) to absolute distance (px) */
807
807
  setMouseConnectDistMult(connectDistMult) {
808
- const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
808
+ const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
809
809
  this.option.mouse.connectDist = this.option.particles.connectDist * mult;
810
810
  }
811
811
  /** Format particle color and opacity */
@@ -832,5 +832,16 @@ class CanvasParticles {
832
832
  }
833
833
  }
834
834
  }
835
+ // Global event listeners that handle all instances at once
836
+ window.addEventListener('mousemove', (e) => {
837
+ for (const instance of CanvasParticles.instances) {
838
+ instance.handleMouseMove(e);
839
+ }
840
+ }, { passive: true });
841
+ window.addEventListener('scroll', () => {
842
+ for (const instance of CanvasParticles.instances) {
843
+ instance.handleScroll();
844
+ }
845
+ }, { passive: true });
835
846
 
836
847
  export { CanvasParticles as default };
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}}}(4294967296*Math.random()).next;class e{static version="4.4.10";static MAX_DT=1e3/30;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({OFF:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,e=t.instance;if(!e.options?.animation)return;(t.inViewbox=i.isIntersecting)?e.option.animation?.startOnEnter&&e.start({auto:!0}):e.option.animation?.stopOnLeave&&e.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}const i=window.devicePixelRatio||1;for(const e of t){e.target.instance.#t(i)}});static defaultIfNaN(t,i){return 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;dpr=1;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),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}#t(t=window.devicePixelRatio||1){const i=this.canvas.width=this.canvas.rect.width*t,s=this.canvas.height=this.canvas.rect.height*t;1!==t&&this.ctx.scale(t,t),this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(i+2*this.option.particles.connectDist,1),this.height=Math.max(s+2*this.option.particles.connectDist,1),this.offX=(i-this.width)/2,this.offY=(s-this.height)/2;const n=this.option.particles.generationType;n!==e.generationType.OFF&&(n===e.generationType.NEW||0===this.particles.length?this.newParticles():n===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#i()}resizeCanvas(t=!0){t&&this.updateCanvasRect(),this.#t()}#e(){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.#e();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.#s()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#e();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.#n(t);for(let t=this.particles.length;t<i;t++)this.#s()}#s(){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.#n(a),this.particles.push(a),this.hasManualParticles=!0}#n(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.#n(s)}#o(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;const d=1/Math.sqrt(p+l),u=d*d*d;if(p<c){const e=u*a,s=-n*e,o=-h*e;i.velX-=s,i.velY-=o,t.velX+=s,t.velY+=o}if(!e)continue;const f=u*r,g=-n*f,m=-h*f;i.velX+=g,i.velY+=m,t.velX-=g,t.velY-=m}}}#a(i){const s=this.width,n=this.height,o=this.offX,a=this.offY,r=this.mouseX,c=this.mouseY,l=this.option.mouse.interactionType===e.interactionType.NONE,h=this.option.mouse.interactionType===e.interactionType.MOVE,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.particles.rotationSpeed*i,f=this.option.gravity.friction,g=this.option.gravity.maxVelocity,m=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*u*i,e.dir%=t;const v=Math.sin(e.dir)*e.speed,x=Math.cos(e.dir)*e.speed;g>0&&(e.velX>g&&(e.velX=g),e.velX<-g&&(e.velX=-g),e.velY>g&&(e.velY=g),e.velY<-g&&(e.velY=-g)),e.posX+=(v+e.velX)*i,e.posY+=(x+e.velY)*i,e.posX%=s,e.posX<0&&(e.posX+=s),e.posY%=n,e.posY<0&&(e.posY+=n),e.velX*=Math.pow(f,i),e.velY*=Math.pow(f,i);const y=e.posX+o-r,M=e.posY+a-c;if(!l){const t=p/Math.hypot(y,M);d<t?(e.offX+=(t*y-y-e.offX)*m,e.offY+=(t*M-M-e.offY)*m):(e.offX-=e.offX*m,e.offY-=e.offY*m)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,h&&(e.posX=e.x,e.posY=e.y),e.x+=o,e.y+=a,e.gridPos.x=+(e.x>=e.bounds.left)+ +(e.x>e.bounds.right),e.gridPos.y=+(e.y>=e.bounds.top)+ +(e.y>e.bounds.bottom),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#r(){const i=this.ctx;for(const e of this.particles)e.isVisible&&(e.size>1?(i.beginPath(),i.arc(e.x,e.y,e.size,0,t),i.fill(),i.closePath()):i.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}#c(t,i){const e=this.particles,s=e.length,n=new Map;for(let o=0;o<s;o++){const s=e[o],a=(s.x*i|0)+Math.imul(s.y*i,t),r=n.get(a);r?r.push(o):n.set(a,[o])}return n}static#l(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)}#h(){const t=this.particles,i=t.length,s=this.ctx,n=this.option.particles.connectDist,o=n**2,a=(n/2)**2,r=1/n,c=Math.ceil(this.width*r),l=n>=Math.min(this.canvas.width,this.canvas.height),h=o*this.option.particles.maxWork,p=this.color.alpha,d=this.color.alpha*n,u=[],f=this.#c(c,r);let g=0,m=!0;function v(t,i,e,n){const r=t-e,c=i-n,l=r*r+c*c;l>o||(l>a?(s.globalAlpha=d/Math.sqrt(l)-p,s.beginPath(),s.moveTo(t,i),s.lineTo(e,n),s.stroke()):u.push(t,i,e,n),g+=l,m=g<h)}function x(i,s,n){for(const o of i){if(s>=o)continue;const i=t[o];if((l||e.#l(n,i))&&(v(n.x,n.y,i.x,i.y),!m))break}}function y(i,s){for(const n of i){const i=t[n];if((l||e.#l(s,i))&&(v(s.x,s.y,i.x,i.y),!m))break}}for(let e=0;e<i;e++){g=0,m=!0;let s,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c);if((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;if(g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&(s=f.get(l+c+1))&&y(s,n))))}}}if(u.length){s.globalAlpha=p,s.beginPath();for(let t=0;t<u.length;t+=4)s.moveTo(u[t],u[t+1]),s.lineTo(u[t+2],u[t+3]);s.stroke()}}#p(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#d(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#u(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#o(i),this.#a(i),this.lastAnimationFrame=t}#i(){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.option.particles.drawLines&&this.#h(),this.option.debug.drawGrid&&this.#p(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#d()}#f(){this.isAnimating&&(requestAnimationFrame(()=>this.#f()),this.#u(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#f())),!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}),maxVelocity:i("gravity.maxVelocity",t.gravity?.maxVelocity,1/0,{min:0})},debug:{drawGrid:!!t.debug?.drawGrid,drawIndexes:!!t.debug?.drawIndexes}},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";function t(t,i,e,s){if(null==i)return e;const{min:n=-1/0,max:o=1/0}=s??{};return i<n?console.warn(`option.${t} was clamped to ${n} as ${i} is too low`):i>o&&console.warn(`option.${t} was clamped to ${o} as ${i} is too high`),function(t,i){return isNaN(+t)?i:+t}(Math.min(Math.max(i??e,n),o),e)}const i=2*Math.PI;const e=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}}}(4294967296*Math.random()).next;class s{static version="4.5.1";static MAX_DT=1e3/30;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({OFF:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,e=t.instance;if(!e.options?.animation)return;(t.inViewbox=i.isIntersecting)?e.option.animation?.startOnEnter&&e.start({auto:!0}):e.option.animation?.stopOnLeave&&e.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}const i=window.devicePixelRatio||1;for(const e of t){e.target.instance.#t(i)}});static instances=new Set;canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];hasManualParticles=!1;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;dpr=1;width;height;offX;offY;option;color;constructor(t,i={}){let e;if(t instanceof HTMLCanvasElement)e=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(e=document.querySelector(t),!(e instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=e,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,s.instances.add(this),s.canvasIntersectionObserver.observe(this.canvas),s.canvasResizeObserver.observe(this.canvas)}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}#t(t=window.devicePixelRatio||1){const i=this.canvas.width=this.canvas.rect.width*t,e=this.canvas.height=this.canvas.rect.height*t;1!==t&&this.ctx.scale(t,t),this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(i+2*this.option.particles.connectDist,1),this.height=Math.max(e+2*this.option.particles.connectDist,1),this.offX=(i-this.width)/2,this.offY=(e-this.height)/2;const n=this.option.particles.generationType;n!==s.generationType.OFF&&(n===s.generationType.NEW||0===this.particles.length?this.newParticles():n===s.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#i()}resizeCanvas(t=!0){t&&this.updateCanvasRect(),this.#t()}#e(){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.#e();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.#s()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#e();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.#n(t);for(let t=this.particles.length;t<i;t++)this.#s()}#s(){const t=e()*this.width,s=e()*this.height;this.createParticle(t,s,e()*i,(.5+.5*e())*this.option.particles.relSpeed,(.5+2*Math.pow(e(),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,bounds:{top:-n,right:this.canvas.width+n,bottom:this.canvas.height+n,left:-n}};this.particles.push(a),this.hasManualParticles=!0}#o(t){t.bounds.top=-t.size,t.bounds.right=this.canvas.width+t.size,t.bounds.bottom=this.canvas.height+t.size,t.bounds.left=-t.size}#n(t){t.bounds.right=this.canvas.width+t.size,t.bounds.bottom=this.canvas.height+t.size}updateParticles(){const t=this.option.particles.relSpeed,i=this.option.particles.relSize;for(const s of this.particles)s.speed=(.5+.5*e())*t,s.size=(.5+2*Math.pow(e(),5))*i,this.#o(s)}#a(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;const d=1/Math.sqrt(p+l),u=d*d*d;if(p<c){const e=u*a,s=-n*e,o=-h*e;i.velX-=s,i.velY-=o,t.velX+=s,t.velY+=o}if(!e)continue;const f=u*r,g=-n*f,m=-h*f;i.velX+=g,i.velY+=m,t.velX-=g,t.velY-=m}}}#r(t){const e=this.width,n=this.height,o=this.offX,a=this.offY,r=this.mouseX,c=this.mouseY,l=this.option.mouse.interactionType===s.interactionType.NONE,h=this.option.mouse.interactionType===s.interactionType.MOVE,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.particles.rotationSpeed*t,f=this.option.gravity.friction,g=this.option.gravity.maxVelocity,m=1-Math.pow(3/4,t);for(const s of this.particles){s.dir+=2*(Math.random()-.5)*u*t,s.dir%=i;const v=Math.sin(s.dir)*s.speed,x=Math.cos(s.dir)*s.speed;g>0&&(s.velX>g&&(s.velX=g),s.velX<-g&&(s.velX=-g),s.velY>g&&(s.velY=g),s.velY<-g&&(s.velY=-g)),s.posX+=(v+s.velX)*t,s.posY+=(x+s.velY)*t,s.posX%=e,s.posX<0&&(s.posX+=e),s.posY%=n,s.posY<0&&(s.posY+=n),s.velX*=Math.pow(f,t),s.velY*=Math.pow(f,t);const y=s.posX+o-r,b=s.posY+a-c;if(!l){const t=p/Math.hypot(y,b);d<t?(s.offX+=(t*y-y-s.offX)*m,s.offY+=(t*b-b-s.offY)*m):(s.offX-=s.offX*m,s.offY-=s.offY*m)}s.x=s.posX+s.offX,s.y=s.posY+s.offY,h&&(s.posX=s.x,s.posY=s.y),s.x+=o,s.y+=a,s.gridPos.x=+(s.x>=s.bounds.left)+ +(s.x>s.bounds.right),s.gridPos.y=+(s.y>=s.bounds.top)+ +(s.y>s.bounds.bottom),s.isVisible=1===s.gridPos.x&&1===s.gridPos.y}}#c(){const t=this.ctx;for(const e of this.particles)e.isVisible&&(e.size>1?(t.beginPath(),t.arc(e.x,e.y,e.size,0,i),t.fill(),t.closePath()):t.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}#l(t,i){const e=this.particles,s=e.length,n=new Map;for(let o=0;o<s;o++){const s=e[o],a=(s.x*i|0)+Math.imul(s.y*i,t),r=n.get(a);r?r.push(o):n.set(a,[o])}return n}static#h(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)}#p(){const t=this.particles,i=t.length,e=this.ctx,n=this.option.particles.connectDist,o=n**2,a=(n/2)**2,r=1/n,c=Math.ceil(this.width*r),l=n>=Math.min(this.canvas.width,this.canvas.height),h=o*this.option.particles.maxWork,p=this.color.alpha,d=this.color.alpha*n,u=[],f=this.#l(c,r);let g=0,m=!0;function v(t,i,s,n){const r=t-s,c=i-n,l=r*r+c*c;l>o||(l>a?(e.globalAlpha=d/Math.sqrt(l)-p,e.beginPath(),e.moveTo(t,i),e.lineTo(s,n),e.stroke()):u.push(t,i,s,n),g+=l,m=g<h)}function x(i,e,n){for(const o of i){if(e>=o)continue;const i=t[o];if((l||s.#h(n,i))&&(v(n.x,n.y,i.x,i.y),!m))break}}function y(i,e){for(const n of i){const i=t[n];if((l||s.#h(e,i))&&(v(e.x,e.y,i.x,i.y),!m))break}}for(let e=0;e<i;e++){g=0,m=!0;let s,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c);if((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;if(g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&(s=f.get(l+c+1))&&y(s,n))))}}}if(u.length){e.globalAlpha=p,e.beginPath();for(let t=0;t<u.length;t+=4)e.moveTo(u[t],u[t+1]),e.lineTo(u[t+2],u[t+3]);e.stroke()}}#d(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#u(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#f(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,s.MAX_DT)/s.BASE_DT;this.#a(i),this.#r(i),this.lastAnimationFrame=t}#i(){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.#p(),this.option.debug.drawGrid&&this.#d(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#u()}#g(){this.isAnimating&&(requestAnimationFrame(()=>this.#g()),this.#f(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#g())),!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(),s.instances.delete(this),s.canvasIntersectionObserver.unobserve(this.canvas),s.canvasResizeObserver.unobserve(this.canvas),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(i){const e=t;this.option={background:i.background??!1,animation:{startOnEnter:!!(i.animation?.startOnEnter??1),stopOnLeave:!!(i.animation?.stopOnLeave??1)},mouse:{interactionType:~~e("mouse.interactionType",i.mouse?.interactionType,s.interactionType.MOVE,{min:0,max:2}),connectDist:1,distRatio:e("mouse.distRatio",i.mouse?.distRatio,2/3,{min:0})},particles:{generationType:~~e("particles.generationType",i.particles?.generationType,s.generationType.MATCH,{min:0,max:2}),drawLines:!!(i.particles?.drawLines??1),color:i.particles?.color??"black",ppm:~~e("particles.ppm",i.particles?.ppm,100),max:Math.round(e("particles.max",i.particles?.max,1/0,{min:0})),maxWork:Math.round(e("particles.maxWork",i.particles?.maxWork,1/0,{min:0})),connectDist:~~e("particles.connectDistance",i.particles?.connectDistance,150,{min:1}),relSpeed:e("particles.relSpeed",i.particles?.relSpeed,1,{min:0}),relSize:e("particles.relSize",i.particles?.relSize,1,{min:0}),rotationSpeed:e("particles.rotationSpeed",i.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:e("gravity.repulsive",i.gravity?.repulsive,0,{min:0}),pulling:e("gravity.pulling",i.gravity?.pulling,0,{min:0}),friction:e("gravity.friction",i.gravity?.friction,.8,{min:0,max:1}),maxVelocity:e("gravity.maxVelocity",i.gravity?.maxVelocity,1/0,{min:0})},debug:{drawGrid:!!i.debug?.drawGrid,drawIndexes:!!i.debug?.drawIndexes}},this.setBackground(this.option.background),this.setMouseConnectDistMult(i.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(i){const e=t("mouse.connectDistMult",i,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*e}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 window.addEventListener("mousemove",t=>{for(const i of s.instances)i.handleMouseMove(t)},{passive:!0}),window.addEventListener("scroll",()=>{for(const t of s.instances)t.handleScroll()},{passive:!0}),s});
@@ -0,0 +1,6 @@
1
+ /** Helper functions for options parsing */
2
+ export declare function defaultIfNaN(value: number, defaultValue: number): number;
3
+ export declare function parseNumericOption(name: string, value: number | undefined, defaultValue: number, clamp?: {
4
+ min?: number;
5
+ max?: number;
6
+ }): number;
@@ -6,7 +6,6 @@ export interface CanvasParticlesOptions {
6
6
  };
7
7
  mouse: {
8
8
  interactionType: 0 | 1 | 2;
9
- connectDistMult: number;
10
9
  connectDist: number;
11
10
  distRatio: number;
12
11
  };
@@ -37,5 +36,9 @@ export interface CanvasParticlesOptions {
37
36
  type DeepPartial<T> = {
38
37
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
39
38
  };
40
- export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions>;
39
+ export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions> & {
40
+ mouse?: {
41
+ connectDistMult?: number;
42
+ };
43
+ };
41
44
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.4.10",
3
+ "version": "4.5.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",
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) 2022–2026 Kyle Hoeckman, MIT License
2
2
  // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
3
3
 
4
+ import { parseNumericOption } from './options'
5
+
4
6
  import type { CanvasParticlesCanvas, Particle, GridPos, ContextColor, SpatialGrid } from './types'
5
7
  import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
6
8
 
@@ -50,7 +52,7 @@ export default class CanvasParticles {
50
52
  MATCH: 2, // Add or remove some particles to match the new count (default)
51
53
  })
52
54
 
53
- /** Observes canvas elements entering or leaving the viewport to start/stop animation */
55
+ /** Observes when canvas elements enter or leave the viewport to start/stop animation */
54
56
  static readonly canvasIntersectionObserver = new IntersectionObserver(
55
57
  (entries) => {
56
58
  for (const entry of entries) {
@@ -69,6 +71,7 @@ export default class CanvasParticles {
69
71
  }
70
72
  )
71
73
 
74
+ /** Observes when canvas elements change size */
72
75
  static readonly canvasResizeObserver = new ResizeObserver((entries) => {
73
76
  // Seperate for loops is very important to prevent huge forced reflow overhead
74
77
 
@@ -88,29 +91,7 @@ export default class CanvasParticles {
88
91
  }
89
92
  })
90
93
 
91
- /** Helper functions for options parsing */
92
- private static defaultIfNaN(value: number, defaultValue: number): number {
93
- return isNaN(+value) ? defaultValue : +value
94
- }
95
-
96
- private static parseNumericOption(
97
- name: string,
98
- value: number | undefined,
99
- defaultValue: number,
100
- clamp?: { min?: number; max?: number }
101
- ): number {
102
- if (value == undefined) return defaultValue
103
-
104
- const { min = -Infinity, max = Infinity } = clamp ?? {}
105
-
106
- if (value < min) {
107
- console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
108
- } else if (value > max) {
109
- console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
110
- }
111
-
112
- return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue)
113
- }
94
+ static instances = new Set<CanvasParticles>()
114
95
 
115
96
  canvas: CanvasParticlesCanvas
116
97
  private ctx: CanvasRenderingContext2D
@@ -162,17 +143,9 @@ export default class CanvasParticles {
162
143
 
163
144
  this.options = options // Uses setter
164
145
 
146
+ CanvasParticles.instances.add(this)
165
147
  CanvasParticles.canvasIntersectionObserver.observe(this.canvas)
166
148
  CanvasParticles.canvasResizeObserver.observe(this.canvas)
167
-
168
- // Setup event handlers
169
- this.resizeCanvas = this.resizeCanvas.bind(this)
170
- this.handleMouseMove = this.handleMouseMove.bind(this)
171
- this.handleScroll = this.handleScroll.bind(this)
172
-
173
- // this.resizeCanvas()
174
- window.addEventListener('mousemove', this.handleMouseMove, { passive: true })
175
- window.addEventListener('scroll', this.handleScroll, { passive: true })
176
149
  }
177
150
 
178
151
  updateCanvasRect() {
@@ -272,17 +245,18 @@ export default class CanvasParticles {
272
245
  const pruned: Particle[] = []
273
246
  let autoCount = 0
274
247
 
275
- // Keep manual particles while pruning automatic particles that exceed `particleCount`
276
- // Only count automatic particles towards `particledCount`
277
248
  for (const particle of this.particles) {
249
+ // Keep manual particles
278
250
  if (particle.isManual) {
279
251
  pruned.push(particle)
280
252
  continue
281
253
  }
282
254
 
283
- if (autoCount >= particleCount) continue
284
- pruned.push(particle)
285
- autoCount++
255
+ // Only keep `autoCount` amount of automatic particles
256
+ if (autoCount < particleCount) {
257
+ pruned.push(particle)
258
+ autoCount++
259
+ }
286
260
  }
287
261
  this.particles = pruned
288
262
  } else {
@@ -292,7 +266,7 @@ export default class CanvasParticles {
292
266
  // Only necessary after resize
293
267
  if (updateBounds) {
294
268
  for (const particle of this.particles) {
295
- this.#updateParticleBounds(particle)
269
+ this.#updateParticleUpperBounds(particle)
296
270
  }
297
271
  }
298
272
 
@@ -316,7 +290,7 @@ export default class CanvasParticles {
316
290
 
317
291
  /** Create a new particle with optional parameters */
318
292
  createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual = true) {
319
- const particle: Omit<Particle, 'bounds'> = {
293
+ const particle: Particle = {
320
294
  posX, // Logical position in pixels
321
295
  posY, // Logical position in pixels
322
296
  x: posX, // Visual position in pixels
@@ -331,23 +305,28 @@ export default class CanvasParticles {
331
305
  gridPos: { x: 1, y: 1 },
332
306
  isVisible: false,
333
307
  isManual,
308
+ bounds: {
309
+ top: -size,
310
+ right: this.canvas.width + size,
311
+ bottom: this.canvas.height + size,
312
+ left: -size,
313
+ },
334
314
  }
335
- this.#updateParticleBounds(particle)
336
315
  this.particles.push(particle)
337
316
  this.hasManualParticles = true
338
317
  }
339
318
 
340
- /** Update the visible bounds of a particle */
341
- #updateParticleBounds(
342
- particle: Omit<Particle, 'bounds'> & Partial<Pick<Particle, 'bounds'>> // Make bounds optional on particle
343
- ): asserts particle is Particle {
344
- // The particle is considered visible within these bounds
345
- particle.bounds = {
346
- top: -particle.size,
347
- right: this.canvas.width + particle.size,
348
- bottom: this.canvas.height + particle.size,
349
- left: -particle.size,
350
- }
319
+ /** Updates the particle's bounding box, including size padding, for visibility checks. */
320
+ #updateParticleBounds(particle: Particle) {
321
+ particle.bounds.top = -particle.size
322
+ particle.bounds.right = this.canvas.width + particle.size
323
+ particle.bounds.bottom = this.canvas.height + particle.size
324
+ particle.bounds.left = -particle.size
325
+ }
326
+
327
+ #updateParticleUpperBounds(particle: Particle) {
328
+ particle.bounds.right = this.canvas.width + particle.size
329
+ particle.bounds.bottom = this.canvas.height + particle.size
351
330
  }
352
331
 
353
332
  /* Randomize speed and size of all particles based on current options */
@@ -845,12 +824,10 @@ export default class CanvasParticles {
845
824
  destroy() {
846
825
  this.stop()
847
826
 
827
+ CanvasParticles.instances.delete(this)
848
828
  CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas)
849
829
  CanvasParticles.canvasResizeObserver.unobserve(this.canvas)
850
830
 
851
- window.removeEventListener('mousemove', this.handleMouseMove)
852
- window.removeEventListener('scroll', this.handleScroll)
853
-
854
831
  this.canvas?.remove()
855
832
 
856
833
  Object.keys(this).forEach((key) => delete (this as any)[key]) // Remove references to help GC
@@ -858,7 +835,7 @@ export default class CanvasParticles {
858
835
 
859
836
  /** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
860
837
  set options(options: CanvasParticlesOptionsInput) {
861
- const pno = CanvasParticles.parseNumericOption
838
+ const pno = parseNumericOption
862
839
 
863
840
  // Format and parse all options
864
841
  this.option = {
@@ -874,7 +851,6 @@ export default class CanvasParticles {
874
851
  CanvasParticles.interactionType.MOVE,
875
852
  { min: 0, max: 2 }
876
853
  ) as 0 | 1 | 2,
877
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
878
854
  connectDist: 1 /* post processed */,
879
855
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
880
856
  },
@@ -908,7 +884,7 @@ export default class CanvasParticles {
908
884
  }
909
885
 
910
886
  this.setBackground(this.option.background)
911
- this.setMouseConnectDistMult(this.option.mouse.connectDistMult)
887
+ this.setMouseConnectDistMult(options.mouse?.connectDistMult)
912
888
  this.setParticleColor(this.option.particles.color)
913
889
  }
914
890
 
@@ -924,8 +900,8 @@ export default class CanvasParticles {
924
900
  }
925
901
 
926
902
  /** Transform the distance multiplier (float) to absolute distance (px) */
927
- setMouseConnectDistMult(connectDistMult: number) {
928
- const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 })
903
+ setMouseConnectDistMult(connectDistMult: number | undefined) {
904
+ const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 })
929
905
  this.option.mouse.connectDist = this.option.particles.connectDist * mult
930
906
  }
931
907
 
@@ -956,3 +932,24 @@ export default class CanvasParticles {
956
932
  }
957
933
  }
958
934
  }
935
+
936
+ // Global event listeners that handle all instances at once
937
+ window.addEventListener(
938
+ 'mousemove',
939
+ (e) => {
940
+ for (const instance of CanvasParticles.instances) {
941
+ instance.handleMouseMove(e)
942
+ }
943
+ },
944
+ { passive: true }
945
+ )
946
+
947
+ window.addEventListener(
948
+ 'scroll',
949
+ () => {
950
+ for (const instance of CanvasParticles.instances) {
951
+ instance.handleScroll()
952
+ }
953
+ },
954
+ { passive: true }
955
+ )
package/src/options.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** Helper functions for options parsing */
2
+
3
+ export function defaultIfNaN(value: number, defaultValue: number): number {
4
+ return isNaN(+value) ? defaultValue : +value
5
+ }
6
+
7
+ export function parseNumericOption(
8
+ name: string,
9
+ value: number | undefined,
10
+ defaultValue: number,
11
+ clamp?: { min?: number; max?: number }
12
+ ): number {
13
+ if (value == undefined) return defaultValue
14
+
15
+ const { min = -Infinity, max = Infinity } = clamp ?? {}
16
+
17
+ if (value < min) {
18
+ console.warn(`option.${name} was clamped to ${min} as ${value} is too low`)
19
+ } else if (value > max) {
20
+ console.warn(`option.${name} was clamped to ${max} as ${value} is too high`)
21
+ }
22
+
23
+ return defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue)
24
+ }
@@ -8,7 +8,6 @@ export interface CanvasParticlesOptions {
8
8
 
9
9
  mouse: {
10
10
  interactionType: 0 | 1 | 2 /* see CanvasParticles.interactionType */
11
- connectDistMult: number
12
11
  connectDist: number /* post processed */
13
12
  distRatio: number
14
13
  }
@@ -44,4 +43,6 @@ type DeepPartial<T> = {
44
43
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
45
44
  }
46
45
 
47
- export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions>
46
+ export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions> & {
47
+ mouse?: { connectDistMult?: number }
48
+ }