canvasparticles-js 4.4.3 → 4.4.5
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 +38 -26
- package/dist/index.cjs +23 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +23 -10
- package/dist/index.umd.js +1 -1
- package/dist/types/options.d.ts +1 -0
- package/package.json +1 -1
- package/src/index.ts +20 -9
- package/src/types/options.ts +1 -0
package/README.md
CHANGED
|
@@ -175,19 +175,24 @@ 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
|
-
|
|
178
|
+
The default value will be used when an option is assigned an invalid value.
|
|
179
|
+
|
|
180
|
+
**TypeScript Usage**:
|
|
181
|
+
Both `float` and `integer` mean `number`.<br>
|
|
182
|
+
`integer` is used to specify that the number is internally floored to a whole number.
|
|
180
183
|
|
|
181
184
|
### Root options
|
|
182
185
|
|
|
183
|
-
| Option | Type | Default | Description
|
|
184
|
-
| ------------ | ----------------- | ------- |
|
|
185
|
-
| `background` | `string \| false` | `false` | Canvas background. Any valid CSS `background` value. Ignored
|
|
186
|
+
| Option | Type | Default | Description |
|
|
187
|
+
| ------------ | ----------------- | ------- | ----------------------------------------------------------------------------------- |
|
|
188
|
+
| `background` | `string \| false` | `false` | Canvas background. Any valid CSS `background` value. Ignored when strictly `false`. |
|
|
186
189
|
|
|
187
190
|
---
|
|
188
191
|
|
|
189
192
|
### `animation`
|
|
190
193
|
|
|
194
|
+
It's best to not touch these values if it's unclear what it does.
|
|
195
|
+
|
|
191
196
|
| Option | Type | Default | Description |
|
|
192
197
|
| ------------------------ | --------- | ------- | ---------------------------------------------------- |
|
|
193
198
|
| `animation.startOnEnter` | `boolean` | `true` | Start animation when the canvas enters the viewport. |
|
|
@@ -207,24 +212,30 @@ Your screen resolution and refresh rate will directly impact perfomance!
|
|
|
207
212
|
|
|
208
213
|
- `NONE (0)` – No interaction
|
|
209
214
|
- `SHIFT (1)` – Visual displacement only
|
|
210
|
-
- `MOVE (2)` – Actual particle movement
|
|
215
|
+
- `MOVE (2)` – Actual particle movement (default)
|
|
211
216
|
|
|
212
217
|
---
|
|
213
218
|
|
|
214
219
|
### `particles`
|
|
215
220
|
|
|
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.
|
|
221
|
+
| Option | Type | Default | Description |
|
|
222
|
+
| --------------------------- | ------------- | ---------- | ------------------------------------------------------------------------------------------------- |
|
|
223
|
+
| `particles.generationType` | `0 \| 1 \| 2` | `false` | Auto-generate particles on initialization and when the canvas resizes. `1 = OFF`, `2 = MATCH` |
|
|
224
|
+
| `particles.color` | `string` | `'black'` | Particle and connection color. Any CSS color format. |
|
|
225
|
+
| `particles.ppm` | `integer` | `100` | Particles per million pixels. |
|
|
226
|
+
| `particles.max` | `integer` | `Infinity` | Maximum number of particles allowed. |
|
|
227
|
+
| `particles.maxWork` | `integer` | `Infinity` | Maximum total connection length per particle. Lower values stabilize performance but may flicker. |
|
|
228
|
+
| `particles.connectDistance` | `integer` | `150` | Maximum distance for particle connections (px). |
|
|
229
|
+
| `particles.relSpeed` | `float` | `1` | Relative particle speed multiplier. |
|
|
230
|
+
| `particles.relSize` | `float` | `1` | Relative particle size multiplier. |
|
|
231
|
+
| `particles.rotationSpeed` | `float` | `2` | Direction change speed. |
|
|
232
|
+
| `particles.drawLines` | `boolean` | `true` | Whether to draw lines between particles. |
|
|
233
|
+
|
|
234
|
+
**Generation types** (enum)
|
|
235
|
+
|
|
236
|
+
- `OFF (0)` – Never auto-generate particles
|
|
237
|
+
- `NEW (1)` – Generate all particles from scratch
|
|
238
|
+
- `MATCH (2)` – Add or remove some particles to match the new count (default)
|
|
228
239
|
|
|
229
240
|
---
|
|
230
241
|
|
|
@@ -232,11 +243,12 @@ Your screen resolution and refresh rate will directly impact perfomance!
|
|
|
232
243
|
|
|
233
244
|
Enabling gravity (`repulsive` or `pulling` > 0) performs an extra **O(n²)** gravity computations per frame.
|
|
234
245
|
|
|
235
|
-
| Option
|
|
236
|
-
|
|
|
237
|
-
| `gravity.repulsive`
|
|
238
|
-
| `gravity.pulling`
|
|
239
|
-
| `gravity.friction`
|
|
246
|
+
| Option | Type | Default | Description |
|
|
247
|
+
| --------------------- | ------- | ---------- | ---------------------------------------------------------------------------------------------------- |
|
|
248
|
+
| `gravity.repulsive` | `float` | `0` | Repulsive force between particles. |
|
|
249
|
+
| `gravity.pulling` | `float` | `0` | Attractive force between particles. Requires sufficient repulsion to avoid clustering. |
|
|
250
|
+
| `gravity.friction` | `float` | `0.8` | Damping factor applied to gravitational velocity each update (`0.0 – 1.0`). |
|
|
251
|
+
| `gravity.maxVelocity` | `float` | `Infinity` | Clamp the velocity of each particle to this value to prevent explosions under heavy pulling gravity. |
|
|
240
252
|
|
|
241
253
|
---
|
|
242
254
|
|
|
@@ -342,13 +354,13 @@ instance.options = { ... }
|
|
|
342
354
|
createParticle(posX?: number, posY?: number, dir?: number, speed?: number, size?: number)
|
|
343
355
|
```
|
|
344
356
|
|
|
345
|
-
By default `particles.ppm` and `particles.max` are used to auto-generate random particles.
|
|
357
|
+
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
358
|
|
|
347
359
|
```js
|
|
348
360
|
const canvas = '#my-canvas'
|
|
349
361
|
const options = {
|
|
350
362
|
particles: {
|
|
351
|
-
|
|
363
|
+
generationType: CanvasParticles.generationType.OFF, // = 0
|
|
352
364
|
rotationSpeed: 0,
|
|
353
365
|
},
|
|
354
366
|
}
|
|
@@ -393,7 +405,7 @@ instance.newParticles({ keepAuto: true, keepManual: false })
|
|
|
393
405
|
const options = {
|
|
394
406
|
background: 'hsl(125, 42%, 35%)',
|
|
395
407
|
mouse: {
|
|
396
|
-
interactionType: CanvasParticles.interactionType.
|
|
408
|
+
interactionType: CanvasParticles.interactionType.SHIFT, // = 1
|
|
397
409
|
},
|
|
398
410
|
particles: {
|
|
399
411
|
color: 'rgba(150, 255, 105, 0.95)',
|
package/dist/index.cjs
CHANGED
|
@@ -21,8 +21,8 @@ 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.
|
|
25
|
-
static MAX_DT = 1000 /
|
|
24
|
+
static version = "4.4.5";
|
|
25
|
+
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 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({
|
|
@@ -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 maxVel = this.option.gravity.maxVelocity;
|
|
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,17 @@ 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 (maxVel > 0) {
|
|
362
|
+
if (p.velX > maxVel)
|
|
363
|
+
p.velX = maxVel;
|
|
364
|
+
if (p.velX < -maxVel)
|
|
365
|
+
p.velX = -maxVel;
|
|
366
|
+
if (p.velY > maxVel)
|
|
367
|
+
p.velY = maxVel;
|
|
368
|
+
if (p.velY < -maxVel)
|
|
369
|
+
p.velY = -maxVel;
|
|
370
|
+
}
|
|
359
371
|
// Apply velocities
|
|
360
372
|
p.posX += (movX + p.velX) * step;
|
|
361
373
|
p.posY += (movY + p.velY) * step;
|
|
@@ -761,6 +773,7 @@ class CanvasParticles {
|
|
|
761
773
|
repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
|
|
762
774
|
pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
|
|
763
775
|
friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
|
|
776
|
+
maxVelocity: pno('gravity.maxVelocity', options.gravity?.maxVelocity, Infinity, { min: 0 }),
|
|
764
777
|
},
|
|
765
778
|
debug: {
|
|
766
779
|
drawGrid: !!options.debug?.drawGrid,
|
package/dist/index.d.ts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -19,8 +19,8 @@ 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.
|
|
23
|
-
static MAX_DT = 1000 /
|
|
22
|
+
static version = "4.4.5";
|
|
23
|
+
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 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({
|
|
@@ -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 maxVel = this.option.gravity.maxVelocity;
|
|
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,17 @@ 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 (maxVel > 0) {
|
|
360
|
+
if (p.velX > maxVel)
|
|
361
|
+
p.velX = maxVel;
|
|
362
|
+
if (p.velX < -maxVel)
|
|
363
|
+
p.velX = -maxVel;
|
|
364
|
+
if (p.velY > maxVel)
|
|
365
|
+
p.velY = maxVel;
|
|
366
|
+
if (p.velY < -maxVel)
|
|
367
|
+
p.velY = -maxVel;
|
|
368
|
+
}
|
|
357
369
|
// Apply velocities
|
|
358
370
|
p.posX += (movX + p.velX) * step;
|
|
359
371
|
p.posY += (movY + p.velY) * step;
|
|
@@ -759,6 +771,7 @@ class CanvasParticles {
|
|
|
759
771
|
repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
|
|
760
772
|
pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
|
|
761
773
|
friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
|
|
774
|
+
maxVelocity: pno('gravity.maxVelocity', options.gravity?.maxVelocity, Infinity, { min: 0 }),
|
|
762
775
|
},
|
|
763
776
|
debug: {
|
|
764
777
|
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.3";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>=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>=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+=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})},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.5";static MAX_DT=1e3/30;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,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.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.maxVelocity,m=1-Math.pow(3/4,i);for(const e of this.particles){e.dir+=2*(Math.random()-.5)*u*i,e.dir%=t;const v=Math.sin(e.dir)*e.speed,y=Math.cos(e.dir)*e.speed;g>0&&(e.velX>g&&(e.velX=g),e.velX<-g&&(e.velX=-g),e.velY>g&&(e.velY=g),e.velY<-g&&(e.velY=-g)),e.posX+=(v+e.velX)*i,e.posY+=(y+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 x=e.posX+o-r,M=e.posY+a-c;if(!l){const t=p/Math.hypot(x,M);d<t?(e.offX+=(t*x-x-e.offX)*m,e.offY+=(t*M-M-e.offY)*m):(e.offX-=e.offX*m,e.offY-=e.offY*m)}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,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 y(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 x(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))&&x(s,n),m&&((s=f.get(l+c))&&x(s,n),m&&((s=f.get(l+c+1))&&x(s,n),m&&((s=f.get(l+c-1))&&x(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&y(s||[],e,n),++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))&&x(s,n),m&&((s=f.get(l+c-1))&&x(s,n),m&&((s=f.get(l+1))&&x(s,n),m&&((s=f.get(l+c))&&x(s,n),m)))){if(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&y(s||[],e,n),++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))&&x(s,n),m&&((s=f.get(l+1))&&x(s,n),m&&(o>=0&&a>=0&&o<c-2&&(s=f.get(l))&&y(s||[],e,n),m&&((s=f.get(l+c-1))&&x(s,n),m&&(s=f.get(l+c+1))&&x(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}),maxVelocity:i("gravity.maxVelocity",t.gravity?.maxVelocity,1/0,{min:0})},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/options.d.ts
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ declare const __VERSION__: string
|
|
|
31
31
|
export default class CanvasParticles {
|
|
32
32
|
static readonly version = __VERSION__
|
|
33
33
|
|
|
34
|
-
private static readonly MAX_DT = 1000 /
|
|
34
|
+
private static readonly MAX_DT = 1000 / 30 // milliseconds between updates @ 30 FPS
|
|
35
35
|
private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
|
|
36
36
|
|
|
37
37
|
/** Defines mouse interaction types with the particles */
|
|
@@ -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 maxVel = this.option.gravity.maxVelocity
|
|
423
424
|
const easing = 1 - Math.pow(3 / 4, step)
|
|
424
425
|
|
|
425
426
|
for (const p of this.particles) {
|
|
@@ -430,6 +431,15 @@ 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 (maxVel > 0) {
|
|
436
|
+
if (p.velX > maxVel) p.velX = maxVel
|
|
437
|
+
if (p.velX < -maxVel) p.velX = -maxVel
|
|
438
|
+
|
|
439
|
+
if (p.velY > maxVel) p.velY = maxVel
|
|
440
|
+
if (p.velY < -maxVel) p.velY = -maxVel
|
|
441
|
+
}
|
|
442
|
+
|
|
433
443
|
// Apply velocities
|
|
434
444
|
p.posX += (movX + p.velX) * step
|
|
435
445
|
p.posY += (movY + p.velY) * step
|
|
@@ -878,6 +888,7 @@ export default class CanvasParticles {
|
|
|
878
888
|
repulsive: pno('gravity.repulsive', options.gravity?.repulsive, 0, { min: 0 }),
|
|
879
889
|
pulling: pno('gravity.pulling', options.gravity?.pulling, 0, { min: 0 }),
|
|
880
890
|
friction: pno('gravity.friction', options.gravity?.friction, 0.8, { min: 0, max: 1 }),
|
|
891
|
+
maxVelocity: pno('gravity.maxVelocity', options.gravity?.maxVelocity, Infinity, { min: 0 }),
|
|
881
892
|
},
|
|
882
893
|
debug: {
|
|
883
894
|
drawGrid: !!options.debug?.drawGrid,
|