235 lines
11 KiB
JavaScript
235 lines
11 KiB
JavaScript
/**
|
|
* VIZ 2.0 - Particle Physics Engine (8 Specialized Audio-Reactive Modes)
|
|
*/
|
|
|
|
import { state } from '../state.js';
|
|
|
|
export function initParticles(count) {
|
|
state.particles = [];
|
|
const w = state.width || 1920;
|
|
const h = state.height || 1080;
|
|
for (let i = 0; i < count; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = Math.random() * 500 + 40;
|
|
state.particles.push({
|
|
x: (Math.random() - 0.5) * w,
|
|
y: (Math.random() - 0.5) * h,
|
|
z: Math.random() * 1000 + 100,
|
|
radius: Math.random() * 3.0 + 1.2,
|
|
alpha: Math.random() * 0.7 + 0.3,
|
|
speedMultiplier: Math.random() * 0.8 + 0.6,
|
|
vx: (Math.random() - 0.5) * 1.5,
|
|
vy: (Math.random() - 0.5) * 1.5,
|
|
angle: angle,
|
|
orbitRadius: dist,
|
|
orbitSpeed: (Math.random() * 0.018 + 0.008) * (Math.random() < 0.5 ? 1 : -1),
|
|
wobblePhase: Math.random() * Math.PI * 2,
|
|
wobbleSpeed: Math.random() * 0.05 + 0.02,
|
|
length: Math.random() * 16 + 8,
|
|
life: Math.random(),
|
|
maxLife: Math.random() * 120 + 80
|
|
});
|
|
}
|
|
}
|
|
|
|
export function updateAndDrawParticles(targetCtx, width, height, cfg, particleEnergy, midEnergy, trebleEnergy, isPlaying) {
|
|
if (!cfg.enableParticles || !state.particles || state.particles.length === 0) return;
|
|
targetCtx.save();
|
|
const pType = cfg.particleType || 'starfield';
|
|
const reactivity = cfg.particleAudioReactivity !== undefined ? cfg.particleAudioReactivity : 1.5;
|
|
const kickPunch = state._kickPunch || 0;
|
|
const speedMult = cfg.particleSpeed * (1.0 + (particleEnergy * cfg.particleWarp * reactivity * 0.5));
|
|
const isTransparentMode = (cfg.backgroundMode === 'transparent');
|
|
|
|
for (let i = 0; i < state.particles.length; i++) {
|
|
const p = state.particles[i];
|
|
|
|
if (isPlaying) {
|
|
if (pType === 'starfield') {
|
|
p.z -= speedMult * p.speedMultiplier * (5.5 + kickPunch * 8.0 * reactivity);
|
|
if (p.z <= 10) {
|
|
p.z = 1000;
|
|
p.x = (Math.random() - 0.5) * width;
|
|
p.y = (Math.random() - 0.5) * height;
|
|
}
|
|
} else if (pType === 'dust') {
|
|
p.x += p.vx * speedMult * (1.0 + midEnergy * 0.8 * reactivity);
|
|
p.y += p.vy * speedMult * (1.0 + midEnergy * 0.8 * reactivity);
|
|
if (p.x < -width / 2) p.x = width / 2;
|
|
if (p.x > width / 2) p.x = -width / 2;
|
|
if (p.y < -height / 2) p.y = height / 2;
|
|
if (p.y > height / 2) p.y = -height / 2;
|
|
} else if (pType === 'sparks') {
|
|
const sparkBoost = 1.0 + (kickPunch * 4.5 + trebleEnergy * 2.0) * reactivity;
|
|
p.x += p.vx * speedMult * sparkBoost;
|
|
p.y += p.vy * speedMult * sparkBoost;
|
|
if (Math.hypot(p.x, p.y) > width * 0.6) {
|
|
const initR = (cfg.circleRadius || 100) * (0.4 + Math.random() * 0.6);
|
|
const initA = Math.random() * Math.PI * 2;
|
|
p.x = Math.cos(initA) * initR;
|
|
p.y = Math.sin(initA) * initR;
|
|
p.vx = Math.cos(initA) * (Math.random() * 2.5 + 1.0);
|
|
p.vy = Math.sin(initA) * (Math.random() * 2.5 + 1.0);
|
|
}
|
|
} else if (pType === 'vortex') {
|
|
// Swirling cosmic vortex around center
|
|
const rotVel = p.orbitSpeed * speedMult * (1.0 + kickPunch * 3.0 * reactivity);
|
|
p.angle += rotVel;
|
|
p.orbitRadius -= (0.45 * speedMult) - (kickPunch * 8.0 * reactivity);
|
|
if (p.orbitRadius < 40) {
|
|
p.orbitRadius = Math.max(width, height) * (0.45 + Math.random() * 0.25);
|
|
} else if (p.orbitRadius > Math.max(width, height) * 0.75) {
|
|
p.orbitRadius = 45;
|
|
}
|
|
p.x = Math.cos(p.angle) * p.orbitRadius;
|
|
p.y = Math.sin(p.angle) * p.orbitRadius;
|
|
} else if (pType === 'bubbles') {
|
|
// Gentle rising bubbles with audio wobble and beat expansion
|
|
p.y -= (p.speedMultiplier * 1.8 * speedMult) * (1.0 + (midEnergy + kickPunch) * 1.5 * reactivity);
|
|
p.wobblePhase += p.wobbleSpeed * (1.0 + midEnergy * reactivity);
|
|
if (p.y < -height / 2 - 30) {
|
|
p.y = height / 2 + 30;
|
|
p.x = (Math.random() - 0.5) * width * 0.95;
|
|
}
|
|
} else if (pType === 'embers') {
|
|
// High-energy rising fire sparks / glowing embers
|
|
const emberRise = (p.speedMultiplier * 2.8 * speedMult) * (1.0 + kickPunch * 3.5 * reactivity);
|
|
p.y -= emberRise;
|
|
p.x += Math.sin(currentTime * 4 + i) * (1.5 + kickPunch * 3.0 * reactivity);
|
|
p.life -= 0.014 * (1.0 + kickPunch * reactivity);
|
|
if (p.life <= 0 || p.y < -height / 2 - 20) {
|
|
p.y = height / 2 + 20 + Math.random() * 40;
|
|
p.x = (Math.random() - 0.5) * width * 0.85;
|
|
p.life = 1.0;
|
|
}
|
|
} else if (pType === 'digital_rain') {
|
|
// Matrix-like falling digital rain cascades
|
|
const streamFall = (p.speedMultiplier * 8.0 * speedMult) * (1.0 + trebleEnergy * 2.2 * reactivity);
|
|
p.y += streamFall;
|
|
if (p.y > height / 2 + p.length) {
|
|
p.y = -height / 2 - Math.random() * 60;
|
|
const colWidth = 24;
|
|
p.x = (Math.floor(Math.random() * (width / colWidth)) * colWidth) - width / 2;
|
|
}
|
|
} else if (pType === 'shockwave_blast') {
|
|
// Concentric shockwave blast expanding outwards with kick punches
|
|
p.orbitRadius += (p.speedMultiplier * 5.0 * speedMult) * (1.0 + kickPunch * 5.0 * reactivity);
|
|
if (p.orbitRadius > Math.max(width, height) * 0.65) {
|
|
p.orbitRadius = (cfg.circleRadius || 120) * 0.6 + Math.random() * 30;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render particles
|
|
if (pType === 'starfield') {
|
|
const k = 400 / p.z;
|
|
const px = p.x * k;
|
|
const py = p.y * k;
|
|
const pSize = Math.max(0.75, p.radius * k);
|
|
|
|
if (Math.abs(px) < width / 2 && Math.abs(py) < height / 2) {
|
|
let pAlpha = p.alpha * (1 - p.z / 1000);
|
|
if (isTransparentMode) {
|
|
if (pAlpha < 0.22 || p.z > 750) continue;
|
|
pAlpha = Math.max(0.5, pAlpha);
|
|
}
|
|
targetCtx.fillStyle = cfg.colorParticle || '#ffffff';
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha);
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(px, py, pSize, 0, Math.PI * 2);
|
|
targetCtx.fill();
|
|
}
|
|
} else if (pType === 'dust') {
|
|
let pAlpha = p.alpha * 0.65;
|
|
if (isTransparentMode) {
|
|
if (pAlpha < 0.22) continue;
|
|
pAlpha = Math.max(0.5, pAlpha);
|
|
}
|
|
targetCtx.fillStyle = cfg.colorParticle || '#ffffff';
|
|
targetCtx.globalAlpha = pAlpha;
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
|
targetCtx.fill();
|
|
} else if (pType === 'sparks') {
|
|
let pAlpha = p.alpha * (0.8 + kickPunch * 0.5);
|
|
if (isTransparentMode) pAlpha = Math.max(0.6, pAlpha);
|
|
targetCtx.fillStyle = cfg.colorParticle || '#ffffff';
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha);
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(p.x, p.y, p.radius * (1.0 + kickPunch * 0.8), 0, Math.PI * 2);
|
|
targetCtx.fill();
|
|
} else if (pType === 'vortex') {
|
|
let pAlpha = p.alpha * (0.65 + kickPunch * 0.35);
|
|
if (isTransparentMode) pAlpha = Math.max(0.55, pAlpha);
|
|
targetCtx.save();
|
|
targetCtx.translate(p.x, p.y);
|
|
targetCtx.rotate(p.angle + Math.PI / 2);
|
|
targetCtx.strokeStyle = cfg.colorParticle || '#ffffff';
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha);
|
|
targetCtx.lineWidth = Math.max(1.2, p.radius * 0.8);
|
|
targetCtx.beginPath();
|
|
targetCtx.moveTo(0, -p.length * (0.6 + kickPunch * 0.4));
|
|
targetCtx.lineTo(0, p.length * (0.6 + kickPunch * 0.4));
|
|
targetCtx.stroke();
|
|
targetCtx.restore();
|
|
} else if (pType === 'bubbles') {
|
|
const wobbleX = p.x + Math.sin(p.wobblePhase) * 16;
|
|
let pAlpha = p.alpha * 0.75;
|
|
if (isTransparentMode) pAlpha = Math.max(0.5, pAlpha);
|
|
const bRadius = p.radius * 2.2 * (1.0 + kickPunch * 0.35);
|
|
targetCtx.strokeStyle = cfg.colorParticle || '#ffffff';
|
|
targetCtx.lineWidth = 1.4;
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha);
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(wobbleX, p.y, bRadius, 0, Math.PI * 2);
|
|
targetCtx.stroke();
|
|
// Highlight shine spot
|
|
targetCtx.fillStyle = '#ffffff';
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha * 0.8);
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(wobbleX - bRadius * 0.35, p.y - bRadius * 0.35, Math.max(0.8, bRadius * 0.22), 0, Math.PI * 2);
|
|
targetCtx.fill();
|
|
} else if (pType === 'embers') {
|
|
let pAlpha = Math.max(0, p.life) * (0.75 + kickPunch * 0.4);
|
|
if (isTransparentMode) pAlpha = Math.max(0.55, pAlpha);
|
|
const eSize = p.radius * (0.8 + (1 - p.life) * 0.5 + kickPunch * 0.6);
|
|
targetCtx.fillStyle = cfg.colorParticle || '#ffaa44';
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha);
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(p.x, p.y, eSize, 0, Math.PI * 2);
|
|
targetCtx.fill();
|
|
} else if (pType === 'digital_rain') {
|
|
let pAlpha = p.alpha * (0.7 + trebleEnergy * 0.4);
|
|
if (isTransparentMode) pAlpha = Math.max(0.55, pAlpha);
|
|
const trailLen = p.length * (1.2 + trebleEnergy * 1.5);
|
|
targetCtx.strokeStyle = cfg.colorParticle || '#00ff88';
|
|
targetCtx.lineWidth = Math.max(1.5, p.radius * 0.8);
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha);
|
|
targetCtx.beginPath();
|
|
targetCtx.moveTo(p.x, p.y - trailLen);
|
|
targetCtx.lineTo(p.x, p.y);
|
|
targetCtx.stroke();
|
|
// Leading bright matrix head
|
|
targetCtx.fillStyle = '#ffffff';
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha * 1.2);
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(p.x, p.y, Math.max(1.2, p.radius * 0.7), 0, Math.PI * 2);
|
|
targetCtx.fill();
|
|
} else if (pType === 'shockwave_blast') {
|
|
const maxDist = Math.max(width, height) * 0.65;
|
|
let pAlpha = Math.max(0, 1.0 - (p.orbitRadius / maxDist)) * (0.6 + kickPunch * 0.4);
|
|
if (isTransparentMode) {
|
|
if (pAlpha < 0.2) continue;
|
|
pAlpha = Math.max(0.45, pAlpha);
|
|
}
|
|
targetCtx.strokeStyle = cfg.colorParticle || '#ffffff';
|
|
targetCtx.lineWidth = Math.max(1.2, 3.5 * (1.0 - p.orbitRadius / maxDist));
|
|
targetCtx.globalAlpha = Math.min(1.0, pAlpha);
|
|
targetCtx.beginPath();
|
|
targetCtx.arc(0, 0, p.orbitRadius, 0, Math.PI * 2);
|
|
targetCtx.stroke();
|
|
}
|
|
}
|
|
targetCtx.restore();
|
|
}
|