228 lines
No EOL
7.2 KiB
JavaScript
228 lines
No EOL
7.2 KiB
JavaScript
//js/visualizer.js
|
|
import { visualizerSettings } from './storage.js';
|
|
export class Visualizer {
|
|
constructor(canvas, audio) {
|
|
this.canvas = canvas;
|
|
this.ctx = canvas.getContext('2d');
|
|
this.audio = audio;
|
|
this.audioContext = null;
|
|
this.analyser = null;
|
|
this.source = null;
|
|
this.isActive = false;
|
|
this.animationId = null;
|
|
this.particles = [];
|
|
|
|
this.kick = 0;
|
|
this.lastIntensity = 0;
|
|
this.lastBeatTime = 0;
|
|
this.energyAverage = 0.3;
|
|
this.upbeatSmoother = 0;
|
|
}
|
|
|
|
init() {
|
|
if (this.audioContext) return;
|
|
|
|
try {
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
this.audioContext = new AudioContext();
|
|
this.analyser = this.audioContext.createAnalyser();
|
|
this.analyser.fftSize = 512;
|
|
this.analyser.smoothingTimeConstant = 0.7;
|
|
|
|
this.source = this.audioContext.createMediaElementSource(this.audio);
|
|
this.source.connect(this.analyser);
|
|
this.analyser.connect(this.audioContext.destination);
|
|
} catch (e) {
|
|
console.warn("Visualizer init failed (likely CORS or already connected):", e);
|
|
}
|
|
}
|
|
|
|
start() {
|
|
if (!this.audioContext) this.init();
|
|
if (!this.analyser) return;
|
|
|
|
this.isActive = true;
|
|
if (this.audioContext.state === 'suspended') {
|
|
this.audioContext.resume();
|
|
}
|
|
|
|
this.resize();
|
|
window.addEventListener('resize', this.resizeBound);
|
|
this.canvas.style.display = 'block';
|
|
|
|
this.particles = [];
|
|
this.energyAverage = 0.3;
|
|
this.kick = 0;
|
|
this.upbeatSmoother = 0;
|
|
this.animate();
|
|
}
|
|
|
|
stop() {
|
|
this.isActive = false;
|
|
if (this.animationId) cancelAnimationFrame(this.animationId);
|
|
window.removeEventListener('resize', this.resizeBound);
|
|
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.canvas.style.display = 'none';
|
|
}
|
|
|
|
resize() {
|
|
this.canvas.width = window.innerWidth;
|
|
this.canvas.height = window.innerHeight;
|
|
}
|
|
|
|
resizeBound = () => this.resize();
|
|
|
|
animate() {
|
|
if (!this.isActive) return;
|
|
this.animationId = requestAnimationFrame(() => this.animate());
|
|
|
|
const w = this.canvas.width;
|
|
const h = this.canvas.height;
|
|
const ctx = this.ctx;
|
|
|
|
let sensitivity = visualizerSettings.getSensitivity();
|
|
ctx.fillStyle = 'rgba(10, 10, 10, 0.3)';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
const bufferLength = this.analyser.frequencyBinCount;
|
|
const dataArray = new Uint8Array(bufferLength);
|
|
this.analyser.getByteFrequencyData(dataArray);
|
|
|
|
let bassSum = 0;
|
|
|
|
for (let i = 0; i < 4; i++) bassSum += dataArray[i];
|
|
const bass = bassSum / 4 / 255;
|
|
|
|
|
|
const intensity = bass * bass;
|
|
|
|
|
|
this.energyAverage = this.energyAverage * 0.99 + intensity * 0.01;
|
|
this.upbeatSmoother = this.upbeatSmoother * 0.92 + intensity * 0.08;
|
|
|
|
|
|
if (visualizerSettings.isSmartIntensityEnabled()) {
|
|
let target = 0.1;
|
|
if (this.energyAverage > 0.4) {
|
|
target = 0.7;
|
|
} else if (this.energyAverage > 0.2) {
|
|
const t = (this.energyAverage - 0.2) / 0.2;
|
|
target = 0.1 + t * 0.6;
|
|
}
|
|
sensitivity = target;
|
|
}
|
|
|
|
|
|
let threshold = 0.5;
|
|
if (this.energyAverage < 0.3) {
|
|
|
|
threshold = 0.5 + (0.3 - this.energyAverage) * 2;
|
|
}
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
if (intensity > threshold) {
|
|
|
|
if (intensity > this.lastIntensity + 0.05 && now - this.lastBeatTime > 50) {
|
|
this.kick = 1.0;
|
|
this.lastBeatTime = now;
|
|
} else {
|
|
if (this.upbeatSmoother > 0.6 && this.energyAverage > 0.4) {
|
|
const upbeatLevel = (this.upbeatSmoother - 0.6) / 0.4;
|
|
if (this.kick < upbeatLevel) {
|
|
this.kick = upbeatLevel;
|
|
} else {
|
|
this.kick *= 0.95;
|
|
}
|
|
} else {
|
|
this.kick *= 0.9;
|
|
}
|
|
}
|
|
} else {
|
|
this.kick *= 0.95;
|
|
}
|
|
this.lastIntensity = intensity;
|
|
|
|
|
|
let shakeX = 0;
|
|
let shakeY = 0;
|
|
if (this.kick > 0.1) {
|
|
const shakeAmt = this.kick * 8 * sensitivity;
|
|
shakeX = (Math.random() - 0.5) * shakeAmt;
|
|
shakeY = (Math.random() - 0.5) * shakeAmt;
|
|
}
|
|
|
|
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#ffffff';
|
|
|
|
|
|
const particleCount = 180;
|
|
if (this.particles.length !== particleCount) {
|
|
this.particles = [];
|
|
for (let i = 0; i < particleCount; i++) {
|
|
this.particles.push({
|
|
x: Math.random() * w,
|
|
y: Math.random() * h,
|
|
vx: (Math.random() - 0.5) * 2,
|
|
vy: (Math.random() - 0.5) * 2,
|
|
size: Math.random() * 3 + 1,
|
|
baseSize: Math.random() * 3 + 1
|
|
});
|
|
}
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.translate(shakeX, shakeY);
|
|
|
|
ctx.fillStyle = primaryColor;
|
|
ctx.strokeStyle = primaryColor;
|
|
|
|
for (let i = 0; i < this.particles.length; i++) {
|
|
let p = this.particles[i];
|
|
|
|
|
|
const speedMult = 1 + (intensity * 2) + this.kick * 8 * sensitivity;
|
|
p.x += p.vx * speedMult;
|
|
p.y += p.vy * speedMult;
|
|
|
|
if (this.kick > 0.3) {
|
|
p.x += (Math.random() - 0.5) * this.kick * 2 * sensitivity;
|
|
p.y += (Math.random() - 0.5) * this.kick * 2 * sensitivity;
|
|
}
|
|
|
|
if (p.x < 0) p.x = w;
|
|
if (p.x > w) p.x = 0;
|
|
if (p.y < 0) p.y = h;
|
|
if (p.y > h) p.y = 0;
|
|
|
|
|
|
const size = p.baseSize * (1 + intensity * 0.5 + this.kick * 0.8 * sensitivity);
|
|
ctx.globalAlpha = 0.4 + (intensity * 0.2) + this.kick * 0.15 * sensitivity;
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
for (let j = i + 1; j < this.particles.length; j++) {
|
|
const p2 = this.particles[j];
|
|
const dx = p.x - p2.x;
|
|
const dy = p.y - p2.y;
|
|
const distSq = dx*dx + dy*dy;
|
|
|
|
const maxDist = 150 + intensity * 50 + this.kick * 50 * sensitivity;
|
|
const maxDistSq = maxDist * maxDist;
|
|
|
|
if (distSq < maxDistSq) {
|
|
const dist = Math.sqrt(distSq);
|
|
ctx.beginPath();
|
|
ctx.lineWidth = (1 - dist / maxDist) * (1 + this.kick * 1.5 * sensitivity);
|
|
ctx.globalAlpha = (1 - dist / maxDist) * (0.3 + intensity * 0.2 + this.kick * 0.3 * sensitivity);
|
|
ctx.moveTo(p.x, p.y);
|
|
ctx.lineTo(p2.x, p2.y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
ctx.restore();
|
|
}
|
|
} |