canvasparticles-js 4.2.4 → 4.3.0
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 +61 -13
- package/dist/index.cjs +45 -24
- package/dist/index.d.ts +7 -1
- package/dist/index.mjs +45 -24
- package/dist/index.umd.js +1 -1
- package/dist/types/options.d.ts +2 -2
- package/package.json +5 -5
- package/src/index.ts +53 -23
- package/src/types/options.ts +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Creating a fun and interactive background. Colors, interaction and gravity can b
|
|
|
15
15
|
[Implementation](#implementation)<br>
|
|
16
16
|
[Class Instantiation](#class-instantiation)<br>
|
|
17
17
|
[Options](#options)<br>
|
|
18
|
+
[Manually creating particles](#manually-creating-particles)<br>
|
|
18
19
|
[One Pager Example](#one-pager-example)
|
|
19
20
|
|
|
20
21
|
---
|
|
@@ -212,18 +213,18 @@ Your screen resolution and refresh rate will directly impact perfomance!
|
|
|
212
213
|
|
|
213
214
|
### `particles`
|
|
214
215
|
|
|
215
|
-
| Option
|
|
216
|
-
|
|
|
217
|
-
| `particles.
|
|
218
|
-
| `particles.
|
|
219
|
-
| `particles.
|
|
220
|
-
| `particles.
|
|
221
|
-
| `particles.
|
|
222
|
-
| `particles.
|
|
223
|
-
| `particles.
|
|
224
|
-
| `particles.
|
|
225
|
-
| `particles.
|
|
226
|
-
| `particles.drawLines`
|
|
216
|
+
| Option | Type | Default | Description |
|
|
217
|
+
| --------------------------- | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- |
|
|
218
|
+
| `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `0 = OFF`, `1 = NEW`, `2 = MATCH` |
|
|
219
|
+
| `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
|
|
220
|
+
| `particles.ppm` | `integer` | `100` | Particles per million pixels. _Heavily impacts performance_ |
|
|
221
|
+
| `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
|
|
222
|
+
| `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
|
|
223
|
+
| `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). _Heavily impacts performance_ |
|
|
224
|
+
| `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
|
|
225
|
+
| `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
|
|
226
|
+
| `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
|
|
227
|
+
| `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
|
|
227
228
|
|
|
228
229
|
---
|
|
229
230
|
|
|
@@ -273,13 +274,32 @@ instance.option.particles.ppm = 100
|
|
|
273
274
|
instance.option.particles.max = 300
|
|
274
275
|
```
|
|
275
276
|
|
|
276
|
-
The changes are only applied when one of the following methods is called
|
|
277
|
+
The changes are only applied when one of the following methods is called:
|
|
277
278
|
|
|
278
279
|
```js
|
|
279
280
|
instance.newParticles() // Remove all particles and create the correct amount of new ones
|
|
280
281
|
instance.matchParticleCount() // Add or remove some particles to match the count
|
|
281
282
|
```
|
|
282
283
|
|
|
284
|
+
### Changing particle properties
|
|
285
|
+
|
|
286
|
+
After updating the following options, the particles are **not automatically updated**:
|
|
287
|
+
|
|
288
|
+
- `particles.relSize`
|
|
289
|
+
- `particles.relSpeed`
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
// Note: the backing field is called `option` not `options`!
|
|
293
|
+
instance.option.particles.relSize = 2
|
|
294
|
+
instance.option.particles.relSpeed = 3
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The changes are only applied when the following method is called:
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
instance.updateParticles() // Updates the particle.speed and particle.size properties without regenerating any particles
|
|
301
|
+
```
|
|
302
|
+
|
|
283
303
|
#### Modifying object properties
|
|
284
304
|
|
|
285
305
|
**All** other options can be updated by only modifying the `option` internal field properties, with changes taking effect immediately.
|
|
@@ -297,12 +317,40 @@ instance.option.gravity.repulsive = 1
|
|
|
297
317
|
|
|
298
318
|
To reinitialize all options, pass a new options object to the `options` setter.
|
|
299
319
|
|
|
320
|
+
> Existing particles their properties will not be updated automatically. [Changing particle properties](#changing-particle-properties)
|
|
321
|
+
|
|
300
322
|
```js
|
|
301
323
|
instance.options = { ... }
|
|
302
324
|
```
|
|
303
325
|
|
|
304
326
|
---
|
|
305
327
|
|
|
328
|
+
## Manually creating particles
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
By default `particles.ppm` and `particles.max` are used to auto-generate random particles. This might destroy manually created particles. To fix this, set `particles.generationType` to `MANUAL (0)`.
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
const canvas = '#my-canvas'
|
|
338
|
+
const options = {
|
|
339
|
+
particles: {
|
|
340
|
+
generationType: CanvasParticles.generationType.MANUAL, // = 0
|
|
341
|
+
rotationSpeed: 0,
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
const instance = new CanvasParticles(canvas, options).start()
|
|
345
|
+
|
|
346
|
+
// Create a horizontal line of particles moving down
|
|
347
|
+
for (let x = 100; x < 300; x += 4) {
|
|
348
|
+
instance.createParticle(x, 100, 0, 1, 5)
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
306
354
|
## One Pager Example
|
|
307
355
|
|
|
308
356
|
```html
|
package/dist/index.cjs
CHANGED
|
@@ -21,14 +21,20 @@ function Mulberry32(seed) {
|
|
|
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.
|
|
24
|
+
static version = "4.3.0";
|
|
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 */
|
|
28
28
|
static interactionType = Object.freeze({
|
|
29
29
|
NONE: 0, // No mouse interaction
|
|
30
30
|
SHIFT: 1, // Visual displacement only
|
|
31
|
-
MOVE: 2, // Actual particle movement
|
|
31
|
+
MOVE: 2, // Actual particle movement (default)
|
|
32
|
+
});
|
|
33
|
+
/** Defines how the particles are auto-generated */
|
|
34
|
+
static generationType = Object.freeze({
|
|
35
|
+
MANUAL: 0, // Never auto-generate particles
|
|
36
|
+
NEW: 1, // Generate particles from scratch
|
|
37
|
+
MATCH: 2, // Add or remove particles to match new count (default)
|
|
32
38
|
});
|
|
33
39
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
34
40
|
static canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
@@ -81,7 +87,6 @@ class CanvasParticles {
|
|
|
81
87
|
isAnimating = false;
|
|
82
88
|
lastAnimationFrame = 0;
|
|
83
89
|
particles = [];
|
|
84
|
-
particleCount = 0;
|
|
85
90
|
clientX = Infinity;
|
|
86
91
|
clientY = Infinity;
|
|
87
92
|
mouseX = Infinity;
|
|
@@ -168,35 +173,39 @@ class CanvasParticles {
|
|
|
168
173
|
this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
|
|
169
174
|
this.offX = (width - this.width) / 2;
|
|
170
175
|
this.offY = (height - this.height) / 2;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
const generationType = this.option.particles.generationType;
|
|
177
|
+
if (generationType !== CanvasParticles.generationType.MANUAL) {
|
|
178
|
+
if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
|
|
179
|
+
this.newParticles();
|
|
180
|
+
else if (generationType === CanvasParticles.generationType.MATCH)
|
|
181
|
+
this.matchParticleCount({ updateBounds: true });
|
|
182
|
+
}
|
|
175
183
|
if (this.isAnimating)
|
|
176
184
|
this.#render();
|
|
177
185
|
}
|
|
178
186
|
/** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
|
|
179
|
-
#
|
|
187
|
+
#targetParticleCount() {
|
|
180
188
|
// Amount of particles to be created
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!isFinite(
|
|
189
|
+
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
|
|
190
|
+
particleCount = Math.min(this.option.particles.max, particleCount);
|
|
191
|
+
if (!isFinite(particleCount))
|
|
184
192
|
throw new RangeError('particleCount must be finite');
|
|
193
|
+
return particleCount | 0;
|
|
185
194
|
}
|
|
186
195
|
/** @public Remove existing particles and generate new ones */
|
|
187
196
|
newParticles() {
|
|
188
|
-
this.#
|
|
197
|
+
const particleCount = this.#targetParticleCount();
|
|
189
198
|
this.particles = [];
|
|
190
|
-
for (let i = 0; i <
|
|
199
|
+
for (let i = 0; i < particleCount; i++)
|
|
191
200
|
this.createParticle();
|
|
192
201
|
}
|
|
193
202
|
/** @public Adjust particle array length to match `options.particles.ppm` */
|
|
194
203
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
195
|
-
this.#
|
|
196
|
-
this.particles = this.particles.slice(0,
|
|
204
|
+
const particleCount = this.#targetParticleCount();
|
|
205
|
+
this.particles = this.particles.slice(0, particleCount);
|
|
197
206
|
if (updateBounds)
|
|
198
207
|
this.particles.forEach((particle) => this.#updateParticleBounds(particle));
|
|
199
|
-
while (
|
|
208
|
+
while (particleCount > this.particles.length)
|
|
200
209
|
this.createParticle();
|
|
201
210
|
}
|
|
202
211
|
/** @public Create a new particle with optional parameters */
|
|
@@ -212,9 +221,9 @@ class CanvasParticles {
|
|
|
212
221
|
velY: 0, // Vertical speed in pixels per update
|
|
213
222
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
214
223
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
215
|
-
dir: dir
|
|
216
|
-
speed: speed
|
|
217
|
-
size: size
|
|
224
|
+
dir: dir ?? prng() * TWO_PI, // Direction in radians
|
|
225
|
+
speed: speed ?? (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
226
|
+
size: size ?? (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
218
227
|
gridPos: { x: 1, y: 1 },
|
|
219
228
|
isVisible: false,
|
|
220
229
|
};
|
|
@@ -231,14 +240,26 @@ class CanvasParticles {
|
|
|
231
240
|
left: -particle.size,
|
|
232
241
|
};
|
|
233
242
|
}
|
|
243
|
+
/* @public Randomize speed and size of all particles based on current options */
|
|
244
|
+
updateParticles() {
|
|
245
|
+
const particles = this.particles;
|
|
246
|
+
const len = particles.length;
|
|
247
|
+
const relSpeed = this.option.particles.relSpeed;
|
|
248
|
+
const relSize = this.option.particles.relSize;
|
|
249
|
+
for (let i = 0; i < len; i++) {
|
|
250
|
+
const particle = particles[i];
|
|
251
|
+
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
252
|
+
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
234
255
|
/** @private Apply gravity forces between particles */
|
|
235
256
|
#updateGravity(step) {
|
|
236
257
|
const isRepulsiveEnabled = this.option.gravity.repulsive > 0;
|
|
237
258
|
const isPullingEnabled = this.option.gravity.pulling > 0;
|
|
238
259
|
if (!isRepulsiveEnabled && !isPullingEnabled)
|
|
239
260
|
return;
|
|
240
|
-
const len = this.particleCount;
|
|
241
261
|
const particles = this.particles;
|
|
262
|
+
const len = particles.length;
|
|
242
263
|
const connectDist = this.option.particles.connectDist;
|
|
243
264
|
const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
|
|
244
265
|
const gravPullingMult = connectDist * this.option.gravity.pulling * step;
|
|
@@ -285,8 +306,8 @@ class CanvasParticles {
|
|
|
285
306
|
}
|
|
286
307
|
/** @private Update positions, directions, and visibility of all particles */
|
|
287
308
|
#updateParticles(step) {
|
|
288
|
-
const len = this.particleCount;
|
|
289
309
|
const particles = this.particles;
|
|
310
|
+
const len = particles.length;
|
|
290
311
|
const width = this.width;
|
|
291
312
|
const height = this.height;
|
|
292
313
|
const offX = this.offX;
|
|
@@ -380,8 +401,8 @@ class CanvasParticles {
|
|
|
380
401
|
}
|
|
381
402
|
/** @private Draw the particles on the canvas */
|
|
382
403
|
#renderParticles() {
|
|
383
|
-
const len = this.particleCount;
|
|
384
404
|
const particles = this.particles;
|
|
405
|
+
const len = particles.length;
|
|
385
406
|
const ctx = this.ctx;
|
|
386
407
|
for (let i = 0; i < len; i++) {
|
|
387
408
|
const particle = particles[i];
|
|
@@ -403,8 +424,8 @@ class CanvasParticles {
|
|
|
403
424
|
}
|
|
404
425
|
/** @private Draw lines between particles if they are close enough */
|
|
405
426
|
#renderConnections() {
|
|
406
|
-
const len = this.particleCount;
|
|
407
427
|
const particles = this.particles;
|
|
428
|
+
const len = particles.length;
|
|
408
429
|
const ctx = this.ctx;
|
|
409
430
|
const maxDist = this.option.particles.connectDist;
|
|
410
431
|
const maxDistSq = maxDist ** 2;
|
|
@@ -538,7 +559,7 @@ class CanvasParticles {
|
|
|
538
559
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
539
560
|
},
|
|
540
561
|
particles: {
|
|
541
|
-
|
|
562
|
+
generationType: ~~pno('particles.generationType', options.particles?.generationType, CanvasParticles.generationType.MATCH, { min: 0, max: 2 }),
|
|
542
563
|
drawLines: !!(options.particles?.drawLines ?? true),
|
|
543
564
|
color: options.particles?.color ?? 'black',
|
|
544
565
|
ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export default class CanvasParticles {
|
|
|
11
11
|
SHIFT: 1;
|
|
12
12
|
MOVE: 2;
|
|
13
13
|
}>;
|
|
14
|
+
/** Defines how the particles are auto-generated */
|
|
15
|
+
static readonly generationType: Readonly<{
|
|
16
|
+
MANUAL: 0;
|
|
17
|
+
NEW: 1;
|
|
18
|
+
MATCH: 2;
|
|
19
|
+
}>;
|
|
14
20
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
15
21
|
static readonly canvasIntersectionObserver: IntersectionObserver;
|
|
16
22
|
static readonly canvasResizeObserver: ResizeObserver;
|
|
@@ -23,7 +29,6 @@ export default class CanvasParticles {
|
|
|
23
29
|
isAnimating: boolean;
|
|
24
30
|
private lastAnimationFrame;
|
|
25
31
|
particles: Particle[];
|
|
26
|
-
particleCount: number;
|
|
27
32
|
private clientX;
|
|
28
33
|
private clientY;
|
|
29
34
|
mouseX: number;
|
|
@@ -55,6 +60,7 @@ export default class CanvasParticles {
|
|
|
55
60
|
}): void;
|
|
56
61
|
/** @public Create a new particle with optional parameters */
|
|
57
62
|
createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number): void;
|
|
63
|
+
updateParticles(): void;
|
|
58
64
|
/** @public Start the particle animation if it was not running before */
|
|
59
65
|
start({ auto }?: {
|
|
60
66
|
auto?: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -19,14 +19,20 @@ function Mulberry32(seed) {
|
|
|
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.
|
|
22
|
+
static version = "4.3.0";
|
|
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 */
|
|
26
26
|
static interactionType = Object.freeze({
|
|
27
27
|
NONE: 0, // No mouse interaction
|
|
28
28
|
SHIFT: 1, // Visual displacement only
|
|
29
|
-
MOVE: 2, // Actual particle movement
|
|
29
|
+
MOVE: 2, // Actual particle movement (default)
|
|
30
|
+
});
|
|
31
|
+
/** Defines how the particles are auto-generated */
|
|
32
|
+
static generationType = Object.freeze({
|
|
33
|
+
MANUAL: 0, // Never auto-generate particles
|
|
34
|
+
NEW: 1, // Generate particles from scratch
|
|
35
|
+
MATCH: 2, // Add or remove particles to match new count (default)
|
|
30
36
|
});
|
|
31
37
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
32
38
|
static canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
@@ -79,7 +85,6 @@ class CanvasParticles {
|
|
|
79
85
|
isAnimating = false;
|
|
80
86
|
lastAnimationFrame = 0;
|
|
81
87
|
particles = [];
|
|
82
|
-
particleCount = 0;
|
|
83
88
|
clientX = Infinity;
|
|
84
89
|
clientY = Infinity;
|
|
85
90
|
mouseX = Infinity;
|
|
@@ -166,35 +171,39 @@ class CanvasParticles {
|
|
|
166
171
|
this.height = Math.max(height + this.option.particles.connectDist * 2, 1);
|
|
167
172
|
this.offX = (width - this.width) / 2;
|
|
168
173
|
this.offY = (height - this.height) / 2;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
const generationType = this.option.particles.generationType;
|
|
175
|
+
if (generationType !== CanvasParticles.generationType.MANUAL) {
|
|
176
|
+
if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
|
|
177
|
+
this.newParticles();
|
|
178
|
+
else if (generationType === CanvasParticles.generationType.MATCH)
|
|
179
|
+
this.matchParticleCount({ updateBounds: true });
|
|
180
|
+
}
|
|
173
181
|
if (this.isAnimating)
|
|
174
182
|
this.#render();
|
|
175
183
|
}
|
|
176
184
|
/** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
|
|
177
|
-
#
|
|
185
|
+
#targetParticleCount() {
|
|
178
186
|
// Amount of particles to be created
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (!isFinite(
|
|
187
|
+
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000);
|
|
188
|
+
particleCount = Math.min(this.option.particles.max, particleCount);
|
|
189
|
+
if (!isFinite(particleCount))
|
|
182
190
|
throw new RangeError('particleCount must be finite');
|
|
191
|
+
return particleCount | 0;
|
|
183
192
|
}
|
|
184
193
|
/** @public Remove existing particles and generate new ones */
|
|
185
194
|
newParticles() {
|
|
186
|
-
this.#
|
|
195
|
+
const particleCount = this.#targetParticleCount();
|
|
187
196
|
this.particles = [];
|
|
188
|
-
for (let i = 0; i <
|
|
197
|
+
for (let i = 0; i < particleCount; i++)
|
|
189
198
|
this.createParticle();
|
|
190
199
|
}
|
|
191
200
|
/** @public Adjust particle array length to match `options.particles.ppm` */
|
|
192
201
|
matchParticleCount({ updateBounds = false } = {}) {
|
|
193
|
-
this.#
|
|
194
|
-
this.particles = this.particles.slice(0,
|
|
202
|
+
const particleCount = this.#targetParticleCount();
|
|
203
|
+
this.particles = this.particles.slice(0, particleCount);
|
|
195
204
|
if (updateBounds)
|
|
196
205
|
this.particles.forEach((particle) => this.#updateParticleBounds(particle));
|
|
197
|
-
while (
|
|
206
|
+
while (particleCount > this.particles.length)
|
|
198
207
|
this.createParticle();
|
|
199
208
|
}
|
|
200
209
|
/** @public Create a new particle with optional parameters */
|
|
@@ -210,9 +219,9 @@ class CanvasParticles {
|
|
|
210
219
|
velY: 0, // Vertical speed in pixels per update
|
|
211
220
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
212
221
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
213
|
-
dir: dir
|
|
214
|
-
speed: speed
|
|
215
|
-
size: size
|
|
222
|
+
dir: dir ?? prng() * TWO_PI, // Direction in radians
|
|
223
|
+
speed: speed ?? (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
224
|
+
size: size ?? (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
216
225
|
gridPos: { x: 1, y: 1 },
|
|
217
226
|
isVisible: false,
|
|
218
227
|
};
|
|
@@ -229,14 +238,26 @@ class CanvasParticles {
|
|
|
229
238
|
left: -particle.size,
|
|
230
239
|
};
|
|
231
240
|
}
|
|
241
|
+
/* @public Randomize speed and size of all particles based on current options */
|
|
242
|
+
updateParticles() {
|
|
243
|
+
const particles = this.particles;
|
|
244
|
+
const len = particles.length;
|
|
245
|
+
const relSpeed = this.option.particles.relSpeed;
|
|
246
|
+
const relSize = this.option.particles.relSize;
|
|
247
|
+
for (let i = 0; i < len; i++) {
|
|
248
|
+
const particle = particles[i];
|
|
249
|
+
particle.speed = (0.5 + prng() * 0.5) * relSpeed;
|
|
250
|
+
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
232
253
|
/** @private Apply gravity forces between particles */
|
|
233
254
|
#updateGravity(step) {
|
|
234
255
|
const isRepulsiveEnabled = this.option.gravity.repulsive > 0;
|
|
235
256
|
const isPullingEnabled = this.option.gravity.pulling > 0;
|
|
236
257
|
if (!isRepulsiveEnabled && !isPullingEnabled)
|
|
237
258
|
return;
|
|
238
|
-
const len = this.particleCount;
|
|
239
259
|
const particles = this.particles;
|
|
260
|
+
const len = particles.length;
|
|
240
261
|
const connectDist = this.option.particles.connectDist;
|
|
241
262
|
const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step;
|
|
242
263
|
const gravPullingMult = connectDist * this.option.gravity.pulling * step;
|
|
@@ -283,8 +304,8 @@ class CanvasParticles {
|
|
|
283
304
|
}
|
|
284
305
|
/** @private Update positions, directions, and visibility of all particles */
|
|
285
306
|
#updateParticles(step) {
|
|
286
|
-
const len = this.particleCount;
|
|
287
307
|
const particles = this.particles;
|
|
308
|
+
const len = particles.length;
|
|
288
309
|
const width = this.width;
|
|
289
310
|
const height = this.height;
|
|
290
311
|
const offX = this.offX;
|
|
@@ -378,8 +399,8 @@ class CanvasParticles {
|
|
|
378
399
|
}
|
|
379
400
|
/** @private Draw the particles on the canvas */
|
|
380
401
|
#renderParticles() {
|
|
381
|
-
const len = this.particleCount;
|
|
382
402
|
const particles = this.particles;
|
|
403
|
+
const len = particles.length;
|
|
383
404
|
const ctx = this.ctx;
|
|
384
405
|
for (let i = 0; i < len; i++) {
|
|
385
406
|
const particle = particles[i];
|
|
@@ -401,8 +422,8 @@ class CanvasParticles {
|
|
|
401
422
|
}
|
|
402
423
|
/** @private Draw lines between particles if they are close enough */
|
|
403
424
|
#renderConnections() {
|
|
404
|
-
const len = this.particleCount;
|
|
405
425
|
const particles = this.particles;
|
|
426
|
+
const len = particles.length;
|
|
406
427
|
const ctx = this.ctx;
|
|
407
428
|
const maxDist = this.option.particles.connectDist;
|
|
408
429
|
const maxDistSq = maxDist ** 2;
|
|
@@ -536,7 +557,7 @@ class CanvasParticles {
|
|
|
536
557
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
537
558
|
},
|
|
538
559
|
particles: {
|
|
539
|
-
|
|
560
|
+
generationType: ~~pno('particles.generationType', options.particles?.generationType, CanvasParticles.generationType.MATCH, { min: 0, max: 2 }),
|
|
540
561
|
drawLines: !!(options.particles?.drawLines ?? true),
|
|
541
562
|
color: options.particles?.color ?? 'black',
|
|
542
563
|
ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
|
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.2.4";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})}},{rootMargin:"-1px"});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)**2,h=o**2/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,l=i.posY-t.posY,p=s*s+l*l;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-l,-s),d=1/(p+h);const f=Math.cos(u),v=Math.sin(u);if(p<c){m=d*a;const e=f*m,s=v*m;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;m=d*r;const g=f*m,y=v*m;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,m=this.option.mouse.distRatio,f=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(!f){const t=d/Math.hypot(b,M);m<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,m=p.y-t.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=h/Math.sqrt(f)-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+=f)>=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.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,{min:0}),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3,{min:0})},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,{min: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,{min:0}),pulling:i("gravity.pulling",t.gravity?.pulling,0,{min: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){const i=e.parseNumericOption("mouse.connectDistMult",t,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*i}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.3.0";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({MANUAL:0,NEW:1,MATCH: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})}},{rootMargin:"-1px"});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=[];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;const s=this.option.particles.generationType;s!==e.generationType.MANUAL&&(s===e.generationType.NEW||0===this.particles.length?this.newParticles():s===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#t()}#i(){let t=Math.round(this.option.particles.ppm*this.width*this.height/1e6);if(t=Math.min(this.option.particles.max,t),!isFinite(t))throw new RangeError("particleCount must be finite");return 0|t}newParticles(){const t=this.#i();this.particles=[];for(let i=0;i<t;i++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#i();for(this.particles=this.particles.slice(0,i),t&&this.particles.forEach(t=>this.#e(t));i>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+2*Math.pow(i(),5))*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}}updateParticles(){const t=this.particles,e=t.length,s=this.option.particles.relSpeed,n=this.option.particles.relSize;for(let o=0;o<e;o++){const e=t[o];e.speed=(.5+.5*i())*s,e.size=(.5+2*Math.pow(i(),5))*n}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particles,n=s.length,o=this.option.particles.connectDist,a=o*this.option.gravity.repulsive*t,r=o*this.option.gravity.pulling*t,c=(o/2)**2,h=o**2/256;for(let t=0;t<n;t++){const i=s[t];for(let o=t+1;o<n;o++){const t=s[o],n=i.posX-t.posX,l=i.posY-t.posY,p=n*n+l*l;if(p>=c&&!e)continue;let u,d,m;u=Math.atan2(-l,-n),d=1/(p+h);const f=Math.cos(u),g=Math.sin(u);if(p<c){m=d*a;const e=f*m,s=g*m;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;m=d*r;const v=f*m,y=g*m;i.velX+=v,i.velY+=y,t.velX-=v,t.velY-=y}}}#n(i){const s=this.particles,n=s.length,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,m=this.option.mouse.distRatio,f=this.option.mouse.interactionType===e.interactionType.NONE,g=this.option.mouse.interactionType===e.interactionType.MOVE,v=1-Math.pow(.75,i);for(let e=0;e<n;e++){const n=s[e];n.dir+=2*(Math.random()-.5)*p*i,n.dir%=t;const y=Math.sin(n.dir)*n.speed,x=Math.cos(n.dir)*n.speed;n.posX+=(y+n.velX)*i,n.posY+=(x+n.velY)*i,n.posX%=o,n.posX<0&&(n.posX+=o),n.posY%=a,n.posY<0&&(n.posY+=a),n.velX*=Math.pow(u,i),n.velY*=Math.pow(u,i);const M=n.posX+r-h,b=n.posY+c-l;if(!f){const t=d/Math.hypot(M,b);m<t?(n.offX+=(t*M-M-n.offX)*v,n.offY+=(t*b-b-n.offY)*v):(n.offX-=n.offX*v,n.offY-=n.offY*v)}n.x=n.posX+n.offX,n.y=n.posY+n.offY,g&&(n.posX=n.x,n.posY=n.y),n.x+=r,n.y+=c,this.#o(n),n.isVisible=1===n.gridPos.x&&1===n.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.particles,e=i.length,s=this.ctx;for(let n=0;n<e;n++){const e=i[n];e.isVisible&&(e.size>1?(s.beginPath(),s.arc(e.x,e.y,e.size,0,t),s.fill(),s.closePath()):s.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}}#c(){const t=this.particles,i=t.length,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<i;s++){const p=t[s];let u=0;for(let d=s+1;d<i;d++){const i=t[d];if(!a&&!this.#a(p,i))continue;const s=p.x-i.x,m=p.y-i.y,f=s*s+m*m;if(!(f>n)&&(f>o?(e.globalAlpha=h/Math.sqrt(f)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(i.x,i.y),e.stroke()):l.push([p.x,p.y,i.x,i.y]),(u+=f)>=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.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,{min:0}),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3,{min:0})},particles:{generationType:~~i("particles.generationType",t.particles?.generationType,e.generationType.MATCH,{min:0,max:2}),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,{min: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,{min:0}),pulling:i("gravity.pulling",t.gravity?.pulling,0,{min: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){const i=e.parseNumericOption("mouse.connectDistMult",t,2/3,{min:0});this.option.mouse.connectDist=this.option.particles.connectDist*i}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
|
@@ -5,13 +5,13 @@ export interface CanvasParticlesOptions {
|
|
|
5
5
|
stopOnLeave: boolean;
|
|
6
6
|
};
|
|
7
7
|
mouse: {
|
|
8
|
-
interactionType:
|
|
8
|
+
interactionType: 0 | 1 | 2;
|
|
9
9
|
connectDistMult: number;
|
|
10
10
|
connectDist: number;
|
|
11
11
|
distRatio: number;
|
|
12
12
|
};
|
|
13
13
|
particles: {
|
|
14
|
-
|
|
14
|
+
generationType: 0 | 1 | 2;
|
|
15
15
|
drawLines: boolean;
|
|
16
16
|
color: string | CanvasGradient | CanvasPattern;
|
|
17
17
|
ppm: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvasparticles-js",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
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",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@rollup/plugin-replace": "^6.0.3",
|
|
30
30
|
"@rollup/plugin-terser": "^0.4.4",
|
|
31
31
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
32
|
-
"@types/node": "^24.
|
|
33
|
-
"prettier": "^3.
|
|
34
|
-
"rollup": "^4.
|
|
35
|
-
"rollup-plugin-delete": "^3.0.
|
|
32
|
+
"@types/node": "^24.10.4",
|
|
33
|
+
"prettier": "^3.7.4",
|
|
34
|
+
"rollup": "^4.54.0",
|
|
35
|
+
"rollup-plugin-delete": "^3.0.2",
|
|
36
36
|
"tslib": "^2.8.1",
|
|
37
37
|
"typescript": "^5.9.3"
|
|
38
38
|
},
|
package/src/index.ts
CHANGED
|
@@ -38,7 +38,14 @@ export default class CanvasParticles {
|
|
|
38
38
|
static readonly interactionType = Object.freeze({
|
|
39
39
|
NONE: 0, // No mouse interaction
|
|
40
40
|
SHIFT: 1, // Visual displacement only
|
|
41
|
-
MOVE: 2, // Actual particle movement
|
|
41
|
+
MOVE: 2, // Actual particle movement (default)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/** Defines how the particles are auto-generated */
|
|
45
|
+
static readonly generationType = Object.freeze({
|
|
46
|
+
MANUAL: 0, // Never auto-generate particles
|
|
47
|
+
NEW: 1, // Generate particles from scratch
|
|
48
|
+
MATCH: 2, // Add or remove particles to match new count (default)
|
|
42
49
|
})
|
|
43
50
|
|
|
44
51
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
@@ -110,8 +117,6 @@ export default class CanvasParticles {
|
|
|
110
117
|
private lastAnimationFrame: number = 0
|
|
111
118
|
|
|
112
119
|
particles: Particle[] = []
|
|
113
|
-
particleCount: number = 0
|
|
114
|
-
|
|
115
120
|
private clientX: number = Infinity
|
|
116
121
|
private clientY: number = Infinity
|
|
117
122
|
mouseX: number = Infinity
|
|
@@ -210,36 +215,42 @@ export default class CanvasParticles {
|
|
|
210
215
|
this.offX = (width - this.width) / 2
|
|
211
216
|
this.offY = (height - this.height) / 2
|
|
212
217
|
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
const generationType = this.option.particles.generationType
|
|
219
|
+
|
|
220
|
+
if (generationType !== CanvasParticles.generationType.MANUAL) {
|
|
221
|
+
if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0) this.newParticles()
|
|
222
|
+
else if (generationType === CanvasParticles.generationType.MATCH) this.matchParticleCount({ updateBounds: true })
|
|
223
|
+
}
|
|
215
224
|
|
|
216
225
|
if (this.isAnimating) this.#render()
|
|
217
226
|
}
|
|
218
227
|
|
|
219
228
|
/** @private Update the target number of particles based on the current canvas size and `options.particles.ppm`, capped at `options.particles.max`. */
|
|
220
|
-
#
|
|
229
|
+
#targetParticleCount(): number {
|
|
221
230
|
// Amount of particles to be created
|
|
222
|
-
|
|
223
|
-
|
|
231
|
+
let particleCount = Math.round((this.option.particles.ppm * this.width * this.height) / 1_000_000)
|
|
232
|
+
particleCount = Math.min(this.option.particles.max, particleCount)
|
|
224
233
|
|
|
225
|
-
if (!isFinite(
|
|
234
|
+
if (!isFinite(particleCount)) throw new RangeError('particleCount must be finite')
|
|
235
|
+
return particleCount | 0
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
/** @public Remove existing particles and generate new ones */
|
|
229
239
|
newParticles() {
|
|
230
|
-
this.#
|
|
240
|
+
const particleCount = this.#targetParticleCount()
|
|
231
241
|
|
|
232
242
|
this.particles = []
|
|
233
|
-
for (let i = 0; i <
|
|
243
|
+
for (let i = 0; i < particleCount; i++) this.createParticle()
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
/** @public Adjust particle array length to match `options.particles.ppm` */
|
|
237
247
|
matchParticleCount({ updateBounds = false }: { updateBounds?: boolean } = {}) {
|
|
238
|
-
this.#
|
|
248
|
+
const particleCount = this.#targetParticleCount()
|
|
239
249
|
|
|
240
|
-
this.particles = this.particles.slice(0,
|
|
250
|
+
this.particles = this.particles.slice(0, particleCount)
|
|
241
251
|
if (updateBounds) this.particles.forEach((particle) => this.#updateParticleBounds(particle))
|
|
242
|
-
|
|
252
|
+
|
|
253
|
+
while (particleCount > this.particles.length) this.createParticle()
|
|
243
254
|
}
|
|
244
255
|
|
|
245
256
|
/** @public Create a new particle with optional parameters */
|
|
@@ -256,9 +267,9 @@ export default class CanvasParticles {
|
|
|
256
267
|
velY: 0, // Vertical speed in pixels per update
|
|
257
268
|
offX: 0, // Horizontal distance from drawn to logical position in pixels
|
|
258
269
|
offY: 0, // Vertical distance from drawn to logical position in pixels
|
|
259
|
-
dir: dir
|
|
260
|
-
speed: speed
|
|
261
|
-
size: size
|
|
270
|
+
dir: dir ?? prng() * TWO_PI, // Direction in radians
|
|
271
|
+
speed: speed ?? (0.5 + prng() * 0.5) * this.option.particles.relSpeed, // Velocity in pixels per update
|
|
272
|
+
size: size ?? (0.5 + Math.pow(prng(), 5) * 2) * this.option.particles.relSize, // Ray in pixels of the particle
|
|
262
273
|
gridPos: { x: 1, y: 1 },
|
|
263
274
|
isVisible: false,
|
|
264
275
|
}
|
|
@@ -277,6 +288,20 @@ export default class CanvasParticles {
|
|
|
277
288
|
}
|
|
278
289
|
}
|
|
279
290
|
|
|
291
|
+
/* @public Randomize speed and size of all particles based on current options */
|
|
292
|
+
updateParticles() {
|
|
293
|
+
const particles = this.particles
|
|
294
|
+
const len = particles.length
|
|
295
|
+
const relSpeed = this.option.particles.relSpeed
|
|
296
|
+
const relSize = this.option.particles.relSize
|
|
297
|
+
|
|
298
|
+
for (let i = 0; i < len; i++) {
|
|
299
|
+
const particle = particles[i]
|
|
300
|
+
particle.speed = (0.5 + prng() * 0.5) * relSpeed
|
|
301
|
+
particle.size = (0.5 + Math.pow(prng(), 5) * 2) * relSize
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
280
305
|
/** @private Apply gravity forces between particles */
|
|
281
306
|
#updateGravity(step: number) {
|
|
282
307
|
const isRepulsiveEnabled = this.option.gravity.repulsive > 0
|
|
@@ -284,8 +309,8 @@ export default class CanvasParticles {
|
|
|
284
309
|
|
|
285
310
|
if (!isRepulsiveEnabled && !isPullingEnabled) return
|
|
286
311
|
|
|
287
|
-
const len = this.particleCount
|
|
288
312
|
const particles = this.particles
|
|
313
|
+
const len = particles.length
|
|
289
314
|
const connectDist = this.option.particles.connectDist
|
|
290
315
|
const gravRepulsiveMult = connectDist * this.option.gravity.repulsive * step
|
|
291
316
|
const gravPullingMult = connectDist * this.option.gravity.pulling * step
|
|
@@ -340,8 +365,8 @@ export default class CanvasParticles {
|
|
|
340
365
|
|
|
341
366
|
/** @private Update positions, directions, and visibility of all particles */
|
|
342
367
|
#updateParticles(step: number) {
|
|
343
|
-
const len = this.particleCount
|
|
344
368
|
const particles = this.particles
|
|
369
|
+
const len = particles.length
|
|
345
370
|
const width = this.width
|
|
346
371
|
const height = this.height
|
|
347
372
|
const offX = this.offX
|
|
@@ -450,8 +475,8 @@ export default class CanvasParticles {
|
|
|
450
475
|
|
|
451
476
|
/** @private Draw the particles on the canvas */
|
|
452
477
|
#renderParticles() {
|
|
453
|
-
const len = this.particleCount
|
|
454
478
|
const particles = this.particles
|
|
479
|
+
const len = particles.length
|
|
455
480
|
const ctx = this.ctx
|
|
456
481
|
|
|
457
482
|
for (let i = 0; i < len; i++) {
|
|
@@ -475,8 +500,8 @@ export default class CanvasParticles {
|
|
|
475
500
|
|
|
476
501
|
/** @private Draw lines between particles if they are close enough */
|
|
477
502
|
#renderConnections() {
|
|
478
|
-
const len = this.particleCount
|
|
479
503
|
const particles = this.particles
|
|
504
|
+
const len = particles.length
|
|
480
505
|
const ctx = this.ctx
|
|
481
506
|
const maxDist = this.option.particles.connectDist
|
|
482
507
|
const maxDistSq = maxDist ** 2
|
|
@@ -632,13 +657,18 @@ export default class CanvasParticles {
|
|
|
632
657
|
options.mouse?.interactionType,
|
|
633
658
|
CanvasParticles.interactionType.MOVE,
|
|
634
659
|
{ min: 0, max: 2 }
|
|
635
|
-
),
|
|
660
|
+
) as 0 | 1 | 2,
|
|
636
661
|
connectDistMult: pno('mouse.connectDistMult', options.mouse?.connectDistMult, 2 / 3, { min: 0 }),
|
|
637
662
|
connectDist: 1 /* post processed */,
|
|
638
663
|
distRatio: pno('mouse.distRatio', options.mouse?.distRatio, 2 / 3, { min: 0 }),
|
|
639
664
|
},
|
|
640
665
|
particles: {
|
|
641
|
-
|
|
666
|
+
generationType: ~~pno(
|
|
667
|
+
'particles.generationType',
|
|
668
|
+
options.particles?.generationType,
|
|
669
|
+
CanvasParticles.generationType.MATCH,
|
|
670
|
+
{ min: 0, max: 2 }
|
|
671
|
+
) as 0 | 1 | 2,
|
|
642
672
|
drawLines: !!(options.particles?.drawLines ?? true),
|
|
643
673
|
color: options.particles?.color ?? 'black',
|
|
644
674
|
ppm: ~~pno('particles.ppm', options.particles?.ppm, 100),
|
package/src/types/options.ts
CHANGED
|
@@ -7,14 +7,14 @@ export interface CanvasParticlesOptions {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
mouse: {
|
|
10
|
-
interactionType:
|
|
10
|
+
interactionType: 0 | 1 | 2 /* see CanvasParticles.interactionType */
|
|
11
11
|
connectDistMult: number
|
|
12
12
|
connectDist: number /* post processed */
|
|
13
13
|
distRatio: number
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
particles: {
|
|
17
|
-
|
|
17
|
+
generationType: 0 | 1 | 2 /* see CanvasParticles.generationType */
|
|
18
18
|
drawLines: boolean
|
|
19
19
|
color: string | CanvasGradient | CanvasPattern
|
|
20
20
|
ppm: number
|