canvasparticles-js 4.4.2 → 4.4.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/README.md CHANGED
@@ -175,8 +175,7 @@ Play around with these values in the [Sandbox](https://khoeckman.github.io/canva
175
175
 
176
176
  ### Options Object
177
177
 
178
- The default value will be used when an option is assigned an invalid value.<br>
179
- Your screen resolution and refresh rate will directly impact perfomance!
178
+ The default value will be used when an option is assigned an invalid value.
180
179
 
181
180
  ### Root options
182
181
 
@@ -188,6 +187,8 @@ Your screen resolution and refresh rate will directly impact perfomance!
188
187
 
189
188
  ### `animation`
190
189
 
190
+ It's best to not touch these values if it's unclear what it does.
191
+
191
192
  | Option | Type | Default | Description |
192
193
  | ------------------------ | --------- | ------- | ---------------------------------------------------- |
193
194
  | `animation.startOnEnter` | `boolean` | `true` | Start animation when the canvas enters the viewport. |
@@ -207,24 +208,30 @@ Your screen resolution and refresh rate will directly impact perfomance!
207
208
 
208
209
  - `NONE (0)` – No interaction
209
210
  - `SHIFT (1)` – Visual displacement only
210
- - `MOVE (2)` – Actual particle movement
211
+ - `MOVE (2)` – Actual particle movement (default)
211
212
 
212
213
  ---
213
214
 
214
215
  ### `particles`
215
216
 
216
- | Option | Type | Default | Description |
217
- | --------------------------- | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- |
218
- | `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `0 = OFF`, `1 = NEW`, `2 = MATCH` |
219
- | `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
220
- | `particles.ppm` | `integer` | `100` | Particles per million pixels. _Heavily impacts performance_ |
221
- | `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
222
- | `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
223
- | `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). _Heavily impacts performance_ |
224
- | `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
225
- | `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
226
- | `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
227
- | `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
217
+ | Option | Type | Default | Description |
218
+ | --------------------------- | ------------- | ---------- | ------------------------------------------------------------------------------------------------- |
219
+ | `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `1 = OFF`, `2 = MATCH` |
220
+ | `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
221
+ | `particles.ppm` | `integer` | `100` | Particles per million pixels. |
222
+ | `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
223
+ | `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
224
+ | `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). |
225
+ | `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
226
+ | `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
227
+ | `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
228
+ | `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
229
+
230
+ **Generation types** (enum)
231
+
232
+ - `OFF (0)` – Never auto-generate particles
233
+ - `NEW (1)` – Generate all particles from scratch
234
+ - `MATCH (2)` – Add or remove some particles to match the new count (default)
228
235
 
229
236
  ---
230
237
 
@@ -232,11 +239,12 @@ Your screen resolution and refresh rate will directly impact perfomance!
232
239
 
233
240
  Enabling gravity (`repulsive` or `pulling` > 0) performs an extra **O(n²)** gravity computations per frame.
234
241
 
235
- | Option | Type | Default | Description |
236
- | ------------------- | ------- | ------- | -------------------------------------------------------------------------------------- |
237
- | `gravity.repulsive` | `float` | `0` | Repulsive force between particles. Strongly impacts performance. |
238
- | `gravity.pulling` | `float` | `0` | Attractive force between particles. Requires sufficient repulsion to avoid clustering. |
239
- | `gravity.friction` | `float` | `0.8` | Damping factor applied to gravitational velocity each update (`0.0 – 1.0`). |
242
+ | Option | Type | Default | Description |
243
+ | --------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------- |
244
+ | `gravity.repulsive` | `float` | `0` | Repulsive force between particles. |
245
+ | `gravity.pulling` | `float` | `0` | Attractive force between particles. Requires sufficient repulsion to avoid clustering. |
246
+ | `gravity.friction` | `float` | `0.8` | Damping factor applied to gravitational velocity each update (`0.0 – 1.0`). |
247
+ | `gravity.preventExplosions` | `boolean` | `false` | Clamp the maximum velocity so particles will not explode outward under heavy pulling gravity. |
240
248
 
241
249
  ---
242
250
 
@@ -342,13 +350,13 @@ instance.options = { ... }
342
350
  createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number)
343
351
  ```
344
352
 
345
- By default `particles.ppm` and `particles.max` are used to auto-generate random particles. This might destroy manually created particles. To fix this, set `particles.generationType` to `MANUAL (0)`.
353
+ By default `particles.ppm` and `particles.max` are used to auto-generate random particles. Set one or both of these properties to `0` or set `particles.generationType` to `OFF (0)` (slightly more performant).
346
354
 
347
355
  ```js
348
356
  const canvas = '#my-canvas'
349
357
  const options = {
350
358
  particles: {
351
- max: 0,
359
+ generationType: CanvasParticles.generationType.OFF, // = 0
352
360
  rotationSpeed: 0,
353
361
  },
354
362
  }
@@ -393,7 +401,7 @@ instance.newParticles({ keepAuto: true, keepManual: false })
393
401
  const options = {
394
402
  background: 'hsl(125, 42%, 35%)',
395
403
  mouse: {
396
- interactionType: CanvasParticles.interactionType.MOVE, // = 2
404
+ interactionType: CanvasParticles.interactionType.SHIFT, // = 1
397
405
  },
398
406
  particles: {
399
407
  color: 'rgba(150, 255, 105, 0.95)',
package/dist/index.cjs CHANGED
@@ -21,7 +21,7 @@ function Mulberry32(seed) {
21
21
  // Spectral test: /demo/mulberry32.html
22
22
  const prng = Mulberry32(Math.random() * 2 ** 32).next;
23
23
  class CanvasParticles {
24
- static version = "4.4.2";
24
+ static version = "4.4.4";
25
25
  static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
26
26
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
27
27
  /** Defines mouse interaction types with the particles */
@@ -32,9 +32,9 @@ class CanvasParticles {
32
32
  });
33
33
  /** Defines how the particles are auto-generated */
34
34
  static generationType = Object.freeze({
35
- MANUAL: 0, // Never auto-generate particles
36
- NEW: 1, // Generate particles from scratch
37
- MATCH: 2, // Add or remove particles to match new count (default)
35
+ OFF: 0, // Never auto-generate particles
36
+ NEW: 1, // Generate all particles from scratch
37
+ MATCH: 2, // Add or remove some particles to match the new count (default)
38
38
  });
39
39
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
40
40
  static canvasIntersectionObserver = new IntersectionObserver((entries) => {
@@ -169,7 +169,7 @@ class CanvasParticles {
169
169
  this.offX = (width - this.width) / 2;
170
170
  this.offY = (height - this.height) / 2;
171
171
  const generationType = this.option.particles.generationType;
172
- if (generationType !== CanvasParticles.generationType.MANUAL) {
172
+ if (generationType !== CanvasParticles.generationType.OFF) {
173
173
  if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
174
174
  this.newParticles();
175
175
  else if (generationType === CanvasParticles.generationType.MATCH)
@@ -343,12 +343,13 @@ class CanvasParticles {
343
343
  const offY = this.offY;
344
344
  const mouseX = this.mouseX;
345
345
  const mouseY = this.mouseY;
346
- const rotationSpeed = this.option.particles.rotationSpeed * step;
347
- const friction = this.option.gravity.friction;
348
- const mouseConnectDist = this.option.mouse.connectDist;
349
- const mouseDistRatio = this.option.mouse.distRatio;
350
346
  const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
351
347
  const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
348
+ const mouseConnectDist = this.option.mouse.connectDist;
349
+ const mouseDistRatio = this.option.mouse.distRatio;
350
+ const rotationSpeed = this.option.particles.rotationSpeed * step;
351
+ const friction = this.option.gravity.friction;
352
+ const preventExplosions = this.option.gravity.preventExplosions;
352
353
  const easing = 1 - Math.pow(3 / 4, step);
353
354
  for (const p of this.particles) {
354
355
  p.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
@@ -356,6 +357,18 @@ class CanvasParticles {
356
357
  // Constant velocity
357
358
  const movX = Math.sin(p.dir) * p.speed;
358
359
  const movY = Math.cos(p.dir) * p.speed;
360
+ // Maximum velocity
361
+ if (preventExplosions) {
362
+ const maxVel = Math.max(p.speed, friction) * 2;
363
+ if (p.velX > maxVel)
364
+ p.velX = maxVel;
365
+ if (p.velX < -maxVel)
366
+ p.velX = -maxVel;
367
+ if (p.velY > maxVel)
368
+ p.velY = maxVel;
369
+ if (p.velY < -maxVel)
370
+ p.velY = -maxVel;
371
+ }
359
372
  // Apply velocities
360
373
  p.posX += (movX + p.velX) * step;
361
374
  p.posY += (movY + p.velY) * step;
@@ -474,7 +487,7 @@ class CanvasParticles {
474
487
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
475
488
  const alpha = this.color.alpha;
476
489
  const alphaFactor = this.color.alpha * maxDist;
477
- const bucket = []; // Batch line segments of max alpha
490
+ const bucket = []; // Batch line segments of max alpha (2D -> 1D; stride = 4)
478
491
  const grid = this.#buildSpatialGrid(stride, invCellSize); // O(n^2) -> O(n)
479
492
  let particleWork = 0;
480
493
  let allowWork = true;
@@ -494,7 +507,7 @@ class CanvasParticles {
494
507
  }
495
508
  else {
496
509
  // Cache lines with max alpha to later be drawn in one batch
497
- bucket.push([ax, ay, bx, by]);
510
+ bucket.push(ax, ay, bx, by);
498
511
  }
499
512
  particleWork += distSq;
500
513
  allowWork = particleWork < maxWorkPerParticle;
@@ -561,8 +574,7 @@ class CanvasParticles {
561
574
  if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
562
575
  renderConnectionsToOwnCell(cell || [], a, pa);
563
576
  // Next iteration
564
- a++;
565
- if (!(a < len))
577
+ if (++a >= len)
566
578
  break;
567
579
  // Same code inline but the order of grid.get() is different to remove maxWork artifacts
568
580
  particleWork = 0;
@@ -590,8 +602,7 @@ class CanvasParticles {
590
602
  if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
591
603
  renderConnectionsToOwnCell(cell || [], a, pa);
592
604
  // Next iteration
593
- a++;
594
- if (!(a < len))
605
+ if (++a >= len)
595
606
  break;
596
607
  // Same code inline but the order of grid.get() is different to remove maxWork artifacts
597
608
  particleWork = 0;
@@ -624,10 +635,9 @@ class CanvasParticles {
624
635
  // Render all bucketed lines at once
625
636
  ctx.globalAlpha = alpha;
626
637
  ctx.beginPath();
627
- for (let i = 0; i < bucket.length; i++) {
628
- const line = bucket[i];
629
- ctx.moveTo(line[0], line[1]);
630
- ctx.lineTo(line[2], line[3]);
638
+ for (let line = 0; line < bucket.length; line += 4) {
639
+ ctx.moveTo(bucket[line], bucket[line + 1]);
640
+ ctx.lineTo(bucket[line + 2], bucket[line + 3]);
631
641
  }
632
642
  ctx.stroke();
633
643
  }
@@ -764,6 +774,7 @@ class CanvasParticles {
764
774
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
765
775
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
766
776
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
777
+ preventExplosions: !!options.gravity?.preventExplosions,
767
778
  },
768
779
  debug: {
769
780
  drawGrid: !!options.debug?.drawGrid,
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export default class CanvasParticles {
13
13
  }>;
14
14
  /** Defines how the particles are auto-generated */
15
15
  static readonly generationType: Readonly<{
16
- MANUAL: 0;
16
+ OFF: 0;
17
17
  NEW: 1;
18
18
  MATCH: 2;
19
19
  }>;
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ function Mulberry32(seed) {
19
19
  // Spectral test: /demo/mulberry32.html
20
20
  const prng = Mulberry32(Math.random() * 2 ** 32).next;
21
21
  class CanvasParticles {
22
- static version = "4.4.2";
22
+ static version = "4.4.4";
23
23
  static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
24
24
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
25
25
  /** Defines mouse interaction types with the particles */
@@ -30,9 +30,9 @@ class CanvasParticles {
30
30
  });
31
31
  /** Defines how the particles are auto-generated */
32
32
  static generationType = Object.freeze({
33
- MANUAL: 0, // Never auto-generate particles
34
- NEW: 1, // Generate particles from scratch
35
- MATCH: 2, // Add or remove particles to match new count (default)
33
+ OFF: 0, // Never auto-generate particles
34
+ NEW: 1, // Generate all particles from scratch
35
+ MATCH: 2, // Add or remove some particles to match the new count (default)
36
36
  });
37
37
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
38
38
  static canvasIntersectionObserver = new IntersectionObserver((entries) => {
@@ -167,7 +167,7 @@ class CanvasParticles {
167
167
  this.offX = (width - this.width) / 2;
168
168
  this.offY = (height - this.height) / 2;
169
169
  const generationType = this.option.particles.generationType;
170
- if (generationType !== CanvasParticles.generationType.MANUAL) {
170
+ if (generationType !== CanvasParticles.generationType.OFF) {
171
171
  if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
172
172
  this.newParticles();
173
173
  else if (generationType === CanvasParticles.generationType.MATCH)
@@ -341,12 +341,13 @@ class CanvasParticles {
341
341
  const offY = this.offY;
342
342
  const mouseX = this.mouseX;
343
343
  const mouseY = this.mouseY;
344
- const rotationSpeed = this.option.particles.rotationSpeed * step;
345
- const friction = this.option.gravity.friction;
346
- const mouseConnectDist = this.option.mouse.connectDist;
347
- const mouseDistRatio = this.option.mouse.distRatio;
348
344
  const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
349
345
  const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
346
+ const mouseConnectDist = this.option.mouse.connectDist;
347
+ const mouseDistRatio = this.option.mouse.distRatio;
348
+ const rotationSpeed = this.option.particles.rotationSpeed * step;
349
+ const friction = this.option.gravity.friction;
350
+ const preventExplosions = this.option.gravity.preventExplosions;
350
351
  const easing = 1 - Math.pow(3 / 4, step);
351
352
  for (const p of this.particles) {
352
353
  p.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
@@ -354,6 +355,18 @@ class CanvasParticles {
354
355
  // Constant velocity
355
356
  const movX = Math.sin(p.dir) * p.speed;
356
357
  const movY = Math.cos(p.dir) * p.speed;
358
+ // Maximum velocity
359
+ if (preventExplosions) {
360
+ const maxVel = Math.max(p.speed, friction) * 2;
361
+ if (p.velX > maxVel)
362
+ p.velX = maxVel;
363
+ if (p.velX < -maxVel)
364
+ p.velX = -maxVel;
365
+ if (p.velY > maxVel)
366
+ p.velY = maxVel;
367
+ if (p.velY < -maxVel)
368
+ p.velY = -maxVel;
369
+ }
357
370
  // Apply velocities
358
371
  p.posX += (movX + p.velX) * step;
359
372
  p.posY += (movY + p.velY) * step;
@@ -472,7 +485,7 @@ class CanvasParticles {
472
485
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
473
486
  const alpha = this.color.alpha;
474
487
  const alphaFactor = this.color.alpha * maxDist;
475
- const bucket = []; // Batch line segments of max alpha
488
+ const bucket = []; // Batch line segments of max alpha (2D -> 1D; stride = 4)
476
489
  const grid = this.#buildSpatialGrid(stride, invCellSize); // O(n^2) -> O(n)
477
490
  let particleWork = 0;
478
491
  let allowWork = true;
@@ -492,7 +505,7 @@ class CanvasParticles {
492
505
  }
493
506
  else {
494
507
  // Cache lines with max alpha to later be drawn in one batch
495
- bucket.push([ax, ay, bx, by]);
508
+ bucket.push(ax, ay, bx, by);
496
509
  }
497
510
  particleWork += distSq;
498
511
  allowWork = particleWork < maxWorkPerParticle;
@@ -559,8 +572,7 @@ class CanvasParticles {
559
572
  if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
560
573
  renderConnectionsToOwnCell(cell || [], a, pa);
561
574
  // Next iteration
562
- a++;
563
- if (!(a < len))
575
+ if (++a >= len)
564
576
  break;
565
577
  // Same code inline but the order of grid.get() is different to remove maxWork artifacts
566
578
  particleWork = 0;
@@ -588,8 +600,7 @@ class CanvasParticles {
588
600
  if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
589
601
  renderConnectionsToOwnCell(cell || [], a, pa);
590
602
  // Next iteration
591
- a++;
592
- if (!(a < len))
603
+ if (++a >= len)
593
604
  break;
594
605
  // Same code inline but the order of grid.get() is different to remove maxWork artifacts
595
606
  particleWork = 0;
@@ -622,10 +633,9 @@ class CanvasParticles {
622
633
  // Render all bucketed lines at once
623
634
  ctx.globalAlpha = alpha;
624
635
  ctx.beginPath();
625
- for (let i = 0; i < bucket.length; i++) {
626
- const line = bucket[i];
627
- ctx.moveTo(line[0], line[1]);
628
- ctx.lineTo(line[2], line[3]);
636
+ for (let line = 0; line < bucket.length; line += 4) {
637
+ ctx.moveTo(bucket[line], bucket[line + 1]);
638
+ ctx.lineTo(bucket[line + 2], bucket[line + 3]);
629
639
  }
630
640
  ctx.stroke();
631
641
  }
@@ -762,6 +772,7 @@ class CanvasParticles {
762
772
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
763
773
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
764
774
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
775
+ preventExplosions: !!options.gravity?.preventExplosions,
765
776
  },
766
777
  debug: {
767
778
  drawGrid: !!options.debug?.drawGrid,
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.4.2";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({MANUAL:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(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)=>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=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2;const s=this.option.particles.generationType;s!==e.generationType.MANUAL&&(s===e.generationType.NEW||0===this.particles.length?this.newParticles():s===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#i()}resizeCanvas(){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.particles.rotationSpeed*i,h=this.option.gravity.friction,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.mouse.interactionType===e.interactionType.NONE,f=this.option.mouse.interactionType===e.interactionType.MOVE,g=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*l*i,e.dir%=t;const m=Math.sin(e.dir)*e.speed,v=Math.cos(e.dir)*e.speed;e.posX+=(m+e.velX)*i,e.posY+=(v+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(h,i),e.velY*=Math.pow(h,i);const x=e.posX+o-r,y=e.posY+a-c;if(!u){const t=p/Math.hypot(x,y);d<t?(e.offX+=(t*x-x-e.offX)*g,e.offY+=(t*y-y-e.offY)*g):(e.offX-=e.offX*g,e.offY-=e.offY*g)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,f&&(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++,!(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++,!(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++){const i=u[t];s.moveTo(i[0],i[1]),s.lineTo(i[2],i[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})},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";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.4.4";static MAX_DT=20;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)=>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=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2;const s=this.option.particles.generationType;s!==e.generationType.OFF&&(s===e.generationType.NEW||0===this.particles.length?this.newParticles():s===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#i()}resizeCanvas(){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,v=-h*f;i.velX+=g,i.velY+=v,t.velX-=g,t.velY-=v}}}#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.preventExplosions,v=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*u*i,e.dir%=t;const m=Math.sin(e.dir)*e.speed,x=Math.cos(e.dir)*e.speed;if(g){const t=2*Math.max(e.speed,f);e.velX>t&&(e.velX=t),e.velX<-t&&(e.velX=-t),e.velY>t&&(e.velY=t),e.velY<-t&&(e.velY=-t)}e.posX+=(m+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)*v,e.offY+=(t*M-M-e.offY)*v):(e.offX-=e.offX*v,e.offY-=e.offY*v)}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,v=!0;function m(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,v=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))&&(m(n.x,n.y,i.x,i.y),!v))break}}function y(i,s){for(const n of i){const i=t[n];if((l||e.#l(s,i))&&(m(s.x,s.y,i.x,i.y),!v))break}}for(let e=0;e<i;e++){g=0,v=!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),v&&((s=f.get(l+c))&&y(s,n),v&&((s=f.get(l+c+1))&&y(s,n),v&&((s=f.get(l+c-1))&&y(s,n),v)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;if(g=0,v=!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),v&&((s=f.get(l+c-1))&&y(s,n),v&&((s=f.get(l+1))&&y(s,n),v&&((s=f.get(l+c))&&y(s,n),v)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;g=0,v=!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),v&&((s=f.get(l+1))&&y(s,n),v&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),v&&((s=f.get(l+c-1))&&y(s,n),v&&(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}),preventExplosions:!!t.gravity?.preventExplosions},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});
@@ -44,6 +44,5 @@ export interface ContextColor {
44
44
  hex: string;
45
45
  alpha: number;
46
46
  }
47
- export type LineSegment = [ax: number, ay: number, bx: number, by: number];
48
47
  export type SpatialGrid = Map</* key: */ number, /* indexesOfParticles: */ number[]>;
49
48
  export {};
@@ -27,6 +27,7 @@ export interface CanvasParticlesOptions {
27
27
  repulsive: number;
28
28
  pulling: number;
29
29
  friction: number;
30
+ preventExplosions: boolean;
30
31
  };
31
32
  debug: {
32
33
  drawGrid: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.4.2",
3
+ "version": "4.4.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
@@ -1,7 +1,7 @@
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 type { CanvasParticlesCanvas, Particle, GridPos, ContextColor, LineSegment, SpatialGrid } from './types'
4
+ import type { CanvasParticlesCanvas, Particle, GridPos, ContextColor, SpatialGrid } from './types'
5
5
  import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
6
6
 
7
7
  const TWO_PI = 2 * Math.PI
@@ -43,9 +43,9 @@ export default class CanvasParticles {
43
43
 
44
44
  /** Defines how the particles are auto-generated */
45
45
  static readonly generationType = Object.freeze({
46
- MANUAL: 0, // Never auto-generate particles
47
- NEW: 1, // Generate particles from scratch
48
- MATCH: 2, // Add or remove particles to match new count (default)
46
+ OFF: 0, // Never auto-generate particles
47
+ NEW: 1, // Generate all particles from scratch
48
+ MATCH: 2, // Add or remove some particles to match the new count (default)
49
49
  })
50
50
 
51
51
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
@@ -211,7 +211,7 @@ export default class CanvasParticles {
211
211
 
212
212
  const generationType = this.option.particles.generationType
213
213
 
214
- if (generationType !== CanvasParticles.generationType.MANUAL) {
214
+ if (generationType !== CanvasParticles.generationType.OFF) {
215
215
  if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0) this.newParticles()
216
216
  else if (generationType === CanvasParticles.generationType.MATCH) this.matchParticleCount({ updateBounds: true })
217
217
  }
@@ -414,12 +414,13 @@ export default class CanvasParticles {
414
414
  const offY = this.offY
415
415
  const mouseX = this.mouseX
416
416
  const mouseY = this.mouseY
417
- const rotationSpeed = this.option.particles.rotationSpeed * step
418
- const friction = this.option.gravity.friction
419
- const mouseConnectDist = this.option.mouse.connectDist
420
- const mouseDistRatio = this.option.mouse.distRatio
421
417
  const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE
422
418
  const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE
419
+ const mouseConnectDist = this.option.mouse.connectDist
420
+ const mouseDistRatio = this.option.mouse.distRatio
421
+ const rotationSpeed = this.option.particles.rotationSpeed * step
422
+ const friction = this.option.gravity.friction
423
+ const preventExplosions = this.option.gravity.preventExplosions
423
424
  const easing = 1 - Math.pow(3 / 4, step)
424
425
 
425
426
  for (const p of this.particles) {
@@ -430,6 +431,17 @@ export default class CanvasParticles {
430
431
  const movX = Math.sin(p.dir) * p.speed
431
432
  const movY = Math.cos(p.dir) * p.speed
432
433
 
434
+ // Maximum velocity
435
+ if (preventExplosions) {
436
+ const maxVel = Math.max(p.speed, friction) * 2
437
+
438
+ if (p.velX > maxVel) p.velX = maxVel
439
+ if (p.velX < -maxVel) p.velX = -maxVel
440
+
441
+ if (p.velY > maxVel) p.velY = maxVel
442
+ if (p.velY < -maxVel) p.velY = -maxVel
443
+ }
444
+
433
445
  // Apply velocities
434
446
  p.posX += (movX + p.velX) * step
435
447
  p.posY += (movY + p.velY) * step
@@ -564,7 +576,7 @@ export default class CanvasParticles {
564
576
  const alpha = this.color.alpha
565
577
  const alphaFactor = this.color.alpha * maxDist
566
578
 
567
- const bucket: LineSegment[] = [] // Batch line segments of max alpha
579
+ const bucket: number[] = [] // Batch line segments of max alpha (2D -> 1D; stride = 4)
568
580
  const grid = this.#buildSpatialGrid(stride, invCellSize) // O(n^2) -> O(n)
569
581
 
570
582
  let particleWork = 0
@@ -587,7 +599,7 @@ export default class CanvasParticles {
587
599
  ctx.stroke()
588
600
  } else {
589
601
  // Cache lines with max alpha to later be drawn in one batch
590
- bucket.push([ax, ay, bx, by])
602
+ bucket.push(ax, ay, bx, by)
591
603
  }
592
604
  particleWork += distSq
593
605
  allowWork = particleWork < maxWorkPerParticle
@@ -654,8 +666,7 @@ export default class CanvasParticles {
654
666
  renderConnectionsToOwnCell(cell || [], a, pa)
655
667
 
656
668
  // Next iteration
657
- a++
658
- if (!(a < len)) break
669
+ if (++a >= len) break
659
670
 
660
671
  // Same code inline but the order of grid.get() is different to remove maxWork artifacts
661
672
  particleWork = 0
@@ -678,8 +689,7 @@ export default class CanvasParticles {
678
689
  renderConnectionsToOwnCell(cell || [], a, pa)
679
690
 
680
691
  // Next iteration
681
- a++
682
- if (!(a < len)) break
692
+ if (++a >= len) break
683
693
 
684
694
  // Same code inline but the order of grid.get() is different to remove maxWork artifacts
685
695
  particleWork = 0
@@ -708,10 +718,9 @@ export default class CanvasParticles {
708
718
  ctx.globalAlpha = alpha
709
719
  ctx.beginPath()
710
720
 
711
- for (let i = 0; i < bucket.length; i++) {
712
- const line = bucket[i]
713
- ctx.moveTo(line[0], line[1])
714
- ctx.lineTo(line[2], line[3])
721
+ for (let line = 0; line < bucket.length; line += 4) {
722
+ ctx.moveTo(bucket[line], bucket[line + 1])
723
+ ctx.lineTo(bucket[line + 2], bucket[line + 3])
715
724
  }
716
725
  ctx.stroke()
717
726
  }
@@ -881,6 +890,7 @@ export default class CanvasParticles {
881
890
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
882
891
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
883
892
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
893
+ preventExplosions: !!options.gravity?.preventExplosions,
884
894
  },
885
895
  debug: {
886
896
  drawGrid: !!options.debug?.drawGrid,
@@ -52,6 +52,4 @@ export interface ContextColor {
52
52
  alpha: number
53
53
  }
54
54
 
55
- export type LineSegment = [ax: number, ay: number, bx: number, by: number]
56
-
57
55
  export type SpatialGrid = Map</* key: */ number, /* indexesOfParticles: */ number[]>
@@ -31,6 +31,7 @@ export interface CanvasParticlesOptions {
31
31
  repulsive: number
32
32
  pulling: number
33
33
  friction: number
34
+ preventExplosions: boolean
34
35
  }
35
36
 
36
37
  debug: {