canvasparticles-js 4.1.5 → 4.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,7 +21,9 @@ 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.1.5";
24
+ static version = "4.1.6";
25
+ static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
26
+ static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
25
27
  /** Defines mouse interaction types with the particles */
26
28
  static interactionType = Object.freeze({
27
29
  NONE: 0, // No mouse interaction
@@ -57,10 +59,8 @@ class CanvasParticles {
57
59
  canvas.instance.resizeCanvas();
58
60
  }
59
61
  });
60
- static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
61
- static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
62
62
  /** Helper functions for options parsing */
63
- static defaultIfNaN = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value);
63
+ static defaultIfNaN = (value, defaultValue) => isNaN(+value) ? defaultValue : +value;
64
64
  static parseNumericOption = (name, value, defaultValue, clamp) => {
65
65
  if (value == undefined)
66
66
  return defaultValue;
@@ -68,7 +68,7 @@ class CanvasParticles {
68
68
  if (isFinite(min) && value < min) {
69
69
  console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
70
70
  }
71
- if (isFinite(max) && value > max) {
71
+ else if (isFinite(max) && value > max) {
72
72
  console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
73
73
  }
74
74
  return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
@@ -237,11 +237,11 @@ class CanvasParticles {
237
237
  return;
238
238
  const len = this.particleCount;
239
239
  const particles = this.particles;
240
- const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive;
241
- const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling;
240
+ const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step;
241
+ const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step;
242
242
  const maxRepulsiveDist = this.option.particles.connectDist / 2;
243
243
  const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
244
- const maxGrav = this.option.particles.connectDist * 0.1;
244
+ const maxGrav = this.option.particles.connectDist * 0.1 * step;
245
245
  for (let i = 0; i < len; i++) {
246
246
  const particleA = particles[i];
247
247
  for (let j = i + 1; j < len; j++) {
@@ -256,7 +256,7 @@ class CanvasParticles {
256
256
  if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
257
257
  continue;
258
258
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
259
- grav = (1 / Math.sqrt(distSq)) ** 1.8;
259
+ grav = Math.pow(1 / Math.sqrt(distSq), 1.8);
260
260
  const angleX = Math.cos(angle);
261
261
  const angleY = Math.sin(angle);
262
262
  if (distSq < maxRepulsiveDistSq) {
@@ -290,22 +290,23 @@ class CanvasParticles {
290
290
  const offY = this.offY;
291
291
  const mouseX = this.mouseX;
292
292
  const mouseY = this.mouseY;
293
- const rotationSpeed = this.option.particles.rotationSpeed;
293
+ const rotationSpeed = this.option.particles.rotationSpeed * step;
294
294
  const friction = this.option.gravity.friction;
295
295
  const mouseConnectDist = this.option.mouse.connectDist;
296
296
  const mouseDistRatio = this.option.mouse.distRatio;
297
297
  const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
298
298
  const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
299
+ const easing = 1 - Math.pow(1 - 1 / 4, step);
299
300
  for (let i = 0; i < len; i++) {
300
301
  const particle = particles[i];
301
- particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed;
302
+ particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
302
303
  particle.dir %= TWO_PI;
303
304
  // Constant velocity
304
305
  const movX = Math.sin(particle.dir) * particle.speed;
305
306
  const movY = Math.cos(particle.dir) * particle.speed;
306
307
  // Apply velocities
307
- particle.posX += movX + particle.velX;
308
- particle.posY += movY + particle.velY;
308
+ particle.posX += (movX + particle.velX) * step;
309
+ particle.posY += (movY + particle.velY) * step;
309
310
  // Wrap particles around the canvas
310
311
  particle.posX %= width;
311
312
  if (particle.posX < 0)
@@ -314,8 +315,8 @@ class CanvasParticles {
314
315
  if (particle.posY < 0)
315
316
  particle.posY += height;
316
317
  // Slightly decrease dynamic velocity
317
- particle.velX *= friction;
318
- particle.velY *= friction;
318
+ particle.velX *= Math.pow(friction, step);
319
+ particle.velY *= Math.pow(friction, step);
319
320
  // Distance from mouse
320
321
  const distX = particle.posX + offX - mouseX;
321
322
  const distY = particle.posY + offY - mouseY;
@@ -323,12 +324,12 @@ class CanvasParticles {
323
324
  if (!isMouseInteractionTypeNone) {
324
325
  const distRatio = mouseConnectDist / Math.hypot(distX, distY);
325
326
  if (mouseDistRatio < distRatio) {
326
- particle.offX += (distRatio * distX - distX - particle.offX) / 4;
327
- particle.offY += (distRatio * distY - distY - particle.offY) / 4;
327
+ particle.offX += (distRatio * distX - distX - particle.offX) * easing;
328
+ particle.offY += (distRatio * distY - distY - particle.offY) * easing;
328
329
  }
329
330
  else {
330
- particle.offX -= particle.offX / 4;
331
- particle.offY -= particle.offY / 4;
331
+ particle.offX -= particle.offX * easing;
332
+ particle.offY -= particle.offY * easing;
332
333
  }
333
334
  }
334
335
  // Visually displace the particles
@@ -478,7 +479,7 @@ class CanvasParticles {
478
479
  // - step = 1 → exactly one baseline update (dt === BASE_DT)
479
480
  // - step > 1 → more time passed (lower FPS), advance further
480
481
  // - step < 1 → less time passed (higher FPS), advance less
481
- const step = CanvasParticles.BASE_DT / dt;
482
+ const step = dt / CanvasParticles.BASE_DT;
482
483
  this.#updateGravity(step);
483
484
  this.#updateParticles(step);
484
485
  this.#render();
package/dist/index.d.ts CHANGED
@@ -3,8 +3,10 @@ import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './type
3
3
  export default class CanvasParticles {
4
4
  #private;
5
5
  static readonly version: string;
6
+ private static readonly MAX_DT;
7
+ private static readonly BASE_DT;
6
8
  /** Defines mouse interaction types with the particles */
7
- static interactionType: Readonly<{
9
+ static readonly interactionType: Readonly<{
8
10
  NONE: 0;
9
11
  SHIFT: 1;
10
12
  MOVE: 2;
@@ -12,11 +14,9 @@ export default class CanvasParticles {
12
14
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
13
15
  static readonly canvasIntersectionObserver: IntersectionObserver;
14
16
  static readonly canvasResizeObserver: ResizeObserver;
15
- private static readonly MAX_DT;
16
- private static readonly BASE_DT;
17
17
  /** Helper functions for options parsing */
18
- private static defaultIfNaN;
19
- private static parseNumericOption;
18
+ private static readonly defaultIfNaN;
19
+ private static readonly parseNumericOption;
20
20
  canvas: CanvasParticlesCanvas;
21
21
  private ctx;
22
22
  enableAnimating: boolean;
package/dist/index.mjs CHANGED
@@ -19,7 +19,9 @@ 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.1.5";
22
+ static version = "4.1.6";
23
+ static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
24
+ static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
23
25
  /** Defines mouse interaction types with the particles */
24
26
  static interactionType = Object.freeze({
25
27
  NONE: 0, // No mouse interaction
@@ -55,10 +57,8 @@ class CanvasParticles {
55
57
  canvas.instance.resizeCanvas();
56
58
  }
57
59
  });
58
- static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
59
- static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
60
60
  /** Helper functions for options parsing */
61
- static defaultIfNaN = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value);
61
+ static defaultIfNaN = (value, defaultValue) => isNaN(+value) ? defaultValue : +value;
62
62
  static parseNumericOption = (name, value, defaultValue, clamp) => {
63
63
  if (value == undefined)
64
64
  return defaultValue;
@@ -66,7 +66,7 @@ class CanvasParticles {
66
66
  if (isFinite(min) && value < min) {
67
67
  console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
68
68
  }
69
- if (isFinite(max) && value > max) {
69
+ else if (isFinite(max) && value > max) {
70
70
  console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
71
71
  }
72
72
  return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
@@ -235,11 +235,11 @@ class CanvasParticles {
235
235
  return;
236
236
  const len = this.particleCount;
237
237
  const particles = this.particles;
238
- const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive;
239
- const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling;
238
+ const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step;
239
+ const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step;
240
240
  const maxRepulsiveDist = this.option.particles.connectDist / 2;
241
241
  const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
242
- const maxGrav = this.option.particles.connectDist * 0.1;
242
+ const maxGrav = this.option.particles.connectDist * 0.1 * step;
243
243
  for (let i = 0; i < len; i++) {
244
244
  const particleA = particles[i];
245
245
  for (let j = i + 1; j < len; j++) {
@@ -254,7 +254,7 @@ class CanvasParticles {
254
254
  if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
255
255
  continue;
256
256
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
257
- grav = (1 / Math.sqrt(distSq)) ** 1.8;
257
+ grav = Math.pow(1 / Math.sqrt(distSq), 1.8);
258
258
  const angleX = Math.cos(angle);
259
259
  const angleY = Math.sin(angle);
260
260
  if (distSq < maxRepulsiveDistSq) {
@@ -288,22 +288,23 @@ class CanvasParticles {
288
288
  const offY = this.offY;
289
289
  const mouseX = this.mouseX;
290
290
  const mouseY = this.mouseY;
291
- const rotationSpeed = this.option.particles.rotationSpeed;
291
+ const rotationSpeed = this.option.particles.rotationSpeed * step;
292
292
  const friction = this.option.gravity.friction;
293
293
  const mouseConnectDist = this.option.mouse.connectDist;
294
294
  const mouseDistRatio = this.option.mouse.distRatio;
295
295
  const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
296
296
  const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
297
+ const easing = 1 - Math.pow(1 - 1 / 4, step);
297
298
  for (let i = 0; i < len; i++) {
298
299
  const particle = particles[i];
299
- particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed;
300
+ particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
300
301
  particle.dir %= TWO_PI;
301
302
  // Constant velocity
302
303
  const movX = Math.sin(particle.dir) * particle.speed;
303
304
  const movY = Math.cos(particle.dir) * particle.speed;
304
305
  // Apply velocities
305
- particle.posX += movX + particle.velX;
306
- particle.posY += movY + particle.velY;
306
+ particle.posX += (movX + particle.velX) * step;
307
+ particle.posY += (movY + particle.velY) * step;
307
308
  // Wrap particles around the canvas
308
309
  particle.posX %= width;
309
310
  if (particle.posX < 0)
@@ -312,8 +313,8 @@ class CanvasParticles {
312
313
  if (particle.posY < 0)
313
314
  particle.posY += height;
314
315
  // Slightly decrease dynamic velocity
315
- particle.velX *= friction;
316
- particle.velY *= friction;
316
+ particle.velX *= Math.pow(friction, step);
317
+ particle.velY *= Math.pow(friction, step);
317
318
  // Distance from mouse
318
319
  const distX = particle.posX + offX - mouseX;
319
320
  const distY = particle.posY + offY - mouseY;
@@ -321,12 +322,12 @@ class CanvasParticles {
321
322
  if (!isMouseInteractionTypeNone) {
322
323
  const distRatio = mouseConnectDist / Math.hypot(distX, distY);
323
324
  if (mouseDistRatio < distRatio) {
324
- particle.offX += (distRatio * distX - distX - particle.offX) / 4;
325
- particle.offY += (distRatio * distY - distY - particle.offY) / 4;
325
+ particle.offX += (distRatio * distX - distX - particle.offX) * easing;
326
+ particle.offY += (distRatio * distY - distY - particle.offY) * easing;
326
327
  }
327
328
  else {
328
- particle.offX -= particle.offX / 4;
329
- particle.offY -= particle.offY / 4;
329
+ particle.offX -= particle.offX * easing;
330
+ particle.offY -= particle.offY * easing;
330
331
  }
331
332
  }
332
333
  // Visually displace the particles
@@ -476,7 +477,7 @@ class CanvasParticles {
476
477
  // - step = 1 → exactly one baseline update (dt === BASE_DT)
477
478
  // - step > 1 → more time passed (lower FPS), advance further
478
479
  // - step < 1 → less time passed (higher FPS), advance less
479
- const step = CanvasParticles.BASE_DT / dt;
480
+ const step = dt / CanvasParticles.BASE_DT;
480
481
  this.#updateGravity(step);
481
482
  this.#updateParticles(step);
482
483
  this.#render();
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.1.5";static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static MAX_DT=1e3/30;static BASE_DT=1e3/60;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 isFinite(o)&&i<o&&console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)),isFinite(a)&&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=[];particleCount=0;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.updateCanvasRect(),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}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("particleCount must be finite")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#e(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particleCount,n=this.particles,o=this.option.particles.connectDist*this.option.gravity.repulsive,a=this.option.particles.connectDist*this.option.gravity.pulling,r=(this.option.particles.connectDist/2)**2,c=.1*this.option.particles.connectDist;for(let t=0;t<s;t++){const i=n[t];for(let h=t+1;h<s;h++){const t=n[h],s=i.posX-t.posX,l=i.posY-t.posY,p=s*s+l*l;let u,d,f;if(p>=r&&!e)continue;u=Math.atan2(t.posY-i.posY,t.posX-i.posX),d=(1/Math.sqrt(p))**1.8;const m=Math.cos(u),v=Math.sin(u);if(p<r){f=Math.min(c,d*o);const e=m*f,s=v*f;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;f=Math.min(c,d*a);const g=m*f,y=v*f;i.velX+=g,i.velY+=y,t.velX-=g,t.velY-=y}}}#n(i){const s=this.particleCount,n=this.particles,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed,u=this.option.gravity.friction,d=this.option.mouse.connectDist,f=this.option.mouse.distRatio,m=this.option.mouse.interactionType===e.interactionType.NONE,v=this.option.mouse.interactionType===e.interactionType.MOVE;for(let i=0;i<s;i++){const e=n[i];e.dir+=2*(Math.random()-.5)*p,e.dir%=t;const s=Math.sin(e.dir)*e.speed,g=Math.cos(e.dir)*e.speed;e.posX+=s+e.velX,e.posY+=g+e.velY,e.posX%=o,e.posX<0&&(e.posX+=o),e.posY%=a,e.posY<0&&(e.posY+=a),e.velX*=u,e.velY*=u;const y=e.posX+r-h,x=e.posY+c-l;if(!m){const t=d/Math.hypot(y,x);f<t?(e.offX+=(t*y-y-e.offX)/4,e.offY+=(t*x-x-e.offY)/4):(e.offX-=e.offX/4,e.offY-=e.offY/4)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,v&&(e.posX=e.x,e.posY=e.y),e.x+=r,e.y+=c,this.#o(e),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particleCount,e=this.particles,s=this.ctx;for(let n=0;n<i;n++){const i=e[n];i.isVisible&&(i.size>1?(s.beginPath(),s.arc(i.x,i.y,i.size,0,t),s.fill(),s.closePath()):s.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}}#c(){const t=this.particleCount,i=this.particles,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<t;s++){const p=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!a&&!this.#a(p,t))continue;const s=p.x-t.x,f=p.y-t.y,m=s*s+f*f;if(!(m>n)&&(m>o?(e.globalAlpha=h/Math.sqrt(m)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(t.x,t.y),e.stroke()):l.push([p.x,p.y,t.x,t.y]),(u+=m)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT),s=e.BASE_DT/i;this.#s(s),this.#n(s),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!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,1),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i("particles.ppm",t.particles?.ppm,100),max:i("particles.max",t.particles?.max,1/0),maxWork: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:1}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0),pulling:i("gravity.pulling",t.gravity?.pulling,0),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return 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.1.6";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});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 isFinite(o)&&i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):isFinite(a)&&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=[];particleCount=0;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.updateCanvasRect(),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}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("particleCount must be finite")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#e(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particleCount,n=this.particles,o=this.option.particles.connectDist*this.option.gravity.repulsive*t,a=this.option.particles.connectDist*this.option.gravity.pulling*t,r=(this.option.particles.connectDist/2)**2,c=.1*this.option.particles.connectDist*t;for(let t=0;t<s;t++){const i=n[t];for(let h=t+1;h<s;h++){const t=n[h],s=i.posX-t.posX,l=i.posY-t.posY,p=s*s+l*l;let u,d,f;if(p>=r&&!e)continue;u=Math.atan2(t.posY-i.posY,t.posX-i.posX),d=Math.pow(1/Math.sqrt(p),1.8);const m=Math.cos(u),v=Math.sin(u);if(p<r){f=Math.min(c,d*o);const e=m*f,s=v*f;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;f=Math.min(c,d*a);const g=m*f,y=v*f;i.velX+=g,i.velY+=y,t.velX-=g,t.velY-=y}}}#n(i){const s=this.particleCount,n=this.particles,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,f=this.option.mouse.distRatio,m=this.option.mouse.interactionType===e.interactionType.NONE,v=this.option.mouse.interactionType===e.interactionType.MOVE,g=1-Math.pow(.75,i);for(let e=0;e<s;e++){const s=n[e];s.dir+=2*(Math.random()-.5)*p*i,s.dir%=t;const y=Math.sin(s.dir)*s.speed,x=Math.cos(s.dir)*s.speed;s.posX+=(y+s.velX)*i,s.posY+=(x+s.velY)*i,s.posX%=o,s.posX<0&&(s.posX+=o),s.posY%=a,s.posY<0&&(s.posY+=a),s.velX*=Math.pow(u,i),s.velY*=Math.pow(u,i);const b=s.posX+r-h,M=s.posY+c-l;if(!m){const t=d/Math.hypot(b,M);f<t?(s.offX+=(t*b-b-s.offX)*g,s.offY+=(t*M-M-s.offY)*g):(s.offX-=s.offX*g,s.offY-=s.offY*g)}s.x=s.posX+s.offX,s.y=s.posY+s.offY,v&&(s.posX=s.x,s.posY=s.y),s.x+=r,s.y+=c,this.#o(s),s.isVisible=1===s.gridPos.x&&1===s.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particleCount,e=this.particles,s=this.ctx;for(let n=0;n<i;n++){const i=e[n];i.isVisible&&(i.size>1?(s.beginPath(),s.arc(i.x,i.y,i.size,0,t),s.fill(),s.closePath()):s.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}}#c(){const t=this.particleCount,i=this.particles,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<t;s++){const p=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!a&&!this.#a(p,t))continue;const s=p.x-t.x,f=p.y-t.y,m=s*s+f*f;if(!(m>n)&&(m>o?(e.globalAlpha=h/Math.sqrt(m)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(t.x,t.y),e.stroke()):l.push([p.x,p.y,t.x,t.y]),(u+=m)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#s(i),this.#n(i),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!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,1),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i("particles.ppm",t.particles?.ppm,100),max:i("particles.max",t.particles?.max,1/0),maxWork: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:1}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0),pulling:i("gravity.pulling",t.gravity?.pulling,0),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.1.5",
3
+ "version": "4.1.6",
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,8 +31,11 @@ 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
35
+ private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
36
+
34
37
  /** Defines mouse interaction types with the particles */
35
- static interactionType = Object.freeze({
38
+ static readonly interactionType = Object.freeze({
36
39
  NONE: 0, // No mouse interaction
37
40
  SHIFT: 1, // Visual displacement only
38
41
  MOVE: 2, // Actual particle movement
@@ -71,12 +74,11 @@ export default class CanvasParticles {
71
74
  }
72
75
  })
73
76
 
74
- private static readonly MAX_DT = 1000 / 30 // milliseconds between updates @ 30 FPS
75
- private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
76
77
  /** Helper functions for options parsing */
77
- private static defaultIfNaN = (value: number, defaultValue: number): number => (isNaN(+value) ? defaultValue : +value)
78
+ private static readonly defaultIfNaN = (value: number, defaultValue: number): number =>
79
+ isNaN(+value) ? defaultValue : +value
78
80
 
79
- private static parseNumericOption = (
81
+ private static readonly parseNumericOption = (
80
82
  name: string,
81
83
  value: number | undefined,
82
84
  defaultValue: number,
@@ -88,9 +90,7 @@ export default class CanvasParticles {
88
90
 
89
91
  if (isFinite(min) && value < min) {
90
92
  console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
91
- }
92
-
93
- if (isFinite(max) && value > max) {
93
+ } else if (isFinite(max) && value > max) {
94
94
  console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
95
95
  }
96
96
 
@@ -281,11 +281,11 @@ export default class CanvasParticles {
281
281
 
282
282
  const len = this.particleCount
283
283
  const particles = this.particles
284
- const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive
285
- const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling
284
+ const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step
285
+ const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step
286
286
  const maxRepulsiveDist = this.option.particles.connectDist / 2
287
287
  const maxRepulsiveDistSq = maxRepulsiveDist ** 2
288
- const maxGrav = this.option.particles.connectDist * 0.1
288
+ const maxGrav = this.option.particles.connectDist * 0.1 * step
289
289
 
290
290
  for (let i = 0; i < len; i++) {
291
291
  const particleA = particles[i]
@@ -305,7 +305,7 @@ export default class CanvasParticles {
305
305
  if (distSq >= maxRepulsiveDistSq && !isPullingEnabled) continue
306
306
 
307
307
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
308
- grav = (1 / Math.sqrt(distSq)) ** 1.8
308
+ grav = Math.pow(1 / Math.sqrt(distSq), 1.8)
309
309
  const angleX = Math.cos(angle)
310
310
  const angleY = Math.sin(angle)
311
311
 
@@ -342,17 +342,18 @@ export default class CanvasParticles {
342
342
  const offY = this.offY
343
343
  const mouseX = this.mouseX
344
344
  const mouseY = this.mouseY
345
- const rotationSpeed = this.option.particles.rotationSpeed
345
+ const rotationSpeed = this.option.particles.rotationSpeed * step
346
346
  const friction = this.option.gravity.friction
347
347
  const mouseConnectDist = this.option.mouse.connectDist
348
348
  const mouseDistRatio = this.option.mouse.distRatio
349
349
  const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE
350
350
  const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE
351
+ const easing = 1 - Math.pow(1 - 1 / 4, step)
351
352
 
352
353
  for (let i = 0; i < len; i++) {
353
354
  const particle = particles[i]
354
355
 
355
- particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed
356
+ particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step
356
357
  particle.dir %= TWO_PI
357
358
 
358
359
  // Constant velocity
@@ -360,8 +361,8 @@ export default class CanvasParticles {
360
361
  const movY = Math.cos(particle.dir) * particle.speed
361
362
 
362
363
  // Apply velocities
363
- particle.posX += movX + particle.velX
364
- particle.posY += movY + particle.velY
364
+ particle.posX += (movX + particle.velX) * step
365
+ particle.posY += (movY + particle.velY) * step
365
366
 
366
367
  // Wrap particles around the canvas
367
368
  particle.posX %= width
@@ -371,8 +372,8 @@ export default class CanvasParticles {
371
372
  if (particle.posY < 0) particle.posY += height
372
373
 
373
374
  // Slightly decrease dynamic velocity
374
- particle.velX *= friction
375
- particle.velY *= friction
375
+ particle.velX *= Math.pow(friction, step)
376
+ particle.velY *= Math.pow(friction, step)
376
377
 
377
378
  // Distance from mouse
378
379
  const distX = particle.posX + offX - mouseX
@@ -383,11 +384,11 @@ export default class CanvasParticles {
383
384
  const distRatio = mouseConnectDist / Math.hypot(distX, distY)
384
385
 
385
386
  if (mouseDistRatio < distRatio) {
386
- particle.offX += (distRatio * distX - distX - particle.offX) / 4
387
- particle.offY += (distRatio * distY - distY - particle.offY) / 4
387
+ particle.offX += (distRatio * distX - distX - particle.offX) * easing
388
+ particle.offY += (distRatio * distY - distY - particle.offY) * easing
388
389
  } else {
389
- particle.offX -= particle.offX / 4
390
- particle.offY -= particle.offY / 4
390
+ particle.offX -= particle.offX * easing
391
+ particle.offY -= particle.offY * easing
391
392
  }
392
393
  }
393
394
 
@@ -562,7 +563,7 @@ export default class CanvasParticles {
562
563
  // - step = 1 → exactly one baseline update (dt === BASE_DT)
563
564
  // - step > 1 → more time passed (lower FPS), advance further
564
565
  // - step < 1 → less time passed (higher FPS), advance less
565
- const step = CanvasParticles.BASE_DT / dt
566
+ const step = dt / CanvasParticles.BASE_DT
566
567
 
567
568
  this.#updateGravity(step)
568
569
  this.#updateParticles(step)