diff --git a/js/settings.js b/js/settings.js index c38cf6d..7d1cb45 100644 --- a/js/settings.js +++ b/js/settings.js @@ -447,6 +447,38 @@ export function initializeSettings(scrobbler, player, api, ui) { }); } + // Visualizer Enabled Toggle + const visualizerEnabledToggle = document.getElementById('visualizer-enabled-toggle'); + const visualizerModeSetting = document.getElementById('visualizer-mode-setting'); + const visualizerSmartIntensitySetting = document.getElementById('visualizer-smart-intensity-setting'); + const visualizerSensitivitySetting = document.getElementById('visualizer-sensitivity-setting'); + + const updateVisualizerSettingsVisibility = (enabled) => { + const display = enabled ? 'flex' : 'none'; + if (visualizerModeSetting) visualizerModeSetting.style.display = display; + if (visualizerSmartIntensitySetting) visualizerSmartIntensitySetting.style.display = display; + if (visualizerSensitivitySetting) visualizerSensitivitySetting.style.display = display; + }; + + if (visualizerEnabledToggle) { + visualizerEnabledToggle.checked = visualizerSettings.isEnabled(); + updateVisualizerSettingsVisibility(visualizerEnabledToggle.checked); + + visualizerEnabledToggle.addEventListener('change', (e) => { + visualizerSettings.setEnabled(e.target.checked); + updateVisualizerSettingsVisibility(e.target.checked); + }); + } + + // Visualizer Mode Select + const visualizerModeSelect = document.getElementById('visualizer-mode-select'); + if (visualizerModeSelect) { + visualizerModeSelect.value = visualizerSettings.getMode(); + visualizerModeSelect.addEventListener('change', (e) => { + visualizerSettings.setMode(e.target.value); + }); + } + // Filename template setting const filenameTemplate = document.getElementById('filename-template'); if (filenameTemplate) { diff --git a/js/storage.js b/js/storage.js index d65a9eb..0deb145 100644 --- a/js/storage.js +++ b/js/storage.js @@ -624,6 +624,33 @@ export const bulkDownloadSettings = { export const visualizerSettings = { SENSITIVITY_KEY: 'visualizer-sensitivity', SMART_INTENSITY_KEY: 'visualizer-smart-intensity', + ENABLED_KEY: 'visualizer-enabled', + MODE_KEY: 'visualizer-mode', // 'solid' or 'blended' + + isEnabled() { + try { + const val = localStorage.getItem(this.ENABLED_KEY); + return val === null ? true : val === 'true'; + } catch { + return true; + } + }, + + setEnabled(enabled) { + localStorage.setItem(this.ENABLED_KEY, enabled); + }, + + getMode() { + try { + return localStorage.getItem(this.MODE_KEY) || 'solid'; + } catch { + return 'solid'; + } + }, + + setMode(mode) { + localStorage.setItem(this.MODE_KEY, mode); + }, getSensitivity() { try { diff --git a/js/ui.js b/js/ui.js index bae921f..42d5502 100644 --- a/js/ui.js +++ b/js/ui.js @@ -17,7 +17,7 @@ import { escapeHtml, } from './utils.js'; import { openLyricsPanel } from './lyrics.js'; -import { recentActivityManager, backgroundSettings, cardSettings } from './storage.js'; +import { recentActivityManager, backgroundSettings, cardSettings, visualizerSettings } from './storage.js'; import { db } from './db.js'; import { getVibrantColorFromImage } from './vibrant-color.js'; import { syncManager } from './accounts/pocketbase.js'; @@ -748,6 +748,11 @@ export class UIRenderer { overlay.style.display = 'flex'; const startVisualizer = () => { + if (!visualizerSettings.isEnabled()) { + if (this.visualizer) this.visualizer.stop(); + return; + } + if (!this.visualizer && audioPlayer) { const canvas = document.getElementById('visualizer-canvas'); if (canvas) { diff --git a/js/visualizer.js b/js/visualizer.js index 65efa8c..135f6e9 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -82,18 +82,28 @@ export class Visualizer { const ctx = this.ctx; let sensitivity = visualizerSettings.getSensitivity(); - ctx.fillStyle = 'rgba(10, 10, 10, 0.3)'; - ctx.fillRect(0, 0, w, h); + const mode = visualizerSettings.getMode(); + const isDark = document.documentElement.getAttribute('data-theme') !== 'light'; + + if (mode === 'blended') { + ctx.clearRect(0, 0, w, h); + } else { + // Match background to theme if in solid mode + if (isDark) { + ctx.fillStyle = 'rgba(10, 10, 10, 0.3)'; + } else { + ctx.fillStyle = 'rgba(240, 240, 240, 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; @@ -116,7 +126,6 @@ export class Visualizer { } const now = Date.now(); - if (intensity > threshold) { if (intensity > this.lastIntensity + 0.05 && now - this.lastBeatTime > 50) { this.kick = 1.0; @@ -158,7 +167,6 @@ export class Visualizer { 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, }); } @@ -170,6 +178,9 @@ export class Visualizer { ctx.fillStyle = primaryColor; ctx.strokeStyle = primaryColor; + const maxDist = 150 + intensity * 50 + this.kick * 50 * sensitivity; + const maxDistSq = maxDist * maxDist; + for (let i = 0; i < this.particles.length; i++) { let p = this.particles[i]; @@ -197,13 +208,14 @@ export class Visualizer { const p2 = this.particles[j]; const dx = p.x - p2.x; const dy = p.y - p2.y; + + // Optimization: Early exit for x distance + if (Math.abs(dx) > maxDist) continue; + 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); + const dist = Math.sqrt(distSq); // Still need dist for alpha/linewidth, but now we only sqrt when necessary 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);