From 4245e81a35303b5748121d9bf2fb7bd8c414a2b0 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 12 Mar 2026 11:35:49 +0300 Subject: [PATCH] Merge floating particles into WebGL shader and fix color picker clipping Remove overflow:hidden from .card and .template-card that was clipping the color picker popover. Combine noise field + particle glow into a single GPU shader pass (40 drifting particles as uniforms). Co-Authored-By: Claude Opus 4.6 --- .../src/wled_controller/static/css/cards.css | 1 - .../wled_controller/static/css/streams.css | 1 - .../wled_controller/static/js/core/bg-anim.js | 86 ++++++++++++++++--- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css index 59670dc..74f5059 100644 --- a/server/src/wled_controller/static/css/cards.css +++ b/server/src/wled_controller/static/css/cards.css @@ -56,7 +56,6 @@ section { border-radius: 8px; padding: 12px 20px 20px; position: relative; - overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; display: flex; flex-direction: column; diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index 3e7c39c..6f90ac1 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -13,7 +13,6 @@ border: 1px solid var(--border-color); border-radius: 8px; padding: 16px; - overflow: hidden; transition: box-shadow 0.2s ease, transform 0.2s ease; display: flex; flex-direction: column; diff --git a/server/src/wled_controller/static/js/core/bg-anim.js b/server/src/wled_controller/static/js/core/bg-anim.js index 9dd38e5..479e86f 100644 --- a/server/src/wled_controller/static/js/core/bg-anim.js +++ b/server/src/wled_controller/static/js/core/bg-anim.js @@ -1,10 +1,12 @@ /** - * Ambient background animation — WebGL shader. + * Ambient background animation — WebGL shader + floating particles. * - * Renders a smooth flowing noise field with accent-colored highlights. - * GPU-accelerated, no banding, very lightweight. + * Renders a smooth flowing noise field with accent-colored highlights + * plus drifting glowing particles, all in a single GPU shader pass. */ +const PARTICLE_COUNT = 40; + const VERT_SRC = ` attribute vec2 a_pos; void main() { gl_Position = vec4(a_pos, 0.0, 1.0); } @@ -16,8 +18,9 @@ uniform float u_time; uniform vec2 u_res; uniform vec3 u_accent; uniform vec3 u_bg; +uniform vec3 u_particles[${PARTICLE_COUNT}]; // xy = position (0-1), z = radius -// Simplex-style noise (compact hash-based) +// Simplex-style noise vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; } vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; } vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); } @@ -59,31 +62,76 @@ void main() { float n = n1 * 0.5 + n2 * 0.3 + n3 * 0.2; - // Map noise to a soft glow: only show the bright peaks + // Map noise to a soft glow float glow = smoothstep(0.05, 0.6, n); - // Create two accent-derived colors with hue shift + // Two accent-derived colors vec3 col1 = u_accent; - vec3 col2 = u_accent.gbr; // channel rotation for variety + vec3 col2 = u_accent.gbr; - // Mix between the two colors based on second noise layer float colorMix = n2 * 0.5 + 0.5; vec3 color = mix(col1, col2, colorMix); - // Final: blend colored glow onto background + // Noise background vec3 result = mix(u_bg, u_bg + color * 0.6, glow * 0.14); + // Floating particles — accumulate glow from each + float particleGlow = 0.0; + float particleColor = 0.0; + for (int i = 0; i < ${PARTICLE_COUNT}; i++) { + vec2 ppos = u_particles[i].xy; + float pradius = u_particles[i].z; + // Particle position in aspect-corrected space + vec2 pp = vec2(ppos.x * aspect, ppos.y); + float d = length(p - pp); + float g = pradius / (d * d * 800.0 + pradius); + particleGlow += g; + // Alternate color based on index parity (use float comparison) + particleColor += g * step(0.5, fract(float(i) * 0.5)); + } + + // Mix particle colors: accent and white-ish accent + vec3 pCol = mix(u_accent, mix(u_accent, vec3(1.0), 0.5), particleColor / max(particleGlow, 0.001)); + result += pCol * particleGlow * 0.5; + gl_FragColor = vec4(result, 1.0); } `; let _canvas, _gl, _prog; -let _uTime, _uRes, _uAccent, _uBg; +let _uTime, _uRes, _uAccent, _uBg, _uParticles; let _raf = null; let _startTime = 0; let _accent = [76 / 255, 175 / 255, 80 / 255]; let _bgColor = [26 / 255, 26 / 255, 26 / 255]; +// Particle state (CPU-side, positions in 0..1 UV space) +const _particles = []; + +function _initParticles() { + _particles.length = 0; + for (let i = 0; i < PARTICLE_COUNT; i++) { + _particles.push({ + x: Math.random(), + y: Math.random(), + vx: (Math.random() - 0.5) * 0.0002, + vy: 0.0001 + Math.random() * 0.0003, + r: 0.003 + Math.random() * 0.006, + }); + } +} + +function _updateParticles() { + for (const p of _particles) { + p.x += p.vx; + p.y += p.vy; + // Wrap around + if (p.y > 1.05) { p.y = -0.05; p.x = Math.random(); } + if (p.x < -0.05) p.x = 1.05; + if (p.x > 1.05) p.x = -0.05; + } +} + function _compile(gl, type, src) { const s = gl.createShader(type); gl.shaderSource(s, src); @@ -126,15 +174,17 @@ function _initGL() { _uRes = gl.getUniformLocation(_prog, 'u_res'); _uAccent = gl.getUniformLocation(_prog, 'u_accent'); _uBg = gl.getUniformLocation(_prog, 'u_bg'); + _uParticles = []; + for (let i = 0; i < PARTICLE_COUNT; i++) { + _uParticles.push(gl.getUniformLocation(_prog, `u_particles[${i}]`)); + } return true; } function _resize() { - // Render at half resolution for performance - const dpr = Math.min(window.devicePixelRatio || 1, 1); - const w = Math.round(window.innerWidth * dpr * 0.5); - const h = Math.round(window.innerHeight * dpr * 0.5); + const w = Math.round(window.innerWidth * 0.5); + const h = Math.round(window.innerHeight * 0.5); _canvas.width = w; _canvas.height = h; if (_gl) _gl.viewport(0, 0, w, h); @@ -145,12 +195,19 @@ function _draw(time) { const gl = _gl; if (!gl) return; + _updateParticles(); + const t = (time - _startTime) * 0.001; gl.uniform1f(_uTime, t); gl.uniform2f(_uRes, _canvas.width, _canvas.height); gl.uniform3f(_uAccent, _accent[0], _accent[1], _accent[2]); gl.uniform3f(_uBg, _bgColor[0], _bgColor[1], _bgColor[2]); + for (let i = 0; i < PARTICLE_COUNT; i++) { + const p = _particles[i]; + gl.uniform3f(_uParticles[i], p.x, p.y, p.r); + } + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } @@ -158,6 +215,7 @@ function _start() { if (_raf) return; if (!_gl && !_initGL()) return; _resize(); + _initParticles(); _startTime = performance.now(); _raf = requestAnimationFrame(_draw); }