feat: configurable ambient background FPS in settings

Add setting for immersive background animation frame rate:
- Disabled, 15 FPS (default), 30 FPS, 60 FPS, 120 FPS
- Persisted via userStorage, read at canvas init
- Disabled renders a single static frame
- Setting placed in Appearance section after immersive default view
- i18n keys added to all 4 locales (en, es, de, fr)

Visualizer panels reverted to 30fps (unrelated to ambient bg).
This commit is contained in:
vicrodh committed 2026-02-28 07:48:02 -06:00
1 parent fae4d912ab
commit 8c179ecf00
13 files changed
+94 -12

No files matched your search

@@ -45,7 +45,7 @@
let animationFrame: number | null = null;
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const FRAME_INTERVAL = 1000 / 30;
// Artwork-derived glow color
let glowColor = $state('rgba(100, 100, 255, 0.3)');
@@ -43,7 +43,7 @@
const energyData = new Float32Array(NUM_BANDS);
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const FRAME_INTERVAL = 1000 / 30;
let bandColors = $state([
{ r: 255, g: 60, b: 60 },
@@ -54,7 +54,7 @@
let trailIndex = 0;
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const FRAME_INTERVAL = 1000 / 30;
// Primary color extracted from artwork
let plotColor = $state({ r: 0, g: 220, b: 200 });
@@ -47,7 +47,7 @@
const SMOOTHING = 0.3;
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const FRAME_INTERVAL = 1000 / 30;
// Colors extracted from artwork
let colorLeft = $state({ r: 0, g: 220, b: 160 });
@@ -40,7 +40,7 @@
let isInitialized = false;
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const FRAME_INTERVAL = 1000 / 30;
// Ring particles - expanding circles triggered by transients
interface Ring {
@@ -50,9 +50,9 @@
// Smoothing for visual continuity
const SMOOTHING = 0.6;
// Throttle rendering to 60fps
// Throttle rendering to 30fps max
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const FRAME_INTERVAL = 1000 / 30;
// Colors extracted from artwork (Material You style)
let colorPrimary = $state({ r: 0, g: 220, b: 200 }); // Default cyan
@@ -1040,6 +1040,31 @@
}
}
// Immersive ambient background FPS
const AMBIENT_FPS_KEY = 'qbz-immersive-ambient-fps';
const AMBIENT_FPS_OPTIONS = ['0', '15', '30', '60', '120'] as const;
let ambientFps = $state(getUserItem(AMBIENT_FPS_KEY) || '15');
function getAmbientFpsOptions(): string[] {
return AMBIENT_FPS_OPTIONS.map(val =>
$t(`settings.appearance.ambientFpsOptions.${val === '0' ? 'disabled' : val}`)
);
}
function getAmbientFpsDisplayValue(): string {
const key = ambientFps === '0' ? 'disabled' : ambientFps;
return $t(`settings.appearance.ambientFpsOptions.${key}`);
}
function handleAmbientFpsChange(displayValue: string) {
const options = getAmbientFpsOptions();
const index = options.indexOf(displayValue);
if (index >= 0) {
ambientFps = AMBIENT_FPS_OPTIONS[index];
setUserItem(AMBIENT_FPS_KEY, ambientFps);
}
}
// Mini player default view
const MINIPLAYER_VIEW_KEYS = ['remember', 'micro', 'compact', 'artwork', 'queue', 'lyrics'] as const;
let miniPlayerDefaultView = $state(
@@ -4099,6 +4124,17 @@
onchange={handleImmersiveViewChange}
/>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">{$t('settings.appearance.ambientFps')}</span>
<span class="setting-desc">{$t('settings.appearance.ambientFpsDesc')}</span>
</div>
<Dropdown
value={getAmbientFpsDisplayValue()}
options={getAmbientFpsOptions()}
onchange={handleAmbientFpsChange}
/>
</div>
<div class="setting-row">
<span class="setting-label">{$t('settings.appearance.miniplayerDefaultView')}</span>
<Dropdown
+9
View File
@@ -760,6 +760,15 @@
"split-suggestions": "Geteilt: Vorschläge",
"split-queue": "Geteilt: Warteschlange"
},
"ambientFps": "Immersive Hintergrundanimation",
"ambientFpsDesc": "Bildrate für den Hintergrundeffekt im Immersiv-Modus. Niedrigere Werte sparen CPU. Erfordert erneutes Aufrufen des Immersiv-Modus.",
"ambientFpsOptions": {
"disabled": "Deaktiviert",
"15": "15 FPS (Niedrig)",
"30": "30 FPS (Mittel)",
"60": "60 FPS (Flüssig)",
"120": "120 FPS (Ultra)"
},
"startupPage": "Startseite",
"startupPageDesc": "Wähle welche Seite beim App-Start angezeigt wird",
"startupPages": {
+9
View File
@@ -788,6 +788,15 @@
"split-suggestions": "Split: Suggestions",
"split-queue": "Split: Queue"
},
"ambientFps": "Immersive background animation",
"ambientFpsDesc": "Frame rate for the ambient background effect in immersive mode. Lower values save CPU. Requires re-entering immersive mode.",
"ambientFpsOptions": {
"disabled": "Disabled",
"15": "15 FPS (Low)",
"30": "30 FPS (Medium)",
"60": "60 FPS (Smooth)",
"120": "120 FPS (Ultra)"
},
"miniplayerDefaultView": "Mini player default view",
"miniplayerViews": {
"remember": "Remember last used",
+9
View File
@@ -788,6 +788,15 @@
"split-suggestions": "Dividido: Sugerencias",
"split-queue": "Dividido: Cola"
},
"ambientFps": "Animación de fondo inmersivo",
"ambientFpsDesc": "Tasa de cuadros del efecto de fondo en el modo inmersivo. Valores bajos ahorran CPU. Requiere volver a entrar al modo inmersivo.",
"ambientFpsOptions": {
"disabled": "Desactivado",
"15": "15 FPS (Bajo)",
"30": "30 FPS (Medio)",
"60": "60 FPS (Fluido)",
"120": "120 FPS (Ultra)"
},
"miniplayerDefaultView": "Vista predeterminada del mini reproductor",
"miniplayerViews": {
"remember": "Recordar la última usada",
+9
View File
@@ -760,6 +760,15 @@
"split-suggestions": "Divisé : Suggestions",
"split-queue": "Divisé : File d'attente"
},
"ambientFps": "Animation de fond immersif",
"ambientFpsDesc": "Fréquence d'images de l'effet de fond en mode immersif. Des valeurs basses économisent le CPU. Nécessite de ré-entrer en mode immersif.",
"ambientFpsOptions": {
"disabled": "Désactivé",
"15": "15 FPS (Bas)",
"30": "30 FPS (Moyen)",
"60": "60 FPS (Fluide)",
"120": "120 FPS (Ultra)"
},
"startupPage": "Page de démarrage",
"startupPageDesc": "Choisissez quelle page afficher au lancement de l'application",
"startupPages": {
@@ -34,6 +34,7 @@
handleContextRestored,
getConfig,
} from './ImmersiveRenderer';
import { getUserItem } from '$lib/utils/userStorage';
// Props
interface Props {
@@ -80,9 +81,10 @@
let lastFrameTime = 0;
let isVisible = true;
// Frame throttling
const TARGET_FPS = 60;
const FRAME_INTERVAL = 1000 / TARGET_FPS;
// Frame throttling — read user preference (0 = disabled, default 15)
const AMBIENT_FPS_KEY = 'qbz-immersive-ambient-fps';
const ambientFps = parseInt(getUserItem(AMBIENT_FPS_KEY) || '15', 10);
const FRAME_INTERVAL = ambientFps > 0 ? 1000 / ambientFps : 0;
/**
* Initialize WebGL resources.
@@ -250,11 +252,19 @@
*/
function startAnimation(): void {
if (isAnimating || !isInitialized) return;
// Animation disabled — render a single static frame
if (ambientFps === 0) {
render();
console.log('[ImmersiveCanvas] Animation disabled, rendered static frame');
return;
}
isAnimating = true;
lastFrameTime = performance.now();
startTime = performance.now(); // Reset time for animation
animationFrameId = requestAnimationFrame(animationLoop);
console.log('[ImmersiveCanvas] Animation started, intensity:', intensity ?? getConfig().ambientIntensity);
console.log('[ImmersiveCanvas] Animation started at', ambientFps, 'fps, intensity:', intensity ?? getConfig().ambientIntensity);
}
/**
+1 -1
View File
@@ -56,7 +56,7 @@ export interface ImmersiveConfig {
/** Default configuration */
export const DEFAULT_IMMERSIVE_CONFIG: ImmersiveConfig = {
targetFps: 60,
targetFps: 15,
ambientIntensity: 1.0, // Full intensity for maximum ambient motion
pauseWhenHidden: true,
debugOverlay: false,