canvasparticles-js 4.1.5 → 4.2.1
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 +113 -205
- package/dist/index.cjs +42 -38
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +42 -38
- package/dist/index.umd.js +1 -1
- package/dist/types/options.d.ts +1 -0
- package/package.json +1 -1
- package/src/index.ts +50 -42
- package/src/types/options.ts +1 -0
package/README.md
CHANGED
|
@@ -11,19 +11,25 @@ In an HTML canvas, a bunch of floating particles connected with lines when they
|
|
|
11
11
|
Creating a fun and interactive background. Colors, interaction and gravity can be customized!
|
|
12
12
|
|
|
13
13
|
[Showcase](#showcase)<br>
|
|
14
|
+
[Import](#import)<br>
|
|
14
15
|
[Implementation](#implementation)<br>
|
|
15
|
-
[Class
|
|
16
|
+
[Class Instantiation](#class-instantiation)<br>
|
|
16
17
|
[Options](#options)<br>
|
|
17
|
-
[One
|
|
18
|
+
[One Pager Example](#one-pager-example)
|
|
19
|
+
|
|
20
|
+
---
|
|
18
21
|
|
|
19
22
|
## Showcase
|
|
20
23
|
|
|
21
24
|
If you dont like reading documentation this website is for you:<br>
|
|
25
|
+
|
|
22
26
|
[https://khoeckman.github.io/canvasparticles-js/](https://khoeckman.github.io/canvasparticles-js/)
|
|
23
27
|
|
|
24
|
-

|
|
28
|
+
[](https://khoeckman.github.io/canvasparticles-js/)
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Import
|
|
27
33
|
|
|
28
34
|
Particles will be drawn onto a `<canvas>` element.
|
|
29
35
|
|
|
@@ -31,107 +37,51 @@ Particles will be drawn onto a `<canvas>` element.
|
|
|
31
37
|
<canvas id="my-canvas"></canvas>
|
|
32
38
|
```
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
<summary><h3 style="display: inline;">Import with npm</h3></summary>
|
|
40
|
+
### npm
|
|
36
41
|
|
|
37
42
|
```bash
|
|
38
43
|
npm install canvasparticles-js
|
|
39
44
|
# or
|
|
40
45
|
pnpm add canvasparticles-js
|
|
41
|
-
# or
|
|
42
|
-
yarn add canvasparticles-js
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Add a `<script>` element in the `<head>` to import _initParticles.js_.
|
|
46
|
-
|
|
47
|
-
```html
|
|
48
|
-
<head>
|
|
49
|
-
<script src="./initParticles.js" type="module"></script>
|
|
50
|
-
</head>
|
|
51
46
|
```
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
**ES Module import**
|
|
54
49
|
|
|
55
50
|
```js
|
|
56
51
|
import CanvasParticles from 'canvasparticles-js'
|
|
57
|
-
|
|
58
|
-
const selector = '#my-canvas' // Query Selector for the canvas
|
|
59
|
-
const options = { ... } // See #options
|
|
60
|
-
new CanvasParticles(selector, options).start()
|
|
61
52
|
```
|
|
62
53
|
|
|
63
|
-
|
|
54
|
+
If you don't have a bundler:
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
```js
|
|
57
|
+
import CanvasParticles from './node_modules/canvasparticles-js/dist/index.mjs'
|
|
58
|
+
```
|
|
67
59
|
|
|
68
|
-
|
|
60
|
+
**Global import**
|
|
69
61
|
|
|
70
62
|
```html
|
|
71
|
-
<
|
|
72
|
-
<script src="https://cdn.jsdelivr.net/npm/canvasparticles-js/dist/index.umd.js" defer></script>
|
|
73
|
-
</head>
|
|
63
|
+
<script src="./node_modules/canvasparticles-js/dist/index.umd.js" defer></script>
|
|
74
64
|
```
|
|
75
65
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<details>
|
|
79
|
-
<summary><h3 style="display: inline;">Import raw file as ES module (click to expand)</h3></summary>
|
|
66
|
+
No import required in each JavaScript file!
|
|
80
67
|
|
|
81
|
-
|
|
82
|
-
[Same Origin Policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)
|
|
68
|
+
### Import with jsDelivr
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```html
|
|
87
|
-
<head>
|
|
88
|
-
<script src="./initParticles.js" type="module"></script>
|
|
89
|
-
</head>
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Inside _initParticles.js_:
|
|
70
|
+
**ES Module import**
|
|
93
71
|
|
|
94
72
|
```js
|
|
95
|
-
import CanvasParticles from '
|
|
96
|
-
|
|
97
|
-
const selector = '#my-canvas' // Query Selector for the canvas
|
|
98
|
-
const options = { ... } // See #options
|
|
99
|
-
new CanvasParticles(selector, options).start()
|
|
73
|
+
import CanvasParticles from 'https://cdn.jsdelivr.net/npm/canvasparticles-js/dist/index.min.mjs'
|
|
100
74
|
```
|
|
101
75
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<details>
|
|
105
|
-
<summary><h3 style="display: inline;">Import raw file globally (click to expand)</h3></summary>
|
|
106
|
-
|
|
107
|
-
Add a `<script>` element in the `<head>` to import the [dist/index.umd.js](https://github.com/Khoeckman/canvasparticles-js/blob/main/dist/index.umd.js) file.<br>
|
|
108
|
-
```html
|
|
109
|
-
<head>
|
|
110
|
-
<script src="./index.umd.js" defer></script>
|
|
111
|
-
</head>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Add an inline `<script>` element at the very bottom of the `<body>`.
|
|
76
|
+
**Global import**
|
|
115
77
|
|
|
116
78
|
```html
|
|
117
|
-
<
|
|
118
|
-
...
|
|
119
|
-
|
|
120
|
-
<script>
|
|
121
|
-
const initParticles = () => {
|
|
122
|
-
const selector = '#my-canvas' // Query Selector for the canvas
|
|
123
|
-
const options = { ... } // See #options
|
|
124
|
-
new CanvasParticles(selector, options).start()
|
|
125
|
-
}
|
|
126
|
-
document.addEventListener('DOMContentLoaded', initParticles)
|
|
127
|
-
</script>
|
|
128
|
-
</body>
|
|
79
|
+
<script src="https://cdn.jsdelivr.net/npm/canvasparticles-js/dist/index.umd.min.js" defer></script>
|
|
129
80
|
```
|
|
130
81
|
|
|
131
|
-
|
|
82
|
+
---
|
|
132
83
|
|
|
133
|
-
|
|
134
|
-
<br>
|
|
84
|
+
## Implementation
|
|
135
85
|
|
|
136
86
|
### Start animating
|
|
137
87
|
|
|
@@ -164,7 +114,9 @@ particles.destroy()
|
|
|
164
114
|
delete particles // Optional
|
|
165
115
|
```
|
|
166
116
|
|
|
167
|
-
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Class Instantiation
|
|
168
120
|
|
|
169
121
|
### Valid ways to instantiate `CanvasParticles`
|
|
170
122
|
|
|
@@ -217,137 +169,87 @@ canvas = new CanvasParticles(selector).anyOtherMethod().canvas
|
|
|
217
169
|
|
|
218
170
|
## Options
|
|
219
171
|
|
|
220
|
-
|
|
221
|
-
Play around with these values
|
|
172
|
+
Options to change the particles and their behavior aswell as what happens on `MouseMove` or `Resize` events.<br>
|
|
173
|
+
Play around with these values in the [Sandbox](https://khoeckman.github.io/canvasparticles-js/#sandbox).
|
|
222
174
|
|
|
223
|
-
|
|
224
|
-
<summary><h3 style="display: inline;">Options structure (click to expand)</h3></summary>
|
|
175
|
+
### Options Object
|
|
225
176
|
|
|
226
177
|
The default value will be used when an option is assigned an invalid value.<br>
|
|
227
178
|
Your screen resolution and refresh rate will directly impact perfomance!
|
|
228
179
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
/** @param {float} [options.mouse.connectDistMult=2÷3] - The maximum distance for the mouse to interact with the particles.
|
|
257
|
-
* The value is multiplied by particles.connectDistance
|
|
258
|
-
* @example 0.8 connectDistMult * 150 particles.connectDistance = 120 pixels
|
|
259
|
-
*/
|
|
260
|
-
connectDistMult: 0.8,
|
|
261
|
-
|
|
262
|
-
/** @param {number} [options.mouse.distRatio=2÷3] - All particles within set radius from the mouse will be drawn to mouse.connectDistance pixels from the mouse.
|
|
263
|
-
* @example radius = 150 connectDistance / 0.4 distRatio = 375 pixels
|
|
264
|
-
* @note Keep this value above mouse.connectDistMult
|
|
265
|
-
*/
|
|
266
|
-
distRatio: 1, // recommended: 0.2 - 1
|
|
267
|
-
},
|
|
268
|
-
|
|
269
|
-
/** @param {Object} [options.particles] - Particle settings. */
|
|
270
|
-
particles: {
|
|
271
|
-
/** param {string} [options.particles.color='black'] - The color of the particles and their connections. Can be any CSS supported color format. */
|
|
272
|
-
color: '#88c8ffa0',
|
|
273
|
-
|
|
274
|
-
/** @param {number} [options.particles.ppm=100] - Particles per million (ppm).
|
|
275
|
-
* This determines how many particles are created per million pixels of the canvas.
|
|
276
|
-
* @example FHD on Chrome = 1920 width * 937 height = 1799040 pixels; 1799040 pixels * 100 ppm / 1_000_000 = 179.904 = 179 particles
|
|
277
|
-
* @important The amount of particles exponentially reduces performance.
|
|
278
|
-
* People with large screens will have a bad experience with high values.
|
|
279
|
-
* One solution is to increase particles.connectDistance and decrease this value.
|
|
280
|
-
*/
|
|
281
|
-
ppm: 100, // recommended: < 120
|
|
282
|
-
|
|
283
|
-
/** @param {number} [options.particles.max=500] - The maximum number of particles allowed. */
|
|
284
|
-
max: 200, // recommended: < 500
|
|
285
|
-
|
|
286
|
-
/** @param {number} [options.particles.maxWork=Infinity] - The maximum "work" a particle can perform before its connections are no longer drawn.
|
|
287
|
-
* @example 10 maxWork = 10 * 150 connectDistance = max 1500 pixels of lines drawn per particle
|
|
288
|
-
* @important Low values will stabilize performance at the cost of creating an ugly effect where connections may flicker.
|
|
289
|
-
*/
|
|
290
|
-
maxWork: 10,
|
|
291
|
-
|
|
292
|
-
/** @param {number} [options.particles.connectDistance=150] - The maximum distance for a connection between 2 particles.
|
|
293
|
-
* @note Heavily affects performance. */
|
|
294
|
-
connectDistance: 150,
|
|
295
|
-
|
|
296
|
-
/** @param {number} [options.particles.relSpeed=1] - The relative moving speed of the particles.
|
|
297
|
-
* The moving speed is a random value between 0.5 and 1 pixels per update multiplied by this value.
|
|
298
|
-
*/
|
|
299
|
-
relSpeed: 0.8,
|
|
300
|
-
|
|
301
|
-
/** @param {number} [options.particles.relSize=1] - The relative size of the particles.
|
|
302
|
-
* The ray is a random value between 0.5 and 2.5 pixels multiplied by this value.
|
|
303
|
-
*/
|
|
304
|
-
relSize: 1.1,
|
|
305
|
-
|
|
306
|
-
/** @param {number} [options.particles.rotationSpeed=2] - The speed at which the particles randomly changes direction.
|
|
307
|
-
* @example 1 rotationSpeed = max direction change of 0.01 radians per update
|
|
308
|
-
*/
|
|
309
|
-
rotationSpeed: 1, // recommended: < 10
|
|
310
|
-
|
|
311
|
-
/** @param {boolean} [options.particles.regenerateOnResize=false] - Create new particles when the canvas gets resized.
|
|
312
|
-
* @note If false, will instead add or remove a few particles to match particles.ppm
|
|
313
|
-
*/
|
|
314
|
-
regenerateOnResize: false,
|
|
315
|
-
},
|
|
316
|
-
|
|
317
|
-
/** @param {Object} [options.gravity] - Gravitational force settings.
|
|
318
|
-
* @important Heavily reduces performance if gravity.repulsive or gravity.pulling is not equal to 0
|
|
319
|
-
*/
|
|
320
|
-
gravity: {
|
|
321
|
-
/** @param {number} [options.gravity.repulsive=0] - The repulsive force between particles. */
|
|
322
|
-
repulsive: 2, // recommended: 0.50 - 5.00
|
|
323
|
-
|
|
324
|
-
/** @param {number} [options.gravity.pulling=0] - The attractive force pulling particles together. Works poorly if `gravity.repulsive` is too low.
|
|
325
|
-
* @note gravity.repulsive should be great enough to prevent forming a singularity.
|
|
326
|
-
*/
|
|
327
|
-
pulling: 0.0, // recommended: 0.01 - 0.10
|
|
328
|
-
|
|
329
|
-
/** @param {number} [options.gravity.friction=0.9] - The smoothness of the gravitational forces.
|
|
330
|
-
* The force gets multiplied by the fricion every update.
|
|
331
|
-
* @example force after x updates = force * friction ** x
|
|
332
|
-
*/
|
|
333
|
-
friction: 0.8, // recommended: 0.500 - 0.999
|
|
334
|
-
},
|
|
335
|
-
}
|
|
336
|
-
```
|
|
180
|
+
### Root options
|
|
181
|
+
|
|
182
|
+
| Option | Type | Default | Description |
|
|
183
|
+
| ------------ | ----------------- | ------- | ----------------------------------------------------------------------------- |
|
|
184
|
+
| `background` | `string \| false` | `false` | Canvas background. Any valid CSS `background` value. Ignored if not a string. |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### `animation`
|
|
189
|
+
|
|
190
|
+
| Option | Type | Default | Description |
|
|
191
|
+
| ------------------------ | --------- | ------- | ---------------------------------------------------- |
|
|
192
|
+
| `animation.startOnEnter` | `boolean` | `true` | Start animation when the canvas enters the viewport. |
|
|
193
|
+
| `animation.stopOnLeave` | `boolean` | `true` | Stop animation when the canvas leaves the viewport. |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### `mouse`
|
|
198
|
+
|
|
199
|
+
| Option | Type | Default | Description |
|
|
200
|
+
| ----------------------- | ------------- | ------- | ------------------------------------------------------------------------------------------------------------ |
|
|
201
|
+
| `mouse.interactionType` | `0 \| 1 \| 2` | `2` | Mouse interaction mode.<br>`0 = NONE`, `1 = SHIFT`, `2 = MOVE`. |
|
|
202
|
+
| `mouse.connectDistMult` | `float` | `2/3` | Multiplier applied to `particles.connectDistance` to compute mouse interaction distance. |
|
|
203
|
+
| `mouse.distRatio` | `float` | `2/3` | Controls how strongly particles are pulled toward the mouse. Keep equal to or above `mouse.connectDistMult`. |
|
|
204
|
+
|
|
205
|
+
**Interaction types** (enum)
|
|
337
206
|
|
|
338
|
-
|
|
207
|
+
- `NONE (0)` – No interaction
|
|
208
|
+
- `SHIFT (1)` – Visual displacement only
|
|
209
|
+
- `MOVE (2)` – Actual particle movement
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### `particles`
|
|
214
|
+
|
|
215
|
+
| Option | Type | Default | Description |
|
|
216
|
+
| ------------------------------ | --------- | ---------- | ------------------------------------------------------------------------------------------------- |
|
|
217
|
+
| `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
|
|
218
|
+
| `particles.ppm` | `integer` | `100` | Particles per million pixels. _Heavily impacts performance_ |
|
|
219
|
+
| `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
|
|
220
|
+
| `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
|
|
221
|
+
| `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). _Heavily impacts performance_ |
|
|
222
|
+
| `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
|
|
223
|
+
| `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
|
|
224
|
+
| `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
|
|
225
|
+
| `particles.regenerateOnResize` | `boolean` | `false` | Regenerate all particles when the canvas resizes. |
|
|
226
|
+
| `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### `gravity`
|
|
231
|
+
|
|
232
|
+
Enabling gravity (`repulsive` or `pulling` > 0) performs an extra **O(n²)** gravity computations per frame.
|
|
233
|
+
|
|
234
|
+
| Option | Type | Default | Description |
|
|
235
|
+
| ------------------- | ------- | ------- | -------------------------------------------------------------------------------------- |
|
|
236
|
+
| `gravity.repulsive` | `float` | `0` | Repulsive force between particles. Strongly impacts performance. |
|
|
237
|
+
| `gravity.pulling` | `float` | `0` | Attractive force between particles. Requires sufficient repulsion to avoid clustering. |
|
|
238
|
+
| `gravity.friction` | `float` | `0.8` | Damping factor applied to gravitational velocity each update (`0.0 – 1.0`). |
|
|
239
|
+
|
|
240
|
+
---
|
|
339
241
|
|
|
340
242
|
### Update options on the fly
|
|
341
243
|
|
|
342
|
-
|
|
244
|
+
You can update every option while an instance is animating and it works great; but some options require an extra step.
|
|
343
245
|
|
|
344
|
-
#### Using the setter
|
|
246
|
+
#### Using the available setter
|
|
345
247
|
|
|
346
|
-
These options require dedicated
|
|
248
|
+
These options are the only ones that have and require a dedicated setter to ensure proper integration:
|
|
347
249
|
|
|
348
|
-
-
|
|
349
|
-
-
|
|
350
|
-
-
|
|
250
|
+
- `background`
|
|
251
|
+
- `mouse.connectDistMult`
|
|
252
|
+
- `particles.color`
|
|
351
253
|
|
|
352
254
|
```js
|
|
353
255
|
const instance = new CanvasParticles(selector, options)
|
|
@@ -360,24 +262,29 @@ instance.setParticleColor('hsl(149, 100%, 50%)')
|
|
|
360
262
|
|
|
361
263
|
#### Changing the particle count
|
|
362
264
|
|
|
363
|
-
After updating the following options, the number of particles is not automatically adjusted
|
|
265
|
+
After updating the following options, the number of particles is **not automatically adjusted**:
|
|
364
266
|
|
|
365
|
-
-
|
|
366
|
-
-
|
|
267
|
+
- `particles.ppm`
|
|
268
|
+
- `particles.max`
|
|
367
269
|
|
|
368
270
|
```js
|
|
369
271
|
// Note: the backing field is called `option` not `options`!
|
|
370
272
|
instance.option.particles.ppm = 100
|
|
371
273
|
instance.option.particles.max = 300
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The changes are only applied when one of the following methods is called.
|
|
372
277
|
|
|
373
|
-
|
|
278
|
+
```js
|
|
374
279
|
instance.newParticles() // Remove all particles and create the correct amount of new ones
|
|
375
280
|
instance.matchParticleCount() // Add or remove some particles to match the count
|
|
376
281
|
```
|
|
377
282
|
|
|
378
283
|
#### Modifying object properties
|
|
379
284
|
|
|
380
|
-
**All** other options can be updated by modifying the `option` internal field properties, with changes taking
|
|
285
|
+
**All** other options can be updated by only modifying the `option` internal field properties, with changes taking effect immediately.
|
|
286
|
+
|
|
287
|
+
> The new option values are not validated. Assigning invalid values will lead to unexpected behavior. It is therefore recommended to use the [options setter](#updating-entire-options-object).
|
|
381
288
|
|
|
382
289
|
```js
|
|
383
290
|
// Note: the backing field is called `option` not `options`!
|
|
@@ -391,11 +298,12 @@ instance.option.gravity.repulsive = 1
|
|
|
391
298
|
To reinitialize all options, pass a new options object to the `options` setter.
|
|
392
299
|
|
|
393
300
|
```js
|
|
394
|
-
|
|
395
|
-
instance.options = options
|
|
301
|
+
instance.options = { ... }
|
|
396
302
|
```
|
|
397
303
|
|
|
398
|
-
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## One Pager Example
|
|
399
307
|
|
|
400
308
|
```html
|
|
401
309
|
<!DOCTYPE html>
|
|
@@ -423,7 +331,7 @@ instance.options = options
|
|
|
423
331
|
const options = {
|
|
424
332
|
background: 'hsl(125, 42%, 35%)',
|
|
425
333
|
mouse: {
|
|
426
|
-
interactionType: CanvasParticles.interactionType.MOVE, //
|
|
334
|
+
interactionType: CanvasParticles.interactionType.MOVE, // = 2
|
|
427
335
|
},
|
|
428
336
|
particles: {
|
|
429
337
|
color: 'rgba(150, 255, 105, 0.95)',
|
package/dist/index.cjs
CHANGED
|
@@ -16,12 +16,14 @@ function Mulberry32(seed) {
|
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
// Mulberry32 is ±
|
|
19
|
+
// Mulberry32 is ±392% faster than Math.random()
|
|
20
20
|
// Benchmark: https://jsbm.dev/muLCWR9RJCbmy
|
|
21
21
|
// Spectral test: /demo/mulberry32.html
|
|
22
22
|
const prng = Mulberry32(Math.random() * 2 ** 32).next;
|
|
23
23
|
class CanvasParticles {
|
|
24
|
-
static version = "4.1
|
|
24
|
+
static version = "4.2.1";
|
|
25
|
+
static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
|
|
26
|
+
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
25
27
|
/** Defines mouse interaction types with the particles */
|
|
26
28
|
static interactionType = Object.freeze({
|
|
27
29
|
NONE: 0, // No mouse interaction
|
|
@@ -57,10 +59,8 @@ class CanvasParticles {
|
|
|
57
59
|
canvas.instance.resizeCanvas();
|
|
58
60
|
}
|
|
59
61
|
});
|
|
60
|
-
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
|
|
61
|
-
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
62
62
|
/** Helper functions for options parsing */
|
|
63
|
-
static defaultIfNaN = (value, defaultValue) =>
|
|
63
|
+
static defaultIfNaN = (value, defaultValue) => isNaN(+value) ? defaultValue : +value;
|
|
64
64
|
static parseNumericOption = (name, value, defaultValue, clamp) => {
|
|
65
65
|
if (value == undefined)
|
|
66
66
|
return defaultValue;
|
|
@@ -68,7 +68,7 @@ class CanvasParticles {
|
|
|
68
68
|
if (isFinite(min) && value < min) {
|
|
69
69
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
70
70
|
}
|
|
71
|
-
if (isFinite(max) && value > max) {
|
|
71
|
+
else if (isFinite(max) && value > max) {
|
|
72
72
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
73
73
|
}
|
|
74
74
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -237,11 +237,12 @@ class CanvasParticles {
|
|
|
237
237
|
return;
|
|
238
238
|
const len = this.particleCount;
|
|
239
239
|
const particles = this.particles;
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
const
|
|
240
|
+
const connectDist = this.option.particles.connectDist;
|
|
241
|
+
const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
|
|
242
|
+
const gravPullingMult = connectDist * this.option.gravity.pulling * step;
|
|
243
|
+
const maxRepulsiveDist = connectDist / 2;
|
|
244
|
+
const maxRepulsiveDistSq = maxRepulsiveDist * maxRepulsiveDist;
|
|
245
|
+
const eps = (connectDist * connectDist) / 256;
|
|
245
246
|
for (let i = 0; i < len; i++) {
|
|
246
247
|
const particleA = particles[i];
|
|
247
248
|
for (let j = i + 1; j < len; j++) {
|
|
@@ -250,17 +251,17 @@ class CanvasParticles {
|
|
|
250
251
|
const distX = particleA.posX - particleB.posX;
|
|
251
252
|
const distY = particleA.posY - particleB.posY;
|
|
252
253
|
const distSq = distX * distX + distY * distY;
|
|
254
|
+
if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
|
|
255
|
+
continue;
|
|
253
256
|
let angle;
|
|
254
257
|
let grav;
|
|
255
258
|
let gravMult;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
|
|
259
|
-
grav = (1 / Math.sqrt(distSq)) ** 1.8;
|
|
259
|
+
angle = Math.atan2(-distY, -distX);
|
|
260
|
+
grav = 1 / (distSq + eps);
|
|
260
261
|
const angleX = Math.cos(angle);
|
|
261
262
|
const angleY = Math.sin(angle);
|
|
262
263
|
if (distSq < maxRepulsiveDistSq) {
|
|
263
|
-
gravMult =
|
|
264
|
+
gravMult = grav * gravRepulsiveMult;
|
|
264
265
|
const gravX = angleX * gravMult;
|
|
265
266
|
const gravY = angleY * gravMult;
|
|
266
267
|
particleA.velX -= gravX;
|
|
@@ -270,7 +271,7 @@ class CanvasParticles {
|
|
|
270
271
|
}
|
|
271
272
|
if (!isPullingEnabled)
|
|
272
273
|
continue;
|
|
273
|
-
gravMult =
|
|
274
|
+
gravMult = grav * gravPullingMult;
|
|
274
275
|
const gravX = angleX * gravMult;
|
|
275
276
|
const gravY = angleY * gravMult;
|
|
276
277
|
particleA.velX += gravX;
|
|
@@ -290,22 +291,23 @@ class CanvasParticles {
|
|
|
290
291
|
const offY = this.offY;
|
|
291
292
|
const mouseX = this.mouseX;
|
|
292
293
|
const mouseY = this.mouseY;
|
|
293
|
-
const rotationSpeed = this.option.particles.rotationSpeed;
|
|
294
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
294
295
|
const friction = this.option.gravity.friction;
|
|
295
296
|
const mouseConnectDist = this.option.mouse.connectDist;
|
|
296
297
|
const mouseDistRatio = this.option.mouse.distRatio;
|
|
297
298
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
|
|
298
299
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
|
|
300
|
+
const easing = 1 - Math.pow(1 - 1 / 4, step);
|
|
299
301
|
for (let i = 0; i < len; i++) {
|
|
300
302
|
const particle = particles[i];
|
|
301
|
-
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed;
|
|
303
|
+
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
|
|
302
304
|
particle.dir %= TWO_PI;
|
|
303
305
|
// Constant velocity
|
|
304
306
|
const movX = Math.sin(particle.dir) * particle.speed;
|
|
305
307
|
const movY = Math.cos(particle.dir) * particle.speed;
|
|
306
308
|
// Apply velocities
|
|
307
|
-
particle.posX += movX + particle.velX;
|
|
308
|
-
particle.posY += movY + particle.velY;
|
|
309
|
+
particle.posX += (movX + particle.velX) * step;
|
|
310
|
+
particle.posY += (movY + particle.velY) * step;
|
|
309
311
|
// Wrap particles around the canvas
|
|
310
312
|
particle.posX %= width;
|
|
311
313
|
if (particle.posX < 0)
|
|
@@ -314,8 +316,8 @@ class CanvasParticles {
|
|
|
314
316
|
if (particle.posY < 0)
|
|
315
317
|
particle.posY += height;
|
|
316
318
|
// Slightly decrease dynamic velocity
|
|
317
|
-
particle.velX *= friction;
|
|
318
|
-
particle.velY *= friction;
|
|
319
|
+
particle.velX *= Math.pow(friction, step);
|
|
320
|
+
particle.velY *= Math.pow(friction, step);
|
|
319
321
|
// Distance from mouse
|
|
320
322
|
const distX = particle.posX + offX - mouseX;
|
|
321
323
|
const distY = particle.posY + offY - mouseY;
|
|
@@ -323,12 +325,12 @@ class CanvasParticles {
|
|
|
323
325
|
if (!isMouseInteractionTypeNone) {
|
|
324
326
|
const distRatio = mouseConnectDist / Math.hypot(distX, distY);
|
|
325
327
|
if (mouseDistRatio < distRatio) {
|
|
326
|
-
particle.offX += (distRatio * distX - distX - particle.offX)
|
|
327
|
-
particle.offY += (distRatio * distY - distY - particle.offY)
|
|
328
|
+
particle.offX += (distRatio * distX - distX - particle.offX) * easing;
|
|
329
|
+
particle.offY += (distRatio * distY - distY - particle.offY) * easing;
|
|
328
330
|
}
|
|
329
331
|
else {
|
|
330
|
-
particle.offX -= particle.offX
|
|
331
|
-
particle.offY -= particle.offY
|
|
332
|
+
particle.offX -= particle.offX * easing;
|
|
333
|
+
particle.offY -= particle.offY * easing;
|
|
332
334
|
}
|
|
333
335
|
}
|
|
334
336
|
// Visually displace the particles
|
|
@@ -403,9 +405,9 @@ class CanvasParticles {
|
|
|
403
405
|
const particles = this.particles;
|
|
404
406
|
const ctx = this.ctx;
|
|
405
407
|
const maxDist = this.option.particles.connectDist;
|
|
406
|
-
const maxDistSq = maxDist
|
|
408
|
+
const maxDistSq = maxDist * maxDist;
|
|
407
409
|
const halfMaxDist = maxDist / 2;
|
|
408
|
-
const halfMaxDistSq = halfMaxDist
|
|
410
|
+
const halfMaxDistSq = halfMaxDist * halfMaxDist;
|
|
409
411
|
const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height);
|
|
410
412
|
const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
|
|
411
413
|
const alpha = this.color.alpha;
|
|
@@ -419,7 +421,7 @@ class CanvasParticles {
|
|
|
419
421
|
// Code in this scope runs O(n^2) times per frame!
|
|
420
422
|
const particleB = particles[j];
|
|
421
423
|
// Don't draw the line if it wouldn't be visible
|
|
422
|
-
if (!
|
|
424
|
+
if (!drawAll && !this.#isLineVisible(particleA, particleB))
|
|
423
425
|
continue;
|
|
424
426
|
const distX = particleA.x - particleB.x;
|
|
425
427
|
const distY = particleA.y - particleB.y;
|
|
@@ -464,7 +466,8 @@ class CanvasParticles {
|
|
|
464
466
|
this.ctx.strokeStyle = this.color.hex;
|
|
465
467
|
this.ctx.lineWidth = 1;
|
|
466
468
|
this.#renderParticles();
|
|
467
|
-
this
|
|
469
|
+
if (this.options.particles.drawLines)
|
|
470
|
+
this.#renderConnections();
|
|
468
471
|
}
|
|
469
472
|
/** @private Main animation loop that updates and renders the particles */
|
|
470
473
|
#animation() {
|
|
@@ -478,7 +481,7 @@ class CanvasParticles {
|
|
|
478
481
|
// - step = 1 → exactly one baseline update (dt === BASE_DT)
|
|
479
482
|
// - step > 1 → more time passed (lower FPS), advance further
|
|
480
483
|
// - step < 1 → less time passed (higher FPS), advance less
|
|
481
|
-
const step = CanvasParticles.BASE_DT
|
|
484
|
+
const step = dt / CanvasParticles.BASE_DT;
|
|
482
485
|
this.#updateGravity(step);
|
|
483
486
|
this.#updateParticles(step);
|
|
484
487
|
this.#render();
|
|
@@ -527,20 +530,21 @@ class CanvasParticles {
|
|
|
527
530
|
stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
|
|
528
531
|
},
|
|
529
532
|
mouse: {
|
|
530
|
-
interactionType: pno('mouse.interactionType', options.mouse?.interactionType,
|
|
533
|
+
interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
|
|
531
534
|
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
|
|
532
535
|
connectDist: 1 /* post processed */,
|
|
533
536
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
|
|
534
537
|
},
|
|
535
538
|
particles: {
|
|
536
539
|
regenerateOnResize: !!options.particles?.regenerateOnResize,
|
|
540
|
+
drawLines: !!(options.particles?.drawLines ?? true),
|
|
537
541
|
color: options.particles?.color ?? 'black',
|
|
538
|
-
ppm: pno('particles.ppm', options.particles?.ppm, 100),
|
|
539
|
-
max: pno('particles.max', options.particles?.max, Infinity),
|
|
540
|
-
maxWork: pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 }),
|
|
541
|
-
connectDist: pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
|
|
542
|
+
ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
|
|
543
|
+
max: Math.round(pno('particles.max', options.particles?.max, Infinity)),
|
|
544
|
+
maxWork: Math.round(pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 })),
|
|
545
|
+
connectDist: ~~pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
|
|
542
546
|
relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
|
|
543
|
-
relSize: pno('particles.relSize', options.particles?.relSize, 1, { min:
|
|
547
|
+
relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 0 }),
|
|
544
548
|
rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
|
|
545
549
|
},
|
|
546
550
|
gravity: {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './type
|
|
|
3
3
|
export default class CanvasParticles {
|
|
4
4
|
#private;
|
|
5
5
|
static readonly version: string;
|
|
6
|
+
private static readonly MAX_DT;
|
|
7
|
+
private static readonly BASE_DT;
|
|
6
8
|
/** Defines mouse interaction types with the particles */
|
|
7
|
-
static interactionType: Readonly<{
|
|
9
|
+
static readonly interactionType: Readonly<{
|
|
8
10
|
NONE: 0;
|
|
9
11
|
SHIFT: 1;
|
|
10
12
|
MOVE: 2;
|
|
@@ -12,11 +14,9 @@ export default class CanvasParticles {
|
|
|
12
14
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
13
15
|
static readonly canvasIntersectionObserver: IntersectionObserver;
|
|
14
16
|
static readonly canvasResizeObserver: ResizeObserver;
|
|
15
|
-
private static readonly MAX_DT;
|
|
16
|
-
private static readonly BASE_DT;
|
|
17
17
|
/** Helper functions for options parsing */
|
|
18
|
-
private static defaultIfNaN;
|
|
19
|
-
private static parseNumericOption;
|
|
18
|
+
private static readonly defaultIfNaN;
|
|
19
|
+
private static readonly parseNumericOption;
|
|
20
20
|
canvas: CanvasParticlesCanvas;
|
|
21
21
|
private ctx;
|
|
22
22
|
enableAnimating: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -14,12 +14,14 @@ function Mulberry32(seed) {
|
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
// Mulberry32 is ±
|
|
17
|
+
// Mulberry32 is ±392% faster than Math.random()
|
|
18
18
|
// Benchmark: https://jsbm.dev/muLCWR9RJCbmy
|
|
19
19
|
// Spectral test: /demo/mulberry32.html
|
|
20
20
|
const prng = Mulberry32(Math.random() * 2 ** 32).next;
|
|
21
21
|
class CanvasParticles {
|
|
22
|
-
static version = "4.1
|
|
22
|
+
static version = "4.2.1";
|
|
23
|
+
static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
|
|
24
|
+
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
23
25
|
/** Defines mouse interaction types with the particles */
|
|
24
26
|
static interactionType = Object.freeze({
|
|
25
27
|
NONE: 0, // No mouse interaction
|
|
@@ -55,10 +57,8 @@ class CanvasParticles {
|
|
|
55
57
|
canvas.instance.resizeCanvas();
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
|
-
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
|
|
59
|
-
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
60
60
|
/** Helper functions for options parsing */
|
|
61
|
-
static defaultIfNaN = (value, defaultValue) =>
|
|
61
|
+
static defaultIfNaN = (value, defaultValue) => isNaN(+value) ? defaultValue : +value;
|
|
62
62
|
static parseNumericOption = (name, value, defaultValue, clamp) => {
|
|
63
63
|
if (value == undefined)
|
|
64
64
|
return defaultValue;
|
|
@@ -66,7 +66,7 @@ class CanvasParticles {
|
|
|
66
66
|
if (isFinite(min) && value < min) {
|
|
67
67
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
68
68
|
}
|
|
69
|
-
if (isFinite(max) && value > max) {
|
|
69
|
+
else if (isFinite(max) && value > max) {
|
|
70
70
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
71
71
|
}
|
|
72
72
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -235,11 +235,12 @@ class CanvasParticles {
|
|
|
235
235
|
return;
|
|
236
236
|
const len = this.particleCount;
|
|
237
237
|
const particles = this.particles;
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const
|
|
238
|
+
const connectDist = this.option.particles.connectDist;
|
|
239
|
+
const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
|
|
240
|
+
const gravPullingMult = connectDist * this.option.gravity.pulling * step;
|
|
241
|
+
const maxRepulsiveDist = connectDist / 2;
|
|
242
|
+
const maxRepulsiveDistSq = maxRepulsiveDist * maxRepulsiveDist;
|
|
243
|
+
const eps = (connectDist * connectDist) / 256;
|
|
243
244
|
for (let i = 0; i < len; i++) {
|
|
244
245
|
const particleA = particles[i];
|
|
245
246
|
for (let j = i + 1; j < len; j++) {
|
|
@@ -248,17 +249,17 @@ class CanvasParticles {
|
|
|
248
249
|
const distX = particleA.posX - particleB.posX;
|
|
249
250
|
const distY = particleA.posY - particleB.posY;
|
|
250
251
|
const distSq = distX * distX + distY * distY;
|
|
252
|
+
if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
|
|
253
|
+
continue;
|
|
251
254
|
let angle;
|
|
252
255
|
let grav;
|
|
253
256
|
let gravMult;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
|
|
257
|
-
grav = (1 / Math.sqrt(distSq)) ** 1.8;
|
|
257
|
+
angle = Math.atan2(-distY, -distX);
|
|
258
|
+
grav = 1 / (distSq + eps);
|
|
258
259
|
const angleX = Math.cos(angle);
|
|
259
260
|
const angleY = Math.sin(angle);
|
|
260
261
|
if (distSq < maxRepulsiveDistSq) {
|
|
261
|
-
gravMult =
|
|
262
|
+
gravMult = grav * gravRepulsiveMult;
|
|
262
263
|
const gravX = angleX * gravMult;
|
|
263
264
|
const gravY = angleY * gravMult;
|
|
264
265
|
particleA.velX -= gravX;
|
|
@@ -268,7 +269,7 @@ class CanvasParticles {
|
|
|
268
269
|
}
|
|
269
270
|
if (!isPullingEnabled)
|
|
270
271
|
continue;
|
|
271
|
-
gravMult =
|
|
272
|
+
gravMult = grav * gravPullingMult;
|
|
272
273
|
const gravX = angleX * gravMult;
|
|
273
274
|
const gravY = angleY * gravMult;
|
|
274
275
|
particleA.velX += gravX;
|
|
@@ -288,22 +289,23 @@ class CanvasParticles {
|
|
|
288
289
|
const offY = this.offY;
|
|
289
290
|
const mouseX = this.mouseX;
|
|
290
291
|
const mouseY = this.mouseY;
|
|
291
|
-
const rotationSpeed = this.option.particles.rotationSpeed;
|
|
292
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
292
293
|
const friction = this.option.gravity.friction;
|
|
293
294
|
const mouseConnectDist = this.option.mouse.connectDist;
|
|
294
295
|
const mouseDistRatio = this.option.mouse.distRatio;
|
|
295
296
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
|
|
296
297
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
|
|
298
|
+
const easing = 1 - Math.pow(1 - 1 / 4, step);
|
|
297
299
|
for (let i = 0; i < len; i++) {
|
|
298
300
|
const particle = particles[i];
|
|
299
|
-
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed;
|
|
301
|
+
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
|
|
300
302
|
particle.dir %= TWO_PI;
|
|
301
303
|
// Constant velocity
|
|
302
304
|
const movX = Math.sin(particle.dir) * particle.speed;
|
|
303
305
|
const movY = Math.cos(particle.dir) * particle.speed;
|
|
304
306
|
// Apply velocities
|
|
305
|
-
particle.posX += movX + particle.velX;
|
|
306
|
-
particle.posY += movY + particle.velY;
|
|
307
|
+
particle.posX += (movX + particle.velX) * step;
|
|
308
|
+
particle.posY += (movY + particle.velY) * step;
|
|
307
309
|
// Wrap particles around the canvas
|
|
308
310
|
particle.posX %= width;
|
|
309
311
|
if (particle.posX < 0)
|
|
@@ -312,8 +314,8 @@ class CanvasParticles {
|
|
|
312
314
|
if (particle.posY < 0)
|
|
313
315
|
particle.posY += height;
|
|
314
316
|
// Slightly decrease dynamic velocity
|
|
315
|
-
particle.velX *= friction;
|
|
316
|
-
particle.velY *= friction;
|
|
317
|
+
particle.velX *= Math.pow(friction, step);
|
|
318
|
+
particle.velY *= Math.pow(friction, step);
|
|
317
319
|
// Distance from mouse
|
|
318
320
|
const distX = particle.posX + offX - mouseX;
|
|
319
321
|
const distY = particle.posY + offY - mouseY;
|
|
@@ -321,12 +323,12 @@ class CanvasParticles {
|
|
|
321
323
|
if (!isMouseInteractionTypeNone) {
|
|
322
324
|
const distRatio = mouseConnectDist / Math.hypot(distX, distY);
|
|
323
325
|
if (mouseDistRatio < distRatio) {
|
|
324
|
-
particle.offX += (distRatio * distX - distX - particle.offX)
|
|
325
|
-
particle.offY += (distRatio * distY - distY - particle.offY)
|
|
326
|
+
particle.offX += (distRatio * distX - distX - particle.offX) * easing;
|
|
327
|
+
particle.offY += (distRatio * distY - distY - particle.offY) * easing;
|
|
326
328
|
}
|
|
327
329
|
else {
|
|
328
|
-
particle.offX -= particle.offX
|
|
329
|
-
particle.offY -= particle.offY
|
|
330
|
+
particle.offX -= particle.offX * easing;
|
|
331
|
+
particle.offY -= particle.offY * easing;
|
|
330
332
|
}
|
|
331
333
|
}
|
|
332
334
|
// Visually displace the particles
|
|
@@ -401,9 +403,9 @@ class CanvasParticles {
|
|
|
401
403
|
const particles = this.particles;
|
|
402
404
|
const ctx = this.ctx;
|
|
403
405
|
const maxDist = this.option.particles.connectDist;
|
|
404
|
-
const maxDistSq = maxDist
|
|
406
|
+
const maxDistSq = maxDist * maxDist;
|
|
405
407
|
const halfMaxDist = maxDist / 2;
|
|
406
|
-
const halfMaxDistSq = halfMaxDist
|
|
408
|
+
const halfMaxDistSq = halfMaxDist * halfMaxDist;
|
|
407
409
|
const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height);
|
|
408
410
|
const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
|
|
409
411
|
const alpha = this.color.alpha;
|
|
@@ -417,7 +419,7 @@ class CanvasParticles {
|
|
|
417
419
|
// Code in this scope runs O(n^2) times per frame!
|
|
418
420
|
const particleB = particles[j];
|
|
419
421
|
// Don't draw the line if it wouldn't be visible
|
|
420
|
-
if (!
|
|
422
|
+
if (!drawAll && !this.#isLineVisible(particleA, particleB))
|
|
421
423
|
continue;
|
|
422
424
|
const distX = particleA.x - particleB.x;
|
|
423
425
|
const distY = particleA.y - particleB.y;
|
|
@@ -462,7 +464,8 @@ class CanvasParticles {
|
|
|
462
464
|
this.ctx.strokeStyle = this.color.hex;
|
|
463
465
|
this.ctx.lineWidth = 1;
|
|
464
466
|
this.#renderParticles();
|
|
465
|
-
this
|
|
467
|
+
if (this.options.particles.drawLines)
|
|
468
|
+
this.#renderConnections();
|
|
466
469
|
}
|
|
467
470
|
/** @private Main animation loop that updates and renders the particles */
|
|
468
471
|
#animation() {
|
|
@@ -476,7 +479,7 @@ class CanvasParticles {
|
|
|
476
479
|
// - step = 1 → exactly one baseline update (dt === BASE_DT)
|
|
477
480
|
// - step > 1 → more time passed (lower FPS), advance further
|
|
478
481
|
// - step < 1 → less time passed (higher FPS), advance less
|
|
479
|
-
const step = CanvasParticles.BASE_DT
|
|
482
|
+
const step = dt / CanvasParticles.BASE_DT;
|
|
480
483
|
this.#updateGravity(step);
|
|
481
484
|
this.#updateParticles(step);
|
|
482
485
|
this.#render();
|
|
@@ -525,20 +528,21 @@ class CanvasParticles {
|
|
|
525
528
|
stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
|
|
526
529
|
},
|
|
527
530
|
mouse: {
|
|
528
|
-
interactionType: pno('mouse.interactionType', options.mouse?.interactionType,
|
|
531
|
+
interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
|
|
529
532
|
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
|
|
530
533
|
connectDist: 1 /* post processed */,
|
|
531
534
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
|
|
532
535
|
},
|
|
533
536
|
particles: {
|
|
534
537
|
regenerateOnResize: !!options.particles?.regenerateOnResize,
|
|
538
|
+
drawLines: !!(options.particles?.drawLines ?? true),
|
|
535
539
|
color: options.particles?.color ?? 'black',
|
|
536
|
-
ppm: pno('particles.ppm', options.particles?.ppm, 100),
|
|
537
|
-
max: pno('particles.max', options.particles?.max, Infinity),
|
|
538
|
-
maxWork: pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 }),
|
|
539
|
-
connectDist: pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
|
|
540
|
+
ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
|
|
541
|
+
max: Math.round(pno('particles.max', options.particles?.max, Infinity)),
|
|
542
|
+
maxWork: Math.round(pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 })),
|
|
543
|
+
connectDist: ~~pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
|
|
540
544
|
relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
|
|
541
|
-
relSize: pno('particles.relSize', options.particles?.relSize, 1, { min:
|
|
545
|
+
relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 0 }),
|
|
542
546
|
rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
|
|
543
547
|
},
|
|
544
548
|
gravity: {
|
package/dist/index.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.1.5";static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static MAX_DT=1e3/30;static BASE_DT=1e3/60;static defaultIfNaN=(t,i)=>isNaN(+t)?i:+t;static parseNumericOption=(t,i,s,n)=>{if(null==i)return s;const{min:o=-1/0,max:a=1/0}=n??{};return isFinite(o)&&i<o&&console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)),isFinite(a)&&i>a&&console.warn(new RangeError(`option.${t} was clamped to ${a} as ${i} is too high`)),e.defaultIfNaN(Math.min(Math.max(i??s,o),a),s)};canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];particleCount=0;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("particleCount must be finite")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#e(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particleCount,n=this.particles,o=this.option.particles.connectDist*this.option.gravity.repulsive,a=this.option.particles.connectDist*this.option.gravity.pulling,r=(this.option.particles.connectDist/2)**2,c=.1*this.option.particles.connectDist;for(let t=0;t<s;t++){const i=n[t];for(let h=t+1;h<s;h++){const t=n[h],s=i.posX-t.posX,l=i.posY-t.posY,p=s*s+l*l;let u,d,f;if(p>=r&&!e)continue;u=Math.atan2(t.posY-i.posY,t.posX-i.posX),d=(1/Math.sqrt(p))**1.8;const m=Math.cos(u),v=Math.sin(u);if(p<r){f=Math.min(c,d*o);const e=m*f,s=v*f;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;f=Math.min(c,d*a);const g=m*f,y=v*f;i.velX+=g,i.velY+=y,t.velX-=g,t.velY-=y}}}#n(i){const s=this.particleCount,n=this.particles,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed,u=this.option.gravity.friction,d=this.option.mouse.connectDist,f=this.option.mouse.distRatio,m=this.option.mouse.interactionType===e.interactionType.NONE,v=this.option.mouse.interactionType===e.interactionType.MOVE;for(let i=0;i<s;i++){const e=n[i];e.dir+=2*(Math.random()-.5)*p,e.dir%=t;const s=Math.sin(e.dir)*e.speed,g=Math.cos(e.dir)*e.speed;e.posX+=s+e.velX,e.posY+=g+e.velY,e.posX%=o,e.posX<0&&(e.posX+=o),e.posY%=a,e.posY<0&&(e.posY+=a),e.velX*=u,e.velY*=u;const y=e.posX+r-h,x=e.posY+c-l;if(!m){const t=d/Math.hypot(y,x);f<t?(e.offX+=(t*y-y-e.offX)/4,e.offY+=(t*x-x-e.offY)/4):(e.offX-=e.offX/4,e.offY-=e.offY/4)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,v&&(e.posX=e.x,e.posY=e.y),e.x+=r,e.y+=c,this.#o(e),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particleCount,e=this.particles,s=this.ctx;for(let n=0;n<i;n++){const i=e[n];i.isVisible&&(i.size>1?(s.beginPath(),s.arc(i.x,i.y,i.size,0,t),s.fill(),s.closePath()):s.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}}#c(){const t=this.particleCount,i=this.particles,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<t;s++){const p=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!a&&!this.#a(p,t))continue;const s=p.x-t.x,f=p.y-t.y,m=s*s+f*f;if(!(m>n)&&(m>o?(e.globalAlpha=h/Math.sqrt(m)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(t.x,t.y),e.stroke()):l.push([p.x,p.y,t.x,t.y]),(u+=m)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT),s=e.BASE_DT/i;this.#s(s),this.#n(s),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:i("mouse.interactionType",t.mouse?.interactionType,1),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i("particles.ppm",t.particles?.ppm,100),max:i("particles.max",t.particles?.max,1/0),maxWork:i("particles.maxWork",t.particles?.maxWork,1/0,{min:0}),connectDist:i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:1}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0),pulling:i("gravity.pulling",t.gravity?.pulling,0),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
|
|
1
|
+
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.2.1";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static defaultIfNaN=(t,i)=>isNaN(+t)?i:+t;static parseNumericOption=(t,i,s,n)=>{if(null==i)return s;const{min:o=-1/0,max:a=1/0}=n??{};return isFinite(o)&&i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):isFinite(a)&&i>a&&console.warn(new RangeError(`option.${t} was clamped to ${a} as ${i} is too high`)),e.defaultIfNaN(Math.min(Math.max(i??s,o),a),s)};canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];particleCount=0;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("particleCount must be finite")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#e(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particleCount,n=this.particles,o=this.option.particles.connectDist,a=o*this.option.gravity.repulsive*t,r=o*this.option.gravity.pulling*t,c=o/2,h=c*c,l=o*o/256;for(let t=0;t<s;t++){const i=n[t];for(let o=t+1;o<s;o++){const t=n[o],s=i.posX-t.posX,c=i.posY-t.posY,p=s*s+c*c;if(p>=h&&!e)continue;let u,d,f;u=Math.atan2(-c,-s),d=1/(p+l);const m=Math.cos(u),v=Math.sin(u);if(p<h){f=d*a;const e=m*f,s=v*f;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;f=d*r;const g=m*f,y=v*f;i.velX+=g,i.velY+=y,t.velX-=g,t.velY-=y}}}#n(i){const s=this.particleCount,n=this.particles,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,f=this.option.mouse.distRatio,m=this.option.mouse.interactionType===e.interactionType.NONE,v=this.option.mouse.interactionType===e.interactionType.MOVE,g=1-Math.pow(.75,i);for(let e=0;e<s;e++){const s=n[e];s.dir+=2*(Math.random()-.5)*p*i,s.dir%=t;const y=Math.sin(s.dir)*s.speed,x=Math.cos(s.dir)*s.speed;s.posX+=(y+s.velX)*i,s.posY+=(x+s.velY)*i,s.posX%=o,s.posX<0&&(s.posX+=o),s.posY%=a,s.posY<0&&(s.posY+=a),s.velX*=Math.pow(u,i),s.velY*=Math.pow(u,i);const b=s.posX+r-h,w=s.posY+c-l;if(!m){const t=d/Math.hypot(b,w);f<t?(s.offX+=(t*b-b-s.offX)*g,s.offY+=(t*w-w-s.offY)*g):(s.offX-=s.offX*g,s.offY-=s.offY*g)}s.x=s.posX+s.offX,s.y=s.posY+s.offY,v&&(s.posX=s.x,s.posY=s.y),s.x+=r,s.y+=c,this.#o(s),s.isVisible=1===s.gridPos.x&&1===s.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particleCount,e=this.particles,s=this.ctx;for(let n=0;n<i;n++){const i=e[n];i.isVisible&&(i.size>1?(s.beginPath(),s.arc(i.x,i.y,i.size,0,t),s.fill(),s.closePath()):s.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}}#c(){const t=this.particleCount,i=this.particles,e=this.ctx,s=this.option.particles.connectDist,n=s*s,o=s/2,a=o*o,r=s>=Math.min(this.canvas.width,this.canvas.height),c=n*this.option.particles.maxWork,h=this.color.alpha,l=this.color.alpha*s,p=[];for(let s=0;s<t;s++){const o=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!r&&!this.#a(o,t))continue;const s=o.x-t.x,f=o.y-t.y,m=s*s+f*f;if(!(m>n)&&(m>a?(e.globalAlpha=l/Math.sqrt(m)-h,e.beginPath(),e.moveTo(o.x,o.y),e.lineTo(t.x,t.y),e.stroke()):p.push([o.x,o.y,t.x,t.y]),(u+=m)>=c))break}}if(p.length){e.globalAlpha=h,e.beginPath();for(let t=0;t<p.length;t++){const i=p[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.options.particles.drawLines&&this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#s(i),this.#n(i),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:~~i("mouse.interactionType",t.mouse?.interactionType,e.interactionType.MOVE,{min:0,max:2}),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,drawLines:!!(t.particles?.drawLines??1),color:t.particles?.color??"black",ppm:~~i("particles.ppm",t.particles?.ppm,100),max:Math.round(i("particles.max",t.particles?.max,1/0)),maxWork:Math.round(i("particles.maxWork",t.particles?.maxWork,1/0,{min:0})),connectDist:~~i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:0}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0),pulling:i("gravity.pulling",t.gravity?.pulling,0),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
|
package/dist/types/options.d.ts
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2022–2025 Kyle Hoeckman, MIT License
|
|
2
2
|
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
3
3
|
|
|
4
|
-
import type { CanvasParticlesCanvas, Particle,
|
|
4
|
+
import type { CanvasParticlesCanvas, Particle, ContextColor, LineSegment } from './types'
|
|
5
5
|
import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
|
|
6
6
|
|
|
7
7
|
const TWO_PI = 2 * Math.PI
|
|
@@ -21,7 +21,7 @@ function Mulberry32(seed: number) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Mulberry32 is ±
|
|
24
|
+
// Mulberry32 is ±392% faster than Math.random()
|
|
25
25
|
// Benchmark: https://jsbm.dev/muLCWR9RJCbmy
|
|
26
26
|
// Spectral test: /demo/mulberry32.html
|
|
27
27
|
const prng = Mulberry32(Math.random() * 2 ** 32).next
|
|
@@ -31,8 +31,11 @@ declare const __VERSION__: string
|
|
|
31
31
|
export default class CanvasParticles {
|
|
32
32
|
static readonly version = __VERSION__
|
|
33
33
|
|
|
34
|
+
private static readonly MAX_DT = 1000 / 50 // milliseconds between updates @ 50 FPS
|
|
35
|
+
private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
|
|
36
|
+
|
|
34
37
|
/** Defines mouse interaction types with the particles */
|
|
35
|
-
static interactionType = Object.freeze({
|
|
38
|
+
static readonly interactionType = Object.freeze({
|
|
36
39
|
NONE: 0, // No mouse interaction
|
|
37
40
|
SHIFT: 1, // Visual displacement only
|
|
38
41
|
MOVE: 2, // Actual particle movement
|
|
@@ -71,12 +74,11 @@ export default class CanvasParticles {
|
|
|
71
74
|
}
|
|
72
75
|
})
|
|
73
76
|
|
|
74
|
-
private static readonly MAX_DT = 1000 / 30 // milliseconds between updates @ 30 FPS
|
|
75
|
-
private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
|
|
76
77
|
/** Helper functions for options parsing */
|
|
77
|
-
private static defaultIfNaN = (value: number, defaultValue: number): number =>
|
|
78
|
+
private static readonly defaultIfNaN = (value: number, defaultValue: number): number =>
|
|
79
|
+
isNaN(+value) ? defaultValue : +value
|
|
78
80
|
|
|
79
|
-
private static parseNumericOption = (
|
|
81
|
+
private static readonly parseNumericOption = (
|
|
80
82
|
name: string,
|
|
81
83
|
value: number | undefined,
|
|
82
84
|
defaultValue: number,
|
|
@@ -88,9 +90,7 @@ export default class CanvasParticles {
|
|
|
88
90
|
|
|
89
91
|
if (isFinite(min) && value < min) {
|
|
90
92
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (isFinite(max) && value > max) {
|
|
93
|
+
} else if (isFinite(max) && value > max) {
|
|
94
94
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -281,11 +281,12 @@ export default class CanvasParticles {
|
|
|
281
281
|
|
|
282
282
|
const len = this.particleCount
|
|
283
283
|
const particles = this.particles
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
const
|
|
284
|
+
const connectDist = this.option.particles.connectDist
|
|
285
|
+
const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step
|
|
286
|
+
const gravPullingMult = connectDist * this.option.gravity.pulling * step
|
|
287
|
+
const maxRepulsiveDist = connectDist / 2
|
|
288
|
+
const maxRepulsiveDistSq = maxRepulsiveDist * maxRepulsiveDist
|
|
289
|
+
const eps = (connectDist * connectDist) / 256
|
|
289
290
|
|
|
290
291
|
for (let i = 0; i < len; i++) {
|
|
291
292
|
const particleA = particles[i]
|
|
@@ -298,19 +299,19 @@ export default class CanvasParticles {
|
|
|
298
299
|
const distY = particleA.posY - particleB.posY
|
|
299
300
|
const distSq = distX * distX + distY * distY
|
|
300
301
|
|
|
302
|
+
if (distSq >= maxRepulsiveDistSq && !isPullingEnabled) continue
|
|
303
|
+
|
|
301
304
|
let angle
|
|
302
305
|
let grav
|
|
303
306
|
let gravMult
|
|
304
307
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
|
|
308
|
-
grav = (1 / Math.sqrt(distSq)) ** 1.8
|
|
308
|
+
angle = Math.atan2(-distY, -distX)
|
|
309
|
+
grav = 1 / (distSq + eps)
|
|
309
310
|
const angleX = Math.cos(angle)
|
|
310
311
|
const angleY = Math.sin(angle)
|
|
311
312
|
|
|
312
313
|
if (distSq < maxRepulsiveDistSq) {
|
|
313
|
-
gravMult =
|
|
314
|
+
gravMult = grav * gravRepulsiveMult
|
|
314
315
|
const gravX = angleX * gravMult
|
|
315
316
|
const gravY = angleY * gravMult
|
|
316
317
|
particleA.velX -= gravX
|
|
@@ -321,7 +322,7 @@ export default class CanvasParticles {
|
|
|
321
322
|
|
|
322
323
|
if (!isPullingEnabled) continue
|
|
323
324
|
|
|
324
|
-
gravMult =
|
|
325
|
+
gravMult = grav * gravPullingMult
|
|
325
326
|
const gravX = angleX * gravMult
|
|
326
327
|
const gravY = angleY * gravMult
|
|
327
328
|
particleA.velX += gravX
|
|
@@ -342,17 +343,18 @@ export default class CanvasParticles {
|
|
|
342
343
|
const offY = this.offY
|
|
343
344
|
const mouseX = this.mouseX
|
|
344
345
|
const mouseY = this.mouseY
|
|
345
|
-
const rotationSpeed = this.option.particles.rotationSpeed
|
|
346
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step
|
|
346
347
|
const friction = this.option.gravity.friction
|
|
347
348
|
const mouseConnectDist = this.option.mouse.connectDist
|
|
348
349
|
const mouseDistRatio = this.option.mouse.distRatio
|
|
349
350
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE
|
|
350
351
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE
|
|
352
|
+
const easing = 1 - Math.pow(1 - 1 / 4, step)
|
|
351
353
|
|
|
352
354
|
for (let i = 0; i < len; i++) {
|
|
353
355
|
const particle = particles[i]
|
|
354
356
|
|
|
355
|
-
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed
|
|
357
|
+
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step
|
|
356
358
|
particle.dir %= TWO_PI
|
|
357
359
|
|
|
358
360
|
// Constant velocity
|
|
@@ -360,8 +362,8 @@ export default class CanvasParticles {
|
|
|
360
362
|
const movY = Math.cos(particle.dir) * particle.speed
|
|
361
363
|
|
|
362
364
|
// Apply velocities
|
|
363
|
-
particle.posX += movX + particle.velX
|
|
364
|
-
particle.posY += movY + particle.velY
|
|
365
|
+
particle.posX += (movX + particle.velX) * step
|
|
366
|
+
particle.posY += (movY + particle.velY) * step
|
|
365
367
|
|
|
366
368
|
// Wrap particles around the canvas
|
|
367
369
|
particle.posX %= width
|
|
@@ -371,8 +373,8 @@ export default class CanvasParticles {
|
|
|
371
373
|
if (particle.posY < 0) particle.posY += height
|
|
372
374
|
|
|
373
375
|
// Slightly decrease dynamic velocity
|
|
374
|
-
particle.velX *= friction
|
|
375
|
-
particle.velY *= friction
|
|
376
|
+
particle.velX *= Math.pow(friction, step)
|
|
377
|
+
particle.velY *= Math.pow(friction, step)
|
|
376
378
|
|
|
377
379
|
// Distance from mouse
|
|
378
380
|
const distX = particle.posX + offX - mouseX
|
|
@@ -383,11 +385,11 @@ export default class CanvasParticles {
|
|
|
383
385
|
const distRatio = mouseConnectDist / Math.hypot(distX, distY)
|
|
384
386
|
|
|
385
387
|
if (mouseDistRatio < distRatio) {
|
|
386
|
-
particle.offX += (distRatio * distX - distX - particle.offX)
|
|
387
|
-
particle.offY += (distRatio * distY - distY - particle.offY)
|
|
388
|
+
particle.offX += (distRatio * distX - distX - particle.offX) * easing
|
|
389
|
+
particle.offY += (distRatio * distY - distY - particle.offY) * easing
|
|
388
390
|
} else {
|
|
389
|
-
particle.offX -= particle.offX
|
|
390
|
-
particle.offY -= particle.offY
|
|
391
|
+
particle.offX -= particle.offX * easing
|
|
392
|
+
particle.offY -= particle.offY * easing
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
395
|
|
|
@@ -472,9 +474,9 @@ export default class CanvasParticles {
|
|
|
472
474
|
const particles = this.particles
|
|
473
475
|
const ctx = this.ctx
|
|
474
476
|
const maxDist = this.option.particles.connectDist
|
|
475
|
-
const maxDistSq = maxDist
|
|
477
|
+
const maxDistSq = maxDist * maxDist
|
|
476
478
|
const halfMaxDist = maxDist / 2
|
|
477
|
-
const halfMaxDistSq = halfMaxDist
|
|
479
|
+
const halfMaxDistSq = halfMaxDist * halfMaxDist
|
|
478
480
|
const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height)
|
|
479
481
|
const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork
|
|
480
482
|
const alpha = this.color.alpha
|
|
@@ -492,7 +494,7 @@ export default class CanvasParticles {
|
|
|
492
494
|
const particleB = particles[j]
|
|
493
495
|
|
|
494
496
|
// Don't draw the line if it wouldn't be visible
|
|
495
|
-
if (!
|
|
497
|
+
if (!drawAll && !this.#isLineVisible(particleA, particleB)) continue
|
|
496
498
|
|
|
497
499
|
const distX = particleA.x - particleB.x
|
|
498
500
|
const distY = particleA.y - particleB.y
|
|
@@ -544,7 +546,7 @@ export default class CanvasParticles {
|
|
|
544
546
|
this.ctx.lineWidth = 1
|
|
545
547
|
|
|
546
548
|
this.#renderParticles()
|
|
547
|
-
this.#renderConnections()
|
|
549
|
+
if (this.options.particles.drawLines) this.#renderConnections()
|
|
548
550
|
}
|
|
549
551
|
|
|
550
552
|
/** @private Main animation loop that updates and renders the particles */
|
|
@@ -562,7 +564,7 @@ export default class CanvasParticles {
|
|
|
562
564
|
// - step = 1 → exactly one baseline update (dt === BASE_DT)
|
|
563
565
|
// - step > 1 → more time passed (lower FPS), advance further
|
|
564
566
|
// - step < 1 → less time passed (higher FPS), advance less
|
|
565
|
-
const step = CanvasParticles.BASE_DT
|
|
567
|
+
const step = dt / CanvasParticles.BASE_DT
|
|
566
568
|
|
|
567
569
|
this.#updateGravity(step)
|
|
568
570
|
this.#updateParticles(step)
|
|
@@ -620,20 +622,26 @@ export default class CanvasParticles {
|
|
|
620
622
|
stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
|
|
621
623
|
},
|
|
622
624
|
mouse: {
|
|
623
|
-
interactionType: pno(
|
|
625
|
+
interactionType: ~~pno(
|
|
626
|
+
'mouse.interactionType',
|
|
627
|
+
options.mouse?.interactionType,
|
|
628
|
+
CanvasParticles.interactionType.MOVE,
|
|
629
|
+
{ min: 0, max: 2 }
|
|
630
|
+
),
|
|
624
631
|
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
|
|
625
632
|
connectDist: 1 /* post processed */,
|
|
626
633
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
|
|
627
634
|
},
|
|
628
635
|
particles: {
|
|
629
636
|
regenerateOnResize: !!options.particles?.regenerateOnResize,
|
|
637
|
+
drawLines: !!(options.particles?.drawLines ?? true),
|
|
630
638
|
color: options.particles?.color ?? 'black',
|
|
631
|
-
ppm: pno('particles.ppm', options.particles?.ppm, 100),
|
|
632
|
-
max: pno('particles.max', options.particles?.max, Infinity),
|
|
633
|
-
maxWork: pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 }),
|
|
634
|
-
connectDist: pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
|
|
639
|
+
ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
|
|
640
|
+
max: Math.round(pno('particles.max', options.particles?.max, Infinity)),
|
|
641
|
+
maxWork: Math.round(pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 })),
|
|
642
|
+
connectDist: ~~pno('particles.connectDistance', options.particles?.connectDistance, 150, { min: 1 }),
|
|
635
643
|
relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
|
|
636
|
-
relSize: pno('particles.relSize', options.particles?.relSize, 1, { min:
|
|
644
|
+
relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 0 }),
|
|
637
645
|
rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
|
|
638
646
|
},
|
|
639
647
|
gravity: {
|