canvasparticles-js 4.4.10 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +60 -49
- package/dist/index.d.ts +4 -5
- package/dist/index.mjs +60 -49
- package/dist/index.umd.js +1 -1
- package/dist/options.d.ts +6 -0
- package/dist/types/options.d.ts +5 -2
- package/package.json +1 -1
- package/src/index.ts +57 -60
- package/src/options.ts +24 -0
- package/src/types/options.ts +3 -2
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/** Helper functions for options parsing */
|
|
4
|
+
function defaultIfNaN(value, defaultValue) {
|
|
5
|
+
return isNaN(+value) ? defaultValue : +value;
|
|
6
|
+
}
|
|
7
|
+
function parseNumericOption(name, value, defaultValue, clamp) {
|
|
8
|
+
if (value == undefined)
|
|
9
|
+
return defaultValue;
|
|
10
|
+
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
11
|
+
if (value < min) {
|
|
12
|
+
console.warn(`option.${name} was clamped to ${min} as ${value} is too low`);
|
|
13
|
+
}
|
|
14
|
+
else if (value > max) {
|
|
15
|
+
console.warn(`option.${name} was clamped to ${max} as ${value} is too high`);
|
|
16
|
+
}
|
|
17
|
+
return defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
// Copyright (c) 2022–2026 Kyle Hoeckman, MIT License
|
|
4
21
|
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
5
22
|
const TWO_PI = 2 * Math.PI;
|
|
@@ -22,7 +39,7 @@ function Mulberry32(seed) {
|
|
|
22
39
|
const prng = Mulberry32(Math.random() * 4294967296).next;
|
|
23
40
|
class CanvasParticles {
|
|
24
41
|
/** Version of the library, injected via Rollup replace plugin. */
|
|
25
|
-
static version = "4.
|
|
42
|
+
static version = "4.5.1";
|
|
26
43
|
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
|
|
27
44
|
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
28
45
|
/** Defines mouse interaction types with the particles */
|
|
@@ -37,7 +54,7 @@ class CanvasParticles {
|
|
|
37
54
|
NEW: 1, // Generate all particles from scratch
|
|
38
55
|
MATCH: 2, // Add or remove some particles to match the new count (default)
|
|
39
56
|
});
|
|
40
|
-
/** Observes canvas elements
|
|
57
|
+
/** Observes when canvas elements enter or leave the viewport to start/stop animation */
|
|
41
58
|
static canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
42
59
|
for (const entry of entries) {
|
|
43
60
|
const canvas = entry.target;
|
|
@@ -52,6 +69,7 @@ class CanvasParticles {
|
|
|
52
69
|
}, {
|
|
53
70
|
rootMargin: '-1px',
|
|
54
71
|
});
|
|
72
|
+
/** Observes when canvas elements change size */
|
|
55
73
|
static canvasResizeObserver = new ResizeObserver((entries) => {
|
|
56
74
|
// Seperate for loops is very important to prevent huge forced reflow overhead
|
|
57
75
|
// First read all canvas rects at once
|
|
@@ -67,22 +85,7 @@ class CanvasParticles {
|
|
|
67
85
|
canvas.instance.#resizeCanvas(dpr);
|
|
68
86
|
}
|
|
69
87
|
});
|
|
70
|
-
|
|
71
|
-
static defaultIfNaN(value, defaultValue) {
|
|
72
|
-
return isNaN(+value) ? defaultValue : +value;
|
|
73
|
-
}
|
|
74
|
-
static parseNumericOption(name, value, defaultValue, clamp) {
|
|
75
|
-
if (value == undefined)
|
|
76
|
-
return defaultValue;
|
|
77
|
-
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
78
|
-
if (value < min) {
|
|
79
|
-
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
80
|
-
}
|
|
81
|
-
else if (value > max) {
|
|
82
|
-
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
83
|
-
}
|
|
84
|
-
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
85
|
-
}
|
|
88
|
+
static instances = new Set();
|
|
86
89
|
canvas;
|
|
87
90
|
ctx;
|
|
88
91
|
enableAnimating = false;
|
|
@@ -127,15 +130,9 @@ class CanvasParticles {
|
|
|
127
130
|
throw new Error('failed to get 2D context from canvas');
|
|
128
131
|
this.ctx = ctx;
|
|
129
132
|
this.options = options; // Uses setter
|
|
133
|
+
CanvasParticles.instances.add(this);
|
|
130
134
|
CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
|
|
131
135
|
CanvasParticles.canvasResizeObserver.observe(this.canvas);
|
|
132
|
-
// Setup event handlers
|
|
133
|
-
this.resizeCanvas = this.resizeCanvas.bind(this);
|
|
134
|
-
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
135
|
-
this.handleScroll = this.handleScroll.bind(this);
|
|
136
|
-
// this.resizeCanvas()
|
|
137
|
-
window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
|
|
138
|
-
window.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
139
136
|
}
|
|
140
137
|
updateCanvasRect() {
|
|
141
138
|
const { top, left, width, height } = this.canvas.getBoundingClientRect();
|
|
@@ -223,17 +220,17 @@ class CanvasParticles {
|
|
|
223
220
|
if (this.hasManualParticles) {
|
|
224
221
|
const pruned = [];
|
|
225
222
|
let autoCount = 0;
|
|
226
|
-
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
227
|
-
// Only count automatic particles towards `particledCount`
|
|
228
223
|
for (const particle of this.particles) {
|
|
224
|
+
// Keep manual particles
|
|
229
225
|
if (particle.isManual) {
|
|
230
226
|
pruned.push(particle);
|
|
231
227
|
continue;
|
|
232
228
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
229
|
+
// Only keep `autoCount` amount of automatic particles
|
|
230
|
+
if (autoCount < particleCount) {
|
|
231
|
+
pruned.push(particle);
|
|
232
|
+
autoCount++;
|
|
233
|
+
}
|
|
237
234
|
}
|
|
238
235
|
this.particles = pruned;
|
|
239
236
|
}
|
|
@@ -243,7 +240,7 @@ class CanvasParticles {
|
|
|
243
240
|
// Only necessary after resize
|
|
244
241
|
if (updateBounds) {
|
|
245
242
|
for (const particle of this.particles) {
|
|
246
|
-
this.#
|
|
243
|
+
this.#updateParticleUpperBounds(particle);
|
|
247
244
|
}
|
|
248
245
|
}
|
|
249
246
|
for (let i = this.particles.length; i < particleCount; i++)
|
|
@@ -272,21 +269,26 @@ class CanvasParticles {
|
|
|
272
269
|
gridPos: { x: 1, y: 1 },
|
|
273
270
|
isVisible: false,
|
|
274
271
|
isManual,
|
|
272
|
+
bounds: {
|
|
273
|
+
top: -size,
|
|
274
|
+
right: this.canvas.width + size,
|
|
275
|
+
bottom: this.canvas.height + size,
|
|
276
|
+
left: -size,
|
|
277
|
+
},
|
|
275
278
|
};
|
|
276
|
-
this.#updateParticleBounds(particle);
|
|
277
279
|
this.particles.push(particle);
|
|
278
280
|
this.hasManualParticles = true;
|
|
279
281
|
}
|
|
280
|
-
/**
|
|
281
|
-
#updateParticleBounds(particle
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
particle.bounds =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
/** Updates the particle's bounding box, including size padding, for visibility checks. */
|
|
283
|
+
#updateParticleBounds(particle) {
|
|
284
|
+
particle.bounds.top = -particle.size;
|
|
285
|
+
particle.bounds.right = this.canvas.width + particle.size;
|
|
286
|
+
particle.bounds.bottom = this.canvas.height + particle.size;
|
|
287
|
+
particle.bounds.left = -particle.size;
|
|
288
|
+
}
|
|
289
|
+
#updateParticleUpperBounds(particle) {
|
|
290
|
+
particle.bounds.right = this.canvas.width + particle.size;
|
|
291
|
+
particle.bounds.bottom = this.canvas.height + particle.size;
|
|
290
292
|
}
|
|
291
293
|
/* Randomize speed and size of all particles based on current options */
|
|
292
294
|
updateParticles() {
|
|
@@ -744,16 +746,15 @@ class CanvasParticles {
|
|
|
744
746
|
/** Gracefully destroy the instance and remove the canvas element */
|
|
745
747
|
destroy() {
|
|
746
748
|
this.stop();
|
|
749
|
+
CanvasParticles.instances.delete(this);
|
|
747
750
|
CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
|
|
748
751
|
CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
|
|
749
|
-
window.removeEventListener('mousemove', this.handleMouseMove);
|
|
750
|
-
window.removeEventListener('scroll', this.handleScroll);
|
|
751
752
|
this.canvas?.remove();
|
|
752
753
|
Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
|
|
753
754
|
}
|
|
754
755
|
/** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
|
|
755
756
|
set options(options) {
|
|
756
|
-
const pno =
|
|
757
|
+
const pno = parseNumericOption;
|
|
757
758
|
// Format and parse all options
|
|
758
759
|
this.option = {
|
|
759
760
|
background: options.background ?? false,
|
|
@@ -763,7 +764,6 @@ class CanvasParticles {
|
|
|
763
764
|
},
|
|
764
765
|
mouse: {
|
|
765
766
|
interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
|
|
766
|
-
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
|
|
767
767
|
connectDist: 1 /* post processed */,
|
|
768
768
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
769
769
|
},
|
|
@@ -791,7 +791,7 @@ class CanvasParticles {
|
|
|
791
791
|
},
|
|
792
792
|
};
|
|
793
793
|
this.setBackground(this.option.background);
|
|
794
|
-
this.setMouseConnectDistMult(
|
|
794
|
+
this.setMouseConnectDistMult(options.mouse?.connectDistMult);
|
|
795
795
|
this.setParticleColor(this.option.particles.color);
|
|
796
796
|
}
|
|
797
797
|
get options() {
|
|
@@ -807,7 +807,7 @@ class CanvasParticles {
|
|
|
807
807
|
}
|
|
808
808
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
809
809
|
setMouseConnectDistMult(connectDistMult) {
|
|
810
|
-
const mult =
|
|
810
|
+
const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
|
|
811
811
|
this.option.mouse.connectDist = this.option.particles.connectDist * mult;
|
|
812
812
|
}
|
|
813
813
|
/** Format particle color and opacity */
|
|
@@ -834,5 +834,16 @@ class CanvasParticles {
|
|
|
834
834
|
}
|
|
835
835
|
}
|
|
836
836
|
}
|
|
837
|
+
// Global event listeners that handle all instances at once
|
|
838
|
+
window.addEventListener('mousemove', (e) => {
|
|
839
|
+
for (const instance of CanvasParticles.instances) {
|
|
840
|
+
instance.handleMouseMove(e);
|
|
841
|
+
}
|
|
842
|
+
}, { passive: true });
|
|
843
|
+
window.addEventListener('scroll', () => {
|
|
844
|
+
for (const instance of CanvasParticles.instances) {
|
|
845
|
+
instance.handleScroll();
|
|
846
|
+
}
|
|
847
|
+
}, { passive: true });
|
|
837
848
|
|
|
838
849
|
module.exports = CanvasParticles;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,12 +18,11 @@ export default class CanvasParticles {
|
|
|
18
18
|
NEW: 1;
|
|
19
19
|
MATCH: 2;
|
|
20
20
|
}>;
|
|
21
|
-
/** Observes canvas elements
|
|
21
|
+
/** Observes when canvas elements enter or leave the viewport to start/stop animation */
|
|
22
22
|
static readonly canvasIntersectionObserver: IntersectionObserver;
|
|
23
|
+
/** Observes when canvas elements change size */
|
|
23
24
|
static readonly canvasResizeObserver: ResizeObserver;
|
|
24
|
-
|
|
25
|
-
private static defaultIfNaN;
|
|
26
|
-
private static parseNumericOption;
|
|
25
|
+
static instances: Set<CanvasParticles>;
|
|
27
26
|
canvas: CanvasParticlesCanvas;
|
|
28
27
|
private ctx;
|
|
29
28
|
enableAnimating: boolean;
|
|
@@ -83,7 +82,7 @@ export default class CanvasParticles {
|
|
|
83
82
|
/** Sets the canvas background */
|
|
84
83
|
setBackground(background: CanvasParticlesOptionsInput['background']): void;
|
|
85
84
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
86
|
-
setMouseConnectDistMult(connectDistMult: number): void;
|
|
85
|
+
setMouseConnectDistMult(connectDistMult: number | undefined): void;
|
|
87
86
|
/** Format particle color and opacity */
|
|
88
87
|
setParticleColor(color: string | CanvasGradient | CanvasPattern): void;
|
|
89
88
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/** Helper functions for options parsing */
|
|
2
|
+
function defaultIfNaN(value, defaultValue) {
|
|
3
|
+
return isNaN(+value) ? defaultValue : +value;
|
|
4
|
+
}
|
|
5
|
+
function parseNumericOption(name, value, defaultValue, clamp) {
|
|
6
|
+
if (value == undefined)
|
|
7
|
+
return defaultValue;
|
|
8
|
+
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
9
|
+
if (value < min) {
|
|
10
|
+
console.warn(`option.${name} was clamped to ${min} as ${value} is too low`);
|
|
11
|
+
}
|
|
12
|
+
else if (value > max) {
|
|
13
|
+
console.warn(`option.${name} was clamped to ${max} as ${value} is too high`);
|
|
14
|
+
}
|
|
15
|
+
return defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
// Copyright (c) 2022–2026 Kyle Hoeckman, MIT License
|
|
2
19
|
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
3
20
|
const TWO_PI = 2 * Math.PI;
|
|
@@ -20,7 +37,7 @@ function Mulberry32(seed) {
|
|
|
20
37
|
const prng = Mulberry32(Math.random() * 4294967296).next;
|
|
21
38
|
class CanvasParticles {
|
|
22
39
|
/** Version of the library, injected via Rollup replace plugin. */
|
|
23
|
-
static version = "4.
|
|
40
|
+
static version = "4.5.1";
|
|
24
41
|
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
|
|
25
42
|
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
26
43
|
/** Defines mouse interaction types with the particles */
|
|
@@ -35,7 +52,7 @@ class CanvasParticles {
|
|
|
35
52
|
NEW: 1, // Generate all particles from scratch
|
|
36
53
|
MATCH: 2, // Add or remove some particles to match the new count (default)
|
|
37
54
|
});
|
|
38
|
-
/** Observes canvas elements
|
|
55
|
+
/** Observes when canvas elements enter or leave the viewport to start/stop animation */
|
|
39
56
|
static canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
40
57
|
for (const entry of entries) {
|
|
41
58
|
const canvas = entry.target;
|
|
@@ -50,6 +67,7 @@ class CanvasParticles {
|
|
|
50
67
|
}, {
|
|
51
68
|
rootMargin: '-1px',
|
|
52
69
|
});
|
|
70
|
+
/** Observes when canvas elements change size */
|
|
53
71
|
static canvasResizeObserver = new ResizeObserver((entries) => {
|
|
54
72
|
// Seperate for loops is very important to prevent huge forced reflow overhead
|
|
55
73
|
// First read all canvas rects at once
|
|
@@ -65,22 +83,7 @@ class CanvasParticles {
|
|
|
65
83
|
canvas.instance.#resizeCanvas(dpr);
|
|
66
84
|
}
|
|
67
85
|
});
|
|
68
|
-
|
|
69
|
-
static defaultIfNaN(value, defaultValue) {
|
|
70
|
-
return isNaN(+value) ? defaultValue : +value;
|
|
71
|
-
}
|
|
72
|
-
static parseNumericOption(name, value, defaultValue, clamp) {
|
|
73
|
-
if (value == undefined)
|
|
74
|
-
return defaultValue;
|
|
75
|
-
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
76
|
-
if (value < min) {
|
|
77
|
-
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
78
|
-
}
|
|
79
|
-
else if (value > max) {
|
|
80
|
-
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
81
|
-
}
|
|
82
|
-
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
83
|
-
}
|
|
86
|
+
static instances = new Set();
|
|
84
87
|
canvas;
|
|
85
88
|
ctx;
|
|
86
89
|
enableAnimating = false;
|
|
@@ -125,15 +128,9 @@ class CanvasParticles {
|
|
|
125
128
|
throw new Error('failed to get 2D context from canvas');
|
|
126
129
|
this.ctx = ctx;
|
|
127
130
|
this.options = options; // Uses setter
|
|
131
|
+
CanvasParticles.instances.add(this);
|
|
128
132
|
CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
|
|
129
133
|
CanvasParticles.canvasResizeObserver.observe(this.canvas);
|
|
130
|
-
// Setup event handlers
|
|
131
|
-
this.resizeCanvas = this.resizeCanvas.bind(this);
|
|
132
|
-
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
133
|
-
this.handleScroll = this.handleScroll.bind(this);
|
|
134
|
-
// this.resizeCanvas()
|
|
135
|
-
window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
|
|
136
|
-
window.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
137
134
|
}
|
|
138
135
|
updateCanvasRect() {
|
|
139
136
|
const { top, left, width, height } = this.canvas.getBoundingClientRect();
|
|
@@ -221,17 +218,17 @@ class CanvasParticles {
|
|
|
221
218
|
if (this.hasManualParticles) {
|
|
222
219
|
const pruned = [];
|
|
223
220
|
let autoCount = 0;
|
|
224
|
-
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
225
|
-
// Only count automatic particles towards `particledCount`
|
|
226
221
|
for (const particle of this.particles) {
|
|
222
|
+
// Keep manual particles
|
|
227
223
|
if (particle.isManual) {
|
|
228
224
|
pruned.push(particle);
|
|
229
225
|
continue;
|
|
230
226
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
227
|
+
// Only keep `autoCount` amount of automatic particles
|
|
228
|
+
if (autoCount < particleCount) {
|
|
229
|
+
pruned.push(particle);
|
|
230
|
+
autoCount++;
|
|
231
|
+
}
|
|
235
232
|
}
|
|
236
233
|
this.particles = pruned;
|
|
237
234
|
}
|
|
@@ -241,7 +238,7 @@ class CanvasParticles {
|
|
|
241
238
|
// Only necessary after resize
|
|
242
239
|
if (updateBounds) {
|
|
243
240
|
for (const particle of this.particles) {
|
|
244
|
-
this.#
|
|
241
|
+
this.#updateParticleUpperBounds(particle);
|
|
245
242
|
}
|
|
246
243
|
}
|
|
247
244
|
for (let i = this.particles.length; i < particleCount; i++)
|
|
@@ -270,21 +267,26 @@ class CanvasParticles {
|
|
|
270
267
|
gridPos: { x: 1, y: 1 },
|
|
271
268
|
isVisible: false,
|
|
272
269
|
isManual,
|
|
270
|
+
bounds: {
|
|
271
|
+
top: -size,
|
|
272
|
+
right: this.canvas.width + size,
|
|
273
|
+
bottom: this.canvas.height + size,
|
|
274
|
+
left: -size,
|
|
275
|
+
},
|
|
273
276
|
};
|
|
274
|
-
this.#updateParticleBounds(particle);
|
|
275
277
|
this.particles.push(particle);
|
|
276
278
|
this.hasManualParticles = true;
|
|
277
279
|
}
|
|
278
|
-
/**
|
|
279
|
-
#updateParticleBounds(particle
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
particle.bounds =
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
280
|
+
/** Updates the particle's bounding box, including size padding, for visibility checks. */
|
|
281
|
+
#updateParticleBounds(particle) {
|
|
282
|
+
particle.bounds.top = -particle.size;
|
|
283
|
+
particle.bounds.right = this.canvas.width + particle.size;
|
|
284
|
+
particle.bounds.bottom = this.canvas.height + particle.size;
|
|
285
|
+
particle.bounds.left = -particle.size;
|
|
286
|
+
}
|
|
287
|
+
#updateParticleUpperBounds(particle) {
|
|
288
|
+
particle.bounds.right = this.canvas.width + particle.size;
|
|
289
|
+
particle.bounds.bottom = this.canvas.height + particle.size;
|
|
288
290
|
}
|
|
289
291
|
/* Randomize speed and size of all particles based on current options */
|
|
290
292
|
updateParticles() {
|
|
@@ -742,16 +744,15 @@ class CanvasParticles {
|
|
|
742
744
|
/** Gracefully destroy the instance and remove the canvas element */
|
|
743
745
|
destroy() {
|
|
744
746
|
this.stop();
|
|
747
|
+
CanvasParticles.instances.delete(this);
|
|
745
748
|
CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
|
|
746
749
|
CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
|
|
747
|
-
window.removeEventListener('mousemove', this.handleMouseMove);
|
|
748
|
-
window.removeEventListener('scroll', this.handleScroll);
|
|
749
750
|
this.canvas?.remove();
|
|
750
751
|
Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
|
|
751
752
|
}
|
|
752
753
|
/** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
|
|
753
754
|
set options(options) {
|
|
754
|
-
const pno =
|
|
755
|
+
const pno = parseNumericOption;
|
|
755
756
|
// Format and parse all options
|
|
756
757
|
this.option = {
|
|
757
758
|
background: options.background ?? false,
|
|
@@ -761,7 +762,6 @@ class CanvasParticles {
|
|
|
761
762
|
},
|
|
762
763
|
mouse: {
|
|
763
764
|
interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
|
|
764
|
-
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
|
|
765
765
|
connectDist: 1 /* post processed */,
|
|
766
766
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
767
767
|
},
|
|
@@ -789,7 +789,7 @@ class CanvasParticles {
|
|
|
789
789
|
},
|
|
790
790
|
};
|
|
791
791
|
this.setBackground(this.option.background);
|
|
792
|
-
this.setMouseConnectDistMult(
|
|
792
|
+
this.setMouseConnectDistMult(options.mouse?.connectDistMult);
|
|
793
793
|
this.setParticleColor(this.option.particles.color);
|
|
794
794
|
}
|
|
795
795
|
get options() {
|
|
@@ -805,7 +805,7 @@ class CanvasParticles {
|
|
|
805
805
|
}
|
|
806
806
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
807
807
|
setMouseConnectDistMult(connectDistMult) {
|
|
808
|
-
const mult =
|
|
808
|
+
const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
|
|
809
809
|
this.option.mouse.connectDist = this.option.particles.connectDist * mult;
|
|
810
810
|
}
|
|
811
811
|
/** Format particle color and opacity */
|
|
@@ -832,5 +832,16 @@ class CanvasParticles {
|
|
|
832
832
|
}
|
|
833
833
|
}
|
|
834
834
|
}
|
|
835
|
+
// Global event listeners that handle all instances at once
|
|
836
|
+
window.addEventListener('mousemove', (e) => {
|
|
837
|
+
for (const instance of CanvasParticles.instances) {
|
|
838
|
+
instance.handleMouseMove(e);
|
|
839
|
+
}
|
|
840
|
+
}, { passive: true });
|
|
841
|
+
window.addEventListener('scroll', () => {
|
|
842
|
+
for (const instance of CanvasParticles.instances) {
|
|
843
|
+
instance.handleScroll();
|
|
844
|
+
}
|
|
845
|
+
}, { passive: true });
|
|
835
846
|
|
|
836
847
|
export { CanvasParticles as default };
|
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}}}(4294967296*Math.random()).next;class e{static version="4.4.10";static MAX_DT=1e3/30;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({OFF:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,e=t.instance;if(!e.options?.animation)return;(t.inViewbox=i.isIntersecting)?e.option.animation?.startOnEnter&&e.start({auto:!0}):e.option.animation?.stopOnLeave&&e.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}const i=window.devicePixelRatio||1;for(const e of t){e.target.instance.#t(i)}});static defaultIfNaN(t,i){return 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;dpr=1;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),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}#t(t=window.devicePixelRatio||1){const i=this.canvas.width=this.canvas.rect.width*t,s=this.canvas.height=this.canvas.rect.height*t;1!==t&&this.ctx.scale(t,t),this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(i+2*this.option.particles.connectDist,1),this.height=Math.max(s+2*this.option.particles.connectDist,1),this.offX=(i-this.width)/2,this.offY=(s-this.height)/2;const n=this.option.particles.generationType;n!==e.generationType.OFF&&(n===e.generationType.NEW||0===this.particles.length?this.newParticles():n===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#i()}resizeCanvas(t=!0){t&&this.updateCanvasRect(),this.#t()}#e(){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.#e();if(this.hasManualParticles&&(t||i)?(this.particles=this.particles.filter(e=>t&&!e.isManual||i&&e.isManual),this.hasManualParticles=this.particles.length>0):this.particles=[],!t)for(let t=0;t<e;t++)this.#s()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#e();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.#n(t);for(let t=this.particles.length;t<i;t++)this.#s()}#s(){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.#n(a),this.particles.push(a),this.hasManualParticles=!0}#n(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.#n(s)}#o(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;const d=1/Math.sqrt(p+l),u=d*d*d;if(p<c){const e=u*a,s=-n*e,o=-h*e;i.velX-=s,i.velY-=o,t.velX+=s,t.velY+=o}if(!e)continue;const f=u*r,g=-n*f,m=-h*f;i.velX+=g,i.velY+=m,t.velX-=g,t.velY-=m}}}#a(i){const s=this.width,n=this.height,o=this.offX,a=this.offY,r=this.mouseX,c=this.mouseY,l=this.option.mouse.interactionType===e.interactionType.NONE,h=this.option.mouse.interactionType===e.interactionType.MOVE,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.particles.rotationSpeed*i,f=this.option.gravity.friction,g=this.option.gravity.maxVelocity,m=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*u*i,e.dir%=t;const v=Math.sin(e.dir)*e.speed,x=Math.cos(e.dir)*e.speed;g>0&&(e.velX>g&&(e.velX=g),e.velX<-g&&(e.velX=-g),e.velY>g&&(e.velY=g),e.velY<-g&&(e.velY=-g)),e.posX+=(v+e.velX)*i,e.posY+=(x+e.velY)*i,e.posX%=s,e.posX<0&&(e.posX+=s),e.posY%=n,e.posY<0&&(e.posY+=n),e.velX*=Math.pow(f,i),e.velY*=Math.pow(f,i);const y=e.posX+o-r,M=e.posY+a-c;if(!l){const t=p/Math.hypot(y,M);d<t?(e.offX+=(t*y-y-e.offX)*m,e.offY+=(t*M-M-e.offY)*m):(e.offX-=e.offX*m,e.offY-=e.offY*m)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,h&&(e.posX=e.x,e.posY=e.y),e.x+=o,e.y+=a,e.gridPos.x=+(e.x>=e.bounds.left)+ +(e.x>e.bounds.right),e.gridPos.y=+(e.y>=e.bounds.top)+ +(e.y>e.bounds.bottom),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#r(){const i=this.ctx;for(const e of this.particles)e.isVisible&&(e.size>1?(i.beginPath(),i.arc(e.x,e.y,e.size,0,t),i.fill(),i.closePath()):i.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}#c(t,i){const e=this.particles,s=e.length,n=new Map;for(let o=0;o<s;o++){const s=e[o],a=(s.x*i|0)+Math.imul(s.y*i,t),r=n.get(a);r?r.push(o):n.set(a,[o])}return n}static#l(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)}#h(){const t=this.particles,i=t.length,s=this.ctx,n=this.option.particles.connectDist,o=n**2,a=(n/2)**2,r=1/n,c=Math.ceil(this.width*r),l=n>=Math.min(this.canvas.width,this.canvas.height),h=o*this.option.particles.maxWork,p=this.color.alpha,d=this.color.alpha*n,u=[],f=this.#c(c,r);let g=0,m=!0;function v(t,i,e,n){const r=t-e,c=i-n,l=r*r+c*c;l>o||(l>a?(s.globalAlpha=d/Math.sqrt(l)-p,s.beginPath(),s.moveTo(t,i),s.lineTo(e,n),s.stroke()):u.push(t,i,e,n),g+=l,m=g<h)}function x(i,s,n){for(const o of i){if(s>=o)continue;const i=t[o];if((l||e.#l(n,i))&&(v(n.x,n.y,i.x,i.y),!m))break}}function y(i,s){for(const n of i){const i=t[n];if((l||e.#l(s,i))&&(v(s.x,s.y,i.x,i.y),!m))break}}for(let e=0;e<i;e++){g=0,m=!0;let s,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c);if((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;if(g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&(s=f.get(l+c+1))&&y(s,n))))}}}if(u.length){s.globalAlpha=p,s.beginPath();for(let t=0;t<u.length;t+=4)s.moveTo(u[t],u[t+1]),s.lineTo(u[t+2],u[t+3]);s.stroke()}}#p(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#d(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#u(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#o(i),this.#a(i),this.lastAnimationFrame=t}#i(){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.option.particles.drawLines&&this.#h(),this.option.debug.drawGrid&&this.#p(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#d()}#f(){this.isAnimating&&(requestAnimationFrame(()=>this.#f()),this.#u(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#f())),!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}),maxVelocity:i("gravity.maxVelocity",t.gravity?.maxVelocity,1/0,{min:0})},debug:{drawGrid:!!t.debug?.drawGrid,drawIndexes:!!t.debug?.drawIndexes}},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";function t(t,i,e,s){if(null==i)return e;const{min:n=-1/0,max:o=1/0}=s??{};return i<n?console.warn(`option.${t} was clamped to ${n} as ${i} is too low`):i>o&&console.warn(`option.${t} was clamped to ${o} as ${i} is too high`),function(t,i){return isNaN(+t)?i:+t}(Math.min(Math.max(i??e,n),o),e)}const i=2*Math.PI;const e=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}}}(4294967296*Math.random()).next;class s{static version="4.5.1";static MAX_DT=1e3/30;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({OFF:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,e=t.instance;if(!e.options?.animation)return;(t.inViewbox=i.isIntersecting)?e.option.animation?.startOnEnter&&e.start({auto:!0}):e.option.animation?.stopOnLeave&&e.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}const i=window.devicePixelRatio||1;for(const e of t){e.target.instance.#t(i)}});static instances=new Set;canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];hasManualParticles=!1;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;dpr=1;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.instances.add(this),s.canvasIntersectionObserver.observe(this.canvas),s.canvasResizeObserver.observe(this.canvas)}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}#t(t=window.devicePixelRatio||1){const i=this.canvas.width=this.canvas.rect.width*t,e=this.canvas.height=this.canvas.rect.height*t;1!==t&&this.ctx.scale(t,t),this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(i+2*this.option.particles.connectDist,1),this.height=Math.max(e+2*this.option.particles.connectDist,1),this.offX=(i-this.width)/2,this.offY=(e-this.height)/2;const n=this.option.particles.generationType;n!==s.generationType.OFF&&(n===s.generationType.NEW||0===this.particles.length?this.newParticles():n===s.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#i()}resizeCanvas(t=!0){t&&this.updateCanvasRect(),this.#t()}#e(){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.#e();if(this.hasManualParticles&&(t||i)?(this.particles=this.particles.filter(e=>t&&!e.isManual||i&&e.isManual),this.hasManualParticles=this.particles.length>0):this.particles=[],!t)for(let t=0;t<e;t++)this.#s()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#e();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.#n(t);for(let t=this.particles.length;t<i;t++)this.#s()}#s(){const t=e()*this.width,s=e()*this.height;this.createParticle(t,s,e()*i,(.5+.5*e())*this.option.particles.relSpeed,(.5+2*Math.pow(e(),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,bounds:{top:-n,right:this.canvas.width+n,bottom:this.canvas.height+n,left:-n}};this.particles.push(a),this.hasManualParticles=!0}#o(t){t.bounds.top=-t.size,t.bounds.right=this.canvas.width+t.size,t.bounds.bottom=this.canvas.height+t.size,t.bounds.left=-t.size}#n(t){t.bounds.right=this.canvas.width+t.size,t.bounds.bottom=this.canvas.height+t.size}updateParticles(){const t=this.option.particles.relSpeed,i=this.option.particles.relSize;for(const s of this.particles)s.speed=(.5+.5*e())*t,s.size=(.5+2*Math.pow(e(),5))*i,this.#o(s)}#a(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;const d=1/Math.sqrt(p+l),u=d*d*d;if(p<c){const e=u*a,s=-n*e,o=-h*e;i.velX-=s,i.velY-=o,t.velX+=s,t.velY+=o}if(!e)continue;const f=u*r,g=-n*f,m=-h*f;i.velX+=g,i.velY+=m,t.velX-=g,t.velY-=m}}}#r(t){const e=this.width,n=this.height,o=this.offX,a=this.offY,r=this.mouseX,c=this.mouseY,l=this.option.mouse.interactionType===s.interactionType.NONE,h=this.option.mouse.interactionType===s.interactionType.MOVE,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.particles.rotationSpeed*t,f=this.option.gravity.friction,g=this.option.gravity.maxVelocity,m=1-Math.pow(3/4,t);for(const s of this.particles){s.dir+=2*(Math.random()-.5)*u*t,s.dir%=i;const v=Math.sin(s.dir)*s.speed,x=Math.cos(s.dir)*s.speed;g>0&&(s.velX>g&&(s.velX=g),s.velX<-g&&(s.velX=-g),s.velY>g&&(s.velY=g),s.velY<-g&&(s.velY=-g)),s.posX+=(v+s.velX)*t,s.posY+=(x+s.velY)*t,s.posX%=e,s.posX<0&&(s.posX+=e),s.posY%=n,s.posY<0&&(s.posY+=n),s.velX*=Math.pow(f,t),s.velY*=Math.pow(f,t);const y=s.posX+o-r,b=s.posY+a-c;if(!l){const t=p/Math.hypot(y,b);d<t?(s.offX+=(t*y-y-s.offX)*m,s.offY+=(t*b-b-s.offY)*m):(s.offX-=s.offX*m,s.offY-=s.offY*m)}s.x=s.posX+s.offX,s.y=s.posY+s.offY,h&&(s.posX=s.x,s.posY=s.y),s.x+=o,s.y+=a,s.gridPos.x=+(s.x>=s.bounds.left)+ +(s.x>s.bounds.right),s.gridPos.y=+(s.y>=s.bounds.top)+ +(s.y>s.bounds.bottom),s.isVisible=1===s.gridPos.x&&1===s.gridPos.y}}#c(){const t=this.ctx;for(const e of this.particles)e.isVisible&&(e.size>1?(t.beginPath(),t.arc(e.x,e.y,e.size,0,i),t.fill(),t.closePath()):t.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}#l(t,i){const e=this.particles,s=e.length,n=new Map;for(let o=0;o<s;o++){const s=e[o],a=(s.x*i|0)+Math.imul(s.y*i,t),r=n.get(a);r?r.push(o):n.set(a,[o])}return n}static#h(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)}#p(){const t=this.particles,i=t.length,e=this.ctx,n=this.option.particles.connectDist,o=n**2,a=(n/2)**2,r=1/n,c=Math.ceil(this.width*r),l=n>=Math.min(this.canvas.width,this.canvas.height),h=o*this.option.particles.maxWork,p=this.color.alpha,d=this.color.alpha*n,u=[],f=this.#l(c,r);let g=0,m=!0;function v(t,i,s,n){const r=t-s,c=i-n,l=r*r+c*c;l>o||(l>a?(e.globalAlpha=d/Math.sqrt(l)-p,e.beginPath(),e.moveTo(t,i),e.lineTo(s,n),e.stroke()):u.push(t,i,s,n),g+=l,m=g<h)}function x(i,e,n){for(const o of i){if(e>=o)continue;const i=t[o];if((l||s.#h(n,i))&&(v(n.x,n.y,i.x,i.y),!m))break}}function y(i,e){for(const n of i){const i=t[n];if((l||s.#h(e,i))&&(v(e.x,e.y,i.x,i.y),!m))break}}for(let e=0;e<i;e++){g=0,m=!0;let s,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c);if((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;if(g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&(s=f.get(l+c+1))&&y(s,n))))}}}if(u.length){e.globalAlpha=p,e.beginPath();for(let t=0;t<u.length;t+=4)e.moveTo(u[t],u[t+1]),e.lineTo(u[t+2],u[t+3]);e.stroke()}}#d(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#u(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#f(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,s.MAX_DT)/s.BASE_DT;this.#a(i),this.#r(i),this.lastAnimationFrame=t}#i(){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.#p(),this.option.debug.drawGrid&&this.#d(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#u()}#g(){this.isAnimating&&(requestAnimationFrame(()=>this.#g()),this.#f(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#g())),!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.instances.delete(this),s.canvasIntersectionObserver.unobserve(this.canvas),s.canvasResizeObserver.unobserve(this.canvas),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(i){const e=t;this.option={background:i.background??!1,animation:{startOnEnter:!!(i.animation?.startOnEnter??1),stopOnLeave:!!(i.animation?.stopOnLeave??1)},mouse:{interactionType:~~e("mouse.interactionType",i.mouse?.interactionType,s.interactionType.MOVE,{min:0,max:2}),connectDist:1,distRatio:e("mouse.distRatio",i.mouse?.distRatio,2/3,{min:0})},particles:{generationType:~~e("particles.generationType",i.particles?.generationType,s.generationType.MATCH,{min:0,max:2}),drawLines:!!(i.particles?.drawLines??1),color:i.particles?.color??"black",ppm:~~e("particles.ppm",i.particles?.ppm,100),max:Math.round(e("particles.max",i.particles?.max,1/0,{min:0})),maxWork:Math.round(e("particles.maxWork",i.particles?.maxWork,1/0,{min:0})),connectDist:~~e("particles.connectDistance",i.particles?.connectDistance,150,{min:1}),relSpeed:e("particles.relSpeed",i.particles?.relSpeed,1,{min:0}),relSize:e("particles.relSize",i.particles?.relSize,1,{min:0}),rotationSpeed:e("particles.rotationSpeed",i.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:e("gravity.repulsive",i.gravity?.repulsive,0,{min:0}),pulling:e("gravity.pulling",i.gravity?.pulling,0,{min:0}),friction:e("gravity.friction",i.gravity?.friction,.8,{min:0,max:1}),maxVelocity:e("gravity.maxVelocity",i.gravity?.maxVelocity,1/0,{min:0})},debug:{drawGrid:!!i.debug?.drawGrid,drawIndexes:!!i.debug?.drawIndexes}},this.setBackground(this.option.background),this.setMouseConnectDistMult(i.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(i){const e=t("mouse.connectDistMult",i,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*e}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 window.addEventListener("mousemove",t=>{for(const i of s.instances)i.handleMouseMove(t)},{passive:!0}),window.addEventListener("scroll",()=>{for(const t of s.instances)t.handleScroll()},{passive:!0}),s});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Helper functions for options parsing */
|
|
2
|
+
export declare function defaultIfNaN(value: number, defaultValue: number): number;
|
|
3
|
+
export declare function parseNumericOption(name: string, value: number | undefined, defaultValue: number, clamp?: {
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
}): number;
|
package/dist/types/options.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ export interface CanvasParticlesOptions {
|
|
|
6
6
|
};
|
|
7
7
|
mouse: {
|
|
8
8
|
interactionType: 0 | 1 | 2;
|
|
9
|
-
connectDistMult: number;
|
|
10
9
|
connectDist: number;
|
|
11
10
|
distRatio: number;
|
|
12
11
|
};
|
|
@@ -37,5 +36,9 @@ export interface CanvasParticlesOptions {
|
|
|
37
36
|
type DeepPartial<T> = {
|
|
38
37
|
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
39
38
|
};
|
|
40
|
-
export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions
|
|
39
|
+
export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions> & {
|
|
40
|
+
mouse?: {
|
|
41
|
+
connectDistMult?: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
41
44
|
export {};
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Copyright (c) 2022–2026 Kyle Hoeckman, MIT License
|
|
2
2
|
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
3
3
|
|
|
4
|
+
import { parseNumericOption } from './options'
|
|
5
|
+
|
|
4
6
|
import type { CanvasParticlesCanvas, Particle, GridPos, ContextColor, SpatialGrid } from './types'
|
|
5
7
|
import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
|
|
6
8
|
|
|
@@ -50,7 +52,7 @@ export default class CanvasParticles {
|
|
|
50
52
|
MATCH: 2, // Add or remove some particles to match the new count (default)
|
|
51
53
|
})
|
|
52
54
|
|
|
53
|
-
/** Observes canvas elements
|
|
55
|
+
/** Observes when canvas elements enter or leave the viewport to start/stop animation */
|
|
54
56
|
static readonly canvasIntersectionObserver = new IntersectionObserver(
|
|
55
57
|
(entries) => {
|
|
56
58
|
for (const entry of entries) {
|
|
@@ -69,6 +71,7 @@ export default class CanvasParticles {
|
|
|
69
71
|
}
|
|
70
72
|
)
|
|
71
73
|
|
|
74
|
+
/** Observes when canvas elements change size */
|
|
72
75
|
static readonly canvasResizeObserver = new ResizeObserver((entries) => {
|
|
73
76
|
// Seperate for loops is very important to prevent huge forced reflow overhead
|
|
74
77
|
|
|
@@ -88,29 +91,7 @@ export default class CanvasParticles {
|
|
|
88
91
|
}
|
|
89
92
|
})
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
private static defaultIfNaN(value: number, defaultValue: number): number {
|
|
93
|
-
return isNaN(+value) ? defaultValue : +value
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private static parseNumericOption(
|
|
97
|
-
name: string,
|
|
98
|
-
value: number | undefined,
|
|
99
|
-
defaultValue: number,
|
|
100
|
-
clamp?: { min?: number; max?: number }
|
|
101
|
-
): number {
|
|
102
|
-
if (value == undefined) return defaultValue
|
|
103
|
-
|
|
104
|
-
const { min = -Infinity, max = Infinity } = clamp ?? {}
|
|
105
|
-
|
|
106
|
-
if (value < min) {
|
|
107
|
-
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
|
|
108
|
-
} else if (value > max) {
|
|
109
|
-
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue)
|
|
113
|
-
}
|
|
94
|
+
static instances = new Set<CanvasParticles>()
|
|
114
95
|
|
|
115
96
|
canvas: CanvasParticlesCanvas
|
|
116
97
|
private ctx: CanvasRenderingContext2D
|
|
@@ -162,17 +143,9 @@ export default class CanvasParticles {
|
|
|
162
143
|
|
|
163
144
|
this.options = options // Uses setter
|
|
164
145
|
|
|
146
|
+
CanvasParticles.instances.add(this)
|
|
165
147
|
CanvasParticles.canvasIntersectionObserver.observe(this.canvas)
|
|
166
148
|
CanvasParticles.canvasResizeObserver.observe(this.canvas)
|
|
167
|
-
|
|
168
|
-
// Setup event handlers
|
|
169
|
-
this.resizeCanvas = this.resizeCanvas.bind(this)
|
|
170
|
-
this.handleMouseMove = this.handleMouseMove.bind(this)
|
|
171
|
-
this.handleScroll = this.handleScroll.bind(this)
|
|
172
|
-
|
|
173
|
-
// this.resizeCanvas()
|
|
174
|
-
window.addEventListener('mousemove', this.handleMouseMove, { passive: true })
|
|
175
|
-
window.addEventListener('scroll', this.handleScroll, { passive: true })
|
|
176
149
|
}
|
|
177
150
|
|
|
178
151
|
updateCanvasRect() {
|
|
@@ -272,17 +245,18 @@ export default class CanvasParticles {
|
|
|
272
245
|
const pruned: Particle[] = []
|
|
273
246
|
let autoCount = 0
|
|
274
247
|
|
|
275
|
-
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
276
|
-
// Only count automatic particles towards `particledCount`
|
|
277
248
|
for (const particle of this.particles) {
|
|
249
|
+
// Keep manual particles
|
|
278
250
|
if (particle.isManual) {
|
|
279
251
|
pruned.push(particle)
|
|
280
252
|
continue
|
|
281
253
|
}
|
|
282
254
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
255
|
+
// Only keep `autoCount` amount of automatic particles
|
|
256
|
+
if (autoCount < particleCount) {
|
|
257
|
+
pruned.push(particle)
|
|
258
|
+
autoCount++
|
|
259
|
+
}
|
|
286
260
|
}
|
|
287
261
|
this.particles = pruned
|
|
288
262
|
} else {
|
|
@@ -292,7 +266,7 @@ export default class CanvasParticles {
|
|
|
292
266
|
// Only necessary after resize
|
|
293
267
|
if (updateBounds) {
|
|
294
268
|
for (const particle of this.particles) {
|
|
295
|
-
this.#
|
|
269
|
+
this.#updateParticleUpperBounds(particle)
|
|
296
270
|
}
|
|
297
271
|
}
|
|
298
272
|
|
|
@@ -316,7 +290,7 @@ export default class CanvasParticles {
|
|
|
316
290
|
|
|
317
291
|
/** Create a new particle with optional parameters */
|
|
318
292
|
createParticle(posX: number, posY: number, dir: number, speed: number, size: number, isManual = true) {
|
|
319
|
-
const particle:
|
|
293
|
+
const particle: Particle = {
|
|
320
294
|
posX, // Logical position in pixels
|
|
321
295
|
posY, // Logical position in pixels
|
|
322
296
|
x: posX, // Visual position in pixels
|
|
@@ -331,23 +305,28 @@ export default class CanvasParticles {
|
|
|
331
305
|
gridPos: { x: 1, y: 1 },
|
|
332
306
|
isVisible: false,
|
|
333
307
|
isManual,
|
|
308
|
+
bounds: {
|
|
309
|
+
top: -size,
|
|
310
|
+
right: this.canvas.width + size,
|
|
311
|
+
bottom: this.canvas.height + size,
|
|
312
|
+
left: -size,
|
|
313
|
+
},
|
|
334
314
|
}
|
|
335
|
-
this.#updateParticleBounds(particle)
|
|
336
315
|
this.particles.push(particle)
|
|
337
316
|
this.hasManualParticles = true
|
|
338
317
|
}
|
|
339
318
|
|
|
340
|
-
/**
|
|
341
|
-
#updateParticleBounds(
|
|
342
|
-
particle
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
particle.bounds =
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
319
|
+
/** Updates the particle's bounding box, including size padding, for visibility checks. */
|
|
320
|
+
#updateParticleBounds(particle: Particle) {
|
|
321
|
+
particle.bounds.top = -particle.size
|
|
322
|
+
particle.bounds.right = this.canvas.width + particle.size
|
|
323
|
+
particle.bounds.bottom = this.canvas.height + particle.size
|
|
324
|
+
particle.bounds.left = -particle.size
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#updateParticleUpperBounds(particle: Particle) {
|
|
328
|
+
particle.bounds.right = this.canvas.width + particle.size
|
|
329
|
+
particle.bounds.bottom = this.canvas.height + particle.size
|
|
351
330
|
}
|
|
352
331
|
|
|
353
332
|
/* Randomize speed and size of all particles based on current options */
|
|
@@ -845,12 +824,10 @@ export default class CanvasParticles {
|
|
|
845
824
|
destroy() {
|
|
846
825
|
this.stop()
|
|
847
826
|
|
|
827
|
+
CanvasParticles.instances.delete(this)
|
|
848
828
|
CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas)
|
|
849
829
|
CanvasParticles.canvasResizeObserver.unobserve(this.canvas)
|
|
850
830
|
|
|
851
|
-
window.removeEventListener('mousemove', this.handleMouseMove)
|
|
852
|
-
window.removeEventListener('scroll', this.handleScroll)
|
|
853
|
-
|
|
854
831
|
this.canvas?.remove()
|
|
855
832
|
|
|
856
833
|
Object.keys(this).forEach((key) => delete (this as any)[key]) // Remove references to help GC
|
|
@@ -858,7 +835,7 @@ export default class CanvasParticles {
|
|
|
858
835
|
|
|
859
836
|
/** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
|
|
860
837
|
set options(options: CanvasParticlesOptionsInput) {
|
|
861
|
-
const pno =
|
|
838
|
+
const pno = parseNumericOption
|
|
862
839
|
|
|
863
840
|
// Format and parse all options
|
|
864
841
|
this.option = {
|
|
@@ -874,7 +851,6 @@ export default class CanvasParticles {
|
|
|
874
851
|
CanvasParticles.interactionType.MOVE,
|
|
875
852
|
{ min: 0, max: 2 }
|
|
876
853
|
) as 0 | 1 | 2,
|
|
877
|
-
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
|
|
878
854
|
connectDist: 1 /* post processed */,
|
|
879
855
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
880
856
|
},
|
|
@@ -908,7 +884,7 @@ export default class CanvasParticles {
|
|
|
908
884
|
}
|
|
909
885
|
|
|
910
886
|
this.setBackground(this.option.background)
|
|
911
|
-
this.setMouseConnectDistMult(
|
|
887
|
+
this.setMouseConnectDistMult(options.mouse?.connectDistMult)
|
|
912
888
|
this.setParticleColor(this.option.particles.color)
|
|
913
889
|
}
|
|
914
890
|
|
|
@@ -924,8 +900,8 @@ export default class CanvasParticles {
|
|
|
924
900
|
}
|
|
925
901
|
|
|
926
902
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
927
|
-
setMouseConnectDistMult(connectDistMult: number) {
|
|
928
|
-
const mult =
|
|
903
|
+
setMouseConnectDistMult(connectDistMult: number | undefined) {
|
|
904
|
+
const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 })
|
|
929
905
|
this.option.mouse.connectDist = this.option.particles.connectDist * mult
|
|
930
906
|
}
|
|
931
907
|
|
|
@@ -956,3 +932,24 @@ export default class CanvasParticles {
|
|
|
956
932
|
}
|
|
957
933
|
}
|
|
958
934
|
}
|
|
935
|
+
|
|
936
|
+
// Global event listeners that handle all instances at once
|
|
937
|
+
window.addEventListener(
|
|
938
|
+
'mousemove',
|
|
939
|
+
(e) => {
|
|
940
|
+
for (const instance of CanvasParticles.instances) {
|
|
941
|
+
instance.handleMouseMove(e)
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
{ passive: true }
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
window.addEventListener(
|
|
948
|
+
'scroll',
|
|
949
|
+
() => {
|
|
950
|
+
for (const instance of CanvasParticles.instances) {
|
|
951
|
+
instance.handleScroll()
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
{ passive: true }
|
|
955
|
+
)
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Helper functions for options parsing */
|
|
2
|
+
|
|
3
|
+
export function defaultIfNaN(value: number, defaultValue: number): number {
|
|
4
|
+
return isNaN(+value) ? defaultValue : +value
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseNumericOption(
|
|
8
|
+
name: string,
|
|
9
|
+
value: number | undefined,
|
|
10
|
+
defaultValue: number,
|
|
11
|
+
clamp?: { min?: number; max?: number }
|
|
12
|
+
): number {
|
|
13
|
+
if (value == undefined) return defaultValue
|
|
14
|
+
|
|
15
|
+
const { min = -Infinity, max = Infinity } = clamp ?? {}
|
|
16
|
+
|
|
17
|
+
if (value < min) {
|
|
18
|
+
console.warn(`option.${name} was clamped to ${min} as ${value} is too low`)
|
|
19
|
+
} else if (value > max) {
|
|
20
|
+
console.warn(`option.${name} was clamped to ${max} as ${value} is too high`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue)
|
|
24
|
+
}
|
package/src/types/options.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface CanvasParticlesOptions {
|
|
|
8
8
|
|
|
9
9
|
mouse: {
|
|
10
10
|
interactionType: 0 | 1 | 2 /* see CanvasParticles.interactionType */
|
|
11
|
-
connectDistMult: number
|
|
12
11
|
connectDist: number /* post processed */
|
|
13
12
|
distRatio: number
|
|
14
13
|
}
|
|
@@ -44,4 +43,6 @@ type DeepPartial<T> = {
|
|
|
44
43
|
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions>
|
|
46
|
+
export type CanvasParticlesOptionsInput = DeepPartial<CanvasParticlesOptions> & {
|
|
47
|
+
mouse?: { connectDistMult?: number }
|
|
48
|
+
}
|