IMP: toggle and blended mode for visualizer
This commit is contained in:
parent
631fdc276e
commit
35080751f9
4 changed files with 87 additions and 11 deletions
|
|
@ -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
|
// Filename template setting
|
||||||
const filenameTemplate = document.getElementById('filename-template');
|
const filenameTemplate = document.getElementById('filename-template');
|
||||||
if (filenameTemplate) {
|
if (filenameTemplate) {
|
||||||
|
|
|
||||||
|
|
@ -624,6 +624,33 @@ export const bulkDownloadSettings = {
|
||||||
export const visualizerSettings = {
|
export const visualizerSettings = {
|
||||||
SENSITIVITY_KEY: 'visualizer-sensitivity',
|
SENSITIVITY_KEY: 'visualizer-sensitivity',
|
||||||
SMART_INTENSITY_KEY: 'visualizer-smart-intensity',
|
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() {
|
getSensitivity() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
7
js/ui.js
7
js/ui.js
|
|
@ -17,7 +17,7 @@ import {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { openLyricsPanel } from './lyrics.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 { db } from './db.js';
|
||||||
import { getVibrantColorFromImage } from './vibrant-color.js';
|
import { getVibrantColorFromImage } from './vibrant-color.js';
|
||||||
import { syncManager } from './accounts/pocketbase.js';
|
import { syncManager } from './accounts/pocketbase.js';
|
||||||
|
|
@ -748,6 +748,11 @@ export class UIRenderer {
|
||||||
overlay.style.display = 'flex';
|
overlay.style.display = 'flex';
|
||||||
|
|
||||||
const startVisualizer = () => {
|
const startVisualizer = () => {
|
||||||
|
if (!visualizerSettings.isEnabled()) {
|
||||||
|
if (this.visualizer) this.visualizer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.visualizer && audioPlayer) {
|
if (!this.visualizer && audioPlayer) {
|
||||||
const canvas = document.getElementById('visualizer-canvas');
|
const canvas = document.getElementById('visualizer-canvas');
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
|
|
|
||||||
|
|
@ -82,18 +82,28 @@ export class Visualizer {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
|
|
||||||
let sensitivity = visualizerSettings.getSensitivity();
|
let sensitivity = visualizerSettings.getSensitivity();
|
||||||
ctx.fillStyle = 'rgba(10, 10, 10, 0.3)';
|
const mode = visualizerSettings.getMode();
|
||||||
ctx.fillRect(0, 0, w, h);
|
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 bufferLength = this.analyser.frequencyBinCount;
|
||||||
const dataArray = new Uint8Array(bufferLength);
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
this.analyser.getByteFrequencyData(dataArray);
|
this.analyser.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
let bassSum = 0;
|
let bassSum = 0;
|
||||||
|
|
||||||
for (let i = 0; i < 4; i++) bassSum += dataArray[i];
|
for (let i = 0; i < 4; i++) bassSum += dataArray[i];
|
||||||
const bass = bassSum / 4 / 255;
|
const bass = bassSum / 4 / 255;
|
||||||
|
|
||||||
const intensity = bass * bass;
|
const intensity = bass * bass;
|
||||||
|
|
||||||
this.energyAverage = this.energyAverage * 0.99 + intensity * 0.01;
|
this.energyAverage = this.energyAverage * 0.99 + intensity * 0.01;
|
||||||
|
|
@ -116,7 +126,6 @@ export class Visualizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (intensity > threshold) {
|
if (intensity > threshold) {
|
||||||
if (intensity > this.lastIntensity + 0.05 && now - this.lastBeatTime > 50) {
|
if (intensity > this.lastIntensity + 0.05 && now - this.lastBeatTime > 50) {
|
||||||
this.kick = 1.0;
|
this.kick = 1.0;
|
||||||
|
|
@ -158,7 +167,6 @@ export class Visualizer {
|
||||||
y: Math.random() * h,
|
y: Math.random() * h,
|
||||||
vx: (Math.random() - 0.5) * 2,
|
vx: (Math.random() - 0.5) * 2,
|
||||||
vy: (Math.random() - 0.5) * 2,
|
vy: (Math.random() - 0.5) * 2,
|
||||||
size: Math.random() * 3 + 1,
|
|
||||||
baseSize: Math.random() * 3 + 1,
|
baseSize: Math.random() * 3 + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +178,9 @@ export class Visualizer {
|
||||||
ctx.fillStyle = primaryColor;
|
ctx.fillStyle = primaryColor;
|
||||||
ctx.strokeStyle = 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++) {
|
for (let i = 0; i < this.particles.length; i++) {
|
||||||
let p = this.particles[i];
|
let p = this.particles[i];
|
||||||
|
|
||||||
|
|
@ -197,13 +208,14 @@ export class Visualizer {
|
||||||
const p2 = this.particles[j];
|
const p2 = this.particles[j];
|
||||||
const dx = p.x - p2.x;
|
const dx = p.x - p2.x;
|
||||||
const dy = p.y - p2.y;
|
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 distSq = dx * dx + dy * dy;
|
||||||
|
|
||||||
const maxDist = 150 + intensity * 50 + this.kick * 50 * sensitivity;
|
|
||||||
const maxDistSq = maxDist * maxDist;
|
|
||||||
|
|
||||||
if (distSq < maxDistSq) {
|
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.beginPath();
|
||||||
ctx.lineWidth = (1 - dist / maxDist) * (1 + this.kick * 1.5 * sensitivity);
|
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.globalAlpha = (1 - dist / maxDist) * (0.3 + intensity * 0.2 + this.kick * 0.3 * sensitivity);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue