canvasparticles-js 4.1.2 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +45 -29
- package/dist/index.d.ts +7 -7
- package/dist/index.mjs +45 -29
- package/dist/index.umd.js +1 -1
- package/dist/types/options.d.ts +0 -1
- package/package.json +1 -1
- package/src/index.ts +52 -32
- package/src/types/options.ts +0 -1
package/dist/index.cjs
CHANGED
|
@@ -2,8 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
// Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
|
|
4
4
|
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
5
|
+
const TWO_PI = 2 * Math.PI;
|
|
6
|
+
/** Extremely fast, simple 32‑bit PRNG */
|
|
7
|
+
function Mulberry32(seed) {
|
|
8
|
+
let state = seed >>> 0;
|
|
9
|
+
return {
|
|
10
|
+
next() {
|
|
11
|
+
let t = (state + 0x6d2b79f5) | 0;
|
|
12
|
+
state = t;
|
|
13
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
14
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
15
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Mulberry32 is ±388% faster than Math.random()
|
|
20
|
+
// Benchmark: https://jsbm.dev/muLCWR9RJCbmy
|
|
21
|
+
// Spectral test: /demo/mulberry32.html
|
|
22
|
+
const prng = Mulberry32(Math.random() * 2 ** 32).next;
|
|
5
23
|
class CanvasParticles {
|
|
6
|
-
static version = "4.1.
|
|
24
|
+
static version = "4.1.4";
|
|
7
25
|
/** Defines mouse interaction types with the particles */
|
|
8
26
|
static interactionType = Object.freeze({
|
|
9
27
|
NONE: 0, // No mouse interaction
|
|
@@ -38,9 +56,11 @@ class CanvasParticles {
|
|
|
38
56
|
});
|
|
39
57
|
canvas;
|
|
40
58
|
ctx;
|
|
59
|
+
lastAnimationFrame = 0;
|
|
41
60
|
enableAnimating = false;
|
|
42
61
|
isAnimating = false;
|
|
43
62
|
particles = [];
|
|
63
|
+
particleCount = 0;
|
|
44
64
|
clientX = Infinity;
|
|
45
65
|
clientY = Infinity;
|
|
46
66
|
mouseX = Infinity;
|
|
@@ -49,10 +69,8 @@ class CanvasParticles {
|
|
|
49
69
|
height;
|
|
50
70
|
offX;
|
|
51
71
|
offY;
|
|
52
|
-
updateCount;
|
|
53
|
-
particleCount;
|
|
54
72
|
option;
|
|
55
|
-
color
|
|
73
|
+
color;
|
|
56
74
|
/**
|
|
57
75
|
* Initialize a CanvasParticles instance
|
|
58
76
|
* @param selector - Canvas element or CSS selector
|
|
@@ -87,8 +105,8 @@ class CanvasParticles {
|
|
|
87
105
|
this.handleScroll = this.handleScroll.bind(this);
|
|
88
106
|
this.updateCanvasRect();
|
|
89
107
|
this.resizeCanvas();
|
|
90
|
-
window.addEventListener('mousemove', this.handleMouseMove);
|
|
91
|
-
window.addEventListener('scroll', this.handleScroll);
|
|
108
|
+
window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
|
|
109
|
+
window.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
92
110
|
}
|
|
93
111
|
/* @public Update the canvas bounding rectangle and mouse position relative to it */
|
|
94
112
|
updateCanvasRect() {
|
|
@@ -125,7 +143,6 @@ class CanvasParticles {
|
|
|
125
143
|
// Hide the mouse when resizing because it must be outside the viewport to do so
|
|
126
144
|
this.mouseX = Infinity;
|
|
127
145
|
this.mouseY = Infinity;
|
|
128
|
-
this.updateCount = Infinity;
|
|
129
146
|
this.width = Math.max(width + this.option.particles.connectDist * 2, 1);
|
|
130
147
|
this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
|
|
131
148
|
this.offX = (width - this.width) / 2;
|
|
@@ -163,8 +180,8 @@ class CanvasParticles {
|
|
|
163
180
|
}
|
|
164
181
|
/** @public Create a new particle with optional parameters */
|
|
165
182
|
createParticle(posX, posY, dir, speed, size) {
|
|
166
|
-
posX = typeof posX === 'number' ? posX - this.offX :
|
|
167
|
-
posY = typeof posY === 'number' ? posY - this.offY :
|
|
183
|
+
posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
|
|
184
|
+
posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
|
|
168
185
|
const particle = {
|
|
169
186
|
posX, // Logical position in pixels
|
|
170
187
|
posY, // Logical position in pixels
|
|
@@ -174,9 +191,9 @@ class CanvasParticles {
|
|
|
174
191
|
velY: 0, // Vertical speed in pixels per update
|
|
175
192
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
176
193
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
177
|
-
dir: dir ||
|
|
178
|
-
speed: speed || (0.5 +
|
|
179
|
-
size: size || (0.5 +
|
|
194
|
+
dir: dir || prng() * TWO_PI, // Direction in radians
|
|
195
|
+
speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
196
|
+
size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
180
197
|
gridPos: { x: 1, y: 1 },
|
|
181
198
|
isVisible: false,
|
|
182
199
|
};
|
|
@@ -193,7 +210,7 @@ class CanvasParticles {
|
|
|
193
210
|
left: -particle.size,
|
|
194
211
|
};
|
|
195
212
|
}
|
|
196
|
-
/** @private Apply gravity forces between particles
|
|
213
|
+
/** @private Apply gravity forces between particles */
|
|
197
214
|
#updateGravity() {
|
|
198
215
|
const isRepulsiveEnabled = this.option.gravity.repulsive !== 0;
|
|
199
216
|
const isPullingEnabled = this.option.gravity.pulling !== 0;
|
|
@@ -243,13 +260,12 @@ class CanvasParticles {
|
|
|
243
260
|
}
|
|
244
261
|
}
|
|
245
262
|
}
|
|
246
|
-
/** @private Update positions, directions, and visibility of all particles
|
|
263
|
+
/** @private Update positions, directions, and visibility of all particles */
|
|
247
264
|
#updateParticles() {
|
|
248
265
|
for (let particle of this.particles) {
|
|
249
266
|
// Randomly perturb direction
|
|
250
267
|
particle.dir =
|
|
251
|
-
(particle.dir +
|
|
252
|
-
(2 * Math.PI);
|
|
268
|
+
(particle.dir + prng() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) % TWO_PI;
|
|
253
269
|
particle.velX *= this.option.gravity.friction;
|
|
254
270
|
particle.velY *= this.option.gravity.friction;
|
|
255
271
|
particle.posX =
|
|
@@ -326,7 +342,7 @@ class CanvasParticles {
|
|
|
326
342
|
if (particle.size > 1) {
|
|
327
343
|
// Draw circle
|
|
328
344
|
this.ctx.beginPath();
|
|
329
|
-
this.ctx.arc(particle.x, particle.y, particle.size, 0,
|
|
345
|
+
this.ctx.arc(particle.x, particle.y, particle.size, 0, TWO_PI);
|
|
330
346
|
this.ctx.fill();
|
|
331
347
|
this.ctx.closePath();
|
|
332
348
|
}
|
|
@@ -393,12 +409,9 @@ class CanvasParticles {
|
|
|
393
409
|
if (!this.isAnimating)
|
|
394
410
|
return;
|
|
395
411
|
requestAnimationFrame(() => this.#animation());
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.#updateParticles();
|
|
400
|
-
this.#render();
|
|
401
|
-
}
|
|
412
|
+
this.#updateGravity();
|
|
413
|
+
this.#updateParticles();
|
|
414
|
+
this.#render();
|
|
402
415
|
}
|
|
403
416
|
/** @public Start the particle animation if it was not running before */
|
|
404
417
|
start({ auto = false } = {}) {
|
|
@@ -419,7 +432,7 @@ class CanvasParticles {
|
|
|
419
432
|
this.enableAnimating = false;
|
|
420
433
|
this.isAnimating = false;
|
|
421
434
|
if (clear !== false)
|
|
422
|
-
this.canvas.width
|
|
435
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
423
436
|
return true;
|
|
424
437
|
}
|
|
425
438
|
/** @public Gracefully destroy the instance and remove the canvas element */
|
|
@@ -442,7 +455,6 @@ class CanvasParticles {
|
|
|
442
455
|
// Format and parse all options
|
|
443
456
|
this.option = {
|
|
444
457
|
background: options.background ?? false,
|
|
445
|
-
framesPerUpdate: parseNumericOption(options.framesPerUpdate, 1, { min: 1 }),
|
|
446
458
|
animation: {
|
|
447
459
|
startOnEnter: !!(options.animation?.startOnEnter ?? true),
|
|
448
460
|
stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
|
|
@@ -495,18 +507,22 @@ class CanvasParticles {
|
|
|
495
507
|
this.ctx.fillStyle = color;
|
|
496
508
|
// Check if `ctx.fillStyle` is in hex format ("#RRGGBB")
|
|
497
509
|
if (String(this.ctx.fillStyle)[0] === '#') {
|
|
498
|
-
this.color
|
|
499
|
-
|
|
510
|
+
this.color = {
|
|
511
|
+
hex: String(this.ctx.fillStyle),
|
|
512
|
+
alpha: 1.0,
|
|
513
|
+
};
|
|
500
514
|
}
|
|
501
515
|
else {
|
|
502
516
|
// JavaScript's `ctx.fillStyle` causes the color to otherwise end up in in rgba format ("rgba(136, 244, 255, 0.25)")
|
|
503
517
|
// Extract the alpha value from the rgba string
|
|
504
518
|
let alpha = String(this.ctx.fillStyle).split(',').at(-1); // ' 0.25)'
|
|
505
519
|
alpha = alpha?.slice(1, -1) ?? '1'; // '0.25'
|
|
506
|
-
this.color.alpha = isNaN(+alpha) ? 1 : +alpha; // 0.25 or 1
|
|
507
520
|
// Extracts e.g. 136, 244 and 255 from rgba(136, 244, 255, 0.25) and converts it to '#rrggbb'
|
|
508
521
|
this.ctx.fillStyle = String(this.ctx.fillStyle).split(',').slice(0, -1).join(',') + ', 1)';
|
|
509
|
-
this.color
|
|
522
|
+
this.color = {
|
|
523
|
+
hex: String(this.ctx.fillStyle),
|
|
524
|
+
alpha: isNaN(+alpha) ? 1 : +alpha,
|
|
525
|
+
}; // 0.25 or 1
|
|
510
526
|
}
|
|
511
527
|
}
|
|
512
528
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { CanvasParticlesCanvas, Particle } from './types';
|
|
1
|
+
import type { CanvasParticlesCanvas, Particle, ContextColor } from './types';
|
|
2
2
|
import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options';
|
|
3
3
|
export default class CanvasParticles {
|
|
4
4
|
#private;
|
|
5
|
-
static version: string;
|
|
5
|
+
static readonly version: string;
|
|
6
6
|
/** Defines mouse interaction types with the particles */
|
|
7
7
|
static interactionType: Readonly<{
|
|
8
8
|
NONE: 0;
|
|
@@ -10,13 +10,15 @@ export default class CanvasParticles {
|
|
|
10
10
|
MOVE: 2;
|
|
11
11
|
}>;
|
|
12
12
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
13
|
-
static canvasIntersectionObserver: IntersectionObserver;
|
|
14
|
-
static canvasResizeObserver: ResizeObserver;
|
|
13
|
+
static readonly canvasIntersectionObserver: IntersectionObserver;
|
|
14
|
+
static readonly canvasResizeObserver: ResizeObserver;
|
|
15
15
|
canvas: CanvasParticlesCanvas;
|
|
16
16
|
private ctx;
|
|
17
|
+
private lastAnimationFrame;
|
|
17
18
|
enableAnimating: boolean;
|
|
18
19
|
isAnimating: boolean;
|
|
19
20
|
particles: Particle[];
|
|
21
|
+
particleCount: number;
|
|
20
22
|
private clientX;
|
|
21
23
|
private clientY;
|
|
22
24
|
mouseX: number;
|
|
@@ -25,10 +27,8 @@ export default class CanvasParticles {
|
|
|
25
27
|
height: number;
|
|
26
28
|
private offX;
|
|
27
29
|
private offY;
|
|
28
|
-
private updateCount;
|
|
29
|
-
particleCount: number;
|
|
30
30
|
option: CanvasParticlesOptions;
|
|
31
|
-
|
|
31
|
+
color: ContextColor;
|
|
32
32
|
/**
|
|
33
33
|
* Initialize a CanvasParticles instance
|
|
34
34
|
* @param selector - Canvas element or CSS selector
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
// Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
|
|
2
2
|
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
3
|
+
const TWO_PI = 2 * Math.PI;
|
|
4
|
+
/** Extremely fast, simple 32‑bit PRNG */
|
|
5
|
+
function Mulberry32(seed) {
|
|
6
|
+
let state = seed >>> 0;
|
|
7
|
+
return {
|
|
8
|
+
next() {
|
|
9
|
+
let t = (state + 0x6d2b79f5) | 0;
|
|
10
|
+
state = t;
|
|
11
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
12
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
13
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Mulberry32 is ±388% faster than Math.random()
|
|
18
|
+
// Benchmark: https://jsbm.dev/muLCWR9RJCbmy
|
|
19
|
+
// Spectral test: /demo/mulberry32.html
|
|
20
|
+
const prng = Mulberry32(Math.random() * 2 ** 32).next;
|
|
3
21
|
class CanvasParticles {
|
|
4
|
-
static version = "4.1.
|
|
22
|
+
static version = "4.1.4";
|
|
5
23
|
/** Defines mouse interaction types with the particles */
|
|
6
24
|
static interactionType = Object.freeze({
|
|
7
25
|
NONE: 0, // No mouse interaction
|
|
@@ -36,9 +54,11 @@ class CanvasParticles {
|
|
|
36
54
|
});
|
|
37
55
|
canvas;
|
|
38
56
|
ctx;
|
|
57
|
+
lastAnimationFrame = 0;
|
|
39
58
|
enableAnimating = false;
|
|
40
59
|
isAnimating = false;
|
|
41
60
|
particles = [];
|
|
61
|
+
particleCount = 0;
|
|
42
62
|
clientX = Infinity;
|
|
43
63
|
clientY = Infinity;
|
|
44
64
|
mouseX = Infinity;
|
|
@@ -47,10 +67,8 @@ class CanvasParticles {
|
|
|
47
67
|
height;
|
|
48
68
|
offX;
|
|
49
69
|
offY;
|
|
50
|
-
updateCount;
|
|
51
|
-
particleCount;
|
|
52
70
|
option;
|
|
53
|
-
color
|
|
71
|
+
color;
|
|
54
72
|
/**
|
|
55
73
|
* Initialize a CanvasParticles instance
|
|
56
74
|
* @param selector - Canvas element or CSS selector
|
|
@@ -85,8 +103,8 @@ class CanvasParticles {
|
|
|
85
103
|
this.handleScroll = this.handleScroll.bind(this);
|
|
86
104
|
this.updateCanvasRect();
|
|
87
105
|
this.resizeCanvas();
|
|
88
|
-
window.addEventListener('mousemove', this.handleMouseMove);
|
|
89
|
-
window.addEventListener('scroll', this.handleScroll);
|
|
106
|
+
window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
|
|
107
|
+
window.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
90
108
|
}
|
|
91
109
|
/* @public Update the canvas bounding rectangle and mouse position relative to it */
|
|
92
110
|
updateCanvasRect() {
|
|
@@ -123,7 +141,6 @@ class CanvasParticles {
|
|
|
123
141
|
// Hide the mouse when resizing because it must be outside the viewport to do so
|
|
124
142
|
this.mouseX = Infinity;
|
|
125
143
|
this.mouseY = Infinity;
|
|
126
|
-
this.updateCount = Infinity;
|
|
127
144
|
this.width = Math.max(width + this.option.particles.connectDist * 2, 1);
|
|
128
145
|
this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
|
|
129
146
|
this.offX = (width - this.width) / 2;
|
|
@@ -161,8 +178,8 @@ class CanvasParticles {
|
|
|
161
178
|
}
|
|
162
179
|
/** @public Create a new particle with optional parameters */
|
|
163
180
|
createParticle(posX, posY, dir, speed, size) {
|
|
164
|
-
posX = typeof posX === 'number' ? posX - this.offX :
|
|
165
|
-
posY = typeof posY === 'number' ? posY - this.offY :
|
|
181
|
+
posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width;
|
|
182
|
+
posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height;
|
|
166
183
|
const particle = {
|
|
167
184
|
posX, // Logical position in pixels
|
|
168
185
|
posY, // Logical position in pixels
|
|
@@ -172,9 +189,9 @@ class CanvasParticles {
|
|
|
172
189
|
velY: 0, // Vertical speed in pixels per update
|
|
173
190
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
174
191
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
175
|
-
dir: dir ||
|
|
176
|
-
speed: speed || (0.5 +
|
|
177
|
-
size: size || (0.5 +
|
|
192
|
+
dir: dir || prng() * TWO_PI, // Direction in radians
|
|
193
|
+
speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
194
|
+
size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
178
195
|
gridPos: { x: 1, y: 1 },
|
|
179
196
|
isVisible: false,
|
|
180
197
|
};
|
|
@@ -191,7 +208,7 @@ class CanvasParticles {
|
|
|
191
208
|
left: -particle.size,
|
|
192
209
|
};
|
|
193
210
|
}
|
|
194
|
-
/** @private Apply gravity forces between particles
|
|
211
|
+
/** @private Apply gravity forces between particles */
|
|
195
212
|
#updateGravity() {
|
|
196
213
|
const isRepulsiveEnabled = this.option.gravity.repulsive !== 0;
|
|
197
214
|
const isPullingEnabled = this.option.gravity.pulling !== 0;
|
|
@@ -241,13 +258,12 @@ class CanvasParticles {
|
|
|
241
258
|
}
|
|
242
259
|
}
|
|
243
260
|
}
|
|
244
|
-
/** @private Update positions, directions, and visibility of all particles
|
|
261
|
+
/** @private Update positions, directions, and visibility of all particles */
|
|
245
262
|
#updateParticles() {
|
|
246
263
|
for (let particle of this.particles) {
|
|
247
264
|
// Randomly perturb direction
|
|
248
265
|
particle.dir =
|
|
249
|
-
(particle.dir +
|
|
250
|
-
(2 * Math.PI);
|
|
266
|
+
(particle.dir + prng() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) % TWO_PI;
|
|
251
267
|
particle.velX *= this.option.gravity.friction;
|
|
252
268
|
particle.velY *= this.option.gravity.friction;
|
|
253
269
|
particle.posX =
|
|
@@ -324,7 +340,7 @@ class CanvasParticles {
|
|
|
324
340
|
if (particle.size > 1) {
|
|
325
341
|
// Draw circle
|
|
326
342
|
this.ctx.beginPath();
|
|
327
|
-
this.ctx.arc(particle.x, particle.y, particle.size, 0,
|
|
343
|
+
this.ctx.arc(particle.x, particle.y, particle.size, 0, TWO_PI);
|
|
328
344
|
this.ctx.fill();
|
|
329
345
|
this.ctx.closePath();
|
|
330
346
|
}
|
|
@@ -391,12 +407,9 @@ class CanvasParticles {
|
|
|
391
407
|
if (!this.isAnimating)
|
|
392
408
|
return;
|
|
393
409
|
requestAnimationFrame(() => this.#animation());
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.#updateParticles();
|
|
398
|
-
this.#render();
|
|
399
|
-
}
|
|
410
|
+
this.#updateGravity();
|
|
411
|
+
this.#updateParticles();
|
|
412
|
+
this.#render();
|
|
400
413
|
}
|
|
401
414
|
/** @public Start the particle animation if it was not running before */
|
|
402
415
|
start({ auto = false } = {}) {
|
|
@@ -417,7 +430,7 @@ class CanvasParticles {
|
|
|
417
430
|
this.enableAnimating = false;
|
|
418
431
|
this.isAnimating = false;
|
|
419
432
|
if (clear !== false)
|
|
420
|
-
this.canvas.width
|
|
433
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
421
434
|
return true;
|
|
422
435
|
}
|
|
423
436
|
/** @public Gracefully destroy the instance and remove the canvas element */
|
|
@@ -440,7 +453,6 @@ class CanvasParticles {
|
|
|
440
453
|
// Format and parse all options
|
|
441
454
|
this.option = {
|
|
442
455
|
background: options.background ?? false,
|
|
443
|
-
framesPerUpdate: parseNumericOption(options.framesPerUpdate, 1, { min: 1 }),
|
|
444
456
|
animation: {
|
|
445
457
|
startOnEnter: !!(options.animation?.startOnEnter ?? true),
|
|
446
458
|
stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
|
|
@@ -493,18 +505,22 @@ class CanvasParticles {
|
|
|
493
505
|
this.ctx.fillStyle = color;
|
|
494
506
|
// Check if `ctx.fillStyle` is in hex format ("#RRGGBB")
|
|
495
507
|
if (String(this.ctx.fillStyle)[0] === '#') {
|
|
496
|
-
this.color
|
|
497
|
-
|
|
508
|
+
this.color = {
|
|
509
|
+
hex: String(this.ctx.fillStyle),
|
|
510
|
+
alpha: 1.0,
|
|
511
|
+
};
|
|
498
512
|
}
|
|
499
513
|
else {
|
|
500
514
|
// JavaScript's `ctx.fillStyle` causes the color to otherwise end up in in rgba format ("rgba(136, 244, 255, 0.25)")
|
|
501
515
|
// Extract the alpha value from the rgba string
|
|
502
516
|
let alpha = String(this.ctx.fillStyle).split(',').at(-1); // ' 0.25)'
|
|
503
517
|
alpha = alpha?.slice(1, -1) ?? '1'; // '0.25'
|
|
504
|
-
this.color.alpha = isNaN(+alpha) ? 1 : +alpha; // 0.25 or 1
|
|
505
518
|
// Extracts e.g. 136, 244 and 255 from rgba(136, 244, 255, 0.25) and converts it to '#rrggbb'
|
|
506
519
|
this.ctx.fillStyle = String(this.ctx.fillStyle).split(',').slice(0, -1).join(',') + ', 1)';
|
|
507
|
-
this.color
|
|
520
|
+
this.color = {
|
|
521
|
+
hex: String(this.ctx.fillStyle),
|
|
522
|
+
alpha: isNaN(+alpha) ? 1 : +alpha,
|
|
523
|
+
}; // 0.25 or 1
|
|
508
524
|
}
|
|
509
525
|
}
|
|
510
526
|
}
|
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";
|
|
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 s{static version="4.1.4";static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,s=t.instance;if(!s.options?.animation)return;(t.inViewbox=i.isIntersecting)?s.options.animation?.startOnEnter&&s.start({auto:!0}):s.options.animation?.stopOnLeave&&s.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}for(const i of t){i.target.instance.resizeCanvas()}});canvas;ctx;lastAnimationFrame=0;enableAnimating=!1;isAnimating=!1;particles=[];particleCount=0;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let e;if(t instanceof HTMLCanvasElement)e=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(e=document.querySelector(t),!(e instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=e,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,s.canvasIntersectionObserver.observe(this.canvas),s.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:s,height:e}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:s,height:e}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("number of particles must be finite. (options.particles.ppm)")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#s(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(s,e,n,o,a){const r={posX:s="number"==typeof s?s-this.offX:i()*this.width,posY:e="number"==typeof e?e-this.offY:i()*this.height,x:s,y:e,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#s(r),this.particles.push(r)}#s(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#e(){const t=0!==this.option.gravity.repulsive,i=0!==this.option.gravity.pulling;if(!t&&!i)return;const s=this.particleCount,e=this.option.particles.connectDist*this.option.gravity.repulsive,n=this.option.particles.connectDist*this.option.gravity.pulling,o=this.option.particles.connectDist/2,a=.1*this.option.particles.connectDist;for(let t=0;t<s;t++)for(let r=t+1;r<s;r++){const s=this.particles[t],h=this.particles[r],c=s.posX-h.posX,l=s.posY-h.posY,p=Math.sqrt(c*c+l*l);let u,d=1;if(p<o){u=Math.atan2(h.posY-s.posY,h.posX-s.posX),d=(1/p)**1.8;const t=Math.min(a,d*e),i=Math.cos(u)*t,n=Math.sin(u)*t;s.velX-=i,s.velY-=n,h.velX+=i,h.velY+=n}if(!i)continue;void 0===u&&(u=Math.atan2(h.posY-s.posY,h.posX-s.posX),d=(1/p)**1.8);const f=Math.min(a,d*n),v=Math.cos(u)*f,m=Math.sin(u)*f;s.velX+=v,s.velY+=m,h.velX-=v,h.velY-=m}}#n(){for(let e of this.particles){e.dir=(e.dir+i()*this.option.particles.rotationSpeed*2-this.option.particles.rotationSpeed)%t,e.velX*=this.option.gravity.friction,e.velY*=this.option.gravity.friction,e.posX=(e.posX+e.velX+Math.sin(e.dir)*e.speed%this.width+this.width)%this.width,e.posY=(e.posY+e.velY+Math.cos(e.dir)*e.speed%this.height+this.height)%this.height;const n=e.posX+this.offX-this.mouseX,o=e.posY+this.offY-this.mouseY;if(this.option.mouse.interactionType!==s.interactionType.NONE){const t=this.option.mouse.connectDist/Math.hypot(n,o);this.option.mouse.distRatio<t?(e.offX+=(t*n-n-e.offX)/4,e.offY+=(t*o-o-e.offY)/4):(e.offX-=e.offX/4,e.offY-=e.offY/4)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,this.option.mouse.interactionType===s.interactionType.MOVE&&(e.posX=e.x,e.posY=e.y),e.x+=this.offX,e.y+=this.offY,e.gridPos=this.#o(e),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#o(t){return{x:+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),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(){for(let i of this.particles)i.isVisible&&(i.size>1?(this.ctx.beginPath(),this.ctx.arc(i.x,i.y,i.size,0,t),this.ctx.fill(),this.ctx.closePath()):this.ctx.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}#h(){const t=this.particleCount,i=this.option.particles.connectDist,s=i>=Math.min(this.canvas.width,this.canvas.height),e=i*this.option.particles.maxWork,n=this.color.alpha,o=this.color.alpha*i;for(let a=0;a<t;a++){let r=0;for(let h=a+1;h<t;h++){const t=this.particles[a],c=this.particles[h];if(!s&&!this.#a(t,c))continue;const l=t.x-c.x,p=t.y-c.y,u=Math.sqrt(l*l+p*p);if(!(u>i)&&(this.ctx.globalAlpha=u>i/2?o/u-n:n,this.ctx.beginPath(),this.ctx.moveTo(t.x,t.y),this.ctx.lineTo(c.x,c.y),this.ctx.stroke(),(r+=u)>=e))break}}}#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.#h()}#c(){this.isAnimating&&(requestAnimationFrame(()=>this.#c()),this.#e(),this.#n(),this.#t())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#c())),!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(),s.canvasIntersectionObserver.unobserve(this.canvas),s.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=(t,i,s)=>{const{min:e=-1/0,max:n=1/0}=s??{};return((t,i)=>isNaN(+t)?i:+t)(Math.min(Math.max(t??i,e),n),i)};this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:i(t.mouse?.interactionType,1),connectDistMult:i(t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i(t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i(t.particles?.ppm,100),max:i(t.particles?.max,500),maxWork:i(t.particles?.maxWork,1/0,{min:0}),connectDist:i(t.particles?.connectDistance,150,{min:1}),relSpeed:i(t.particles?.relSpeed,1,{min:0}),relSize:i(t.particles?.relSize,1,{min:1}),rotationSpeed:i(t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i(t.gravity?.repulsive,0),pulling:i(t.gravity?.pulling,0),friction:i(t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return s});
|
package/dist/types/options.d.ts
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,10 +4,32 @@
|
|
|
4
4
|
import type { CanvasParticlesCanvas, Particle, ParticleGridPos, ContextColor } from './types'
|
|
5
5
|
import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
|
|
6
6
|
|
|
7
|
+
const TWO_PI = 2 * Math.PI
|
|
8
|
+
|
|
9
|
+
/** Extremely fast, simple 32‑bit PRNG */
|
|
10
|
+
function Mulberry32(seed: number) {
|
|
11
|
+
let state = seed >>> 0
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
next() {
|
|
15
|
+
let t = (state + 0x6d2b79f5) | 0
|
|
16
|
+
state = t
|
|
17
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
18
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
19
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Mulberry32 is ±388% faster than Math.random()
|
|
25
|
+
// Benchmark: https://jsbm.dev/muLCWR9RJCbmy
|
|
26
|
+
// Spectral test: /demo/mulberry32.html
|
|
27
|
+
const prng = Mulberry32(Math.random() * 2 ** 32).next
|
|
28
|
+
|
|
7
29
|
declare const __VERSION__: string
|
|
8
30
|
|
|
9
31
|
export default class CanvasParticles {
|
|
10
|
-
static version = __VERSION__
|
|
32
|
+
static readonly version = __VERSION__
|
|
11
33
|
|
|
12
34
|
/** Defines mouse interaction types with the particles */
|
|
13
35
|
static interactionType = Object.freeze({
|
|
@@ -17,7 +39,7 @@ export default class CanvasParticles {
|
|
|
17
39
|
})
|
|
18
40
|
|
|
19
41
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
20
|
-
static canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
42
|
+
static readonly canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
21
43
|
for (const entry of entries) {
|
|
22
44
|
const canvas = entry.target as CanvasParticlesCanvas
|
|
23
45
|
const instance = canvas.instance // The CanvasParticles class instance bound to this canvas
|
|
@@ -30,7 +52,7 @@ export default class CanvasParticles {
|
|
|
30
52
|
}
|
|
31
53
|
})
|
|
32
54
|
|
|
33
|
-
static canvasResizeObserver = new ResizeObserver((entries) => {
|
|
55
|
+
static readonly canvasResizeObserver = new ResizeObserver((entries) => {
|
|
34
56
|
// Seperate for loops is very important to prevent huge forced reflow overhead
|
|
35
57
|
|
|
36
58
|
// First read all canvas rects at once
|
|
@@ -48,11 +70,13 @@ export default class CanvasParticles {
|
|
|
48
70
|
|
|
49
71
|
canvas: CanvasParticlesCanvas
|
|
50
72
|
private ctx: CanvasRenderingContext2D
|
|
73
|
+
private lastAnimationFrame: number = 0
|
|
51
74
|
|
|
52
75
|
enableAnimating: boolean = false
|
|
53
76
|
isAnimating: boolean = false
|
|
54
77
|
|
|
55
78
|
particles: Particle[] = []
|
|
79
|
+
particleCount: number = 0
|
|
56
80
|
|
|
57
81
|
private clientX: number = Infinity
|
|
58
82
|
private clientY: number = Infinity
|
|
@@ -62,10 +86,8 @@ export default class CanvasParticles {
|
|
|
62
86
|
height!: number
|
|
63
87
|
private offX!: number
|
|
64
88
|
private offY!: number
|
|
65
|
-
private updateCount!: number
|
|
66
|
-
particleCount!: number
|
|
67
89
|
option!: CanvasParticlesOptions
|
|
68
|
-
|
|
90
|
+
color!: ContextColor
|
|
69
91
|
|
|
70
92
|
/**
|
|
71
93
|
* Initialize a CanvasParticles instance
|
|
@@ -106,8 +128,8 @@ export default class CanvasParticles {
|
|
|
106
128
|
this.updateCanvasRect()
|
|
107
129
|
this.resizeCanvas()
|
|
108
130
|
|
|
109
|
-
window.addEventListener('mousemove', this.handleMouseMove)
|
|
110
|
-
window.addEventListener('scroll', this.handleScroll)
|
|
131
|
+
window.addEventListener('mousemove', this.handleMouseMove, { passive: true })
|
|
132
|
+
window.addEventListener('scroll', this.handleScroll, { passive: true })
|
|
111
133
|
}
|
|
112
134
|
|
|
113
135
|
/* @public Update the canvas bounding rectangle and mouse position relative to it */
|
|
@@ -149,7 +171,6 @@ export default class CanvasParticles {
|
|
|
149
171
|
this.mouseX = Infinity
|
|
150
172
|
this.mouseY = Infinity
|
|
151
173
|
|
|
152
|
-
this.updateCount = Infinity
|
|
153
174
|
this.width = Math.max(width + this.option.particles.connectDist * 2, 1)
|
|
154
175
|
this.height = Math.max(height + this.option.particles.connectDist * 2, 1)
|
|
155
176
|
this.offX = (width - this.width) / 2
|
|
@@ -190,8 +211,8 @@ export default class CanvasParticles {
|
|
|
190
211
|
|
|
191
212
|
/** @public Create a new particle with optional parameters */
|
|
192
213
|
createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number) {
|
|
193
|
-
posX = typeof posX === 'number' ? posX - this.offX :
|
|
194
|
-
posY = typeof posY === 'number' ? posY - this.offY :
|
|
214
|
+
posX = typeof posX === 'number' ? posX - this.offX : prng() * this.width
|
|
215
|
+
posY = typeof posY === 'number' ? posY - this.offY : prng() * this.height
|
|
195
216
|
|
|
196
217
|
const particle: Omit<Particle, 'bounds'> = {
|
|
197
218
|
posX, // Logical position in pixels
|
|
@@ -202,9 +223,9 @@ export default class CanvasParticles {
|
|
|
202
223
|
velY: 0, // Vertical speed in pixels per update
|
|
203
224
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
204
225
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
205
|
-
dir: dir ||
|
|
206
|
-
speed: speed || (0.5 +
|
|
207
|
-
size: size || (0.5 +
|
|
226
|
+
dir: dir || prng() * TWO_PI, // Direction in radians
|
|
227
|
+
speed: speed || (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
228
|
+
size: size || (0.5 + prng() ** 5 * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
208
229
|
gridPos: { x: 1, y: 1 },
|
|
209
230
|
isVisible: false,
|
|
210
231
|
}
|
|
@@ -223,7 +244,7 @@ export default class CanvasParticles {
|
|
|
223
244
|
}
|
|
224
245
|
}
|
|
225
246
|
|
|
226
|
-
/** @private Apply gravity forces between particles
|
|
247
|
+
/** @private Apply gravity forces between particles */
|
|
227
248
|
#updateGravity() {
|
|
228
249
|
const isRepulsiveEnabled = this.option.gravity.repulsive !== 0
|
|
229
250
|
const isPullingEnabled = this.option.gravity.pulling !== 0
|
|
@@ -281,13 +302,12 @@ export default class CanvasParticles {
|
|
|
281
302
|
}
|
|
282
303
|
}
|
|
283
304
|
|
|
284
|
-
/** @private Update positions, directions, and visibility of all particles
|
|
305
|
+
/** @private Update positions, directions, and visibility of all particles */
|
|
285
306
|
#updateParticles() {
|
|
286
307
|
for (let particle of this.particles) {
|
|
287
308
|
// Randomly perturb direction
|
|
288
309
|
particle.dir =
|
|
289
|
-
(particle.dir +
|
|
290
|
-
(2 * Math.PI)
|
|
310
|
+
(particle.dir + prng() * this.option.particles.rotationSpeed * 2 - this.option.particles.rotationSpeed) % TWO_PI
|
|
291
311
|
particle.velX *= this.option.gravity.friction
|
|
292
312
|
particle.velY *= this.option.gravity.friction
|
|
293
313
|
particle.posX =
|
|
@@ -374,7 +394,7 @@ export default class CanvasParticles {
|
|
|
374
394
|
if (particle.size > 1) {
|
|
375
395
|
// Draw circle
|
|
376
396
|
this.ctx.beginPath()
|
|
377
|
-
this.ctx.arc(particle.x, particle.y, particle.size, 0,
|
|
397
|
+
this.ctx.arc(particle.x, particle.y, particle.size, 0, TWO_PI)
|
|
378
398
|
this.ctx.fill()
|
|
379
399
|
this.ctx.closePath()
|
|
380
400
|
} else {
|
|
@@ -451,12 +471,9 @@ export default class CanvasParticles {
|
|
|
451
471
|
|
|
452
472
|
requestAnimationFrame(() => this.#animation())
|
|
453
473
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
this.#updateParticles()
|
|
458
|
-
this.#render()
|
|
459
|
-
}
|
|
474
|
+
this.#updateGravity()
|
|
475
|
+
this.#updateParticles()
|
|
476
|
+
this.#render()
|
|
460
477
|
}
|
|
461
478
|
|
|
462
479
|
/** @public Start the particle animation if it was not running before */
|
|
@@ -478,8 +495,7 @@ export default class CanvasParticles {
|
|
|
478
495
|
stop({ auto = false, clear = true }: { auto?: boolean; clear?: boolean } = {}): boolean {
|
|
479
496
|
if (!auto) this.enableAnimating = false
|
|
480
497
|
this.isAnimating = false
|
|
481
|
-
if (clear !== false) this.canvas.width
|
|
482
|
-
|
|
498
|
+
if (clear !== false) this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
483
499
|
return true
|
|
484
500
|
}
|
|
485
501
|
|
|
@@ -514,7 +530,6 @@ export default class CanvasParticles {
|
|
|
514
530
|
// Format and parse all options
|
|
515
531
|
this.option = {
|
|
516
532
|
background: options.background ?? false,
|
|
517
|
-
framesPerUpdate: parseNumericOption(options.framesPerUpdate, 1, { min: 1 }),
|
|
518
533
|
animation: {
|
|
519
534
|
startOnEnter: !!(options.animation?.startOnEnter ?? true),
|
|
520
535
|
stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
|
|
@@ -571,19 +586,24 @@ export default class CanvasParticles {
|
|
|
571
586
|
|
|
572
587
|
// Check if `ctx.fillStyle` is in hex format ("#RRGGBB")
|
|
573
588
|
if (String(this.ctx.fillStyle)[0] === '#') {
|
|
574
|
-
this.color
|
|
575
|
-
|
|
589
|
+
this.color = {
|
|
590
|
+
hex: String(this.ctx.fillStyle),
|
|
591
|
+
alpha: 1.0,
|
|
592
|
+
}
|
|
576
593
|
} else {
|
|
577
594
|
// JavaScript's `ctx.fillStyle` causes the color to otherwise end up in in rgba format ("rgba(136, 244, 255, 0.25)")
|
|
578
595
|
|
|
579
596
|
// Extract the alpha value from the rgba string
|
|
580
597
|
let alpha = String(this.ctx.fillStyle).split(',').at(-1) // ' 0.25)'
|
|
581
598
|
alpha = alpha?.slice(1, -1) ?? '1' // '0.25'
|
|
582
|
-
this.color.alpha = isNaN(+alpha) ? 1 : +alpha // 0.25 or 1
|
|
583
599
|
|
|
584
600
|
// Extracts e.g. 136, 244 and 255 from rgba(136, 244, 255, 0.25) and converts it to '#rrggbb'
|
|
585
601
|
this.ctx.fillStyle = String(this.ctx.fillStyle).split(',').slice(0, -1).join(',') + ', 1)'
|
|
586
|
-
|
|
602
|
+
|
|
603
|
+
this.color = {
|
|
604
|
+
hex: String(this.ctx.fillStyle),
|
|
605
|
+
alpha: isNaN(+alpha) ? 1 : +alpha,
|
|
606
|
+
} // 0.25 or 1
|
|
587
607
|
}
|
|
588
608
|
}
|
|
589
609
|
}
|