canvasparticles-js 3.4.6 → 3.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/README.md CHANGED
@@ -175,13 +175,17 @@ const options = {
175
175
  /** @param {integer} [options.framesPerUpdate=1] - How many times the same frame will be shown before an update happens.
176
176
  * @example 60 fps / 2 framesPerUpdate = 30 updates/s
177
177
  * @example 144 fps / 3 framesPerUpdate = 48 updates/s
178
- * */
178
+ */
179
179
  framesPerUpdate: 1, // recommended: 1 - 3
180
180
 
181
- /** @param {boolean} [options.resetOnResize=false] - Create new particles when the canvas gets resized.
182
- * @info If false, will instead add or remove a few particles to match particles.ppm
183
- */
184
- resetOnResize: false,
181
+ /** @param {Object} [options.animation] - Animation settings. */
182
+ animation: {
183
+ /** @param {boolean} [options.animation.startOnEnter=true] - Whether to start the animation when the canvas enters the viewport. */
184
+ startOnEnter: true,
185
+
186
+ /** @param {boolean} [options.animation.stopOnLeave=true] - Whether to stop the animation when the canvas leaves the viewport. */
187
+ stopOnLeave: true,
188
+ },
185
189
 
186
190
  /** @param {Object} [options.mouse] - Mouse interaction settings. */
187
191
  mouse: {
@@ -247,6 +251,11 @@ const options = {
247
251
  * @example 1 rotationSpeed = max direction change of 0.01 radians per update
248
252
  */
249
253
  rotationSpeed: 1, // recommended: < 10
254
+
255
+ /** @param {boolean} [options.particles.regenerateOnResize=false] - Create new particles when the canvas gets resized.
256
+ * @note If false, will instead add or remove a few particles to match particles.ppm
257
+ */
258
+ regenerateOnResize: false,
250
259
  },
251
260
 
252
261
  /** @param {Object} [options.gravity] - Gravitational force settings.
@@ -9,7 +9,18 @@
9
9
  typeof self !== 'undefined' ? self : this,
10
10
  () =>
11
11
  class CanvasParticles {
12
- static version = '3.4.6'
12
+ static version = '3.5.0'
13
+
14
+ // Start or stop the animation when the canvas enters or exits the viewport
15
+ static canvasObserver = new IntersectionObserver(entry => {
16
+ entry.forEach(change => {
17
+ // CanvasParticles instance of the target canvas
18
+ const instance = change.target.instance
19
+
20
+ if (change.isIntersecting) instance.options.animation.startOnEnter && instance.start()
21
+ else instance.options.animation.stopOnLeave && instance.stop({ clear: false })
22
+ })
23
+ })
13
24
 
14
25
  /**
15
26
  * Creates a new CanvasParticles instance.
@@ -23,6 +34,8 @@
23
34
  this.canvas = document.querySelector(selector)
24
35
  if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
25
36
 
37
+ this.canvas.instance = this
38
+
26
39
  // Get 2d drawing functions
27
40
  this.ctx = this.canvas.getContext('2d')
28
41
 
@@ -30,47 +43,64 @@
30
43
  this.particles = []
31
44
  this.setOptions(options)
32
45
 
33
- // Event handling
34
- window.addEventListener('resize', this.#resizeCanvas)
35
- this.#resizeCanvas()
46
+ CanvasParticles.canvasObserver.observe(this.canvas)
36
47
 
37
- window.addEventListener('mousemove', this.#updateMousePos)
38
- window.addEventListener('scroll', this.#updateMousePos)
48
+ this.#setupEventHandlers()
39
49
  }
40
50
 
41
- #updateMousePos = event => {
42
- if (!this.animating) return
51
+ // Helper function
52
+ #defaultIfNaN(value, defaultValue) {
53
+ return isNaN(+value) ? defaultValue : +value
54
+ }
43
55
 
44
- if (event instanceof MouseEvent) {
45
- this.clientX = event.clientX
46
- this.clientY = event.clientY
56
+ #setupEventHandlers() {
57
+ const updateMousePos = event => {
58
+ if (!this.animating) return
59
+
60
+ if (event instanceof MouseEvent) {
61
+ this.clientX = event.clientX
62
+ this.clientY = event.clientY
63
+ }
64
+ const { left, top } = this.canvas.getBoundingClientRect()
65
+ this.mouseX = this.clientX - left
66
+ this.mouseY = this.clientY - top
47
67
  }
48
- const { left, top } = this.canvas.getBoundingClientRect()
49
- this.mouseX = this.clientX - left
50
- this.mouseY = this.clientY - top
51
- }
52
68
 
53
- #resizeCanvas = () => {
54
- this.canvas.width = this.canvas.offsetWidth
55
- this.canvas.height = this.canvas.offsetHeight
69
+ const resizeCanvas = () => {
70
+ this.canvas.width = this.canvas.offsetWidth
71
+ this.canvas.height = this.canvas.offsetHeight
56
72
 
57
- // Prevent the mouse from affecting particles at (x: 0, y: 0) before it has moved.
58
- this.mouseX = Infinity
59
- this.mouseY = Infinity
73
+ // Prevent the mouse acting like it's at (x: 0, y: 0) before it has moved.
74
+ this.mouseX = Infinity
75
+ this.mouseY = Infinity
60
76
 
61
- this.updateCount = Infinity
62
- this.width = this.canvas.width + this.options.particles.connectDist * 2
63
- this.height = this.canvas.height + this.options.particles.connectDist * 2
64
- this.offX = (this.canvas.width - this.width) / 2
65
- this.offY = (this.canvas.height - this.height) / 2
77
+ this.updateCount = Infinity
78
+ this.width = this.canvas.width + this.options.particles.connectDist * 2
79
+ this.height = this.canvas.height + this.options.particles.connectDist * 2
80
+ this.offX = (this.canvas.width - this.width) / 2
81
+ this.offY = (this.canvas.height - this.height) / 2
66
82
 
67
- if (this.options.resetOnResize || this.particles.length === 0) this.newParticles()
68
- else this.matchParticleCount()
83
+ if (this.options.particles.regenerateOnResize || this.particles.length === 0) this.newParticles()
84
+ else this.matchParticleCount()
69
85
 
70
- this.#updateParticleBounds()
86
+ this.#updateParticleBounds()
87
+ }
88
+
89
+ window.addEventListener('resize', resizeCanvas)
90
+ resizeCanvas()
91
+
92
+ window.addEventListener('mousemove', updateMousePos)
93
+ window.addEventListener('scroll', updateMousePos)
71
94
  }
72
95
 
73
- #getParticleCount = () => {
96
+ /**
97
+ * Update the target number of particles based on the current canvas size and 'options.particles.ppm'
98
+ * Capped at 'options.particles.max'.
99
+ *
100
+ * @private
101
+ * @throws {RangeError} If the particle count is not finite.
102
+ */
103
+ #updateParticleCount() {
74
104
  // Amount of particles to be created
75
105
  const particleCount = ~~((this.options.particles.ppm * this.width * this.height) / 1_000_000)
76
106
  this.particleCount = Math.min(this.options.particles.max, particleCount)
@@ -80,26 +110,26 @@
80
110
 
81
111
  /**
82
112
  * Remove all particles and generate new ones.
83
- * The amount of new particles will match 'options.particles.ppm'
113
+ * The amount of new particles will match 'options.particles.ppm'.
84
114
  * */
85
- newParticles = () => {
86
- this.#getParticleCount()
115
+ newParticles() {
116
+ this.#updateParticleCount()
87
117
 
88
118
  this.particles = []
89
119
  for (let i = 0; i < this.particleCount; i++) this.createParticle()
90
120
  }
91
121
 
92
122
  /**
93
- * When resizing, add or remove some particles so that the final amount of particles will match 'options.particles.ppm'
123
+ * When resizing, add or remove some particles so that the final amount of particles will match 'options.particles.ppm'.
94
124
  * */
95
- matchParticleCount = () => {
96
- this.#getParticleCount()
125
+ matchParticleCount() {
126
+ this.#updateParticleCount()
97
127
 
98
128
  this.particles = this.particles.slice(0, this.particleCount)
99
129
  while (this.particleCount > this.particles.length) this.createParticle()
100
130
  }
101
131
 
102
- createParticle = (posX, posY, dir, speed, size) => {
132
+ createParticle(posX, posY, dir, speed, size) {
103
133
  size = size || 0.5 + Math.random() ** 5 * 2 * this.options.particles.relSize
104
134
 
105
135
  this.particles.push({
@@ -118,7 +148,7 @@
118
148
  this.#updateParticleBounds()
119
149
  }
120
150
 
121
- #updateParticleBounds = () => {
151
+ #updateParticleBounds() {
122
152
  this.particles.map(
123
153
  particle =>
124
154
  // Within these bounds the particle is considered visible
@@ -134,8 +164,10 @@
134
164
  /**
135
165
  * Calculates the gravity properties of each particle on the next frame.
136
166
  * Is executed once every 'options.framesPerUpdate' frames.
167
+ *
168
+ * @private
137
169
  * */
138
- #updateGravity = () => {
170
+ #updateGravity() {
139
171
  const isRepulsiveEnabled = this.options.gravity.repulsive !== 0
140
172
  const isPullingEnabled = this.options.gravity.pulling !== 0
141
173
 
@@ -194,8 +226,10 @@
194
226
  /**
195
227
  * Calculates the properties of each particle on the next frame.
196
228
  * Is executed once every 'options.framesPerUpdate' frames.
229
+ *
230
+ * @private
197
231
  * */
198
- #updateParticles = () => {
232
+ #updateParticles() {
199
233
  for (let particle of this.particles) {
200
234
  // Moving the particle
201
235
  particle.dir = (particle.dir + Math.random() * this.options.particles.rotationSpeed * 2 - this.options.particles.rotationSpeed) % (2 * Math.PI)
@@ -237,7 +271,7 @@
237
271
 
238
272
  /**
239
273
  * Determines the location of the particle in a 3x3 grid on the canvas.
240
- * The grid represents different regions of the canvas
274
+ * The grid represents different regions of the canvas:
241
275
  *
242
276
  * - { x: 0, y: 0 } = top-left
243
277
  * - { x: 1, y: 0 } = top
@@ -249,6 +283,7 @@
249
283
  * - { x: 1, y: 2 } = bottom
250
284
  * - { x: 2, y: 2 } = bottom-right
251
285
  *
286
+ * @private
252
287
  * @param {Object} particle - The coordinates of the particle.
253
288
  * @param {number} particle.x - The x-coordinate of the particle.
254
289
  * @param {number} particle.y - The y-coordinate of the particle.
@@ -256,7 +291,7 @@
256
291
  * @returns {number} x - The horizontal grid position (0, 1, or 2).
257
292
  * @returns {number} y - The vertical grid position (0, 1, or 2).
258
293
  */
259
- #gridPos = particle => {
294
+ #gridPos(particle) {
260
295
  return {
261
296
  x: (particle.x >= particle.bounds.left) + (particle.x > particle.bounds.right),
262
297
  y: (particle.y >= particle.bounds.top) + (particle.y > particle.bounds.bottom),
@@ -265,6 +300,8 @@
265
300
 
266
301
  /**
267
302
  * Determines whether a line between 2 particles crosses through the visible center of the canvas.
303
+ *
304
+ * @private
268
305
  * @param {Object} particleA - First particle with {gridPos, isVisible}.
269
306
  * @param {Object} particleB - Second particle with {gridPos, isVisible}.
270
307
  * @returns {boolean} - True if the line crosses the visible center, false otherwise.
@@ -284,6 +321,7 @@
284
321
  * Precomputes and caches stroke style strings for a given base color and all possible alpha values (0–255).
285
322
  * This is necessary because the rendering process involves up to [particles ** 2 / 2] lookups per frame.
286
323
  *
324
+ * @private
287
325
  * @param {string} color - The base color in the format `#rrggbb`.
288
326
  * @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format `#rrggbbaa`.
289
327
  *
@@ -297,7 +335,7 @@
297
335
  * hexadecimal alpha value (0x00–0xFF) to the base color.
298
336
  * - The table is stored in `this.strokeStyleTable` for quick lookups.
299
337
  */
300
- #generateStrokeStyleTable = color => {
338
+ #generateStrokeStyleTable(color) {
301
339
  const table = {}
302
340
 
303
341
  // Precompute stroke styles for alpha values 0–255
@@ -308,7 +346,12 @@
308
346
  return table
309
347
  }
310
348
 
311
- #renderParticles = () => {
349
+ /**
350
+ * Renders the particles on the canvas.
351
+ *
352
+ * @private
353
+ */
354
+ #renderParticles() {
312
355
  for (let particle of this.particles) {
313
356
  if (particle.isVisible) {
314
357
  // Draw the particle as a square if the size is smaller than 1 pixel (±183% faster than drawing only circles, using default settings)
@@ -328,8 +371,10 @@
328
371
 
329
372
  /**
330
373
  * Connects particles with lines if they are within the connection distance.
374
+ *
375
+ * @private
331
376
  */
332
- #renderConnections = () => {
377
+ #renderConnections() {
333
378
  const len = this.particleCount
334
379
  const drawAll = this.options.particles.connectDist >= Math.min(this.canvas.width, this.canvas.height)
335
380
 
@@ -375,8 +420,10 @@
375
420
 
376
421
  /**
377
422
  * Clear the canvas and render the particles and their connections onto the canvas.
423
+ *
424
+ * @private
378
425
  */
379
- #render = () => {
426
+ #render() {
380
427
  this.canvas.width = this.canvas.width
381
428
  this.ctx.fillStyle = this.options.particles.colorWithAlpha
382
429
  this.ctx.lineWidth = 1
@@ -387,9 +434,11 @@
387
434
 
388
435
  /**
389
436
  * Main animation loop that updates and renders the particles.
390
- * Runs recursively using `requestAnimationFrame`.
437
+ * Runs recursively using 'requestAnimationFrame'.
438
+ *
439
+ * @private
391
440
  */
392
- #animation = () => {
441
+ #animation() {
393
442
  if (!this.animating) return
394
443
 
395
444
  requestAnimationFrame(() => this.#animation())
@@ -408,20 +457,26 @@
408
457
 
409
458
  /**
410
459
  * Starts the particle animation.
411
- * If already animating, does nothing.
460
+ * Does nothing if already animating.
461
+ *
462
+ * @returns {CanvasParticles} - The current instance.
412
463
  */
413
- start = () => {
414
- if (this.animating) return
415
- this.animating = true
416
- requestAnimationFrame(() => this.#animation())
464
+ start() {
465
+ if (!this.animating) {
466
+ this.animating = true
467
+ requestAnimationFrame(() => this.#animation())
468
+ }
469
+ return this
417
470
  }
418
471
 
419
472
  /**
420
473
  * Stops the particle animation and clears the canvas.
421
474
  */
422
- stop = () => {
475
+ stop(options) {
423
476
  this.animating = false
424
- this.canvas.width = this.canvas.width
477
+ if (options?.clear !== false) this.canvas.width = this.canvas.width
478
+
479
+ return true
425
480
  }
426
481
 
427
482
  /**
@@ -429,36 +484,40 @@
429
484
  */
430
485
 
431
486
  /**
432
- * Set and validate the options object
487
+ * Set and validate the options object.
433
488
  * @param {Object} options - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
434
489
  */
435
- setOptions = options => {
436
- const defaultIfNaN = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
490
+ setOptions(options) {
491
+ const parse = this.#defaultIfNaN
437
492
 
438
493
  // Format and store options
439
494
  this.options = {
440
495
  background: options.background ?? false,
441
- framesPerUpdate: defaultIfNaN(Math.max(1, parseInt(options.framesPerUpdate)), 1),
442
- resetOnResize: !!options.resetOnResize,
496
+ framesPerUpdate: parse(Math.max(1, parseInt(options.framesPerUpdate)), 1),
497
+ animation: {
498
+ startOnEnter: !!(options.animation?.startOnEnter ?? true),
499
+ stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
500
+ },
443
501
  mouse: {
444
- interactionType: defaultIfNaN(parseInt(options.mouse?.interactionType), 1),
445
- connectDistMult: defaultIfNaN(options.mouse?.connectDistMult, 2 / 3),
446
- distRatio: defaultIfNaN(options.mouse?.distRatio, 2 / 3),
502
+ interactionType: parse(parseInt(options.mouse?.interactionType), 1),
503
+ connectDistMult: parse(options.mouse?.connectDistMult, 2 / 3),
504
+ distRatio: parse(options.mouse?.distRatio, 2 / 3),
447
505
  },
448
506
  particles: {
507
+ regenerateOnResize: !!options.particles?.regenerateOnResize,
449
508
  color: options.particles?.color ?? 'black',
450
- ppm: defaultIfNaN(options.particles?.ppm, 100),
451
- max: defaultIfNaN(options.particles?.max, 500),
452
- maxWork: defaultIfNaN(Math.max(0, options.particles?.maxWork), Infinity),
453
- connectDist: defaultIfNaN(Math.max(1, options.particles?.connectDistance), 150),
454
- relSpeed: defaultIfNaN(Math.max(0, options.particles?.relSpeed), 1),
455
- relSize: defaultIfNaN(Math.max(0, options.particles?.relSize), 1),
456
- rotationSpeed: defaultIfNaN(Math.max(0, options.particles?.rotationSpeed / 100), 0.02),
509
+ ppm: parse(options.particles?.ppm, 100),
510
+ max: parse(options.particles?.max, 500),
511
+ maxWork: parse(Math.max(0, options.particles?.maxWork), Infinity),
512
+ connectDist: parse(Math.max(1, options.particles?.connectDistance), 150),
513
+ relSpeed: parse(Math.max(0, options.particles?.relSpeed), 1),
514
+ relSize: parse(Math.max(0, options.particles?.relSize), 1),
515
+ rotationSpeed: parse(Math.max(0, options.particles?.rotationSpeed / 100), 0.02),
457
516
  },
458
517
  gravity: {
459
- repulsive: defaultIfNaN(options.gravity?.repulsive, 0),
460
- pulling: defaultIfNaN(options.gravity?.pulling, 0),
461
- friction: defaultIfNaN(Math.max(0, Math.min(1, options.particles?.friction)), 0.8),
518
+ repulsive: parse(options.gravity?.repulsive, 0),
519
+ pulling: parse(options.gravity?.pulling, 0),
520
+ friction: parse(Math.max(0, Math.min(1, options.particles?.friction)), 0.8),
462
521
  },
463
522
  }
464
523
 
@@ -468,37 +527,38 @@
468
527
  }
469
528
 
470
529
  /**
471
- * Set canvas background
530
+ * Set canvas background.
472
531
  * @param {string} background - The style of the background. Can be any CSS supported background format.
473
532
  */
474
- setBackground = background => {
475
- if (typeof background === 'string') this.canvas.style.background = this.options.background = background
533
+ setBackground(background) {
534
+ if (typeof background === 'string') return (this.canvas.style.background = this.options.background = background), true
535
+ return false
476
536
  }
477
537
 
478
538
  /**
479
- * Transform distance multiplier to absolute distance
539
+ * Transform distance multiplier to absolute distance.
480
540
  * @param {float} connectDistMult - The maximum distance for the mouse to interact with the particles.
481
- * The value is multiplied by particles.connectDistance
541
+ * The value is multiplied by 'particles.connectDistance'.
482
542
  * @example 0.8 connectDistMult * 150 particles.connectDistance = 120 pixels
483
543
  */
484
- setMouseConnectDistMult = connectDistMult => {
485
- this.options.mouse.connectDist = this.options.particles.connectDist * (isNaN(connectDistMult) ? 2 / 3 : connectDistMult)
544
+ setMouseConnectDistMult(connectDistMult) {
545
+ this.options.mouse.connectDist = this.options.particles.connectDist * this.#defaultIfNaN(connectDistMult, 2 / 3)
486
546
  }
487
547
 
488
548
  /**
489
- * Format particle color and opacity
549
+ * Format particle color and opacity.
490
550
  * @param {string} color - The color of the particles and their connections. Can be any CSS supported color format.
491
551
  */
492
- setParticleColor = color => {
552
+ setParticleColor(color) {
493
553
  this.ctx.fillStyle = color
494
554
 
495
- // Check if `ctx.fillStyle` is in hex format ("#RRGGBB" without alpha).
555
+ // Check if 'ctx.fillStyle' is in hex format ("#RRGGBB" without alpha).
496
556
  if (this.ctx.fillStyle[0] === '#') this.options.particles.opacity = 255
497
557
  else {
498
- // JavaScript's `ctx.fillStyle` ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
558
+ // JavaScript's 'ctx.fillStyle' ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
499
559
 
500
560
  // Extract the alpha value (0.25) from the rgba string, scale it to the range 0x00 to 0xff,
501
- // and convert it to an integer. This value represents the opacity as a 2-character hex string.
561
+ // and convert it to an integer. This value represents the opacity as a 2-character hex string
502
562
  this.options.particles.opacity = ~~(this.ctx.fillStyle.split(',').at(-1).slice(1, -1) * 255)
503
563
 
504
564
  // Example: extract 136, 244 and 255 from rgba(136, 244, 255, 0.25) and convert to hexadecimal '#rrggbb' format
@@ -2,7 +2,18 @@
2
2
  // https://github.com/Khoeckman/canvasParticles/blob/main/LICENSE
3
3
 
4
4
  export default class CanvasParticles {
5
- static version = '3.4.6'
5
+ static version = '3.5.0'
6
+
7
+ // Start or stop the animation when the canvas enters or exits the viewport
8
+ static canvasObserver = new IntersectionObserver(entry => {
9
+ entry.forEach(change => {
10
+ // CanvasParticles instance of the target canvas
11
+ const instance = change.target.instance
12
+
13
+ if (change.isIntersecting) instance.options.animation.startOnEnter && instance.start()
14
+ else instance.options.animation.stopOnLeave && instance.stop({ clear: false })
15
+ })
16
+ })
6
17
 
7
18
  /**
8
19
  * Creates a new CanvasParticles instance.
@@ -16,6 +27,8 @@ export default class CanvasParticles {
16
27
  this.canvas = document.querySelector(selector)
17
28
  if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
18
29
 
30
+ this.canvas.instance = this
31
+
19
32
  // Get 2d drawing functions
20
33
  this.ctx = this.canvas.getContext('2d')
21
34
 
@@ -23,47 +36,64 @@ export default class CanvasParticles {
23
36
  this.particles = []
24
37
  this.setOptions(options)
25
38
 
26
- // Event handling
27
- window.addEventListener('resize', this.#resizeCanvas)
28
- this.#resizeCanvas()
39
+ CanvasParticles.canvasObserver.observe(this.canvas)
29
40
 
30
- window.addEventListener('mousemove', this.#updateMousePos)
31
- window.addEventListener('scroll', this.#updateMousePos)
41
+ this.#setupEventHandlers()
32
42
  }
33
43
 
34
- #updateMousePos = event => {
35
- if (!this.animating) return
44
+ // Helper function
45
+ #defaultIfNaN(value, defaultValue) {
46
+ return isNaN(+value) ? defaultValue : +value
47
+ }
36
48
 
37
- if (event instanceof MouseEvent) {
38
- this.clientX = event.clientX
39
- this.clientY = event.clientY
49
+ #setupEventHandlers() {
50
+ const updateMousePos = event => {
51
+ if (!this.animating) return
52
+
53
+ if (event instanceof MouseEvent) {
54
+ this.clientX = event.clientX
55
+ this.clientY = event.clientY
56
+ }
57
+ const { left, top } = this.canvas.getBoundingClientRect()
58
+ this.mouseX = this.clientX - left
59
+ this.mouseY = this.clientY - top
40
60
  }
41
- const { left, top } = this.canvas.getBoundingClientRect()
42
- this.mouseX = this.clientX - left
43
- this.mouseY = this.clientY - top
44
- }
45
61
 
46
- #resizeCanvas = () => {
47
- this.canvas.width = this.canvas.offsetWidth
48
- this.canvas.height = this.canvas.offsetHeight
62
+ const resizeCanvas = () => {
63
+ this.canvas.width = this.canvas.offsetWidth
64
+ this.canvas.height = this.canvas.offsetHeight
49
65
 
50
- // Prevent the mouse from affecting particles at (x: 0, y: 0) before it has moved.
51
- this.mouseX = Infinity
52
- this.mouseY = Infinity
66
+ // Prevent the mouse acting like it's at (x: 0, y: 0) before it has moved.
67
+ this.mouseX = Infinity
68
+ this.mouseY = Infinity
53
69
 
54
- this.updateCount = Infinity
55
- this.width = this.canvas.width + this.options.particles.connectDist * 2
56
- this.height = this.canvas.height + this.options.particles.connectDist * 2
57
- this.offX = (this.canvas.width - this.width) / 2
58
- this.offY = (this.canvas.height - this.height) / 2
70
+ this.updateCount = Infinity
71
+ this.width = this.canvas.width + this.options.particles.connectDist * 2
72
+ this.height = this.canvas.height + this.options.particles.connectDist * 2
73
+ this.offX = (this.canvas.width - this.width) / 2
74
+ this.offY = (this.canvas.height - this.height) / 2
59
75
 
60
- if (this.options.resetOnResize || this.particles.length === 0) this.newParticles()
61
- else this.matchParticleCount()
76
+ if (this.options.particles.regenerateOnResize || this.particles.length === 0) this.newParticles()
77
+ else this.matchParticleCount()
62
78
 
63
- this.#updateParticleBounds()
79
+ this.#updateParticleBounds()
80
+ }
81
+
82
+ window.addEventListener('resize', resizeCanvas)
83
+ resizeCanvas()
84
+
85
+ window.addEventListener('mousemove', updateMousePos)
86
+ window.addEventListener('scroll', updateMousePos)
64
87
  }
65
88
 
66
- #getParticleCount = () => {
89
+ /**
90
+ * Update the target number of particles based on the current canvas size and 'options.particles.ppm'
91
+ * Capped at 'options.particles.max'.
92
+ *
93
+ * @private
94
+ * @throws {RangeError} If the particle count is not finite.
95
+ */
96
+ #updateParticleCount() {
67
97
  // Amount of particles to be created
68
98
  const particleCount = ~~((this.options.particles.ppm * this.width * this.height) / 1_000_000)
69
99
  this.particleCount = Math.min(this.options.particles.max, particleCount)
@@ -73,26 +103,26 @@ export default class CanvasParticles {
73
103
 
74
104
  /**
75
105
  * Remove all particles and generate new ones.
76
- * The amount of new particles will match 'options.particles.ppm'
106
+ * The amount of new particles will match 'options.particles.ppm'.
77
107
  * */
78
- newParticles = () => {
79
- this.#getParticleCount()
108
+ newParticles() {
109
+ this.#updateParticleCount()
80
110
 
81
111
  this.particles = []
82
112
  for (let i = 0; i < this.particleCount; i++) this.createParticle()
83
113
  }
84
114
 
85
115
  /**
86
- * When resizing, add or remove some particles so that the final amount of particles will match 'options.particles.ppm'
116
+ * When resizing, add or remove some particles so that the final amount of particles will match 'options.particles.ppm'.
87
117
  * */
88
- matchParticleCount = () => {
89
- this.#getParticleCount()
118
+ matchParticleCount() {
119
+ this.#updateParticleCount()
90
120
 
91
121
  this.particles = this.particles.slice(0, this.particleCount)
92
122
  while (this.particleCount > this.particles.length) this.createParticle()
93
123
  }
94
124
 
95
- createParticle = (posX, posY, dir, speed, size) => {
125
+ createParticle(posX, posY, dir, speed, size) {
96
126
  size = size || 0.5 + Math.random() ** 5 * 2 * this.options.particles.relSize
97
127
 
98
128
  this.particles.push({
@@ -111,7 +141,7 @@ export default class CanvasParticles {
111
141
  this.#updateParticleBounds()
112
142
  }
113
143
 
114
- #updateParticleBounds = () => {
144
+ #updateParticleBounds() {
115
145
  this.particles.map(
116
146
  particle =>
117
147
  // Within these bounds the particle is considered visible
@@ -127,8 +157,10 @@ export default class CanvasParticles {
127
157
  /**
128
158
  * Calculates the gravity properties of each particle on the next frame.
129
159
  * Is executed once every 'options.framesPerUpdate' frames.
160
+ *
161
+ * @private
130
162
  * */
131
- #updateGravity = () => {
163
+ #updateGravity() {
132
164
  const isRepulsiveEnabled = this.options.gravity.repulsive !== 0
133
165
  const isPullingEnabled = this.options.gravity.pulling !== 0
134
166
 
@@ -187,8 +219,10 @@ export default class CanvasParticles {
187
219
  /**
188
220
  * Calculates the properties of each particle on the next frame.
189
221
  * Is executed once every 'options.framesPerUpdate' frames.
222
+ *
223
+ * @private
190
224
  * */
191
- #updateParticles = () => {
225
+ #updateParticles() {
192
226
  for (let particle of this.particles) {
193
227
  // Moving the particle
194
228
  particle.dir = (particle.dir + Math.random() * this.options.particles.rotationSpeed * 2 - this.options.particles.rotationSpeed) % (2 * Math.PI)
@@ -230,7 +264,7 @@ export default class CanvasParticles {
230
264
 
231
265
  /**
232
266
  * Determines the location of the particle in a 3x3 grid on the canvas.
233
- * The grid represents different regions of the canvas
267
+ * The grid represents different regions of the canvas:
234
268
  *
235
269
  * - { x: 0, y: 0 } = top-left
236
270
  * - { x: 1, y: 0 } = top
@@ -242,6 +276,7 @@ export default class CanvasParticles {
242
276
  * - { x: 1, y: 2 } = bottom
243
277
  * - { x: 2, y: 2 } = bottom-right
244
278
  *
279
+ * @private
245
280
  * @param {Object} particle - The coordinates of the particle.
246
281
  * @param {number} particle.x - The x-coordinate of the particle.
247
282
  * @param {number} particle.y - The y-coordinate of the particle.
@@ -249,7 +284,7 @@ export default class CanvasParticles {
249
284
  * @returns {number} x - The horizontal grid position (0, 1, or 2).
250
285
  * @returns {number} y - The vertical grid position (0, 1, or 2).
251
286
  */
252
- #gridPos = particle => {
287
+ #gridPos(particle) {
253
288
  return {
254
289
  x: (particle.x >= particle.bounds.left) + (particle.x > particle.bounds.right),
255
290
  y: (particle.y >= particle.bounds.top) + (particle.y > particle.bounds.bottom),
@@ -258,6 +293,8 @@ export default class CanvasParticles {
258
293
 
259
294
  /**
260
295
  * Determines whether a line between 2 particles crosses through the visible center of the canvas.
296
+ *
297
+ * @private
261
298
  * @param {Object} particleA - First particle with {gridPos, isVisible}.
262
299
  * @param {Object} particleB - Second particle with {gridPos, isVisible}.
263
300
  * @returns {boolean} - True if the line crosses the visible center, false otherwise.
@@ -277,6 +314,7 @@ export default class CanvasParticles {
277
314
  * Precomputes and caches stroke style strings for a given base color and all possible alpha values (0–255).
278
315
  * This is necessary because the rendering process involves up to [particles ** 2 / 2] lookups per frame.
279
316
  *
317
+ * @private
280
318
  * @param {string} color - The base color in the format `#rrggbb`.
281
319
  * @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format `#rrggbbaa`.
282
320
  *
@@ -290,7 +328,7 @@ export default class CanvasParticles {
290
328
  * hexadecimal alpha value (0x00–0xFF) to the base color.
291
329
  * - The table is stored in `this.strokeStyleTable` for quick lookups.
292
330
  */
293
- #generateStrokeStyleTable = color => {
331
+ #generateStrokeStyleTable(color) {
294
332
  const table = {}
295
333
 
296
334
  // Precompute stroke styles for alpha values 0–255
@@ -301,7 +339,12 @@ export default class CanvasParticles {
301
339
  return table
302
340
  }
303
341
 
304
- #renderParticles = () => {
342
+ /**
343
+ * Renders the particles on the canvas.
344
+ *
345
+ * @private
346
+ */
347
+ #renderParticles() {
305
348
  for (let particle of this.particles) {
306
349
  if (particle.isVisible) {
307
350
  // Draw the particle as a square if the size is smaller than 1 pixel (±183% faster than drawing only circles, using default settings)
@@ -321,8 +364,10 @@ export default class CanvasParticles {
321
364
 
322
365
  /**
323
366
  * Connects particles with lines if they are within the connection distance.
367
+ *
368
+ * @private
324
369
  */
325
- #renderConnections = () => {
370
+ #renderConnections() {
326
371
  const len = this.particleCount
327
372
  const drawAll = this.options.particles.connectDist >= Math.min(this.canvas.width, this.canvas.height)
328
373
 
@@ -368,8 +413,10 @@ export default class CanvasParticles {
368
413
 
369
414
  /**
370
415
  * Clear the canvas and render the particles and their connections onto the canvas.
416
+ *
417
+ * @private
371
418
  */
372
- #render = () => {
419
+ #render() {
373
420
  this.canvas.width = this.canvas.width
374
421
  this.ctx.fillStyle = this.options.particles.colorWithAlpha
375
422
  this.ctx.lineWidth = 1
@@ -380,9 +427,11 @@ export default class CanvasParticles {
380
427
 
381
428
  /**
382
429
  * Main animation loop that updates and renders the particles.
383
- * Runs recursively using `requestAnimationFrame`.
430
+ * Runs recursively using 'requestAnimationFrame'.
431
+ *
432
+ * @private
384
433
  */
385
- #animation = () => {
434
+ #animation() {
386
435
  if (!this.animating) return
387
436
 
388
437
  requestAnimationFrame(() => this.#animation())
@@ -401,20 +450,26 @@ export default class CanvasParticles {
401
450
 
402
451
  /**
403
452
  * Starts the particle animation.
404
- * If already animating, does nothing.
453
+ * Does nothing if already animating.
454
+ *
455
+ * @returns {CanvasParticles} - The current instance.
405
456
  */
406
- start = () => {
407
- if (this.animating) return
408
- this.animating = true
409
- requestAnimationFrame(() => this.#animation())
457
+ start() {
458
+ if (!this.animating) {
459
+ this.animating = true
460
+ requestAnimationFrame(() => this.#animation())
461
+ }
462
+ return this
410
463
  }
411
464
 
412
465
  /**
413
466
  * Stops the particle animation and clears the canvas.
414
467
  */
415
- stop = () => {
468
+ stop(options) {
416
469
  this.animating = false
417
- this.canvas.width = this.canvas.width
470
+ if (options?.clear !== false) this.canvas.width = this.canvas.width
471
+
472
+ return true
418
473
  }
419
474
 
420
475
  /**
@@ -422,36 +477,40 @@ export default class CanvasParticles {
422
477
  */
423
478
 
424
479
  /**
425
- * Set and validate the options object
480
+ * Set and validate the options object.
426
481
  * @param {Object} options - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
427
482
  */
428
- setOptions = options => {
429
- const defaultIfNaN = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
483
+ setOptions(options) {
484
+ const parse = this.#defaultIfNaN
430
485
 
431
486
  // Format and store options
432
487
  this.options = {
433
488
  background: options.background ?? false,
434
- framesPerUpdate: defaultIfNaN(Math.max(1, parseInt(options.framesPerUpdate)), 1),
435
- resetOnResize: !!options.resetOnResize,
489
+ framesPerUpdate: parse(Math.max(1, parseInt(options.framesPerUpdate)), 1),
490
+ animation: {
491
+ startOnEnter: !!(options.animation?.startOnEnter ?? true),
492
+ stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
493
+ },
436
494
  mouse: {
437
- interactionType: defaultIfNaN(parseInt(options.mouse?.interactionType), 1),
438
- connectDistMult: defaultIfNaN(options.mouse?.connectDistMult, 2 / 3),
439
- distRatio: defaultIfNaN(options.mouse?.distRatio, 2 / 3),
495
+ interactionType: parse(parseInt(options.mouse?.interactionType), 1),
496
+ connectDistMult: parse(options.mouse?.connectDistMult, 2 / 3),
497
+ distRatio: parse(options.mouse?.distRatio, 2 / 3),
440
498
  },
441
499
  particles: {
500
+ regenerateOnResize: !!options.particles?.regenerateOnResize,
442
501
  color: options.particles?.color ?? 'black',
443
- ppm: defaultIfNaN(options.particles?.ppm, 100),
444
- max: defaultIfNaN(options.particles?.max, 500),
445
- maxWork: defaultIfNaN(Math.max(0, options.particles?.maxWork), Infinity),
446
- connectDist: defaultIfNaN(Math.max(1, options.particles?.connectDistance), 150),
447
- relSpeed: defaultIfNaN(Math.max(0, options.particles?.relSpeed), 1),
448
- relSize: defaultIfNaN(Math.max(0, options.particles?.relSize), 1),
449
- rotationSpeed: defaultIfNaN(Math.max(0, options.particles?.rotationSpeed / 100), 0.02),
502
+ ppm: parse(options.particles?.ppm, 100),
503
+ max: parse(options.particles?.max, 500),
504
+ maxWork: parse(Math.max(0, options.particles?.maxWork), Infinity),
505
+ connectDist: parse(Math.max(1, options.particles?.connectDistance), 150),
506
+ relSpeed: parse(Math.max(0, options.particles?.relSpeed), 1),
507
+ relSize: parse(Math.max(0, options.particles?.relSize), 1),
508
+ rotationSpeed: parse(Math.max(0, options.particles?.rotationSpeed / 100), 0.02),
450
509
  },
451
510
  gravity: {
452
- repulsive: defaultIfNaN(options.gravity?.repulsive, 0),
453
- pulling: defaultIfNaN(options.gravity?.pulling, 0),
454
- friction: defaultIfNaN(Math.max(0, Math.min(1, options.particles?.friction)), 0.8),
511
+ repulsive: parse(options.gravity?.repulsive, 0),
512
+ pulling: parse(options.gravity?.pulling, 0),
513
+ friction: parse(Math.max(0, Math.min(1, options.particles?.friction)), 0.8),
455
514
  },
456
515
  }
457
516
 
@@ -461,37 +520,38 @@ export default class CanvasParticles {
461
520
  }
462
521
 
463
522
  /**
464
- * Set canvas background
523
+ * Set canvas background.
465
524
  * @param {string} background - The style of the background. Can be any CSS supported background format.
466
525
  */
467
- setBackground = background => {
468
- if (typeof background === 'string') this.canvas.style.background = this.options.background = background
526
+ setBackground(background) {
527
+ if (typeof background === 'string') return (this.canvas.style.background = this.options.background = background), true
528
+ return false
469
529
  }
470
530
 
471
531
  /**
472
- * Transform distance multiplier to absolute distance
532
+ * Transform distance multiplier to absolute distance.
473
533
  * @param {float} connectDistMult - The maximum distance for the mouse to interact with the particles.
474
- * The value is multiplied by particles.connectDistance
534
+ * The value is multiplied by 'particles.connectDistance'.
475
535
  * @example 0.8 connectDistMult * 150 particles.connectDistance = 120 pixels
476
536
  */
477
- setMouseConnectDistMult = connectDistMult => {
478
- this.options.mouse.connectDist = this.options.particles.connectDist * (isNaN(connectDistMult) ? 2 / 3 : connectDistMult)
537
+ setMouseConnectDistMult(connectDistMult) {
538
+ this.options.mouse.connectDist = this.options.particles.connectDist * this.#defaultIfNaN(connectDistMult, 2 / 3)
479
539
  }
480
540
 
481
541
  /**
482
- * Format particle color and opacity
542
+ * Format particle color and opacity.
483
543
  * @param {string} color - The color of the particles and their connections. Can be any CSS supported color format.
484
544
  */
485
- setParticleColor = color => {
545
+ setParticleColor(color) {
486
546
  this.ctx.fillStyle = color
487
547
 
488
- // Check if `ctx.fillStyle` is in hex format ("#RRGGBB" without alpha).
548
+ // Check if 'ctx.fillStyle' is in hex format ("#RRGGBB" without alpha).
489
549
  if (this.ctx.fillStyle[0] === '#') this.options.particles.opacity = 255
490
550
  else {
491
- // JavaScript's `ctx.fillStyle` ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
551
+ // JavaScript's 'ctx.fillStyle' ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
492
552
 
493
553
  // Extract the alpha value (0.25) from the rgba string, scale it to the range 0x00 to 0xff,
494
- // and convert it to an integer. This value represents the opacity as a 2-character hex string.
554
+ // and convert it to an integer. This value represents the opacity as a 2-character hex string
495
555
  this.options.particles.opacity = ~~(this.ctx.fillStyle.split(',').at(-1).slice(1, -1) * 255)
496
556
 
497
557
  // Example: extract 136, 244 and 255 from rgba(136, 244, 255, 0.25) and convert to hexadecimal '#rrggbb' format
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "3.4.6",
3
+ "version": "3.5.0",
4
4
  "description": "In an HTML canvas, a bunch of interactive particles connected with lines when they approach each other.",
5
5
  "main": "canvasParticles.js",
6
6
  "module": "canvasParticles.mjs",