canvasparticles-js 4.4.9 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.9";
42
+ static version = "4.5.0";
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
@@ -59,28 +77,15 @@ class CanvasParticles {
59
77
  const canvas = entry.target;
60
78
  canvas.instance.updateCanvasRect();
61
79
  }
80
+ // Cache to prevent fetching the dpr for every instance
81
+ const dpr = window.devicePixelRatio || 1;
62
82
  // Then resize all canvases at once
63
83
  for (const entry of entries) {
64
84
  const canvas = entry.target;
65
- canvas.instance.#resizeCanvas();
85
+ canvas.instance.#resizeCanvas(dpr);
66
86
  }
67
87
  });
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
- }
88
+ static instances = new Set();
84
89
  canvas;
85
90
  ctx;
86
91
  enableAnimating = false;
@@ -92,6 +97,7 @@ class CanvasParticles {
92
97
  clientY = Infinity;
93
98
  mouseX = Infinity;
94
99
  mouseY = Infinity;
100
+ dpr = 1;
95
101
  width;
96
102
  height;
97
103
  offX;
@@ -124,15 +130,9 @@ class CanvasParticles {
124
130
  throw new Error('failed to get 2D context from canvas');
125
131
  this.ctx = ctx;
126
132
  this.options = options; // Uses setter
133
+ CanvasParticles.instances.add(this);
127
134
  CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
128
135
  CanvasParticles.canvasResizeObserver.observe(this.canvas);
129
- // Setup event handlers
130
- this.resizeCanvas = this.resizeCanvas.bind(this);
131
- this.handleMouseMove = this.handleMouseMove.bind(this);
132
- this.handleScroll = this.handleScroll.bind(this);
133
- this.resizeCanvas();
134
- window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
135
- window.addEventListener('scroll', this.handleScroll, { passive: true });
136
136
  }
137
137
  updateCanvasRect() {
138
138
  const { top, left, width, height } = this.canvas.getBoundingClientRect();
@@ -161,11 +161,12 @@ class CanvasParticles {
161
161
  this.mouseY = this.clientY - top;
162
162
  }
163
163
  /** Resize the canvas and update particles accordingly */
164
- #resizeCanvas() {
165
- const dpr = window.devicePixelRatio || 1;
164
+ #resizeCanvas(dpr = window.devicePixelRatio || 1) {
166
165
  const width = (this.canvas.width = this.canvas.rect.width * dpr);
167
166
  const height = (this.canvas.height = this.canvas.rect.height * dpr);
168
- this.ctx.scale(dpr, dpr);
167
+ // Must be set every time width or height changes because scale is removed
168
+ if (dpr !== 1)
169
+ this.ctx.scale(dpr, dpr);
169
170
  // Hide the mouse when resizing because it must be outside the viewport to do so
170
171
  this.mouseX = Infinity;
171
172
  this.mouseY = Infinity;
@@ -219,17 +220,17 @@ class CanvasParticles {
219
220
  if (this.hasManualParticles) {
220
221
  const pruned = [];
221
222
  let autoCount = 0;
222
- // Keep manual particles while pruning automatic particles that exceed `particleCount`
223
- // Only count automatic particles towards `particledCount`
224
223
  for (const particle of this.particles) {
224
+ // Keep manual particles
225
225
  if (particle.isManual) {
226
226
  pruned.push(particle);
227
227
  continue;
228
228
  }
229
- if (autoCount >= particleCount)
230
- continue;
231
- pruned.push(particle);
232
- autoCount++;
229
+ // Only keep `autoCount` amount of automatic particles
230
+ if (autoCount < particleCount) {
231
+ pruned.push(particle);
232
+ autoCount++;
233
+ }
233
234
  }
234
235
  this.particles = pruned;
235
236
  }
@@ -740,16 +741,15 @@ class CanvasParticles {
740
741
  /** Gracefully destroy the instance and remove the canvas element */
741
742
  destroy() {
742
743
  this.stop();
744
+ CanvasParticles.instances.delete(this);
743
745
  CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
744
746
  CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
745
- window.removeEventListener('mousemove', this.handleMouseMove);
746
- window.removeEventListener('scroll', this.handleScroll);
747
747
  this.canvas?.remove();
748
748
  Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
749
749
  }
750
750
  /** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
751
751
  set options(options) {
752
- const pno = CanvasParticles.parseNumericOption;
752
+ const pno = parseNumericOption;
753
753
  // Format and parse all options
754
754
  this.option = {
755
755
  background: options.background ?? false,
@@ -759,7 +759,6 @@ class CanvasParticles {
759
759
  },
760
760
  mouse: {
761
761
  interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
762
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
763
762
  connectDist: 1 /* post processed */,
764
763
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
765
764
  },
@@ -787,7 +786,7 @@ class CanvasParticles {
787
786
  },
788
787
  };
789
788
  this.setBackground(this.option.background);
790
- this.setMouseConnectDistMult(this.option.mouse.connectDistMult);
789
+ this.setMouseConnectDistMult(options.mouse?.connectDistMult);
791
790
  this.setParticleColor(this.option.particles.color);
792
791
  }
793
792
  get options() {
@@ -803,7 +802,7 @@ class CanvasParticles {
803
802
  }
804
803
  /** Transform the distance multiplier (float) to absolute distance (px) */
805
804
  setMouseConnectDistMult(connectDistMult) {
806
- const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
805
+ const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
807
806
  this.option.mouse.connectDist = this.option.particles.connectDist * mult;
808
807
  }
809
808
  /** Format particle color and opacity */
@@ -830,5 +829,16 @@ class CanvasParticles {
830
829
  }
831
830
  }
832
831
  }
832
+ // Global event listeners that handle all instances at once
833
+ window.addEventListener('mousemove', (e) => {
834
+ for (const instance of CanvasParticles.instances) {
835
+ instance.handleMouseMove(e);
836
+ }
837
+ }, { passive: true });
838
+ window.addEventListener('scroll', () => {
839
+ for (const instance of CanvasParticles.instances) {
840
+ instance.handleScroll();
841
+ }
842
+ }, { passive: true });
833
843
 
834
844
  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;
@@ -35,6 +34,7 @@ export default class CanvasParticles {
35
34
  private clientY;
36
35
  mouseX: number;
37
36
  mouseY: number;
37
+ dpr: number;
38
38
  width: number;
39
39
  height: number;
40
40
  private offX;
@@ -82,7 +82,7 @@ export default class CanvasParticles {
82
82
  /** Sets the canvas background */
83
83
  setBackground(background: CanvasParticlesOptionsInput['background']): void;
84
84
  /** Transform the distance multiplier (float) to absolute distance (px) */
85
- setMouseConnectDistMult(connectDistMult: number): void;
85
+ setMouseConnectDistMult(connectDistMult: number | undefined): void;
86
86
  /** Format particle color and opacity */
87
87
  setParticleColor(color: string | CanvasGradient | CanvasPattern): void;
88
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.9";
40
+ static version = "4.5.0";
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
@@ -57,28 +75,15 @@ class CanvasParticles {
57
75
  const canvas = entry.target;
58
76
  canvas.instance.updateCanvasRect();
59
77
  }
78
+ // Cache to prevent fetching the dpr for every instance
79
+ const dpr = window.devicePixelRatio || 1;
60
80
  // Then resize all canvases at once
61
81
  for (const entry of entries) {
62
82
  const canvas = entry.target;
63
- canvas.instance.#resizeCanvas();
83
+ canvas.instance.#resizeCanvas(dpr);
64
84
  }
65
85
  });
66
- /** Helper functions for options parsing */
67
- static defaultIfNaN(value, defaultValue) {
68
- return isNaN(+value) ? defaultValue : +value;
69
- }
70
- static parseNumericOption(name, value, defaultValue, clamp) {
71
- if (value == undefined)
72
- return defaultValue;
73
- const { min = -Infinity, max = Infinity } = clamp ?? {};
74
- if (value < min) {
75
- console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
76
- }
77
- else if (value > max) {
78
- console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
79
- }
80
- return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
81
- }
86
+ static instances = new Set();
82
87
  canvas;
83
88
  ctx;
84
89
  enableAnimating = false;
@@ -90,6 +95,7 @@ class CanvasParticles {
90
95
  clientY = Infinity;
91
96
  mouseX = Infinity;
92
97
  mouseY = Infinity;
98
+ dpr = 1;
93
99
  width;
94
100
  height;
95
101
  offX;
@@ -122,15 +128,9 @@ class CanvasParticles {
122
128
  throw new Error('failed to get 2D context from canvas');
123
129
  this.ctx = ctx;
124
130
  this.options = options; // Uses setter
131
+ CanvasParticles.instances.add(this);
125
132
  CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
126
133
  CanvasParticles.canvasResizeObserver.observe(this.canvas);
127
- // Setup event handlers
128
- this.resizeCanvas = this.resizeCanvas.bind(this);
129
- this.handleMouseMove = this.handleMouseMove.bind(this);
130
- this.handleScroll = this.handleScroll.bind(this);
131
- this.resizeCanvas();
132
- window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
133
- window.addEventListener('scroll', this.handleScroll, { passive: true });
134
134
  }
135
135
  updateCanvasRect() {
136
136
  const { top, left, width, height } = this.canvas.getBoundingClientRect();
@@ -159,11 +159,12 @@ class CanvasParticles {
159
159
  this.mouseY = this.clientY - top;
160
160
  }
161
161
  /** Resize the canvas and update particles accordingly */
162
- #resizeCanvas() {
163
- const dpr = window.devicePixelRatio || 1;
162
+ #resizeCanvas(dpr = window.devicePixelRatio || 1) {
164
163
  const width = (this.canvas.width = this.canvas.rect.width * dpr);
165
164
  const height = (this.canvas.height = this.canvas.rect.height * dpr);
166
- this.ctx.scale(dpr, dpr);
165
+ // Must be set every time width or height changes because scale is removed
166
+ if (dpr !== 1)
167
+ this.ctx.scale(dpr, dpr);
167
168
  // Hide the mouse when resizing because it must be outside the viewport to do so
168
169
  this.mouseX = Infinity;
169
170
  this.mouseY = Infinity;
@@ -217,17 +218,17 @@ class CanvasParticles {
217
218
  if (this.hasManualParticles) {
218
219
  const pruned = [];
219
220
  let autoCount = 0;
220
- // Keep manual particles while pruning automatic particles that exceed `particleCount`
221
- // Only count automatic particles towards `particledCount`
222
221
  for (const particle of this.particles) {
222
+ // Keep manual particles
223
223
  if (particle.isManual) {
224
224
  pruned.push(particle);
225
225
  continue;
226
226
  }
227
- if (autoCount >= particleCount)
228
- continue;
229
- pruned.push(particle);
230
- autoCount++;
227
+ // Only keep `autoCount` amount of automatic particles
228
+ if (autoCount < particleCount) {
229
+ pruned.push(particle);
230
+ autoCount++;
231
+ }
231
232
  }
232
233
  this.particles = pruned;
233
234
  }
@@ -738,16 +739,15 @@ class CanvasParticles {
738
739
  /** Gracefully destroy the instance and remove the canvas element */
739
740
  destroy() {
740
741
  this.stop();
742
+ CanvasParticles.instances.delete(this);
741
743
  CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
742
744
  CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
743
- window.removeEventListener('mousemove', this.handleMouseMove);
744
- window.removeEventListener('scroll', this.handleScroll);
745
745
  this.canvas?.remove();
746
746
  Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
747
747
  }
748
748
  /** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
749
749
  set options(options) {
750
- const pno = CanvasParticles.parseNumericOption;
750
+ const pno = parseNumericOption;
751
751
  // Format and parse all options
752
752
  this.option = {
753
753
  background: options.background ?? false,
@@ -757,7 +757,6 @@ class CanvasParticles {
757
757
  },
758
758
  mouse: {
759
759
  interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
760
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
761
760
  connectDist: 1 /* post processed */,
762
761
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
763
762
  },
@@ -785,7 +784,7 @@ class CanvasParticles {
785
784
  },
786
785
  };
787
786
  this.setBackground(this.option.background);
788
- this.setMouseConnectDistMult(this.option.mouse.connectDistMult);
787
+ this.setMouseConnectDistMult(options.mouse?.connectDistMult);
789
788
  this.setParticleColor(this.option.particles.color);
790
789
  }
791
790
  get options() {
@@ -801,7 +800,7 @@ class CanvasParticles {
801
800
  }
802
801
  /** Transform the distance multiplier (float) to absolute distance (px) */
803
802
  setMouseConnectDistMult(connectDistMult) {
804
- const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
803
+ const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
805
804
  this.option.mouse.connectDist = this.option.particles.connectDist * mult;
806
805
  }
807
806
  /** Format particle color and opacity */
@@ -828,5 +827,16 @@ class CanvasParticles {
828
827
  }
829
828
  }
830
829
  }
830
+ // Global event listeners that handle all instances at once
831
+ window.addEventListener('mousemove', (e) => {
832
+ for (const instance of CanvasParticles.instances) {
833
+ instance.handleMouseMove(e);
834
+ }
835
+ }, { passive: true });
836
+ window.addEventListener('scroll', () => {
837
+ for (const instance of CanvasParticles.instances) {
838
+ instance.handleScroll();
839
+ }
840
+ }, { passive: true });
831
841
 
832
842
  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.9";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()}for(const i of t){i.target.instance.#t()}});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;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.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}#t(){const t=window.devicePixelRatio||1,i=this.canvas.width=this.canvas.rect.width*t,s=this.canvas.height=this.canvas.rect.height*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.0";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};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,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.#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(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}}#r(){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))}#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,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.#c(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.#l(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.#l(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()}}#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,s.MAX_DT)/s.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(),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.9",
3
+ "version": "4.5.0",
4
4
  "description": "In an HTML canvas, a bunch of interactive particles connected with lines when they approach each other.",
5
5
  "author": "Khoeckman",
6
6
  "license": "MIT",
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
 
@@ -78,36 +81,17 @@ export default class CanvasParticles {
78
81
  canvas.instance.updateCanvasRect()
79
82
  }
80
83
 
84
+ // Cache to prevent fetching the dpr for every instance
85
+ const dpr = window.devicePixelRatio || 1
86
+
81
87
  // Then resize all canvases at once
82
88
  for (const entry of entries) {
83
89
  const canvas = entry.target as CanvasParticlesCanvas
84
- canvas.instance.#resizeCanvas()
90
+ canvas.instance.#resizeCanvas(dpr)
85
91
  }
86
92
  })
87
93
 
88
- /** Helper functions for options parsing */
89
- private static defaultIfNaN(value: number, defaultValue: number): number {
90
- return isNaN(+value) ? defaultValue : +value
91
- }
92
-
93
- private static parseNumericOption(
94
- name: string,
95
- value: number | undefined,
96
- defaultValue: number,
97
- clamp?: { min?: number; max?: number }
98
- ): number {
99
- if (value == undefined) return defaultValue
100
-
101
- const { min = -Infinity, max = Infinity } = clamp ?? {}
102
-
103
- if (value < min) {
104
- console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
105
- } else if (value > max) {
106
- console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
107
- }
108
-
109
- return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue)
110
- }
94
+ static instances = new Set<CanvasParticles>()
111
95
 
112
96
  canvas: CanvasParticlesCanvas
113
97
  private ctx: CanvasRenderingContext2D
@@ -122,6 +106,8 @@ export default class CanvasParticles {
122
106
  private clientY: number = Infinity
123
107
  mouseX: number = Infinity
124
108
  mouseY: number = Infinity
109
+
110
+ dpr: number = 1
125
111
  width!: number
126
112
  height!: number
127
113
  private offX!: number
@@ -157,17 +143,9 @@ export default class CanvasParticles {
157
143
 
158
144
  this.options = options // Uses setter
159
145
 
146
+ CanvasParticles.instances.add(this)
160
147
  CanvasParticles.canvasIntersectionObserver.observe(this.canvas)
161
148
  CanvasParticles.canvasResizeObserver.observe(this.canvas)
162
-
163
- // Setup event handlers
164
- this.resizeCanvas = this.resizeCanvas.bind(this)
165
- this.handleMouseMove = this.handleMouseMove.bind(this)
166
- this.handleScroll = this.handleScroll.bind(this)
167
-
168
- this.resizeCanvas()
169
- window.addEventListener('mousemove', this.handleMouseMove, { passive: true })
170
- window.addEventListener('scroll', this.handleScroll, { passive: true })
171
149
  }
172
150
 
173
151
  updateCanvasRect() {
@@ -199,11 +177,12 @@ export default class CanvasParticles {
199
177
  }
200
178
 
201
179
  /** Resize the canvas and update particles accordingly */
202
- #resizeCanvas() {
203
- const dpr = window.devicePixelRatio || 1
180
+ #resizeCanvas(dpr = window.devicePixelRatio || 1) {
204
181
  const width = (this.canvas.width = this.canvas.rect.width * dpr)
205
182
  const height = (this.canvas.height = this.canvas.rect.height * dpr)
206
- this.ctx.scale(dpr, dpr)
183
+
184
+ // Must be set every time width or height changes because scale is removed
185
+ if (dpr !== 1) this.ctx.scale(dpr, dpr)
207
186
 
208
187
  // Hide the mouse when resizing because it must be outside the viewport to do so
209
188
  this.mouseX = Infinity
@@ -266,17 +245,18 @@ export default class CanvasParticles {
266
245
  const pruned: Particle[] = []
267
246
  let autoCount = 0
268
247
 
269
- // Keep manual particles while pruning automatic particles that exceed `particleCount`
270
- // Only count automatic particles towards `particledCount`
271
248
  for (const particle of this.particles) {
249
+ // Keep manual particles
272
250
  if (particle.isManual) {
273
251
  pruned.push(particle)
274
252
  continue
275
253
  }
276
254
 
277
- if (autoCount >= particleCount) continue
278
- pruned.push(particle)
279
- autoCount++
255
+ // Only keep `autoCount` amount of automatic particles
256
+ if (autoCount < particleCount) {
257
+ pruned.push(particle)
258
+ autoCount++
259
+ }
280
260
  }
281
261
  this.particles = pruned
282
262
  } else {
@@ -839,12 +819,10 @@ export default class CanvasParticles {
839
819
  destroy() {
840
820
  this.stop()
841
821
 
822
+ CanvasParticles.instances.delete(this)
842
823
  CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas)
843
824
  CanvasParticles.canvasResizeObserver.unobserve(this.canvas)
844
825
 
845
- window.removeEventListener('mousemove', this.handleMouseMove)
846
- window.removeEventListener('scroll', this.handleScroll)
847
-
848
826
  this.canvas?.remove()
849
827
 
850
828
  Object.keys(this).forEach((key) => delete (this as any)[key]) // Remove references to help GC
@@ -852,7 +830,7 @@ export default class CanvasParticles {
852
830
 
853
831
  /** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
854
832
  set options(options: CanvasParticlesOptionsInput) {
855
- const pno = CanvasParticles.parseNumericOption
833
+ const pno = parseNumericOption
856
834
 
857
835
  // Format and parse all options
858
836
  this.option = {
@@ -868,7 +846,6 @@ export default class CanvasParticles {
868
846
  CanvasParticles.interactionType.MOVE,
869
847
  { min: 0, max: 2 }
870
848
  ) as 0 | 1 | 2,
871
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
872
849
  connectDist: 1 /* post processed */,
873
850
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
874
851
  },
@@ -902,7 +879,7 @@ export default class CanvasParticles {
902
879
  }
903
880
 
904
881
  this.setBackground(this.option.background)
905
- this.setMouseConnectDistMult(this.option.mouse.connectDistMult)
882
+ this.setMouseConnectDistMult(options.mouse?.connectDistMult)
906
883
  this.setParticleColor(this.option.particles.color)
907
884
  }
908
885
 
@@ -918,8 +895,8 @@ export default class CanvasParticles {
918
895
  }
919
896
 
920
897
  /** Transform the distance multiplier (float) to absolute distance (px) */
921
- setMouseConnectDistMult(connectDistMult: number) {
922
- const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 })
898
+ setMouseConnectDistMult(connectDistMult: number | undefined) {
899
+ const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 })
923
900
  this.option.mouse.connectDist = this.option.particles.connectDist * mult
924
901
  }
925
902
 
@@ -950,3 +927,24 @@ export default class CanvasParticles {
950
927
  }
951
928
  }
952
929
  }
930
+
931
+ // Global event listeners that handle all instances at once
932
+ window.addEventListener(
933
+ 'mousemove',
934
+ (e) => {
935
+ for (const instance of CanvasParticles.instances) {
936
+ instance.handleMouseMove(e)
937
+ }
938
+ },
939
+ { passive: true }
940
+ )
941
+
942
+ window.addEventListener(
943
+ 'scroll',
944
+ () => {
945
+ for (const instance of CanvasParticles.instances) {
946
+ instance.handleScroll()
947
+ }
948
+ },
949
+ { passive: true }
950
+ )
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
+ }