canvasparticles-js 4.3.3 → 4.3.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/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.3.3";
24
+ static version = "4.3.5";
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 */
@@ -64,7 +64,7 @@ class CanvasParticles {
64
64
  for (let i = 0; i < entries.length; i++) {
65
65
  const entry = entries[i];
66
66
  const canvas = entry.target;
67
- canvas.instance.resizeCanvas();
67
+ canvas.instance.#resizeCanvas();
68
68
  }
69
69
  });
70
70
  /** Helper functions for options parsing */
@@ -130,12 +130,10 @@ class CanvasParticles {
130
130
  this.resizeCanvas = this.resizeCanvas.bind(this);
131
131
  this.handleMouseMove = this.handleMouseMove.bind(this);
132
132
  this.handleScroll = this.handleScroll.bind(this);
133
- this.updateCanvasRect();
134
133
  this.resizeCanvas();
135
134
  window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
136
135
  window.addEventListener('scroll', this.handleScroll, { passive: true });
137
136
  }
138
- /* @public Update the canvas bounding rectangle and mouse position relative to it */
139
137
  updateCanvasRect() {
140
138
  const { top, left, width, height } = this.canvas.getBoundingClientRect();
141
139
  this.canvas.rect = { top, left, width, height };
@@ -157,14 +155,13 @@ class CanvasParticles {
157
155
  return;
158
156
  this.updateMousePos();
159
157
  }
160
- /** @public Update mouse coordinates */
161
158
  updateMousePos() {
162
159
  const { top, left } = this.canvas.rect;
163
160
  this.mouseX = this.clientX - left;
164
161
  this.mouseY = this.clientY - top;
165
162
  }
166
- /** @public Resize the canvas and update particles accordingly */
167
- resizeCanvas() {
163
+ /** @private Resize the canvas and update particles accordingly */
164
+ #resizeCanvas() {
168
165
  const width = (this.canvas.width = this.canvas.rect.width);
169
166
  const height = (this.canvas.height = this.canvas.rect.height);
170
167
  // Hide the mouse when resizing because it must be outside the viewport to do so
@@ -184,6 +181,11 @@ class CanvasParticles {
184
181
  if (this.isAnimating)
185
182
  this.#render();
186
183
  }
184
+ /** @public Update the canvas bounding rectangle, resize the canvas and update particles accordingly */
185
+ resizeCanvas() {
186
+ this.updateCanvasRect();
187
+ this.#resizeCanvas();
188
+ }
187
189
  /** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
188
190
  #targetParticleCount() {
189
191
  // Amount of particles to be created
@@ -301,7 +303,7 @@ class CanvasParticles {
301
303
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
302
304
  const maxRepulsiveDist = connectDist / 2;
303
305
  const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
304
- const eps = connectDist ** 2 / 256;
306
+ const epsilon = connectDist ** 2 / 256;
305
307
  for (let i = 0; i < len; i++) {
306
308
  const particleA = particles[i];
307
309
  for (let j = i + 1; j < len; j++) {
@@ -312,17 +314,12 @@ class CanvasParticles {
312
314
  const distSq = distX * distX + distY * distY;
313
315
  if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
314
316
  continue;
315
- let angle;
316
- let grav;
317
- let gravMult;
318
- angle = Math.atan2(-distY, -distX);
319
- grav = 1 / (distSq + eps);
320
- const angleX = Math.cos(angle);
321
- const angleY = Math.sin(angle);
317
+ const invSqrt = 1 / Math.sqrt(distSq + epsilon);
318
+ const invDist = invSqrt * invSqrt * invSqrt;
322
319
  if (distSq < maxRepulsiveDistSq) {
323
- gravMult = grav * gravRepulsiveMult;
324
- const gravX = angleX * gravMult;
325
- const gravY = angleY * gravMult;
320
+ const grav = invDist * gravRepulsiveMult;
321
+ const gravX = -distX * grav;
322
+ const gravY = -distY * grav;
326
323
  particleA.velX -= gravX;
327
324
  particleA.velY -= gravY;
328
325
  particleB.velX += gravX;
@@ -330,9 +327,9 @@ class CanvasParticles {
330
327
  }
331
328
  if (!isPullingEnabled)
332
329
  continue;
333
- gravMult = grav * gravPullingMult;
334
- const gravX = angleX * gravMult;
335
- const gravY = angleY * gravMult;
330
+ const grav = invDist * gravPullingMult;
331
+ const gravX = -distX * grav;
332
+ const gravY = -distY * grav;
336
333
  particleA.velX += gravX;
337
334
  particleA.velY += gravY;
338
335
  particleB.velX -= gravX;
package/dist/index.d.ts CHANGED
@@ -49,9 +49,8 @@ export default class CanvasParticles {
49
49
  updateCanvasRect(): void;
50
50
  handleMouseMove(event: MouseEvent): void;
51
51
  handleScroll(): void;
52
- /** @public Update mouse coordinates */
53
52
  updateMousePos(): void;
54
- /** @public Resize the canvas and update particles accordingly */
53
+ /** @public Update the canvas bounding rectangle, resize the canvas and update particles accordingly */
55
54
  resizeCanvas(): void;
56
55
  /** @public Remove existing particles and generate new ones */
57
56
  newParticles({ keepAuto, keepManual }?: {
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.3.3";
22
+ static version = "4.3.5";
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 */
@@ -62,7 +62,7 @@ class CanvasParticles {
62
62
  for (let i = 0; i < entries.length; i++) {
63
63
  const entry = entries[i];
64
64
  const canvas = entry.target;
65
- canvas.instance.resizeCanvas();
65
+ canvas.instance.#resizeCanvas();
66
66
  }
67
67
  });
68
68
  /** Helper functions for options parsing */
@@ -128,12 +128,10 @@ class CanvasParticles {
128
128
  this.resizeCanvas = this.resizeCanvas.bind(this);
129
129
  this.handleMouseMove = this.handleMouseMove.bind(this);
130
130
  this.handleScroll = this.handleScroll.bind(this);
131
- this.updateCanvasRect();
132
131
  this.resizeCanvas();
133
132
  window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
134
133
  window.addEventListener('scroll', this.handleScroll, { passive: true });
135
134
  }
136
- /* @public Update the canvas bounding rectangle and mouse position relative to it */
137
135
  updateCanvasRect() {
138
136
  const { top, left, width, height } = this.canvas.getBoundingClientRect();
139
137
  this.canvas.rect = { top, left, width, height };
@@ -155,14 +153,13 @@ class CanvasParticles {
155
153
  return;
156
154
  this.updateMousePos();
157
155
  }
158
- /** @public Update mouse coordinates */
159
156
  updateMousePos() {
160
157
  const { top, left } = this.canvas.rect;
161
158
  this.mouseX = this.clientX - left;
162
159
  this.mouseY = this.clientY - top;
163
160
  }
164
- /** @public Resize the canvas and update particles accordingly */
165
- resizeCanvas() {
161
+ /** @private Resize the canvas and update particles accordingly */
162
+ #resizeCanvas() {
166
163
  const width = (this.canvas.width = this.canvas.rect.width);
167
164
  const height = (this.canvas.height = this.canvas.rect.height);
168
165
  // Hide the mouse when resizing because it must be outside the viewport to do so
@@ -182,6 +179,11 @@ class CanvasParticles {
182
179
  if (this.isAnimating)
183
180
  this.#render();
184
181
  }
182
+ /** @public Update the canvas bounding rectangle, resize the canvas and update particles accordingly */
183
+ resizeCanvas() {
184
+ this.updateCanvasRect();
185
+ this.#resizeCanvas();
186
+ }
185
187
  /** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
186
188
  #targetParticleCount() {
187
189
  // Amount of particles to be created
@@ -299,7 +301,7 @@ class CanvasParticles {
299
301
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
300
302
  const maxRepulsiveDist = connectDist / 2;
301
303
  const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
302
- const eps = connectDist ** 2 / 256;
304
+ const epsilon = connectDist ** 2 / 256;
303
305
  for (let i = 0; i < len; i++) {
304
306
  const particleA = particles[i];
305
307
  for (let j = i + 1; j < len; j++) {
@@ -310,17 +312,12 @@ class CanvasParticles {
310
312
  const distSq = distX * distX + distY * distY;
311
313
  if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
312
314
  continue;
313
- let angle;
314
- let grav;
315
- let gravMult;
316
- angle = Math.atan2(-distY, -distX);
317
- grav = 1 / (distSq + eps);
318
- const angleX = Math.cos(angle);
319
- const angleY = Math.sin(angle);
315
+ const invSqrt = 1 / Math.sqrt(distSq + epsilon);
316
+ const invDist = invSqrt * invSqrt * invSqrt;
320
317
  if (distSq < maxRepulsiveDistSq) {
321
- gravMult = grav * gravRepulsiveMult;
322
- const gravX = angleX * gravMult;
323
- const gravY = angleY * gravMult;
318
+ const grav = invDist * gravRepulsiveMult;
319
+ const gravX = -distX * grav;
320
+ const gravY = -distY * grav;
324
321
  particleA.velX -= gravX;
325
322
  particleA.velY -= gravY;
326
323
  particleB.velX += gravX;
@@ -328,9 +325,9 @@ class CanvasParticles {
328
325
  }
329
326
  if (!isPullingEnabled)
330
327
  continue;
331
- gravMult = grav * gravPullingMult;
332
- const gravX = angleX * gravMult;
333
- const gravY = angleY * gravMult;
328
+ const grav = invDist * gravPullingMult;
329
+ const gravX = -distX * grav;
330
+ const gravY = -distY * grav;
334
331
  particleA.velX += gravX;
335
332
  particleA.velY += gravY;
336
333
  particleB.velX -= gravX;
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.3.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(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.option.animation?.startOnEnter&&n.start({auto:!0}):n.option.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});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 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.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;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.#t()}#i(){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.#i();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.#e()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();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.#s(t);for(let t=this.particles.length;t<i;t++)this.#e()}#e(){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.#s(a),this.particles.push(a),this.hasManualParticles=!0}#s(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.#s(s)}#n(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;let u,d,m;u=Math.atan2(-h,-n),d=1/(p+l);const f=Math.cos(u),g=Math.sin(u);if(p<c){m=d*a;const e=f*m,s=g*m;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;m=d*r;const v=f*m,y=g*m;i.velX+=v,i.velY+=y,t.velX-=v,t.velY-=y}}}#o(i){const s=this.particles,n=s.length,o=this.width,a=this.height,r=this.offX,c=this.offY,l=this.mouseX,h=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,m=this.option.mouse.distRatio,f=this.option.mouse.interactionType===e.interactionType.NONE,g=this.option.mouse.interactionType===e.interactionType.MOVE,v=1-Math.pow(.75,i);for(let e=0;e<n;e++){const n=s[e];n.dir+=2*(Math.random()-.5)*p*i,n.dir%=t;const y=Math.sin(n.dir)*n.speed,M=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(M+n.velY)*i,n.posX%=o,n.posX<0&&(n.posX+=o),n.posY%=a,n.posY<0&&(n.posY+=a),n.velX*=Math.pow(u,i),n.velY*=Math.pow(u,i);const x=n.posX+r-l,b=n.posY+c-h;if(!f){const t=d/Math.hypot(x,b);m<t?(n.offX+=(t*x-x-n.offX)*v,n.offY+=(t*b-b-n.offY)*v):(n.offX-=n.offX*v,n.offY-=n.offY*v)}n.x=n.posX+n.offX,n.y=n.posY+n.offY,g&&(n.posX=n.x,n.posY=n.y),n.x+=r,n.y+=c,this.#a(n),n.isVisible=1===n.gridPos.x&&1===n.gridPos.y}}#a(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)}#r(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)}#c(){const i=this.particles,e=i.length,s=this.ctx;for(let n=0;n<e;n++){const e=i[n];e.isVisible&&(e.size>1?(s.beginPath(),s.arc(e.x,e.y,e.size,0,t),s.fill(),s.closePath()):s.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}}#l(){const t=this.particles,i=t.length,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,l=this.color.alpha*s,h=[];for(let s=0;s<i;s++){const p=t[s];let u=0;for(let d=s+1;d<i;d++){const i=t[d];if(!a&&!this.#r(p,i))continue;const s=p.x-i.x,m=p.y-i.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=l/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(i.x,i.y),e.stroke()):h.push([p.x,p.y,i.x,i.y]),(u+=f)>=r))break}}if(h.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<h.length;t++){const i=h[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.#c(),this.option.particles.drawLines&&this.#l()}#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.#n(i),this.#o(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,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})}},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.3.5";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(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.option.animation?.startOnEnter&&n.start({auto:!0}):n.option.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});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.#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 u=1/Math.sqrt(p+l),d=u*u*u;if(p<c){const e=d*a,s=-n*e,o=-h*e;i.velX-=s,i.velY-=o,t.velX+=s,t.velY+=o}if(!e)continue;const m=d*r,f=-n*m,g=-h*m;i.velX+=f,i.velY+=g,t.velX-=f,t.velY-=g}}}#a(i){const s=this.particles,n=s.length,o=this.width,a=this.height,r=this.offX,c=this.offY,l=this.mouseX,h=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,m=this.option.mouse.distRatio,f=this.option.mouse.interactionType===e.interactionType.NONE,g=this.option.mouse.interactionType===e.interactionType.MOVE,v=1-Math.pow(.75,i);for(let e=0;e<n;e++){const n=s[e];n.dir+=2*(Math.random()-.5)*p*i,n.dir%=t;const y=Math.sin(n.dir)*n.speed,M=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(M+n.velY)*i,n.posX%=o,n.posX<0&&(n.posX+=o),n.posY%=a,n.posY<0&&(n.posY+=a),n.velX*=Math.pow(u,i),n.velY*=Math.pow(u,i);const x=n.posX+r-l,b=n.posY+c-h;if(!f){const t=d/Math.hypot(x,b);m<t?(n.offX+=(t*x-x-n.offX)*v,n.offY+=(t*b-b-n.offY)*v):(n.offX-=n.offX*v,n.offY-=n.offY*v)}n.x=n.posX+n.offX,n.y=n.posY+n.offY,g&&(n.posX=n.x,n.posY=n.y),n.x+=r,n.y+=c,this.#r(n),n.isVisible=1===n.gridPos.x&&1===n.gridPos.y}}#r(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)}#c(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)}#l(){const i=this.particles,e=i.length,s=this.ctx;for(let n=0;n<e;n++){const e=i[n];e.isVisible&&(e.size>1?(s.beginPath(),s.arc(e.x,e.y,e.size,0,t),s.fill(),s.closePath()):s.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}}#h(){const t=this.particles,i=t.length,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,l=this.color.alpha*s,h=[];for(let s=0;s<i;s++){const p=t[s];let u=0;for(let d=s+1;d<i;d++){const i=t[d];if(!a&&!this.#c(p,i))continue;const s=p.x-i.x,m=p.y-i.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=l/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(i.x,i.y),e.stroke()):h.push([p.x,p.y,i.x,i.y]),(u+=f)>=r))break}}if(h.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<h.length;t++){const i=h[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#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.#l(),this.option.particles.drawLines&&this.#h()}#p(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#p());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#o(i),this.#a(i),this.#i(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#p())),!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})}},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});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.3.3",
3
+ "version": "4.3.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
@@ -82,7 +82,7 @@ export default class CanvasParticles {
82
82
  for (let i = 0; i < entries.length; i++) {
83
83
  const entry = entries[i]
84
84
  const canvas = entry.target as CanvasParticlesCanvas
85
- canvas.instance.resizeCanvas()
85
+ canvas.instance.#resizeCanvas()
86
86
  }
87
87
  })
88
88
 
@@ -165,14 +165,11 @@ export default class CanvasParticles {
165
165
  this.handleMouseMove = this.handleMouseMove.bind(this)
166
166
  this.handleScroll = this.handleScroll.bind(this)
167
167
 
168
- this.updateCanvasRect()
169
168
  this.resizeCanvas()
170
-
171
169
  window.addEventListener('mousemove', this.handleMouseMove, { passive: true })
172
170
  window.addEventListener('scroll', this.handleScroll, { passive: true })
173
171
  }
174
172
 
175
- /* @public Update the canvas bounding rectangle and mouse position relative to it */
176
173
  updateCanvasRect() {
177
174
  const { top, left, width, height } = this.canvas.getBoundingClientRect()
178
175
  this.canvas.rect = { top, left, width, height }
@@ -195,15 +192,14 @@ export default class CanvasParticles {
195
192
  this.updateMousePos()
196
193
  }
197
194
 
198
- /** @public Update mouse coordinates */
199
195
  updateMousePos() {
200
196
  const { top, left } = this.canvas.rect
201
197
  this.mouseX = this.clientX - left
202
198
  this.mouseY = this.clientY - top
203
199
  }
204
200
 
205
- /** @public Resize the canvas and update particles accordingly */
206
- resizeCanvas() {
201
+ /** @private Resize the canvas and update particles accordingly */
202
+ #resizeCanvas() {
207
203
  const width = (this.canvas.width = this.canvas.rect.width)
208
204
  const height = (this.canvas.height = this.canvas.rect.height)
209
205
 
@@ -226,6 +222,12 @@ export default class CanvasParticles {
226
222
  if (this.isAnimating) this.#render()
227
223
  }
228
224
 
225
+ /** @public Update the canvas bounding rectangle, resize the canvas and update particles accordingly */
226
+ resizeCanvas() {
227
+ this.updateCanvasRect()
228
+ this.#resizeCanvas()
229
+ }
230
+
229
231
  /** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
230
232
  #targetParticleCount(): number {
231
233
  // Amount of particles to be created
@@ -366,7 +368,7 @@ export default class CanvasParticles {
366
368
  const gravPullingMult = connectDist * this.option.gravity.pulling * step
367
369
  const maxRepulsiveDist = connectDist / 2
368
370
  const maxRepulsiveDistSq = maxRepulsiveDist ** 2
369
- const eps = connectDist ** 2 / 256
371
+ const epsilon = connectDist ** 2 / 256
370
372
 
371
373
  for (let i = 0; i < len; i++) {
372
374
  const particleA = particles[i]
@@ -381,19 +383,13 @@ export default class CanvasParticles {
381
383
 
382
384
  if (distSq >= maxRepulsiveDistSq && !isPullingEnabled) continue
383
385
 
384
- let angle
385
- let grav
386
- let gravMult
387
-
388
- angle = Math.atan2(-distY, -distX)
389
- grav = 1 / (distSq + eps)
390
- const angleX = Math.cos(angle)
391
- const angleY = Math.sin(angle)
386
+ const invSqrt = 1 / Math.sqrt(distSq + epsilon)
387
+ const invDist = invSqrt * invSqrt * invSqrt
392
388
 
393
389
  if (distSq < maxRepulsiveDistSq) {
394
- gravMult = grav * gravRepulsiveMult
395
- const gravX = angleX * gravMult
396
- const gravY = angleY * gravMult
390
+ const grav = invDist * gravRepulsiveMult
391
+ const gravX = -distX * grav
392
+ const gravY = -distY * grav
397
393
  particleA.velX -= gravX
398
394
  particleA.velY -= gravY
399
395
  particleB.velX += gravX
@@ -402,9 +398,9 @@ export default class CanvasParticles {
402
398
 
403
399
  if (!isPullingEnabled) continue
404
400
 
405
- gravMult = grav * gravPullingMult
406
- const gravX = angleX * gravMult
407
- const gravY = angleY * gravMult
401
+ const grav = invDist * gravPullingMult
402
+ const gravX = -distX * grav
403
+ const gravY = -distY * grav
408
404
  particleA.velX += gravX
409
405
  particleA.velY += gravY
410
406
  particleB.velX -= gravX