canvasparticles-js 4.1.2 → 4.1.4

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
@@ -2,8 +2,26 @@
2
2
 
3
3
  // Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
4
4
  // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
5
+ const TWO_PI = 2 * Math.PI;
6
+ /** Extremely fast, simple 32‑bit PRNG */
7
+ function Mulberry32(seed) {
8
+ let state = seed >>> 0;
9
+ return {
10
+ next() {
11
+ let t = (state + 0x6d2b79f5) | 0;
12
+ state = t;
13
+ t = Math.imul(t ^ (t >>> 15), t | 1);
14
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
15
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
16
+ },
17
+ };
18
+ }
19
+ // Mulberry32 is ±388% faster than Math.random()
20
+ // Benchmark: https://jsbm.dev/muLCWR9RJCbmy
21
+ // Spectral test: /demo/mulberry32.html
22
+ const prng = Mulberry32(Math.random() * 2 ** 32).next;
5
23
  class CanvasParticles {
6
- static version = "4.1.2";
24
+ static version = "4.1.4";
7
25
  /** Defines mouse interaction types with the particles */
8
26
  static interactionType = Object.freeze({
9
27
  NONE: 0, // No mouse interaction
@@ -38,9 +56,11 @@ class CanvasParticles {
38
56
  });
39
57
  canvas;
40
58
  ctx;
59
+ lastAnimationFrame = 0;
41
60
  enableAnimating = false;
42
61
  isAnimating = false;
43
62
  particles = [];
63
+ particleCount = 0;
44
64
  clientX = Infinity;
45
65
  clientY = Infinity;
46
66
  mouseX = Infinity;
@@ -49,10 +69,8 @@ class CanvasParticles {
49
69
  height;
50
70
  offX;
51
71
  offY;
52
- updateCount;
53
- particleCount;
54
72
  option;
55
- color = { hex: '000000', alpha: 0.0 }; // Overwritten on initialization
73
+ color;
56
74
  /**
57
75
  * Initialize a CanvasParticles instance
58
76
  * @param selector - Canvas element or CSS selector
@@ -87,8 +105,8 @@ class CanvasParticles {
87
105
  this.handleScroll = this.handleScroll.bind(this);
88
106
  this.updateCanvasRect();
89
107
  this.resizeCanvas();
90
- window.addEventListener('mousemove', this.handleMouseMove);
91
- window.addEventListener('scroll', this.handleScroll);
108
+ window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
109
+ window.addEventListener('scroll', this.handleScroll, { passive: true });
92
110
  }
93
111
  /* @public Update the canvas bounding rectangle and mouse position relative to it */
94
112
  updateCanvasRect() {
@@ -125,7 +143,6 @@ class CanvasParticles {
125
143
  // Hide the mouse when resizing because it must be outside the viewport to do so
126
144
  this.mouseX = Infinity;
127
145
  this.mouseY = Infinity;
128
- this.updateCount = Infinity;
129
146
  this.width = Math.max(width + this.option.particles.connectDist * 2, 1);
130
147
  this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
131
148
  this.offX = (width - this.width) / 2;
@@ -163,8 +180,8 @@ class CanvasParticles {
163
180
  }
164
181
  /** @public Create a new particle with optional parameters */
165
182
  createParticle(posX, posY, dir, speed, size) {
166
- posX = typeof posX === 'number' ? posX - this.offX : Math.random() * this.width;
167
- posY = typeof posY === 'number' ? posY - this.offY : Math.random() * this.height;
183
+ posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
184
+ posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
168
185
  const particle = {
169
186
  posX, // Logical position in pixels
170
187
  posY, // Logical position in pixels
@@ -174,9 +191,9 @@ class CanvasParticles {
174
191
  velY: 0, // Vertical speed in pixels per update
175
192
  offX: 0, // Horizontal distance from drawn to logical position in pixels
176
193
  offY: 0, // Vertical distance from drawn to logical position in pixels
177
- dir: dir || Math.random() * 2 * Math.PI, // Direction in radians
178
- speed: speed || (0.5 + Math.random() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
179
- size: size || (0.5 + Math.random() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
194
+ dir: dir || prng() * TWO_PI, // Direction in radians
195
+ speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
196
+ size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
180
197
  gridPos: { x: 1, y: 1 },
181
198
  isVisible: false,
182
199
  };
@@ -193,7 +210,7 @@ class CanvasParticles {
193
210
  left: -particle.size,
194
211
  };
195
212
  }
196
- /** @private Apply gravity forces between particles once every `options.framesPerUpdate` frames */
213
+ /** @private Apply gravity forces between particles */
197
214
  #updateGravity() {
198
215
  const isRepulsiveEnabled = this.option.gravity.repulsive !== 0;
199
216
  const isPullingEnabled = this.option.gravity.pulling !== 0;
@@ -243,13 +260,12 @@ class CanvasParticles {
243
260
  }
244
261
  }
245
262
  }
246
- /** @private Update positions, directions, and visibility of all particles once every `options.framesPerUpdate` frames */
263
+ /** @private Update positions, directions, and visibility of all particles */
247
264
  #updateParticles() {
248
265
  for (let particle of this.particles) {
249
266
  // Randomly perturb direction
250
267
  particle.dir =
251
- (particle.dir + Math.random() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) %
252
- (2 * Math.PI);
268
+ (particle.dir + prng() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) % TWO_PI;
253
269
  particle.velX *= this.option.gravity.friction;
254
270
  particle.velY *= this.option.gravity.friction;
255
271
  particle.posX =
@@ -326,7 +342,7 @@ class CanvasParticles {
326
342
  if (particle.size > 1) {
327
343
  // Draw circle
328
344
  this.ctx.beginPath();
329
- this.ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);
345
+ this.ctx.arc(particle.x, particle.y, particle.size, 0, TWO_PI);
330
346
  this.ctx.fill();
331
347
  this.ctx.closePath();
332
348
  }
@@ -393,12 +409,9 @@ class CanvasParticles {
393
409
  if (!this.isAnimating)
394
410
  return;
395
411
  requestAnimationFrame(() => this.#animation());
396
- if (++this.updateCount >= this.option.framesPerUpdate) {
397
- this.updateCount = 0;
398
- this.#updateGravity();
399
- this.#updateParticles();
400
- this.#render();
401
- }
412
+ this.#updateGravity();
413
+ this.#updateParticles();
414
+ this.#render();
402
415
  }
403
416
  /** @public Start the particle animation if it was not running before */
404
417
  start({ auto = false } = {}) {
@@ -419,7 +432,7 @@ class CanvasParticles {
419
432
  this.enableAnimating = false;
420
433
  this.isAnimating = false;
421
434
  if (clear !== false)
422
- this.canvas.width = this.canvas.width;
435
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
423
436
  return true;
424
437
  }
425
438
  /** @public Gracefully destroy the instance and remove the canvas element */
@@ -442,7 +455,6 @@ class CanvasParticles {
442
455
  // Format and parse all options
443
456
  this.option = {
444
457
  background: options.background ?? false,
445
- framesPerUpdate: parseNumericOption(options.framesPerUpdate, 1, { min: 1 }),
446
458
  animation: {
447
459
  startOnEnter: !!(options.animation?.startOnEnter ?? true),
448
460
  stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
@@ -495,18 +507,22 @@ class CanvasParticles {
495
507
  this.ctx.fillStyle = color;
496
508
  // Check if `ctx.fillStyle` is in hex format ("#RRGGBB")
497
509
  if (String(this.ctx.fillStyle)[0] === '#') {
498
- this.color.hex = String(this.ctx.fillStyle);
499
- this.color.alpha = 1.0;
510
+ this.color = {
511
+ hex: String(this.ctx.fillStyle),
512
+ alpha: 1.0,
513
+ };
500
514
  }
501
515
  else {
502
516
  // JavaScript's `ctx.fillStyle` causes the color to otherwise end up in in rgba format ("rgba(136, 244, 255, 0.25)")
503
517
  // Extract the alpha value from the rgba string
504
518
  let alpha = String(this.ctx.fillStyle).split(',').at(-1); // ' 0.25)'
505
519
  alpha = alpha?.slice(1, -1) ?? '1'; // '0.25'
506
- this.color.alpha = isNaN(+alpha) ? 1 : +alpha; // 0.25 or 1
507
520
  // Extracts e.g. 136, 244 and 255 from rgba(136, 244, 255, 0.25) and converts it to '#rrggbb'
508
521
  this.ctx.fillStyle = String(this.ctx.fillStyle).split(',').slice(0, -1).join(',') + ', 1)';
509
- this.color.hex = this.ctx.fillStyle;
522
+ this.color = {
523
+ hex: String(this.ctx.fillStyle),
524
+ alpha: isNaN(+alpha) ? 1 : +alpha,
525
+ }; // 0.25 or 1
510
526
  }
511
527
  }
512
528
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { CanvasParticlesCanvas, Particle } from './types';
1
+ import type { CanvasParticlesCanvas, Particle, ContextColor } from './types';
2
2
  import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options';
3
3
  export default class CanvasParticles {
4
4
  #private;
5
- static version: string;
5
+ static readonly version: string;
6
6
  /** Defines mouse interaction types with the particles */
7
7
  static interactionType: Readonly<{
8
8
  NONE: 0;
@@ -10,13 +10,15 @@ export default class CanvasParticles {
10
10
  MOVE: 2;
11
11
  }>;
12
12
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
13
- static canvasIntersectionObserver: IntersectionObserver;
14
- static canvasResizeObserver: ResizeObserver;
13
+ static readonly canvasIntersectionObserver: IntersectionObserver;
14
+ static readonly canvasResizeObserver: ResizeObserver;
15
15
  canvas: CanvasParticlesCanvas;
16
16
  private ctx;
17
+ private lastAnimationFrame;
17
18
  enableAnimating: boolean;
18
19
  isAnimating: boolean;
19
20
  particles: Particle[];
21
+ particleCount: number;
20
22
  private clientX;
21
23
  private clientY;
22
24
  mouseX: number;
@@ -25,10 +27,8 @@ export default class CanvasParticles {
25
27
  height: number;
26
28
  private offX;
27
29
  private offY;
28
- private updateCount;
29
- particleCount: number;
30
30
  option: CanvasParticlesOptions;
31
- private color;
31
+ color: ContextColor;
32
32
  /**
33
33
  * Initialize a CanvasParticles instance
34
34
  * @param selector - Canvas element or CSS selector
package/dist/index.mjs CHANGED
@@ -1,7 +1,25 @@
1
1
  // Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
2
2
  // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
3
+ const TWO_PI = 2 * Math.PI;
4
+ /** Extremely fast, simple 32‑bit PRNG */
5
+ function Mulberry32(seed) {
6
+ let state = seed >>> 0;
7
+ return {
8
+ next() {
9
+ let t = (state + 0x6d2b79f5) | 0;
10
+ state = t;
11
+ t = Math.imul(t ^ (t >>> 15), t | 1);
12
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
13
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
14
+ },
15
+ };
16
+ }
17
+ // Mulberry32 is ±388% faster than Math.random()
18
+ // Benchmark: https://jsbm.dev/muLCWR9RJCbmy
19
+ // Spectral test: /demo/mulberry32.html
20
+ const prng = Mulberry32(Math.random() * 2 ** 32).next;
3
21
  class CanvasParticles {
4
- static version = "4.1.2";
22
+ static version = "4.1.4";
5
23
  /** Defines mouse interaction types with the particles */
6
24
  static interactionType = Object.freeze({
7
25
  NONE: 0, // No mouse interaction
@@ -36,9 +54,11 @@ class CanvasParticles {
36
54
  });
37
55
  canvas;
38
56
  ctx;
57
+ lastAnimationFrame = 0;
39
58
  enableAnimating = false;
40
59
  isAnimating = false;
41
60
  particles = [];
61
+ particleCount = 0;
42
62
  clientX = Infinity;
43
63
  clientY = Infinity;
44
64
  mouseX = Infinity;
@@ -47,10 +67,8 @@ class CanvasParticles {
47
67
  height;
48
68
  offX;
49
69
  offY;
50
- updateCount;
51
- particleCount;
52
70
  option;
53
- color = { hex: '000000', alpha: 0.0 }; // Overwritten on initialization
71
+ color;
54
72
  /**
55
73
  * Initialize a CanvasParticles instance
56
74
  * @param selector - Canvas element or CSS selector
@@ -85,8 +103,8 @@ class CanvasParticles {
85
103
  this.handleScroll = this.handleScroll.bind(this);
86
104
  this.updateCanvasRect();
87
105
  this.resizeCanvas();
88
- window.addEventListener('mousemove', this.handleMouseMove);
89
- window.addEventListener('scroll', this.handleScroll);
106
+ window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
107
+ window.addEventListener('scroll', this.handleScroll, { passive: true });
90
108
  }
91
109
  /* @public Update the canvas bounding rectangle and mouse position relative to it */
92
110
  updateCanvasRect() {
@@ -123,7 +141,6 @@ class CanvasParticles {
123
141
  // Hide the mouse when resizing because it must be outside the viewport to do so
124
142
  this.mouseX = Infinity;
125
143
  this.mouseY = Infinity;
126
- this.updateCount = Infinity;
127
144
  this.width = Math.max(width + this.option.particles.connectDist * 2, 1);
128
145
  this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
129
146
  this.offX = (width - this.width) / 2;
@@ -161,8 +178,8 @@ class CanvasParticles {
161
178
  }
162
179
  /** @public Create a new particle with optional parameters */
163
180
  createParticle(posX, posY, dir, speed, size) {
164
- posX = typeof posX === 'number' ? posX - this.offX : Math.random() * this.width;
165
- posY = typeof posY === 'number' ? posY - this.offY : Math.random() * this.height;
181
+ posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
182
+ posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
166
183
  const particle = {
167
184
  posX, // Logical position in pixels
168
185
  posY, // Logical position in pixels
@@ -172,9 +189,9 @@ class CanvasParticles {
172
189
  velY: 0, // Vertical speed in pixels per update
173
190
  offX: 0, // Horizontal distance from drawn to logical position in pixels
174
191
  offY: 0, // Vertical distance from drawn to logical position in pixels
175
- dir: dir || Math.random() * 2 * Math.PI, // Direction in radians
176
- speed: speed || (0.5 + Math.random() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
177
- size: size || (0.5 + Math.random() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
192
+ dir: dir || prng() * TWO_PI, // Direction in radians
193
+ speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
194
+ size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
178
195
  gridPos: { x: 1, y: 1 },
179
196
  isVisible: false,
180
197
  };
@@ -191,7 +208,7 @@ class CanvasParticles {
191
208
  left: -particle.size,
192
209
  };
193
210
  }
194
- /** @private Apply gravity forces between particles once every `options.framesPerUpdate` frames */
211
+ /** @private Apply gravity forces between particles */
195
212
  #updateGravity() {
196
213
  const isRepulsiveEnabled = this.option.gravity.repulsive !== 0;
197
214
  const isPullingEnabled = this.option.gravity.pulling !== 0;
@@ -241,13 +258,12 @@ class CanvasParticles {
241
258
  }
242
259
  }
243
260
  }
244
- /** @private Update positions, directions, and visibility of all particles once every `options.framesPerUpdate` frames */
261
+ /** @private Update positions, directions, and visibility of all particles */
245
262
  #updateParticles() {
246
263
  for (let particle of this.particles) {
247
264
  // Randomly perturb direction
248
265
  particle.dir =
249
- (particle.dir + Math.random() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) %
250
- (2 * Math.PI);
266
+ (particle.dir + prng() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) % TWO_PI;
251
267
  particle.velX *= this.option.gravity.friction;
252
268
  particle.velY *= this.option.gravity.friction;
253
269
  particle.posX =
@@ -324,7 +340,7 @@ class CanvasParticles {
324
340
  if (particle.size > 1) {
325
341
  // Draw circle
326
342
  this.ctx.beginPath();
327
- this.ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);
343
+ this.ctx.arc(particle.x, particle.y, particle.size, 0, TWO_PI);
328
344
  this.ctx.fill();
329
345
  this.ctx.closePath();
330
346
  }
@@ -391,12 +407,9 @@ class CanvasParticles {
391
407
  if (!this.isAnimating)
392
408
  return;
393
409
  requestAnimationFrame(() => this.#animation());
394
- if (++this.updateCount >= this.option.framesPerUpdate) {
395
- this.updateCount = 0;
396
- this.#updateGravity();
397
- this.#updateParticles();
398
- this.#render();
399
- }
410
+ this.#updateGravity();
411
+ this.#updateParticles();
412
+ this.#render();
400
413
  }
401
414
  /** @public Start the particle animation if it was not running before */
402
415
  start({ auto = false } = {}) {
@@ -417,7 +430,7 @@ class CanvasParticles {
417
430
  this.enableAnimating = false;
418
431
  this.isAnimating = false;
419
432
  if (clear !== false)
420
- this.canvas.width = this.canvas.width;
433
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
421
434
  return true;
422
435
  }
423
436
  /** @public Gracefully destroy the instance and remove the canvas element */
@@ -440,7 +453,6 @@ class CanvasParticles {
440
453
  // Format and parse all options
441
454
  this.option = {
442
455
  background: options.background ?? false,
443
- framesPerUpdate: parseNumericOption(options.framesPerUpdate, 1, { min: 1 }),
444
456
  animation: {
445
457
  startOnEnter: !!(options.animation?.startOnEnter ?? true),
446
458
  stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
@@ -493,18 +505,22 @@ class CanvasParticles {
493
505
  this.ctx.fillStyle = color;
494
506
  // Check if `ctx.fillStyle` is in hex format ("#RRGGBB")
495
507
  if (String(this.ctx.fillStyle)[0] === '#') {
496
- this.color.hex = String(this.ctx.fillStyle);
497
- this.color.alpha = 1.0;
508
+ this.color = {
509
+ hex: String(this.ctx.fillStyle),
510
+ alpha: 1.0,
511
+ };
498
512
  }
499
513
  else {
500
514
  // JavaScript's `ctx.fillStyle` causes the color to otherwise end up in in rgba format ("rgba(136, 244, 255, 0.25)")
501
515
  // Extract the alpha value from the rgba string
502
516
  let alpha = String(this.ctx.fillStyle).split(',').at(-1); // ' 0.25)'
503
517
  alpha = alpha?.slice(1, -1) ?? '1'; // '0.25'
504
- this.color.alpha = isNaN(+alpha) ? 1 : +alpha; // 0.25 or 1
505
518
  // Extracts e.g. 136, 244 and 255 from rgba(136, 244, 255, 0.25) and converts it to '#rrggbb'
506
519
  this.ctx.fillStyle = String(this.ctx.fillStyle).split(',').slice(0, -1).join(',') + ', 1)';
507
- this.color.hex = this.ctx.fillStyle;
520
+ this.color = {
521
+ hex: String(this.ctx.fillStyle),
522
+ alpha: isNaN(+alpha) ? 1 : +alpha,
523
+ }; // 0.25 or 1
508
524
  }
509
525
  }
510
526
  }
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";class t{static version="4.1.2";static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,s=t.instance;if(!s.options?.animation)return;(t.inViewbox=i.isIntersecting)?s.options.animation?.startOnEnter&&s.start({auto:!0}):s.options.animation?.stopOnLeave&&s.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}for(const i of t){i.target.instance.resizeCanvas()}});canvas;ctx;enableAnimating=!1;isAnimating=!1;particles=[];clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;updateCount;particleCount;option;color={hex:"000000",alpha:0};constructor(i,s={}){let e;if(i instanceof HTMLCanvasElement)e=i;else{if("string"!=typeof i)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(e=document.querySelector(i),!(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 o=this.canvas.getContext("2d");if(!o)throw new Error("failed to get 2D context from canvas");this.ctx=o,this.options=s,t.canvasIntersectionObserver.observe(this.canvas),t.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove),window.addEventListener("scroll",this.handleScroll)}updateCanvasRect(){const{top:t,left:i,width:s,height:e}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:s,height:e}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.updateCount=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("number of particles must be finite. (options.particles.ppm)")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#s(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(t,i,s,e,o){const n={posX:t="number"==typeof t?t-this.offX:Math.random()*this.width,posY:i="number"==typeof i?i-this.offY:Math.random()*this.height,x:t,y:i,velX:0,velY:0,offX:0,offY:0,dir:s||2*Math.random()*Math.PI,speed:e||(.5+.5*Math.random())*this.option.particles.relSpeed,size:o||(.5+Math.random()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#s(n),this.particles.push(n)}#s(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#e(){const t=0!==this.option.gravity.repulsive,i=0!==this.option.gravity.pulling;if(!t&&!i)return;const s=this.particleCount,e=this.option.particles.connectDist*this.option.gravity.repulsive,o=this.option.particles.connectDist*this.option.gravity.pulling,n=this.option.particles.connectDist/2,a=.1*this.option.particles.connectDist;for(let t=0;t<s;t++)for(let r=t+1;r<s;r++){const s=this.particles[t],h=this.particles[r],c=s.posX-h.posX,l=s.posY-h.posY,p=Math.sqrt(c*c+l*l);let d,u=1;if(p<n){d=Math.atan2(h.posY-s.posY,h.posX-s.posX),u=(1/p)**1.8;const t=Math.min(a,u*e),i=Math.cos(d)*t,o=Math.sin(d)*t;s.velX-=i,s.velY-=o,h.velX+=i,h.velY+=o}if(!i)continue;void 0===d&&(d=Math.atan2(h.posY-s.posY,h.posX-s.posX),u=(1/p)**1.8);const f=Math.min(a,u*o),v=Math.cos(d)*f,m=Math.sin(d)*f;s.velX+=v,s.velY+=m,h.velX-=v,h.velY-=m}}#o(){for(let i of this.particles){i.dir=(i.dir+Math.random()*this.option.particles.rotationSpeed*2-this.option.particles.rotationSpeed)%(2*Math.PI),i.velX*=this.option.gravity.friction,i.velY*=this.option.gravity.friction,i.posX=(i.posX+i.velX+Math.sin(i.dir)*i.speed%this.width+this.width)%this.width,i.posY=(i.posY+i.velY+Math.cos(i.dir)*i.speed%this.height+this.height)%this.height;const s=i.posX+this.offX-this.mouseX,e=i.posY+this.offY-this.mouseY;if(this.option.mouse.interactionType!==t.interactionType.NONE){const t=this.option.mouse.connectDist/Math.hypot(s,e);this.option.mouse.distRatio<t?(i.offX+=(t*s-s-i.offX)/4,i.offY+=(t*e-e-i.offY)/4):(i.offX-=i.offX/4,i.offY-=i.offY/4)}i.x=i.posX+i.offX,i.y=i.posY+i.offY,this.option.mouse.interactionType===t.interactionType.MOVE&&(i.posX=i.x,i.posY=i.y),i.x+=this.offX,i.y+=this.offY,i.gridPos=this.#n(i),i.isVisible=1===i.gridPos.x&&1===i.gridPos.y}}#n(t){return{x:+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),y:+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){for(let t of this.particles)t.isVisible&&(t.size>1?(this.ctx.beginPath(),this.ctx.arc(t.x,t.y,t.size,0,2*Math.PI),this.ctx.fill(),this.ctx.closePath()):this.ctx.fillRect(t.x-t.size,t.y-t.size,2*t.size,2*t.size))}#h(){const t=this.particleCount,i=this.option.particles.connectDist,s=i>=Math.min(this.canvas.width,this.canvas.height),e=i*this.option.particles.maxWork,o=this.color.alpha,n=this.color.alpha*i;for(let a=0;a<t;a++){let r=0;for(let h=a+1;h<t;h++){const t=this.particles[a],c=this.particles[h];if(!s&&!this.#a(t,c))continue;const l=t.x-c.x,p=t.y-c.y,d=Math.sqrt(l*l+p*p);if(!(d>i)&&(this.ctx.globalAlpha=d>i/2?n/d-o:o,this.ctx.beginPath(),this.ctx.moveTo(t.x,t.y),this.ctx.lineTo(c.x,c.y),this.ctx.stroke(),(r+=d)>=e))break}}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.#h()}#c(){this.isAnimating&&(requestAnimationFrame(()=>this.#c()),++this.updateCount>=this.option.framesPerUpdate&&(this.updateCount=0,this.#e(),this.#o(),this.#t()))}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#c())),!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.canvas.width=this.canvas.width),!0}destroy(){this.stop(),t.canvasIntersectionObserver.unobserve(this.canvas),t.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=(t,i,s)=>{const{min:e=-1/0,max:o=1/0}=s??{};return((t,i)=>isNaN(+t)?i:+t)(Math.min(Math.max(t??i,e),o),i)};this.option={background:t.background??!1,framesPerUpdate:i(t.framesPerUpdate,1,{min:1}),animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:i(t.mouse?.interactionType,1),connectDistMult:i(t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i(t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i(t.particles?.ppm,100),max:i(t.particles?.max,500),maxWork:i(t.particles?.maxWork,1/0,{min:0}),connectDist:i(t.particles?.connectDistance,150,{min:1}),relSpeed:i(t.particles?.relSpeed,1,{min:0}),relSize:i(t.particles?.relSize,1,{min:1}),rotationSpeed:i(t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i(t.gravity?.repulsive,0),pulling:i(t.gravity?.pulling,0),friction:i(t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color.hex=String(this.ctx.fillStyle),this.color.alpha=1;else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.color.alpha=isNaN(+t)?1:+t,this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color.hex=this.ctx.fillStyle}}}return t});
1
+ !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class s{static version="4.1.4";static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,s=t.instance;if(!s.options?.animation)return;(t.inViewbox=i.isIntersecting)?s.options.animation?.startOnEnter&&s.start({auto:!0}):s.options.animation?.stopOnLeave&&s.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}for(const i of t){i.target.instance.resizeCanvas()}});canvas;ctx;lastAnimationFrame=0;enableAnimating=!1;isAnimating=!1;particles=[];particleCount=0;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let 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.canvasIntersectionObserver.observe(this.canvas),s.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:s,height:e}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:s,height:e}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("number of particles must be finite. (options.particles.ppm)")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#s(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(s,e,n,o,a){const r={posX:s="number"==typeof s?s-this.offX:i()*this.width,posY:e="number"==typeof e?e-this.offY:i()*this.height,x:s,y:e,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#s(r),this.particles.push(r)}#s(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#e(){const t=0!==this.option.gravity.repulsive,i=0!==this.option.gravity.pulling;if(!t&&!i)return;const s=this.particleCount,e=this.option.particles.connectDist*this.option.gravity.repulsive,n=this.option.particles.connectDist*this.option.gravity.pulling,o=this.option.particles.connectDist/2,a=.1*this.option.particles.connectDist;for(let t=0;t<s;t++)for(let r=t+1;r<s;r++){const s=this.particles[t],h=this.particles[r],c=s.posX-h.posX,l=s.posY-h.posY,p=Math.sqrt(c*c+l*l);let u,d=1;if(p<o){u=Math.atan2(h.posY-s.posY,h.posX-s.posX),d=(1/p)**1.8;const t=Math.min(a,d*e),i=Math.cos(u)*t,n=Math.sin(u)*t;s.velX-=i,s.velY-=n,h.velX+=i,h.velY+=n}if(!i)continue;void 0===u&&(u=Math.atan2(h.posY-s.posY,h.posX-s.posX),d=(1/p)**1.8);const f=Math.min(a,d*n),v=Math.cos(u)*f,m=Math.sin(u)*f;s.velX+=v,s.velY+=m,h.velX-=v,h.velY-=m}}#n(){for(let e of this.particles){e.dir=(e.dir+i()*this.option.particles.rotationSpeed*2-this.option.particles.rotationSpeed)%t,e.velX*=this.option.gravity.friction,e.velY*=this.option.gravity.friction,e.posX=(e.posX+e.velX+Math.sin(e.dir)*e.speed%this.width+this.width)%this.width,e.posY=(e.posY+e.velY+Math.cos(e.dir)*e.speed%this.height+this.height)%this.height;const n=e.posX+this.offX-this.mouseX,o=e.posY+this.offY-this.mouseY;if(this.option.mouse.interactionType!==s.interactionType.NONE){const t=this.option.mouse.connectDist/Math.hypot(n,o);this.option.mouse.distRatio<t?(e.offX+=(t*n-n-e.offX)/4,e.offY+=(t*o-o-e.offY)/4):(e.offX-=e.offX/4,e.offY-=e.offY/4)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,this.option.mouse.interactionType===s.interactionType.MOVE&&(e.posX=e.x,e.posY=e.y),e.x+=this.offX,e.y+=this.offY,e.gridPos=this.#o(e),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#o(t){return{x:+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),y:+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){for(let i of this.particles)i.isVisible&&(i.size>1?(this.ctx.beginPath(),this.ctx.arc(i.x,i.y,i.size,0,t),this.ctx.fill(),this.ctx.closePath()):this.ctx.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}#h(){const t=this.particleCount,i=this.option.particles.connectDist,s=i>=Math.min(this.canvas.width,this.canvas.height),e=i*this.option.particles.maxWork,n=this.color.alpha,o=this.color.alpha*i;for(let a=0;a<t;a++){let r=0;for(let h=a+1;h<t;h++){const t=this.particles[a],c=this.particles[h];if(!s&&!this.#a(t,c))continue;const l=t.x-c.x,p=t.y-c.y,u=Math.sqrt(l*l+p*p);if(!(u>i)&&(this.ctx.globalAlpha=u>i/2?o/u-n:n,this.ctx.beginPath(),this.ctx.moveTo(t.x,t.y),this.ctx.lineTo(c.x,c.y),this.ctx.stroke(),(r+=u)>=e))break}}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.#h()}#c(){this.isAnimating&&(requestAnimationFrame(()=>this.#c()),this.#e(),this.#n(),this.#t())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#c())),!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.canvasIntersectionObserver.unobserve(this.canvas),s.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=(t,i,s)=>{const{min:e=-1/0,max:n=1/0}=s??{};return((t,i)=>isNaN(+t)?i:+t)(Math.min(Math.max(t??i,e),n),i)};this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:i(t.mouse?.interactionType,1),connectDistMult:i(t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i(t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i(t.particles?.ppm,100),max:i(t.particles?.max,500),maxWork:i(t.particles?.maxWork,1/0,{min:0}),connectDist:i(t.particles?.connectDistance,150,{min:1}),relSpeed:i(t.particles?.relSpeed,1,{min:0}),relSize:i(t.particles?.relSize,1,{min:1}),rotationSpeed:i(t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i(t.gravity?.repulsive,0),pulling:i(t.gravity?.pulling,0),friction:i(t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}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 s});
@@ -1,6 +1,5 @@
1
1
  export interface CanvasParticlesOptions {
2
2
  background: CSSStyleDeclaration['background'] | false;
3
- framesPerUpdate: number;
4
3
  animation: {
5
4
  startOnEnter: boolean;
6
5
  stopOnLeave: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
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
@@ -4,10 +4,32 @@
4
4
  import type { CanvasParticlesCanvas, Particle, ParticleGridPos, ContextColor } from './types'
5
5
  import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
6
6
 
7
+ const TWO_PI = 2 * Math.PI
8
+
9
+ /** Extremely fast, simple 32‑bit PRNG */
10
+ function Mulberry32(seed: number) {
11
+ let state = seed >>> 0
12
+
13
+ return {
14
+ next() {
15
+ let t = (state + 0x6d2b79f5) | 0
16
+ state = t
17
+ t = Math.imul(t ^ (t >>> 15), t | 1)
18
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
19
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
20
+ },
21
+ }
22
+ }
23
+
24
+ // Mulberry32 is ±388% faster than Math.random()
25
+ // Benchmark: https://jsbm.dev/muLCWR9RJCbmy
26
+ // Spectral test: /demo/mulberry32.html
27
+ const prng = Mulberry32(Math.random() * 2 ** 32).next
28
+
7
29
  declare const __VERSION__: string
8
30
 
9
31
  export default class CanvasParticles {
10
- static version = __VERSION__
32
+ static readonly version = __VERSION__
11
33
 
12
34
  /** Defines mouse interaction types with the particles */
13
35
  static interactionType = Object.freeze({
@@ -17,7 +39,7 @@ export default class CanvasParticles {
17
39
  })
18
40
 
19
41
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
20
- static canvasIntersectionObserver = new IntersectionObserver((entries) => {
42
+ static readonly canvasIntersectionObserver = new IntersectionObserver((entries) => {
21
43
  for (const entry of entries) {
22
44
  const canvas = entry.target as CanvasParticlesCanvas
23
45
  const instance = canvas.instance // The CanvasParticles class instance bound to this canvas
@@ -30,7 +52,7 @@ export default class CanvasParticles {
30
52
  }
31
53
  })
32
54
 
33
- static canvasResizeObserver = new ResizeObserver((entries) => {
55
+ static readonly canvasResizeObserver = new ResizeObserver((entries) => {
34
56
  // Seperate for loops is very important to prevent huge forced reflow overhead
35
57
 
36
58
  // First read all canvas rects at once
@@ -48,11 +70,13 @@ export default class CanvasParticles {
48
70
 
49
71
  canvas: CanvasParticlesCanvas
50
72
  private ctx: CanvasRenderingContext2D
73
+ private lastAnimationFrame: number = 0
51
74
 
52
75
  enableAnimating: boolean = false
53
76
  isAnimating: boolean = false
54
77
 
55
78
  particles: Particle[] = []
79
+ particleCount: number = 0
56
80
 
57
81
  private clientX: number = Infinity
58
82
  private clientY: number = Infinity
@@ -62,10 +86,8 @@ export default class CanvasParticles {
62
86
  height!: number
63
87
  private offX!: number
64
88
  private offY!: number
65
- private updateCount!: number
66
- particleCount!: number
67
89
  option!: CanvasParticlesOptions
68
- private color: ContextColor = { hex: '000000', alpha: 0.0 } // Overwritten on initialization
90
+ color!: ContextColor
69
91
 
70
92
  /**
71
93
  * Initialize a CanvasParticles instance
@@ -106,8 +128,8 @@ export default class CanvasParticles {
106
128
  this.updateCanvasRect()
107
129
  this.resizeCanvas()
108
130
 
109
- window.addEventListener('mousemove', this.handleMouseMove)
110
- window.addEventListener('scroll', this.handleScroll)
131
+ window.addEventListener('mousemove', this.handleMouseMove, { passive: true })
132
+ window.addEventListener('scroll', this.handleScroll, { passive: true })
111
133
  }
112
134
 
113
135
  /* @public Update the canvas bounding rectangle and mouse position relative to it */
@@ -149,7 +171,6 @@ export default class CanvasParticles {
149
171
  this.mouseX = Infinity
150
172
  this.mouseY = Infinity
151
173
 
152
- this.updateCount = Infinity
153
174
  this.width = Math.max(width + this.option.particles.connectDist * 2, 1)
154
175
  this.height = Math.max(height + this.option.particles.connectDist * 2, 1)
155
176
  this.offX = (width - this.width) / 2
@@ -190,8 +211,8 @@ export default class CanvasParticles {
190
211
 
191
212
  /** @public Create a new particle with optional parameters */
192
213
  createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number) {
193
- posX = typeof posX === 'number' ? posX - this.offX : Math.random() * this.width
194
- posY = typeof posY === 'number' ? posY - this.offY : Math.random() * this.height
214
+ posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width
215
+ posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height
195
216
 
196
217
  const particle: Omit<Particle, 'bounds'> = {
197
218
  posX, // Logical position in pixels
@@ -202,9 +223,9 @@ export default class CanvasParticles {
202
223
  velY: 0, // Vertical speed in pixels per update
203
224
  offX: 0, // Horizontal distance from drawn to logical position in pixels
204
225
  offY: 0, // Vertical distance from drawn to logical position in pixels
205
- dir: dir || Math.random() * 2 * Math.PI, // Direction in radians
206
- speed: speed || (0.5 + Math.random() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
207
- size: size || (0.5 + Math.random() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
226
+ dir: dir || prng() * TWO_PI, // Direction in radians
227
+ speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
228
+ size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
208
229
  gridPos: { x: 1, y: 1 },
209
230
  isVisible: false,
210
231
  }
@@ -223,7 +244,7 @@ export default class CanvasParticles {
223
244
  }
224
245
  }
225
246
 
226
- /** @private Apply gravity forces between particles once every `options.framesPerUpdate` frames */
247
+ /** @private Apply gravity forces between particles */
227
248
  #updateGravity() {
228
249
  const isRepulsiveEnabled = this.option.gravity.repulsive !== 0
229
250
  const isPullingEnabled = this.option.gravity.pulling !== 0
@@ -281,13 +302,12 @@ export default class CanvasParticles {
281
302
  }
282
303
  }
283
304
 
284
- /** @private Update positions, directions, and visibility of all particles once every `options.framesPerUpdate` frames */
305
+ /** @private Update positions, directions, and visibility of all particles */
285
306
  #updateParticles() {
286
307
  for (let particle of this.particles) {
287
308
  // Randomly perturb direction
288
309
  particle.dir =
289
- (particle.dir + Math.random() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) %
290
- (2 * Math.PI)
310
+ (particle.dir + prng() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) % TWO_PI
291
311
  particle.velX *= this.option.gravity.friction
292
312
  particle.velY *= this.option.gravity.friction
293
313
  particle.posX =
@@ -374,7 +394,7 @@ export default class CanvasParticles {
374
394
  if (particle.size > 1) {
375
395
  // Draw circle
376
396
  this.ctx.beginPath()
377
- this.ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI)
397
+ this.ctx.arc(particle.x, particle.y, particle.size, 0, TWO_PI)
378
398
  this.ctx.fill()
379
399
  this.ctx.closePath()
380
400
  } else {
@@ -451,12 +471,9 @@ export default class CanvasParticles {
451
471
 
452
472
  requestAnimationFrame(() => this.#animation())
453
473
 
454
- if (++this.updateCount >= this.option.framesPerUpdate) {
455
- this.updateCount = 0
456
- this.#updateGravity()
457
- this.#updateParticles()
458
- this.#render()
459
- }
474
+ this.#updateGravity()
475
+ this.#updateParticles()
476
+ this.#render()
460
477
  }
461
478
 
462
479
  /** @public Start the particle animation if it was not running before */
@@ -478,8 +495,7 @@ export default class CanvasParticles {
478
495
  stop({ auto = false, clear = true }: { auto?: boolean; clear?: boolean } = {}): boolean {
479
496
  if (!auto) this.enableAnimating = false
480
497
  this.isAnimating = false
481
- if (clear !== false) this.canvas.width = this.canvas.width
482
-
498
+ if (clear !== false) this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
483
499
  return true
484
500
  }
485
501
 
@@ -514,7 +530,6 @@ export default class CanvasParticles {
514
530
  // Format and parse all options
515
531
  this.option = {
516
532
  background: options.background ?? false,
517
- framesPerUpdate: parseNumericOption(options.framesPerUpdate, 1, { min: 1 }),
518
533
  animation: {
519
534
  startOnEnter: !!(options.animation?.startOnEnter ?? true),
520
535
  stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
@@ -571,19 +586,24 @@ export default class CanvasParticles {
571
586
 
572
587
  // Check if `ctx.fillStyle` is in hex format ("#RRGGBB")
573
588
  if (String(this.ctx.fillStyle)[0] === '#') {
574
- this.color.hex = String(this.ctx.fillStyle)
575
- this.color.alpha = 1.0
589
+ this.color = {
590
+ hex: String(this.ctx.fillStyle),
591
+ alpha: 1.0,
592
+ }
576
593
  } else {
577
594
  // JavaScript's `ctx.fillStyle` causes the color to otherwise end up in in rgba format ("rgba(136, 244, 255, 0.25)")
578
595
 
579
596
  // Extract the alpha value from the rgba string
580
597
  let alpha = String(this.ctx.fillStyle).split(',').at(-1) // ' 0.25)'
581
598
  alpha = alpha?.slice(1, -1) ?? '1' // '0.25'
582
- this.color.alpha = isNaN(+alpha) ? 1 : +alpha // 0.25 or 1
583
599
 
584
600
  // Extracts e.g. 136, 244 and 255 from rgba(136, 244, 255, 0.25) and converts it to '#rrggbb'
585
601
  this.ctx.fillStyle = String(this.ctx.fillStyle).split(',').slice(0, -1).join(',') + ', 1)'
586
- this.color.hex = this.ctx.fillStyle
602
+
603
+ this.color = {
604
+ hex: String(this.ctx.fillStyle),
605
+ alpha: isNaN(+alpha) ? 1 : +alpha,
606
+ } // 0.25 or 1
587
607
  }
588
608
  }
589
609
  }
@@ -1,6 +1,5 @@
1
1
  export interface CanvasParticlesOptions {
2
2
  background: CSSStyleDeclaration['background'] | false
3
- framesPerUpdate: number
4
3
 
5
4
  animation: {
6
5
  startOnEnter: boolean