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 +64 -10
- package/canvasParticles.js +96 -62
- package/canvasParticles.mjs +96 -62
- package/package.json +56 -59
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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)',
|
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,36 +9,47 @@
|
|
|
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
|
-
|
|
18
|
-
const 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
|
|
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
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
245
|
-
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) {
|
|
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
|
|
260
|
-
|
|
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)
|
|
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 [
|
|
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
|
|
326
|
-
* @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
520
|
+
// Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
|
|
521
|
+
const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
|
|
492
522
|
|
|
493
|
-
// Format
|
|
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
|
-
*
|
|
531
|
-
*
|
|
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 (
|
|
535
|
-
|
|
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 *
|
|
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
|
-
|
|
603
|
+
// Recalculate the stroke style table.
|
|
604
|
+
this.strokeStyleTable = this.#generateStrokeStyleTable(this.options.particles.color)
|
|
571
605
|
}
|
|
572
606
|
}
|
|
573
607
|
)
|
package/canvasParticles.mjs
CHANGED
|
@@ -1,37 +1,48 @@
|
|
|
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
|
-
|
|
11
|
-
const 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
|
|
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
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
238
|
-
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) {
|
|
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
|
|
253
|
-
|
|
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)
|
|
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 [
|
|
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
|
|
319
|
-
* @returns {Object} - A lookup table mapping each alpha value (0–255) to its corresponding stroke style string in the format
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
513
|
+
// Returns 'defaultValue' if 'value' is NaN, else returns 'value'.
|
|
514
|
+
const parse = (value, defaultValue) => (isNaN(+value) ? defaultValue : +value)
|
|
485
515
|
|
|
486
|
-
// Format
|
|
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
|
-
*
|
|
524
|
-
*
|
|
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 (
|
|
528
|
-
|
|
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 *
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|