canvasparticles-js 4.3.1 → 4.3.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/README.md +5 -2
- package/dist/index.cjs +25 -39
- package/dist/index.d.ts +6 -3
- package/dist/index.mjs +25 -39
- package/dist/index.umd.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/index.ts +33 -36
- package/src/types/index.ts +1 -1
package/README.md
CHANGED
|
@@ -337,16 +337,19 @@ By default `particles.ppm` and `particles.max` are used to auto-generate random
|
|
|
337
337
|
const canvas = '#my-canvas'
|
|
338
338
|
const options = {
|
|
339
339
|
particles: {
|
|
340
|
-
|
|
340
|
+
max: 0,
|
|
341
341
|
rotationSpeed: 0,
|
|
342
342
|
},
|
|
343
343
|
}
|
|
344
344
|
const instance = new CanvasParticles(canvas, options).start()
|
|
345
345
|
|
|
346
346
|
// Create a horizontal line of particles moving down
|
|
347
|
-
for (let x =
|
|
347
|
+
for (let x = 0; x < instance.width; x += 4) {
|
|
348
348
|
instance.createParticle(x, 100, 0, 1, 5)
|
|
349
349
|
}
|
|
350
|
+
|
|
351
|
+
// Keep automatically generated particles and remove manually created ones
|
|
352
|
+
instance.newParticles({ keepAuto: true, keepManual: false })
|
|
350
353
|
```
|
|
351
354
|
|
|
352
355
|
---
|
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.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 */
|
|
@@ -45,9 +45,9 @@ class CanvasParticles {
|
|
|
45
45
|
if (!instance.options?.animation)
|
|
46
46
|
return;
|
|
47
47
|
if ((canvas.inViewbox = entry.isIntersecting))
|
|
48
|
-
instance.
|
|
48
|
+
instance.option.animation?.startOnEnter && instance.start({ auto: true });
|
|
49
49
|
else
|
|
50
|
-
instance.
|
|
50
|
+
instance.option.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
|
|
51
51
|
}
|
|
52
52
|
}, {
|
|
53
53
|
rootMargin: '-1px',
|
|
@@ -184,7 +184,7 @@ class CanvasParticles {
|
|
|
184
184
|
if (this.isAnimating)
|
|
185
185
|
this.#render();
|
|
186
186
|
}
|
|
187
|
-
/** @private Update the target number of particles based on the current canvas size and `
|
|
187
|
+
/** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
|
|
188
188
|
#targetParticleCount() {
|
|
189
189
|
// Amount of particles to be created
|
|
190
190
|
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
|
|
@@ -194,19 +194,21 @@ class CanvasParticles {
|
|
|
194
194
|
return particleCount | 0;
|
|
195
195
|
}
|
|
196
196
|
/** @public Remove existing particles and generate new ones */
|
|
197
|
-
newParticles() {
|
|
197
|
+
newParticles({ keepAuto = false, keepManual = true } = {}) {
|
|
198
198
|
const particleCount = this.#targetParticleCount();
|
|
199
|
-
if (this.hasManualParticles) {
|
|
200
|
-
this.particles = this.particles.filter((particle) => particle.
|
|
199
|
+
if (this.hasManualParticles && (keepAuto || keepManual)) {
|
|
200
|
+
this.particles = this.particles.filter((particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual));
|
|
201
201
|
this.hasManualParticles = this.particles.length > 0;
|
|
202
202
|
}
|
|
203
203
|
else {
|
|
204
204
|
this.particles = [];
|
|
205
205
|
}
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
if (!keepAuto) {
|
|
207
|
+
for (let i = 0; i < particleCount; i++)
|
|
208
|
+
this.#createParticle();
|
|
209
|
+
}
|
|
208
210
|
}
|
|
209
|
-
/** @public Adjust particle array length to match `
|
|
211
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
210
212
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
211
213
|
const particleCount = this.#targetParticleCount();
|
|
212
214
|
if (this.hasManualParticles) {
|
|
@@ -215,11 +217,14 @@ class CanvasParticles {
|
|
|
215
217
|
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
216
218
|
// Only count automatic particles towards `particledCount`
|
|
217
219
|
for (const particle of this.particles) {
|
|
220
|
+
if (particle.isManual) {
|
|
221
|
+
pruned.push(particle);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
218
224
|
if (autoCount >= particleCount)
|
|
219
|
-
|
|
220
|
-
if (particle.manual)
|
|
221
|
-
autoCount++;
|
|
225
|
+
continue;
|
|
222
226
|
pruned.push(particle);
|
|
227
|
+
autoCount++;
|
|
223
228
|
}
|
|
224
229
|
this.particles = pruned;
|
|
225
230
|
}
|
|
@@ -239,27 +244,10 @@ class CanvasParticles {
|
|
|
239
244
|
#createParticle() {
|
|
240
245
|
const posX = prng() * this.width;
|
|
241
246
|
const posY = prng() * this.height;
|
|
242
|
-
|
|
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);
|
|
247
|
+
this.createParticle(posX, posY, prng() * TWO_PI, (0.5 + prng() * 0.5) * this.option.particles.relSpeed, (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, false);
|
|
260
248
|
}
|
|
261
249
|
/** @public Create a new particle with optional parameters */
|
|
262
|
-
createParticle(posX, posY, dir, speed, size) {
|
|
250
|
+
createParticle(posX, posY, dir, speed, size, isManual = true) {
|
|
263
251
|
const particle = {
|
|
264
252
|
posX, // Logical position in pixels
|
|
265
253
|
posY, // Logical position in pixels
|
|
@@ -274,7 +262,7 @@ class CanvasParticles {
|
|
|
274
262
|
size: size, // Ray in pixels of the particle
|
|
275
263
|
gridPos: { x: 1, y: 1 },
|
|
276
264
|
isVisible: false,
|
|
277
|
-
|
|
265
|
+
isManual,
|
|
278
266
|
};
|
|
279
267
|
this.#updateParticleBounds(particle);
|
|
280
268
|
this.particles.push(particle);
|
|
@@ -292,14 +280,12 @@ class CanvasParticles {
|
|
|
292
280
|
}
|
|
293
281
|
/* @public Randomize speed and size of all particles based on current options */
|
|
294
282
|
updateParticles() {
|
|
295
|
-
const particles = this.particles;
|
|
296
|
-
const len = particles.length;
|
|
297
283
|
const relSpeed = this.option.particles.relSpeed;
|
|
298
284
|
const relSize = this.option.particles.relSize;
|
|
299
|
-
for (
|
|
300
|
-
const particle = particles[i];
|
|
285
|
+
for (const particle of this.particles) {
|
|
301
286
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
302
287
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
288
|
+
this.#updateParticleBounds(particle); // because size changed
|
|
303
289
|
}
|
|
304
290
|
}
|
|
305
291
|
/** @private Apply gravity forces between particles */
|
|
@@ -445,7 +431,7 @@ class CanvasParticles {
|
|
|
445
431
|
// Visible if either particle is in the center
|
|
446
432
|
if (particleA.isVisible || particleB.isVisible)
|
|
447
433
|
return true;
|
|
448
|
-
// Not visible if both particles are in the same vertical or horizontal line
|
|
434
|
+
// Not visible if both particles are in the same vertical or horizontal line that does not cross the center
|
|
449
435
|
return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
450
436
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
|
|
451
437
|
}
|
|
@@ -539,7 +525,7 @@ class CanvasParticles {
|
|
|
539
525
|
this.ctx.strokeStyle = this.color.hex;
|
|
540
526
|
this.ctx.lineWidth = 1;
|
|
541
527
|
this.#renderParticles();
|
|
542
|
-
if (this.
|
|
528
|
+
if (this.option.particles.drawLines)
|
|
543
529
|
this.#renderConnections();
|
|
544
530
|
}
|
|
545
531
|
/** @private Main animation loop that updates and renders the particles */
|
package/dist/index.d.ts
CHANGED
|
@@ -54,13 +54,16 @@ export default class CanvasParticles {
|
|
|
54
54
|
/** @public Resize the canvas and update particles accordingly */
|
|
55
55
|
resizeCanvas(): void;
|
|
56
56
|
/** @public Remove existing particles and generate new ones */
|
|
57
|
-
newParticles(
|
|
58
|
-
|
|
57
|
+
newParticles({ keepAuto, keepManual }?: {
|
|
58
|
+
keepAuto?: boolean | undefined;
|
|
59
|
+
keepManual?: boolean | undefined;
|
|
60
|
+
}): void;
|
|
61
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
59
62
|
matchParticleCount({ updateBounds }?: {
|
|
60
63
|
updateBounds?: boolean;
|
|
61
64
|
}): void;
|
|
62
65
|
/** @public Create a new particle with optional parameters */
|
|
63
|
-
createParticle(posX: number, posY: number, dir: number, speed: number, size: number): void;
|
|
66
|
+
createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual?: boolean): void;
|
|
64
67
|
updateParticles(): void;
|
|
65
68
|
/** @public Start the particle animation if it was not running before */
|
|
66
69
|
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.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 */
|
|
@@ -43,9 +43,9 @@ class CanvasParticles {
|
|
|
43
43
|
if (!instance.options?.animation)
|
|
44
44
|
return;
|
|
45
45
|
if ((canvas.inViewbox = entry.isIntersecting))
|
|
46
|
-
instance.
|
|
46
|
+
instance.option.animation?.startOnEnter && instance.start({ auto: true });
|
|
47
47
|
else
|
|
48
|
-
instance.
|
|
48
|
+
instance.option.animation?.stopOnLeave && instance.stop({ auto: true, clear: false });
|
|
49
49
|
}
|
|
50
50
|
}, {
|
|
51
51
|
rootMargin: '-1px',
|
|
@@ -182,7 +182,7 @@ class CanvasParticles {
|
|
|
182
182
|
if (this.isAnimating)
|
|
183
183
|
this.#render();
|
|
184
184
|
}
|
|
185
|
-
/** @private Update the target number of particles based on the current canvas size and `
|
|
185
|
+
/** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
|
|
186
186
|
#targetParticleCount() {
|
|
187
187
|
// Amount of particles to be created
|
|
188
188
|
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
|
|
@@ -192,19 +192,21 @@ class CanvasParticles {
|
|
|
192
192
|
return particleCount | 0;
|
|
193
193
|
}
|
|
194
194
|
/** @public Remove existing particles and generate new ones */
|
|
195
|
-
newParticles() {
|
|
195
|
+
newParticles({ keepAuto = false, keepManual = true } = {}) {
|
|
196
196
|
const particleCount = this.#targetParticleCount();
|
|
197
|
-
if (this.hasManualParticles) {
|
|
198
|
-
this.particles = this.particles.filter((particle) => particle.
|
|
197
|
+
if (this.hasManualParticles && (keepAuto || keepManual)) {
|
|
198
|
+
this.particles = this.particles.filter((particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual));
|
|
199
199
|
this.hasManualParticles = this.particles.length > 0;
|
|
200
200
|
}
|
|
201
201
|
else {
|
|
202
202
|
this.particles = [];
|
|
203
203
|
}
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
if (!keepAuto) {
|
|
205
|
+
for (let i = 0; i < particleCount; i++)
|
|
206
|
+
this.#createParticle();
|
|
207
|
+
}
|
|
206
208
|
}
|
|
207
|
-
/** @public Adjust particle array length to match `
|
|
209
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
208
210
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
209
211
|
const particleCount = this.#targetParticleCount();
|
|
210
212
|
if (this.hasManualParticles) {
|
|
@@ -213,11 +215,14 @@ class CanvasParticles {
|
|
|
213
215
|
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
214
216
|
// Only count automatic particles towards `particledCount`
|
|
215
217
|
for (const particle of this.particles) {
|
|
218
|
+
if (particle.isManual) {
|
|
219
|
+
pruned.push(particle);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
216
222
|
if (autoCount >= particleCount)
|
|
217
|
-
|
|
218
|
-
if (particle.manual)
|
|
219
|
-
autoCount++;
|
|
223
|
+
continue;
|
|
220
224
|
pruned.push(particle);
|
|
225
|
+
autoCount++;
|
|
221
226
|
}
|
|
222
227
|
this.particles = pruned;
|
|
223
228
|
}
|
|
@@ -237,27 +242,10 @@ class CanvasParticles {
|
|
|
237
242
|
#createParticle() {
|
|
238
243
|
const posX = prng() * this.width;
|
|
239
244
|
const posY = prng() * this.height;
|
|
240
|
-
|
|
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);
|
|
245
|
+
this.createParticle(posX, posY, prng() * TWO_PI, (0.5 + prng() * 0.5) * this.option.particles.relSpeed, (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, false);
|
|
258
246
|
}
|
|
259
247
|
/** @public Create a new particle with optional parameters */
|
|
260
|
-
createParticle(posX, posY, dir, speed, size) {
|
|
248
|
+
createParticle(posX, posY, dir, speed, size, isManual = true) {
|
|
261
249
|
const particle = {
|
|
262
250
|
posX, // Logical position in pixels
|
|
263
251
|
posY, // Logical position in pixels
|
|
@@ -272,7 +260,7 @@ class CanvasParticles {
|
|
|
272
260
|
size: size, // Ray in pixels of the particle
|
|
273
261
|
gridPos: { x: 1, y: 1 },
|
|
274
262
|
isVisible: false,
|
|
275
|
-
|
|
263
|
+
isManual,
|
|
276
264
|
};
|
|
277
265
|
this.#updateParticleBounds(particle);
|
|
278
266
|
this.particles.push(particle);
|
|
@@ -290,14 +278,12 @@ class CanvasParticles {
|
|
|
290
278
|
}
|
|
291
279
|
/* @public Randomize speed and size of all particles based on current options */
|
|
292
280
|
updateParticles() {
|
|
293
|
-
const particles = this.particles;
|
|
294
|
-
const len = particles.length;
|
|
295
281
|
const relSpeed = this.option.particles.relSpeed;
|
|
296
282
|
const relSize = this.option.particles.relSize;
|
|
297
|
-
for (
|
|
298
|
-
const particle = particles[i];
|
|
283
|
+
for (const particle of this.particles) {
|
|
299
284
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
300
285
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
286
|
+
this.#updateParticleBounds(particle); // because size changed
|
|
301
287
|
}
|
|
302
288
|
}
|
|
303
289
|
/** @private Apply gravity forces between particles */
|
|
@@ -443,7 +429,7 @@ class CanvasParticles {
|
|
|
443
429
|
// Visible if either particle is in the center
|
|
444
430
|
if (particleA.isVisible || particleB.isVisible)
|
|
445
431
|
return true;
|
|
446
|
-
// Not visible if both particles are in the same vertical or horizontal line
|
|
432
|
+
// Not visible if both particles are in the same vertical or horizontal line that does not cross the center
|
|
447
433
|
return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
448
434
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
|
|
449
435
|
}
|
|
@@ -537,7 +523,7 @@ class CanvasParticles {
|
|
|
537
523
|
this.ctx.strokeStyle = this.color.hex;
|
|
538
524
|
this.ctx.lineWidth = 1;
|
|
539
525
|
this.#renderParticles();
|
|
540
|
-
if (this.
|
|
526
|
+
if (this.option.particles.drawLines)
|
|
541
527
|
this.#renderConnections();
|
|
542
528
|
}
|
|
543
529
|
/** @private Main animation loop that updates and renders the particles */
|
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.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});
|
|
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});
|
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.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",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@rollup/plugin-replace": "^6.0.3",
|
|
30
30
|
"@rollup/plugin-terser": "^0.4.4",
|
|
31
31
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
32
|
-
"@types/node": "^24.10.
|
|
33
|
-
"pnpm": "^10.
|
|
32
|
+
"@types/node": "^24.10.7",
|
|
33
|
+
"pnpm": "^10.28.0",
|
|
34
34
|
"prettier": "^3.7.4",
|
|
35
|
-
"rollup": "^4.
|
|
35
|
+
"rollup": "^4.55.1",
|
|
36
36
|
"rollup-plugin-delete": "^3.0.2",
|
|
37
37
|
"tslib": "^2.8.1",
|
|
38
38
|
"typescript": "^5.9.3"
|
package/src/index.ts
CHANGED
|
@@ -59,8 +59,8 @@ export default class CanvasParticles {
|
|
|
59
59
|
if (!instance.options?.animation) return
|
|
60
60
|
|
|
61
61
|
if ((canvas.inViewbox = entry.isIntersecting))
|
|
62
|
-
instance.
|
|
63
|
-
else instance.
|
|
62
|
+
instance.option.animation?.startOnEnter && instance.start({ auto: true })
|
|
63
|
+
else instance.option.animation?.stopOnLeave && instance.stop({ auto: true, clear: false })
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
{
|
|
@@ -226,7 +226,7 @@ export default class CanvasParticles {
|
|
|
226
226
|
if (this.isAnimating) this.#render()
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
/** @private Update the target number of particles based on the current canvas size and `
|
|
229
|
+
/** @private Update the target number of particles based on the current canvas size and `option.particles.ppm`, capped at `option.particles.max`. */
|
|
230
230
|
#targetParticleCount(): number {
|
|
231
231
|
// Amount of particles to be created
|
|
232
232
|
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000)
|
|
@@ -237,20 +237,24 @@ export default class CanvasParticles {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/** @public Remove existing particles and generate new ones */
|
|
240
|
-
newParticles() {
|
|
240
|
+
newParticles({ keepAuto = false, keepManual = true } = {}) {
|
|
241
241
|
const particleCount = this.#targetParticleCount()
|
|
242
242
|
|
|
243
|
-
if (this.hasManualParticles) {
|
|
244
|
-
this.particles = this.particles.filter(
|
|
243
|
+
if (this.hasManualParticles && (keepAuto || keepManual)) {
|
|
244
|
+
this.particles = this.particles.filter(
|
|
245
|
+
(particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual)
|
|
246
|
+
)
|
|
245
247
|
this.hasManualParticles = this.particles.length > 0
|
|
246
248
|
} else {
|
|
247
249
|
this.particles = []
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
|
|
252
|
+
if (!keepAuto) {
|
|
253
|
+
for (let i = 0; i < particleCount; i++) this.#createParticle()
|
|
254
|
+
}
|
|
251
255
|
}
|
|
252
256
|
|
|
253
|
-
/** @public Adjust particle array length to match `
|
|
257
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
254
258
|
matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
|
|
255
259
|
const particleCount = this.#targetParticleCount()
|
|
256
260
|
|
|
@@ -261,9 +265,14 @@ export default class CanvasParticles {
|
|
|
261
265
|
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
262
266
|
// Only count automatic particles towards `particledCount`
|
|
263
267
|
for (const particle of this.particles) {
|
|
264
|
-
if (
|
|
265
|
-
|
|
268
|
+
if (particle.isManual) {
|
|
269
|
+
pruned.push(particle)
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (autoCount >= particleCount) continue
|
|
266
274
|
pruned.push(particle)
|
|
275
|
+
autoCount++
|
|
267
276
|
}
|
|
268
277
|
this.particles = pruned
|
|
269
278
|
} else {
|
|
@@ -285,28 +294,18 @@ export default class CanvasParticles {
|
|
|
285
294
|
const posX = prng() * this.width
|
|
286
295
|
const posY = prng() * this.height
|
|
287
296
|
|
|
288
|
-
|
|
289
|
-
posX,
|
|
290
|
-
posY,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
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)
|
|
297
|
+
this.createParticle(
|
|
298
|
+
posX,
|
|
299
|
+
posY,
|
|
300
|
+
prng() * TWO_PI,
|
|
301
|
+
(0.5 + prng() * 0.5) * this.option.particles.relSpeed,
|
|
302
|
+
(0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize,
|
|
303
|
+
false
|
|
304
|
+
)
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
/** @public Create a new particle with optional parameters */
|
|
309
|
-
createParticle(posX: number, posY: number, dir: number, speed: number, size: number) {
|
|
308
|
+
createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual = true) {
|
|
310
309
|
const particle: Omit<Particle, 'bounds'> = {
|
|
311
310
|
posX, // Logical position in pixels
|
|
312
311
|
posY, // Logical position in pixels
|
|
@@ -321,7 +320,7 @@ export default class CanvasParticles {
|
|
|
321
320
|
size: size, // Ray in pixels of the particle
|
|
322
321
|
gridPos: { x: 1, y: 1 },
|
|
323
322
|
isVisible: false,
|
|
324
|
-
|
|
323
|
+
isManual,
|
|
325
324
|
}
|
|
326
325
|
this.#updateParticleBounds(particle)
|
|
327
326
|
this.particles.push(particle)
|
|
@@ -343,15 +342,13 @@ export default class CanvasParticles {
|
|
|
343
342
|
|
|
344
343
|
/* @public Randomize speed and size of all particles based on current options */
|
|
345
344
|
updateParticles() {
|
|
346
|
-
const particles = this.particles
|
|
347
|
-
const len = particles.length
|
|
348
345
|
const relSpeed = this.option.particles.relSpeed
|
|
349
346
|
const relSize = this.option.particles.relSize
|
|
350
347
|
|
|
351
|
-
for (
|
|
352
|
-
const particle = particles[i]
|
|
348
|
+
for (const particle of this.particles) {
|
|
353
349
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed
|
|
354
350
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize
|
|
351
|
+
this.#updateParticleBounds(particle) // because size changed
|
|
355
352
|
}
|
|
356
353
|
}
|
|
357
354
|
|
|
@@ -519,7 +516,7 @@ export default class CanvasParticles {
|
|
|
519
516
|
// Visible if either particle is in the center
|
|
520
517
|
if (particleA.isVisible || particleB.isVisible) return true
|
|
521
518
|
|
|
522
|
-
// Not visible if both particles are in the same vertical or horizontal line
|
|
519
|
+
// Not visible if both particles are in the same vertical or horizontal line that does not cross the center
|
|
523
520
|
return !(
|
|
524
521
|
(particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
525
522
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
|
|
@@ -629,7 +626,7 @@ export default class CanvasParticles {
|
|
|
629
626
|
this.ctx.lineWidth = 1
|
|
630
627
|
|
|
631
628
|
this.#renderParticles()
|
|
632
|
-
if (this.
|
|
629
|
+
if (this.option.particles.drawLines) this.#renderConnections()
|
|
633
630
|
}
|
|
634
631
|
|
|
635
632
|
/** @private Main animation loop that updates and renders the particles */
|