IMP: toggle and blended mode for visualizer

This commit is contained in:
Julien Maille 2026-01-25 10:47:58 +01:00
parent 631fdc276e
commit 35080751f9
4 changed files with 87 additions and 11 deletions

View file

@ -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) {

View file

@ -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 {

View file

@ -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) {

View file

@ -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);