canvasparticles-js 4.3.0 → 4.3.1
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 +65 -15
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +65 -15
- package/dist/index.umd.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/package.json +2 -1
- package/src/index.ts +69 -16
- package/src/types/index.ts +1 -0
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.
|
|
24
|
+
static version = "4.3.1";
|
|
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 */
|
|
@@ -73,10 +73,10 @@ class CanvasParticles {
|
|
|
73
73
|
if (value == undefined)
|
|
74
74
|
return defaultValue;
|
|
75
75
|
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
76
|
-
if (
|
|
76
|
+
if (value < min) {
|
|
77
77
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
78
78
|
}
|
|
79
|
-
else if (
|
|
79
|
+
else if (value > max) {
|
|
80
80
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
81
81
|
}
|
|
82
82
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -87,6 +87,7 @@ class CanvasParticles {
|
|
|
87
87
|
isAnimating = false;
|
|
88
88
|
lastAnimationFrame = 0;
|
|
89
89
|
particles = [];
|
|
90
|
+
hasManualParticles = false; // set to true once @public createParticle() is used
|
|
90
91
|
clientX = Infinity;
|
|
91
92
|
clientY = Infinity;
|
|
92
93
|
mouseX = Infinity;
|
|
@@ -195,23 +196,70 @@ class CanvasParticles {
|
|
|
195
196
|
/** @public Remove existing particles and generate new ones */
|
|
196
197
|
newParticles() {
|
|
197
198
|
const particleCount = this.#targetParticleCount();
|
|
198
|
-
this.
|
|
199
|
+
if (this.hasManualParticles) {
|
|
200
|
+
this.particles = this.particles.filter((particle) => particle.manual);
|
|
201
|
+
this.hasManualParticles = this.particles.length > 0;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.particles = [];
|
|
205
|
+
}
|
|
199
206
|
for (let i = 0; i < particleCount; i++)
|
|
200
|
-
this
|
|
207
|
+
this.#createParticle();
|
|
201
208
|
}
|
|
202
209
|
/** @public Adjust particle array length to match `options.particles.ppm` */
|
|
203
210
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
204
211
|
const particleCount = this.#targetParticleCount();
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
if (this.hasManualParticles) {
|
|
213
|
+
const pruned = [];
|
|
214
|
+
let autoCount = 0;
|
|
215
|
+
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
216
|
+
// Only count automatic particles towards `particledCount`
|
|
217
|
+
for (const particle of this.particles) {
|
|
218
|
+
if (autoCount >= particleCount)
|
|
219
|
+
break;
|
|
220
|
+
if (particle.manual)
|
|
221
|
+
autoCount++;
|
|
222
|
+
pruned.push(particle);
|
|
223
|
+
}
|
|
224
|
+
this.particles = pruned;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.particles = this.particles.slice(0, particleCount);
|
|
228
|
+
}
|
|
229
|
+
// Only necessary after resize
|
|
230
|
+
if (updateBounds) {
|
|
231
|
+
for (const particle of this.particles) {
|
|
232
|
+
this.#updateParticleBounds(particle);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (let i = this.particles.length; i < particleCount; i++)
|
|
236
|
+
this.#createParticle();
|
|
237
|
+
}
|
|
238
|
+
/** @private Create a random new particle */
|
|
239
|
+
#createParticle() {
|
|
240
|
+
const posX = prng() * this.width;
|
|
241
|
+
const posY = prng() * this.height;
|
|
242
|
+
const particle = {
|
|
243
|
+
posX, // Logical position in pixels
|
|
244
|
+
posY, // Logical position in pixels
|
|
245
|
+
x: posX, // Visual position in pixels
|
|
246
|
+
y: posY, // Visual position in pixels
|
|
247
|
+
velX: 0, // Horizonal speed in pixels per update
|
|
248
|
+
velY: 0, // Vertical speed in pixels per update
|
|
249
|
+
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
250
|
+
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
251
|
+
dir: prng() * TWO_PI, // Direction in radians
|
|
252
|
+
speed: (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
253
|
+
size: (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
254
|
+
gridPos: { x: 1, y: 1 },
|
|
255
|
+
isVisible: false,
|
|
256
|
+
manual: false,
|
|
257
|
+
};
|
|
258
|
+
this.#updateParticleBounds(particle);
|
|
259
|
+
this.particles.push(particle);
|
|
210
260
|
}
|
|
211
261
|
/** @public Create a new particle with optional parameters */
|
|
212
262
|
createParticle(posX, posY, dir, speed, size) {
|
|
213
|
-
posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
|
|
214
|
-
posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
|
|
215
263
|
const particle = {
|
|
216
264
|
posX, // Logical position in pixels
|
|
217
265
|
posY, // Logical position in pixels
|
|
@@ -221,14 +269,16 @@ class CanvasParticles {
|
|
|
221
269
|
velY: 0, // Vertical speed in pixels per update
|
|
222
270
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
223
271
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
224
|
-
dir: dir
|
|
225
|
-
speed: speed
|
|
226
|
-
size: size
|
|
272
|
+
dir: dir, // Direction in radians
|
|
273
|
+
speed: speed, // Velocity in pixels per update
|
|
274
|
+
size: size, // Ray in pixels of the particle
|
|
227
275
|
gridPos: { x: 1, y: 1 },
|
|
228
276
|
isVisible: false,
|
|
277
|
+
manual: true,
|
|
229
278
|
};
|
|
230
279
|
this.#updateParticleBounds(particle);
|
|
231
280
|
this.particles.push(particle);
|
|
281
|
+
this.hasManualParticles = true;
|
|
232
282
|
}
|
|
233
283
|
/** @private Update the visible bounds of a particle */
|
|
234
284
|
#updateParticleBounds(particle) {
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export default class CanvasParticles {
|
|
|
29
29
|
isAnimating: boolean;
|
|
30
30
|
private lastAnimationFrame;
|
|
31
31
|
particles: Particle[];
|
|
32
|
+
hasManualParticles: boolean;
|
|
32
33
|
private clientX;
|
|
33
34
|
private clientY;
|
|
34
35
|
mouseX: number;
|
|
@@ -59,7 +60,7 @@ export default class CanvasParticles {
|
|
|
59
60
|
updateBounds?: boolean;
|
|
60
61
|
}): void;
|
|
61
62
|
/** @public Create a new particle with optional parameters */
|
|
62
|
-
createParticle(posX
|
|
63
|
+
createParticle(posX: number, posY: number, dir: number, speed: number, size: number): void;
|
|
63
64
|
updateParticles(): void;
|
|
64
65
|
/** @public Start the particle animation if it was not running before */
|
|
65
66
|
start({ auto }?: {
|
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.
|
|
22
|
+
static version = "4.3.1";
|
|
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 */
|
|
@@ -71,10 +71,10 @@ class CanvasParticles {
|
|
|
71
71
|
if (value == undefined)
|
|
72
72
|
return defaultValue;
|
|
73
73
|
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
74
|
-
if (
|
|
74
|
+
if (value < min) {
|
|
75
75
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
76
76
|
}
|
|
77
|
-
else if (
|
|
77
|
+
else if (value > max) {
|
|
78
78
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
79
79
|
}
|
|
80
80
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -85,6 +85,7 @@ class CanvasParticles {
|
|
|
85
85
|
isAnimating = false;
|
|
86
86
|
lastAnimationFrame = 0;
|
|
87
87
|
particles = [];
|
|
88
|
+
hasManualParticles = false; // set to true once @public createParticle() is used
|
|
88
89
|
clientX = Infinity;
|
|
89
90
|
clientY = Infinity;
|
|
90
91
|
mouseX = Infinity;
|
|
@@ -193,23 +194,70 @@ class CanvasParticles {
|
|
|
193
194
|
/** @public Remove existing particles and generate new ones */
|
|
194
195
|
newParticles() {
|
|
195
196
|
const particleCount = this.#targetParticleCount();
|
|
196
|
-
this.
|
|
197
|
+
if (this.hasManualParticles) {
|
|
198
|
+
this.particles = this.particles.filter((particle) => particle.manual);
|
|
199
|
+
this.hasManualParticles = this.particles.length > 0;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.particles = [];
|
|
203
|
+
}
|
|
197
204
|
for (let i = 0; i < particleCount; i++)
|
|
198
|
-
this
|
|
205
|
+
this.#createParticle();
|
|
199
206
|
}
|
|
200
207
|
/** @public Adjust particle array length to match `options.particles.ppm` */
|
|
201
208
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
202
209
|
const particleCount = this.#targetParticleCount();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
if (this.hasManualParticles) {
|
|
211
|
+
const pruned = [];
|
|
212
|
+
let autoCount = 0;
|
|
213
|
+
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
214
|
+
// Only count automatic particles towards `particledCount`
|
|
215
|
+
for (const particle of this.particles) {
|
|
216
|
+
if (autoCount >= particleCount)
|
|
217
|
+
break;
|
|
218
|
+
if (particle.manual)
|
|
219
|
+
autoCount++;
|
|
220
|
+
pruned.push(particle);
|
|
221
|
+
}
|
|
222
|
+
this.particles = pruned;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
this.particles = this.particles.slice(0, particleCount);
|
|
226
|
+
}
|
|
227
|
+
// Only necessary after resize
|
|
228
|
+
if (updateBounds) {
|
|
229
|
+
for (const particle of this.particles) {
|
|
230
|
+
this.#updateParticleBounds(particle);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (let i = this.particles.length; i < particleCount; i++)
|
|
234
|
+
this.#createParticle();
|
|
235
|
+
}
|
|
236
|
+
/** @private Create a random new particle */
|
|
237
|
+
#createParticle() {
|
|
238
|
+
const posX = prng() * this.width;
|
|
239
|
+
const posY = prng() * this.height;
|
|
240
|
+
const particle = {
|
|
241
|
+
posX, // Logical position in pixels
|
|
242
|
+
posY, // Logical position in pixels
|
|
243
|
+
x: posX, // Visual position in pixels
|
|
244
|
+
y: posY, // Visual position in pixels
|
|
245
|
+
velX: 0, // Horizonal speed in pixels per update
|
|
246
|
+
velY: 0, // Vertical speed in pixels per update
|
|
247
|
+
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
248
|
+
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
249
|
+
dir: prng() * TWO_PI, // Direction in radians
|
|
250
|
+
speed: (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
251
|
+
size: (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
252
|
+
gridPos: { x: 1, y: 1 },
|
|
253
|
+
isVisible: false,
|
|
254
|
+
manual: false,
|
|
255
|
+
};
|
|
256
|
+
this.#updateParticleBounds(particle);
|
|
257
|
+
this.particles.push(particle);
|
|
208
258
|
}
|
|
209
259
|
/** @public Create a new particle with optional parameters */
|
|
210
260
|
createParticle(posX, posY, dir, speed, size) {
|
|
211
|
-
posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
|
|
212
|
-
posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
|
|
213
261
|
const particle = {
|
|
214
262
|
posX, // Logical position in pixels
|
|
215
263
|
posY, // Logical position in pixels
|
|
@@ -219,14 +267,16 @@ class CanvasParticles {
|
|
|
219
267
|
velY: 0, // Vertical speed in pixels per update
|
|
220
268
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
221
269
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
222
|
-
dir: dir
|
|
223
|
-
speed: speed
|
|
224
|
-
size: size
|
|
270
|
+
dir: dir, // Direction in radians
|
|
271
|
+
speed: speed, // Velocity in pixels per update
|
|
272
|
+
size: size, // Ray in pixels of the particle
|
|
225
273
|
gridPos: { x: 1, y: 1 },
|
|
226
274
|
isVisible: false,
|
|
275
|
+
manual: true,
|
|
227
276
|
};
|
|
228
277
|
this.#updateParticleBounds(particle);
|
|
229
278
|
this.particles.push(particle);
|
|
279
|
+
this.hasManualParticles = true;
|
|
230
280
|
}
|
|
231
281
|
/** @private Update the visible bounds of a particle */
|
|
232
282
|
#updateParticleBounds(particle) {
|
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.0";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.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=[];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(){const t=this.#i();this.particles=[];for(let i=0;i<t;i++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();for(this.particles=this.particles.slice(0,i),t&&this.particles.forEach(t=>this.#e(t));i>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+2*Math.pow(i(),5))*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}}updateParticles(){const t=this.particles,e=t.length,s=this.option.particles.relSpeed,n=this.option.particles.relSize;for(let o=0;o<e;o++){const e=t[o];e.speed=(.5+.5*i())*s,e.size=(.5+2*Math.pow(i(),5))*n}}#s(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,h=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,l=i.posY-t.posY,p=n*n+l*l;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-l,-n),d=1/(p+h);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}}}#n(i){const s=this.particles,n=s.length,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,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,x=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(x+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 M=n.posX+r-h,b=n.posY+c-l;if(!f){const t=d/Math.hypot(M,b);m<t?(n.offX+=(t*M-M-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.#o(n),n.isVisible=1===n.gridPos.x&&1===n.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.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))}}#c(){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,h=this.color.alpha*s,l=[];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.#a(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=h/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(i.x,i.y),e.stroke()):l.push([p.x,p.y,i.x,i.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:{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.1";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.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 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(){const t=this.#i();this.hasManualParticles?(this.particles=this.particles.filter(t=>t.manual),this.hasManualParticles=this.particles.length>0):this.particles=[];for(let i=0;i<t;i++)this.#e()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();if(this.hasManualParticles){const t=[];let e=0;for(const s of this.particles){if(e>=i)break;s.manual&&e++,t.push(s)}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,n={posX:e,posY:s,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:i()*t,speed:(.5+.5*i())*this.option.particles.relSpeed,size:(.5+2*Math.pow(i(),5))*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1,manual:!1};this.#s(n),this.particles.push(n)}createParticle(t,i,e,s,n){const o={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,manual:!0};this.#s(o),this.particles.push(o),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.particles,e=t.length,s=this.option.particles.relSpeed,n=this.option.particles.relSize;for(let o=0;o<e;o++){const e=t[o];e.speed=(.5+.5*i())*s,e.size=(.5+2*Math.pow(i(),5))*n}}#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,x=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(x+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 M=n.posX+r-l,b=n.posY+c-h;if(!f){const t=d/Math.hypot(M,b);m<t?(n.offX+=(t*M-M-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.options.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});
|
package/dist/types/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvasparticles-js",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.1",
|
|
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@rollup/plugin-terser": "^0.4.4",
|
|
31
31
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
32
32
|
"@types/node": "^24.10.4",
|
|
33
|
+
"pnpm": "^10.27.0",
|
|
33
34
|
"prettier": "^3.7.4",
|
|
34
35
|
"rollup": "^4.54.0",
|
|
35
36
|
"rollup-plugin-delete": "^3.0.2",
|
package/src/index.ts
CHANGED
|
@@ -100,9 +100,9 @@ export default class CanvasParticles {
|
|
|
100
100
|
|
|
101
101
|
const { min = -Infinity, max = Infinity } = clamp ?? {}
|
|
102
102
|
|
|
103
|
-
if (
|
|
103
|
+
if (value < min) {
|
|
104
104
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
|
|
105
|
-
} else if (
|
|
105
|
+
} else if (value > max) {
|
|
106
106
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -117,6 +117,7 @@ export default class CanvasParticles {
|
|
|
117
117
|
private lastAnimationFrame: number = 0
|
|
118
118
|
|
|
119
119
|
particles: Particle[] = []
|
|
120
|
+
hasManualParticles = false // set to true once @public createParticle() is used
|
|
120
121
|
private clientX: number = Infinity
|
|
121
122
|
private clientY: number = Infinity
|
|
122
123
|
mouseX: number = Infinity
|
|
@@ -239,24 +240,50 @@ export default class CanvasParticles {
|
|
|
239
240
|
newParticles() {
|
|
240
241
|
const particleCount = this.#targetParticleCount()
|
|
241
242
|
|
|
242
|
-
this.
|
|
243
|
-
|
|
243
|
+
if (this.hasManualParticles) {
|
|
244
|
+
this.particles = this.particles.filter((particle) => particle.manual)
|
|
245
|
+
this.hasManualParticles = this.particles.length > 0
|
|
246
|
+
} else {
|
|
247
|
+
this.particles = []
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < particleCount; i++) this.#createParticle()
|
|
244
251
|
}
|
|
245
252
|
|
|
246
253
|
/** @public Adjust particle array length to match `options.particles.ppm` */
|
|
247
254
|
matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
|
|
248
255
|
const particleCount = this.#targetParticleCount()
|
|
249
256
|
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
if (this.hasManualParticles) {
|
|
258
|
+
const pruned: Particle[] = []
|
|
259
|
+
let autoCount = 0
|
|
260
|
+
|
|
261
|
+
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
262
|
+
// Only count automatic particles towards `particledCount`
|
|
263
|
+
for (const particle of this.particles) {
|
|
264
|
+
if (autoCount >= particleCount) break
|
|
265
|
+
if (particle.manual) autoCount++
|
|
266
|
+
pruned.push(particle)
|
|
267
|
+
}
|
|
268
|
+
this.particles = pruned
|
|
269
|
+
} else {
|
|
270
|
+
this.particles = this.particles.slice(0, particleCount)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Only necessary after resize
|
|
274
|
+
if (updateBounds) {
|
|
275
|
+
for (const particle of this.particles) {
|
|
276
|
+
this.#updateParticleBounds(particle)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
252
279
|
|
|
253
|
-
|
|
280
|
+
for (let i = this.particles.length; i < particleCount; i++) this.#createParticle()
|
|
254
281
|
}
|
|
255
282
|
|
|
256
|
-
/** @
|
|
257
|
-
createParticle(
|
|
258
|
-
posX =
|
|
259
|
-
posY =
|
|
283
|
+
/** @private Create a random new particle */
|
|
284
|
+
#createParticle() {
|
|
285
|
+
const posX = prng() * this.width
|
|
286
|
+
const posY = prng() * this.height
|
|
260
287
|
|
|
261
288
|
const particle: Omit<Particle, 'bounds'> = {
|
|
262
289
|
posX, // Logical position in pixels
|
|
@@ -267,18 +294,44 @@ export default class CanvasParticles {
|
|
|
267
294
|
velY: 0, // Vertical speed in pixels per update
|
|
268
295
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
269
296
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
270
|
-
dir:
|
|
271
|
-
speed:
|
|
272
|
-
size:
|
|
297
|
+
dir: prng() * TWO_PI, // Direction in radians
|
|
298
|
+
speed: (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
299
|
+
size: (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
300
|
+
gridPos: { x: 1, y: 1 },
|
|
301
|
+
isVisible: false,
|
|
302
|
+
manual: false,
|
|
303
|
+
}
|
|
304
|
+
this.#updateParticleBounds(particle)
|
|
305
|
+
this.particles.push(particle)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** @public Create a new particle with optional parameters */
|
|
309
|
+
createParticle(posX: number, posY: number, dir: number, speed: number, size: number) {
|
|
310
|
+
const particle: Omit<Particle, 'bounds'> = {
|
|
311
|
+
posX, // Logical position in pixels
|
|
312
|
+
posY, // Logical position in pixels
|
|
313
|
+
x: posX, // Visual position in pixels
|
|
314
|
+
y: posY, // Visual position in pixels
|
|
315
|
+
velX: 0, // Horizonal speed in pixels per update
|
|
316
|
+
velY: 0, // Vertical speed in pixels per update
|
|
317
|
+
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
318
|
+
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
319
|
+
dir: dir, // Direction in radians
|
|
320
|
+
speed: speed, // Velocity in pixels per update
|
|
321
|
+
size: size, // Ray in pixels of the particle
|
|
273
322
|
gridPos: { x: 1, y: 1 },
|
|
274
323
|
isVisible: false,
|
|
324
|
+
manual: true,
|
|
275
325
|
}
|
|
276
326
|
this.#updateParticleBounds(particle)
|
|
277
|
-
this.particles.push(particle
|
|
327
|
+
this.particles.push(particle)
|
|
328
|
+
this.hasManualParticles = true
|
|
278
329
|
}
|
|
279
330
|
|
|
280
331
|
/** @private Update the visible bounds of a particle */
|
|
281
|
-
#updateParticleBounds(
|
|
332
|
+
#updateParticleBounds(
|
|
333
|
+
particle: Omit<Particle, 'bounds'> & Partial<Pick<Particle, 'bounds'>>
|
|
334
|
+
): asserts particle is Particle {
|
|
282
335
|
// The particle is considered visible within these bounds
|
|
283
336
|
particle.bounds = {
|
|
284
337
|
top: -particle.size,
|