canvasparticles-js 4.4.9 → 4.5.0
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 +51 -41
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +51 -41
- 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 +48 -50
- 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.0";
|
|
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
|
|
@@ -59,28 +77,15 @@ class CanvasParticles {
|
|
|
59
77
|
const canvas = entry.target;
|
|
60
78
|
canvas.instance.updateCanvasRect();
|
|
61
79
|
}
|
|
80
|
+
// Cache to prevent fetching the dpr for every instance
|
|
81
|
+
const dpr = window.devicePixelRatio || 1;
|
|
62
82
|
// Then resize all canvases at once
|
|
63
83
|
for (const entry of entries) {
|
|
64
84
|
const canvas = entry.target;
|
|
65
|
-
canvas.instance.#resizeCanvas();
|
|
85
|
+
canvas.instance.#resizeCanvas(dpr);
|
|
66
86
|
}
|
|
67
87
|
});
|
|
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
|
-
}
|
|
88
|
+
static instances = new Set();
|
|
84
89
|
canvas;
|
|
85
90
|
ctx;
|
|
86
91
|
enableAnimating = false;
|
|
@@ -92,6 +97,7 @@ class CanvasParticles {
|
|
|
92
97
|
clientY = Infinity;
|
|
93
98
|
mouseX = Infinity;
|
|
94
99
|
mouseY = Infinity;
|
|
100
|
+
dpr = 1;
|
|
95
101
|
width;
|
|
96
102
|
height;
|
|
97
103
|
offX;
|
|
@@ -124,15 +130,9 @@ class CanvasParticles {
|
|
|
124
130
|
throw new Error('failed to get 2D context from canvas');
|
|
125
131
|
this.ctx = ctx;
|
|
126
132
|
this.options = options; // Uses setter
|
|
133
|
+
CanvasParticles.instances.add(this);
|
|
127
134
|
CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
|
|
128
135
|
CanvasParticles.canvasResizeObserver.observe(this.canvas);
|
|
129
|
-
// Setup event handlers
|
|
130
|
-
this.resizeCanvas = this.resizeCanvas.bind(this);
|
|
131
|
-
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
132
|
-
this.handleScroll = this.handleScroll.bind(this);
|
|
133
|
-
this.resizeCanvas();
|
|
134
|
-
window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
|
|
135
|
-
window.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
136
136
|
}
|
|
137
137
|
updateCanvasRect() {
|
|
138
138
|
const { top, left, width, height } = this.canvas.getBoundingClientRect();
|
|
@@ -161,11 +161,12 @@ class CanvasParticles {
|
|
|
161
161
|
this.mouseY = this.clientY - top;
|
|
162
162
|
}
|
|
163
163
|
/** Resize the canvas and update particles accordingly */
|
|
164
|
-
#resizeCanvas() {
|
|
165
|
-
const dpr = window.devicePixelRatio || 1;
|
|
164
|
+
#resizeCanvas(dpr = window.devicePixelRatio || 1) {
|
|
166
165
|
const width = (this.canvas.width = this.canvas.rect.width * dpr);
|
|
167
166
|
const height = (this.canvas.height = this.canvas.rect.height * dpr);
|
|
168
|
-
|
|
167
|
+
// Must be set every time width or height changes because scale is removed
|
|
168
|
+
if (dpr !== 1)
|
|
169
|
+
this.ctx.scale(dpr, dpr);
|
|
169
170
|
// Hide the mouse when resizing because it must be outside the viewport to do so
|
|
170
171
|
this.mouseX = Infinity;
|
|
171
172
|
this.mouseY = Infinity;
|
|
@@ -219,17 +220,17 @@ class CanvasParticles {
|
|
|
219
220
|
if (this.hasManualParticles) {
|
|
220
221
|
const pruned = [];
|
|
221
222
|
let autoCount = 0;
|
|
222
|
-
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
223
|
-
// Only count automatic particles towards `particledCount`
|
|
224
223
|
for (const particle of this.particles) {
|
|
224
|
+
// Keep manual particles
|
|
225
225
|
if (particle.isManual) {
|
|
226
226
|
pruned.push(particle);
|
|
227
227
|
continue;
|
|
228
228
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
// Only keep `autoCount` amount of automatic particles
|
|
230
|
+
if (autoCount < particleCount) {
|
|
231
|
+
pruned.push(particle);
|
|
232
|
+
autoCount++;
|
|
233
|
+
}
|
|
233
234
|
}
|
|
234
235
|
this.particles = pruned;
|
|
235
236
|
}
|
|
@@ -740,16 +741,15 @@ class CanvasParticles {
|
|
|
740
741
|
/** Gracefully destroy the instance and remove the canvas element */
|
|
741
742
|
destroy() {
|
|
742
743
|
this.stop();
|
|
744
|
+
CanvasParticles.instances.delete(this);
|
|
743
745
|
CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
|
|
744
746
|
CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
|
|
745
|
-
window.removeEventListener('mousemove', this.handleMouseMove);
|
|
746
|
-
window.removeEventListener('scroll', this.handleScroll);
|
|
747
747
|
this.canvas?.remove();
|
|
748
748
|
Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
|
|
749
749
|
}
|
|
750
750
|
/** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
|
|
751
751
|
set options(options) {
|
|
752
|
-
const pno =
|
|
752
|
+
const pno = parseNumericOption;
|
|
753
753
|
// Format and parse all options
|
|
754
754
|
this.option = {
|
|
755
755
|
background: options.background ?? false,
|
|
@@ -759,7 +759,6 @@ class CanvasParticles {
|
|
|
759
759
|
},
|
|
760
760
|
mouse: {
|
|
761
761
|
interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
|
|
762
|
-
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
|
|
763
762
|
connectDist: 1 /* post processed */,
|
|
764
763
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
765
764
|
},
|
|
@@ -787,7 +786,7 @@ class CanvasParticles {
|
|
|
787
786
|
},
|
|
788
787
|
};
|
|
789
788
|
this.setBackground(this.option.background);
|
|
790
|
-
this.setMouseConnectDistMult(
|
|
789
|
+
this.setMouseConnectDistMult(options.mouse?.connectDistMult);
|
|
791
790
|
this.setParticleColor(this.option.particles.color);
|
|
792
791
|
}
|
|
793
792
|
get options() {
|
|
@@ -803,7 +802,7 @@ class CanvasParticles {
|
|
|
803
802
|
}
|
|
804
803
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
805
804
|
setMouseConnectDistMult(connectDistMult) {
|
|
806
|
-
const mult =
|
|
805
|
+
const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
|
|
807
806
|
this.option.mouse.connectDist = this.option.particles.connectDist * mult;
|
|
808
807
|
}
|
|
809
808
|
/** Format particle color and opacity */
|
|
@@ -830,5 +829,16 @@ class CanvasParticles {
|
|
|
830
829
|
}
|
|
831
830
|
}
|
|
832
831
|
}
|
|
832
|
+
// Global event listeners that handle all instances at once
|
|
833
|
+
window.addEventListener('mousemove', (e) => {
|
|
834
|
+
for (const instance of CanvasParticles.instances) {
|
|
835
|
+
instance.handleMouseMove(e);
|
|
836
|
+
}
|
|
837
|
+
}, { passive: true });
|
|
838
|
+
window.addEventListener('scroll', () => {
|
|
839
|
+
for (const instance of CanvasParticles.instances) {
|
|
840
|
+
instance.handleScroll();
|
|
841
|
+
}
|
|
842
|
+
}, { passive: true });
|
|
833
843
|
|
|
834
844
|
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;
|
|
@@ -35,6 +34,7 @@ export default class CanvasParticles {
|
|
|
35
34
|
private clientY;
|
|
36
35
|
mouseX: number;
|
|
37
36
|
mouseY: number;
|
|
37
|
+
dpr: number;
|
|
38
38
|
width: number;
|
|
39
39
|
height: number;
|
|
40
40
|
private offX;
|
|
@@ -82,7 +82,7 @@ export default class CanvasParticles {
|
|
|
82
82
|
/** Sets the canvas background */
|
|
83
83
|
setBackground(background: CanvasParticlesOptionsInput['background']): void;
|
|
84
84
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
85
|
-
setMouseConnectDistMult(connectDistMult: number): void;
|
|
85
|
+
setMouseConnectDistMult(connectDistMult: number | undefined): void;
|
|
86
86
|
/** Format particle color and opacity */
|
|
87
87
|
setParticleColor(color: string | CanvasGradient | CanvasPattern): void;
|
|
88
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.0";
|
|
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
|
|
@@ -57,28 +75,15 @@ class CanvasParticles {
|
|
|
57
75
|
const canvas = entry.target;
|
|
58
76
|
canvas.instance.updateCanvasRect();
|
|
59
77
|
}
|
|
78
|
+
// Cache to prevent fetching the dpr for every instance
|
|
79
|
+
const dpr = window.devicePixelRatio || 1;
|
|
60
80
|
// Then resize all canvases at once
|
|
61
81
|
for (const entry of entries) {
|
|
62
82
|
const canvas = entry.target;
|
|
63
|
-
canvas.instance.#resizeCanvas();
|
|
83
|
+
canvas.instance.#resizeCanvas(dpr);
|
|
64
84
|
}
|
|
65
85
|
});
|
|
66
|
-
|
|
67
|
-
static defaultIfNaN(value, defaultValue) {
|
|
68
|
-
return isNaN(+value) ? defaultValue : +value;
|
|
69
|
-
}
|
|
70
|
-
static parseNumericOption(name, value, defaultValue, clamp) {
|
|
71
|
-
if (value == undefined)
|
|
72
|
-
return defaultValue;
|
|
73
|
-
const { min = -Infinity, max = Infinity } = clamp ?? {};
|
|
74
|
-
if (value < min) {
|
|
75
|
-
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
76
|
-
}
|
|
77
|
-
else if (value > max) {
|
|
78
|
-
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
79
|
-
}
|
|
80
|
-
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
81
|
-
}
|
|
86
|
+
static instances = new Set();
|
|
82
87
|
canvas;
|
|
83
88
|
ctx;
|
|
84
89
|
enableAnimating = false;
|
|
@@ -90,6 +95,7 @@ class CanvasParticles {
|
|
|
90
95
|
clientY = Infinity;
|
|
91
96
|
mouseX = Infinity;
|
|
92
97
|
mouseY = Infinity;
|
|
98
|
+
dpr = 1;
|
|
93
99
|
width;
|
|
94
100
|
height;
|
|
95
101
|
offX;
|
|
@@ -122,15 +128,9 @@ class CanvasParticles {
|
|
|
122
128
|
throw new Error('failed to get 2D context from canvas');
|
|
123
129
|
this.ctx = ctx;
|
|
124
130
|
this.options = options; // Uses setter
|
|
131
|
+
CanvasParticles.instances.add(this);
|
|
125
132
|
CanvasParticles.canvasIntersectionObserver.observe(this.canvas);
|
|
126
133
|
CanvasParticles.canvasResizeObserver.observe(this.canvas);
|
|
127
|
-
// Setup event handlers
|
|
128
|
-
this.resizeCanvas = this.resizeCanvas.bind(this);
|
|
129
|
-
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
130
|
-
this.handleScroll = this.handleScroll.bind(this);
|
|
131
|
-
this.resizeCanvas();
|
|
132
|
-
window.addEventListener('mousemove', this.handleMouseMove, { passive: true });
|
|
133
|
-
window.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
134
134
|
}
|
|
135
135
|
updateCanvasRect() {
|
|
136
136
|
const { top, left, width, height } = this.canvas.getBoundingClientRect();
|
|
@@ -159,11 +159,12 @@ class CanvasParticles {
|
|
|
159
159
|
this.mouseY = this.clientY - top;
|
|
160
160
|
}
|
|
161
161
|
/** Resize the canvas and update particles accordingly */
|
|
162
|
-
#resizeCanvas() {
|
|
163
|
-
const dpr = window.devicePixelRatio || 1;
|
|
162
|
+
#resizeCanvas(dpr = window.devicePixelRatio || 1) {
|
|
164
163
|
const width = (this.canvas.width = this.canvas.rect.width * dpr);
|
|
165
164
|
const height = (this.canvas.height = this.canvas.rect.height * dpr);
|
|
166
|
-
|
|
165
|
+
// Must be set every time width or height changes because scale is removed
|
|
166
|
+
if (dpr !== 1)
|
|
167
|
+
this.ctx.scale(dpr, dpr);
|
|
167
168
|
// Hide the mouse when resizing because it must be outside the viewport to do so
|
|
168
169
|
this.mouseX = Infinity;
|
|
169
170
|
this.mouseY = Infinity;
|
|
@@ -217,17 +218,17 @@ class CanvasParticles {
|
|
|
217
218
|
if (this.hasManualParticles) {
|
|
218
219
|
const pruned = [];
|
|
219
220
|
let autoCount = 0;
|
|
220
|
-
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
221
|
-
// Only count automatic particles towards `particledCount`
|
|
222
221
|
for (const particle of this.particles) {
|
|
222
|
+
// Keep manual particles
|
|
223
223
|
if (particle.isManual) {
|
|
224
224
|
pruned.push(particle);
|
|
225
225
|
continue;
|
|
226
226
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
// Only keep `autoCount` amount of automatic particles
|
|
228
|
+
if (autoCount < particleCount) {
|
|
229
|
+
pruned.push(particle);
|
|
230
|
+
autoCount++;
|
|
231
|
+
}
|
|
231
232
|
}
|
|
232
233
|
this.particles = pruned;
|
|
233
234
|
}
|
|
@@ -738,16 +739,15 @@ class CanvasParticles {
|
|
|
738
739
|
/** Gracefully destroy the instance and remove the canvas element */
|
|
739
740
|
destroy() {
|
|
740
741
|
this.stop();
|
|
742
|
+
CanvasParticles.instances.delete(this);
|
|
741
743
|
CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas);
|
|
742
744
|
CanvasParticles.canvasResizeObserver.unobserve(this.canvas);
|
|
743
|
-
window.removeEventListener('mousemove', this.handleMouseMove);
|
|
744
|
-
window.removeEventListener('scroll', this.handleScroll);
|
|
745
745
|
this.canvas?.remove();
|
|
746
746
|
Object.keys(this).forEach((key) => delete this[key]); // Remove references to help GC
|
|
747
747
|
}
|
|
748
748
|
/** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
|
|
749
749
|
set options(options) {
|
|
750
|
-
const pno =
|
|
750
|
+
const pno = parseNumericOption;
|
|
751
751
|
// Format and parse all options
|
|
752
752
|
this.option = {
|
|
753
753
|
background: options.background ?? false,
|
|
@@ -757,7 +757,6 @@ class CanvasParticles {
|
|
|
757
757
|
},
|
|
758
758
|
mouse: {
|
|
759
759
|
interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
|
|
760
|
-
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
|
|
761
760
|
connectDist: 1 /* post processed */,
|
|
762
761
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
763
762
|
},
|
|
@@ -785,7 +784,7 @@ class CanvasParticles {
|
|
|
785
784
|
},
|
|
786
785
|
};
|
|
787
786
|
this.setBackground(this.option.background);
|
|
788
|
-
this.setMouseConnectDistMult(
|
|
787
|
+
this.setMouseConnectDistMult(options.mouse?.connectDistMult);
|
|
789
788
|
this.setParticleColor(this.option.particles.color);
|
|
790
789
|
}
|
|
791
790
|
get options() {
|
|
@@ -801,7 +800,7 @@ class CanvasParticles {
|
|
|
801
800
|
}
|
|
802
801
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
803
802
|
setMouseConnectDistMult(connectDistMult) {
|
|
804
|
-
const mult =
|
|
803
|
+
const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 });
|
|
805
804
|
this.option.mouse.connectDist = this.option.particles.connectDist * mult;
|
|
806
805
|
}
|
|
807
806
|
/** Format particle color and opacity */
|
|
@@ -828,5 +827,16 @@ class CanvasParticles {
|
|
|
828
827
|
}
|
|
829
828
|
}
|
|
830
829
|
}
|
|
830
|
+
// Global event listeners that handle all instances at once
|
|
831
|
+
window.addEventListener('mousemove', (e) => {
|
|
832
|
+
for (const instance of CanvasParticles.instances) {
|
|
833
|
+
instance.handleMouseMove(e);
|
|
834
|
+
}
|
|
835
|
+
}, { passive: true });
|
|
836
|
+
window.addEventListener('scroll', () => {
|
|
837
|
+
for (const instance of CanvasParticles.instances) {
|
|
838
|
+
instance.handleScroll();
|
|
839
|
+
}
|
|
840
|
+
}, { passive: true });
|
|
831
841
|
|
|
832
842
|
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.9";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()}for(const i of t){i.target.instance.#t()}});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;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}#t(){const t=window.devicePixelRatio||1,i=this.canvas.width=this.canvas.rect.width*t,s=this.canvas.height=this.canvas.rect.height*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.0";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};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,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.#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(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}}#r(){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))}#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,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.#c(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.#l(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.#l(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()}}#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,s.MAX_DT)/s.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(),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
|
|
|
@@ -78,36 +81,17 @@ export default class CanvasParticles {
|
|
|
78
81
|
canvas.instance.updateCanvasRect()
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
// Cache to prevent fetching the dpr for every instance
|
|
85
|
+
const dpr = window.devicePixelRatio || 1
|
|
86
|
+
|
|
81
87
|
// Then resize all canvases at once
|
|
82
88
|
for (const entry of entries) {
|
|
83
89
|
const canvas = entry.target as CanvasParticlesCanvas
|
|
84
|
-
canvas.instance.#resizeCanvas()
|
|
90
|
+
canvas.instance.#resizeCanvas(dpr)
|
|
85
91
|
}
|
|
86
92
|
})
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
private static defaultIfNaN(value: number, defaultValue: number): number {
|
|
90
|
-
return isNaN(+value) ? defaultValue : +value
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private static parseNumericOption(
|
|
94
|
-
name: string,
|
|
95
|
-
value: number | undefined,
|
|
96
|
-
defaultValue: number,
|
|
97
|
-
clamp?: { min?: number; max?: number }
|
|
98
|
-
): number {
|
|
99
|
-
if (value == undefined) return defaultValue
|
|
100
|
-
|
|
101
|
-
const { min = -Infinity, max = Infinity } = clamp ?? {}
|
|
102
|
-
|
|
103
|
-
if (value < min) {
|
|
104
|
-
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
|
|
105
|
-
} else if (value > max) {
|
|
106
|
-
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue)
|
|
110
|
-
}
|
|
94
|
+
static instances = new Set<CanvasParticles>()
|
|
111
95
|
|
|
112
96
|
canvas: CanvasParticlesCanvas
|
|
113
97
|
private ctx: CanvasRenderingContext2D
|
|
@@ -122,6 +106,8 @@ export default class CanvasParticles {
|
|
|
122
106
|
private clientY: number = Infinity
|
|
123
107
|
mouseX: number = Infinity
|
|
124
108
|
mouseY: number = Infinity
|
|
109
|
+
|
|
110
|
+
dpr: number = 1
|
|
125
111
|
width!: number
|
|
126
112
|
height!: number
|
|
127
113
|
private offX!: number
|
|
@@ -157,17 +143,9 @@ export default class CanvasParticles {
|
|
|
157
143
|
|
|
158
144
|
this.options = options // Uses setter
|
|
159
145
|
|
|
146
|
+
CanvasParticles.instances.add(this)
|
|
160
147
|
CanvasParticles.canvasIntersectionObserver.observe(this.canvas)
|
|
161
148
|
CanvasParticles.canvasResizeObserver.observe(this.canvas)
|
|
162
|
-
|
|
163
|
-
// Setup event handlers
|
|
164
|
-
this.resizeCanvas = this.resizeCanvas.bind(this)
|
|
165
|
-
this.handleMouseMove = this.handleMouseMove.bind(this)
|
|
166
|
-
this.handleScroll = this.handleScroll.bind(this)
|
|
167
|
-
|
|
168
|
-
this.resizeCanvas()
|
|
169
|
-
window.addEventListener('mousemove', this.handleMouseMove, { passive: true })
|
|
170
|
-
window.addEventListener('scroll', this.handleScroll, { passive: true })
|
|
171
149
|
}
|
|
172
150
|
|
|
173
151
|
updateCanvasRect() {
|
|
@@ -199,11 +177,12 @@ export default class CanvasParticles {
|
|
|
199
177
|
}
|
|
200
178
|
|
|
201
179
|
/** Resize the canvas and update particles accordingly */
|
|
202
|
-
#resizeCanvas() {
|
|
203
|
-
const dpr = window.devicePixelRatio || 1
|
|
180
|
+
#resizeCanvas(dpr = window.devicePixelRatio || 1) {
|
|
204
181
|
const width = (this.canvas.width = this.canvas.rect.width * dpr)
|
|
205
182
|
const height = (this.canvas.height = this.canvas.rect.height * dpr)
|
|
206
|
-
|
|
183
|
+
|
|
184
|
+
// Must be set every time width or height changes because scale is removed
|
|
185
|
+
if (dpr !== 1) this.ctx.scale(dpr, dpr)
|
|
207
186
|
|
|
208
187
|
// Hide the mouse when resizing because it must be outside the viewport to do so
|
|
209
188
|
this.mouseX = Infinity
|
|
@@ -266,17 +245,18 @@ export default class CanvasParticles {
|
|
|
266
245
|
const pruned: Particle[] = []
|
|
267
246
|
let autoCount = 0
|
|
268
247
|
|
|
269
|
-
// Keep manual particles while pruning automatic particles that exceed `particleCount`
|
|
270
|
-
// Only count automatic particles towards `particledCount`
|
|
271
248
|
for (const particle of this.particles) {
|
|
249
|
+
// Keep manual particles
|
|
272
250
|
if (particle.isManual) {
|
|
273
251
|
pruned.push(particle)
|
|
274
252
|
continue
|
|
275
253
|
}
|
|
276
254
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
255
|
+
// Only keep `autoCount` amount of automatic particles
|
|
256
|
+
if (autoCount < particleCount) {
|
|
257
|
+
pruned.push(particle)
|
|
258
|
+
autoCount++
|
|
259
|
+
}
|
|
280
260
|
}
|
|
281
261
|
this.particles = pruned
|
|
282
262
|
} else {
|
|
@@ -839,12 +819,10 @@ export default class CanvasParticles {
|
|
|
839
819
|
destroy() {
|
|
840
820
|
this.stop()
|
|
841
821
|
|
|
822
|
+
CanvasParticles.instances.delete(this)
|
|
842
823
|
CanvasParticles.canvasIntersectionObserver.unobserve(this.canvas)
|
|
843
824
|
CanvasParticles.canvasResizeObserver.unobserve(this.canvas)
|
|
844
825
|
|
|
845
|
-
window.removeEventListener('mousemove', this.handleMouseMove)
|
|
846
|
-
window.removeEventListener('scroll', this.handleScroll)
|
|
847
|
-
|
|
848
826
|
this.canvas?.remove()
|
|
849
827
|
|
|
850
828
|
Object.keys(this).forEach((key) => delete (this as any)[key]) // Remove references to help GC
|
|
@@ -852,7 +830,7 @@ export default class CanvasParticles {
|
|
|
852
830
|
|
|
853
831
|
/** Set and validate options (https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options) */
|
|
854
832
|
set options(options: CanvasParticlesOptionsInput) {
|
|
855
|
-
const pno =
|
|
833
|
+
const pno = parseNumericOption
|
|
856
834
|
|
|
857
835
|
// Format and parse all options
|
|
858
836
|
this.option = {
|
|
@@ -868,7 +846,6 @@ export default class CanvasParticles {
|
|
|
868
846
|
CanvasParticles.interactionType.MOVE,
|
|
869
847
|
{ min: 0, max: 2 }
|
|
870
848
|
) as 0 | 1 | 2,
|
|
871
|
-
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
|
|
872
849
|
connectDist: 1 /* post processed */,
|
|
873
850
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
874
851
|
},
|
|
@@ -902,7 +879,7 @@ export default class CanvasParticles {
|
|
|
902
879
|
}
|
|
903
880
|
|
|
904
881
|
this.setBackground(this.option.background)
|
|
905
|
-
this.setMouseConnectDistMult(
|
|
882
|
+
this.setMouseConnectDistMult(options.mouse?.connectDistMult)
|
|
906
883
|
this.setParticleColor(this.option.particles.color)
|
|
907
884
|
}
|
|
908
885
|
|
|
@@ -918,8 +895,8 @@ export default class CanvasParticles {
|
|
|
918
895
|
}
|
|
919
896
|
|
|
920
897
|
/** Transform the distance multiplier (float) to absolute distance (px) */
|
|
921
|
-
setMouseConnectDistMult(connectDistMult: number) {
|
|
922
|
-
const mult =
|
|
898
|
+
setMouseConnectDistMult(connectDistMult: number | undefined) {
|
|
899
|
+
const mult = parseNumericOption('mouse.connectDistMult', connectDistMult, 2 / 3, { min: 0 })
|
|
923
900
|
this.option.mouse.connectDist = this.option.particles.connectDist * mult
|
|
924
901
|
}
|
|
925
902
|
|
|
@@ -950,3 +927,24 @@ export default class CanvasParticles {
|
|
|
950
927
|
}
|
|
951
928
|
}
|
|
952
929
|
}
|
|
930
|
+
|
|
931
|
+
// Global event listeners that handle all instances at once
|
|
932
|
+
window.addEventListener(
|
|
933
|
+
'mousemove',
|
|
934
|
+
(e) => {
|
|
935
|
+
for (const instance of CanvasParticles.instances) {
|
|
936
|
+
instance.handleMouseMove(e)
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
{ passive: true }
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
window.addEventListener(
|
|
943
|
+
'scroll',
|
|
944
|
+
() => {
|
|
945
|
+
for (const instance of CanvasParticles.instances) {
|
|
946
|
+
instance.handleScroll()
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
{ passive: true }
|
|
950
|
+
)
|
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
|
+
}
|