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 +5 -4
- package/canvasParticles.js +52 -43
- package/canvasParticles.mjs +52 -43
- package/package.json +56 -59
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
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
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)',
|
package/canvasParticles.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
|
|
2
|
-
// https://github.com/Khoeckman/
|
|
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.
|
|
12
|
+
static version = '3.5.3'
|
|
13
13
|
|
|
14
|
-
//
|
|
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
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
250
|
-
if (this.options.mouse.interactionType !==
|
|
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
|
|
265
|
-
|
|
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)
|
|
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 [
|
|
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
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
|
|
520
|
+
// Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
|
|
521
|
+
const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
|
|
514
522
|
|
|
515
|
-
// Format
|
|
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 *
|
|
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
|
-
|
|
603
|
+
// Recalculate the stroke style table.
|
|
604
|
+
this.strokeStyleTable = this.#generateStrokeStyleTable(this.options.particles.color)
|
|
596
605
|
}
|
|
597
606
|
}
|
|
598
607
|
)
|
package/canvasParticles.mjs
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
|
|
2
|
-
// https://github.com/Khoeckman/
|
|
2
|
+
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
3
3
|
|
|
4
4
|
export default class CanvasParticles {
|
|
5
|
-
static version = '3.5.
|
|
5
|
+
static version = '3.5.3'
|
|
6
6
|
|
|
7
|
-
//
|
|
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
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
243
|
-
if (this.options.mouse.interactionType !==
|
|
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
|
|
258
|
-
|
|
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)
|
|
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 [
|
|
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
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
|
|
513
|
+
// Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
|
|
514
|
+
const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
|
|
507
515
|
|
|
508
|
-
// Format
|
|
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 *
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"particles",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|