canvasparticles-js 4.4.2 → 4.4.4
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 +31 -23
- package/dist/index.cjs +30 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +30 -19
- package/dist/index.umd.js +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/options.d.ts +1 -0
- package/package.json +1 -1
- package/src/index.ts +29 -19
- package/src/types/index.ts +0 -2
- package/src/types/options.ts +1 -0
package/README.md
CHANGED
|
@@ -175,8 +175,7 @@ Play around with these values in the [Sandbox](https://khoeckman.github.io/canva
|
|
|
175
175
|
|
|
176
176
|
### Options Object
|
|
177
177
|
|
|
178
|
-
The default value will be used when an option is assigned an invalid value
|
|
179
|
-
Your screen resolution and refresh rate will directly impact perfomance!
|
|
178
|
+
The default value will be used when an option is assigned an invalid value.
|
|
180
179
|
|
|
181
180
|
### Root options
|
|
182
181
|
|
|
@@ -188,6 +187,8 @@ Your screen resolution and refresh rate will directly impact perfomance!
|
|
|
188
187
|
|
|
189
188
|
### `animation`
|
|
190
189
|
|
|
190
|
+
It's best to not touch these values if it's unclear what it does.
|
|
191
|
+
|
|
191
192
|
| Option | Type | Default | Description |
|
|
192
193
|
| ------------------------ | --------- | ------- | ---------------------------------------------------- |
|
|
193
194
|
| `animation.startOnEnter` | `boolean` | `true` | Start animation when the canvas enters the viewport. |
|
|
@@ -207,24 +208,30 @@ Your screen resolution and refresh rate will directly impact perfomance!
|
|
|
207
208
|
|
|
208
209
|
- `NONE (0)` – No interaction
|
|
209
210
|
- `SHIFT (1)` – Visual displacement only
|
|
210
|
-
- `MOVE (2)` – Actual particle movement
|
|
211
|
+
- `MOVE (2)` – Actual particle movement (default)
|
|
211
212
|
|
|
212
213
|
---
|
|
213
214
|
|
|
214
215
|
### `particles`
|
|
215
216
|
|
|
216
|
-
| Option | Type | Default | Description
|
|
217
|
-
| --------------------------- | ------------- | ---------- |
|
|
218
|
-
| `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `
|
|
219
|
-
| `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format.
|
|
220
|
-
| `particles.ppm` | `integer` | `100` | Particles per million pixels.
|
|
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).
|
|
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.
|
|
217
|
+
| Option | Type | Default | Description |
|
|
218
|
+
| --------------------------- | ------------- | ---------- | ------------------------------------------------------------------------------------------------- |
|
|
219
|
+
| `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `1 = OFF`, `2 = MATCH` |
|
|
220
|
+
| `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
|
|
221
|
+
| `particles.ppm` | `integer` | `100` | Particles per million pixels. |
|
|
222
|
+
| `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
|
|
223
|
+
| `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
|
|
224
|
+
| `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). |
|
|
225
|
+
| `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
|
|
226
|
+
| `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
|
|
227
|
+
| `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
|
|
228
|
+
| `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
|
|
229
|
+
|
|
230
|
+
**Generation types** (enum)
|
|
231
|
+
|
|
232
|
+
- `OFF (0)` – Never auto-generate particles
|
|
233
|
+
- `NEW (1)` – Generate all particles from scratch
|
|
234
|
+
- `MATCH (2)` – Add or remove some particles to match the new count (default)
|
|
228
235
|
|
|
229
236
|
---
|
|
230
237
|
|
|
@@ -232,11 +239,12 @@ Your screen resolution and refresh rate will directly impact perfomance!
|
|
|
232
239
|
|
|
233
240
|
Enabling gravity (`repulsive` or `pulling` > 0) performs an extra **O(n²)** gravity computations per frame.
|
|
234
241
|
|
|
235
|
-
| Option
|
|
236
|
-
|
|
|
237
|
-
| `gravity.repulsive`
|
|
238
|
-
| `gravity.pulling`
|
|
239
|
-
| `gravity.friction`
|
|
242
|
+
| Option | Type | Default | Description |
|
|
243
|
+
| --------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------- |
|
|
244
|
+
| `gravity.repulsive` | `float` | `0` | Repulsive force between particles. |
|
|
245
|
+
| `gravity.pulling` | `float` | `0` | Attractive force between particles. Requires sufficient repulsion to avoid clustering. |
|
|
246
|
+
| `gravity.friction` | `float` | `0.8` | Damping factor applied to gravitational velocity each update (`0.0 – 1.0`). |
|
|
247
|
+
| `gravity.preventExplosions` | `boolean` | `false` | Clamp the maximum velocity so particles will not explode outward under heavy pulling gravity. |
|
|
240
248
|
|
|
241
249
|
---
|
|
242
250
|
|
|
@@ -342,13 +350,13 @@ instance.options = { ... }
|
|
|
342
350
|
createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number)
|
|
343
351
|
```
|
|
344
352
|
|
|
345
|
-
By default `particles.ppm` and `particles.max` are used to auto-generate random particles.
|
|
353
|
+
By default `particles.ppm` and `particles.max` are used to auto-generate random particles. Set one or both of these properties to `0` or set `particles.generationType` to `OFF (0)` (slightly more performant).
|
|
346
354
|
|
|
347
355
|
```js
|
|
348
356
|
const canvas = '#my-canvas'
|
|
349
357
|
const options = {
|
|
350
358
|
particles: {
|
|
351
|
-
|
|
359
|
+
generationType: CanvasParticles.generationType.OFF, // = 0
|
|
352
360
|
rotationSpeed: 0,
|
|
353
361
|
},
|
|
354
362
|
}
|
|
@@ -393,7 +401,7 @@ instance.newParticles({ keepAuto: true, keepManual: false })
|
|
|
393
401
|
const options = {
|
|
394
402
|
background: 'hsl(125, 42%, 35%)',
|
|
395
403
|
mouse: {
|
|
396
|
-
interactionType: CanvasParticles.interactionType.
|
|
404
|
+
interactionType: CanvasParticles.interactionType.SHIFT, // = 1
|
|
397
405
|
},
|
|
398
406
|
particles: {
|
|
399
407
|
color: 'rgba(150, 255, 105, 0.95)',
|
package/dist/index.cjs
CHANGED
|
@@ -21,7 +21,7 @@ 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.4.
|
|
24
|
+
static version = "4.4.4";
|
|
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 */
|
|
@@ -32,9 +32,9 @@ class CanvasParticles {
|
|
|
32
32
|
});
|
|
33
33
|
/** Defines how the particles are auto-generated */
|
|
34
34
|
static generationType = Object.freeze({
|
|
35
|
-
|
|
36
|
-
NEW: 1, // Generate particles from scratch
|
|
37
|
-
MATCH: 2, // Add or remove particles to match new count (default)
|
|
35
|
+
OFF: 0, // Never auto-generate particles
|
|
36
|
+
NEW: 1, // Generate all particles from scratch
|
|
37
|
+
MATCH: 2, // Add or remove some particles to match the new count (default)
|
|
38
38
|
});
|
|
39
39
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
40
40
|
static canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
@@ -169,7 +169,7 @@ class CanvasParticles {
|
|
|
169
169
|
this.offX = (width - this.width) / 2;
|
|
170
170
|
this.offY = (height - this.height) / 2;
|
|
171
171
|
const generationType = this.option.particles.generationType;
|
|
172
|
-
if (generationType !== CanvasParticles.generationType.
|
|
172
|
+
if (generationType !== CanvasParticles.generationType.OFF) {
|
|
173
173
|
if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
|
|
174
174
|
this.newParticles();
|
|
175
175
|
else if (generationType === CanvasParticles.generationType.MATCH)
|
|
@@ -343,12 +343,13 @@ class CanvasParticles {
|
|
|
343
343
|
const offY = this.offY;
|
|
344
344
|
const mouseX = this.mouseX;
|
|
345
345
|
const mouseY = this.mouseY;
|
|
346
|
-
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
347
|
-
const friction = this.option.gravity.friction;
|
|
348
|
-
const mouseConnectDist = this.option.mouse.connectDist;
|
|
349
|
-
const mouseDistRatio = this.option.mouse.distRatio;
|
|
350
346
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
|
|
351
347
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
|
|
348
|
+
const mouseConnectDist = this.option.mouse.connectDist;
|
|
349
|
+
const mouseDistRatio = this.option.mouse.distRatio;
|
|
350
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
351
|
+
const friction = this.option.gravity.friction;
|
|
352
|
+
const preventExplosions = this.option.gravity.preventExplosions;
|
|
352
353
|
const easing = 1 - Math.pow(3 / 4, step);
|
|
353
354
|
for (const p of this.particles) {
|
|
354
355
|
p.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
|
|
@@ -356,6 +357,18 @@ class CanvasParticles {
|
|
|
356
357
|
// Constant velocity
|
|
357
358
|
const movX = Math.sin(p.dir) * p.speed;
|
|
358
359
|
const movY = Math.cos(p.dir) * p.speed;
|
|
360
|
+
// Maximum velocity
|
|
361
|
+
if (preventExplosions) {
|
|
362
|
+
const maxVel = Math.max(p.speed, friction) * 2;
|
|
363
|
+
if (p.velX > maxVel)
|
|
364
|
+
p.velX = maxVel;
|
|
365
|
+
if (p.velX < -maxVel)
|
|
366
|
+
p.velX = -maxVel;
|
|
367
|
+
if (p.velY > maxVel)
|
|
368
|
+
p.velY = maxVel;
|
|
369
|
+
if (p.velY < -maxVel)
|
|
370
|
+
p.velY = -maxVel;
|
|
371
|
+
}
|
|
359
372
|
// Apply velocities
|
|
360
373
|
p.posX += (movX + p.velX) * step;
|
|
361
374
|
p.posY += (movY + p.velY) * step;
|
|
@@ -474,7 +487,7 @@ class CanvasParticles {
|
|
|
474
487
|
const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
|
|
475
488
|
const alpha = this.color.alpha;
|
|
476
489
|
const alphaFactor = this.color.alpha * maxDist;
|
|
477
|
-
const bucket = []; // Batch line segments of max alpha
|
|
490
|
+
const bucket = []; // Batch line segments of max alpha (2D -> 1D; stride = 4)
|
|
478
491
|
const grid = this.#buildSpatialGrid(stride, invCellSize); // O(n^2) -> O(n)
|
|
479
492
|
let particleWork = 0;
|
|
480
493
|
let allowWork = true;
|
|
@@ -494,7 +507,7 @@ class CanvasParticles {
|
|
|
494
507
|
}
|
|
495
508
|
else {
|
|
496
509
|
// Cache lines with max alpha to later be drawn in one batch
|
|
497
|
-
bucket.push(
|
|
510
|
+
bucket.push(ax, ay, bx, by);
|
|
498
511
|
}
|
|
499
512
|
particleWork += distSq;
|
|
500
513
|
allowWork = particleWork < maxWorkPerParticle;
|
|
@@ -561,8 +574,7 @@ class CanvasParticles {
|
|
|
561
574
|
if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
|
|
562
575
|
renderConnectionsToOwnCell(cell || [], a, pa);
|
|
563
576
|
// Next iteration
|
|
564
|
-
a
|
|
565
|
-
if (!(a < len))
|
|
577
|
+
if (++a >= len)
|
|
566
578
|
break;
|
|
567
579
|
// Same code inline but the order of grid.get() is different to remove maxWork artifacts
|
|
568
580
|
particleWork = 0;
|
|
@@ -590,8 +602,7 @@ class CanvasParticles {
|
|
|
590
602
|
if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
|
|
591
603
|
renderConnectionsToOwnCell(cell || [], a, pa);
|
|
592
604
|
// Next iteration
|
|
593
|
-
a
|
|
594
|
-
if (!(a < len))
|
|
605
|
+
if (++a >= len)
|
|
595
606
|
break;
|
|
596
607
|
// Same code inline but the order of grid.get() is different to remove maxWork artifacts
|
|
597
608
|
particleWork = 0;
|
|
@@ -624,10 +635,9 @@ class CanvasParticles {
|
|
|
624
635
|
// Render all bucketed lines at once
|
|
625
636
|
ctx.globalAlpha = alpha;
|
|
626
637
|
ctx.beginPath();
|
|
627
|
-
for (let
|
|
628
|
-
|
|
629
|
-
ctx.
|
|
630
|
-
ctx.lineTo(line[2], line[3]);
|
|
638
|
+
for (let line = 0; line < bucket.length; line += 4) {
|
|
639
|
+
ctx.moveTo(bucket[line], bucket[line + 1]);
|
|
640
|
+
ctx.lineTo(bucket[line + 2], bucket[line + 3]);
|
|
631
641
|
}
|
|
632
642
|
ctx.stroke();
|
|
633
643
|
}
|
|
@@ -764,6 +774,7 @@ class CanvasParticles {
|
|
|
764
774
|
repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
|
|
765
775
|
pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
|
|
766
776
|
friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
|
|
777
|
+
preventExplosions: !!options.gravity?.preventExplosions,
|
|
767
778
|
},
|
|
768
779
|
debug: {
|
|
769
780
|
drawGrid: !!options.debug?.drawGrid,
|
package/dist/index.d.ts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -19,7 +19,7 @@ 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.4.
|
|
22
|
+
static version = "4.4.4";
|
|
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 */
|
|
@@ -30,9 +30,9 @@ class CanvasParticles {
|
|
|
30
30
|
});
|
|
31
31
|
/** Defines how the particles are auto-generated */
|
|
32
32
|
static generationType = Object.freeze({
|
|
33
|
-
|
|
34
|
-
NEW: 1, // Generate particles from scratch
|
|
35
|
-
MATCH: 2, // Add or remove particles to match new count (default)
|
|
33
|
+
OFF: 0, // Never auto-generate particles
|
|
34
|
+
NEW: 1, // Generate all particles from scratch
|
|
35
|
+
MATCH: 2, // Add or remove some particles to match the new count (default)
|
|
36
36
|
});
|
|
37
37
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
38
38
|
static canvasIntersectionObserver = new IntersectionObserver((entries) => {
|
|
@@ -167,7 +167,7 @@ class CanvasParticles {
|
|
|
167
167
|
this.offX = (width - this.width) / 2;
|
|
168
168
|
this.offY = (height - this.height) / 2;
|
|
169
169
|
const generationType = this.option.particles.generationType;
|
|
170
|
-
if (generationType !== CanvasParticles.generationType.
|
|
170
|
+
if (generationType !== CanvasParticles.generationType.OFF) {
|
|
171
171
|
if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0)
|
|
172
172
|
this.newParticles();
|
|
173
173
|
else if (generationType === CanvasParticles.generationType.MATCH)
|
|
@@ -341,12 +341,13 @@ class CanvasParticles {
|
|
|
341
341
|
const offY = this.offY;
|
|
342
342
|
const mouseX = this.mouseX;
|
|
343
343
|
const mouseY = this.mouseY;
|
|
344
|
-
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
345
|
-
const friction = this.option.gravity.friction;
|
|
346
|
-
const mouseConnectDist = this.option.mouse.connectDist;
|
|
347
|
-
const mouseDistRatio = this.option.mouse.distRatio;
|
|
348
344
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
|
|
349
345
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
|
|
346
|
+
const mouseConnectDist = this.option.mouse.connectDist;
|
|
347
|
+
const mouseDistRatio = this.option.mouse.distRatio;
|
|
348
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
349
|
+
const friction = this.option.gravity.friction;
|
|
350
|
+
const preventExplosions = this.option.gravity.preventExplosions;
|
|
350
351
|
const easing = 1 - Math.pow(3 / 4, step);
|
|
351
352
|
for (const p of this.particles) {
|
|
352
353
|
p.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
|
|
@@ -354,6 +355,18 @@ class CanvasParticles {
|
|
|
354
355
|
// Constant velocity
|
|
355
356
|
const movX = Math.sin(p.dir) * p.speed;
|
|
356
357
|
const movY = Math.cos(p.dir) * p.speed;
|
|
358
|
+
// Maximum velocity
|
|
359
|
+
if (preventExplosions) {
|
|
360
|
+
const maxVel = Math.max(p.speed, friction) * 2;
|
|
361
|
+
if (p.velX > maxVel)
|
|
362
|
+
p.velX = maxVel;
|
|
363
|
+
if (p.velX < -maxVel)
|
|
364
|
+
p.velX = -maxVel;
|
|
365
|
+
if (p.velY > maxVel)
|
|
366
|
+
p.velY = maxVel;
|
|
367
|
+
if (p.velY < -maxVel)
|
|
368
|
+
p.velY = -maxVel;
|
|
369
|
+
}
|
|
357
370
|
// Apply velocities
|
|
358
371
|
p.posX += (movX + p.velX) * step;
|
|
359
372
|
p.posY += (movY + p.velY) * step;
|
|
@@ -472,7 +485,7 @@ class CanvasParticles {
|
|
|
472
485
|
const maxWorkPerParticle = maxDistSq * this.option.particles.maxWork;
|
|
473
486
|
const alpha = this.color.alpha;
|
|
474
487
|
const alphaFactor = this.color.alpha * maxDist;
|
|
475
|
-
const bucket = []; // Batch line segments of max alpha
|
|
488
|
+
const bucket = []; // Batch line segments of max alpha (2D -> 1D; stride = 4)
|
|
476
489
|
const grid = this.#buildSpatialGrid(stride, invCellSize); // O(n^2) -> O(n)
|
|
477
490
|
let particleWork = 0;
|
|
478
491
|
let allowWork = true;
|
|
@@ -492,7 +505,7 @@ class CanvasParticles {
|
|
|
492
505
|
}
|
|
493
506
|
else {
|
|
494
507
|
// Cache lines with max alpha to later be drawn in one batch
|
|
495
|
-
bucket.push(
|
|
508
|
+
bucket.push(ax, ay, bx, by);
|
|
496
509
|
}
|
|
497
510
|
particleWork += distSq;
|
|
498
511
|
allowWork = particleWork < maxWorkPerParticle;
|
|
@@ -559,8 +572,7 @@ class CanvasParticles {
|
|
|
559
572
|
if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
|
|
560
573
|
renderConnectionsToOwnCell(cell || [], a, pa);
|
|
561
574
|
// Next iteration
|
|
562
|
-
a
|
|
563
|
-
if (!(a < len))
|
|
575
|
+
if (++a >= len)
|
|
564
576
|
break;
|
|
565
577
|
// Same code inline but the order of grid.get() is different to remove maxWork artifacts
|
|
566
578
|
particleWork = 0;
|
|
@@ -588,8 +600,7 @@ class CanvasParticles {
|
|
|
588
600
|
if (cellX >= 0 && cellY >= 0 && cellX < stride - 2 && (cell = grid.get(key)))
|
|
589
601
|
renderConnectionsToOwnCell(cell || [], a, pa);
|
|
590
602
|
// Next iteration
|
|
591
|
-
a
|
|
592
|
-
if (!(a < len))
|
|
603
|
+
if (++a >= len)
|
|
593
604
|
break;
|
|
594
605
|
// Same code inline but the order of grid.get() is different to remove maxWork artifacts
|
|
595
606
|
particleWork = 0;
|
|
@@ -622,10 +633,9 @@ class CanvasParticles {
|
|
|
622
633
|
// Render all bucketed lines at once
|
|
623
634
|
ctx.globalAlpha = alpha;
|
|
624
635
|
ctx.beginPath();
|
|
625
|
-
for (let
|
|
626
|
-
|
|
627
|
-
ctx.
|
|
628
|
-
ctx.lineTo(line[2], line[3]);
|
|
636
|
+
for (let line = 0; line < bucket.length; line += 4) {
|
|
637
|
+
ctx.moveTo(bucket[line], bucket[line + 1]);
|
|
638
|
+
ctx.lineTo(bucket[line + 2], bucket[line + 3]);
|
|
629
639
|
}
|
|
630
640
|
ctx.stroke();
|
|
631
641
|
}
|
|
@@ -762,6 +772,7 @@ class CanvasParticles {
|
|
|
762
772
|
repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
|
|
763
773
|
pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
|
|
764
774
|
friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
|
|
775
|
+
preventExplosions: !!options.gravity?.preventExplosions,
|
|
765
776
|
},
|
|
766
777
|
debug: {
|
|
767
778
|
drawGrid: !!options.debug?.drawGrid,
|
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.4.2";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(const i of t){const t=i.target,e=t.instance;if(!e.options?.animation)return;(t.inViewbox=i.isIntersecting)?e.option.animation?.startOnEnter&&e.start({auto:!0}):e.option.animation?.stopOnLeave&&e.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}for(const i of t){i.target.instance.#t()}});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 i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):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=[];hasManualParticles=!1;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.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}#t(){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.#i()}resizeCanvas(){this.updateCanvasRect(),this.#t()}#e(){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({keepAuto:t=!1,keepManual:i=!0}={}){const e=this.#e();if(this.hasManualParticles&&(t||i)?(this.particles=this.particles.filter(e=>t&&!e.isManual||i&&e.isManual),this.hasManualParticles=this.particles.length>0):this.particles=[],!t)for(let t=0;t<e;t++)this.#s()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#e();if(this.hasManualParticles){const t=[];let e=0;for(const s of this.particles)s.isManual?t.push(s):e>=i||(t.push(s),e++);this.particles=t}else this.particles=this.particles.slice(0,i);if(t)for(const t of this.particles)this.#n(t);for(let t=this.particles.length;t<i;t++)this.#s()}#s(){const e=i()*this.width,s=i()*this.height;this.createParticle(e,s,i()*t,(.5+.5*i())*this.option.particles.relSpeed,(.5+2*Math.pow(i(),5))*this.option.particles.relSize,!1)}createParticle(t,i,e,s,n,o=!0){const a={posX:t,posY:i,x:t,y:i,velX:0,velY:0,offX:0,offY:0,dir:e,speed:s,size:n,gridPos:{x:1,y:1},isVisible:!1,isManual:o};this.#n(a),this.particles.push(a),this.hasManualParticles=!0}#n(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.option.particles.relSpeed,e=this.option.particles.relSize;for(const s of this.particles)s.speed=(.5+.5*i())*t,s.size=(.5+2*Math.pow(i(),5))*e,this.#n(s)}#o(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,l=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,h=i.posY-t.posY,p=n*n+h*h;if(p>=c&&!e)continue;const d=1/Math.sqrt(p+l),u=d*d*d;if(p<c){const e=u*a,s=-n*e,o=-h*e;i.velX-=s,i.velY-=o,t.velX+=s,t.velY+=o}if(!e)continue;const f=u*r,g=-n*f,m=-h*f;i.velX+=g,i.velY+=m,t.velX-=g,t.velY-=m}}}#a(i){const s=this.width,n=this.height,o=this.offX,a=this.offY,r=this.mouseX,c=this.mouseY,l=this.option.particles.rotationSpeed*i,h=this.option.gravity.friction,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.mouse.interactionType===e.interactionType.NONE,f=this.option.mouse.interactionType===e.interactionType.MOVE,g=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*l*i,e.dir%=t;const m=Math.sin(e.dir)*e.speed,v=Math.cos(e.dir)*e.speed;e.posX+=(m+e.velX)*i,e.posY+=(v+e.velY)*i,e.posX%=s,e.posX<0&&(e.posX+=s),e.posY%=n,e.posY<0&&(e.posY+=n),e.velX*=Math.pow(h,i),e.velY*=Math.pow(h,i);const x=e.posX+o-r,y=e.posY+a-c;if(!u){const t=p/Math.hypot(x,y);d<t?(e.offX+=(t*x-x-e.offX)*g,e.offY+=(t*y-y-e.offY)*g):(e.offX-=e.offX*g,e.offY-=e.offY*g)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,f&&(e.posX=e.x,e.posY=e.y),e.x+=o,e.y+=a,e.gridPos.x=+(e.x>=e.bounds.left)+ +(e.x>e.bounds.right),e.gridPos.y=+(e.y>=e.bounds.top)+ +(e.y>e.bounds.bottom),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#r(){const i=this.ctx;for(const e of this.particles)e.isVisible&&(e.size>1?(i.beginPath(),i.arc(e.x,e.y,e.size,0,t),i.fill(),i.closePath()):i.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}#c(t,i){const e=this.particles,s=e.length,n=new Map;for(let o=0;o<s;o++){const s=e[o],a=(s.x*i|0)+Math.imul(s.y*i,t),r=n.get(a);r?r.push(o):n.set(a,[o])}return n}static#l(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)}#h(){const t=this.particles,i=t.length,s=this.ctx,n=this.option.particles.connectDist,o=n**2,a=(n/2)**2,r=1/n,c=Math.ceil(this.width*r),l=n>=Math.min(this.canvas.width,this.canvas.height),h=o*this.option.particles.maxWork,p=this.color.alpha,d=this.color.alpha*n,u=[],f=this.#c(c,r);let g=0,m=!0;function v(t,i,e,n){const r=t-e,c=i-n,l=r*r+c*c;l>o||(l>a?(s.globalAlpha=d/Math.sqrt(l)-p,s.beginPath(),s.moveTo(t,i),s.lineTo(e,n),s.stroke()):u.push([t,i,e,n]),g+=l,m=g<h)}function x(i,s,n){for(const o of i){if(s>=o)continue;const i=t[o];if((l||e.#l(n,i))&&(v(n.x,n.y,i.x,i.y),!m))break}}function y(i,s){for(const n of i){const i=t[n];if((l||e.#l(s,i))&&(v(s.x,s.y,i.x,i.y),!m))break}}for(let e=0;e<i;e++){g=0,m=!0;let s,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c);if((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),e++,!(e<i))break;if(g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c+1))&&y(s,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&((s=f.get(l+c))&&y(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),e++,!(e<i))break;g=0,m=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c))&&y(s,n),m&&((s=f.get(l+1))&&y(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),m&&((s=f.get(l+c-1))&&y(s,n),m&&(s=f.get(l+c+1))&&y(s,n))))}}}if(u.length){s.globalAlpha=p,s.beginPath();for(let t=0;t<u.length;t++){const i=u[t];s.moveTo(i[0],i[1]),s.lineTo(i[2],i[3])}s.stroke()}}#p(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#d(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#u(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#o(i),this.#a(i),this.lastAnimationFrame=t}#i(){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.option.particles.drawLines&&this.#h(),this.option.debug.drawGrid&&this.#p(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#d()}#f(){this.isAnimating&&(requestAnimationFrame(()=>this.#f()),this.#u(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#f())),!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})},debug:{drawGrid:!!t.debug?.drawGrid,drawIndexes:!!t.debug?.drawIndexes}},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.4.4";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static generationType=Object.freeze({OFF:0,NEW:1,MATCH:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(const i of t){const t=i.target,e=t.instance;if(!e.options?.animation)return;(t.inViewbox=i.isIntersecting)?e.option.animation?.startOnEnter&&e.start({auto:!0}):e.option.animation?.stopOnLeave&&e.stop({auto:!0,clear:!1})}},{rootMargin:"-1px"});static canvasResizeObserver=new ResizeObserver(t=>{for(const i of t){i.target.instance.updateCanvasRect()}for(const i of t){i.target.instance.#t()}});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 i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):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=[];hasManualParticles=!1;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.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}#t(){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.OFF&&(s===e.generationType.NEW||0===this.particles.length?this.newParticles():s===e.generationType.MATCH&&this.matchParticleCount({updateBounds:!0})),this.isAnimating&&this.#i()}resizeCanvas(){this.updateCanvasRect(),this.#t()}#e(){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({keepAuto:t=!1,keepManual:i=!0}={}){const e=this.#e();if(this.hasManualParticles&&(t||i)?(this.particles=this.particles.filter(e=>t&&!e.isManual||i&&e.isManual),this.hasManualParticles=this.particles.length>0):this.particles=[],!t)for(let t=0;t<e;t++)this.#s()}matchParticleCount({updateBounds:t=!1}={}){const i=this.#e();if(this.hasManualParticles){const t=[];let e=0;for(const s of this.particles)s.isManual?t.push(s):e>=i||(t.push(s),e++);this.particles=t}else this.particles=this.particles.slice(0,i);if(t)for(const t of this.particles)this.#n(t);for(let t=this.particles.length;t<i;t++)this.#s()}#s(){const e=i()*this.width,s=i()*this.height;this.createParticle(e,s,i()*t,(.5+.5*i())*this.option.particles.relSpeed,(.5+2*Math.pow(i(),5))*this.option.particles.relSize,!1)}createParticle(t,i,e,s,n,o=!0){const a={posX:t,posY:i,x:t,y:i,velX:0,velY:0,offX:0,offY:0,dir:e,speed:s,size:n,gridPos:{x:1,y:1},isVisible:!1,isManual:o};this.#n(a),this.particles.push(a),this.hasManualParticles=!0}#n(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.option.particles.relSpeed,e=this.option.particles.relSize;for(const s of this.particles)s.speed=(.5+.5*i())*t,s.size=(.5+2*Math.pow(i(),5))*e,this.#n(s)}#o(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,l=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,h=i.posY-t.posY,p=n*n+h*h;if(p>=c&&!e)continue;const d=1/Math.sqrt(p+l),u=d*d*d;if(p<c){const e=u*a,s=-n*e,o=-h*e;i.velX-=s,i.velY-=o,t.velX+=s,t.velY+=o}if(!e)continue;const f=u*r,g=-n*f,v=-h*f;i.velX+=g,i.velY+=v,t.velX-=g,t.velY-=v}}}#a(i){const s=this.width,n=this.height,o=this.offX,a=this.offY,r=this.mouseX,c=this.mouseY,l=this.option.mouse.interactionType===e.interactionType.NONE,h=this.option.mouse.interactionType===e.interactionType.MOVE,p=this.option.mouse.connectDist,d=this.option.mouse.distRatio,u=this.option.particles.rotationSpeed*i,f=this.option.gravity.friction,g=this.option.gravity.preventExplosions,v=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*u*i,e.dir%=t;const m=Math.sin(e.dir)*e.speed,x=Math.cos(e.dir)*e.speed;if(g){const t=2*Math.max(e.speed,f);e.velX>t&&(e.velX=t),e.velX<-t&&(e.velX=-t),e.velY>t&&(e.velY=t),e.velY<-t&&(e.velY=-t)}e.posX+=(m+e.velX)*i,e.posY+=(x+e.velY)*i,e.posX%=s,e.posX<0&&(e.posX+=s),e.posY%=n,e.posY<0&&(e.posY+=n),e.velX*=Math.pow(f,i),e.velY*=Math.pow(f,i);const y=e.posX+o-r,M=e.posY+a-c;if(!l){const t=p/Math.hypot(y,M);d<t?(e.offX+=(t*y-y-e.offX)*v,e.offY+=(t*M-M-e.offY)*v):(e.offX-=e.offX*v,e.offY-=e.offY*v)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,h&&(e.posX=e.x,e.posY=e.y),e.x+=o,e.y+=a,e.gridPos.x=+(e.x>=e.bounds.left)+ +(e.x>e.bounds.right),e.gridPos.y=+(e.y>=e.bounds.top)+ +(e.y>e.bounds.bottom),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#r(){const i=this.ctx;for(const e of this.particles)e.isVisible&&(e.size>1?(i.beginPath(),i.arc(e.x,e.y,e.size,0,t),i.fill(),i.closePath()):i.fillRect(e.x-e.size,e.y-e.size,2*e.size,2*e.size))}#c(t,i){const e=this.particles,s=e.length,n=new Map;for(let o=0;o<s;o++){const s=e[o],a=(s.x*i|0)+Math.imul(s.y*i,t),r=n.get(a);r?r.push(o):n.set(a,[o])}return n}static#l(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)}#h(){const t=this.particles,i=t.length,s=this.ctx,n=this.option.particles.connectDist,o=n**2,a=(n/2)**2,r=1/n,c=Math.ceil(this.width*r),l=n>=Math.min(this.canvas.width,this.canvas.height),h=o*this.option.particles.maxWork,p=this.color.alpha,d=this.color.alpha*n,u=[],f=this.#c(c,r);let g=0,v=!0;function m(t,i,e,n){const r=t-e,c=i-n,l=r*r+c*c;l>o||(l>a?(s.globalAlpha=d/Math.sqrt(l)-p,s.beginPath(),s.moveTo(t,i),s.lineTo(e,n),s.stroke()):u.push(t,i,e,n),g+=l,v=g<h)}function x(i,s,n){for(const o of i){if(s>=o)continue;const i=t[o];if((l||e.#l(n,i))&&(m(n.x,n.y,i.x,i.y),!v))break}}function y(i,s){for(const n of i){const i=t[n];if((l||e.#l(s,i))&&(m(s.x,s.y,i.x,i.y),!v))break}}for(let e=0;e<i;e++){g=0,v=!0;let s,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c);if((s=f.get(l+1))&&y(s,n),v&&((s=f.get(l+c))&&y(s,n),v&&((s=f.get(l+c+1))&&y(s,n),v&&((s=f.get(l+c-1))&&y(s,n),v)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;if(g=0,v=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c+1))&&y(s,n),v&&((s=f.get(l+c-1))&&y(s,n),v&&((s=f.get(l+1))&&y(s,n),v&&((s=f.get(l+c))&&y(s,n),v)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),++e>=i)break;g=0,v=!0,n=t[e],o=n.x*r|0,a=n.y*r|0,l=o+Math.imul(a,c),(s=f.get(l+c))&&y(s,n),v&&((s=f.get(l+1))&&y(s,n),v&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&x(s||[],e,n),v&&((s=f.get(l+c-1))&&y(s,n),v&&(s=f.get(l+c+1))&&y(s,n))))}}}if(u.length){s.globalAlpha=p,s.beginPath();for(let t=0;t<u.length;t+=4)s.moveTo(u[t],u[t+1]),s.lineTo(u[t+2],u[t+3]);s.stroke()}}#p(t){const i=this.ctx,{width:e,height:s}=this.canvas;i.save(),i.globalAlpha=.5,i.beginPath();for(let n=.5;n<=e;n+=t)i.moveTo(n,0),i.lineTo(n,s);for(let n=.5;n<=s;n+=t)i.moveTo(0,n),i.lineTo(e,n);i.stroke(),i.restore()}#d(){const t=this.ctx,i=this.particles,e=i.length;t.save(),t.globalAlpha=1,t.fillStyle="#fff",t.textAlign="center",t.textBaseline="middle";for(let s=0;s<e;s++){const e=i[s];t.fillText(String(s),e.x,e.y)}t.restore()}#u(){const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#o(i),this.#a(i),this.lastAnimationFrame=t}#i(){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.option.particles.drawLines&&this.#h(),this.option.debug.drawGrid&&this.#p(this.option.particles.connectDist),this.option.debug.drawIndexes&&this.#d()}#f(){this.isAnimating&&(requestAnimationFrame(()=>this.#f()),this.#u(),this.#i())}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#f())),!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}),preventExplosions:!!t.gravity?.preventExplosions},debug:{drawGrid:!!t.debug?.drawGrid,drawIndexes:!!t.debug?.drawIndexes}},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/index.d.ts
CHANGED
package/dist/types/options.d.ts
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2022–2026 Kyle Hoeckman, MIT License
|
|
2
2
|
// https://github.com/Khoeckman/canvasparticles-js/blob/main/LICENSE
|
|
3
3
|
|
|
4
|
-
import type { CanvasParticlesCanvas, Particle, GridPos, ContextColor,
|
|
4
|
+
import type { CanvasParticlesCanvas, Particle, GridPos, ContextColor, SpatialGrid } from './types'
|
|
5
5
|
import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './types/options'
|
|
6
6
|
|
|
7
7
|
const TWO_PI = 2 * Math.PI
|
|
@@ -43,9 +43,9 @@ export default class CanvasParticles {
|
|
|
43
43
|
|
|
44
44
|
/** Defines how the particles are auto-generated */
|
|
45
45
|
static readonly generationType = Object.freeze({
|
|
46
|
-
|
|
47
|
-
NEW: 1, // Generate particles from scratch
|
|
48
|
-
MATCH: 2, // Add or remove particles to match new count (default)
|
|
46
|
+
OFF: 0, // Never auto-generate particles
|
|
47
|
+
NEW: 1, // Generate all particles from scratch
|
|
48
|
+
MATCH: 2, // Add or remove some particles to match the new count (default)
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
@@ -211,7 +211,7 @@ export default class CanvasParticles {
|
|
|
211
211
|
|
|
212
212
|
const generationType = this.option.particles.generationType
|
|
213
213
|
|
|
214
|
-
if (generationType !== CanvasParticles.generationType.
|
|
214
|
+
if (generationType !== CanvasParticles.generationType.OFF) {
|
|
215
215
|
if (generationType === CanvasParticles.generationType.NEW || this.particles.length === 0) this.newParticles()
|
|
216
216
|
else if (generationType === CanvasParticles.generationType.MATCH) this.matchParticleCount({ updateBounds: true })
|
|
217
217
|
}
|
|
@@ -414,12 +414,13 @@ export default class CanvasParticles {
|
|
|
414
414
|
const offY = this.offY
|
|
415
415
|
const mouseX = this.mouseX
|
|
416
416
|
const mouseY = this.mouseY
|
|
417
|
-
const rotationSpeed = this.option.particles.rotationSpeed * step
|
|
418
|
-
const friction = this.option.gravity.friction
|
|
419
|
-
const mouseConnectDist = this.option.mouse.connectDist
|
|
420
|
-
const mouseDistRatio = this.option.mouse.distRatio
|
|
421
417
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE
|
|
422
418
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE
|
|
419
|
+
const mouseConnectDist = this.option.mouse.connectDist
|
|
420
|
+
const mouseDistRatio = this.option.mouse.distRatio
|
|
421
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step
|
|
422
|
+
const friction = this.option.gravity.friction
|
|
423
|
+
const preventExplosions = this.option.gravity.preventExplosions
|
|
423
424
|
const easing = 1 - Math.pow(3 / 4, step)
|
|
424
425
|
|
|
425
426
|
for (const p of this.particles) {
|
|
@@ -430,6 +431,17 @@ export default class CanvasParticles {
|
|
|
430
431
|
const movX = Math.sin(p.dir) * p.speed
|
|
431
432
|
const movY = Math.cos(p.dir) * p.speed
|
|
432
433
|
|
|
434
|
+
// Maximum velocity
|
|
435
|
+
if (preventExplosions) {
|
|
436
|
+
const maxVel = Math.max(p.speed, friction) * 2
|
|
437
|
+
|
|
438
|
+
if (p.velX > maxVel) p.velX = maxVel
|
|
439
|
+
if (p.velX < -maxVel) p.velX = -maxVel
|
|
440
|
+
|
|
441
|
+
if (p.velY > maxVel) p.velY = maxVel
|
|
442
|
+
if (p.velY < -maxVel) p.velY = -maxVel
|
|
443
|
+
}
|
|
444
|
+
|
|
433
445
|
// Apply velocities
|
|
434
446
|
p.posX += (movX + p.velX) * step
|
|
435
447
|
p.posY += (movY + p.velY) * step
|
|
@@ -564,7 +576,7 @@ export default class CanvasParticles {
|
|
|
564
576
|
const alpha = this.color.alpha
|
|
565
577
|
const alphaFactor = this.color.alpha * maxDist
|
|
566
578
|
|
|
567
|
-
const bucket:
|
|
579
|
+
const bucket: number[] = [] // Batch line segments of max alpha (2D -> 1D; stride = 4)
|
|
568
580
|
const grid = this.#buildSpatialGrid(stride, invCellSize) // O(n^2) -> O(n)
|
|
569
581
|
|
|
570
582
|
let particleWork = 0
|
|
@@ -587,7 +599,7 @@ export default class CanvasParticles {
|
|
|
587
599
|
ctx.stroke()
|
|
588
600
|
} else {
|
|
589
601
|
// Cache lines with max alpha to later be drawn in one batch
|
|
590
|
-
bucket.push(
|
|
602
|
+
bucket.push(ax, ay, bx, by)
|
|
591
603
|
}
|
|
592
604
|
particleWork += distSq
|
|
593
605
|
allowWork = particleWork < maxWorkPerParticle
|
|
@@ -654,8 +666,7 @@ export default class CanvasParticles {
|
|
|
654
666
|
renderConnectionsToOwnCell(cell || [], a, pa)
|
|
655
667
|
|
|
656
668
|
// Next iteration
|
|
657
|
-
a
|
|
658
|
-
if (!(a < len)) break
|
|
669
|
+
if (++a >= len) break
|
|
659
670
|
|
|
660
671
|
// Same code inline but the order of grid.get() is different to remove maxWork artifacts
|
|
661
672
|
particleWork = 0
|
|
@@ -678,8 +689,7 @@ export default class CanvasParticles {
|
|
|
678
689
|
renderConnectionsToOwnCell(cell || [], a, pa)
|
|
679
690
|
|
|
680
691
|
// Next iteration
|
|
681
|
-
a
|
|
682
|
-
if (!(a < len)) break
|
|
692
|
+
if (++a >= len) break
|
|
683
693
|
|
|
684
694
|
// Same code inline but the order of grid.get() is different to remove maxWork artifacts
|
|
685
695
|
particleWork = 0
|
|
@@ -708,10 +718,9 @@ export default class CanvasParticles {
|
|
|
708
718
|
ctx.globalAlpha = alpha
|
|
709
719
|
ctx.beginPath()
|
|
710
720
|
|
|
711
|
-
for (let
|
|
712
|
-
|
|
713
|
-
ctx.
|
|
714
|
-
ctx.lineTo(line[2], line[3])
|
|
721
|
+
for (let line = 0; line < bucket.length; line += 4) {
|
|
722
|
+
ctx.moveTo(bucket[line], bucket[line + 1])
|
|
723
|
+
ctx.lineTo(bucket[line + 2], bucket[line + 3])
|
|
715
724
|
}
|
|
716
725
|
ctx.stroke()
|
|
717
726
|
}
|
|
@@ -881,6 +890,7 @@ export default class CanvasParticles {
|
|
|
881
890
|
repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
|
|
882
891
|
pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
|
|
883
892
|
friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
|
|
893
|
+
preventExplosions: !!options.gravity?.preventExplosions,
|
|
884
894
|
},
|
|
885
895
|
debug: {
|
|
886
896
|
drawGrid: !!options.debug?.drawGrid,
|
package/src/types/index.ts
CHANGED