adjustments
This commit is contained in:
175
js/flowfield.js
175
js/flowfield.js
@@ -113,7 +113,7 @@ class FlowFieldBackground {
|
|||||||
this.noise = new SimplexNoise((Math.random() * 0xffffffff) | 0);
|
this.noise = new SimplexNoise((Math.random() * 0xffffffff) | 0);
|
||||||
|
|
||||||
this.particles = [];
|
this.particles = [];
|
||||||
this.pulses = [];
|
this.sparks = [];
|
||||||
this.mouse = { x: 0, y: 0, active: false };
|
this.mouse = { x: 0, y: 0, active: false };
|
||||||
this.rafId = null;
|
this.rafId = null;
|
||||||
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
@@ -127,32 +127,27 @@ class FlowFieldBackground {
|
|||||||
|
|
||||||
this.config = {
|
this.config = {
|
||||||
baseSpeed: 0.75,
|
baseSpeed: 0.75,
|
||||||
maxSpeed: 2.4, // velocity cap so ripples can't fling streaks
|
maxSpeed: 2.8, // velocity cap so streaks remain calm
|
||||||
steer: 0.055, // how fast particles align with the field
|
steer: 0.055, // how fast particles align with the field
|
||||||
fieldScale: 0.0016, // noise zoom: small value = large, calm features
|
fieldScale: 0.0015, // noise zoom: small value = large, calm features
|
||||||
warpStrength: 150, // px of domain warp, keeps the flow organic
|
warpStrength: 160, // px of domain warp, keeps the flow organic
|
||||||
winding: 2.4, // field angle range multiplier (x PI)
|
winding: 2.4, // field angle range multiplier (x PI)
|
||||||
timeDrift: 0.00028, // slow evolution of the whole field
|
timeDrift: 0.00028, // slow evolution of the whole field
|
||||||
fadeAlpha: 0.035, // trail persistence (lower = longer trails)
|
fadeAlpha: 0.038, // trail persistence (lower = longer trails)
|
||||||
mouseRadius: 160,
|
mouseRadius: 180,
|
||||||
mouseSwirl: 0.5, // tangential push around the cursor
|
mouseSwirl: 0.5, // tangential push around the cursor
|
||||||
mousePull: 0.06, // whisper of inward pull -> orbiting feel
|
mousePull: 0.06, // whisper of inward pull -> orbiting feel
|
||||||
glowBoost: 0.2, // extra alpha for particles near the cursor
|
glowBoost: 0.25, // extra alpha for particles near the cursor
|
||||||
maxPulses: 4,
|
sparkCount: 10, // subtle micro-sparks per click
|
||||||
pulseSpeed: 3.4,
|
life: { min: 280, max: 920 } // frames before a particle respawns
|
||||||
pulseRadius: 280,
|
|
||||||
pulseBand: 46, // px band around the ring that pushes particles
|
|
||||||
pulseStrength: 0.55,
|
|
||||||
life: { min: 260, max: 900 } // frames before a particle respawns
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mostly near-white hairlines, with a few accent-tinted currents
|
// Rich site palette with cyan, indigo, violet, and soft luminous accents
|
||||||
// sampled from the site palette (cyan / indigo / violet).
|
|
||||||
this.palette = [
|
this.palette = [
|
||||||
{ weight: 0.62, rgb: '206, 224, 255', alpha: 0.125 },
|
{ weight: 0.55, rgb: '206, 224, 255', alpha: 0.13 }, // Soft cyan-white
|
||||||
{ weight: 0.14, rgb: '79, 195, 247', alpha: 0.22 },
|
{ weight: 0.18, rgb: '79, 195, 247', alpha: 0.24 }, // Electric cyan
|
||||||
{ weight: 0.12, rgb: '102, 126, 234', alpha: 0.115 },
|
{ weight: 0.14, rgb: '102, 126, 234', alpha: 0.16 }, // Deep indigo
|
||||||
{ weight: 0.12, rgb: '146, 103, 197', alpha: 0.115 }
|
{ weight: 0.13, rgb: '146, 103, 197', alpha: 0.16 } // Soft violet
|
||||||
];
|
];
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
@@ -182,7 +177,7 @@ class FlowFieldBackground {
|
|||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
window.addEventListener('pointerdown', (e) => {
|
window.addEventListener('pointerdown', (e) => {
|
||||||
this.spawnPulse(e.clientX, e.clientY);
|
this.spawnClickBurst(e.clientX, e.clientY);
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
window.addEventListener('pointerout', () => {
|
window.addEventListener('pointerout', () => {
|
||||||
@@ -255,19 +250,41 @@ class FlowFieldBackground {
|
|||||||
createParticle() {
|
createParticle() {
|
||||||
const tone = this.pickTone();
|
const tone = this.pickTone();
|
||||||
const { min, max } = this.config.life;
|
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 {
|
return {
|
||||||
|
depth,
|
||||||
x: Math.random() * this.width,
|
x: Math.random() * this.width,
|
||||||
y: Math.random() * this.height,
|
y: Math.random() * this.height,
|
||||||
px: 0,
|
px: 0,
|
||||||
py: 0,
|
py: 0,
|
||||||
vx: 0,
|
vx: 0,
|
||||||
vy: 0,
|
vy: 0,
|
||||||
speed: this.config.baseSpeed * (0.7 + Math.random() * 0.6),
|
speed: this.config.baseSpeed * speedMult * (0.75 + Math.random() * 0.5),
|
||||||
width: 0.7 + Math.random() * 0.6,
|
width: (0.7 + Math.random() * 0.5) * widthMult,
|
||||||
rgb: tone.rgb,
|
rgb: tone.rgb,
|
||||||
alpha: tone.alpha * (0.7 + Math.random() * 0.6),
|
baseAlpha: tone.alpha * (0.75 + Math.random() * 0.5) * alphaMult,
|
||||||
|
alpha: 0,
|
||||||
glow: 0,
|
glow: 0,
|
||||||
life: 0,
|
life: Math.floor(Math.random() * (min * 0.5)),
|
||||||
maxLife: min + Math.random() * (max - min),
|
maxLife: min + Math.random() * (max - min),
|
||||||
fresh: true // no previous point yet -> nothing to draw
|
fresh: true // no previous point yet -> nothing to draw
|
||||||
};
|
};
|
||||||
@@ -282,9 +299,27 @@ class FlowFieldBackground {
|
|||||||
p.fresh = true;
|
p.fresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnPulse(x, y) {
|
spawnClickBurst(x, y) {
|
||||||
if (this.pulses.length >= this.config.maxPulses) this.pulses.shift();
|
// Emit a subtle, delicate burst of hair-thin micro-sparks on click
|
||||||
this.pulses.push({ x, y, r: 0 });
|
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() {
|
updateParticles() {
|
||||||
@@ -294,7 +329,19 @@ class FlowFieldBackground {
|
|||||||
const radius = this.config.mouseRadius;
|
const radius = this.config.mouseRadius;
|
||||||
const radiusSq = radius * radius;
|
const radiusSq = radius * radius;
|
||||||
|
|
||||||
|
const fadeIn = 35;
|
||||||
|
const fadeOut = 55;
|
||||||
|
|
||||||
for (const p of this.particles) {
|
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
|
// Domain-warped flow angle: the warp layer bends the main field
|
||||||
// so the current curls organically instead of drifting straight.
|
// so the current curls organically instead of drifting straight.
|
||||||
const warp = this.noise.noise2D(
|
const warp = this.noise.noise2D(
|
||||||
@@ -328,19 +375,6 @@ class FlowFieldBackground {
|
|||||||
// Smooth the glow so the highlight eases in and out.
|
// Smooth the glow so the highlight eases in and out.
|
||||||
p.glow += (proximity - p.glow) * 0.12;
|
p.glow += (proximity - p.glow) * 0.12;
|
||||||
|
|
||||||
// Click / tap ripples shove particles radially outward.
|
|
||||||
for (const pulse of this.pulses) {
|
|
||||||
const dx = p.x - pulse.x;
|
|
||||||
const dy = p.y - pulse.y;
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
const offset = Math.abs(dist - pulse.r);
|
|
||||||
if (offset < this.config.pulseBand) {
|
|
||||||
const push = (1 - offset / this.config.pulseBand) * this.config.pulseStrength;
|
|
||||||
p.vx += (dx / dist) * push;
|
|
||||||
p.vy += (dy / dist) * push;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const speedSq = p.vx * p.vx + p.vy * p.vy;
|
const speedSq = p.vx * p.vx + p.vy * p.vy;
|
||||||
if (speedSq > maxSpeed * maxSpeed) {
|
if (speedSq > maxSpeed * maxSpeed) {
|
||||||
const scale = maxSpeed / Math.sqrt(speedSq);
|
const scale = maxSpeed / Math.sqrt(speedSq);
|
||||||
@@ -366,11 +400,27 @@ class FlowFieldBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePulses() {
|
updateSparks() {
|
||||||
for (let i = this.pulses.length - 1; i >= 0; i--) {
|
const drag = 0.91;
|
||||||
const pulse = this.pulses[i];
|
const { fieldScale } = this.config;
|
||||||
pulse.r += this.config.pulseSpeed;
|
const t = this.time;
|
||||||
if (pulse.r > this.config.pulseRadius) this.pulses.splice(i, 1);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,35 +444,50 @@ class FlowFieldBackground {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const alpha = Math.min(1, p.alpha + p.glow * this.config.glowBoost);
|
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.strokeStyle = `rgba(${p.rgb}, ${alpha.toFixed(3)})`;
|
||||||
ctx.lineWidth = p.width;
|
ctx.lineWidth = p.width;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(p.px, p.py);
|
ctx.moveTo(p.px, p.py);
|
||||||
ctx.lineTo(p.x, p.y);
|
ctx.lineTo(p.x, p.y);
|
||||||
ctx.stroke();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPulses() {
|
drawSparks() {
|
||||||
if (!this.pulses.length) return;
|
if (!this.sparks.length) return;
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
ctx.lineWidth = 1;
|
|
||||||
for (const pulse of this.pulses) {
|
for (const s of this.sparks) {
|
||||||
const fadeOut = 1 - pulse.r / this.config.pulseRadius;
|
const lifeRatio = 1 - s.life / s.maxLife;
|
||||||
ctx.strokeStyle = `rgba(79, 195, 247, ${(fadeOut * 0.45).toFixed(3)})`;
|
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.beginPath();
|
||||||
ctx.arc(pulse.x, pulse.y, pulse.r, 0, Math.PI * 2);
|
ctx.moveTo(s.px, s.py);
|
||||||
|
ctx.lineTo(s.x, s.y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
step(withFade) {
|
step(withFade) {
|
||||||
if (withFade) this.fade();
|
if (withFade) this.fade();
|
||||||
this.updatePulses();
|
this.updateSparks();
|
||||||
this.updateParticles();
|
this.updateParticles();
|
||||||
this.drawParticles();
|
this.drawParticles();
|
||||||
this.drawPulses();
|
this.drawSparks();
|
||||||
this.time += this.config.timeDrift;
|
this.time += this.config.timeDrift;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user