canvasparticles-js 4.3.1 → 4.3.2
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 +21 -37
- package/dist/index.d.ts +6 -3
- package/dist/index.mjs +21 -37
- package/dist/index.umd.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/index.ts +30 -35
- 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.2";
|
|
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,10 +194,10 @@ 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 {
|
|
@@ -206,7 +206,7 @@ class CanvasParticles {
|
|
|
206
206
|
for (let i = 0; i < particleCount; i++)
|
|
207
207
|
this.#createParticle();
|
|
208
208
|
}
|
|
209
|
-
/** @public Adjust particle array length to match `
|
|
209
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
210
210
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
211
211
|
const particleCount = this.#targetParticleCount();
|
|
212
212
|
if (this.hasManualParticles) {
|
|
@@ -215,11 +215,14 @@ class CanvasParticles {
|
|
|
215
215
|
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
216
216
|
// Only count automatic particles towards `particledCount`
|
|
217
217
|
for (const particle of this.particles) {
|
|
218
|
+
if (particle.isManual) {
|
|
219
|
+
pruned.push(particle);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
218
222
|
if (autoCount >= particleCount)
|
|
219
|
-
|
|
220
|
-
if (particle.manual)
|
|
221
|
-
autoCount++;
|
|
223
|
+
continue;
|
|
222
224
|
pruned.push(particle);
|
|
225
|
+
autoCount++;
|
|
223
226
|
}
|
|
224
227
|
this.particles = pruned;
|
|
225
228
|
}
|
|
@@ -239,27 +242,10 @@ class CanvasParticles {
|
|
|
239
242
|
#createParticle() {
|
|
240
243
|
const posX = prng() * this.width;
|
|
241
244
|
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);
|
|
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);
|
|
260
246
|
}
|
|
261
247
|
/** @public Create a new particle with optional parameters */
|
|
262
|
-
createParticle(posX, posY, dir, speed, size) {
|
|
248
|
+
createParticle(posX, posY, dir, speed, size, isManual = true) {
|
|
263
249
|
const particle = {
|
|
264
250
|
posX, // Logical position in pixels
|
|
265
251
|
posY, // Logical position in pixels
|
|
@@ -274,7 +260,7 @@ class CanvasParticles {
|
|
|
274
260
|
size: size, // Ray in pixels of the particle
|
|
275
261
|
gridPos: { x: 1, y: 1 },
|
|
276
262
|
isVisible: false,
|
|
277
|
-
|
|
263
|
+
isManual,
|
|
278
264
|
};
|
|
279
265
|
this.#updateParticleBounds(particle);
|
|
280
266
|
this.particles.push(particle);
|
|
@@ -292,14 +278,12 @@ class CanvasParticles {
|
|
|
292
278
|
}
|
|
293
279
|
/* @public Randomize speed and size of all particles based on current options */
|
|
294
280
|
updateParticles() {
|
|
295
|
-
const particles = this.particles;
|
|
296
|
-
const len = particles.length;
|
|
297
281
|
const relSpeed = this.option.particles.relSpeed;
|
|
298
282
|
const relSize = this.option.particles.relSize;
|
|
299
|
-
for (
|
|
300
|
-
const particle = particles[i];
|
|
283
|
+
for (const particle of this.particles) {
|
|
301
284
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
302
285
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
286
|
+
this.#updateParticleBounds(particle); // because size changed
|
|
303
287
|
}
|
|
304
288
|
}
|
|
305
289
|
/** @private Apply gravity forces between particles */
|
|
@@ -445,7 +429,7 @@ class CanvasParticles {
|
|
|
445
429
|
// Visible if either particle is in the center
|
|
446
430
|
if (particleA.isVisible || particleB.isVisible)
|
|
447
431
|
return true;
|
|
448
|
-
// 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
|
|
449
433
|
return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
450
434
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
|
|
451
435
|
}
|
|
@@ -539,7 +523,7 @@ class CanvasParticles {
|
|
|
539
523
|
this.ctx.strokeStyle = this.color.hex;
|
|
540
524
|
this.ctx.lineWidth = 1;
|
|
541
525
|
this.#renderParticles();
|
|
542
|
-
if (this.
|
|
526
|
+
if (this.option.particles.drawLines)
|
|
543
527
|
this.#renderConnections();
|
|
544
528
|
}
|
|
545
529
|
/** @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.2";
|
|
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,10 +192,10 @@ 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 {
|
|
@@ -204,7 +204,7 @@ class CanvasParticles {
|
|
|
204
204
|
for (let i = 0; i < particleCount; i++)
|
|
205
205
|
this.#createParticle();
|
|
206
206
|
}
|
|
207
|
-
/** @public Adjust particle array length to match `
|
|
207
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
208
208
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
209
209
|
const particleCount = this.#targetParticleCount();
|
|
210
210
|
if (this.hasManualParticles) {
|
|
@@ -213,11 +213,14 @@ class CanvasParticles {
|
|
|
213
213
|
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
214
214
|
// Only count automatic particles towards `particledCount`
|
|
215
215
|
for (const particle of this.particles) {
|
|
216
|
+
if (particle.isManual) {
|
|
217
|
+
pruned.push(particle);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
216
220
|
if (autoCount >= particleCount)
|
|
217
|
-
|
|
218
|
-
if (particle.manual)
|
|
219
|
-
autoCount++;
|
|
221
|
+
continue;
|
|
220
222
|
pruned.push(particle);
|
|
223
|
+
autoCount++;
|
|
221
224
|
}
|
|
222
225
|
this.particles = pruned;
|
|
223
226
|
}
|
|
@@ -237,27 +240,10 @@ class CanvasParticles {
|
|
|
237
240
|
#createParticle() {
|
|
238
241
|
const posX = prng() * this.width;
|
|
239
242
|
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);
|
|
243
|
+
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
244
|
}
|
|
259
245
|
/** @public Create a new particle with optional parameters */
|
|
260
|
-
createParticle(posX, posY, dir, speed, size) {
|
|
246
|
+
createParticle(posX, posY, dir, speed, size, isManual = true) {
|
|
261
247
|
const particle = {
|
|
262
248
|
posX, // Logical position in pixels
|
|
263
249
|
posY, // Logical position in pixels
|
|
@@ -272,7 +258,7 @@ class CanvasParticles {
|
|
|
272
258
|
size: size, // Ray in pixels of the particle
|
|
273
259
|
gridPos: { x: 1, y: 1 },
|
|
274
260
|
isVisible: false,
|
|
275
|
-
|
|
261
|
+
isManual,
|
|
276
262
|
};
|
|
277
263
|
this.#updateParticleBounds(particle);
|
|
278
264
|
this.particles.push(particle);
|
|
@@ -290,14 +276,12 @@ class CanvasParticles {
|
|
|
290
276
|
}
|
|
291
277
|
/* @public Randomize speed and size of all particles based on current options */
|
|
292
278
|
updateParticles() {
|
|
293
|
-
const particles = this.particles;
|
|
294
|
-
const len = particles.length;
|
|
295
279
|
const relSpeed = this.option.particles.relSpeed;
|
|
296
280
|
const relSize = this.option.particles.relSize;
|
|
297
|
-
for (
|
|
298
|
-
const particle = particles[i];
|
|
281
|
+
for (const particle of this.particles) {
|
|
299
282
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
300
283
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
284
|
+
this.#updateParticleBounds(particle); // because size changed
|
|
301
285
|
}
|
|
302
286
|
}
|
|
303
287
|
/** @private Apply gravity forces between particles */
|
|
@@ -443,7 +427,7 @@ class CanvasParticles {
|
|
|
443
427
|
// Visible if either particle is in the center
|
|
444
428
|
if (particleA.isVisible || particleB.isVisible)
|
|
445
429
|
return true;
|
|
446
|
-
// Not visible if both particles are in the same vertical or horizontal line
|
|
430
|
+
// Not visible if both particles are in the same vertical or horizontal line that does not cross the center
|
|
447
431
|
return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
448
432
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
|
|
449
433
|
}
|
|
@@ -537,7 +521,7 @@ class CanvasParticles {
|
|
|
537
521
|
this.ctx.strokeStyle = this.color.hex;
|
|
538
522
|
this.ctx.lineWidth = 1;
|
|
539
523
|
this.#renderParticles();
|
|
540
|
-
if (this.
|
|
524
|
+
if (this.option.particles.drawLines)
|
|
541
525
|
this.#renderConnections();
|
|
542
526
|
}
|
|
543
527
|
/** @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.2";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();this.hasManualParticles&&(t||i)?(this.particles=this.particles.filter(e=>t&&!e.isManual||i&&e.isManual),this.hasManualParticles=this.particles.length>0):this.particles=[];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.2",
|
|
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.6",
|
|
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,11 +237,13 @@ 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 = []
|
|
@@ -250,7 +252,7 @@ export default class CanvasParticles {
|
|
|
250
252
|
for (let i = 0; i < particleCount; i++) this.#createParticle()
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
/** @public Adjust particle array length to match `
|
|
255
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
254
256
|
matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
|
|
255
257
|
const particleCount = this.#targetParticleCount()
|
|
256
258
|
|
|
@@ -261,9 +263,14 @@ export default class CanvasParticles {
|
|
|
261
263
|
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
262
264
|
// Only count automatic particles towards `particledCount`
|
|
263
265
|
for (const particle of this.particles) {
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
+
if (particle.isManual) {
|
|
267
|
+
pruned.push(particle)
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (autoCount >= particleCount) continue
|
|
266
272
|
pruned.push(particle)
|
|
273
|
+
autoCount++
|
|
267
274
|
}
|
|
268
275
|
this.particles = pruned
|
|
269
276
|
} else {
|
|
@@ -285,28 +292,18 @@ export default class CanvasParticles {
|
|
|
285
292
|
const posX = prng() * this.width
|
|
286
293
|
const posY = prng() * this.height
|
|
287
294
|
|
|
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)
|
|
295
|
+
this.createParticle(
|
|
296
|
+
posX,
|
|
297
|
+
posY,
|
|
298
|
+
prng() * TWO_PI,
|
|
299
|
+
(0.5 + prng() * 0.5) * this.option.particles.relSpeed,
|
|
300
|
+
(0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize,
|
|
301
|
+
false
|
|
302
|
+
)
|
|
306
303
|
}
|
|
307
304
|
|
|
308
305
|
/** @public Create a new particle with optional parameters */
|
|
309
|
-
createParticle(posX: number, posY: number, dir: number, speed: number, size: number) {
|
|
306
|
+
createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual = true) {
|
|
310
307
|
const particle: Omit<Particle, 'bounds'> = {
|
|
311
308
|
posX, // Logical position in pixels
|
|
312
309
|
posY, // Logical position in pixels
|
|
@@ -321,7 +318,7 @@ export default class CanvasParticles {
|
|
|
321
318
|
size: size, // Ray in pixels of the particle
|
|
322
319
|
gridPos: { x: 1, y: 1 },
|
|
323
320
|
isVisible: false,
|
|
324
|
-
|
|
321
|
+
isManual,
|
|
325
322
|
}
|
|
326
323
|
this.#updateParticleBounds(particle)
|
|
327
324
|
this.particles.push(particle)
|
|
@@ -343,15 +340,13 @@ export default class CanvasParticles {
|
|
|
343
340
|
|
|
344
341
|
/* @public Randomize speed and size of all particles based on current options */
|
|
345
342
|
updateParticles() {
|
|
346
|
-
const particles = this.particles
|
|
347
|
-
const len = particles.length
|
|
348
343
|
const relSpeed = this.option.particles.relSpeed
|
|
349
344
|
const relSize = this.option.particles.relSize
|
|
350
345
|
|
|
351
|
-
for (
|
|
352
|
-
const particle = particles[i]
|
|
346
|
+
for (const particle of this.particles) {
|
|
353
347
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed
|
|
354
348
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize
|
|
349
|
+
this.#updateParticleBounds(particle) // because size changed
|
|
355
350
|
}
|
|
356
351
|
}
|
|
357
352
|
|
|
@@ -519,7 +514,7 @@ export default class CanvasParticles {
|
|
|
519
514
|
// Visible if either particle is in the center
|
|
520
515
|
if (particleA.isVisible || particleB.isVisible) return true
|
|
521
516
|
|
|
522
|
-
// Not visible if both particles are in the same vertical or horizontal line
|
|
517
|
+
// Not visible if both particles are in the same vertical or horizontal line that does not cross the center
|
|
523
518
|
return !(
|
|
524
519
|
(particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
525
520
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
|
|
@@ -629,7 +624,7 @@ export default class CanvasParticles {
|
|
|
629
624
|
this.ctx.lineWidth = 1
|
|
630
625
|
|
|
631
626
|
this.#renderParticles()
|
|
632
|
-
if (this.
|
|
627
|
+
if (this.option.particles.drawLines) this.#renderConnections()
|
|
633
628
|
}
|
|
634
629
|
|
|
635
630
|
/** @private Main animation loop that updates and renders the particles */
|