canvasparticles-js 4.2.1 → 4.2.3

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.2.1";
24
+ static version = "4.2.3";
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 */
@@ -43,6 +43,8 @@ class CanvasParticles {
43
43
  else
44
44
  instance.options.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
45
45
  }
46
+ }, {
47
+ rootMargin: '-1px',
46
48
  });
47
49
  static canvasResizeObserver = new ResizeObserver((entries) => {
48
50
  // Seperate for loops is very important to prevent huge forced reflow overhead
@@ -241,8 +243,8 @@ class CanvasParticles {
241
243
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
242
244
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
243
245
  const maxRepulsiveDist = connectDist / 2;
244
- const maxRepulsiveDistSq = maxRepulsiveDist * maxRepulsiveDist;
245
- const eps = (connectDist * connectDist) / 256;
246
+ const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
247
+ const eps = connectDist ** 2 / 256;
246
248
  for (let i = 0; i < len; i++) {
247
249
  const particleA = particles[i];
248
250
  for (let j = i + 1; j < len; j++) {
@@ -405,9 +407,9 @@ class CanvasParticles {
405
407
  const particles = this.particles;
406
408
  const ctx = this.ctx;
407
409
  const maxDist = this.option.particles.connectDist;
408
- const maxDistSq = maxDist * maxDist;
410
+ const maxDistSq = maxDist ** 2;
409
411
  const halfMaxDist = maxDist / 2;
410
- const halfMaxDistSq = halfMaxDist * halfMaxDist;
412
+ const halfMaxDistSq = halfMaxDist ** 2;
411
413
  const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height);
412
414
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
413
415
  const alpha = this.color.alpha;
@@ -531,16 +533,16 @@ class CanvasParticles {
531
533
  },
532
534
  mouse: {
533
535
  interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
534
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
536
+ connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
535
537
  connectDist: 1 /* post processed */,
536
- distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
538
+ distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
537
539
  },
538
540
  particles: {
539
541
  regenerateOnResize: !!options.particles?.regenerateOnResize,
540
542
  drawLines: !!(options.particles?.drawLines ?? true),
541
543
  color: options.particles?.color ?? 'black',
542
544
  ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
543
- max: Math.round(pno('particles.max', options.particles?.max, Infinity)),
545
+ max: Math.round(pno('particles.max', options.particles?.max, Infinity, { min: 0 })),
544
546
  maxWork: Math.round(pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 })),
545
547
  connectDist: ~~pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
546
548
  relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
@@ -548,8 +550,8 @@ class CanvasParticles {
548
550
  rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
549
551
  },
550
552
  gravity: {
551
- repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0),
552
- pulling: pno('gravity.pulling', options.gravity?.pulling, 0),
553
+ repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
554
+ pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
553
555
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
554
556
  },
555
557
  };
@@ -570,8 +572,8 @@ class CanvasParticles {
570
572
  }
571
573
  /** @public Transform the distance multiplier (float) to absolute distance (px) */
572
574
  setMouseConnectDistMult(connectDistMult) {
573
- this.option.mouse.connectDist =
574
- this.option.particles.connectDist * (isNaN(connectDistMult) ? 2 / 3 : connectDistMult);
575
+ const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
576
+ this.option.mouse.connectDist = this.option.particles.connectDist * mult;
575
577
  }
576
578
  /** @public Format particle color and opacity */
577
579
  setParticleColor(color) {
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.2.1";
22
+ static version = "4.2.3";
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 */
@@ -41,6 +41,8 @@ class CanvasParticles {
41
41
  else
42
42
  instance.options.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
43
43
  }
44
+ }, {
45
+ rootMargin: '-1px',
44
46
  });
45
47
  static canvasResizeObserver = new ResizeObserver((entries) => {
46
48
  // Seperate for loops is very important to prevent huge forced reflow overhead
@@ -239,8 +241,8 @@ class CanvasParticles {
239
241
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
240
242
  const gravPullingMult = connectDist * this.option.gravity.pulling * step;
241
243
  const maxRepulsiveDist = connectDist / 2;
242
- const maxRepulsiveDistSq = maxRepulsiveDist * maxRepulsiveDist;
243
- const eps = (connectDist * connectDist) / 256;
244
+ const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
245
+ const eps = connectDist ** 2 / 256;
244
246
  for (let i = 0; i < len; i++) {
245
247
  const particleA = particles[i];
246
248
  for (let j = i + 1; j < len; j++) {
@@ -403,9 +405,9 @@ class CanvasParticles {
403
405
  const particles = this.particles;
404
406
  const ctx = this.ctx;
405
407
  const maxDist = this.option.particles.connectDist;
406
- const maxDistSq = maxDist * maxDist;
408
+ const maxDistSq = maxDist ** 2;
407
409
  const halfMaxDist = maxDist / 2;
408
- const halfMaxDistSq = halfMaxDist * halfMaxDist;
410
+ const halfMaxDistSq = halfMaxDist ** 2;
409
411
  const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height);
410
412
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
411
413
  const alpha = this.color.alpha;
@@ -529,16 +531,16 @@ class CanvasParticles {
529
531
  },
530
532
  mouse: {
531
533
  interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
532
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
534
+ connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
533
535
  connectDist: 1 /* post processed */,
534
- distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
536
+ distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
535
537
  },
536
538
  particles: {
537
539
  regenerateOnResize: !!options.particles?.regenerateOnResize,
538
540
  drawLines: !!(options.particles?.drawLines ?? true),
539
541
  color: options.particles?.color ?? 'black',
540
542
  ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
541
- max: Math.round(pno('particles.max', options.particles?.max, Infinity)),
543
+ max: Math.round(pno('particles.max', options.particles?.max, Infinity, { min: 0 })),
542
544
  maxWork: Math.round(pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 })),
543
545
  connectDist: ~~pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
544
546
  relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
@@ -546,8 +548,8 @@ class CanvasParticles {
546
548
  rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
547
549
  },
548
550
  gravity: {
549
- repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0),
550
- pulling: pno('gravity.pulling', options.gravity?.pulling, 0),
551
+ repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
552
+ pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
551
553
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
552
554
  },
553
555
  };
@@ -568,8 +570,8 @@ class CanvasParticles {
568
570
  }
569
571
  /** @public Transform the distance multiplier (float) to absolute distance (px) */
570
572
  setMouseConnectDistMult(connectDistMult) {
571
- this.option.mouse.connectDist =
572
- this.option.particles.connectDist * (isNaN(connectDistMult) ? 2 / 3 : connectDistMult);
573
+ const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
574
+ this.option.mouse.connectDist = this.option.particles.connectDist * mult;
573
575
  }
574
576
  /** @public Format particle color and opacity */
575
577
  setParticleColor(color) {
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.2.1";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,a=o*this.option.gravity.repulsive*t,r=o*this.option.gravity.pulling*t,c=o/2,h=c*c,l=o*o/256;for(let t=0;t<s;t++){const i=n[t];for(let o=t+1;o<s;o++){const t=n[o],s=i.posX-t.posX,c=i.posY-t.posY,p=s*s+c*c;if(p>=h&&!e)continue;let u,d,f;u=Math.atan2(-c,-s),d=1/(p+l);const m=Math.cos(u),v=Math.sin(u);if(p<h){f=d*a;const e=m*f,s=v*f;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;f=d*r;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,w=s.posY+c-l;if(!m){const t=d/Math.hypot(b,w);f<t?(s.offX+=(t*b-b-s.offX)*g,s.offY+=(t*w-w-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*s,o=s/2,a=o*o,r=s>=Math.min(this.canvas.width,this.canvas.height),c=n*this.option.particles.maxWork,h=this.color.alpha,l=this.color.alpha*s,p=[];for(let s=0;s<t;s++){const o=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!r&&!this.#a(o,t))continue;const s=o.x-t.x,f=o.y-t.y,m=s*s+f*f;if(!(m>n)&&(m>a?(e.globalAlpha=l/Math.sqrt(m)-h,e.beginPath(),e.moveTo(o.x,o.y),e.lineTo(t.x,t.y),e.stroke()):p.push([o.x,o.y,t.x,t.y]),(u+=m)>=c))break}}if(p.length){e.globalAlpha=h,e.beginPath();for(let t=0;t<p.length;t++){const i=p[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.options.particles.drawLines&&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,e.interactionType.MOVE,{min:0,max:2}),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,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)),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),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.2.3";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})}},{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 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,a=o*this.option.gravity.repulsive*t,r=o*this.option.gravity.pulling*t,c=(o/2)**2,h=o**2/256;for(let t=0;t<s;t++){const i=n[t];for(let o=t+1;o<s;o++){const t=n[o],s=i.posX-t.posX,l=i.posY-t.posY,p=s*s+l*l;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-l,-s),d=1/(p+h);const f=Math.cos(u),v=Math.sin(u);if(p<c){m=d*a;const e=f*m,s=v*m;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;m=d*r;const g=f*m,y=v*m;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,m=this.option.mouse.distRatio,f=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(!f){const t=d/Math.hypot(b,M);m<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,m=p.y-t.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=h/Math.sqrt(f)-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+=f)>=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.options.particles.drawLines&&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,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:{regenerateOnResize:!!t.particles?.regenerateOnResize,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});
@@ -34,9 +34,10 @@ export interface ParticleBounds {
34
34
  bottom: number;
35
35
  left: number;
36
36
  }
37
+ export type GridPos = 0 | 1 | 2;
37
38
  export interface ParticleGridPos {
38
- x: 0 | 1 | 2;
39
- y: 0 | 1 | 2;
39
+ x: GridPos;
40
+ y: GridPos;
40
41
  }
41
42
  export interface ContextColor {
42
43
  hex: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.2.1",
3
+ "version": "4.2.3",
4
4
  "description": "In an HTML canvas, a bunch of interactive particles connected with lines when they approach each other.",
5
5
  "author": "Khoeckman",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
2
2
  // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
3
3
 
4
- import type { CanvasParticlesCanvas, Particle, ContextColor, LineSegment } from './types'
4
+ import type { CanvasParticlesCanvas, Particle, GridPos, ContextColor, LineSegment } from './types'
5
5
  import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
6
6
 
7
7
  const TWO_PI = 2 * Math.PI
@@ -42,19 +42,24 @@ export default class CanvasParticles {
42
42
  })
43
43
 
44
44
  /** Observes canvas elements entering or leaving the viewport to start/stop animation */
45
- static readonly canvasIntersectionObserver = new IntersectionObserver((entries) => {
46
- for (let i = 0; i < entries.length; i++) {
47
- const entry = entries[i]
48
- const canvas = entry.target as CanvasParticlesCanvas
49
- const instance = canvas.instance // The CanvasParticles class instance bound to this canvas
50
-
51
- if (!instance.options?.animation) return
52
-
53
- if ((canvas.inViewbox = entry.isIntersecting))
54
- instance.options.animation?.startOnEnter && instance.start({ auto: true })
55
- else instance.options.animation?.stopOnLeave && instance.stop({ auto: true, clear: false })
45
+ static readonly canvasIntersectionObserver = new IntersectionObserver(
46
+ (entries) => {
47
+ for (let i = 0; i < entries.length; i++) {
48
+ const entry = entries[i]
49
+ const canvas = entry.target as CanvasParticlesCanvas
50
+ const instance = canvas.instance // The CanvasParticles class instance bound to this canvas
51
+
52
+ if (!instance.options?.animation) return
53
+
54
+ if ((canvas.inViewbox = entry.isIntersecting))
55
+ instance.options.animation?.startOnEnter && instance.start({ auto: true })
56
+ else instance.options.animation?.stopOnLeave && instance.stop({ auto: true, clear: false })
57
+ }
58
+ },
59
+ {
60
+ rootMargin: '-1px',
56
61
  }
57
- })
62
+ )
58
63
 
59
64
  static readonly canvasResizeObserver = new ResizeObserver((entries) => {
60
65
  // Seperate for loops is very important to prevent huge forced reflow overhead
@@ -285,8 +290,8 @@ export default class CanvasParticles {
285
290
  const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step
286
291
  const gravPullingMult = connectDist * this.option.gravity.pulling * step
287
292
  const maxRepulsiveDist = connectDist / 2
288
- const maxRepulsiveDistSq = maxRepulsiveDist * maxRepulsiveDist
289
- const eps = (connectDist * connectDist) / 256
293
+ const maxRepulsiveDistSq = maxRepulsiveDist ** 2
294
+ const eps = connectDist ** 2 / 256
290
295
 
291
296
  for (let i = 0; i < len; i++) {
292
297
  const particleA = particles[i]
@@ -427,8 +432,8 @@ export default class CanvasParticles {
427
432
  * - { x: 2, y: 2 } = bottom-right
428
433
  */
429
434
  #gridPos(particle: Particle): void {
430
- particle.gridPos.x = (+(particle.x >= particle.bounds.left) + +(particle.x > particle.bounds.right)) as 0 | 1 | 2
431
- particle.gridPos.y = (+(particle.y >= particle.bounds.top) + +(particle.y > particle.bounds.bottom)) as 0 | 1 | 2
435
+ particle.gridPos.x = (+(particle.x >= particle.bounds.left) + +(particle.x > particle.bounds.right)) as GridPos
436
+ particle.gridPos.y = (+(particle.y >= particle.bounds.top) + +(particle.y > particle.bounds.bottom)) as GridPos
432
437
  }
433
438
 
434
439
  /** @private Determines whether a line between 2 particles crosses through the visible center of the canvas */
@@ -474,9 +479,9 @@ export default class CanvasParticles {
474
479
  const particles = this.particles
475
480
  const ctx = this.ctx
476
481
  const maxDist = this.option.particles.connectDist
477
- const maxDistSq = maxDist * maxDist
482
+ const maxDistSq = maxDist ** 2
478
483
  const halfMaxDist = maxDist / 2
479
- const halfMaxDistSq = halfMaxDist * halfMaxDist
484
+ const halfMaxDistSq = halfMaxDist ** 2
480
485
  const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height)
481
486
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork
482
487
  const alpha = this.color.alpha
@@ -628,16 +633,16 @@ export default class CanvasParticles {
628
633
  CanvasParticles.interactionType.MOVE,
629
634
  { min: 0, max: 2 }
630
635
  ),
631
- connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
636
+ connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
632
637
  connectDist: 1 /* post processed */,
633
- distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
638
+ distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
634
639
  },
635
640
  particles: {
636
641
  regenerateOnResize: !!options.particles?.regenerateOnResize,
637
642
  drawLines: !!(options.particles?.drawLines ?? true),
638
643
  color: options.particles?.color ?? 'black',
639
644
  ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
640
- max: Math.round(pno('particles.max', options.particles?.max, Infinity)),
645
+ max: Math.round(pno('particles.max', options.particles?.max, Infinity, { min: 0 })),
641
646
  maxWork: Math.round(pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 })),
642
647
  connectDist: ~~pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
643
648
  relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
@@ -645,8 +650,8 @@ export default class CanvasParticles {
645
650
  rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
646
651
  },
647
652
  gravity: {
648
- repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0),
649
- pulling: pno('gravity.pulling', options.gravity?.pulling, 0),
653
+ repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
654
+ pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
650
655
  friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
651
656
  },
652
657
  }
@@ -669,8 +674,8 @@ export default class CanvasParticles {
669
674
 
670
675
  /** @public Transform the distance multiplier (float) to absolute distance (px) */
671
676
  setMouseConnectDistMult(connectDistMult: number) {
672
- this.option.mouse.connectDist =
673
- this.option.particles.connectDist * (isNaN(connectDistMult) ? 2 / 3 : connectDistMult)
677
+ const mult = CanvasParticles.parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 })
678
+ this.option.mouse.connectDist = this.option.particles.connectDist * mult
674
679
  }
675
680
 
676
681
  /** @public Format particle color and opacity */
@@ -39,9 +39,11 @@ export interface ParticleBounds {
39
39
  left: number
40
40
  }
41
41
 
42
+ export type GridPos = 0 | 1 | 2
43
+
42
44
  export interface ParticleGridPos {
43
- x: 0 | 1 | 2
44
- y: 0 | 1 | 2
45
+ x: GridPos
46
+ y: GridPos
45
47
  }
46
48
 
47
49
  export interface ContextColor {