canvasparticles-js 4.1.6 → 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 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 instantiation](#class-instantiation)<br>
16
+ [Class Instantiation](#class-instantiation)<br>
16
17
  [Options](#options)<br>
17
- [One pager example](#one-pager-example)
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
- ![Banner with particles and title: Canvas Particles](./demo/banner.webp)
28
+ [![Banner with particles and title: Canvas Particles](./demo/banner.webp)](https://khoeckman.github.io/canvasparticles-js/)
25
29
 
26
- ## Implementation
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
- <details open>
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
- Inside _initParticles.js_:
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
- </details>
54
+ If you don't have a bundler:
64
55
 
65
- <details>
66
- <summary><h3 style="display: inline;">Import with jsDelivr (click to expand)</h3></summary>
56
+ ```js
57
+ import CanvasParticles from './node_modules/canvasparticles-js/dist/index.mjs'
58
+ ```
67
59
 
68
- Add a `<script>` element in the `<head>` to import `CanvasParticles`.
60
+ **Global import**
69
61
 
70
62
  ```html
71
- <head>
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
- </details>
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
- Be aware that using ES modules is only possible when running the application on a (local) server.<br>
82
- [Same Origin Policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)
68
+ ### Import with jsDelivr
83
69
 
84
- Add a `<script>` element in the `<head>` to import _initParticles.js_.
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 './dist/index.mjs'
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
- </details>
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
- <body>
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
- </details>
82
+ ---
132
83
 
133
- <br>
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
- ## Class instantiation
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
- Configuration options for the particles and their behavior.<br>
221
- Play around with these values: [Sandbox](https://khoeckman.github.io/canvasparticles-js/#sandbox)
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
- <details>
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
- ```js
230
- const options = {
231
- /** @param {string} [options.background=false] - Background of the canvas. Can be any CSS supported value for the background property.
232
- * @note No background will be set if background is not a string.
233
- */
234
- background: 'linear-gradient(115deg, #354089, black)',
235
-
236
- /** @param {Object} [options.animation] - Animation settings. */
237
- animation: {
238
- /** @param {boolean} [options.animation.startOnEnter=true] - Whether to start the animation when the canvas enters the viewport. */
239
- startOnEnter: true,
240
-
241
- /** @param {boolean} [options.animation.stopOnLeave=true] - Whether to stop the animation when the canvas leaves the viewport. */
242
- stopOnLeave: true,
243
- },
244
-
245
- /** @param {Object} [options.mouse] - Mouse interaction settings. */
246
- mouse: {
247
- /** @param {0|1|2} [options.mouse.interactionType=1] - The type of interaction the mouse will have with particles.
248
- *
249
- * CanvasParticles.interactionType.NONE = 0 = No interaction.
250
- * CanvasParticles.interactionType.SHIFT = 1 = The mouse can visually shift the particles.
251
- * CanvasParticles.interactionType.MOVE = 2 = The mouse can move the particles.
252
- * @note mouse.distRatio should be less than 1 to allow dragging, closer to 0 is easier to drag
253
- */
254
- interactionType: 2,
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
- </details>
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
- **Note:** The new option values are not validated, except for the options with a `set...()` function. Assigning invalid values will lead to unexpected behavior.
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 setters to ensure proper integration.
248
+ These options are the only ones that have and require a dedicated setter to ensure proper integration:
347
249
 
348
- - options.background
349
- - options.mouse.connectDistMult
350
- - options.particles.color
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
- - option.particles.ppm
366
- - option.particles.max
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
- // Apply the changes using one of these methods:
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 immediate effect.
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
- const options = { ... }
395
- instance.options = options
301
+ instance.options = { ... }
396
302
  ```
397
303
 
398
- ## One pager example
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, // MOVE === 2
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,12 @@ function Mulberry32(seed) {
16
16
  },
17
17
  };
18
18
  }
19
- // Mulberry32 is ±388% faster than Math.random()
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.6";
24
+ static version = "4.2.1";
25
25
  static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
26
26
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
27
27
  /** Defines mouse interaction types with the particles */
@@ -237,11 +237,12 @@ class CanvasParticles {
237
237
  return;
238
238
  const len = this.particleCount;
239
239
  const particles = this.particles;
240
- const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step;
241
- const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step;
242
- const maxRepulsiveDist = this.option.particles.connectDist / 2;
243
- const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
244
- const maxGrav = this.option.particles.connectDist * 0.1 * step;
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
- if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
257
- continue;
258
- angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
259
- grav = Math.pow(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 = Math.min(maxGrav, grav * gravRepulsiveMult);
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 = Math.min(maxGrav, grav * gravPullingMult);
274
+ gravMult = grav * gravPullingMult;
274
275
  const gravX = angleX * gravMult;
275
276
  const gravY = angleY * gravMult;
276
277
  particleA.velX += gravX;
@@ -404,9 +405,9 @@ class CanvasParticles {
404
405
  const particles = this.particles;
405
406
  const ctx = this.ctx;
406
407
  const maxDist = this.option.particles.connectDist;
407
- const maxDistSq = maxDist ** 2;
408
+ const maxDistSq = maxDist * maxDist;
408
409
  const halfMaxDist = maxDist / 2;
409
- const halfMaxDistSq = halfMaxDist ** 2;
410
+ const halfMaxDistSq = halfMaxDist * halfMaxDist;
410
411
  const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height);
411
412
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
412
413
  const alpha = this.color.alpha;
@@ -420,7 +421,7 @@ class CanvasParticles {
420
421
  // Code in this scope runs O(n^2) times per frame!
421
422
  const particleB = particles[j];
422
423
  // Don't draw the line if it wouldn't be visible
423
- if (!(drawAll || this.#isLineVisible(particleA, particleB)))
424
+ if (!drawAll && !this.#isLineVisible(particleA, particleB))
424
425
  continue;
425
426
  const distX = particleA.x - particleB.x;
426
427
  const distY = particleA.y - particleB.y;
@@ -465,7 +466,8 @@ class CanvasParticles {
465
466
  this.ctx.strokeStyle = this.color.hex;
466
467
  this.ctx.lineWidth = 1;
467
468
  this.#renderParticles();
468
- this.#renderConnections();
469
+ if (this.options.particles.drawLines)
470
+ this.#renderConnections();
469
471
  }
470
472
  /** @private Main animation loop that updates and renders the particles */
471
473
  #animation() {
@@ -528,20 +530,21 @@ class CanvasParticles {
528
530
  stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
529
531
  },
530
532
  mouse: {
531
- interactionType: pno('mouse.interactionType', options.mouse?.interactionType, 1),
533
+ interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
532
534
  connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
533
535
  connectDist: 1 /* post processed */,
534
536
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
535
537
  },
536
538
  particles: {
537
539
  regenerateOnResize: !!options.particles?.regenerateOnResize,
540
+ drawLines: !!(options.particles?.drawLines ?? true),
538
541
  color: options.particles?.color ?? 'black',
539
- ppm: pno('particles.ppm', options.particles?.ppm, 100),
540
- max: pno('particles.max', options.particles?.max, Infinity),
541
- maxWork: pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 }),
542
- 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 }),
543
546
  relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
544
- relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 1 }),
547
+ relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 0 }),
545
548
  rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
546
549
  },
547
550
  gravity: {
package/dist/index.mjs CHANGED
@@ -14,12 +14,12 @@ function Mulberry32(seed) {
14
14
  },
15
15
  };
16
16
  }
17
- // Mulberry32 is ±388% faster than Math.random()
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.6";
22
+ static version = "4.2.1";
23
23
  static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
24
24
  static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
25
25
  /** Defines mouse interaction types with the particles */
@@ -235,11 +235,12 @@ class CanvasParticles {
235
235
  return;
236
236
  const len = this.particleCount;
237
237
  const particles = this.particles;
238
- const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step;
239
- const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step;
240
- const maxRepulsiveDist = this.option.particles.connectDist / 2;
241
- const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
242
- const maxGrav = this.option.particles.connectDist * 0.1 * step;
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
- if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
255
- continue;
256
- angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
257
- grav = Math.pow(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 = Math.min(maxGrav, grav * gravRepulsiveMult);
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 = Math.min(maxGrav, grav * gravPullingMult);
272
+ gravMult = grav * gravPullingMult;
272
273
  const gravX = angleX * gravMult;
273
274
  const gravY = angleY * gravMult;
274
275
  particleA.velX += gravX;
@@ -402,9 +403,9 @@ class CanvasParticles {
402
403
  const particles = this.particles;
403
404
  const ctx = this.ctx;
404
405
  const maxDist = this.option.particles.connectDist;
405
- const maxDistSq = maxDist ** 2;
406
+ const maxDistSq = maxDist * maxDist;
406
407
  const halfMaxDist = maxDist / 2;
407
- const halfMaxDistSq = halfMaxDist ** 2;
408
+ const halfMaxDistSq = halfMaxDist * halfMaxDist;
408
409
  const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height);
409
410
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
410
411
  const alpha = this.color.alpha;
@@ -418,7 +419,7 @@ class CanvasParticles {
418
419
  // Code in this scope runs O(n^2) times per frame!
419
420
  const particleB = particles[j];
420
421
  // Don't draw the line if it wouldn't be visible
421
- if (!(drawAll || this.#isLineVisible(particleA, particleB)))
422
+ if (!drawAll && !this.#isLineVisible(particleA, particleB))
422
423
  continue;
423
424
  const distX = particleA.x - particleB.x;
424
425
  const distY = particleA.y - particleB.y;
@@ -463,7 +464,8 @@ class CanvasParticles {
463
464
  this.ctx.strokeStyle = this.color.hex;
464
465
  this.ctx.lineWidth = 1;
465
466
  this.#renderParticles();
466
- this.#renderConnections();
467
+ if (this.options.particles.drawLines)
468
+ this.#renderConnections();
467
469
  }
468
470
  /** @private Main animation loop that updates and renders the particles */
469
471
  #animation() {
@@ -526,20 +528,21 @@ class CanvasParticles {
526
528
  stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
527
529
  },
528
530
  mouse: {
529
- interactionType: pno('mouse.interactionType', options.mouse?.interactionType, 1),
531
+ interactionType: ~~pno('mouse.interactionType', options.mouse?.interactionType, CanvasParticles.interactionType.MOVE, { min: 0, max: 2 }),
530
532
  connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
531
533
  connectDist: 1 /* post processed */,
532
534
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
533
535
  },
534
536
  particles: {
535
537
  regenerateOnResize: !!options.particles?.regenerateOnResize,
538
+ drawLines: !!(options.particles?.drawLines ?? true),
536
539
  color: options.particles?.color ?? 'black',
537
- ppm: pno('particles.ppm', options.particles?.ppm, 100),
538
- max: pno('particles.max', options.particles?.max, Infinity),
539
- maxWork: pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 }),
540
- 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 }),
541
544
  relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
542
- relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 1 }),
545
+ relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 0 }),
543
546
  rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
544
547
  },
545
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.6";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*this.option.gravity.repulsive*t,a=this.option.particles.connectDist*this.option.gravity.pulling*t,r=(this.option.particles.connectDist/2)**2,c=.1*this.option.particles.connectDist*t;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=Math.pow(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*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,M=s.posY+c-l;if(!m){const t=d/Math.hypot(b,M);f<t?(s.offX+=(t*b-b-s.offX)*g,s.offY+=(t*M-M-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**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)/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,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});
@@ -12,6 +12,7 @@ export interface CanvasParticlesOptions {
12
12
  };
13
13
  particles: {
14
14
  regenerateOnResize: boolean;
15
+ drawLines: boolean;
15
16
  color: string | CanvasGradient | CanvasPattern;
16
17
  ppm: number;
17
18
  max: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasparticles-js",
3
- "version": "4.1.6",
3
+ "version": "4.2.1",
4
4
  "description": "In an HTML canvas, a bunch of interactive particles connected with lines when they approach each other.",
5
5
  "author": "Khoeckman",
6
6
  "license": "MIT",
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, ParticleGridPos, ContextColor, LineSegment } from './types'
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 ±388% faster than Math.random()
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
@@ -281,11 +281,12 @@ export default class CanvasParticles {
281
281
 
282
282
  const len = this.particleCount
283
283
  const particles = this.particles
284
- const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step
285
- const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step
286
- const maxRepulsiveDist = this.option.particles.connectDist / 2
287
- const maxRepulsiveDistSq = maxRepulsiveDist ** 2
288
- const maxGrav = this.option.particles.connectDist * 0.1 * step
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
- if (distSq >= maxRepulsiveDistSq && !isPullingEnabled) continue
306
-
307
- angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
308
- grav = Math.pow(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 = Math.min(maxGrav, grav * gravRepulsiveMult)
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 = Math.min(maxGrav, grav * gravPullingMult)
325
+ gravMult = grav * gravPullingMult
325
326
  const gravX = angleX * gravMult
326
327
  const gravY = angleY * gravMult
327
328
  particleA.velX += gravX
@@ -473,9 +474,9 @@ export default class CanvasParticles {
473
474
  const particles = this.particles
474
475
  const ctx = this.ctx
475
476
  const maxDist = this.option.particles.connectDist
476
- const maxDistSq = maxDist ** 2
477
+ const maxDistSq = maxDist * maxDist
477
478
  const halfMaxDist = maxDist / 2
478
- const halfMaxDistSq = halfMaxDist ** 2
479
+ const halfMaxDistSq = halfMaxDist * halfMaxDist
479
480
  const drawAll = maxDist >= Math.min(this.canvas.width, this.canvas.height)
480
481
  const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork
481
482
  const alpha = this.color.alpha
@@ -493,7 +494,7 @@ export default class CanvasParticles {
493
494
  const particleB = particles[j]
494
495
 
495
496
  // Don't draw the line if it wouldn't be visible
496
- if (!(drawAll || this.#isLineVisible(particleA, particleB))) continue
497
+ if (!drawAll && !this.#isLineVisible(particleA, particleB)) continue
497
498
 
498
499
  const distX = particleA.x - particleB.x
499
500
  const distY = particleA.y - particleB.y
@@ -545,7 +546,7 @@ export default class CanvasParticles {
545
546
  this.ctx.lineWidth = 1
546
547
 
547
548
  this.#renderParticles()
548
- this.#renderConnections()
549
+ if (this.options.particles.drawLines) this.#renderConnections()
549
550
  }
550
551
 
551
552
  /** @private Main animation loop that updates and renders the particles */
@@ -621,20 +622,26 @@ export default class CanvasParticles {
621
622
  stopOnLeave: !!(options.animation?.stopOnLeave ?? true),
622
623
  },
623
624
  mouse: {
624
- interactionType: pno('mouse.interactionType', options.mouse?.interactionType, 1),
625
+ interactionType: ~~pno(
626
+ 'mouse.interactionType',
627
+ options.mouse?.interactionType,
628
+ CanvasParticles.interactionType.MOVE,
629
+ { min: 0, max: 2 }
630
+ ),
625
631
  connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3),
626
632
  connectDist: 1 /* post processed */,
627
633
  distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3),
628
634
  },
629
635
  particles: {
630
636
  regenerateOnResize: !!options.particles?.regenerateOnResize,
637
+ drawLines: !!(options.particles?.drawLines ?? true),
631
638
  color: options.particles?.color ?? 'black',
632
- ppm: pno('particles.ppm', options.particles?.ppm, 100),
633
- max: pno('particles.max', options.particles?.max, Infinity),
634
- maxWork: pno('particles.maxWork', options.particles?.maxWork, Infinity, { min: 0 }),
635
- 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 }),
636
643
  relSpeed: pno('particles.relSpeed', options.particles?.relSpeed, 1, { min: 0 }),
637
- relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 1 }),
644
+ relSize: pno('particles.relSize', options.particles?.relSize, 1, { min: 0 }),
638
645
  rotationSpeed: pno('particles.rotationSpeed', options.particles?.rotationSpeed, 2, { min: 0 }) / 100,
639
646
  },
640
647
  gravity: {
@@ -15,6 +15,7 @@ export interface CanvasParticlesOptions {
15
15
 
16
16
  particles: {
17
17
  regenerateOnResize: boolean
18
+ drawLines: boolean
18
19
  color: string | CanvasGradient | CanvasPattern
19
20
  ppm: number
20
21
  max: number