Replace blob background with floating particle field
Canvas-based particle system with 60 glowing dots drifting upward, tinted with the accent color. Eliminates the gradient banding issue from the previous CSS blur approach. Renders at native resolution with radial gradients for perfectly smooth glow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { t, initLocale, changeLocale } from './core/i18n.js';
|
||||
|
||||
// Layer 1.5: visual effects
|
||||
import { initCardGlare } from './core/card-glare.js';
|
||||
import { initBgAnim, updateBgAnimAccent, updateBgAnimTheme } from './core/bg-anim.js';
|
||||
|
||||
// Layer 2: ui
|
||||
import {
|
||||
@@ -169,6 +170,10 @@ Object.assign(window, {
|
||||
// core / state (for inline script)
|
||||
setApiKey,
|
||||
|
||||
// visual effects (called from inline <script>)
|
||||
_updateBgAnimAccent: updateBgAnimAccent,
|
||||
_updateBgAnimTheme: updateBgAnimTheme,
|
||||
|
||||
// core / ui
|
||||
toggleHint,
|
||||
lockBody,
|
||||
@@ -531,8 +536,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Show content now that translations are loaded and tabs are set
|
||||
document.body.style.visibility = 'visible';
|
||||
|
||||
// Initialize card glare effect
|
||||
// Initialize visual effects
|
||||
initCardGlare();
|
||||
initBgAnim();
|
||||
updateBgAnimTheme(document.documentElement.getAttribute('data-theme') !== 'light');
|
||||
const accent = localStorage.getItem('accentColor') || '#4CAF50';
|
||||
updateBgAnimAccent(accent);
|
||||
|
||||
// Set CSS variable for sticky header height (header now includes tab bar)
|
||||
const headerEl = document.querySelector('header');
|
||||
|
||||
131
server/src/wled_controller/static/js/core/bg-anim.js
Normal file
131
server/src/wled_controller/static/js/core/bg-anim.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Ambient background animation — floating particle field.
|
||||
*
|
||||
* Renders small glowing dots that drift slowly upward on a canvas,
|
||||
* tinted with the current accent color. Lightweight and smooth.
|
||||
*/
|
||||
|
||||
const PARTICLE_COUNT = 60;
|
||||
const FPS = 30;
|
||||
|
||||
let _canvas, _ctx;
|
||||
let _raf = null;
|
||||
let _particles = [];
|
||||
let _accentRgb = [76, 175, 80];
|
||||
let _bgColor = [26, 26, 26];
|
||||
let _w = 0, _h = 0;
|
||||
|
||||
function _createParticle(randomY) {
|
||||
return {
|
||||
x: Math.random() * _w,
|
||||
y: randomY ? Math.random() * _h : _h + Math.random() * 40,
|
||||
r: 1 + Math.random() * 2,
|
||||
alpha: 0.15 + Math.random() * 0.35,
|
||||
vx: (Math.random() - 0.5) * 0.3,
|
||||
vy: -(0.15 + Math.random() * 0.4),
|
||||
// slight color variation: mix accent with white
|
||||
mix: 0.3 + Math.random() * 0.7,
|
||||
};
|
||||
}
|
||||
|
||||
function _resize() {
|
||||
_w = window.innerWidth;
|
||||
_h = window.innerHeight;
|
||||
_canvas.width = _w;
|
||||
_canvas.height = _h;
|
||||
}
|
||||
|
||||
function _initParticles() {
|
||||
_particles = [];
|
||||
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||
_particles.push(_createParticle(true));
|
||||
}
|
||||
}
|
||||
|
||||
let _lastFrame = 0;
|
||||
function _draw(time) {
|
||||
_raf = requestAnimationFrame(_draw);
|
||||
if (time - _lastFrame < 1000 / FPS) return;
|
||||
_lastFrame = time;
|
||||
|
||||
const [br, bg, bb] = _bgColor;
|
||||
const [ar, ag, ab] = _accentRgb;
|
||||
|
||||
_ctx.fillStyle = `rgb(${br},${bg},${bb})`;
|
||||
_ctx.fillRect(0, 0, _w, _h);
|
||||
|
||||
for (let i = 0; i < _particles.length; i++) {
|
||||
const p = _particles[i];
|
||||
|
||||
// Update position
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
|
||||
// Recycle particles that leave the screen
|
||||
if (p.y < -10 || p.x < -10 || p.x > _w + 10) {
|
||||
_particles[i] = _createParticle(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Color: mix between accent and white based on particle's mix factor
|
||||
const m = p.mix;
|
||||
const r = Math.round(ar * m + 255 * (1 - m));
|
||||
const g = Math.round(ag * m + 255 * (1 - m));
|
||||
const b = Math.round(ab * m + 255 * (1 - m));
|
||||
|
||||
// Draw soft glowing dot
|
||||
const grad = _ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 3);
|
||||
grad.addColorStop(0, `rgba(${r},${g},${b},${p.alpha})`);
|
||||
grad.addColorStop(0.4, `rgba(${r},${g},${b},${p.alpha * 0.4})`);
|
||||
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
|
||||
|
||||
_ctx.fillStyle = grad;
|
||||
_ctx.fillRect(p.x - p.r * 3, p.y - p.r * 3, p.r * 6, p.r * 6);
|
||||
}
|
||||
}
|
||||
|
||||
function _start() {
|
||||
if (_raf) return;
|
||||
_resize();
|
||||
_initParticles();
|
||||
_raf = requestAnimationFrame(_draw);
|
||||
}
|
||||
|
||||
function _stop() {
|
||||
if (_raf) {
|
||||
cancelAnimationFrame(_raf);
|
||||
_raf = null;
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
return [
|
||||
parseInt(hex.slice(1, 3), 16),
|
||||
parseInt(hex.slice(3, 5), 16),
|
||||
parseInt(hex.slice(5, 7), 16),
|
||||
];
|
||||
}
|
||||
|
||||
export function updateBgAnimAccent(hex) {
|
||||
_accentRgb = hexToRgb(hex);
|
||||
}
|
||||
|
||||
export function updateBgAnimTheme(isDark) {
|
||||
_bgColor = isDark ? [26, 26, 26] : [245, 245, 245];
|
||||
}
|
||||
|
||||
export function initBgAnim() {
|
||||
_canvas = document.getElementById('bg-anim-canvas');
|
||||
if (!_canvas) return;
|
||||
_ctx = _canvas.getContext('2d');
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const on = document.documentElement.getAttribute('data-bg-anim') === 'on';
|
||||
if (on) _start(); else _stop();
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-bg-anim'] });
|
||||
|
||||
window.addEventListener('resize', () => { if (_raf) _resize(); });
|
||||
|
||||
if (document.documentElement.getAttribute('data-bg-anim') === 'on') _start();
|
||||
}
|
||||
Reference in New Issue
Block a user