Merge pull request 'feat/new-background' (#2) from feat/new-background into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2
404.html
2
404.html
@@ -45,7 +45,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="js/matrix.js"></script>
|
<script src="js/flowfield.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="js/matrix.js"></script>
|
<script src="js/flowfield.js"></script>
|
||||||
<script src="js/social.js"></script>
|
<script src="js/social.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
508
js/flowfield.js
Normal file
508
js/flowfield.js
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
/**
|
||||||
|
* FlowFieldBackground
|
||||||
|
*
|
||||||
|
* Minimal generative backdrop: hundreds of hair-thin particles ride a
|
||||||
|
* domain-warped simplex flow field and leave short silk-like trails.
|
||||||
|
* The cursor bends the current into a slow vortex (nearby strokes pick up
|
||||||
|
* a faint glow); clicking / tapping emits a thin ripple that shoves
|
||||||
|
* particles aside.
|
||||||
|
*
|
||||||
|
* Trails are drawn by erasing the previous frame with a low-alpha
|
||||||
|
* `destination-out` pass, so the CSS gradient painted behind the canvas
|
||||||
|
* element keeps showing through instead of being flooded by an opaque fill.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Deterministic PRNG so the noise permutation is stable within a page load. */
|
||||||
|
function mulberry32(seed) {
|
||||||
|
return function () {
|
||||||
|
seed |= 0;
|
||||||
|
seed = (seed + 0x6D2B79F5) | 0;
|
||||||
|
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||||
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact seeded 2D simplex noise (Gustavson-style), output in ~[-1, 1]. */
|
||||||
|
class SimplexNoise {
|
||||||
|
constructor(seed) {
|
||||||
|
const rand = mulberry32(seed);
|
||||||
|
const p = new Uint8Array(256);
|
||||||
|
for (let i = 0; i < 256; i++) p[i] = i;
|
||||||
|
// Fisher-Yates shuffle for the permutation table.
|
||||||
|
for (let i = 255; i > 0; i--) {
|
||||||
|
const j = (rand() * (i + 1)) | 0;
|
||||||
|
const tmp = p[i];
|
||||||
|
p[i] = p[j];
|
||||||
|
p[j] = tmp;
|
||||||
|
}
|
||||||
|
this.perm = new Uint8Array(512);
|
||||||
|
this.permMod12 = new Uint8Array(512);
|
||||||
|
for (let i = 0; i < 512; i++) {
|
||||||
|
this.perm[i] = p[i & 255];
|
||||||
|
this.permMod12[i] = this.perm[i] % 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noise2D(xin, yin) {
|
||||||
|
const F2 = SimplexNoise.F2;
|
||||||
|
const G2 = SimplexNoise.G2;
|
||||||
|
const GRAD3 = SimplexNoise.GRAD3;
|
||||||
|
let n0 = 0, n1 = 0, n2 = 0;
|
||||||
|
|
||||||
|
const s = (xin + yin) * F2;
|
||||||
|
const i = Math.floor(xin + s);
|
||||||
|
const j = Math.floor(yin + s);
|
||||||
|
const t = (i + j) * G2;
|
||||||
|
|
||||||
|
const x0 = xin - (i - t);
|
||||||
|
const y0 = yin - (j - t);
|
||||||
|
|
||||||
|
// Corner offsets depend on which half of the simplex we landed in.
|
||||||
|
let i1, j1;
|
||||||
|
if (x0 > y0) { i1 = 1; j1 = 0; } else { i1 = 0; j1 = 1; }
|
||||||
|
|
||||||
|
const x1 = x0 - i1 + G2;
|
||||||
|
const y1 = y0 - j1 + G2;
|
||||||
|
const x2 = x0 - 1 + 2 * G2;
|
||||||
|
const y2 = y0 - 1 + 2 * G2;
|
||||||
|
|
||||||
|
const ii = i & 255;
|
||||||
|
const jj = j & 255;
|
||||||
|
|
||||||
|
let t0 = 0.5 - x0 * x0 - y0 * y0;
|
||||||
|
if (t0 >= 0) {
|
||||||
|
const g = GRAD3[this.permMod12[ii + this.perm[jj]]];
|
||||||
|
t0 *= t0;
|
||||||
|
n0 = t0 * t0 * (g[0] * x0 + g[1] * y0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let t1 = 0.5 - x1 * x1 - y1 * y1;
|
||||||
|
if (t1 >= 0) {
|
||||||
|
const g = GRAD3[this.permMod12[ii + i1 + this.perm[jj + j1]]];
|
||||||
|
t1 *= t1;
|
||||||
|
n1 = t1 * t1 * (g[0] * x1 + g[1] * y1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let t2 = 0.5 - x2 * x2 - y2 * y2;
|
||||||
|
if (t2 >= 0) {
|
||||||
|
const g = GRAD3[this.permMod12[ii + 1 + this.perm[jj + 1]]];
|
||||||
|
t2 *= t2;
|
||||||
|
n2 = t2 * t2 * (g[0] * x2 + g[1] * y2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empirical factor scaling the sum into roughly [-1, 1].
|
||||||
|
return 70 * (n0 + n1 + n2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SimplexNoise.F2 = 0.5 * (Math.sqrt(3) - 1);
|
||||||
|
SimplexNoise.G2 = (3 - Math.sqrt(3)) / 6;
|
||||||
|
SimplexNoise.GRAD3 = [
|
||||||
|
[1, 1], [-1, 1], [1, -1], [-1, -1],
|
||||||
|
[1, 0], [-1, 0], [1, 0], [-1, 0],
|
||||||
|
[0, 1], [0, -1], [0, 1], [0, -1]
|
||||||
|
];
|
||||||
|
|
||||||
|
class FlowFieldBackground {
|
||||||
|
constructor() {
|
||||||
|
// Legacy element id kept from the previous background; the canvas
|
||||||
|
// and its CSS (css/matrix.css) are reused on purpose.
|
||||||
|
this.canvas = document.getElementById('matrixCanvas');
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.noise = new SimplexNoise((Math.random() * 0xffffffff) | 0);
|
||||||
|
|
||||||
|
this.particles = [];
|
||||||
|
this.sparks = [];
|
||||||
|
this.mouse = { x: 0, y: 0, active: false };
|
||||||
|
this.rafId = null;
|
||||||
|
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
this.width = 0;
|
||||||
|
this.height = 0;
|
||||||
|
this.dpr = 1;
|
||||||
|
|
||||||
|
// Random phase so each visit opens on a different current.
|
||||||
|
this.time = Math.random() * 4000;
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
baseSpeed: 0.75,
|
||||||
|
maxSpeed: 2.8, // velocity cap so streaks remain calm
|
||||||
|
steer: 0.055, // how fast particles align with the field
|
||||||
|
fieldScale: 0.0015, // noise zoom: small value = large, calm features
|
||||||
|
warpStrength: 160, // px of domain warp, keeps the flow organic
|
||||||
|
winding: 2.4, // field angle range multiplier (x PI)
|
||||||
|
timeDrift: 0.00028, // slow evolution of the whole field
|
||||||
|
fadeAlpha: 0.038, // trail persistence (lower = longer trails)
|
||||||
|
mouseRadius: 180,
|
||||||
|
mouseSwirl: 0.5, // tangential push around the cursor
|
||||||
|
mousePull: 0.06, // whisper of inward pull -> orbiting feel
|
||||||
|
glowBoost: 0.25, // extra alpha for particles near the cursor
|
||||||
|
sparkCount: 10, // subtle micro-sparks per click
|
||||||
|
life: { min: 280, max: 920 } // frames before a particle respawns
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rich site palette with cyan, indigo, violet, and soft luminous accents
|
||||||
|
this.palette = [
|
||||||
|
{ weight: 0.55, rgb: '206, 224, 255', alpha: 0.13 }, // Soft cyan-white
|
||||||
|
{ weight: 0.18, rgb: '79, 195, 247', alpha: 0.24 }, // Electric cyan
|
||||||
|
{ weight: 0.14, rgb: '102, 126, 234', alpha: 0.16 }, // Deep indigo
|
||||||
|
{ weight: 0.13, rgb: '146, 103, 197', alpha: 0.16 } // Soft violet
|
||||||
|
];
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.handleResize();
|
||||||
|
|
||||||
|
if (this.prefersReducedMotion) {
|
||||||
|
// Single long-exposure render; no animation loop at all.
|
||||||
|
this.warmup(180);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-run a short exposure so the first paint is already textured.
|
||||||
|
this.warmup(60);
|
||||||
|
this.animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// The canvas has pointer-events: none, so listen on window.
|
||||||
|
window.addEventListener('pointermove', (e) => {
|
||||||
|
this.mouse.x = e.clientX;
|
||||||
|
this.mouse.y = e.clientY;
|
||||||
|
this.mouse.active = true;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
window.addEventListener('pointerdown', (e) => {
|
||||||
|
this.spawnClickBurst(e.clientX, e.clientY);
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
window.addEventListener('pointerout', () => {
|
||||||
|
this.mouse.active = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pause the rAF loop when the tab is hidden to save CPU / battery.
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.stop();
|
||||||
|
} else if (!this.prefersReducedMotion) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let resizeRaf = null;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||||
|
resizeRaf = requestAnimationFrame(() => this.handleResize());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.rafId) return;
|
||||||
|
this.animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.rafId) {
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
const w = window.innerWidth;
|
||||||
|
const h = window.innerHeight;
|
||||||
|
|
||||||
|
// Mobile browsers fire resize on scroll when the URL bar collapses;
|
||||||
|
// only react to real geometry changes to avoid canvas flicker.
|
||||||
|
if (this.width && w === this.width && Math.abs(h - this.height) < 150) return;
|
||||||
|
|
||||||
|
this.width = w;
|
||||||
|
this.height = h;
|
||||||
|
this.dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
this.canvas.width = Math.round(w * this.dpr);
|
||||||
|
this.canvas.height = Math.round(h * this.dpr);
|
||||||
|
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
||||||
|
|
||||||
|
// Density scales with area, clamped for both phones and ultrawides.
|
||||||
|
const count = Math.round(Math.min(Math.max((w * h) / 2600, 160), 650));
|
||||||
|
this.particles = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.particles.push(this.createParticle());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repaint a short exposure so a resize never flashes an empty canvas.
|
||||||
|
this.warmup(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
pickTone() {
|
||||||
|
let roll = Math.random();
|
||||||
|
for (const tone of this.palette) {
|
||||||
|
roll -= tone.weight;
|
||||||
|
if (roll <= 0) return tone;
|
||||||
|
}
|
||||||
|
return this.palette[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
createParticle() {
|
||||||
|
const tone = this.pickTone();
|
||||||
|
const { min, max } = this.config.life;
|
||||||
|
|
||||||
|
// 3 Depth layers: 0 (background ambient), 1 (main stream), 2 (foreground highlight)
|
||||||
|
const randDepth = Math.random();
|
||||||
|
let depth = 1;
|
||||||
|
let speedMult = 1.0;
|
||||||
|
let widthMult = 1.0;
|
||||||
|
let alphaMult = 1.0;
|
||||||
|
|
||||||
|
if (randDepth < 0.25) {
|
||||||
|
depth = 0;
|
||||||
|
speedMult = 0.65;
|
||||||
|
widthMult = 0.6;
|
||||||
|
alphaMult = 0.65;
|
||||||
|
} else if (randDepth > 0.78) {
|
||||||
|
depth = 2;
|
||||||
|
speedMult = 1.35;
|
||||||
|
widthMult = 1.35;
|
||||||
|
alphaMult = 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
depth,
|
||||||
|
x: Math.random() * this.width,
|
||||||
|
y: Math.random() * this.height,
|
||||||
|
px: 0,
|
||||||
|
py: 0,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
speed: this.config.baseSpeed * speedMult * (0.75 + Math.random() * 0.5),
|
||||||
|
width: (0.7 + Math.random() * 0.5) * widthMult,
|
||||||
|
rgb: tone.rgb,
|
||||||
|
baseAlpha: tone.alpha * (0.75 + Math.random() * 0.5) * alphaMult,
|
||||||
|
alpha: 0,
|
||||||
|
glow: 0,
|
||||||
|
life: Math.floor(Math.random() * (min * 0.5)),
|
||||||
|
maxLife: min + Math.random() * (max - min),
|
||||||
|
fresh: true // no previous point yet -> nothing to draw
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
respawn(p) {
|
||||||
|
p.x = Math.random() * this.width;
|
||||||
|
p.y = Math.random() * this.height;
|
||||||
|
p.vx = 0;
|
||||||
|
p.vy = 0;
|
||||||
|
p.life = 0;
|
||||||
|
p.fresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnClickBurst(x, y) {
|
||||||
|
// Emit a subtle, delicate burst of hair-thin micro-sparks on click
|
||||||
|
const count = this.config.sparkCount;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const speed = 1.0 + Math.random() * 2.2;
|
||||||
|
const tone = this.pickTone();
|
||||||
|
this.sparks.push({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
px: x,
|
||||||
|
py: y,
|
||||||
|
vx: Math.cos(angle) * speed,
|
||||||
|
vy: Math.sin(angle) * speed,
|
||||||
|
rgb: tone.rgb,
|
||||||
|
size: 0.6 + Math.random() * 0.8,
|
||||||
|
alpha: 0.6 + Math.random() * 0.25,
|
||||||
|
life: 0,
|
||||||
|
maxLife: 25 + Math.random() * 25
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParticles() {
|
||||||
|
const { fieldScale, warpStrength, winding, steer, maxSpeed } = this.config;
|
||||||
|
const t = this.time;
|
||||||
|
const m = this.mouse;
|
||||||
|
const radius = this.config.mouseRadius;
|
||||||
|
const radiusSq = radius * radius;
|
||||||
|
|
||||||
|
const fadeIn = 35;
|
||||||
|
const fadeOut = 55;
|
||||||
|
|
||||||
|
for (const p of this.particles) {
|
||||||
|
// Smooth life envelope (fade-in on spawn, fade-out near maxLife)
|
||||||
|
let lifeEnvelope = 1;
|
||||||
|
if (p.life < fadeIn) {
|
||||||
|
lifeEnvelope = p.life / fadeIn;
|
||||||
|
} else if (p.life > p.maxLife - fadeOut) {
|
||||||
|
lifeEnvelope = Math.max(0, (p.maxLife - p.life) / fadeOut);
|
||||||
|
}
|
||||||
|
p.alpha = p.baseAlpha * lifeEnvelope;
|
||||||
|
|
||||||
|
// Domain-warped flow angle: the warp layer bends the main field
|
||||||
|
// so the current curls organically instead of drifting straight.
|
||||||
|
const warp = this.noise.noise2D(
|
||||||
|
p.x * fieldScale * 0.4 + t * 0.5,
|
||||||
|
p.y * fieldScale * 0.4 - t * 0.3
|
||||||
|
);
|
||||||
|
const angle = this.noise.noise2D(
|
||||||
|
(p.x + warp * warpStrength) * fieldScale + t,
|
||||||
|
(p.y - warp * warpStrength) * fieldScale - t * 0.7
|
||||||
|
) * Math.PI * winding;
|
||||||
|
|
||||||
|
p.vx += (Math.cos(angle) * p.speed - p.vx) * steer;
|
||||||
|
p.vy += (Math.sin(angle) * p.speed - p.vy) * steer;
|
||||||
|
|
||||||
|
// Cursor vortex: tangential swirl + slight inward pull.
|
||||||
|
let proximity = 0;
|
||||||
|
if (m.active) {
|
||||||
|
const dx = p.x - m.x;
|
||||||
|
const dy = p.y - m.y;
|
||||||
|
const distSq = dx * dx + dy * dy;
|
||||||
|
if (distSq < radiusSq && distSq > 0.5) {
|
||||||
|
const dist = Math.sqrt(distSq);
|
||||||
|
proximity = 1 - dist / radius;
|
||||||
|
const falloff = proximity * proximity;
|
||||||
|
p.vx += (-dy / dist) * falloff * this.config.mouseSwirl
|
||||||
|
+ (-dx / dist) * falloff * this.config.mousePull;
|
||||||
|
p.vy += (dx / dist) * falloff * this.config.mouseSwirl
|
||||||
|
+ (-dy / dist) * falloff * this.config.mousePull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Smooth the glow so the highlight eases in and out.
|
||||||
|
p.glow += (proximity - p.glow) * 0.12;
|
||||||
|
|
||||||
|
const speedSq = p.vx * p.vx + p.vy * p.vy;
|
||||||
|
if (speedSq > maxSpeed * maxSpeed) {
|
||||||
|
const scale = maxSpeed / Math.sqrt(speedSq);
|
||||||
|
p.vx *= scale;
|
||||||
|
p.vy *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.px = p.x;
|
||||||
|
p.py = p.y;
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
|
||||||
|
// Wrap at the edges; mark fresh so no cross-screen streak is drawn.
|
||||||
|
if (p.x < 0 || p.x > this.width || p.y < 0 || p.y > this.height) {
|
||||||
|
p.x = (p.x + this.width) % this.width;
|
||||||
|
p.y = (p.y + this.height) % this.height;
|
||||||
|
p.fresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++p.life > p.maxLife) {
|
||||||
|
this.respawn(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSparks() {
|
||||||
|
const drag = 0.91;
|
||||||
|
const { fieldScale } = this.config;
|
||||||
|
const t = this.time;
|
||||||
|
for (let i = this.sparks.length - 1; i >= 0; i--) {
|
||||||
|
const s = this.sparks[i];
|
||||||
|
s.px = s.x;
|
||||||
|
s.py = s.y;
|
||||||
|
|
||||||
|
// Gently blend spark velocity with noise field as it slows down
|
||||||
|
const angle = this.noise.noise2D(s.x * fieldScale + t, s.y * fieldScale - t) * Math.PI * 2;
|
||||||
|
s.vx = s.vx * drag + Math.cos(angle) * 0.15;
|
||||||
|
s.vy = s.vy * drag + Math.sin(angle) * 0.15;
|
||||||
|
|
||||||
|
s.x += s.vx;
|
||||||
|
s.y += s.vy;
|
||||||
|
s.life++;
|
||||||
|
|
||||||
|
if (s.life >= s.maxLife) {
|
||||||
|
this.sparks.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fade() {
|
||||||
|
// Erase a fraction of the previous frame towards transparency so
|
||||||
|
// the CSS backdrop behind the canvas keeps showing through.
|
||||||
|
this.ctx.globalCompositeOperation = 'destination-out';
|
||||||
|
this.ctx.fillStyle = `rgba(0, 0, 0, ${this.config.fadeAlpha})`;
|
||||||
|
this.ctx.fillRect(0, 0, this.width, this.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawParticles() {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
// Additive blending gives a soft glow where currents overlap.
|
||||||
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
for (const p of this.particles) {
|
||||||
|
if (p.fresh) {
|
||||||
|
p.fresh = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const alpha = Math.min(1, p.alpha + p.glow * this.config.glowBoost);
|
||||||
|
if (alpha <= 0.005) continue;
|
||||||
|
|
||||||
|
ctx.strokeStyle = `rgba(${p.rgb}, ${alpha.toFixed(3)})`;
|
||||||
|
ctx.lineWidth = p.width;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(p.px, p.py);
|
||||||
|
ctx.lineTo(p.x, p.y);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw a subtle bright dot at the head of foreground particles
|
||||||
|
if (p.depth === 2 && (p.glow > 0.1 || Math.random() < 0.12)) {
|
||||||
|
ctx.fillStyle = `rgba(240, 248, 255, ${(alpha * 0.75).toFixed(3)})`;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, p.width * 0.75, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSparks() {
|
||||||
|
if (!this.sparks.length) return;
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
|
|
||||||
|
for (const s of this.sparks) {
|
||||||
|
const lifeRatio = 1 - s.life / s.maxLife;
|
||||||
|
const alpha = (s.alpha * lifeRatio * lifeRatio).toFixed(3);
|
||||||
|
if (alpha <= 0) continue;
|
||||||
|
|
||||||
|
ctx.strokeStyle = `rgba(${s.rgb}, ${alpha})`;
|
||||||
|
ctx.lineWidth = s.size * lifeRatio;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(s.px, s.py);
|
||||||
|
ctx.lineTo(s.x, s.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step(withFade) {
|
||||||
|
if (withFade) this.fade();
|
||||||
|
this.updateSparks();
|
||||||
|
this.updateParticles();
|
||||||
|
this.drawParticles();
|
||||||
|
this.drawSparks();
|
||||||
|
this.time += this.config.timeDrift;
|
||||||
|
}
|
||||||
|
|
||||||
|
warmup(steps) {
|
||||||
|
for (let i = 0; i < steps; i++) {
|
||||||
|
this.step(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
this.step(true);
|
||||||
|
this.rafId = requestAnimationFrame(() => this.animate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new FlowFieldBackground();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user