canvasparticles-js 4.3.0 → 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 +61 -27
- package/dist/index.d.ts +7 -3
- package/dist/index.mjs +61 -27
- package/dist/index.umd.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/package.json +4 -3
- package/src/index.ts +75 -27
- package/src/types/index.ts +1 -0
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',
|
|
@@ -73,10 +73,10 @@ class CanvasParticles {
|
|
|
73
73
|
if (value == undefined)
|
|
74
74
|
return defaultValue;
|
|
75
75
|
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
76
|
-
if (
|
|
76
|
+
if (value < min) {
|
|
77
77
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
78
78
|
}
|
|
79
|
-
else if (
|
|
79
|
+
else if (value > max) {
|
|
80
80
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
81
81
|
}
|
|
82
82
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -87,6 +87,7 @@ class CanvasParticles {
|
|
|
87
87
|
isAnimating = false;
|
|
88
88
|
lastAnimationFrame = 0;
|
|
89
89
|
particles = [];
|
|
90
|
+
hasManualParticles = false; // set to true once @public createParticle() is used
|
|
90
91
|
clientX = Infinity;
|
|
91
92
|
clientY = Infinity;
|
|
92
93
|
mouseX = Infinity;
|
|
@@ -183,7 +184,7 @@ class CanvasParticles {
|
|
|
183
184
|
if (this.isAnimating)
|
|
184
185
|
this.#render();
|
|
185
186
|
}
|
|
186
|
-
/** @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`. */
|
|
187
188
|
#targetParticleCount() {
|
|
188
189
|
// Amount of particles to be created
|
|
189
190
|
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
|
|
@@ -193,25 +194,58 @@ class CanvasParticles {
|
|
|
193
194
|
return particleCount | 0;
|
|
194
195
|
}
|
|
195
196
|
/** @public Remove existing particles and generate new ones */
|
|
196
|
-
newParticles() {
|
|
197
|
+
newParticles({ keepAuto = false, keepManual = true } = {}) {
|
|
197
198
|
const particleCount = this.#targetParticleCount();
|
|
198
|
-
this.
|
|
199
|
+
if (this.hasManualParticles && (keepAuto || keepManual)) {
|
|
200
|
+
this.particles = this.particles.filter((particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual));
|
|
201
|
+
this.hasManualParticles = this.particles.length > 0;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.particles = [];
|
|
205
|
+
}
|
|
199
206
|
for (let i = 0; i < particleCount; i++)
|
|
200
|
-
this
|
|
207
|
+
this.#createParticle();
|
|
201
208
|
}
|
|
202
|
-
/** @public Adjust particle array length to match `
|
|
209
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
203
210
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
204
211
|
const particleCount = this.#targetParticleCount();
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
if (this.hasManualParticles) {
|
|
213
|
+
const pruned = [];
|
|
214
|
+
let autoCount = 0;
|
|
215
|
+
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
216
|
+
// Only count automatic particles towards `particledCount`
|
|
217
|
+
for (const particle of this.particles) {
|
|
218
|
+
if (particle.isManual) {
|
|
219
|
+
pruned.push(particle);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (autoCount >= particleCount)
|
|
223
|
+
continue;
|
|
224
|
+
pruned.push(particle);
|
|
225
|
+
autoCount++;
|
|
226
|
+
}
|
|
227
|
+
this.particles = pruned;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this.particles = this.particles.slice(0, particleCount);
|
|
231
|
+
}
|
|
232
|
+
// Only necessary after resize
|
|
233
|
+
if (updateBounds) {
|
|
234
|
+
for (const particle of this.particles) {
|
|
235
|
+
this.#updateParticleBounds(particle);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (let i = this.particles.length; i < particleCount; i++)
|
|
239
|
+
this.#createParticle();
|
|
240
|
+
}
|
|
241
|
+
/** @private Create a random new particle */
|
|
242
|
+
#createParticle() {
|
|
243
|
+
const posX = prng() * this.width;
|
|
244
|
+
const posY = prng() * this.height;
|
|
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);
|
|
210
246
|
}
|
|
211
247
|
/** @public Create a new particle with optional parameters */
|
|
212
|
-
createParticle(posX, posY, dir, speed, size) {
|
|
213
|
-
posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
|
|
214
|
-
posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
|
|
248
|
+
createParticle(posX, posY, dir, speed, size, isManual = true) {
|
|
215
249
|
const particle = {
|
|
216
250
|
posX, // Logical position in pixels
|
|
217
251
|
posY, // Logical position in pixels
|
|
@@ -221,14 +255,16 @@ class CanvasParticles {
|
|
|
221
255
|
velY: 0, // Vertical speed in pixels per update
|
|
222
256
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
223
257
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
224
|
-
dir: dir
|
|
225
|
-
speed: speed
|
|
226
|
-
size: size
|
|
258
|
+
dir: dir, // Direction in radians
|
|
259
|
+
speed: speed, // Velocity in pixels per update
|
|
260
|
+
size: size, // Ray in pixels of the particle
|
|
227
261
|
gridPos: { x: 1, y: 1 },
|
|
228
262
|
isVisible: false,
|
|
263
|
+
isManual,
|
|
229
264
|
};
|
|
230
265
|
this.#updateParticleBounds(particle);
|
|
231
266
|
this.particles.push(particle);
|
|
267
|
+
this.hasManualParticles = true;
|
|
232
268
|
}
|
|
233
269
|
/** @private Update the visible bounds of a particle */
|
|
234
270
|
#updateParticleBounds(particle) {
|
|
@@ -242,14 +278,12 @@ class CanvasParticles {
|
|
|
242
278
|
}
|
|
243
279
|
/* @public Randomize speed and size of all particles based on current options */
|
|
244
280
|
updateParticles() {
|
|
245
|
-
const particles = this.particles;
|
|
246
|
-
const len = particles.length;
|
|
247
281
|
const relSpeed = this.option.particles.relSpeed;
|
|
248
282
|
const relSize = this.option.particles.relSize;
|
|
249
|
-
for (
|
|
250
|
-
const particle = particles[i];
|
|
283
|
+
for (const particle of this.particles) {
|
|
251
284
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
252
285
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
286
|
+
this.#updateParticleBounds(particle); // because size changed
|
|
253
287
|
}
|
|
254
288
|
}
|
|
255
289
|
/** @private Apply gravity forces between particles */
|
|
@@ -395,7 +429,7 @@ class CanvasParticles {
|
|
|
395
429
|
// Visible if either particle is in the center
|
|
396
430
|
if (particleA.isVisible || particleB.isVisible)
|
|
397
431
|
return true;
|
|
398
|
-
// 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
|
|
399
433
|
return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
400
434
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
|
|
401
435
|
}
|
|
@@ -489,7 +523,7 @@ class CanvasParticles {
|
|
|
489
523
|
this.ctx.strokeStyle = this.color.hex;
|
|
490
524
|
this.ctx.lineWidth = 1;
|
|
491
525
|
this.#renderParticles();
|
|
492
|
-
if (this.
|
|
526
|
+
if (this.option.particles.drawLines)
|
|
493
527
|
this.#renderConnections();
|
|
494
528
|
}
|
|
495
529
|
/** @private Main animation loop that updates and renders the particles */
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export default class CanvasParticles {
|
|
|
29
29
|
isAnimating: boolean;
|
|
30
30
|
private lastAnimationFrame;
|
|
31
31
|
particles: Particle[];
|
|
32
|
+
hasManualParticles: boolean;
|
|
32
33
|
private clientX;
|
|
33
34
|
private clientY;
|
|
34
35
|
mouseX: number;
|
|
@@ -53,13 +54,16 @@ export default class CanvasParticles {
|
|
|
53
54
|
/** @public Resize the canvas and update particles accordingly */
|
|
54
55
|
resizeCanvas(): void;
|
|
55
56
|
/** @public Remove existing particles and generate new ones */
|
|
56
|
-
newParticles(
|
|
57
|
-
|
|
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` */
|
|
58
62
|
matchParticleCount({ updateBounds }?: {
|
|
59
63
|
updateBounds?: boolean;
|
|
60
64
|
}): void;
|
|
61
65
|
/** @public Create a new particle with optional parameters */
|
|
62
|
-
createParticle(posX
|
|
66
|
+
createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual?: boolean): void;
|
|
63
67
|
updateParticles(): void;
|
|
64
68
|
/** @public Start the particle animation if it was not running before */
|
|
65
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',
|
|
@@ -71,10 +71,10 @@ class CanvasParticles {
|
|
|
71
71
|
if (value == undefined)
|
|
72
72
|
return defaultValue;
|
|
73
73
|
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
74
|
-
if (
|
|
74
|
+
if (value < min) {
|
|
75
75
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
76
76
|
}
|
|
77
|
-
else if (
|
|
77
|
+
else if (value > max) {
|
|
78
78
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
79
79
|
}
|
|
80
80
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -85,6 +85,7 @@ class CanvasParticles {
|
|
|
85
85
|
isAnimating = false;
|
|
86
86
|
lastAnimationFrame = 0;
|
|
87
87
|
particles = [];
|
|
88
|
+
hasManualParticles = false; // set to true once @public createParticle() is used
|
|
88
89
|
clientX = Infinity;
|
|
89
90
|
clientY = Infinity;
|
|
90
91
|
mouseX = Infinity;
|
|
@@ -181,7 +182,7 @@ class CanvasParticles {
|
|
|
181
182
|
if (this.isAnimating)
|
|
182
183
|
this.#render();
|
|
183
184
|
}
|
|
184
|
-
/** @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`. */
|
|
185
186
|
#targetParticleCount() {
|
|
186
187
|
// Amount of particles to be created
|
|
187
188
|
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
|
|
@@ -191,25 +192,58 @@ class CanvasParticles {
|
|
|
191
192
|
return particleCount | 0;
|
|
192
193
|
}
|
|
193
194
|
/** @public Remove existing particles and generate new ones */
|
|
194
|
-
newParticles() {
|
|
195
|
+
newParticles({ keepAuto = false, keepManual = true } = {}) {
|
|
195
196
|
const particleCount = this.#targetParticleCount();
|
|
196
|
-
this.
|
|
197
|
+
if (this.hasManualParticles && (keepAuto || keepManual)) {
|
|
198
|
+
this.particles = this.particles.filter((particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual));
|
|
199
|
+
this.hasManualParticles = this.particles.length > 0;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.particles = [];
|
|
203
|
+
}
|
|
197
204
|
for (let i = 0; i < particleCount; i++)
|
|
198
|
-
this
|
|
205
|
+
this.#createParticle();
|
|
199
206
|
}
|
|
200
|
-
/** @public Adjust particle array length to match `
|
|
207
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
201
208
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
202
209
|
const particleCount = this.#targetParticleCount();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
if (this.hasManualParticles) {
|
|
211
|
+
const pruned = [];
|
|
212
|
+
let autoCount = 0;
|
|
213
|
+
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
214
|
+
// Only count automatic particles towards `particledCount`
|
|
215
|
+
for (const particle of this.particles) {
|
|
216
|
+
if (particle.isManual) {
|
|
217
|
+
pruned.push(particle);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (autoCount >= particleCount)
|
|
221
|
+
continue;
|
|
222
|
+
pruned.push(particle);
|
|
223
|
+
autoCount++;
|
|
224
|
+
}
|
|
225
|
+
this.particles = pruned;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
this.particles = this.particles.slice(0, particleCount);
|
|
229
|
+
}
|
|
230
|
+
// Only necessary after resize
|
|
231
|
+
if (updateBounds) {
|
|
232
|
+
for (const particle of this.particles) {
|
|
233
|
+
this.#updateParticleBounds(particle);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (let i = this.particles.length; i < particleCount; i++)
|
|
237
|
+
this.#createParticle();
|
|
238
|
+
}
|
|
239
|
+
/** @private Create a random new particle */
|
|
240
|
+
#createParticle() {
|
|
241
|
+
const posX = prng() * this.width;
|
|
242
|
+
const posY = prng() * this.height;
|
|
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);
|
|
208
244
|
}
|
|
209
245
|
/** @public Create a new particle with optional parameters */
|
|
210
|
-
createParticle(posX, posY, dir, speed, size) {
|
|
211
|
-
posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
|
|
212
|
-
posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
|
|
246
|
+
createParticle(posX, posY, dir, speed, size, isManual = true) {
|
|
213
247
|
const particle = {
|
|
214
248
|
posX, // Logical position in pixels
|
|
215
249
|
posY, // Logical position in pixels
|
|
@@ -219,14 +253,16 @@ class CanvasParticles {
|
|
|
219
253
|
velY: 0, // Vertical speed in pixels per update
|
|
220
254
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
221
255
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
222
|
-
dir: dir
|
|
223
|
-
speed: speed
|
|
224
|
-
size: size
|
|
256
|
+
dir: dir, // Direction in radians
|
|
257
|
+
speed: speed, // Velocity in pixels per update
|
|
258
|
+
size: size, // Ray in pixels of the particle
|
|
225
259
|
gridPos: { x: 1, y: 1 },
|
|
226
260
|
isVisible: false,
|
|
261
|
+
isManual,
|
|
227
262
|
};
|
|
228
263
|
this.#updateParticleBounds(particle);
|
|
229
264
|
this.particles.push(particle);
|
|
265
|
+
this.hasManualParticles = true;
|
|
230
266
|
}
|
|
231
267
|
/** @private Update the visible bounds of a particle */
|
|
232
268
|
#updateParticleBounds(particle) {
|
|
@@ -240,14 +276,12 @@ class CanvasParticles {
|
|
|
240
276
|
}
|
|
241
277
|
/* @public Randomize speed and size of all particles based on current options */
|
|
242
278
|
updateParticles() {
|
|
243
|
-
const particles = this.particles;
|
|
244
|
-
const len = particles.length;
|
|
245
279
|
const relSpeed = this.option.particles.relSpeed;
|
|
246
280
|
const relSize = this.option.particles.relSize;
|
|
247
|
-
for (
|
|
248
|
-
const particle = particles[i];
|
|
281
|
+
for (const particle of this.particles) {
|
|
249
282
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
250
283
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
284
|
+
this.#updateParticleBounds(particle); // because size changed
|
|
251
285
|
}
|
|
252
286
|
}
|
|
253
287
|
/** @private Apply gravity forces between particles */
|
|
@@ -393,7 +427,7 @@ class CanvasParticles {
|
|
|
393
427
|
// Visible if either particle is in the center
|
|
394
428
|
if (particleA.isVisible || particleB.isVisible)
|
|
395
429
|
return true;
|
|
396
|
-
// 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
|
|
397
431
|
return !((particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
398
432
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1));
|
|
399
433
|
}
|
|
@@ -487,7 +521,7 @@ class CanvasParticles {
|
|
|
487
521
|
this.ctx.strokeStyle = this.color.hex;
|
|
488
522
|
this.ctx.lineWidth = 1;
|
|
489
523
|
this.#renderParticles();
|
|
490
|
-
if (this.
|
|
524
|
+
if (this.option.particles.drawLines)
|
|
491
525
|
this.#renderConnections();
|
|
492
526
|
}
|
|
493
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.0";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({MANUAL:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static defaultIfNaN=(t,i)=>isNaN(+t)?i:+t;static parseNumericOption=(t,i,s,n)=>{if(null==i)return s;const{min:o=-1/0,max:a=1/0}=n??{};return isFinite(o)&&i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):isFinite(a)&&i>a&&console.warn(new RangeError(`option.${t} was clamped to ${a} as ${i} is too high`)),e.defaultIfNaN(Math.min(Math.max(i??s,o),a),s)};canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2;const s=this.option.particles.generationType;s!==e.generationType.MANUAL&&(s===e.generationType.NEW||0===this.particles.length?this.newParticles():s===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#t()}#i(){let t=Math.round(this.option.particles.ppm*this.width*this.height/1e6);if(t=Math.min(this.option.particles.max,t),!isFinite(t))throw new RangeError("particleCount must be finite");return 0|t}newParticles(){const t=this.#i();this.particles=[];for(let i=0;i<t;i++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();for(this.particles=this.particles.slice(0,i),t&&this.particles.forEach(t=>this.#e(t));i>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n??i()*t,speed:o??(.5+.5*i())*this.option.particles.relSpeed,size:a??(.5+2*Math.pow(i(),5))*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}updateParticles(){const t=this.particles,e=t.length,s=this.option.particles.relSpeed,n=this.option.particles.relSize;for(let o=0;o<e;o++){const e=t[o];e.speed=(.5+.5*i())*s,e.size=(.5+2*Math.pow(i(),5))*n}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particles,n=s.length,o=this.option.particles.connectDist,a=o*this.option.gravity.repulsive*t,r=o*this.option.gravity.pulling*t,c=(o/2)**2,h=o**2/256;for(let t=0;t<n;t++){const i=s[t];for(let o=t+1;o<n;o++){const t=s[o],n=i.posX-t.posX,l=i.posY-t.posY,p=n*n+l*l;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-l,-n),d=1/(p+h);const f=Math.cos(u),g=Math.sin(u);if(p<c){m=d*a;const e=f*m,s=g*m;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;m=d*r;const v=f*m,y=g*m;i.velX+=v,i.velY+=y,t.velX-=v,t.velY-=y}}}#n(i){const s=this.particles,n=s.length,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,m=this.option.mouse.distRatio,f=this.option.mouse.interactionType===e.interactionType.NONE,g=this.option.mouse.interactionType===e.interactionType.MOVE,v=1-Math.pow(.75,i);for(let e=0;e<n;e++){const n=s[e];n.dir+=2*(Math.random()-.5)*p*i,n.dir%=t;const y=Math.sin(n.dir)*n.speed,x=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(x+n.velY)*i,n.posX%=o,n.posX<0&&(n.posX+=o),n.posY%=a,n.posY<0&&(n.posY+=a),n.velX*=Math.pow(u,i),n.velY*=Math.pow(u,i);const M=n.posX+r-h,b=n.posY+c-l;if(!f){const t=d/Math.hypot(M,b);m<t?(n.offX+=(t*M-M-n.offX)*v,n.offY+=(t*b-b-n.offY)*v):(n.offX-=n.offX*v,n.offY-=n.offY*v)}n.x=n.posX+n.offX,n.y=n.posY+n.offY,g&&(n.posX=n.x,n.posY=n.y),n.x+=r,n.y+=c,this.#o(n),n.isVisible=1===n.gridPos.x&&1===n.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particles,e=i.length,s=this.ctx;for(let n=0;n<e;n++){const e=i[n];e.isVisible&&(e.size>1?(s.beginPath(),s.arc(e.x,e.y,e.size,0,t),s.fill(),s.closePath()):s.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}}#c(){const t=this.particles,i=t.length,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<i;s++){const p=t[s];let u=0;for(let d=s+1;d<i;d++){const i=t[d];if(!a&&!this.#a(p,i))continue;const s=p.x-i.x,m=p.y-i.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=h/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(i.x,i.y),e.stroke()):l.push([p.x,p.y,i.x,i.y]),(u+=f)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.options.particles.drawLines&&this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#s(i),this.#n(i),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:~~i("mouse.interactionType",t.mouse?.interactionType,e.interactionType.MOVE,{min:0,max:2}),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3,{min:0}),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3,{min:0})},particles:{generationType:~~i("particles.generationType",t.particles?.generationType,e.generationType.MATCH,{min:0,max:2}),drawLines:!!(t.particles?.drawLines??1),color:t.particles?.color??"black",ppm:~~i("particles.ppm",t.particles?.ppm,100),max:Math.round(i("particles.max",t.particles?.max,1/0,{min:0})),maxWork:Math.round(i("particles.maxWork",t.particles?.maxWork,1/0,{min:0})),connectDist:~~i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:0}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0,{min:0}),pulling:i("gravity.pulling",t.gravity?.pulling,0,{min:0}),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){const i=e.parseNumericOption("mouse.connectDistMult",t,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*i}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
|
|
1
|
+
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.3.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,9 +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.
|
|
32
|
+
"@types/node": "^24.10.6",
|
|
33
|
+
"pnpm": "^10.28.0",
|
|
33
34
|
"prettier": "^3.7.4",
|
|
34
|
-
"rollup": "^4.
|
|
35
|
+
"rollup": "^4.55.1",
|
|
35
36
|
"rollup-plugin-delete": "^3.0.2",
|
|
36
37
|
"tslib": "^2.8.1",
|
|
37
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
|
{
|
|
@@ -100,9 +100,9 @@ export default class CanvasParticles {
|
|
|
100
100
|
|
|
101
101
|
const { min = -Infinity, max = Infinity } = clamp ?? {}
|
|
102
102
|
|
|
103
|
-
if (
|
|
103
|
+
if (value < min) {
|
|
104
104
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
|
|
105
|
-
} else if (
|
|
105
|
+
} else if (value > max) {
|
|
106
106
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -117,6 +117,7 @@ export default class CanvasParticles {
|
|
|
117
117
|
private lastAnimationFrame: number = 0
|
|
118
118
|
|
|
119
119
|
particles: Particle[] = []
|
|
120
|
+
hasManualParticles = false // set to true once @public createParticle() is used
|
|
120
121
|
private clientX: number = Infinity
|
|
121
122
|
private clientY: number = Infinity
|
|
122
123
|
mouseX: number = Infinity
|
|
@@ -225,7 +226,7 @@ export default class CanvasParticles {
|
|
|
225
226
|
if (this.isAnimating) this.#render()
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
/** @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`. */
|
|
229
230
|
#targetParticleCount(): number {
|
|
230
231
|
// Amount of particles to be created
|
|
231
232
|
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000)
|
|
@@ -236,28 +237,73 @@ export default class CanvasParticles {
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
/** @public Remove existing particles and generate new ones */
|
|
239
|
-
newParticles() {
|
|
240
|
+
newParticles({ keepAuto = false, keepManual = true } = {}) {
|
|
240
241
|
const particleCount = this.#targetParticleCount()
|
|
241
242
|
|
|
242
|
-
this.
|
|
243
|
-
|
|
243
|
+
if (this.hasManualParticles && (keepAuto || keepManual)) {
|
|
244
|
+
this.particles = this.particles.filter(
|
|
245
|
+
(particle) => (keepAuto && !particle.isManual) || (keepManual && particle.isManual)
|
|
246
|
+
)
|
|
247
|
+
this.hasManualParticles = this.particles.length > 0
|
|
248
|
+
} else {
|
|
249
|
+
this.particles = []
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < particleCount; i++) this.#createParticle()
|
|
244
253
|
}
|
|
245
254
|
|
|
246
|
-
/** @public Adjust particle array length to match `
|
|
255
|
+
/** @public Adjust particle array length to match `option.particles.ppm` */
|
|
247
256
|
matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
|
|
248
257
|
const particleCount = this.#targetParticleCount()
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
if (this.hasManualParticles) {
|
|
260
|
+
const pruned: Particle[] = []
|
|
261
|
+
let autoCount = 0
|
|
262
|
+
|
|
263
|
+
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
264
|
+
// Only count automatic particles towards `particledCount`
|
|
265
|
+
for (const particle of this.particles) {
|
|
266
|
+
if (particle.isManual) {
|
|
267
|
+
pruned.push(particle)
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (autoCount >= particleCount) continue
|
|
272
|
+
pruned.push(particle)
|
|
273
|
+
autoCount++
|
|
274
|
+
}
|
|
275
|
+
this.particles = pruned
|
|
276
|
+
} else {
|
|
277
|
+
this.particles = this.particles.slice(0, particleCount)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Only necessary after resize
|
|
281
|
+
if (updateBounds) {
|
|
282
|
+
for (const particle of this.particles) {
|
|
283
|
+
this.#updateParticleBounds(particle)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
252
286
|
|
|
253
|
-
|
|
287
|
+
for (let i = this.particles.length; i < particleCount; i++) this.#createParticle()
|
|
254
288
|
}
|
|
255
289
|
|
|
256
|
-
/** @
|
|
257
|
-
createParticle(
|
|
258
|
-
posX =
|
|
259
|
-
posY =
|
|
290
|
+
/** @private Create a random new particle */
|
|
291
|
+
#createParticle() {
|
|
292
|
+
const posX = prng() * this.width
|
|
293
|
+
const posY = prng() * this.height
|
|
294
|
+
|
|
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
|
+
)
|
|
303
|
+
}
|
|
260
304
|
|
|
305
|
+
/** @public Create a new particle with optional parameters */
|
|
306
|
+
createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual = true) {
|
|
261
307
|
const particle: Omit<Particle, 'bounds'> = {
|
|
262
308
|
posX, // Logical position in pixels
|
|
263
309
|
posY, // Logical position in pixels
|
|
@@ -267,18 +313,22 @@ export default class CanvasParticles {
|
|
|
267
313
|
velY: 0, // Vertical speed in pixels per update
|
|
268
314
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
269
315
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
270
|
-
dir: dir
|
|
271
|
-
speed: speed
|
|
272
|
-
size: size
|
|
316
|
+
dir: dir, // Direction in radians
|
|
317
|
+
speed: speed, // Velocity in pixels per update
|
|
318
|
+
size: size, // Ray in pixels of the particle
|
|
273
319
|
gridPos: { x: 1, y: 1 },
|
|
274
320
|
isVisible: false,
|
|
321
|
+
isManual,
|
|
275
322
|
}
|
|
276
323
|
this.#updateParticleBounds(particle)
|
|
277
|
-
this.particles.push(particle
|
|
324
|
+
this.particles.push(particle)
|
|
325
|
+
this.hasManualParticles = true
|
|
278
326
|
}
|
|
279
327
|
|
|
280
328
|
/** @private Update the visible bounds of a particle */
|
|
281
|
-
#updateParticleBounds(
|
|
329
|
+
#updateParticleBounds(
|
|
330
|
+
particle: Omit<Particle, 'bounds'> & Partial<Pick<Particle, 'bounds'>>
|
|
331
|
+
): asserts particle is Particle {
|
|
282
332
|
// The particle is considered visible within these bounds
|
|
283
333
|
particle.bounds = {
|
|
284
334
|
top: -particle.size,
|
|
@@ -290,15 +340,13 @@ export default class CanvasParticles {
|
|
|
290
340
|
|
|
291
341
|
/* @public Randomize speed and size of all particles based on current options */
|
|
292
342
|
updateParticles() {
|
|
293
|
-
const particles = this.particles
|
|
294
|
-
const len = particles.length
|
|
295
343
|
const relSpeed = this.option.particles.relSpeed
|
|
296
344
|
const relSize = this.option.particles.relSize
|
|
297
345
|
|
|
298
|
-
for (
|
|
299
|
-
const particle = particles[i]
|
|
346
|
+
for (const particle of this.particles) {
|
|
300
347
|
particle.speed = (0.5 + prng() * 0.5) * relSpeed
|
|
301
348
|
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize
|
|
349
|
+
this.#updateParticleBounds(particle) // because size changed
|
|
302
350
|
}
|
|
303
351
|
}
|
|
304
352
|
|
|
@@ -466,7 +514,7 @@ export default class CanvasParticles {
|
|
|
466
514
|
// Visible if either particle is in the center
|
|
467
515
|
if (particleA.isVisible || particleB.isVisible) return true
|
|
468
516
|
|
|
469
|
-
// 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
|
|
470
518
|
return !(
|
|
471
519
|
(particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
|
|
472
520
|
(particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
|
|
@@ -576,7 +624,7 @@ export default class CanvasParticles {
|
|
|
576
624
|
this.ctx.lineWidth = 1
|
|
577
625
|
|
|
578
626
|
this.#renderParticles()
|
|
579
|
-
if (this.
|
|
627
|
+
if (this.option.particles.drawLines) this.#renderConnections()
|
|
580
628
|
}
|
|
581
629
|
|
|
582
630
|
/** @private Main animation loop that updates and renders the particles */
|