// ============================================================ // 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'); } }