canvasparticles-js 3.5.1 → 3.5.3

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
@@ -243,9 +243,10 @@ const options = {
243
243
  /** @param {Object} [options.mouse] - Mouse interaction settings. */
244
244
  mouse: {
245
245
  /** @param {0|1|2} [options.mouse.interactionType=1] - The type of interaction the mouse will have with particles.
246
- * 0 = No interaction.
247
- * 1 = The mouse can visually shift the particles.
248
- * 2 = The mouse can move the particles.
246
+ *
247
+ * CanvasParticles.interactionType.NONE = 0 = No interaction.
248
+ * CanvasParticles.interactionType.SHIFT = 1 = The mouse can visually shift the particles.
249
+ * CanvasParticles.interactionType.MOVE = 2 = The mouse can move the particles.
249
250
  * @note mouse.distRatio should be less than 1 to allow dragging, closer to 0 is easier to drag
250
251
  */
251
252
  interactionType: 2,
@@ -418,7 +419,7 @@ particles.setOptions(options)
418
419
  const options = {
419
420
  background: 'hsl(125, 42%, 35%)',
420
421
  mouse: {
421
- interactionType: 2,
422
+ interactionType: CanvasParticles.interactionType.MOVE, // === 2
422
423
  },
423
424
  particles: {
424
425
  color: 'rgba(150, 255, 105, 0.95)',
@@ -1,5 +1,5 @@
1
1
  // Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
2
- // https://github.com/Khoeckman/canvasParticles/blob/main/LICENSE
2
+ // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
3
3
 
4
4
  'use strict'
5
5
  ;((root, factory) =>
@@ -9,14 +9,20 @@
9
9
  typeof self !== 'undefined' ? self : this,
10
10
  () =>
11
11
  class CanvasParticles {
12
- static version = '3.5.1'
12
+ static version = '3.5.3'
13
13
 
14
- // Start or stop the animation when the canvas enters or exits the viewport
14
+ // Mouse interaction with the particles.
15
+ static interactionType = Object.freeze({
16
+ NONE: 0, // No interaction
17
+ SHIFT: 1, // Visually shift the particles
18
+ MOVE: 2, // Actually move the particles
19
+ })
20
+
21
+ // Start or stop the animation when the canvas enters or exits the viewport.
15
22
  static canvasObserver = new IntersectionObserver(entry => {
16
23
  entry.forEach(change => {
17
- // CanvasParticles instance of the target canvas
18
24
  const canvas = change.target
19
- const instance = canvas.instance
25
+ const instance = canvas.instance // The 'CanvasParticles' instance bound to 'canvas'.
20
26
 
21
27
  if ((canvas.inViewbox = change.isIntersecting)) instance.options.animation.startOnEnter && instance.start({ auto: true })
22
28
  else instance.options.animation.stopOnLeave && instance.stop({ auto: true, clear: false })
@@ -29,18 +35,18 @@
29
35
  * @param {Object} [options={}] - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
30
36
  */
31
37
  constructor(selector, options = {}) {
38
+ // Find the HTMLCanvasElement and assign it to 'this.canvas'.
32
39
  if (selector instanceof HTMLCanvasElement) this.canvas = selector
33
40
  else {
34
- // Find and initialize canvas
35
41
  if (typeof selector !== 'string') throw new TypeError('selector is not a string and neither a HTMLCanvasElement itself')
36
42
 
37
43
  this.canvas = document.querySelector(selector)
38
44
  if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
39
45
  }
40
- this.canvas.instance = this // Circular assignment to find the instance bound to this canvas inside the 'canvasObserver'
46
+ this.canvas.instance = this // Circular assignment to find the instance bound to this canvas inside the static 'canvasObserver' callback.
41
47
  this.canvas.inViewbox = true
42
48
 
43
- // Get 2d drawing functions
49
+ // Get 2d drawing methods.
44
50
  this.ctx = this.canvas.getContext('2d')
45
51
 
46
52
  this.enableAnimating = false
@@ -53,11 +59,6 @@
53
59
  this.#setupEventHandlers()
54
60
  }
55
61
 
56
- // Helper function
57
- #defaultIfNaN(value, defaultValue) {
58
- return isNaN(+value) ? defaultValue : +value
59
- }
60
-
61
62
  #setupEventHandlers() {
62
63
  const updateMousePos = event => {
63
64
  if (!this.enableAnimating) return
@@ -66,6 +67,8 @@
66
67
  this.clientX = event.clientX
67
68
  this.clientY = event.clientY
68
69
  }
70
+
71
+ // On scroll, the mouse position remains the same, but since the canvas position changes, 'left' and 'top' must be recalculated.
69
72
  const { left, top } = this.canvas.getBoundingClientRect()
70
73
  this.mouseX = this.clientX - left
71
74
  this.mouseY = this.clientY - top
@@ -99,7 +102,7 @@
99
102
  }
100
103
 
101
104
  /**
102
- * Update the target number of particles based on the current canvas size and 'options.particles.ppm'
105
+ * Update the target number of particles based on the current canvas size and 'options.particles.ppm'.
103
106
  * Capped at 'options.particles.max'.
104
107
  *
105
108
  * @private
@@ -156,7 +159,7 @@
156
159
  #updateParticleBounds() {
157
160
  this.particles.map(
158
161
  particle =>
159
- // Within these bounds the particle is considered visible
162
+ // Within these bounds the particle is considered visible.
160
163
  (particle.bounds = {
161
164
  top: -particle.size,
162
165
  right: this.canvas.width + particle.size,
@@ -185,7 +188,7 @@
185
188
 
186
189
  for (let i = 0; i < len; i++) {
187
190
  for (let j = i + 1; j < len; j++) {
188
- // Code in this scope runs [particles ** 2 / 2] times!
191
+ // Code in this scope runs [particleCount ** 2 / 2] times!
189
192
  const particleA = this.particles[i]
190
193
  const particleB = this.particles[j]
191
194
 
@@ -197,7 +200,7 @@
197
200
  let angle, grav
198
201
 
199
202
  if (dist < maxRepulsiveDist) {
200
- // Apply repulsive force on all particles close together
203
+ // Apply repulsive force on all particles closer than 'dist' / 2.
201
204
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
202
205
  grav = (1 / dist) ** 1.8
203
206
  const gravMult = Math.min(maxGrav, grav * gravRepulsiveMult)
@@ -211,7 +214,7 @@
211
214
 
212
215
  if (!isPullingEnabled) continue
213
216
 
214
- // Apply pulling force on all particles not close together
217
+ // Apply pulling force on all particles.
215
218
  if (angle === undefined) {
216
219
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
217
220
  grav = (1 / dist) ** 1.8
@@ -236,7 +239,7 @@
236
239
  * */
237
240
  #updateParticles() {
238
241
  for (let particle of this.particles) {
239
- // Moving the particle
242
+ // Slightly, randomly change the particle's direction and move it in that direction.
240
243
  particle.dir = (particle.dir + Math.random() * this.options.particles.rotationSpeed * 2 - this.options.particles.rotationSpeed) % (2 * Math.PI)
241
244
  particle.velX *= this.options.gravity.friction
242
245
  particle.velY *= this.options.gravity.friction
@@ -246,8 +249,8 @@
246
249
  const distX = particle.posX + this.offX - this.mouseX
247
250
  const distY = particle.posY + this.offY - this.mouseY
248
251
 
249
- // Mouse events
250
- if (this.options.mouse.interactionType !== 0) {
252
+ // If the 'interactionType' is not 'NONE', calculate how much to move the particle away from the mouse.
253
+ if (this.options.mouse.interactionType !== CanvasParticles.interactionType.NONE) {
251
254
  const distRatio = this.options.mouse.connectDist / Math.hypot(distX, distY)
252
255
 
253
256
  if (this.options.mouse.distRatio < distRatio) {
@@ -258,18 +261,20 @@
258
261
  particle.offY -= particle.offY / 4
259
262
  }
260
263
  }
264
+
265
+ // Visually shift the particles
261
266
  particle.x = particle.posX + particle.offX
262
267
  particle.y = particle.posY + particle.offY
263
268
 
264
- if (this.options.mouse.interactionType === 2) {
265
- // Make the mouse actually move the particles
269
+ // Actually move the particles if 'interactionType' is 'MOVE'.
270
+ if (this.options.mouse.interactionType === CanvasParticles.interactionType.MOVE) {
266
271
  particle.posX = particle.x
267
272
  particle.posY = particle.y
268
273
  }
269
274
  particle.x += this.offX
270
275
  particle.y += this.offY
271
276
 
272
- particle.gridPos = this.#gridPos(particle) // The location of the particle relative to the visible center of the canvas
277
+ particle.gridPos = this.#gridPos(particle)
273
278
  particle.isVisible = particle.gridPos.x === 1 && particle.gridPos.y === 1
274
279
  }
275
280
  }
@@ -312,10 +317,10 @@
312
317
  * @returns {boolean} - True if the line crosses the visible center, false otherwise.
313
318
  */
314
319
  #isLineVisible(particleA, particleB) {
315
- // Visible if either particle is in the center
320
+ // Visible if either particle is in the center.
316
321
  if (particleA.isVisible || particleB.isVisible) return true
317
322
 
318
- // Not visible if both particles are in the same vertical or horizontal line but outside the center
323
+ // Not visible if both particles are in the same vertical or horizontal line but outside the center.
319
324
  return !(
320
325
  (particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
321
326
  (particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
@@ -324,7 +329,7 @@
324
329
 
325
330
  /**
326
331
  * Precomputes and caches stroke style strings for a given base color and all possible alpha values (0–255).
327
- * This is necessary because the rendering process involves up to [particles ** 2 / 2] lookups per frame.
332
+ * This is necessary because the rendering process involves up to [particleCount ** 2 / 2] lookups per frame.
328
333
  *
329
334
  * @private
330
335
  * @param {string} color - The base color in the format '#rrggbb'.
@@ -336,8 +341,7 @@
336
341
  * strokeStyleTable[255] -> "#abcdefff"
337
342
  *
338
343
  * Notes:
339
- * - This function precomputes all possible stroke styles by appending a two-character
340
- * hexadecimal alpha value (0x00–0xFF) to the base color.
344
+ * - This function precomputes all possible stroke styles by appending a two-character hexadecimal alpha value (0x00–0xFF) to the base color.
341
345
  * - The table is stored in 'this.strokeStyleTable' for quick lookups.
342
346
  */
343
347
  #generateStrokeStyleTable(color) {
@@ -359,7 +363,8 @@
359
363
  #renderParticles() {
360
364
  for (let particle of this.particles) {
361
365
  if (particle.isVisible) {
362
- // Draw the particle as a square if the size is smaller than 1 pixel (±183% faster than drawing only circles, using default settings)
366
+ // Draw the particle as a square if the size is smaller than 1 pixel.
367
+ // This is ±183% faster than drawing all particle's as circles.
363
368
  if (particle.size > 1) {
364
369
  // Draw circle
365
370
  this.ctx.beginPath()
@@ -367,7 +372,7 @@
367
372
  this.ctx.fill()
368
373
  this.ctx.closePath()
369
374
  } else {
370
- // Draw square (±335% faster than circle)
375
+ // Draw square
371
376
  this.ctx.fillRect(particle.x - particle.size, particle.y - particle.size, particle.size * 2, particle.size * 2)
372
377
  }
373
378
  }
@@ -389,22 +394,23 @@
389
394
  let particleWork = 0
390
395
 
391
396
  for (let j = i + 1; j < len; j++) {
392
- // Code in this scope runs [particles ** 2 / 2] times!
397
+ // Code in this scope runs [particleCount ** 2 / 2] times!
393
398
  const particleA = this.particles[i]
394
399
  const particleB = this.particles[j]
395
400
 
396
401
  if (!(drawAll || this.#isLineVisible(particleA, particleB))) continue
397
- // Draw a line only if will be visible
402
+ // Draw a line only if it's visible.
398
403
 
399
404
  const distX = particleA.x - particleB.x
400
405
  const distY = particleA.y - particleB.y
401
406
 
402
407
  const dist = Math.sqrt(distX * distX + distY * distY)
403
408
 
409
+ // Don't connect the 2 particles with a line if their distance is greater than 'options.particles.connectDist'.
404
410
  if (dist > this.options.particles.connectDist) continue
405
- // Connect the 2 particles with a line only if the distance is small enough
406
411
 
407
- // Calculate the transparency of the line and lookup the stroke style
412
+ // Calculate the transparency of the line and lookup the stroke style.
413
+ // This is the heaviest task of the entire animation process.
408
414
  if (dist > this.options.particles.connectDist / 2) {
409
415
  const alpha = ~~(Math.min(this.options.particles.connectDist / dist - 1, 1) * this.options.particles.opacity)
410
416
  this.ctx.strokeStyle = this.strokeStyleTable[alpha]
@@ -412,12 +418,13 @@
412
418
  this.ctx.strokeStyle = this.options.particles.colorWithAlpha
413
419
  }
414
420
 
415
- // Draw the line
421
+ // Draw the line.
416
422
  this.ctx.beginPath()
417
423
  this.ctx.moveTo(particleA.x, particleA.y)
418
424
  this.ctx.lineTo(particleB.x, particleB.y)
419
425
  this.ctx.stroke()
420
426
 
427
+ // Stop drawing lines from this particles if it has already drawn to many.
421
428
  if ((particleWork += dist) >= maxWorkPerParticle) break
422
429
  }
423
430
  }
@@ -477,7 +484,7 @@
477
484
  requestAnimationFrame(() => this.#animation())
478
485
  }
479
486
 
480
- // Stop animating because it will start automatically once the canvas enters the viewbox
487
+ // Stop animating because it will start automatically once the canvas enters the viewbox.
481
488
  if (!this.canvas.inViewbox && this.options.animation.startOnEnter) this.animating = false
482
489
 
483
490
  return this
@@ -510,9 +517,10 @@
510
517
  * @param {Object} options - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
511
518
  */
512
519
  setOptions(options) {
513
- const parse = this.#defaultIfNaN
520
+ // Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
521
+ const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
514
522
 
515
- // Format and store options
523
+ // Format or default all options.
516
524
  this.options = {
517
525
  background: options.background ?? false,
518
526
  framesPerUpdate: parse(Math.max(1, parseInt(options.framesPerUpdate)), 1),
@@ -567,7 +575,7 @@
567
575
  * @example 0.8 connectDistMult * 150 particles.connectDistance = 120 pixels
568
576
  */
569
577
  setMouseConnectDistMult(connectDistMult) {
570
- this.options.mouse.connectDist = this.options.particles.connectDist * this.#defaultIfNaN(connectDistMult, 2 / 3)
578
+ this.options.mouse.connectDist = this.options.particles.connectDist * (isNaN(connectDistMult) ? 2 / 3 : connectDistMult)
571
579
  }
572
580
 
573
581
  /**
@@ -583,16 +591,17 @@
583
591
  // JavaScript's 'ctx.fillStyle' ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
584
592
 
585
593
  // Extract the alpha value (0.25) from the rgba string, scale it to the range 0x00 to 0xff,
586
- // and convert it to an integer. This value represents the opacity as a 2-character hex string
594
+ // and convert it to an integer. This value represents the opacity as a 2-character hex string.
587
595
  this.options.particles.opacity = ~~(this.ctx.fillStyle.split(',').at(-1).slice(1, -1) * 255)
588
596
 
589
- // Example: extract 136, 244 and 255 from rgba(136, 244, 255, 0.25) and convert to hexadecimal '#rrggbb' format
597
+ // Example: extract 136, 244 and 255 from rgba(136, 244, 255, 0.25) and convert to hexadecimal '#rrggbb' format.
590
598
  this.ctx.fillStyle = this.ctx.fillStyle.split(',').slice(0, -1).join(',') + ', 1)'
591
599
  }
592
600
  this.options.particles.color = this.ctx.fillStyle
593
601
  this.options.particles.colorWithAlpha = this.options.particles.color + this.options.particles.opacity.toString(16)
594
602
 
595
- this.strokeStyleTable = this.#generateStrokeStyleTable(this.options.particles.color) // Recalculate the stroke style table
603
+ // Recalculate the stroke style table.
604
+ this.strokeStyleTable = this.#generateStrokeStyleTable(this.options.particles.color)
596
605
  }
597
606
  }
598
607
  )
@@ -1,15 +1,21 @@
1
1
  // Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
2
- // https://github.com/Khoeckman/canvasParticles/blob/main/LICENSE
2
+ // https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
3
3
 
4
4
  export default class CanvasParticles {
5
- static version = '3.5.1'
5
+ static version = '3.5.3'
6
6
 
7
- // Start or stop the animation when the canvas enters or exits the viewport
7
+ // Mouse interaction with the particles.
8
+ static interactionType = Object.freeze({
9
+ NONE: 0, // No interaction
10
+ SHIFT: 1, // Visually shift the particles
11
+ MOVE: 2, // Actually move the particles
12
+ })
13
+
14
+ // Start or stop the animation when the canvas enters or exits the viewport.
8
15
  static canvasObserver = new IntersectionObserver(entry => {
9
16
  entry.forEach(change => {
10
- // CanvasParticles instance of the target canvas
11
17
  const canvas = change.target
12
- const instance = canvas.instance
18
+ const instance = canvas.instance // The 'CanvasParticles' instance bound to 'canvas'.
13
19
 
14
20
  if ((canvas.inViewbox = change.isIntersecting)) instance.options.animation.startOnEnter && instance.start({ auto: true })
15
21
  else instance.options.animation.stopOnLeave && instance.stop({ auto: true, clear: false })
@@ -22,18 +28,18 @@ export default class CanvasParticles {
22
28
  * @param {Object} [options={}] - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
23
29
  */
24
30
  constructor(selector, options = {}) {
31
+ // Find the HTMLCanvasElement and assign it to 'this.canvas'.
25
32
  if (selector instanceof HTMLCanvasElement) this.canvas = selector
26
33
  else {
27
- // Find and initialize canvas
28
34
  if (typeof selector !== 'string') throw new TypeError('selector is not a string and neither a HTMLCanvasElement itself')
29
35
 
30
36
  this.canvas = document.querySelector(selector)
31
37
  if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
32
38
  }
33
- this.canvas.instance = this // Circular assignment to find the instance bound to this canvas inside the 'canvasObserver'
39
+ this.canvas.instance = this // Circular assignment to find the instance bound to this canvas inside the static 'canvasObserver' callback.
34
40
  this.canvas.inViewbox = true
35
41
 
36
- // Get 2d drawing functions
42
+ // Get 2d drawing methods.
37
43
  this.ctx = this.canvas.getContext('2d')
38
44
 
39
45
  this.enableAnimating = false
@@ -46,11 +52,6 @@ export default class CanvasParticles {
46
52
  this.#setupEventHandlers()
47
53
  }
48
54
 
49
- // Helper function
50
- #defaultIfNaN(value, defaultValue) {
51
- return isNaN(+value) ? defaultValue : +value
52
- }
53
-
54
55
  #setupEventHandlers() {
55
56
  const updateMousePos = event => {
56
57
  if (!this.enableAnimating) return
@@ -59,6 +60,8 @@ export default class CanvasParticles {
59
60
  this.clientX = event.clientX
60
61
  this.clientY = event.clientY
61
62
  }
63
+
64
+ // On scroll, the mouse position remains the same, but since the canvas position changes, 'left' and 'top' must be recalculated.
62
65
  const { left, top } = this.canvas.getBoundingClientRect()
63
66
  this.mouseX = this.clientX - left
64
67
  this.mouseY = this.clientY - top
@@ -92,7 +95,7 @@ export default class CanvasParticles {
92
95
  }
93
96
 
94
97
  /**
95
- * Update the target number of particles based on the current canvas size and 'options.particles.ppm'
98
+ * Update the target number of particles based on the current canvas size and 'options.particles.ppm'.
96
99
  * Capped at 'options.particles.max'.
97
100
  *
98
101
  * @private
@@ -149,7 +152,7 @@ export default class CanvasParticles {
149
152
  #updateParticleBounds() {
150
153
  this.particles.map(
151
154
  particle =>
152
- // Within these bounds the particle is considered visible
155
+ // Within these bounds the particle is considered visible.
153
156
  (particle.bounds = {
154
157
  top: -particle.size,
155
158
  right: this.canvas.width + particle.size,
@@ -178,7 +181,7 @@ export default class CanvasParticles {
178
181
 
179
182
  for (let i = 0; i < len; i++) {
180
183
  for (let j = i + 1; j < len; j++) {
181
- // Code in this scope runs [particles ** 2 / 2] times!
184
+ // Code in this scope runs [particleCount ** 2 / 2] times!
182
185
  const particleA = this.particles[i]
183
186
  const particleB = this.particles[j]
184
187
 
@@ -190,7 +193,7 @@ export default class CanvasParticles {
190
193
  let angle, grav
191
194
 
192
195
  if (dist < maxRepulsiveDist) {
193
- // Apply repulsive force on all particles close together
196
+ // Apply repulsive force on all particles closer than 'dist' / 2.
194
197
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
195
198
  grav = (1 / dist) ** 1.8
196
199
  const gravMult = Math.min(maxGrav, grav * gravRepulsiveMult)
@@ -204,7 +207,7 @@ export default class CanvasParticles {
204
207
 
205
208
  if (!isPullingEnabled) continue
206
209
 
207
- // Apply pulling force on all particles not close together
210
+ // Apply pulling force on all particles.
208
211
  if (angle === undefined) {
209
212
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
210
213
  grav = (1 / dist) ** 1.8
@@ -229,7 +232,7 @@ export default class CanvasParticles {
229
232
  * */
230
233
  #updateParticles() {
231
234
  for (let particle of this.particles) {
232
- // Moving the particle
235
+ // Slightly, randomly change the particle's direction and move it in that direction.
233
236
  particle.dir = (particle.dir + Math.random() * this.options.particles.rotationSpeed * 2 - this.options.particles.rotationSpeed) % (2 * Math.PI)
234
237
  particle.velX *= this.options.gravity.friction
235
238
  particle.velY *= this.options.gravity.friction
@@ -239,8 +242,8 @@ export default class CanvasParticles {
239
242
  const distX = particle.posX + this.offX - this.mouseX
240
243
  const distY = particle.posY + this.offY - this.mouseY
241
244
 
242
- // Mouse events
243
- if (this.options.mouse.interactionType !== 0) {
245
+ // If the 'interactionType' is not 'NONE', calculate how much to move the particle away from the mouse.
246
+ if (this.options.mouse.interactionType !== CanvasParticles.interactionType.NONE) {
244
247
  const distRatio = this.options.mouse.connectDist / Math.hypot(distX, distY)
245
248
 
246
249
  if (this.options.mouse.distRatio < distRatio) {
@@ -251,18 +254,20 @@ export default class CanvasParticles {
251
254
  particle.offY -= particle.offY / 4
252
255
  }
253
256
  }
257
+
258
+ // Visually shift the particles
254
259
  particle.x = particle.posX + particle.offX
255
260
  particle.y = particle.posY + particle.offY
256
261
 
257
- if (this.options.mouse.interactionType === 2) {
258
- // Make the mouse actually move the particles
262
+ // Actually move the particles if 'interactionType' is 'MOVE'.
263
+ if (this.options.mouse.interactionType === CanvasParticles.interactionType.MOVE) {
259
264
  particle.posX = particle.x
260
265
  particle.posY = particle.y
261
266
  }
262
267
  particle.x += this.offX
263
268
  particle.y += this.offY
264
269
 
265
- particle.gridPos = this.#gridPos(particle) // The location of the particle relative to the visible center of the canvas
270
+ particle.gridPos = this.#gridPos(particle)
266
271
  particle.isVisible = particle.gridPos.x === 1 && particle.gridPos.y === 1
267
272
  }
268
273
  }
@@ -305,10 +310,10 @@ export default class CanvasParticles {
305
310
  * @returns {boolean} - True if the line crosses the visible center, false otherwise.
306
311
  */
307
312
  #isLineVisible(particleA, particleB) {
308
- // Visible if either particle is in the center
313
+ // Visible if either particle is in the center.
309
314
  if (particleA.isVisible || particleB.isVisible) return true
310
315
 
311
- // Not visible if both particles are in the same vertical or horizontal line but outside the center
316
+ // Not visible if both particles are in the same vertical or horizontal line but outside the center.
312
317
  return !(
313
318
  (particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
314
319
  (particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
@@ -317,7 +322,7 @@ export default class CanvasParticles {
317
322
 
318
323
  /**
319
324
  * Precomputes and caches stroke style strings for a given base color and all possible alpha values (0–255).
320
- * This is necessary because the rendering process involves up to [particles ** 2 / 2] lookups per frame.
325
+ * This is necessary because the rendering process involves up to [particleCount ** 2 / 2] lookups per frame.
321
326
  *
322
327
  * @private
323
328
  * @param {string} color - The base color in the format '#rrggbb'.
@@ -329,8 +334,7 @@ export default class CanvasParticles {
329
334
  * strokeStyleTable[255] -> "#abcdefff"
330
335
  *
331
336
  * Notes:
332
- * - This function precomputes all possible stroke styles by appending a two-character
333
- * hexadecimal alpha value (0x00–0xFF) to the base color.
337
+ * - This function precomputes all possible stroke styles by appending a two-character hexadecimal alpha value (0x00–0xFF) to the base color.
334
338
  * - The table is stored in 'this.strokeStyleTable' for quick lookups.
335
339
  */
336
340
  #generateStrokeStyleTable(color) {
@@ -352,7 +356,8 @@ export default class CanvasParticles {
352
356
  #renderParticles() {
353
357
  for (let particle of this.particles) {
354
358
  if (particle.isVisible) {
355
- // Draw the particle as a square if the size is smaller than 1 pixel (±183% faster than drawing only circles, using default settings)
359
+ // Draw the particle as a square if the size is smaller than 1 pixel.
360
+ // This is ±183% faster than drawing all particle's as circles.
356
361
  if (particle.size > 1) {
357
362
  // Draw circle
358
363
  this.ctx.beginPath()
@@ -360,7 +365,7 @@ export default class CanvasParticles {
360
365
  this.ctx.fill()
361
366
  this.ctx.closePath()
362
367
  } else {
363
- // Draw square (±335% faster than circle)
368
+ // Draw square
364
369
  this.ctx.fillRect(particle.x - particle.size, particle.y - particle.size, particle.size * 2, particle.size * 2)
365
370
  }
366
371
  }
@@ -382,22 +387,23 @@ export default class CanvasParticles {
382
387
  let particleWork = 0
383
388
 
384
389
  for (let j = i + 1; j < len; j++) {
385
- // Code in this scope runs [particles ** 2 / 2] times!
390
+ // Code in this scope runs [particleCount ** 2 / 2] times!
386
391
  const particleA = this.particles[i]
387
392
  const particleB = this.particles[j]
388
393
 
389
394
  if (!(drawAll || this.#isLineVisible(particleA, particleB))) continue
390
- // Draw a line only if will be visible
395
+ // Draw a line only if it's visible.
391
396
 
392
397
  const distX = particleA.x - particleB.x
393
398
  const distY = particleA.y - particleB.y
394
399
 
395
400
  const dist = Math.sqrt(distX * distX + distY * distY)
396
401
 
402
+ // Don't connect the 2 particles with a line if their distance is greater than 'options.particles.connectDist'.
397
403
  if (dist > this.options.particles.connectDist) continue
398
- // Connect the 2 particles with a line only if the distance is small enough
399
404
 
400
- // Calculate the transparency of the line and lookup the stroke style
405
+ // Calculate the transparency of the line and lookup the stroke style.
406
+ // This is the heaviest task of the entire animation process.
401
407
  if (dist > this.options.particles.connectDist / 2) {
402
408
  const alpha = ~~(Math.min(this.options.particles.connectDist / dist - 1, 1) * this.options.particles.opacity)
403
409
  this.ctx.strokeStyle = this.strokeStyleTable[alpha]
@@ -405,12 +411,13 @@ export default class CanvasParticles {
405
411
  this.ctx.strokeStyle = this.options.particles.colorWithAlpha
406
412
  }
407
413
 
408
- // Draw the line
414
+ // Draw the line.
409
415
  this.ctx.beginPath()
410
416
  this.ctx.moveTo(particleA.x, particleA.y)
411
417
  this.ctx.lineTo(particleB.x, particleB.y)
412
418
  this.ctx.stroke()
413
419
 
420
+ // Stop drawing lines from this particles if it has already drawn to many.
414
421
  if ((particleWork += dist) >= maxWorkPerParticle) break
415
422
  }
416
423
  }
@@ -470,7 +477,7 @@ export default class CanvasParticles {
470
477
  requestAnimationFrame(() => this.#animation())
471
478
  }
472
479
 
473
- // Stop animating because it will start automatically once the canvas enters the viewbox
480
+ // Stop animating because it will start automatically once the canvas enters the viewbox.
474
481
  if (!this.canvas.inViewbox && this.options.animation.startOnEnter) this.animating = false
475
482
 
476
483
  return this
@@ -503,9 +510,10 @@ export default class CanvasParticles {
503
510
  * @param {Object} options - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
504
511
  */
505
512
  setOptions(options) {
506
- const parse = this.#defaultIfNaN
513
+ // Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
514
+ const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
507
515
 
508
- // Format and store options
516
+ // Format or default all options.
509
517
  this.options = {
510
518
  background: options.background ?? false,
511
519
  framesPerUpdate: parse(Math.max(1, parseInt(options.framesPerUpdate)), 1),
@@ -560,7 +568,7 @@ export default class CanvasParticles {
560
568
  * @example 0.8 connectDistMult * 150 particles.connectDistance = 120 pixels
561
569
  */
562
570
  setMouseConnectDistMult(connectDistMult) {
563
- this.options.mouse.connectDist = this.options.particles.connectDist * this.#defaultIfNaN(connectDistMult, 2 / 3)
571
+ this.options.mouse.connectDist = this.options.particles.connectDist * (isNaN(connectDistMult) ? 2 / 3 : connectDistMult)
564
572
  }
565
573
 
566
574
  /**
@@ -576,15 +584,16 @@ export default class CanvasParticles {
576
584
  // JavaScript's 'ctx.fillStyle' ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
577
585
 
578
586
  // Extract the alpha value (0.25) from the rgba string, scale it to the range 0x00 to 0xff,
579
- // and convert it to an integer. This value represents the opacity as a 2-character hex string
587
+ // and convert it to an integer. This value represents the opacity as a 2-character hex string.
580
588
  this.options.particles.opacity = ~~(this.ctx.fillStyle.split(',').at(-1).slice(1, -1) * 255)
581
589
 
582
- // Example: extract 136, 244 and 255 from rgba(136, 244, 255, 0.25) and convert to hexadecimal '#rrggbb' format
590
+ // Example: extract 136, 244 and 255 from rgba(136, 244, 255, 0.25) and convert to hexadecimal '#rrggbb' format.
583
591
  this.ctx.fillStyle = this.ctx.fillStyle.split(',').slice(0, -1).join(',') + ', 1)'
584
592
  }
585
593
  this.options.particles.color = this.ctx.fillStyle
586
594
  this.options.particles.colorWithAlpha = this.options.particles.color + this.options.particles.opacity.toString(16)
587
595
 
588
- this.strokeStyleTable = this.#generateStrokeStyleTable(this.options.particles.color) // Recalculate the stroke style table
596
+ // Recalculate the stroke style table.
597
+ this.strokeStyleTable = this.#generateStrokeStyleTable(this.options.particles.color)
589
598
  }
590
599
  }
package/package.json CHANGED
@@ -1,59 +1,56 @@
1
- {
2
- "name": "canvasparticles-js",
3
- "version": "3.5.1",
4
- "description": "In an HTML canvas, a bunch of interactive particles connected with lines when they approach each other.",
5
- "main": "canvasParticles.js",
6
- "module": "canvasParticles.mjs",
7
- "types": "canvasParticles.d.ts",
8
- "type": "module",
9
- "files": [
10
- "./canvasparticles.d.ts",
11
- "./canvasParticles.js",
12
- "./canvasParticles.mjs"
13
- ],
14
- "exports": {
15
- ".": {
16
- "require": "./canvasParticles.js",
17
- "import": "./canvasParticles.mjs"
18
- }
19
- },
20
- "engines": {
21
- "node": ">=14"
22
- },
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/Khoeckman/canvasParticles.git"
26
- },
27
- "author": "Kyle Hoeckman",
28
- "license": "MIT",
29
- "bugs": {
30
- "url": "https://github.com/Khoeckman/canvasParticles/issues"
31
- },
32
- "homepage": "https://canvasparticleshomepage.onrender.com/",
33
- "keywords": [
34
- "front-end",
35
- "frontend",
36
- "canvas",
37
- "particle",
38
- "particles",
39
- "jsparticles",
40
- "js-particles",
41
- "particles.js",
42
- "particles-js",
43
- "xparticles",
44
- "background",
45
- "animation",
46
- "animated",
47
- "interactive",
48
- "interaction",
49
- "web",
50
- "webdesign",
51
- "web-design",
52
- "javascript",
53
- "js",
54
- "ecmascript",
55
- "module",
56
- "html5",
57
- "html"
58
- ]
59
- }
1
+ {
2
+ "name": "canvasparticles-js",
3
+ "version": "3.5.3",
4
+ "description": "In an HTML canvas, a bunch of interactive particles connected with lines when they approach each other.",
5
+ "main": "canvasParticles.js",
6
+ "module": "canvasParticles.mjs",
7
+ "types": "canvasParticles.d.ts",
8
+ "type": "module",
9
+ "files": [
10
+ "./canvasparticles.d.ts",
11
+ "./canvasParticles.js",
12
+ "./canvasParticles.mjs"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "require": "./canvasParticles.js",
17
+ "import": "./canvasParticles.mjs"
18
+ }
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Khoeckman/canvasparticles-js.git"
23
+ },
24
+ "author": "Kyle Hoeckman",
25
+ "license": "MIT",
26
+ "bugs": {
27
+ "url": "https://github.com/Khoeckman/canvasparticles-js/issues"
28
+ },
29
+ "homepage": "https://canvasparticleshomepage.onrender.com/",
30
+ "keywords": [
31
+ "front-end",
32
+ "frontend",
33
+ "canvas",
34
+ "particle",
35
+ "particles",
36
+ "jsparticles",
37
+ "js-particles",
38
+ "particles.js",
39
+ "particles-js",
40
+ "xparticles",
41
+ "background",
42
+ "animation",
43
+ "animated",
44
+ "interactive",
45
+ "interaction",
46
+ "web",
47
+ "webdesign",
48
+ "web-design",
49
+ "javascript",
50
+ "js",
51
+ "ecmascript",
52
+ "module",
53
+ "html5",
54
+ "html"
55
+ ]
56
+ }