canvasparticles-js 4.4.3 → 4.4.5

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,19 +175,24 @@ 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.
179
+
180
+ **TypeScript Usage**:
181
+ Both `float` and `integer` mean `number`.<br>
182
+ `integer` is used to specify that the number is internally floored to a whole number.
180
183
 
181
184
  ### Root options
182
185
 
183
- | Option | Type | Default | Description |
184
- | ------------ | ----------------- | ------- | ----------------------------------------------------------------------------- |
185
- | `background` | `string \| false` | `false` | Canvas background. Any valid CSS `background` value. Ignored if not a string. |
186
+ | Option | Type | Default | Description |
187
+ | ------------ | ----------------- | ------- | ----------------------------------------------------------------------------------- |
188
+ | `background` | `string \| false` | `false` | Canvas background. Any valid CSS `background` value. Ignored when strictly `false`. |
186
189
 
187
190
  ---
188
191
 
189
192
  ### `animation`
190
193
 
194
+ It's best to not touch these values if it's unclear what it does.
195
+
191
196
  | Option | Type | Default | Description |
192
197
  | ------------------------ | --------- | ------- | ---------------------------------------------------- |
193
198
  | `animation.startOnEnter` | `boolean` | `true` | Start animation when the canvas enters the viewport. |
@@ -207,24 +212,30 @@ Your screen resolution and refresh rate will directly impact perfomance!
207
212
 
208
213
  - `NONE (0)` – No interaction
209
214
  - `SHIFT (1)` – Visual displacement only
210
- - `MOVE (2)` – Actual particle movement
215
+ - `MOVE (2)` – Actual particle movement (default)
211
216
 
212
217
  ---
213
218
 
214
219
  ### `particles`
215
220
 
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. |
221
+ | Option | Type | Default | Description |
222
+ | --------------------------- | ------------- | ---------- | ------------------------------------------------------------------------------------------------- |
223
+ | `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `1 = OFF`, `2 = MATCH` |
224
+ | `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
225
+ | `particles.ppm` | `integer` | `100` | Particles per million pixels. |
226
+ | `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
227
+ | `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
228
+ | `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). |
229
+ | `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
230
+ | `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
231
+ | `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
232
+ | `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
233
+
234
+ **Generation types** (enum)
235
+
236
+ - `OFF (0)` – Never auto-generate particles
237
+ - `NEW (1)` – Generate all particles from scratch
238
+ - `MATCH (2)` – Add or remove some particles to match the new count (default)
228
239
 
229
240
  ---
230
241
 
@@ -232,11 +243,12 @@ Your screen resolution and refresh rate will directly impact perfomance!
232
243
 
233
244
  Enabling gravity (`repulsive` or `pulling` > 0) performs an extra **O(n²)** gravity computations per frame.
234
245
 
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`). |
246
+ | Option | Type | Default | Description |
247
+ | --------------------- | ------- | ---------- | ---------------------------------------------------------------------------------------------------- |
248
+ | `gravity.repulsive` | `float` | `0` | Repulsive force between particles. |
249
+ | `gravity.pulling` | `float` | `0` | Attractive force between particles. Requires sufficient repulsion to avoid clustering. |
250
+ | `gravity.friction` | `float` | `0.8` | Damping factor applied to gravitational velocity each update (`0.0 – 1.0`). |
251
+ | `gravity.maxVelocity` | `float` | `Infinity` | Clamp the velocity of each particle to this value to prevent explosions under heavy pulling gravity. |
240
252
 
241
253
  ---
242
254
 
@@ -342,13 +354,13 @@ instance.options = { ... }
342
354
  createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number)
343
355
  ```
344
356
 
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)`.
357
+ 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
358
 
347
359
  ```js
348
360
  const canvas = '#my-canvas'
349
361
  const options = {
350
362
  particles: {
351
- max: 0,
363
+ generationType: CanvasParticles.generationType.OFF, // = 0
352
364
  rotationSpeed: 0,
353
365
  },
354
366
  }
@@ -393,7 +405,7 @@ instance.newParticles({ keepAuto: true, keepManual: false })
393
405
  const options = {
394
406
  background: 'hsl(125, 42%, 35%)',
395
407
  mouse: {
396
- interactionType: CanvasParticles.interactionType.MOVE, // = 2
408
+ interactionType: CanvasParticles.interactionType.SHIFT, // = 1
397
409
  },
398
410
  particles: {
399
411
  color: 'rgba(150, 255, 105, 0.95)',
package/dist/index.cjs CHANGED
@@ -21,8 +21,8 @@ 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";
25
- static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
24
+ static version = "4.4.5";
25
+ static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
26
26
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
27
27
  /** Defines mouse interaction types with the particles */
28
28
  static interactionType = Object.freeze({
@@ -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 maxVel = this.option.gravity.maxVelocity;
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,17 @@ 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 (maxVel > 0) {
362
+ if (p.velX > maxVel)
363
+ p.velX = maxVel;
364
+ if (p.velX < -maxVel)
365
+ p.velX = -maxVel;
366
+ if (p.velY > maxVel)
367
+ p.velY = maxVel;
368
+ if (p.velY < -maxVel)
369
+ p.velY = -maxVel;
370
+ }
359
371
  // Apply velocities
360
372
  p.posX += (movX + p.velX) * step;
361
373
  p.posY += (movY + p.velY) * step;
@@ -761,6 +773,7 @@ class CanvasParticles {
761
773
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
762
774
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
763
775
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
776
+ maxVelocity: pno('gravity.maxVelocity', options.gravity?.maxVelocity, Infinity, { min: 0 }),
764
777
  },
765
778
  debug: {
766
779
  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,8 +19,8 @@ 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";
23
- static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
22
+ static version = "4.4.5";
23
+ static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
24
24
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
25
25
  /** Defines mouse interaction types with the particles */
26
26
  static interactionType = Object.freeze({
@@ -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 maxVel = this.option.gravity.maxVelocity;
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,17 @@ 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 (maxVel > 0) {
360
+ if (p.velX > maxVel)
361
+ p.velX = maxVel;
362
+ if (p.velX < -maxVel)
363
+ p.velX = -maxVel;
364
+ if (p.velY > maxVel)
365
+ p.velY = maxVel;
366
+ if (p.velY < -maxVel)
367
+ p.velY = -maxVel;
368
+ }
357
369
  // Apply velocities
358
370
  p.posX += (movX + p.velX) * step;
359
371
  p.posY += (movY + p.velY) * step;
@@ -759,6 +771,7 @@ class CanvasParticles {
759
771
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
760
772
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
761
773
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
774
+ maxVelocity: pno('gravity.maxVelocity', options.gravity?.maxVelocity, Infinity, { min: 0 }),
762
775
  },
763
776
  debug: {
764
777
  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.5";static MAX_DT=1e3/30;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({OFF:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,e=t.instance;if(!e.options?.animation)return;(t.inViewbox=i.isIntersecting)?e.option.animation?.startOnEnter&&e.start({auto:!0}):e.option.animation?.stopOnLeave&&e.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}for(const i of t){i.target.instance.#t()}});static defaultIfNaN=(t,i)=>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,m=-h*f;i.velX+=g,i.velY+=m,t.velX-=g,t.velY-=m}}}#a(i){const s=this.width,n=this.height,o=this.offX,a=this.offY,r=this.mouseX,c=this.mouseY,l=this.option.mouse.interactionType===e.interactionType.NONE,h=this.option.mouse.interactionType===e.interactionType.MOVE,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.particles.rotationSpeed*i,f=this.option.gravity.friction,g=this.option.gravity.maxVelocity,m=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*u*i,e.dir%=t;const v=Math.sin(e.dir)*e.speed,y=Math.cos(e.dir)*e.speed;g>0&&(e.velX>g&&(e.velX=g),e.velX<-g&&(e.velX=-g),e.velY>g&&(e.velY=g),e.velY<-g&&(e.velY=-g)),e.posX+=(v+e.velX)*i,e.posY+=(y+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 x=e.posX+o-r,M=e.posY+a-c;if(!l){const t=p/Math.hypot(x,M);d<t?(e.offX+=(t*x-x-e.offX)*m,e.offY+=(t*M-M-e.offY)*m):(e.offX-=e.offX*m,e.offY-=e.offY*m)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,h&&(e.posX=e.x,e.posY=e.y),e.x+=o,e.y+=a,e.gridPos.x=+(e.x>=e.bounds.left)+ +(e.x>e.bounds.right),e.gridPos.y=+(e.y>=e.bounds.top)+ +(e.y>e.bounds.bottom),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#r(){const i=this.ctx;for(const e of this.particles)e.isVisible&&(e.size>1?(i.beginPath(),i.arc(e.x,e.y,e.size,0,t),i.fill(),i.closePath()):i.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}#c(t,i){const e=this.particles,s=e.length,n=new Map;for(let o=0;o<s;o++){const s=e[o],a=(s.x*i|0)+Math.imul(s.y*i,t),r=n.get(a);r?r.push(o):n.set(a,[o])}return n}static#l(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#h(){const t=this.particles,i=t.length,s=this.ctx,n=this.option.particles.connectDist,o=n**2,a=(n/2)**2,r=1/n,c=Math.ceil(this.width*r),l=n>=Math.min(this.canvas.width,this.canvas.height),h=o*this.option.particles.maxWork,p=this.color.alpha,d=this.color.alpha*n,u=[],f=this.#c(c,r);let g=0,m=!0;function v(t,i,e,n){const r=t-e,c=i-n,l=r*r+c*c;l>o||(l>a?(s.globalAlpha=d/Math.sqrt(l)-p,s.beginPath(),s.moveTo(t,i),s.lineTo(e,n),s.stroke()):u.push(t,i,e,n),g+=l,m=g<h)}function y(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 x(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))&&x(s,n),m&&((s=f.get(l+c))&&x(s,n),m&&((s=f.get(l+c+1))&&x(s,n),m&&((s=f.get(l+c-1))&&x(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&y(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))&&x(s,n),m&&((s=f.get(l+c-1))&&x(s,n),m&&((s=f.get(l+1))&&x(s,n),m&&((s=f.get(l+c))&&x(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&y(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))&&x(s,n),m&&((s=f.get(l+1))&&x(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&y(s||[],e,n),m&&((s=f.get(l+c-1))&&x(s,n),m&&(s=f.get(l+c+1))&&x(s,n))))}}}if(u.length){s.globalAlpha=p,s.beginPath();for(let t=0;t<u.length;t+=4)s.moveTo(u[t],u[t+1]),s.lineTo(u[t+2],u[t+3]);s.stroke()}}#p(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#d(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#u(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#o(i),this.#a(i),this.lastAnimationFrame=t}#i(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.option.particles.drawLines&&this.#h(),this.option.debug.drawGrid&&this.#p(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#d()}#f(){this.isAnimating&&(requestAnimationFrame(()=>this.#f()),this.#u(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#f())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:~~i("mouse.interactionType",t.mouse?.interactionType,e.interactionType.MOVE,{min:0,max:2}),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3,{min:0}),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3,{min:0})},particles:{generationType:~~i("particles.generationType",t.particles?.generationType,e.generationType.MATCH,{min:0,max:2}),drawLines:!!(t.particles?.drawLines??1),color:t.particles?.color??"black",ppm:~~i("particles.ppm",t.particles?.ppm,100),max:Math.round(i("particles.max",t.particles?.max,1/0,{min:0})),maxWork:Math.round(i("particles.maxWork",t.particles?.maxWork,1/0,{min:0})),connectDist:~~i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:0}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0,{min:0}),pulling:i("gravity.pulling",t.gravity?.pulling,0,{min:0}),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1}),maxVelocity:i("gravity.maxVelocity",t.gravity?.maxVelocity,1/0,{min:0})},debug:{drawGrid:!!t.debug?.drawGrid,drawIndexes:!!t.debug?.drawIndexes}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){const i=e.parseNumericOption("mouse.connectDistMult",t,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*i}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
@@ -27,6 +27,7 @@ export interface CanvasParticlesOptions {
27
27
  repulsive: number;
28
28
  pulling: number;
29
29
  friction: number;
30
+ maxVelocity: number;
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.5",
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
@@ -31,7 +31,7 @@ declare const __VERSION__: string
31
31
  export default class CanvasParticles {
32
32
  static readonly version = __VERSION__
33
33
 
34
- private static readonly MAX_DT = 1000 / 50 // milliseconds between updates @ 50 FPS
34
+ private static readonly MAX_DT = 1000 / 30 // milliseconds between updates @ 30 FPS
35
35
  private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
36
36
 
37
37
  /** Defines mouse interaction types with the particles */
@@ -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 maxVel = this.option.gravity.maxVelocity
423
424
  const easing = 1 - Math.pow(3 / 4, step)
424
425
 
425
426
  for (const p of this.particles) {
@@ -430,6 +431,15 @@ 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 (maxVel > 0) {
436
+ if (p.velX > maxVel) p.velX = maxVel
437
+ if (p.velX < -maxVel) p.velX = -maxVel
438
+
439
+ if (p.velY > maxVel) p.velY = maxVel
440
+ if (p.velY < -maxVel) p.velY = -maxVel
441
+ }
442
+
433
443
  // Apply velocities
434
444
  p.posX += (movX + p.velX) * step
435
445
  p.posY += (movY + p.velY) * step
@@ -878,6 +888,7 @@ export default class CanvasParticles {
878
888
  repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
879
889
  pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
880
890
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
891
+ maxVelocity: pno('gravity.maxVelocity', options.gravity?.maxVelocity, Infinity, { min: 0 }),
881
892
  },
882
893
  debug: {
883
894
  drawGrid: !!options.debug?.drawGrid,
@@ -31,6 +31,7 @@ export interface CanvasParticlesOptions {
31
31
  repulsive: number
32
32
  pulling: number
33
33
  friction: number
34
+ maxVelocity: number
34
35
  }
35
36
 
36
37
  debug: {