canvasparticles-js 4.4.3 → 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.3";
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;
@@ -761,6 +774,7 @@ class CanvasParticles {
761
774
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
762
775
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
763
776
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
777
+ preventExplosions: !!options.gravity?.preventExplosions,
764
778
  },
765
779
  debug: {
766
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.3";
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;
@@ -759,6 +772,7 @@ class CanvasParticles {
759
772
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
760
773
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
761
774
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
775
+ preventExplosions: !!options.gravity?.preventExplosions,
762
776
  },
763
777
  debug: {
764
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.3";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({MANUAL:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(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>=i)break;if(g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&(s=f.get(l+c+1))&&y(s,n))))}}}if(u.length){s.globalAlpha=p,s.beginPath();for(let t=0;t<u.length;t+=4)s.moveTo(u[t],u[t+1]),s.lineTo(u[t+2],u[t+3]);s.stroke()}}#p(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#d(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#u(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#o(i),this.#a(i),this.lastAnimationFrame=t}#i(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.option.particles.drawLines&&this.#h(),this.option.debug.drawGrid&&this.#p(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#d()}#f(){this.isAnimating&&(requestAnimationFrame(()=>this.#f()),this.#u(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#f())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:~~i("mouse.interactionType",t.mouse?.interactionType,e.interactionType.MOVE,{min:0,max:2}),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3,{min:0}),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3,{min:0})},particles:{generationType:~~i("particles.generationType",t.particles?.generationType,e.generationType.MATCH,{min:0,max:2}),drawLines:!!(t.particles?.drawLines??1),color:t.particles?.color??"black",ppm:~~i("particles.ppm",t.particles?.ppm,100),max:Math.round(i("particles.max",t.particles?.max,1/0,{min:0})),maxWork:Math.round(i("particles.maxWork",t.particles?.maxWork,1/0,{min:0})),connectDist:~~i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:0}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0,{min:0}),pulling:i("gravity.pulling",t.gravity?.pulling,0,{min:0}),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})},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});
@@ -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.3",
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
@@ -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
@@ -878,6 +890,7 @@ export default class CanvasParticles {
878
890
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
879
891
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
880
892
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
893
+ preventExplosions: !!options.gravity?.preventExplosions,
881
894
  },
882
895
  debug: {
883
896
  drawGrid: !!options.debug?.drawGrid,
@@ -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: {