feat: per-panel FPS settings for all immersive animations
All animations default to 60fps. Each panel reads its own setting from userStorage via shared fpsConfig utility. Settings UI shows a collapsed section with per-panel dropdowns: - Background Blur, Spectrum, X/Y (Lissajous), Oscilloscope, Energy Bands, Transient Pulse, Album Reactive, Spectral Ribbon Options: Disabled, 15, 30, 60, 120 FPS i18n keys added to all 4 locales (en, es, de, fr)
This commit is contained in:
15 files changed
+164
-52
No files matched your search
@@ -3,6 +3,7 @@
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||
import { getPanelFrameInterval } from '$lib/immersive/fpsConfig';
|
||||
|
||||
interface Props {
|
||||
enabled?: boolean;
|
||||
@@ -45,7 +46,7 @@
|
||||
|
||||
let animationFrame: number | null = null;
|
||||
let lastRenderTime = 0;
|
||||
const FRAME_INTERVAL = 1000 / 30;
|
||||
const FRAME_INTERVAL = getPanelFrameInterval('album-reactive');
|
||||
|
||||
// Artwork-derived glow color
|
||||
let glowColor = $state('rgba(100, 100, 255, 0.3)');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||
import { getPanelFrameInterval } from '$lib/immersive/fpsConfig';
|
||||
|
||||
interface Props {
|
||||
enabled?: boolean;
|
||||
@@ -43,7 +44,7 @@
|
||||
const energyData = new Float32Array(NUM_BANDS);
|
||||
|
||||
let lastRenderTime = 0;
|
||||
const FRAME_INTERVAL = 1000 / 30;
|
||||
const FRAME_INTERVAL = getPanelFrameInterval('energy-bands');
|
||||
|
||||
let bandColors = $state([
|
||||
{ r: 255, g: 60, b: 60 },
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||
import { getPanelFrameInterval } from '$lib/immersive/fpsConfig';
|
||||
|
||||
interface Props {
|
||||
enabled?: boolean;
|
||||
@@ -54,7 +55,7 @@
|
||||
let trailIndex = 0;
|
||||
|
||||
let lastRenderTime = 0;
|
||||
const FRAME_INTERVAL = 1000 / 30;
|
||||
const FRAME_INTERVAL = getPanelFrameInterval('lissajous');
|
||||
|
||||
// Primary color extracted from artwork
|
||||
let plotColor = $state({ r: 0, g: 220, b: 200 });
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||
import { getPanelFrameInterval } from '$lib/immersive/fpsConfig';
|
||||
|
||||
interface Props {
|
||||
enabled?: boolean;
|
||||
@@ -47,7 +48,7 @@
|
||||
const SMOOTHING = 0.3;
|
||||
|
||||
let lastRenderTime = 0;
|
||||
const FRAME_INTERVAL = 1000 / 30;
|
||||
const FRAME_INTERVAL = getPanelFrameInterval('oscilloscope');
|
||||
|
||||
// Colors extracted from artwork
|
||||
let colorLeft = $state({ r: 0, g: 220, b: 160 });
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||
import { getPanelFrameInterval } from '$lib/immersive/fpsConfig';
|
||||
|
||||
interface Props {
|
||||
enabled?: boolean;
|
||||
@@ -40,8 +41,7 @@
|
||||
|
||||
const NUM_BANDS = 190;
|
||||
const HISTORY_BINS = 4096;
|
||||
const TARGET_FPS = 60;
|
||||
const FRAME_INTERVAL_MS = 1000 / TARGET_FPS;
|
||||
const FRAME_INTERVAL_MS = getPanelFrameInterval('spectral-ribbon');
|
||||
|
||||
let canvasRef: HTMLCanvasElement | null = $state(null);
|
||||
let canvasCtx: CanvasRenderingContext2D | null = null;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||
import { getPanelFrameInterval } from '$lib/immersive/fpsConfig';
|
||||
|
||||
interface Props {
|
||||
enabled?: boolean;
|
||||
@@ -40,7 +41,7 @@
|
||||
let isInitialized = false;
|
||||
|
||||
let lastRenderTime = 0;
|
||||
const FRAME_INTERVAL = 1000 / 30;
|
||||
const FRAME_INTERVAL = getPanelFrameInterval('transient-pulse');
|
||||
|
||||
// Ring particles - expanding circles triggered by transients
|
||||
interface Ring {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||
import { getPanelFrameInterval } from '$lib/immersive/fpsConfig';
|
||||
|
||||
interface Props {
|
||||
enabled?: boolean;
|
||||
@@ -50,9 +51,8 @@
|
||||
// Smoothing for visual continuity
|
||||
const SMOOTHING = 0.6;
|
||||
|
||||
// Throttle rendering to 30fps max
|
||||
let lastRenderTime = 0;
|
||||
const FRAME_INTERVAL = 1000 / 30;
|
||||
const FRAME_INTERVAL = getPanelFrameInterval('visualizer');
|
||||
|
||||
// Colors extracted from artwork (Material You style)
|
||||
let colorPrimary = $state({ r: 0, g: 220, b: 200 }); // Default cyan
|
||||
@@ -155,7 +155,7 @@
|
||||
function render(timestamp: number = 0) {
|
||||
if (!ctx || !canvasRef) return;
|
||||
|
||||
// Throttle to 60fps
|
||||
// Frame throttle
|
||||
const delta = timestamp - lastRenderTime;
|
||||
if (delta < FRAME_INTERVAL) {
|
||||
animationFrame = requestAnimationFrame(render);
|
||||
|
||||
@@ -1040,28 +1040,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
// Immersive FPS settings (per-panel)
|
||||
const FPS_KEY_PREFIX = 'qbz-immersive-fps-';
|
||||
const FPS_OPTIONS = ['0', '15', '30', '60', '120'] as const;
|
||||
const FPS_PANEL_IDS = [
|
||||
'ambient', 'visualizer', 'lissajous', 'oscilloscope',
|
||||
'energy-bands', 'transient-pulse', 'album-reactive', 'spectral-ribbon'
|
||||
] as const;
|
||||
|
||||
function getAmbientFpsOptions(): string[] {
|
||||
return AMBIENT_FPS_OPTIONS.map(val =>
|
||||
$t(`settings.appearance.ambientFpsOptions.${val === '0' ? 'disabled' : val}`)
|
||||
let immersiveFpsCollapsed = $state(true);
|
||||
let panelFpsValues: Record<string, string> = $state(
|
||||
Object.fromEntries(FPS_PANEL_IDS.map(id => [id, getUserItem(`${FPS_KEY_PREFIX}${id}`) || '60']))
|
||||
);
|
||||
|
||||
function getFpsOptions(): string[] {
|
||||
return FPS_OPTIONS.map(val =>
|
||||
$t(`settings.appearance.fpsOptions.${val === '0' ? 'disabled' : val}`)
|
||||
);
|
||||
}
|
||||
|
||||
function getAmbientFpsDisplayValue(): string {
|
||||
const key = ambientFps === '0' ? 'disabled' : ambientFps;
|
||||
return $t(`settings.appearance.ambientFpsOptions.${key}`);
|
||||
function getFpsDisplayValue(panelId: string): string {
|
||||
const val = panelFpsValues[panelId] || '60';
|
||||
const key = val === '0' ? 'disabled' : val;
|
||||
return $t(`settings.appearance.fpsOptions.${key}`);
|
||||
}
|
||||
|
||||
function handleAmbientFpsChange(displayValue: string) {
|
||||
const options = getAmbientFpsOptions();
|
||||
function handleFpsChange(panelId: string, displayValue: string) {
|
||||
const options = getFpsOptions();
|
||||
const index = options.indexOf(displayValue);
|
||||
if (index >= 0) {
|
||||
ambientFps = AMBIENT_FPS_OPTIONS[index];
|
||||
setUserItem(AMBIENT_FPS_KEY, ambientFps);
|
||||
panelFpsValues[panelId] = FPS_OPTIONS[index];
|
||||
setUserItem(`${FPS_KEY_PREFIX}${panelId}`, FPS_OPTIONS[index]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4124,16 +4133,27 @@
|
||||
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 class="collapsible-section">
|
||||
<button
|
||||
class="section-title-btn"
|
||||
onclick={() => immersiveFpsCollapsed = !immersiveFpsCollapsed}
|
||||
>
|
||||
<h4 class="subsection-title">{$t('settings.appearance.immersiveFps.title')}</h4>
|
||||
<span class="collapse-icon" class:collapsed={immersiveFpsCollapsed}>▼</span>
|
||||
</button>
|
||||
{#if !immersiveFpsCollapsed}
|
||||
<p class="setting-desc" style="margin-bottom: 12px;">{$t('settings.appearance.immersiveFps.desc')}</p>
|
||||
{#each FPS_PANEL_IDS as panelId}
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">{$t(`settings.appearance.immersiveFps.panels.${panelId}`)}</span>
|
||||
<Dropdown
|
||||
value={getFpsDisplayValue(panelId)}
|
||||
options={getFpsOptions()}
|
||||
onchange={(val) => handleFpsChange(panelId, val)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">{$t('settings.appearance.miniplayerDefaultView')}</span>
|
||||
|
||||
@@ -760,9 +760,21 @@
|
||||
"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": {
|
||||
"immersiveFps": {
|
||||
"title": "Bildrate der Immersiv-Animationen",
|
||||
"desc": "Bildrate pro Animation konfigurieren. Niedrigere Werte sparen CPU. Erfordert erneutes Aufrufen des Immersiv-Modus.",
|
||||
"panels": {
|
||||
"ambient": "Hintergrund-Unschärfe",
|
||||
"visualizer": "Spektrum-Visualizer",
|
||||
"lissajous": "X/Y (Lissajous)",
|
||||
"oscilloscope": "Oszilloskop",
|
||||
"energy-bands": "Energiebänder",
|
||||
"transient-pulse": "Transientenpuls",
|
||||
"album-reactive": "Album-Reaktiv",
|
||||
"spectral-ribbon": "Spektralband"
|
||||
}
|
||||
},
|
||||
"fpsOptions": {
|
||||
"disabled": "Deaktiviert",
|
||||
"15": "15 FPS (Niedrig)",
|
||||
"30": "30 FPS (Mittel)",
|
||||
|
||||
@@ -788,9 +788,21 @@
|
||||
"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": {
|
||||
"immersiveFps": {
|
||||
"title": "Immersive Animation Frame Rate",
|
||||
"desc": "Configure frame rate per animation. Lower values save CPU. Requires re-entering immersive mode to apply.",
|
||||
"panels": {
|
||||
"ambient": "Background Blur",
|
||||
"visualizer": "Spectrum Visualizer",
|
||||
"lissajous": "X/Y (Lissajous)",
|
||||
"oscilloscope": "Oscilloscope",
|
||||
"energy-bands": "Energy Bands",
|
||||
"transient-pulse": "Transient Pulse",
|
||||
"album-reactive": "Album Reactive",
|
||||
"spectral-ribbon": "Spectral Ribbon"
|
||||
}
|
||||
},
|
||||
"fpsOptions": {
|
||||
"disabled": "Disabled",
|
||||
"15": "15 FPS (Low)",
|
||||
"30": "30 FPS (Medium)",
|
||||
|
||||
@@ -788,9 +788,21 @@
|
||||
"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": {
|
||||
"immersiveFps": {
|
||||
"title": "Tasa de Cuadros de Animaciones Inmersivas",
|
||||
"desc": "Configura la tasa de cuadros por animación. Valores bajos ahorran CPU. Requiere volver a entrar al modo inmersivo.",
|
||||
"panels": {
|
||||
"ambient": "Fondo Difuminado",
|
||||
"visualizer": "Espectro",
|
||||
"lissajous": "X/Y (Lissajous)",
|
||||
"oscilloscope": "Osciloscopio",
|
||||
"energy-bands": "Bandas de Energía",
|
||||
"transient-pulse": "Pulso Transitorio",
|
||||
"album-reactive": "Álbum Reactivo",
|
||||
"spectral-ribbon": "Cinta Espectral"
|
||||
}
|
||||
},
|
||||
"fpsOptions": {
|
||||
"disabled": "Desactivado",
|
||||
"15": "15 FPS (Bajo)",
|
||||
"30": "30 FPS (Medio)",
|
||||
|
||||
@@ -760,9 +760,21 @@
|
||||
"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": {
|
||||
"immersiveFps": {
|
||||
"title": "Fréquence d'Images des Animations Immersives",
|
||||
"desc": "Configurez la fréquence d'images par animation. Des valeurs basses économisent le CPU. Nécessite de ré-entrer en mode immersif.",
|
||||
"panels": {
|
||||
"ambient": "Flou d'Arrière-plan",
|
||||
"visualizer": "Visualiseur de Spectre",
|
||||
"lissajous": "X/Y (Lissajous)",
|
||||
"oscilloscope": "Oscilloscope",
|
||||
"energy-bands": "Bandes d'Énergie",
|
||||
"transient-pulse": "Impulsion Transitoire",
|
||||
"album-reactive": "Album Réactif",
|
||||
"spectral-ribbon": "Ruban Spectral"
|
||||
}
|
||||
},
|
||||
"fpsOptions": {
|
||||
"disabled": "Désactivé",
|
||||
"15": "15 FPS (Bas)",
|
||||
"30": "30 FPS (Moyen)",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
handleContextRestored,
|
||||
getConfig,
|
||||
} from './ImmersiveRenderer';
|
||||
import { getUserItem } from '$lib/utils/userStorage';
|
||||
import { getPanelFps, getPanelFrameInterval } from './fpsConfig';
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
@@ -81,10 +81,9 @@
|
||||
let lastFrameTime = 0;
|
||||
let isVisible = true;
|
||||
|
||||
// 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;
|
||||
// Frame throttling — read user preference (0 = disabled, default 60)
|
||||
const ambientFps = getPanelFps('ambient');
|
||||
const FRAME_INTERVAL = getPanelFrameInterval('ambient');
|
||||
|
||||
/**
|
||||
* Initialize WebGL resources.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Immersive FPS Configuration
|
||||
*
|
||||
* Reads per-panel frame rate settings from userStorage.
|
||||
* Default is 60fps for all panels.
|
||||
*/
|
||||
|
||||
import { getUserItem } from '$lib/utils/userStorage';
|
||||
|
||||
const FPS_KEY_PREFIX = 'qbz-immersive-fps-';
|
||||
const DEFAULT_FPS = 60;
|
||||
|
||||
export type ImmersivePanelId =
|
||||
| 'ambient'
|
||||
| 'visualizer'
|
||||
| 'lissajous'
|
||||
| 'oscilloscope'
|
||||
| 'energy-bands'
|
||||
| 'transient-pulse'
|
||||
| 'album-reactive'
|
||||
| 'spectral-ribbon';
|
||||
|
||||
/**
|
||||
* Get the configured FPS for a panel.
|
||||
* Returns 0 for disabled, or the FPS value.
|
||||
*/
|
||||
export function getPanelFps(panelId: ImmersivePanelId): number {
|
||||
const stored = getUserItem(`${FPS_KEY_PREFIX}${panelId}`);
|
||||
if (stored === null) return DEFAULT_FPS;
|
||||
const parsed = parseInt(stored, 10);
|
||||
return isNaN(parsed) ? DEFAULT_FPS : parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frame interval in ms for a panel (0 if disabled).
|
||||
*/
|
||||
export function getPanelFrameInterval(panelId: ImmersivePanelId): number {
|
||||
const fps = getPanelFps(panelId);
|
||||
return fps > 0 ? 1000 / fps : 0;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export interface ImmersiveConfig {
|
||||
|
||||
/** Default configuration */
|
||||
export const DEFAULT_IMMERSIVE_CONFIG: ImmersiveConfig = {
|
||||
targetFps: 15,
|
||||
targetFps: 60,
|
||||
ambientIntensity: 1.0, // Full intensity for maximum ambient motion
|
||||
pauseWhenHidden: true,
|
||||
debugOverlay: false,
|
||||
|
||||
Reference in new issue
Block a user