canvasparticles-js 4.1.5 → 4.1.6
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/dist/index.cjs +21 -20
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +21 -20
- package/dist/index.umd.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +24 -23
package/dist/index.cjs
CHANGED
|
@@ -21,7 +21,9 @@ 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.1.
|
|
24
|
+
static version = "4.1.6";
|
|
25
|
+
static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
|
|
26
|
+
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
25
27
|
/** Defines mouse interaction types with the particles */
|
|
26
28
|
static interactionType = Object.freeze({
|
|
27
29
|
NONE: 0, // No mouse interaction
|
|
@@ -57,10 +59,8 @@ class CanvasParticles {
|
|
|
57
59
|
canvas.instance.resizeCanvas();
|
|
58
60
|
}
|
|
59
61
|
});
|
|
60
|
-
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
|
|
61
|
-
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
62
62
|
/** Helper functions for options parsing */
|
|
63
|
-
static defaultIfNaN = (value, defaultValue) =>
|
|
63
|
+
static defaultIfNaN = (value, defaultValue) => isNaN(+value) ? defaultValue : +value;
|
|
64
64
|
static parseNumericOption = (name, value, defaultValue, clamp) => {
|
|
65
65
|
if (value == undefined)
|
|
66
66
|
return defaultValue;
|
|
@@ -68,7 +68,7 @@ class CanvasParticles {
|
|
|
68
68
|
if (isFinite(min) && value < min) {
|
|
69
69
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
70
70
|
}
|
|
71
|
-
if (isFinite(max) && value > max) {
|
|
71
|
+
else if (isFinite(max) && value > max) {
|
|
72
72
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
73
73
|
}
|
|
74
74
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -237,11 +237,11 @@ class CanvasParticles {
|
|
|
237
237
|
return;
|
|
238
238
|
const len = this.particleCount;
|
|
239
239
|
const particles = this.particles;
|
|
240
|
-
const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive;
|
|
241
|
-
const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling;
|
|
240
|
+
const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step;
|
|
241
|
+
const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step;
|
|
242
242
|
const maxRepulsiveDist = this.option.particles.connectDist / 2;
|
|
243
243
|
const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
|
|
244
|
-
const maxGrav = this.option.particles.connectDist * 0.1;
|
|
244
|
+
const maxGrav = this.option.particles.connectDist * 0.1 * step;
|
|
245
245
|
for (let i = 0; i < len; i++) {
|
|
246
246
|
const particleA = particles[i];
|
|
247
247
|
for (let j = i + 1; j < len; j++) {
|
|
@@ -256,7 +256,7 @@ class CanvasParticles {
|
|
|
256
256
|
if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
|
|
257
257
|
continue;
|
|
258
258
|
angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
|
|
259
|
-
grav = (1 / Math.sqrt(distSq)
|
|
259
|
+
grav = Math.pow(1 / Math.sqrt(distSq), 1.8);
|
|
260
260
|
const angleX = Math.cos(angle);
|
|
261
261
|
const angleY = Math.sin(angle);
|
|
262
262
|
if (distSq < maxRepulsiveDistSq) {
|
|
@@ -290,22 +290,23 @@ class CanvasParticles {
|
|
|
290
290
|
const offY = this.offY;
|
|
291
291
|
const mouseX = this.mouseX;
|
|
292
292
|
const mouseY = this.mouseY;
|
|
293
|
-
const rotationSpeed = this.option.particles.rotationSpeed;
|
|
293
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
294
294
|
const friction = this.option.gravity.friction;
|
|
295
295
|
const mouseConnectDist = this.option.mouse.connectDist;
|
|
296
296
|
const mouseDistRatio = this.option.mouse.distRatio;
|
|
297
297
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
|
|
298
298
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
|
|
299
|
+
const easing = 1 - Math.pow(1 - 1 / 4, step);
|
|
299
300
|
for (let i = 0; i < len; i++) {
|
|
300
301
|
const particle = particles[i];
|
|
301
|
-
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed;
|
|
302
|
+
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
|
|
302
303
|
particle.dir %= TWO_PI;
|
|
303
304
|
// Constant velocity
|
|
304
305
|
const movX = Math.sin(particle.dir) * particle.speed;
|
|
305
306
|
const movY = Math.cos(particle.dir) * particle.speed;
|
|
306
307
|
// Apply velocities
|
|
307
|
-
particle.posX += movX + particle.velX;
|
|
308
|
-
particle.posY += movY + particle.velY;
|
|
308
|
+
particle.posX += (movX + particle.velX) * step;
|
|
309
|
+
particle.posY += (movY + particle.velY) * step;
|
|
309
310
|
// Wrap particles around the canvas
|
|
310
311
|
particle.posX %= width;
|
|
311
312
|
if (particle.posX < 0)
|
|
@@ -314,8 +315,8 @@ class CanvasParticles {
|
|
|
314
315
|
if (particle.posY < 0)
|
|
315
316
|
particle.posY += height;
|
|
316
317
|
// Slightly decrease dynamic velocity
|
|
317
|
-
particle.velX *= friction;
|
|
318
|
-
particle.velY *= friction;
|
|
318
|
+
particle.velX *= Math.pow(friction, step);
|
|
319
|
+
particle.velY *= Math.pow(friction, step);
|
|
319
320
|
// Distance from mouse
|
|
320
321
|
const distX = particle.posX + offX - mouseX;
|
|
321
322
|
const distY = particle.posY + offY - mouseY;
|
|
@@ -323,12 +324,12 @@ class CanvasParticles {
|
|
|
323
324
|
if (!isMouseInteractionTypeNone) {
|
|
324
325
|
const distRatio = mouseConnectDist / Math.hypot(distX, distY);
|
|
325
326
|
if (mouseDistRatio < distRatio) {
|
|
326
|
-
particle.offX += (distRatio * distX - distX - particle.offX)
|
|
327
|
-
particle.offY += (distRatio * distY - distY - particle.offY)
|
|
327
|
+
particle.offX += (distRatio * distX - distX - particle.offX) * easing;
|
|
328
|
+
particle.offY += (distRatio * distY - distY - particle.offY) * easing;
|
|
328
329
|
}
|
|
329
330
|
else {
|
|
330
|
-
particle.offX -= particle.offX
|
|
331
|
-
particle.offY -= particle.offY
|
|
331
|
+
particle.offX -= particle.offX * easing;
|
|
332
|
+
particle.offY -= particle.offY * easing;
|
|
332
333
|
}
|
|
333
334
|
}
|
|
334
335
|
// Visually displace the particles
|
|
@@ -478,7 +479,7 @@ class CanvasParticles {
|
|
|
478
479
|
// - step = 1 → exactly one baseline update (dt === BASE_DT)
|
|
479
480
|
// - step > 1 → more time passed (lower FPS), advance further
|
|
480
481
|
// - step < 1 → less time passed (higher FPS), advance less
|
|
481
|
-
const step = CanvasParticles.BASE_DT
|
|
482
|
+
const step = dt / CanvasParticles.BASE_DT;
|
|
482
483
|
this.#updateGravity(step);
|
|
483
484
|
this.#updateParticles(step);
|
|
484
485
|
this.#render();
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { CanvasParticlesOptions, CanvasParticlesOptionsInput } from './type
|
|
|
3
3
|
export default class CanvasParticles {
|
|
4
4
|
#private;
|
|
5
5
|
static readonly version: string;
|
|
6
|
+
private static readonly MAX_DT;
|
|
7
|
+
private static readonly BASE_DT;
|
|
6
8
|
/** Defines mouse interaction types with the particles */
|
|
7
|
-
static interactionType: Readonly<{
|
|
9
|
+
static readonly interactionType: Readonly<{
|
|
8
10
|
NONE: 0;
|
|
9
11
|
SHIFT: 1;
|
|
10
12
|
MOVE: 2;
|
|
@@ -12,11 +14,9 @@ export default class CanvasParticles {
|
|
|
12
14
|
/** Observes canvas elements entering or leaving the viewport to start/stop animation */
|
|
13
15
|
static readonly canvasIntersectionObserver: IntersectionObserver;
|
|
14
16
|
static readonly canvasResizeObserver: ResizeObserver;
|
|
15
|
-
private static readonly MAX_DT;
|
|
16
|
-
private static readonly BASE_DT;
|
|
17
17
|
/** Helper functions for options parsing */
|
|
18
|
-
private static defaultIfNaN;
|
|
19
|
-
private static parseNumericOption;
|
|
18
|
+
private static readonly defaultIfNaN;
|
|
19
|
+
private static readonly parseNumericOption;
|
|
20
20
|
canvas: CanvasParticlesCanvas;
|
|
21
21
|
private ctx;
|
|
22
22
|
enableAnimating: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -19,7 +19,9 @@ 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.1.
|
|
22
|
+
static version = "4.1.6";
|
|
23
|
+
static MAX_DT = 1000 / 50; // milliseconds between updates @ 50 FPS
|
|
24
|
+
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
23
25
|
/** Defines mouse interaction types with the particles */
|
|
24
26
|
static interactionType = Object.freeze({
|
|
25
27
|
NONE: 0, // No mouse interaction
|
|
@@ -55,10 +57,8 @@ class CanvasParticles {
|
|
|
55
57
|
canvas.instance.resizeCanvas();
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
|
-
static MAX_DT = 1000 / 30; // milliseconds between updates @ 30 FPS
|
|
59
|
-
static BASE_DT = 1000 / 60; // milliseconds between updates @ 60 FPS
|
|
60
60
|
/** Helper functions for options parsing */
|
|
61
|
-
static defaultIfNaN = (value, defaultValue) =>
|
|
61
|
+
static defaultIfNaN = (value, defaultValue) => isNaN(+value) ? defaultValue : +value;
|
|
62
62
|
static parseNumericOption = (name, value, defaultValue, clamp) => {
|
|
63
63
|
if (value == undefined)
|
|
64
64
|
return defaultValue;
|
|
@@ -66,7 +66,7 @@ class CanvasParticles {
|
|
|
66
66
|
if (isFinite(min) && value < min) {
|
|
67
67
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`));
|
|
68
68
|
}
|
|
69
|
-
if (isFinite(max) && value > max) {
|
|
69
|
+
else if (isFinite(max) && value > max) {
|
|
70
70
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`));
|
|
71
71
|
}
|
|
72
72
|
return CanvasParticles.defaultIfNaN(Math.min(Math.max(value ?? defaultValue, min), max), defaultValue);
|
|
@@ -235,11 +235,11 @@ class CanvasParticles {
|
|
|
235
235
|
return;
|
|
236
236
|
const len = this.particleCount;
|
|
237
237
|
const particles = this.particles;
|
|
238
|
-
const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive;
|
|
239
|
-
const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling;
|
|
238
|
+
const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step;
|
|
239
|
+
const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step;
|
|
240
240
|
const maxRepulsiveDist = this.option.particles.connectDist / 2;
|
|
241
241
|
const maxRepulsiveDistSq = maxRepulsiveDist ** 2;
|
|
242
|
-
const maxGrav = this.option.particles.connectDist * 0.1;
|
|
242
|
+
const maxGrav = this.option.particles.connectDist * 0.1 * step;
|
|
243
243
|
for (let i = 0; i < len; i++) {
|
|
244
244
|
const particleA = particles[i];
|
|
245
245
|
for (let j = i + 1; j < len; j++) {
|
|
@@ -254,7 +254,7 @@ class CanvasParticles {
|
|
|
254
254
|
if (distSq >= maxRepulsiveDistSq && !isPullingEnabled)
|
|
255
255
|
continue;
|
|
256
256
|
angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX);
|
|
257
|
-
grav = (1 / Math.sqrt(distSq)
|
|
257
|
+
grav = Math.pow(1 / Math.sqrt(distSq), 1.8);
|
|
258
258
|
const angleX = Math.cos(angle);
|
|
259
259
|
const angleY = Math.sin(angle);
|
|
260
260
|
if (distSq < maxRepulsiveDistSq) {
|
|
@@ -288,22 +288,23 @@ class CanvasParticles {
|
|
|
288
288
|
const offY = this.offY;
|
|
289
289
|
const mouseX = this.mouseX;
|
|
290
290
|
const mouseY = this.mouseY;
|
|
291
|
-
const rotationSpeed = this.option.particles.rotationSpeed;
|
|
291
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step;
|
|
292
292
|
const friction = this.option.gravity.friction;
|
|
293
293
|
const mouseConnectDist = this.option.mouse.connectDist;
|
|
294
294
|
const mouseDistRatio = this.option.mouse.distRatio;
|
|
295
295
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE;
|
|
296
296
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE;
|
|
297
|
+
const easing = 1 - Math.pow(1 - 1 / 4, step);
|
|
297
298
|
for (let i = 0; i < len; i++) {
|
|
298
299
|
const particle = particles[i];
|
|
299
|
-
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed;
|
|
300
|
+
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step;
|
|
300
301
|
particle.dir %= TWO_PI;
|
|
301
302
|
// Constant velocity
|
|
302
303
|
const movX = Math.sin(particle.dir) * particle.speed;
|
|
303
304
|
const movY = Math.cos(particle.dir) * particle.speed;
|
|
304
305
|
// Apply velocities
|
|
305
|
-
particle.posX += movX + particle.velX;
|
|
306
|
-
particle.posY += movY + particle.velY;
|
|
306
|
+
particle.posX += (movX + particle.velX) * step;
|
|
307
|
+
particle.posY += (movY + particle.velY) * step;
|
|
307
308
|
// Wrap particles around the canvas
|
|
308
309
|
particle.posX %= width;
|
|
309
310
|
if (particle.posX < 0)
|
|
@@ -312,8 +313,8 @@ class CanvasParticles {
|
|
|
312
313
|
if (particle.posY < 0)
|
|
313
314
|
particle.posY += height;
|
|
314
315
|
// Slightly decrease dynamic velocity
|
|
315
|
-
particle.velX *= friction;
|
|
316
|
-
particle.velY *= friction;
|
|
316
|
+
particle.velX *= Math.pow(friction, step);
|
|
317
|
+
particle.velY *= Math.pow(friction, step);
|
|
317
318
|
// Distance from mouse
|
|
318
319
|
const distX = particle.posX + offX - mouseX;
|
|
319
320
|
const distY = particle.posY + offY - mouseY;
|
|
@@ -321,12 +322,12 @@ class CanvasParticles {
|
|
|
321
322
|
if (!isMouseInteractionTypeNone) {
|
|
322
323
|
const distRatio = mouseConnectDist / Math.hypot(distX, distY);
|
|
323
324
|
if (mouseDistRatio < distRatio) {
|
|
324
|
-
particle.offX += (distRatio * distX - distX - particle.offX)
|
|
325
|
-
particle.offY += (distRatio * distY - distY - particle.offY)
|
|
325
|
+
particle.offX += (distRatio * distX - distX - particle.offX) * easing;
|
|
326
|
+
particle.offY += (distRatio * distY - distY - particle.offY) * easing;
|
|
326
327
|
}
|
|
327
328
|
else {
|
|
328
|
-
particle.offX -= particle.offX
|
|
329
|
-
particle.offY -= particle.offY
|
|
329
|
+
particle.offX -= particle.offX * easing;
|
|
330
|
+
particle.offY -= particle.offY * easing;
|
|
330
331
|
}
|
|
331
332
|
}
|
|
332
333
|
// Visually displace the particles
|
|
@@ -476,7 +477,7 @@ class CanvasParticles {
|
|
|
476
477
|
// - step = 1 → exactly one baseline update (dt === BASE_DT)
|
|
477
478
|
// - step > 1 → more time passed (lower FPS), advance further
|
|
478
479
|
// - step < 1 → less time passed (higher FPS), advance less
|
|
479
|
-
const step = CanvasParticles.BASE_DT
|
|
480
|
+
const step = dt / CanvasParticles.BASE_DT;
|
|
480
481
|
this.#updateGravity(step);
|
|
481
482
|
this.#updateParticles(step);
|
|
482
483
|
this.#render();
|
package/dist/index.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.1.5";static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static MAX_DT=1e3/30;static BASE_DT=1e3/60;static defaultIfNaN=(t,i)=>isNaN(+t)?i:+t;static parseNumericOption=(t,i,s,n)=>{if(null==i)return s;const{min:o=-1/0,max:a=1/0}=n??{};return isFinite(o)&&i<o&&console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)),isFinite(a)&&i>a&&console.warn(new RangeError(`option.${t} was clamped to ${a} as ${i} is too high`)),e.defaultIfNaN(Math.min(Math.max(i??s,o),a),s)};canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];particleCount=0;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("particleCount must be finite")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#e(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particleCount,n=this.particles,o=this.option.particles.connectDist*this.option.gravity.repulsive,a=this.option.particles.connectDist*this.option.gravity.pulling,r=(this.option.particles.connectDist/2)**2,c=.1*this.option.particles.connectDist;for(let t=0;t<s;t++){const i=n[t];for(let h=t+1;h<s;h++){const t=n[h],s=i.posX-t.posX,l=i.posY-t.posY,p=s*s+l*l;let u,d,f;if(p>=r&&!e)continue;u=Math.atan2(t.posY-i.posY,t.posX-i.posX),d=(1/Math.sqrt(p))**1.8;const m=Math.cos(u),v=Math.sin(u);if(p<r){f=Math.min(c,d*o);const e=m*f,s=v*f;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;f=Math.min(c,d*a);const g=m*f,y=v*f;i.velX+=g,i.velY+=y,t.velX-=g,t.velY-=y}}}#n(i){const s=this.particleCount,n=this.particles,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed,u=this.option.gravity.friction,d=this.option.mouse.connectDist,f=this.option.mouse.distRatio,m=this.option.mouse.interactionType===e.interactionType.NONE,v=this.option.mouse.interactionType===e.interactionType.MOVE;for(let i=0;i<s;i++){const e=n[i];e.dir+=2*(Math.random()-.5)*p,e.dir%=t;const s=Math.sin(e.dir)*e.speed,g=Math.cos(e.dir)*e.speed;e.posX+=s+e.velX,e.posY+=g+e.velY,e.posX%=o,e.posX<0&&(e.posX+=o),e.posY%=a,e.posY<0&&(e.posY+=a),e.velX*=u,e.velY*=u;const y=e.posX+r-h,x=e.posY+c-l;if(!m){const t=d/Math.hypot(y,x);f<t?(e.offX+=(t*y-y-e.offX)/4,e.offY+=(t*x-x-e.offY)/4):(e.offX-=e.offX/4,e.offY-=e.offY/4)}e.x=e.posX+e.offX,e.y=e.posY+e.offY,v&&(e.posX=e.x,e.posY=e.y),e.x+=r,e.y+=c,this.#o(e),e.isVisible=1===e.gridPos.x&&1===e.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particleCount,e=this.particles,s=this.ctx;for(let n=0;n<i;n++){const i=e[n];i.isVisible&&(i.size>1?(s.beginPath(),s.arc(i.x,i.y,i.size,0,t),s.fill(),s.closePath()):s.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}}#c(){const t=this.particleCount,i=this.particles,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<t;s++){const p=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!a&&!this.#a(p,t))continue;const s=p.x-t.x,f=p.y-t.y,m=s*s+f*f;if(!(m>n)&&(m>o?(e.globalAlpha=h/Math.sqrt(m)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(t.x,t.y),e.stroke()):l.push([p.x,p.y,t.x,t.y]),(u+=m)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT),s=e.BASE_DT/i;this.#s(s),this.#n(s),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:i("mouse.interactionType",t.mouse?.interactionType,1),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i("particles.ppm",t.particles?.ppm,100),max:i("particles.max",t.particles?.max,1/0),maxWork:i("particles.maxWork",t.particles?.maxWork,1/0,{min:0}),connectDist:i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:1}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0),pulling:i("gravity.pulling",t.gravity?.pulling,0),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
|
|
1
|
+
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(t="undefined"!=typeof globalThis?globalThis:t||self).CanvasParticles=i()}(this,function(){"use strict";const t=2*Math.PI;const i=function(t){let i=t>>>0;return{next(){let t=i+1831565813|0;return i=t,t=Math.imul(t^t>>>15,1|t),t^=t+Math.imul(t^t>>>7,61|t),((t^t>>>14)>>>0)/4294967296}}}(Math.random()*2**32).next;class e{static version="4.1.6";static MAX_DT=20;static BASE_DT=1e3/60;static interactionType=Object.freeze({NONE:0,SHIFT:1,MOVE:2});static canvasIntersectionObserver=new IntersectionObserver(t=>{for(let i=0;i<t.length;i++){const e=t[i],s=e.target,n=s.instance;if(!n.options?.animation)return;(s.inViewbox=e.isIntersecting)?n.options.animation?.startOnEnter&&n.start({auto:!0}):n.options.animation?.stopOnLeave&&n.stop({auto:!0,clear:!1})}});static canvasResizeObserver=new ResizeObserver(t=>{for(let i=0;i<t.length;i++){t[i].target.instance.updateCanvasRect()}for(let i=0;i<t.length;i++){t[i].target.instance.resizeCanvas()}});static defaultIfNaN=(t,i)=>isNaN(+t)?i:+t;static parseNumericOption=(t,i,s,n)=>{if(null==i)return s;const{min:o=-1/0,max:a=1/0}=n??{};return isFinite(o)&&i<o?console.warn(new RangeError(`option.${t} was clamped to ${o} as ${i} is too low`)):isFinite(a)&&i>a&&console.warn(new RangeError(`option.${t} was clamped to ${a} as ${i} is too high`)),e.defaultIfNaN(Math.min(Math.max(i??s,o),a),s)};canvas;ctx;enableAnimating=!1;isAnimating=!1;lastAnimationFrame=0;particles=[];particleCount=0;clientX=1/0;clientY=1/0;mouseX=1/0;mouseY=1/0;width;height;offX;offY;option;color;constructor(t,i={}){let s;if(t instanceof HTMLCanvasElement)s=t;else{if("string"!=typeof t)throw new TypeError("selector is not a string and neither a HTMLCanvasElement itself");if(s=document.querySelector(t),!(s instanceof HTMLCanvasElement))throw new Error("selector does not point to a canvas")}this.canvas=s,this.canvas.instance=this,this.canvas.inViewbox=!0;const n=this.canvas.getContext("2d");if(!n)throw new Error("failed to get 2D context from canvas");this.ctx=n,this.options=i,e.canvasIntersectionObserver.observe(this.canvas),e.canvasResizeObserver.observe(this.canvas),this.resizeCanvas=this.resizeCanvas.bind(this),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleScroll=this.handleScroll.bind(this),this.updateCanvasRect(),this.resizeCanvas(),window.addEventListener("mousemove",this.handleMouseMove,{passive:!0}),window.addEventListener("scroll",this.handleScroll,{passive:!0})}updateCanvasRect(){const{top:t,left:i,width:e,height:s}=this.canvas.getBoundingClientRect();this.canvas.rect={top:t,left:i,width:e,height:s}}handleMouseMove(t){this.enableAnimating&&(this.clientX=t.clientX,this.clientY=t.clientY,this.isAnimating&&this.updateMousePos())}handleScroll(){this.enableAnimating&&(this.updateCanvasRect(),this.isAnimating&&this.updateMousePos())}updateMousePos(){const{top:t,left:i}=this.canvas.rect;this.mouseX=this.clientX-i,this.mouseY=this.clientY-t}resizeCanvas(){const t=this.canvas.width=this.canvas.rect.width,i=this.canvas.height=this.canvas.rect.height;this.mouseX=1/0,this.mouseY=1/0,this.width=Math.max(t+2*this.option.particles.connectDist,1),this.height=Math.max(i+2*this.option.particles.connectDist,1),this.offX=(t-this.width)/2,this.offY=(i-this.height)/2,this.option.particles.regenerateOnResize||0===this.particles.length?this.newParticles():this.matchParticleCount({updateBounds:!0}),this.isAnimating&&this.#t()}#i(){const t=this.option.particles.ppm*this.width*this.height/1e6|0;if(this.particleCount=Math.min(this.option.particles.max,t),!isFinite(this.particleCount))throw new RangeError("particleCount must be finite")}newParticles(){this.#i(),this.particles=[];for(let t=0;t<this.particleCount;t++)this.createParticle()}matchParticleCount({updateBounds:t=!1}={}){for(this.#i(),this.particles=this.particles.slice(0,this.particleCount),t&&this.particles.forEach(t=>this.#e(t));this.particleCount>this.particles.length;)this.createParticle()}createParticle(e,s,n,o,a){const r={posX:e="number"==typeof e?e-this.offX:i()*this.width,posY:s="number"==typeof s?s-this.offY:i()*this.height,x:e,y:s,velX:0,velY:0,offX:0,offY:0,dir:n||i()*t,speed:o||(.5+.5*i())*this.option.particles.relSpeed,size:a||(.5+i()**5*2)*this.option.particles.relSize,gridPos:{x:1,y:1},isVisible:!1};this.#e(r),this.particles.push(r)}#e(t){t.bounds={top:-t.size,right:this.canvas.width+t.size,bottom:this.canvas.height+t.size,left:-t.size}}#s(t){const i=this.option.gravity.repulsive>0,e=this.option.gravity.pulling>0;if(!i&&!e)return;const s=this.particleCount,n=this.particles,o=this.option.particles.connectDist*this.option.gravity.repulsive*t,a=this.option.particles.connectDist*this.option.gravity.pulling*t,r=(this.option.particles.connectDist/2)**2,c=.1*this.option.particles.connectDist*t;for(let t=0;t<s;t++){const i=n[t];for(let h=t+1;h<s;h++){const t=n[h],s=i.posX-t.posX,l=i.posY-t.posY,p=s*s+l*l;let u,d,f;if(p>=r&&!e)continue;u=Math.atan2(t.posY-i.posY,t.posX-i.posX),d=Math.pow(1/Math.sqrt(p),1.8);const m=Math.cos(u),v=Math.sin(u);if(p<r){f=Math.min(c,d*o);const e=m*f,s=v*f;i.velX-=e,i.velY-=s,t.velX+=e,t.velY+=s}if(!e)continue;f=Math.min(c,d*a);const g=m*f,y=v*f;i.velX+=g,i.velY+=y,t.velX-=g,t.velY-=y}}}#n(i){const s=this.particleCount,n=this.particles,o=this.width,a=this.height,r=this.offX,c=this.offY,h=this.mouseX,l=this.mouseY,p=this.option.particles.rotationSpeed*i,u=this.option.gravity.friction,d=this.option.mouse.connectDist,f=this.option.mouse.distRatio,m=this.option.mouse.interactionType===e.interactionType.NONE,v=this.option.mouse.interactionType===e.interactionType.MOVE,g=1-Math.pow(.75,i);for(let e=0;e<s;e++){const s=n[e];s.dir+=2*(Math.random()-.5)*p*i,s.dir%=t;const y=Math.sin(s.dir)*s.speed,x=Math.cos(s.dir)*s.speed;s.posX+=(y+s.velX)*i,s.posY+=(x+s.velY)*i,s.posX%=o,s.posX<0&&(s.posX+=o),s.posY%=a,s.posY<0&&(s.posY+=a),s.velX*=Math.pow(u,i),s.velY*=Math.pow(u,i);const b=s.posX+r-h,M=s.posY+c-l;if(!m){const t=d/Math.hypot(b,M);f<t?(s.offX+=(t*b-b-s.offX)*g,s.offY+=(t*M-M-s.offY)*g):(s.offX-=s.offX*g,s.offY-=s.offY*g)}s.x=s.posX+s.offX,s.y=s.posY+s.offY,v&&(s.posX=s.x,s.posY=s.y),s.x+=r,s.y+=c,this.#o(s),s.isVisible=1===s.gridPos.x&&1===s.gridPos.y}}#o(t){t.gridPos.x=+(t.x>=t.bounds.left)+ +(t.x>t.bounds.right),t.gridPos.y=+(t.y>=t.bounds.top)+ +(t.y>t.bounds.bottom)}#a(t,i){return!(!t.isVisible&&!i.isVisible)||!(t.gridPos.x===i.gridPos.x&&1!==t.gridPos.x||t.gridPos.y===i.gridPos.y&&1!==t.gridPos.y)}#r(){const i=this.particleCount,e=this.particles,s=this.ctx;for(let n=0;n<i;n++){const i=e[n];i.isVisible&&(i.size>1?(s.beginPath(),s.arc(i.x,i.y,i.size,0,t),s.fill(),s.closePath()):s.fillRect(i.x-i.size,i.y-i.size,2*i.size,2*i.size))}}#c(){const t=this.particleCount,i=this.particles,e=this.ctx,s=this.option.particles.connectDist,n=s**2,o=(s/2)**2,a=s>=Math.min(this.canvas.width,this.canvas.height),r=n*this.option.particles.maxWork,c=this.color.alpha,h=this.color.alpha*s,l=[];for(let s=0;s<t;s++){const p=i[s];let u=0;for(let d=s+1;d<t;d++){const t=i[d];if(!a&&!this.#a(p,t))continue;const s=p.x-t.x,f=p.y-t.y,m=s*s+f*f;if(!(m>n)&&(m>o?(e.globalAlpha=h/Math.sqrt(m)-c,e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(t.x,t.y),e.stroke()):l.push([p.x,p.y,t.x,t.y]),(u+=m)>=r))break}}if(l.length){e.globalAlpha=c,e.beginPath();for(let t=0;t<l.length;t++){const i=l[t];e.moveTo(i[0],i[1]),e.lineTo(i[2],i[3])}e.stroke()}}#t(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.globalAlpha=this.color.alpha,this.ctx.fillStyle=this.color.hex,this.ctx.strokeStyle=this.color.hex,this.ctx.lineWidth=1,this.#r(),this.#c()}#h(){if(!this.isAnimating)return;requestAnimationFrame(()=>this.#h());const t=performance.now(),i=Math.min(t-this.lastAnimationFrame,e.MAX_DT)/e.BASE_DT;this.#s(i),this.#n(i),this.#t(),this.lastAnimationFrame=t}start({auto:t=!1}={}){return this.isAnimating||t&&!this.enableAnimating||(this.enableAnimating=!0,this.isAnimating=!0,this.updateCanvasRect(),requestAnimationFrame(()=>this.#h())),!this.canvas.inViewbox&&this.option.animation.startOnEnter&&(this.isAnimating=!1),this}stop({auto:t=!1,clear:i=!0}={}){return t||(this.enableAnimating=!1),this.isAnimating=!1,!1!==i&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),!0}destroy(){this.stop(),e.canvasIntersectionObserver.unobserve(this.canvas),e.canvasResizeObserver.unobserve(this.canvas),window.removeEventListener("mousemove",this.handleMouseMove),window.removeEventListener("scroll",this.handleScroll),this.canvas?.remove(),Object.keys(this).forEach(t=>delete this[t])}set options(t){const i=e.parseNumericOption;this.option={background:t.background??!1,animation:{startOnEnter:!!(t.animation?.startOnEnter??1),stopOnLeave:!!(t.animation?.stopOnLeave??1)},mouse:{interactionType:i("mouse.interactionType",t.mouse?.interactionType,1),connectDistMult:i("mouse.connectDistMult",t.mouse?.connectDistMult,2/3),connectDist:1,distRatio:i("mouse.distRatio",t.mouse?.distRatio,2/3)},particles:{regenerateOnResize:!!t.particles?.regenerateOnResize,color:t.particles?.color??"black",ppm:i("particles.ppm",t.particles?.ppm,100),max:i("particles.max",t.particles?.max,1/0),maxWork:i("particles.maxWork",t.particles?.maxWork,1/0,{min:0}),connectDist:i("particles.connectDistance",t.particles?.connectDistance,150,{min:1}),relSpeed:i("particles.relSpeed",t.particles?.relSpeed,1,{min:0}),relSize:i("particles.relSize",t.particles?.relSize,1,{min:1}),rotationSpeed:i("particles.rotationSpeed",t.particles?.rotationSpeed,2,{min:0})/100},gravity:{repulsive:i("gravity.repulsive",t.gravity?.repulsive,0),pulling:i("gravity.pulling",t.gravity?.pulling,0),friction:i("gravity.friction",t.gravity?.friction,.8,{min:0,max:1})}},this.setBackground(this.option.background),this.setMouseConnectDistMult(this.option.mouse.connectDistMult),this.setParticleColor(this.option.particles.color)}get options(){return this.option}setBackground(t){if(t){if("string"!=typeof t)throw new TypeError("background is not a string");this.canvas.style.background=this.option.background=t}}setMouseConnectDistMult(t){this.option.mouse.connectDist=this.option.particles.connectDist*(isNaN(t)?2/3:t)}setParticleColor(t){if(this.ctx.fillStyle=t,"#"===String(this.ctx.fillStyle)[0])this.color={hex:String(this.ctx.fillStyle),alpha:1};else{let t=String(this.ctx.fillStyle).split(",").at(-1);t=t?.slice(1,-1)??"1",this.ctx.fillStyle=String(this.ctx.fillStyle).split(",").slice(0,-1).join(",")+", 1)",this.color={hex:String(this.ctx.fillStyle),alpha:isNaN(+t)?1:+t}}}}return e});
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -31,8 +31,11 @@ declare const __VERSION__: string
|
|
|
31
31
|
export default class CanvasParticles {
|
|
32
32
|
static readonly version = __VERSION__
|
|
33
33
|
|
|
34
|
+
private static readonly MAX_DT = 1000 / 50 // milliseconds between updates @ 50 FPS
|
|
35
|
+
private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
|
|
36
|
+
|
|
34
37
|
/** Defines mouse interaction types with the particles */
|
|
35
|
-
static interactionType = Object.freeze({
|
|
38
|
+
static readonly interactionType = Object.freeze({
|
|
36
39
|
NONE: 0, // No mouse interaction
|
|
37
40
|
SHIFT: 1, // Visual displacement only
|
|
38
41
|
MOVE: 2, // Actual particle movement
|
|
@@ -71,12 +74,11 @@ export default class CanvasParticles {
|
|
|
71
74
|
}
|
|
72
75
|
})
|
|
73
76
|
|
|
74
|
-
private static readonly MAX_DT = 1000 / 30 // milliseconds between updates @ 30 FPS
|
|
75
|
-
private static readonly BASE_DT = 1000 / 60 // milliseconds between updates @ 60 FPS
|
|
76
77
|
/** Helper functions for options parsing */
|
|
77
|
-
private static defaultIfNaN = (value: number, defaultValue: number): number =>
|
|
78
|
+
private static readonly defaultIfNaN = (value: number, defaultValue: number): number =>
|
|
79
|
+
isNaN(+value) ? defaultValue : +value
|
|
78
80
|
|
|
79
|
-
private static parseNumericOption = (
|
|
81
|
+
private static readonly parseNumericOption = (
|
|
80
82
|
name: string,
|
|
81
83
|
value: number | undefined,
|
|
82
84
|
defaultValue: number,
|
|
@@ -88,9 +90,7 @@ export default class CanvasParticles {
|
|
|
88
90
|
|
|
89
91
|
if (isFinite(min) && value < min) {
|
|
90
92
|
console.warn(new RangeError(`option.${name} was clamped to ${min} as ${value} is too low`))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (isFinite(max) && value > max) {
|
|
93
|
+
} else if (isFinite(max) && value > max) {
|
|
94
94
|
console.warn(new RangeError(`option.${name} was clamped to ${max} as ${value} is too high`))
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -281,11 +281,11 @@ export default class CanvasParticles {
|
|
|
281
281
|
|
|
282
282
|
const len = this.particleCount
|
|
283
283
|
const particles = this.particles
|
|
284
|
-
const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive
|
|
285
|
-
const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling
|
|
284
|
+
const gravRepulsiveMult = this.option.particles.connectDist * this.option.gravity.repulsive * step
|
|
285
|
+
const gravPullingMult = this.option.particles.connectDist * this.option.gravity.pulling * step
|
|
286
286
|
const maxRepulsiveDist = this.option.particles.connectDist / 2
|
|
287
287
|
const maxRepulsiveDistSq = maxRepulsiveDist ** 2
|
|
288
|
-
const maxGrav = this.option.particles.connectDist * 0.1
|
|
288
|
+
const maxGrav = this.option.particles.connectDist * 0.1 * step
|
|
289
289
|
|
|
290
290
|
for (let i = 0; i < len; i++) {
|
|
291
291
|
const particleA = particles[i]
|
|
@@ -305,7 +305,7 @@ export default class CanvasParticles {
|
|
|
305
305
|
if (distSq >= maxRepulsiveDistSq && !isPullingEnabled) continue
|
|
306
306
|
|
|
307
307
|
angle = Math.atan2(particleB.posY - particleA.posY, particleB.posX - particleA.posX)
|
|
308
|
-
grav = (1 / Math.sqrt(distSq)
|
|
308
|
+
grav = Math.pow(1 / Math.sqrt(distSq), 1.8)
|
|
309
309
|
const angleX = Math.cos(angle)
|
|
310
310
|
const angleY = Math.sin(angle)
|
|
311
311
|
|
|
@@ -342,17 +342,18 @@ export default class CanvasParticles {
|
|
|
342
342
|
const offY = this.offY
|
|
343
343
|
const mouseX = this.mouseX
|
|
344
344
|
const mouseY = this.mouseY
|
|
345
|
-
const rotationSpeed = this.option.particles.rotationSpeed
|
|
345
|
+
const rotationSpeed = this.option.particles.rotationSpeed * step
|
|
346
346
|
const friction = this.option.gravity.friction
|
|
347
347
|
const mouseConnectDist = this.option.mouse.connectDist
|
|
348
348
|
const mouseDistRatio = this.option.mouse.distRatio
|
|
349
349
|
const isMouseInteractionTypeNone = this.option.mouse.interactionType === CanvasParticles.interactionType.NONE
|
|
350
350
|
const isMouseInteractionTypeMove = this.option.mouse.interactionType === CanvasParticles.interactionType.MOVE
|
|
351
|
+
const easing = 1 - Math.pow(1 - 1 / 4, step)
|
|
351
352
|
|
|
352
353
|
for (let i = 0; i < len; i++) {
|
|
353
354
|
const particle = particles[i]
|
|
354
355
|
|
|
355
|
-
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed
|
|
356
|
+
particle.dir += 2 * (Math.random() - 0.5) * rotationSpeed * step
|
|
356
357
|
particle.dir %= TWO_PI
|
|
357
358
|
|
|
358
359
|
// Constant velocity
|
|
@@ -360,8 +361,8 @@ export default class CanvasParticles {
|
|
|
360
361
|
const movY = Math.cos(particle.dir) * particle.speed
|
|
361
362
|
|
|
362
363
|
// Apply velocities
|
|
363
|
-
particle.posX += movX + particle.velX
|
|
364
|
-
particle.posY += movY + particle.velY
|
|
364
|
+
particle.posX += (movX + particle.velX) * step
|
|
365
|
+
particle.posY += (movY + particle.velY) * step
|
|
365
366
|
|
|
366
367
|
// Wrap particles around the canvas
|
|
367
368
|
particle.posX %= width
|
|
@@ -371,8 +372,8 @@ export default class CanvasParticles {
|
|
|
371
372
|
if (particle.posY < 0) particle.posY += height
|
|
372
373
|
|
|
373
374
|
// Slightly decrease dynamic velocity
|
|
374
|
-
particle.velX *= friction
|
|
375
|
-
particle.velY *= friction
|
|
375
|
+
particle.velX *= Math.pow(friction, step)
|
|
376
|
+
particle.velY *= Math.pow(friction, step)
|
|
376
377
|
|
|
377
378
|
// Distance from mouse
|
|
378
379
|
const distX = particle.posX + offX - mouseX
|
|
@@ -383,11 +384,11 @@ export default class CanvasParticles {
|
|
|
383
384
|
const distRatio = mouseConnectDist / Math.hypot(distX, distY)
|
|
384
385
|
|
|
385
386
|
if (mouseDistRatio < distRatio) {
|
|
386
|
-
particle.offX += (distRatio * distX - distX - particle.offX)
|
|
387
|
-
particle.offY += (distRatio * distY - distY - particle.offY)
|
|
387
|
+
particle.offX += (distRatio * distX - distX - particle.offX) * easing
|
|
388
|
+
particle.offY += (distRatio * distY - distY - particle.offY) * easing
|
|
388
389
|
} else {
|
|
389
|
-
particle.offX -= particle.offX
|
|
390
|
-
particle.offY -= particle.offY
|
|
390
|
+
particle.offX -= particle.offX * easing
|
|
391
|
+
particle.offY -= particle.offY * easing
|
|
391
392
|
}
|
|
392
393
|
}
|
|
393
394
|
|
|
@@ -562,7 +563,7 @@ export default class CanvasParticles {
|
|
|
562
563
|
// - step = 1 → exactly one baseline update (dt === BASE_DT)
|
|
563
564
|
// - step > 1 → more time passed (lower FPS), advance further
|
|
564
565
|
// - step < 1 → less time passed (higher FPS), advance less
|
|
565
|
-
const step = CanvasParticles.BASE_DT
|
|
566
|
+
const step = dt / CanvasParticles.BASE_DT
|
|
566
567
|
|
|
567
568
|
this.#updateGravity(step)
|
|
568
569
|
this.#updateParticles(step)
|