canvasparticles-js 3.5.0 → 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
@@ -11,6 +11,7 @@ Creating a fun and interactive background. Colors, interaction and gravity can b
11
11
 
12
12
  [Showcase](#showcase)<br>
13
13
  [Implementation](#implementation)<br>
14
+ [Class instantiation](#class-instantiation)<br>
14
15
  [Options](#options)<br>
15
16
  [One pager example](#one-pager-example)
16
17
 
@@ -24,13 +25,13 @@ If you dont like reading documentation this website is for you:<br>
24
25
  Particles will be drawn onto this `<canvas>` element
25
26
 
26
27
  ```html
27
- <canvas id="canvas-particles"></canvas>
28
+ <canvas id="my-canvas"></canvas>
28
29
  ```
29
30
 
30
31
  Resize the `<canvas>` so it covers the whole page and place it behind all elements.
31
32
 
32
33
  ```css
33
- #canvas-particles {
34
+ #my-canvas {
34
35
  position: fixed;
35
36
  top: 0;
36
37
  left: 0;
@@ -60,7 +61,7 @@ Inside _initParticles.js_:
60
61
  ```js
61
62
  import CanvasParticles from 'canvasparticles-js'
62
63
 
63
- const selector = '#canvas-particles' // Query Selector for the canvas
64
+ const selector = '#my-canvas' // Query Selector for the canvas
64
65
  const options = { ... } // See #options
65
66
  new CanvasParticles(selector, options).start()
66
67
  ```
@@ -99,7 +100,7 @@ Inside _initParticles.js_:
99
100
  ```js
100
101
  import CanvasParticles from './canvasParticles.mjs'
101
102
 
102
- const selector = '#canvas-particles' // Query Selector for the canvas
103
+ const selector = '#my-canvas' // Query Selector for the canvas
103
104
  const options = { ... } // See #options
104
105
  new CanvasParticles(selector, options).start()
105
106
  ```
@@ -124,7 +125,7 @@ Add an inline `<script>` element at the very bottom of the `<body>`.
124
125
 
125
126
  <script>
126
127
  const initParticles = () => {
127
- const selector = '#canvas-particles' // Query Selector for the canvas
128
+ const selector = '#my-canvas' // Query Selector for the canvas
128
129
  const options = { ... } // See #options
129
130
  new CanvasParticles(selector, options).start()
130
131
  }
@@ -141,7 +142,7 @@ Add an inline `<script>` element at the very bottom of the `<body>`.
141
142
  ### Start animating
142
143
 
143
144
  ```js
144
- const selector = '#canvas-particles' // Query Selector for the canvas
145
+ const selector = '#my-canvas' // Query Selector for the canvas
145
146
  const options = { ... } // See #options
146
147
  new CanvasParticles(selector, options).start()
147
148
  ```
@@ -152,6 +153,58 @@ new CanvasParticles(selector, options).start()
152
153
  const particles = new CanvasParticles(selector, options)
153
154
  particles.start()
154
155
  particles.stop()
156
+ particles.stop({ clear: false }) // Default: true
157
+ ```
158
+
159
+ ## Class instantiation
160
+
161
+ ### Valid ways to instantiate `CanvasParticles`
162
+
163
+ ```js
164
+ const selector = '#my-canvas'
165
+ const options = {}
166
+ const myCanvas = document.querySelector(selector)
167
+
168
+ let instance, canvas
169
+
170
+ // Basic instantiation
171
+ instance = new CanvasParticles(selector)
172
+ instance = new CanvasParticles(myCanvas)
173
+
174
+ // Instantiation with custom options
175
+ instance = new CanvasParticles(selector, options)
176
+ instance = new CanvasParticles(myCanvas, options)
177
+ ```
178
+
179
+ ### Chaining methods
180
+
181
+ You can chain .start() for a cleaner syntax:
182
+
183
+ ```js
184
+ instance = new CanvasParticles(selector).start()
185
+
186
+ // Access the canvas directly
187
+ canvas = new CanvasParticles(selector).canvas
188
+ canvas = new CanvasParticles(selector).start().canvas
189
+ ```
190
+
191
+ ### Without chaining
192
+
193
+ If you prefer not to chain methods, you can instantiate first and start later:
194
+
195
+ ```js
196
+ instance = new CanvasParticles(selector)
197
+ instance.start()
198
+ canvas = instance.canvas
199
+ ```
200
+
201
+ ### Incorrect usage
202
+
203
+ The following will not return the expected value because `CanvasParticles` only supports method chaining for `.start()`:
204
+
205
+ ```js
206
+ instance = new CanvasParticles(selector).anyOtherMethod()
207
+ canvas = new CanvasParticles(selector).anyOtherMethod().canvas
155
208
  ```
156
209
 
157
210
  ## Options
@@ -190,9 +243,10 @@ const options = {
190
243
  /** @param {Object} [options.mouse] - Mouse interaction settings. */
191
244
  mouse: {
192
245
  /** @param {0|1|2} [options.mouse.interactionType=1] - The type of interaction the mouse will have with particles.
193
- * 0 = No interaction.
194
- * 1 = The mouse can visually shift the particles.
195
- * 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.
196
250
  * @note mouse.distRatio should be less than 1 to allow dragging, closer to 0 is easier to drag
197
251
  */
198
252
  interactionType: 2,
@@ -365,7 +419,7 @@ particles.setOptions(options)
365
419
  const options = {
366
420
  background: 'hsl(125, 42%, 35%)',
367
421
  mouse: {
368
- interactionType: 2,
422
+ interactionType: CanvasParticles.interactionType.MOVE, // === 2
369
423
  },
370
424
  particles: {
371
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,36 +9,47 @@
9
9
  typeof self !== 'undefined' ? self : this,
10
10
  () =>
11
11
  class CanvasParticles {
12
- static version = '3.5.0'
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
- const instance = change.target.instance
24
+ const canvas = change.target
25
+ const instance = canvas.instance // The 'CanvasParticles' instance bound to 'canvas'.
19
26
 
20
- if (change.isIntersecting) instance.options.animation.startOnEnter && instance.start()
21
- else instance.options.animation.stopOnLeave && instance.stop({ clear: false })
27
+ if ((canvas.inViewbox = change.isIntersecting)) instance.options.animation.startOnEnter && instance.start({ auto: true })
28
+ else instance.options.animation.stopOnLeave && instance.stop({ auto: true, clear: false })
22
29
  })
23
30
  })
24
31
 
25
32
  /**
26
33
  * Creates a new CanvasParticles instance.
27
- * @param {string} [selector] - The CSS selector for the canvas element.
34
+ * @param {string} [selector] - The CSS selector to the canvas element or the HTMLCanvasElement itself.
28
35
  * @param {Object} [options={}] - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
29
36
  */
30
37
  constructor(selector, options = {}) {
31
- // Find and initialize canvas
32
- if (typeof selector !== 'string') throw new TypeError('selector is not a string')
33
-
34
- this.canvas = document.querySelector(selector)
35
- if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
38
+ // Find the HTMLCanvasElement and assign it to 'this.canvas'.
39
+ if (selector instanceof HTMLCanvasElement) this.canvas = selector
40
+ else {
41
+ if (typeof selector !== 'string') throw new TypeError('selector is not a string and neither a HTMLCanvasElement itself')
36
42
 
37
- this.canvas.instance = this
43
+ this.canvas = document.querySelector(selector)
44
+ if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
45
+ }
46
+ this.canvas.instance = this // Circular assignment to find the instance bound to this canvas inside the static 'canvasObserver' callback.
47
+ this.canvas.inViewbox = true
38
48
 
39
- // Get 2d drawing functions
49
+ // Get 2d drawing methods.
40
50
  this.ctx = this.canvas.getContext('2d')
41
51
 
52
+ this.enableAnimating = false
42
53
  this.animating = false
43
54
  this.particles = []
44
55
  this.setOptions(options)
@@ -48,19 +59,16 @@
48
59
  this.#setupEventHandlers()
49
60
  }
50
61
 
51
- // Helper function
52
- #defaultIfNaN(value, defaultValue) {
53
- return isNaN(+value) ? defaultValue : +value
54
- }
55
-
56
62
  #setupEventHandlers() {
57
63
  const updateMousePos = event => {
58
- if (!this.animating) return
64
+ if (!this.enableAnimating) return
59
65
 
60
66
  if (event instanceof MouseEvent) {
61
67
  this.clientX = event.clientX
62
68
  this.clientY = event.clientY
63
69
  }
70
+
71
+ // On scroll, the mouse position remains the same, but since the canvas position changes, 'left' and 'top' must be recalculated.
64
72
  const { left, top } = this.canvas.getBoundingClientRect()
65
73
  this.mouseX = this.clientX - left
66
74
  this.mouseY = this.clientY - top
@@ -94,7 +102,7 @@
94
102
  }
95
103
 
96
104
  /**
97
- * 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'.
98
106
  * Capped at 'options.particles.max'.
99
107
  *
100
108
  * @private
@@ -151,7 +159,7 @@
151
159
  #updateParticleBounds() {
152
160
  this.particles.map(
153
161
  particle =>
154
- // Within these bounds the particle is considered visible
162
+ // Within these bounds the particle is considered visible.
155
163
  (particle.bounds = {
156
164
  top: -particle.size,
157
165
  right: this.canvas.width + particle.size,
@@ -180,7 +188,7 @@
180
188
 
181
189
  for (let i = 0; i < len; i++) {
182
190
  for (let j = i + 1; j < len; j++) {
183
- // Code in this scope runs [particles ** 2 / 2] times!
191
+ // Code in this scope runs [particleCount ** 2 / 2] times!
184
192
  const particleA = this.particles[i]
185
193
  const particleB = this.particles[j]
186
194
 
@@ -192,7 +200,7 @@
192
200
  let angle, grav
193
201
 
194
202
  if (dist < maxRepulsiveDist) {
195
- // Apply repulsive force on all particles close together
203
+ // Apply repulsive force on all particles closer than 'dist' / 2.
196
204
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
197
205
  grav = (1 / dist) ** 1.8
198
206
  const gravMult = Math.min(maxGrav, grav * gravRepulsiveMult)
@@ -206,7 +214,7 @@
206
214
 
207
215
  if (!isPullingEnabled) continue
208
216
 
209
- // Apply pulling force on all particles not close together
217
+ // Apply pulling force on all particles.
210
218
  if (angle === undefined) {
211
219
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
212
220
  grav = (1 / dist) ** 1.8
@@ -231,7 +239,7 @@
231
239
  * */
232
240
  #updateParticles() {
233
241
  for (let particle of this.particles) {
234
- // Moving the particle
242
+ // Slightly, randomly change the particle's direction and move it in that direction.
235
243
  particle.dir = (particle.dir + Math.random() * this.options.particles.rotationSpeed * 2 - this.options.particles.rotationSpeed) % (2 * Math.PI)
236
244
  particle.velX *= this.options.gravity.friction
237
245
  particle.velY *= this.options.gravity.friction
@@ -241,8 +249,8 @@
241
249
  const distX = particle.posX + this.offX - this.mouseX
242
250
  const distY = particle.posY + this.offY - this.mouseY
243
251
 
244
- // Mouse events
245
- 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) {
246
254
  const distRatio = this.options.mouse.connectDist / Math.hypot(distX, distY)
247
255
 
248
256
  if (this.options.mouse.distRatio < distRatio) {
@@ -253,18 +261,20 @@
253
261
  particle.offY -= particle.offY / 4
254
262
  }
255
263
  }
264
+
265
+ // Visually shift the particles
256
266
  particle.x = particle.posX + particle.offX
257
267
  particle.y = particle.posY + particle.offY
258
268
 
259
- if (this.options.mouse.interactionType === 2) {
260
- // 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) {
261
271
  particle.posX = particle.x
262
272
  particle.posY = particle.y
263
273
  }
264
274
  particle.x += this.offX
265
275
  particle.y += this.offY
266
276
 
267
- particle.gridPos = this.#gridPos(particle) // The location of the particle relative to the visible center of the canvas
277
+ particle.gridPos = this.#gridPos(particle)
268
278
  particle.isVisible = particle.gridPos.x === 1 && particle.gridPos.y === 1
269
279
  }
270
280
  }
@@ -307,10 +317,10 @@
307
317
  * @returns {boolean} - True if the line crosses the visible center, false otherwise.
308
318
  */
309
319
  #isLineVisible(particleA, particleB) {
310
- // Visible if either particle is in the center
320
+ // Visible if either particle is in the center.
311
321
  if (particleA.isVisible || particleB.isVisible) return true
312
322
 
313
- // 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.
314
324
  return !(
315
325
  (particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
316
326
  (particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
@@ -319,11 +329,11 @@
319
329
 
320
330
  /**
321
331
  * Precomputes and caches stroke style strings for a given base color and all possible alpha values (0–255).
322
- * 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.
323
333
  *
324
334
  * @private
325
- * @param {string} color - The base color in the format `#rrggbb`.
326
- * @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format `#rrggbbaa`.
335
+ * @param {string} color - The base color in the format '#rrggbb'.
336
+ * @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format '#rrggbbaa'.
327
337
  *
328
338
  * @example
329
339
  * const strokeStyleTable = this.#generateStrokeStyleTable("#abcdef");
@@ -331,9 +341,8 @@
331
341
  * strokeStyleTable[255] -> "#abcdefff"
332
342
  *
333
343
  * Notes:
334
- * - This function precomputes all possible stroke styles by appending a two-character
335
- * hexadecimal alpha value (0x00–0xFF) to the base color.
336
- * - The table is stored in `this.strokeStyleTable` for quick lookups.
344
+ * - This function precomputes all possible stroke styles by appending a two-character hexadecimal alpha value (0x00–0xFF) to the base color.
345
+ * - The table is stored in 'this.strokeStyleTable' for quick lookups.
337
346
  */
338
347
  #generateStrokeStyleTable(color) {
339
348
  const table = {}
@@ -354,7 +363,8 @@
354
363
  #renderParticles() {
355
364
  for (let particle of this.particles) {
356
365
  if (particle.isVisible) {
357
- // 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.
358
368
  if (particle.size > 1) {
359
369
  // Draw circle
360
370
  this.ctx.beginPath()
@@ -362,7 +372,7 @@
362
372
  this.ctx.fill()
363
373
  this.ctx.closePath()
364
374
  } else {
365
- // Draw square (±335% faster than circle)
375
+ // Draw square
366
376
  this.ctx.fillRect(particle.x - particle.size, particle.y - particle.size, particle.size * 2, particle.size * 2)
367
377
  }
368
378
  }
@@ -384,22 +394,23 @@
384
394
  let particleWork = 0
385
395
 
386
396
  for (let j = i + 1; j < len; j++) {
387
- // Code in this scope runs [particles ** 2 / 2] times!
397
+ // Code in this scope runs [particleCount ** 2 / 2] times!
388
398
  const particleA = this.particles[i]
389
399
  const particleB = this.particles[j]
390
400
 
391
401
  if (!(drawAll || this.#isLineVisible(particleA, particleB))) continue
392
- // Draw a line only if will be visible
402
+ // Draw a line only if it's visible.
393
403
 
394
404
  const distX = particleA.x - particleB.x
395
405
  const distY = particleA.y - particleB.y
396
406
 
397
407
  const dist = Math.sqrt(distX * distX + distY * distY)
398
408
 
409
+ // Don't connect the 2 particles with a line if their distance is greater than 'options.particles.connectDist'.
399
410
  if (dist > this.options.particles.connectDist) continue
400
- // Connect the 2 particles with a line only if the distance is small enough
401
411
 
402
- // 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.
403
414
  if (dist > this.options.particles.connectDist / 2) {
404
415
  const alpha = ~~(Math.min(this.options.particles.connectDist / dist - 1, 1) * this.options.particles.opacity)
405
416
  this.ctx.strokeStyle = this.strokeStyleTable[alpha]
@@ -407,12 +418,13 @@
407
418
  this.ctx.strokeStyle = this.options.particles.colorWithAlpha
408
419
  }
409
420
 
410
- // Draw the line
421
+ // Draw the line.
411
422
  this.ctx.beginPath()
412
423
  this.ctx.moveTo(particleA.x, particleA.y)
413
424
  this.ctx.lineTo(particleB.x, particleB.y)
414
425
  this.ctx.stroke()
415
426
 
427
+ // Stop drawing lines from this particles if it has already drawn to many.
416
428
  if ((particleWork += dist) >= maxWorkPerParticle) break
417
429
  }
418
430
  }
@@ -457,22 +469,39 @@
457
469
 
458
470
  /**
459
471
  * Starts the particle animation.
460
- * Does nothing if already animating.
461
472
  *
462
- * @returns {CanvasParticles} - The current instance.
473
+ * - If the animation is already running, do nothing.
474
+ * - If the canvas is not within the viewbox and 'startOnEnter' is enabled, animation will be stopped until it enters the viewbox.
475
+ *
476
+ * @param {Object} [options] - Optional configuration for starting the animation.
477
+ * @param {boolean} [options.auto] - If true, indicates that the request comes from 'CanvasParticles.canvasObserver'.
478
+ * @returns {CanvasParticles} The current instance for method chaining.
463
479
  */
464
- start() {
465
- if (!this.animating) {
480
+ start(options) {
481
+ if (!this.animating && (!options?.auto || this.enableAnimating)) {
482
+ this.enableAnimating = true
466
483
  this.animating = true
467
484
  requestAnimationFrame(() => this.#animation())
468
485
  }
486
+
487
+ // Stop animating because it will start automatically once the canvas enters the viewbox.
488
+ if (!this.canvas.inViewbox && this.options.animation.startOnEnter) this.animating = false
489
+
469
490
  return this
470
491
  }
471
492
 
472
493
  /**
473
- * Stops the particle animation and clears the canvas.
494
+ * Stops the particle animation and optionally clears the canvas.
495
+ *
496
+ * - If 'options.clear' is not strictly false, the canvas will be cleared.
497
+ *
498
+ * @param {Object} [options] - Optional configuration for stopping the animation.
499
+ * @param {boolean} [options.auto] - If true, indicates that the request comes from 'CanvasParticles.canvasObserver'.
500
+ * @param {boolean} [options.clear] - If strictly false, prevents clearing the canvas-.
501
+ * @returns {boolean} `true` when the animation is successfully stopped.
474
502
  */
475
503
  stop(options) {
504
+ if (!options?.auto) this.enableAnimating = false
476
505
  this.animating = false
477
506
  if (options?.clear !== false) this.canvas.width = this.canvas.width
478
507
 
@@ -488,9 +517,10 @@
488
517
  * @param {Object} options - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
489
518
  */
490
519
  setOptions(options) {
491
- const parse = this.#defaultIfNaN
520
+ // Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
521
+ const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
492
522
 
493
- // Format and store options
523
+ // Format or default all options.
494
524
  this.options = {
495
525
  background: options.background ?? false,
496
526
  framesPerUpdate: parse(Math.max(1, parseInt(options.framesPerUpdate)), 1),
@@ -527,12 +557,15 @@
527
557
  }
528
558
 
529
559
  /**
530
- * Set canvas background.
531
- * @param {string} background - The style of the background. Can be any CSS supported background format.
560
+ * Sets the canvas background.
561
+ *
562
+ * @param {string} background - The style of the background. Can be any CSS-supported background value.
563
+ * @throws {TypeError} If background is not a string.
532
564
  */
533
565
  setBackground(background) {
534
- if (typeof background === 'string') return (this.canvas.style.background = this.options.background = background), true
535
- return false
566
+ if (background === false) return
567
+ if (typeof background !== 'string') throw new TypeError('background is not a string')
568
+ this.canvas.style.background = this.options.background = background
536
569
  }
537
570
 
538
571
  /**
@@ -542,7 +575,7 @@
542
575
  * @example 0.8 connectDistMult * 150 particles.connectDistance = 120 pixels
543
576
  */
544
577
  setMouseConnectDistMult(connectDistMult) {
545
- 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)
546
579
  }
547
580
 
548
581
  /**
@@ -558,16 +591,17 @@
558
591
  // JavaScript's 'ctx.fillStyle' ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
559
592
 
560
593
  // Extract the alpha value (0.25) from the rgba string, scale it to the range 0x00 to 0xff,
561
- // 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.
562
595
  this.options.particles.opacity = ~~(this.ctx.fillStyle.split(',').at(-1).slice(1, -1) * 255)
563
596
 
564
- // 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.
565
598
  this.ctx.fillStyle = this.ctx.fillStyle.split(',').slice(0, -1).join(',') + ', 1)'
566
599
  }
567
600
  this.options.particles.color = this.ctx.fillStyle
568
601
  this.options.particles.colorWithAlpha = this.options.particles.color + this.options.particles.opacity.toString(16)
569
602
 
570
- 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)
571
605
  }
572
606
  }
573
607
  )
@@ -1,37 +1,48 @@
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.0'
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
- const instance = change.target.instance
17
+ const canvas = change.target
18
+ const instance = canvas.instance // The 'CanvasParticles' instance bound to 'canvas'.
12
19
 
13
- if (change.isIntersecting) instance.options.animation.startOnEnter && instance.start()
14
- else instance.options.animation.stopOnLeave && instance.stop({ clear: false })
20
+ if ((canvas.inViewbox = change.isIntersecting)) instance.options.animation.startOnEnter && instance.start({ auto: true })
21
+ else instance.options.animation.stopOnLeave && instance.stop({ auto: true, clear: false })
15
22
  })
16
23
  })
17
24
 
18
25
  /**
19
26
  * Creates a new CanvasParticles instance.
20
- * @param {string} [selector] - The CSS selector for the canvas element.
27
+ * @param {string} [selector] - The CSS selector to the canvas element or the HTMLCanvasElement itself.
21
28
  * @param {Object} [options={}] - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
22
29
  */
23
30
  constructor(selector, options = {}) {
24
- // Find and initialize canvas
25
- if (typeof selector !== 'string') throw new TypeError('selector is not a string')
26
-
27
- this.canvas = document.querySelector(selector)
28
- if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
31
+ // Find the HTMLCanvasElement and assign it to 'this.canvas'.
32
+ if (selector instanceof HTMLCanvasElement) this.canvas = selector
33
+ else {
34
+ if (typeof selector !== 'string') throw new TypeError('selector is not a string and neither a HTMLCanvasElement itself')
29
35
 
30
- this.canvas.instance = this
36
+ this.canvas = document.querySelector(selector)
37
+ if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('selector does not point to a canvas')
38
+ }
39
+ this.canvas.instance = this // Circular assignment to find the instance bound to this canvas inside the static 'canvasObserver' callback.
40
+ this.canvas.inViewbox = true
31
41
 
32
- // Get 2d drawing functions
42
+ // Get 2d drawing methods.
33
43
  this.ctx = this.canvas.getContext('2d')
34
44
 
45
+ this.enableAnimating = false
35
46
  this.animating = false
36
47
  this.particles = []
37
48
  this.setOptions(options)
@@ -41,19 +52,16 @@ export default class CanvasParticles {
41
52
  this.#setupEventHandlers()
42
53
  }
43
54
 
44
- // Helper function
45
- #defaultIfNaN(value, defaultValue) {
46
- return isNaN(+value) ? defaultValue : +value
47
- }
48
-
49
55
  #setupEventHandlers() {
50
56
  const updateMousePos = event => {
51
- if (!this.animating) return
57
+ if (!this.enableAnimating) return
52
58
 
53
59
  if (event instanceof MouseEvent) {
54
60
  this.clientX = event.clientX
55
61
  this.clientY = event.clientY
56
62
  }
63
+
64
+ // On scroll, the mouse position remains the same, but since the canvas position changes, 'left' and 'top' must be recalculated.
57
65
  const { left, top } = this.canvas.getBoundingClientRect()
58
66
  this.mouseX = this.clientX - left
59
67
  this.mouseY = this.clientY - top
@@ -87,7 +95,7 @@ export default class CanvasParticles {
87
95
  }
88
96
 
89
97
  /**
90
- * 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'.
91
99
  * Capped at 'options.particles.max'.
92
100
  *
93
101
  * @private
@@ -144,7 +152,7 @@ export default class CanvasParticles {
144
152
  #updateParticleBounds() {
145
153
  this.particles.map(
146
154
  particle =>
147
- // Within these bounds the particle is considered visible
155
+ // Within these bounds the particle is considered visible.
148
156
  (particle.bounds = {
149
157
  top: -particle.size,
150
158
  right: this.canvas.width + particle.size,
@@ -173,7 +181,7 @@ export default class CanvasParticles {
173
181
 
174
182
  for (let i = 0; i < len; i++) {
175
183
  for (let j = i + 1; j < len; j++) {
176
- // Code in this scope runs [particles ** 2 / 2] times!
184
+ // Code in this scope runs [particleCount ** 2 / 2] times!
177
185
  const particleA = this.particles[i]
178
186
  const particleB = this.particles[j]
179
187
 
@@ -185,7 +193,7 @@ export default class CanvasParticles {
185
193
  let angle, grav
186
194
 
187
195
  if (dist < maxRepulsiveDist) {
188
- // Apply repulsive force on all particles close together
196
+ // Apply repulsive force on all particles closer than 'dist' / 2.
189
197
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
190
198
  grav = (1 / dist) ** 1.8
191
199
  const gravMult = Math.min(maxGrav, grav * gravRepulsiveMult)
@@ -199,7 +207,7 @@ export default class CanvasParticles {
199
207
 
200
208
  if (!isPullingEnabled) continue
201
209
 
202
- // Apply pulling force on all particles not close together
210
+ // Apply pulling force on all particles.
203
211
  if (angle === undefined) {
204
212
  angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
205
213
  grav = (1 / dist) ** 1.8
@@ -224,7 +232,7 @@ export default class CanvasParticles {
224
232
  * */
225
233
  #updateParticles() {
226
234
  for (let particle of this.particles) {
227
- // Moving the particle
235
+ // Slightly, randomly change the particle's direction and move it in that direction.
228
236
  particle.dir = (particle.dir + Math.random() * this.options.particles.rotationSpeed * 2 - this.options.particles.rotationSpeed) % (2 * Math.PI)
229
237
  particle.velX *= this.options.gravity.friction
230
238
  particle.velY *= this.options.gravity.friction
@@ -234,8 +242,8 @@ export default class CanvasParticles {
234
242
  const distX = particle.posX + this.offX - this.mouseX
235
243
  const distY = particle.posY + this.offY - this.mouseY
236
244
 
237
- // Mouse events
238
- 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) {
239
247
  const distRatio = this.options.mouse.connectDist / Math.hypot(distX, distY)
240
248
 
241
249
  if (this.options.mouse.distRatio < distRatio) {
@@ -246,18 +254,20 @@ export default class CanvasParticles {
246
254
  particle.offY -= particle.offY / 4
247
255
  }
248
256
  }
257
+
258
+ // Visually shift the particles
249
259
  particle.x = particle.posX + particle.offX
250
260
  particle.y = particle.posY + particle.offY
251
261
 
252
- if (this.options.mouse.interactionType === 2) {
253
- // 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) {
254
264
  particle.posX = particle.x
255
265
  particle.posY = particle.y
256
266
  }
257
267
  particle.x += this.offX
258
268
  particle.y += this.offY
259
269
 
260
- particle.gridPos = this.#gridPos(particle) // The location of the particle relative to the visible center of the canvas
270
+ particle.gridPos = this.#gridPos(particle)
261
271
  particle.isVisible = particle.gridPos.x === 1 && particle.gridPos.y === 1
262
272
  }
263
273
  }
@@ -300,10 +310,10 @@ export default class CanvasParticles {
300
310
  * @returns {boolean} - True if the line crosses the visible center, false otherwise.
301
311
  */
302
312
  #isLineVisible(particleA, particleB) {
303
- // Visible if either particle is in the center
313
+ // Visible if either particle is in the center.
304
314
  if (particleA.isVisible || particleB.isVisible) return true
305
315
 
306
- // 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.
307
317
  return !(
308
318
  (particleA.gridPos.x === particleB.gridPos.x && particleA.gridPos.x !== 1) ||
309
319
  (particleA.gridPos.y === particleB.gridPos.y && particleA.gridPos.y !== 1)
@@ -312,11 +322,11 @@ export default class CanvasParticles {
312
322
 
313
323
  /**
314
324
  * Precomputes and caches stroke style strings for a given base color and all possible alpha values (0–255).
315
- * 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.
316
326
  *
317
327
  * @private
318
- * @param {string} color - The base color in the format `#rrggbb`.
319
- * @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format `#rrggbbaa`.
328
+ * @param {string} color - The base color in the format '#rrggbb'.
329
+ * @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format '#rrggbbaa'.
320
330
  *
321
331
  * @example
322
332
  * const strokeStyleTable = this.#generateStrokeStyleTable("#abcdef");
@@ -324,9 +334,8 @@ export default class CanvasParticles {
324
334
  * strokeStyleTable[255] -> "#abcdefff"
325
335
  *
326
336
  * Notes:
327
- * - This function precomputes all possible stroke styles by appending a two-character
328
- * hexadecimal alpha value (0x00–0xFF) to the base color.
329
- * - The table is stored in `this.strokeStyleTable` for quick lookups.
337
+ * - This function precomputes all possible stroke styles by appending a two-character hexadecimal alpha value (0x00–0xFF) to the base color.
338
+ * - The table is stored in 'this.strokeStyleTable' for quick lookups.
330
339
  */
331
340
  #generateStrokeStyleTable(color) {
332
341
  const table = {}
@@ -347,7 +356,8 @@ export default class CanvasParticles {
347
356
  #renderParticles() {
348
357
  for (let particle of this.particles) {
349
358
  if (particle.isVisible) {
350
- // 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.
351
361
  if (particle.size > 1) {
352
362
  // Draw circle
353
363
  this.ctx.beginPath()
@@ -355,7 +365,7 @@ export default class CanvasParticles {
355
365
  this.ctx.fill()
356
366
  this.ctx.closePath()
357
367
  } else {
358
- // Draw square (±335% faster than circle)
368
+ // Draw square
359
369
  this.ctx.fillRect(particle.x - particle.size, particle.y - particle.size, particle.size * 2, particle.size * 2)
360
370
  }
361
371
  }
@@ -377,22 +387,23 @@ export default class CanvasParticles {
377
387
  let particleWork = 0
378
388
 
379
389
  for (let j = i + 1; j < len; j++) {
380
- // Code in this scope runs [particles ** 2 / 2] times!
390
+ // Code in this scope runs [particleCount ** 2 / 2] times!
381
391
  const particleA = this.particles[i]
382
392
  const particleB = this.particles[j]
383
393
 
384
394
  if (!(drawAll || this.#isLineVisible(particleA, particleB))) continue
385
- // Draw a line only if will be visible
395
+ // Draw a line only if it's visible.
386
396
 
387
397
  const distX = particleA.x - particleB.x
388
398
  const distY = particleA.y - particleB.y
389
399
 
390
400
  const dist = Math.sqrt(distX * distX + distY * distY)
391
401
 
402
+ // Don't connect the 2 particles with a line if their distance is greater than 'options.particles.connectDist'.
392
403
  if (dist > this.options.particles.connectDist) continue
393
- // Connect the 2 particles with a line only if the distance is small enough
394
404
 
395
- // 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.
396
407
  if (dist > this.options.particles.connectDist / 2) {
397
408
  const alpha = ~~(Math.min(this.options.particles.connectDist / dist - 1, 1) * this.options.particles.opacity)
398
409
  this.ctx.strokeStyle = this.strokeStyleTable[alpha]
@@ -400,12 +411,13 @@ export default class CanvasParticles {
400
411
  this.ctx.strokeStyle = this.options.particles.colorWithAlpha
401
412
  }
402
413
 
403
- // Draw the line
414
+ // Draw the line.
404
415
  this.ctx.beginPath()
405
416
  this.ctx.moveTo(particleA.x, particleA.y)
406
417
  this.ctx.lineTo(particleB.x, particleB.y)
407
418
  this.ctx.stroke()
408
419
 
420
+ // Stop drawing lines from this particles if it has already drawn to many.
409
421
  if ((particleWork += dist) >= maxWorkPerParticle) break
410
422
  }
411
423
  }
@@ -450,22 +462,39 @@ export default class CanvasParticles {
450
462
 
451
463
  /**
452
464
  * Starts the particle animation.
453
- * Does nothing if already animating.
454
465
  *
455
- * @returns {CanvasParticles} - The current instance.
466
+ * - If the animation is already running, do nothing.
467
+ * - If the canvas is not within the viewbox and 'startOnEnter' is enabled, animation will be stopped until it enters the viewbox.
468
+ *
469
+ * @param {Object} [options] - Optional configuration for starting the animation.
470
+ * @param {boolean} [options.auto] - If true, indicates that the request comes from 'CanvasParticles.canvasObserver'.
471
+ * @returns {CanvasParticles} The current instance for method chaining.
456
472
  */
457
- start() {
458
- if (!this.animating) {
473
+ start(options) {
474
+ if (!this.animating && (!options?.auto || this.enableAnimating)) {
475
+ this.enableAnimating = true
459
476
  this.animating = true
460
477
  requestAnimationFrame(() => this.#animation())
461
478
  }
479
+
480
+ // Stop animating because it will start automatically once the canvas enters the viewbox.
481
+ if (!this.canvas.inViewbox && this.options.animation.startOnEnter) this.animating = false
482
+
462
483
  return this
463
484
  }
464
485
 
465
486
  /**
466
- * Stops the particle animation and clears the canvas.
487
+ * Stops the particle animation and optionally clears the canvas.
488
+ *
489
+ * - If 'options.clear' is not strictly false, the canvas will be cleared.
490
+ *
491
+ * @param {Object} [options] - Optional configuration for stopping the animation.
492
+ * @param {boolean} [options.auto] - If true, indicates that the request comes from 'CanvasParticles.canvasObserver'.
493
+ * @param {boolean} [options.clear] - If strictly false, prevents clearing the canvas-.
494
+ * @returns {boolean} `true` when the animation is successfully stopped.
467
495
  */
468
496
  stop(options) {
497
+ if (!options?.auto) this.enableAnimating = false
469
498
  this.animating = false
470
499
  if (options?.clear !== false) this.canvas.width = this.canvas.width
471
500
 
@@ -481,9 +510,10 @@ export default class CanvasParticles {
481
510
  * @param {Object} options - Object structure: https://github.com/Khoeckman/canvasParticles?tab=readme-ov-file#options
482
511
  */
483
512
  setOptions(options) {
484
- const parse = this.#defaultIfNaN
513
+ // Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
514
+ const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
485
515
 
486
- // Format and store options
516
+ // Format or default all options.
487
517
  this.options = {
488
518
  background: options.background ?? false,
489
519
  framesPerUpdate: parse(Math.max(1, parseInt(options.framesPerUpdate)), 1),
@@ -520,12 +550,15 @@ export default class CanvasParticles {
520
550
  }
521
551
 
522
552
  /**
523
- * Set canvas background.
524
- * @param {string} background - The style of the background. Can be any CSS supported background format.
553
+ * Sets the canvas background.
554
+ *
555
+ * @param {string} background - The style of the background. Can be any CSS-supported background value.
556
+ * @throws {TypeError} If background is not a string.
525
557
  */
526
558
  setBackground(background) {
527
- if (typeof background === 'string') return (this.canvas.style.background = this.options.background = background), true
528
- return false
559
+ if (background === false) return
560
+ if (typeof background !== 'string') throw new TypeError('background is not a string')
561
+ this.canvas.style.background = this.options.background = background
529
562
  }
530
563
 
531
564
  /**
@@ -535,7 +568,7 @@ export default class CanvasParticles {
535
568
  * @example 0.8 connectDistMult * 150 particles.connectDistance = 120 pixels
536
569
  */
537
570
  setMouseConnectDistMult(connectDistMult) {
538
- 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)
539
572
  }
540
573
 
541
574
  /**
@@ -551,15 +584,16 @@ export default class CanvasParticles {
551
584
  // JavaScript's 'ctx.fillStyle' ensures the color will otherwise be in rgba format (e.g., "rgba(136, 244, 255, 0.25)")
552
585
 
553
586
  // Extract the alpha value (0.25) from the rgba string, scale it to the range 0x00 to 0xff,
554
- // 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.
555
588
  this.options.particles.opacity = ~~(this.ctx.fillStyle.split(',').at(-1).slice(1, -1) * 255)
556
589
 
557
- // 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.
558
591
  this.ctx.fillStyle = this.ctx.fillStyle.split(',').slice(0, -1).join(',') + ', 1)'
559
592
  }
560
593
  this.options.particles.color = this.ctx.fillStyle
561
594
  this.options.particles.colorWithAlpha = this.options.particles.color + this.options.particles.opacity.toString(16)
562
595
 
563
- 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)
564
598
  }
565
599
  }
package/package.json CHANGED
@@ -1,59 +1,56 @@
1
- {
2
- "name": "canvasparticles-js",
3
- "version": "3.5.0",
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
+ }