Compare commits
1 Commits
0eca8292cb
...
be48318212
| Author | SHA1 | Date | |
|---|---|---|---|
| be48318212 |
@@ -63,6 +63,27 @@
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Dynamic Background Canvas */
|
||||
.bg-shader-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
.bg-shader-canvas.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.dynamic-bg-active {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -218,6 +239,10 @@ h1 {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.header-btn.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.header-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -3165,7 +3190,6 @@ footer .separator {
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
animation: bannerSlideIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.connection-banner:not(.hidden) {
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Background -->
|
||||
<canvas id="bg-shader-canvas" class="bg-shader-canvas"></canvas>
|
||||
|
||||
<!-- Auth Modal -->
|
||||
<div id="auth-overlay" class="hidden">
|
||||
<div class="auth-modal">
|
||||
@@ -86,6 +89,9 @@
|
||||
</button>
|
||||
<div class="accent-picker-dropdown" id="accentDropdown"></div>
|
||||
</div>
|
||||
<button class="header-btn" onclick="toggleDynamicBackground()" data-i18n-title="player.background" title="Dynamic background" aria-label="Dynamic background" id="bgToggle">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>
|
||||
</button>
|
||||
<button class="header-btn" onclick="toggleTheme()" data-i18n-title="player.theme" title="Toggle theme" aria-label="Toggle theme" id="theme-toggle">
|
||||
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
|
||||
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
|
||||
@@ -652,6 +658,7 @@
|
||||
<script src="/static/js/callbacks.js"></script>
|
||||
<script src="/static/js/browser.js"></script>
|
||||
<script src="/static/js/links.js"></script>
|
||||
<script src="/static/js/background.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
314
media_server/static/js/background.js
Normal file
314
media_server/static/js/background.js
Normal file
@@ -0,0 +1,314 @@
|
||||
// ============================================================
|
||||
// Background: WebGL shader-based dynamic background
|
||||
// ============================================================
|
||||
|
||||
let bgCanvas = null;
|
||||
let bgGL = null;
|
||||
let bgProgram = null;
|
||||
let bgUniforms = null; // Cached uniform locations
|
||||
let bgAnimFrame = null;
|
||||
let bgEnabled = localStorage.getItem('dynamicBackground') === 'true';
|
||||
let bgStartTime = 0;
|
||||
let bgSmoothedBands = new Float32Array(16);
|
||||
let bgSmoothedBass = 0;
|
||||
let bgAccentRGB = [0.114, 0.725, 0.329]; // Cached accent color (default green)
|
||||
let bgBgColorRGB = [0.071, 0.071, 0.071]; // Cached page background (#121212)
|
||||
|
||||
const BG_BAND_COUNT = 16;
|
||||
const BG_SMOOTHING = 0.12;
|
||||
|
||||
// ---- Shaders ----
|
||||
|
||||
const BG_VERT_SRC = `
|
||||
attribute vec2 a_position;
|
||||
void main() {
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const BG_FRAG_SRC = `
|
||||
precision mediump float;
|
||||
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_time;
|
||||
uniform float u_bass;
|
||||
uniform float u_bands[16];
|
||||
uniform vec3 u_accent;
|
||||
uniform vec3 u_bgColor;
|
||||
|
||||
// Smooth noise
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||
}
|
||||
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
float a = hash(i);
|
||||
float b = hash(i + vec2(1.0, 0.0));
|
||||
float c = hash(i + vec2(0.0, 1.0));
|
||||
float d = hash(i + vec2(1.0, 1.0));
|
||||
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / u_resolution;
|
||||
float aspect = u_resolution.x / u_resolution.y;
|
||||
|
||||
// Center coordinates for radial effects
|
||||
vec2 center = (uv - 0.5) * vec2(aspect, 1.0);
|
||||
float dist = length(center);
|
||||
float angle = atan(center.y, center.x);
|
||||
|
||||
// Slow base animation
|
||||
float t = u_time * 0.15;
|
||||
|
||||
// === Layer 1: Flowing wave field ===
|
||||
float waves = 0.0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float fi = float(i);
|
||||
float freq = 1.5 + fi * 0.8;
|
||||
float speed = t * (0.6 + fi * 0.15);
|
||||
// Sample a band for this wave layer
|
||||
int bandIdx = i * 3;
|
||||
float bandVal = 0.0;
|
||||
// Manual indexing (GLSL ES doesn't allow variable array index in some drivers)
|
||||
for (int j = 0; j < 16; j++) {
|
||||
if (j == bandIdx) bandVal = u_bands[j];
|
||||
}
|
||||
float amp = 0.015 + bandVal * 0.06;
|
||||
waves += amp * sin(uv.x * freq * 6.2832 + speed + sin(uv.y * 3.0 + t) * 2.0);
|
||||
waves += amp * 0.5 * sin(uv.y * freq * 4.0 - speed * 0.7 + cos(uv.x * 2.5 + t) * 1.5);
|
||||
}
|
||||
|
||||
// === Layer 2: Radial pulse (bass-driven) ===
|
||||
float pulse = smoothstep(0.6 + u_bass * 0.3, 0.0, dist) * (0.08 + u_bass * 0.15);
|
||||
|
||||
// === Layer 3: Frequency ring arcs ===
|
||||
float rings = 0.0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
float fi = float(i);
|
||||
float bandVal = 0.0;
|
||||
for (int j = 0; j < 16; j++) {
|
||||
if (j == i * 2) bandVal = u_bands[j];
|
||||
}
|
||||
float radius = 0.15 + fi * 0.1;
|
||||
float ringWidth = 0.008 + bandVal * 0.025;
|
||||
float ring = smoothstep(ringWidth, 0.0, abs(dist - radius - bandVal * 0.05));
|
||||
// Fade ring by angle sector for variety
|
||||
float angleFade = 0.5 + 0.5 * sin(angle * (2.0 + fi) + t * (1.0 + fi * 0.3));
|
||||
rings += ring * angleFade * (0.3 + bandVal * 0.7);
|
||||
}
|
||||
|
||||
// === Layer 4: Subtle noise texture ===
|
||||
float n = noise(uv * 4.0 + t * 0.5) * 0.03;
|
||||
|
||||
// Combine layers
|
||||
float intensity = waves + pulse + rings * 0.5 + n;
|
||||
|
||||
// Color: accent color with varying brightness
|
||||
vec3 col = u_accent * intensity;
|
||||
|
||||
// Subtle secondary hue shift for depth
|
||||
vec3 shifted = u_accent.gbr; // Rotated accent
|
||||
col += shifted * rings * 0.15;
|
||||
|
||||
// Vignette
|
||||
float vignette = 1.0 - smoothstep(0.3, 1.2, dist);
|
||||
col *= vignette;
|
||||
|
||||
// Blend over page background
|
||||
col = clamp(col, 0.0, 1.0);
|
||||
float colBright = (col.r + col.g + col.b) / 3.0;
|
||||
float bgLum = dot(u_bgColor, vec3(0.299, 0.587, 0.114));
|
||||
// Dark bg: add accent light. Light bg: tint white toward accent via multiply.
|
||||
vec3 darkResult = u_bgColor + col;
|
||||
vec3 lightResult = u_bgColor * mix(vec3(1.0), u_accent, colBright * 2.0);
|
||||
vec3 finalColor = clamp(mix(darkResult, lightResult, bgLum), 0.0, 1.0);
|
||||
|
||||
gl_FragColor = vec4(finalColor, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
// ---- WebGL setup ----
|
||||
|
||||
function initBackgroundGL() {
|
||||
bgCanvas = document.getElementById('bg-shader-canvas');
|
||||
if (!bgCanvas) return false;
|
||||
|
||||
bgGL = bgCanvas.getContext('webgl', { alpha: false, antialias: false, depth: false, stencil: false });
|
||||
if (!bgGL) {
|
||||
console.warn('WebGL not available for background shader');
|
||||
return false;
|
||||
}
|
||||
|
||||
const gl = bgGL;
|
||||
|
||||
// Compile shaders
|
||||
const vs = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(vs, BG_VERT_SRC);
|
||||
gl.compileShader(vs);
|
||||
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
|
||||
console.error('BG vertex shader:', gl.getShaderInfoLog(vs));
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(fs, BG_FRAG_SRC);
|
||||
gl.compileShader(fs);
|
||||
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
|
||||
console.error('BG fragment shader:', gl.getShaderInfoLog(fs));
|
||||
return false;
|
||||
}
|
||||
|
||||
bgProgram = gl.createProgram();
|
||||
gl.attachShader(bgProgram, vs);
|
||||
gl.attachShader(bgProgram, fs);
|
||||
gl.linkProgram(bgProgram);
|
||||
if (!gl.getProgramParameter(bgProgram, gl.LINK_STATUS)) {
|
||||
console.error('BG program link:', gl.getProgramInfoLog(bgProgram));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fullscreen quad
|
||||
const buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||
-1, -1, 1, -1, -1, 1,
|
||||
-1, 1, 1, -1, 1, 1
|
||||
]), gl.STATIC_DRAW);
|
||||
|
||||
const aPos = gl.getAttribLocation(bgProgram, 'a_position');
|
||||
gl.enableVertexAttribArray(aPos);
|
||||
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.useProgram(bgProgram);
|
||||
|
||||
// Cache uniform locations once (avoids per-frame lookups)
|
||||
bgUniforms = {
|
||||
resolution: gl.getUniformLocation(bgProgram, 'u_resolution'),
|
||||
time: gl.getUniformLocation(bgProgram, 'u_time'),
|
||||
bass: gl.getUniformLocation(bgProgram, 'u_bass'),
|
||||
bands: gl.getUniformLocation(bgProgram, 'u_bands'),
|
||||
accent: gl.getUniformLocation(bgProgram, 'u_accent'),
|
||||
bgColor: gl.getUniformLocation(bgProgram, 'u_bgColor'),
|
||||
};
|
||||
|
||||
bgStartTime = performance.now() / 1000;
|
||||
updateBackgroundColors();
|
||||
resizeBackgroundCanvas();
|
||||
window.addEventListener('resize', resizeBackgroundCanvas);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function resizeBackgroundCanvas() {
|
||||
if (!bgCanvas) return;
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 1.5); // Cap DPR for performance
|
||||
const w = Math.floor(window.innerWidth * dpr);
|
||||
const h = Math.floor(window.innerHeight * dpr);
|
||||
if (bgCanvas.width !== w || bgCanvas.height !== h) {
|
||||
bgCanvas.width = w;
|
||||
bgCanvas.height = h;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Cached color/theme updates (called on accent or theme change, not per-frame) ----
|
||||
|
||||
function updateBackgroundColors() {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const accentHex = style.getPropertyValue('--accent').trim();
|
||||
if (accentHex && accentHex.length >= 7) {
|
||||
bgAccentRGB[0] = parseInt(accentHex.slice(1, 3), 16) / 255;
|
||||
bgAccentRGB[1] = parseInt(accentHex.slice(3, 5), 16) / 255;
|
||||
bgAccentRGB[2] = parseInt(accentHex.slice(5, 7), 16) / 255;
|
||||
}
|
||||
const bgHex = style.getPropertyValue('--bg-primary').trim();
|
||||
if (bgHex && bgHex.length >= 7) {
|
||||
bgBgColorRGB[0] = parseInt(bgHex.slice(1, 3), 16) / 255;
|
||||
bgBgColorRGB[1] = parseInt(bgHex.slice(3, 5), 16) / 255;
|
||||
bgBgColorRGB[2] = parseInt(bgHex.slice(5, 7), 16) / 255;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Render loop ----
|
||||
|
||||
function renderBackgroundFrame() {
|
||||
bgAnimFrame = requestAnimationFrame(renderBackgroundFrame);
|
||||
|
||||
const gl = bgGL;
|
||||
if (!gl || !bgUniforms) return;
|
||||
|
||||
resizeBackgroundCanvas();
|
||||
gl.viewport(0, 0, bgCanvas.width, bgCanvas.height);
|
||||
|
||||
const time = performance.now() / 1000 - bgStartTime;
|
||||
|
||||
// Smooth audio data from the global frequencyData (shared with visualizer)
|
||||
if (typeof frequencyData !== 'undefined' && frequencyData && frequencyData.frequencies) {
|
||||
const bins = frequencyData.frequencies;
|
||||
const step = Math.max(1, Math.floor(bins.length / BG_BAND_COUNT));
|
||||
for (let i = 0; i < BG_BAND_COUNT; i++) {
|
||||
const idx = Math.min(i * step, bins.length - 1);
|
||||
const target = bins[idx] || 0;
|
||||
bgSmoothedBands[i] += (target - bgSmoothedBands[i]) * (1 - BG_SMOOTHING);
|
||||
}
|
||||
const targetBass = frequencyData.bass || 0;
|
||||
bgSmoothedBass += (targetBass - bgSmoothedBass) * (1 - BG_SMOOTHING);
|
||||
} else {
|
||||
// Gentle decay when no audio
|
||||
for (let i = 0; i < BG_BAND_COUNT; i++) {
|
||||
bgSmoothedBands[i] *= 0.95;
|
||||
}
|
||||
bgSmoothedBass *= 0.95;
|
||||
}
|
||||
|
||||
// Set uniforms (locations cached at init, colors cached on change)
|
||||
gl.uniform2f(bgUniforms.resolution, bgCanvas.width, bgCanvas.height);
|
||||
gl.uniform1f(bgUniforms.time, time);
|
||||
gl.uniform1f(bgUniforms.bass, bgSmoothedBass);
|
||||
gl.uniform1fv(bgUniforms.bands, bgSmoothedBands);
|
||||
gl.uniform3f(bgUniforms.accent, bgAccentRGB[0], bgAccentRGB[1], bgAccentRGB[2]);
|
||||
gl.uniform3f(bgUniforms.bgColor, bgBgColorRGB[0], bgBgColorRGB[1], bgBgColorRGB[2]);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
function startBackground() {
|
||||
if (bgAnimFrame) return;
|
||||
if (!bgGL && !initBackgroundGL()) return;
|
||||
bgCanvas.classList.add('visible');
|
||||
document.body.classList.add('dynamic-bg-active');
|
||||
renderBackgroundFrame();
|
||||
}
|
||||
|
||||
function stopBackground() {
|
||||
if (bgAnimFrame) {
|
||||
cancelAnimationFrame(bgAnimFrame);
|
||||
bgAnimFrame = null;
|
||||
}
|
||||
if (bgCanvas) {
|
||||
bgCanvas.classList.remove('visible');
|
||||
}
|
||||
document.body.classList.remove('dynamic-bg-active');
|
||||
}
|
||||
|
||||
// ---- Public API ----
|
||||
|
||||
function toggleDynamicBackground() {
|
||||
bgEnabled = !bgEnabled;
|
||||
localStorage.setItem('dynamicBackground', bgEnabled);
|
||||
applyDynamicBackground();
|
||||
}
|
||||
|
||||
function applyDynamicBackground() {
|
||||
const btn = document.getElementById('bgToggle');
|
||||
if (bgEnabled) {
|
||||
startBackground();
|
||||
if (btn) btn.classList.add('active');
|
||||
} else {
|
||||
stopBackground();
|
||||
if (btn) btn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize dynamic background
|
||||
applyDynamicBackground();
|
||||
|
||||
// Initialize locale (async - loads JSON file)
|
||||
await initLocale();
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ function setTheme(theme) {
|
||||
if (metaThemeColor) {
|
||||
metaThemeColor.setAttribute('content', theme === 'light' ? '#ffffff' : '#121212');
|
||||
}
|
||||
|
||||
if (typeof updateBackgroundColors === 'function') updateBackgroundColors();
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
@@ -147,6 +149,7 @@ function applyAccentColor(color, hover) {
|
||||
localStorage.setItem('accentColor', color);
|
||||
const dot = document.getElementById('accentDot');
|
||||
if (dot) dot.style.background = color;
|
||||
if (typeof updateBackgroundColors === 'function') updateBackgroundColors();
|
||||
}
|
||||
|
||||
function renderAccentSwatches() {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"player.unknown_source": "Unknown",
|
||||
"player.vinyl": "Vinyl mode",
|
||||
"player.visualizer": "Audio visualizer",
|
||||
"player.background": "Dynamic background",
|
||||
"state.playing": "Playing",
|
||||
"state.paused": "Paused",
|
||||
"state.stopped": "Stopped",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"player.unknown_source": "Неизвестно",
|
||||
"player.vinyl": "Режим винила",
|
||||
"player.visualizer": "Аудио визуализатор",
|
||||
"player.background": "Динамический фон",
|
||||
"state.playing": "Воспроизведение",
|
||||
"state.paused": "Пауза",
|
||||
"state.stopped": "Остановлено",
|
||||
|
||||
Reference in New Issue
Block a user