Add cursor-tracking card glare and accent-linked background blobs
- Subtle radial glare follows cursor over card/template-card elements using a single document-level mousemove listener (event delegation) - Ambient background blob colors now derive from the selected accent color with hue-shifted variants - Glare intensity kept very subtle (3.5% dark / 12% light theme) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -136,17 +136,7 @@ body.modal-open {
|
|||||||
animation: blobC 22s ease-in-out infinite alternate;
|
animation: blobC 22s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
/* Blob colors derived from accent via JS (--bg-anim-a/b/c set in index.html) */
|
||||||
--bg-anim-a: rgba(76, 175, 80, 0.10);
|
|
||||||
--bg-anim-b: rgba(33, 150, 243, 0.08);
|
|
||||||
--bg-anim-c: rgba(156, 39, 176, 0.07);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] {
|
|
||||||
--bg-anim-a: rgba(76, 175, 80, 0.10);
|
|
||||||
--bg-anim-b: rgba(33, 150, 243, 0.08);
|
|
||||||
--bg-anim-c: rgba(156, 39, 176, 0.07);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blobA {
|
@keyframes blobA {
|
||||||
0% { transform: translate(0, 0) scale(1); }
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ section {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px 20px 20px;
|
padding: 12px 20px 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -66,6 +67,38 @@ section {
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Card glare effect ── */
|
||||||
|
.card-glare::after,
|
||||||
|
.template-card.card-glare::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle 200px at var(--glare-x, 50%) var(--glare-y, 50%),
|
||||||
|
rgba(255, 255, 255, 0.035) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-glare::after,
|
||||||
|
.template-card.card-glare::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .card-glare::after,
|
||||||
|
[data-theme="light"] .template-card.card-glare::after {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle 200px at var(--glare-x, 50%) var(--glare-y, 50%),
|
||||||
|
rgba(255, 255, 255, 0.12) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Card entrance animation ── */
|
/* ── Card entrance animation ── */
|
||||||
@keyframes cardEnter {
|
@keyframes cardEnter {
|
||||||
from { opacity: 0; transform: translateY(12px); }
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { Modal } from './core/modal.js';
|
|||||||
import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor } from './core/api.js';
|
import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor } from './core/api.js';
|
||||||
import { t, initLocale, changeLocale } from './core/i18n.js';
|
import { t, initLocale, changeLocale } from './core/i18n.js';
|
||||||
|
|
||||||
|
// Layer 1.5: visual effects
|
||||||
|
import { initCardGlare } from './core/card-glare.js';
|
||||||
|
|
||||||
// Layer 2: ui
|
// Layer 2: ui
|
||||||
import {
|
import {
|
||||||
toggleHint, lockBody, unlockBody, closeLightbox,
|
toggleHint, lockBody, unlockBody, closeLightbox,
|
||||||
@@ -528,6 +531,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Show content now that translations are loaded and tabs are set
|
// Show content now that translations are loaded and tabs are set
|
||||||
document.body.style.visibility = 'visible';
|
document.body.style.visibility = 'visible';
|
||||||
|
|
||||||
|
// Initialize card glare effect
|
||||||
|
initCardGlare();
|
||||||
|
|
||||||
// Set CSS variable for sticky header height (header now includes tab bar)
|
// Set CSS variable for sticky header height (header now includes tab bar)
|
||||||
const headerEl = document.querySelector('header');
|
const headerEl = document.querySelector('header');
|
||||||
if (headerEl) {
|
if (headerEl) {
|
||||||
|
|||||||
44
server/src/wled_controller/static/js/core/card-glare.js
Normal file
44
server/src/wled_controller/static/js/core/card-glare.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Card glare effect — cursor-tracking spotlight on .card and .template-card elements.
|
||||||
|
*
|
||||||
|
* Uses a single document-level mousemove listener (event delegation) and
|
||||||
|
* CSS custom properties (--glare-x, --glare-y) to position a radial gradient
|
||||||
|
* overlay via the ::after pseudo-element defined in cards.css.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CARD_SEL = '.card, .template-card';
|
||||||
|
|
||||||
|
let _active = null; // currently illuminated card element
|
||||||
|
|
||||||
|
function _onMove(e) {
|
||||||
|
const card = e.target.closest(CARD_SEL);
|
||||||
|
|
||||||
|
if (card && !card.classList.contains('add-device-card')) {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
card.style.setProperty('--glare-x', `${x}px`);
|
||||||
|
card.style.setProperty('--glare-y', `${y}px`);
|
||||||
|
|
||||||
|
if (_active !== card) {
|
||||||
|
if (_active) _active.classList.remove('card-glare');
|
||||||
|
card.classList.add('card-glare');
|
||||||
|
_active = card;
|
||||||
|
}
|
||||||
|
} else if (_active) {
|
||||||
|
_active.classList.remove('card-glare');
|
||||||
|
_active = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onLeave() {
|
||||||
|
if (_active) {
|
||||||
|
_active.classList.remove('card-glare');
|
||||||
|
_active = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initCardGlare() {
|
||||||
|
document.addEventListener('mousemove', _onMove, { passive: true });
|
||||||
|
document.addEventListener('mouseleave', _onLeave);
|
||||||
|
}
|
||||||
@@ -289,6 +289,21 @@
|
|||||||
return L > 0.36 ? '#1a1a1a' : '#ffffff';
|
return L > 0.36 ? '#1a1a1a' : '#ffffff';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateBgAnimColors(hex) {
|
||||||
|
const r = parseInt(hex.slice(1,3),16);
|
||||||
|
const g = parseInt(hex.slice(3,5),16);
|
||||||
|
const b = parseInt(hex.slice(5,7),16);
|
||||||
|
const root = document.documentElement;
|
||||||
|
const isDark = root.getAttribute('data-theme') !== 'light';
|
||||||
|
const a = isDark ? 0.12 : 0.10;
|
||||||
|
// Blob A: accent color
|
||||||
|
root.style.setProperty('--bg-anim-a', `rgba(${r},${g},${b},${a})`);
|
||||||
|
// Blob B: hue-shifted ~120° toward blue (swap channels)
|
||||||
|
root.style.setProperty('--bg-anim-b', `rgba(${b},${r},${g},${a * 0.8})`);
|
||||||
|
// Blob C: hue-shifted ~240° toward magenta
|
||||||
|
root.style.setProperty('--bg-anim-c', `rgba(${g},${b},${r},${a * 0.7})`);
|
||||||
|
}
|
||||||
|
|
||||||
function applyAccentColor(hex, silent) {
|
function applyAccentColor(hex, silent) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty('--primary-color', hex);
|
root.style.setProperty('--primary-color', hex);
|
||||||
@@ -296,6 +311,7 @@
|
|||||||
root.style.setProperty('--primary-text-color', adjustLightness(hex, theme === 'dark' ? 15 : -15));
|
root.style.setProperty('--primary-text-color', adjustLightness(hex, theme === 'dark' ? 15 : -15));
|
||||||
root.style.setProperty('--primary-hover', adjustLightness(hex, 8));
|
root.style.setProperty('--primary-hover', adjustLightness(hex, 8));
|
||||||
root.style.setProperty('--primary-contrast', contrastColor(hex));
|
root.style.setProperty('--primary-contrast', contrastColor(hex));
|
||||||
|
updateBgAnimColors(hex);
|
||||||
const swatch = document.getElementById('cp-swatch-accent');
|
const swatch = document.getElementById('cp-swatch-accent');
|
||||||
if (swatch) swatch.style.background = hex;
|
if (swatch) swatch.style.background = hex;
|
||||||
const native = document.getElementById('cp-native-accent');
|
const native = document.getElementById('cp-native-accent');
|
||||||
@@ -332,6 +348,7 @@
|
|||||||
|
|
||||||
const savedAccent = localStorage.getItem('accentColor');
|
const savedAccent = localStorage.getItem('accentColor');
|
||||||
if (savedAccent) applyAccentColor(savedAccent, true);
|
if (savedAccent) applyAccentColor(savedAccent, true);
|
||||||
|
updateBgAnimColors(savedAccent || '#4CAF50');
|
||||||
|
|
||||||
// Initialize auth state
|
// Initialize auth state
|
||||||
function updateAuthUI() {
|
function updateAuthUI() {
|
||||||
|
|||||||
Reference in New Issue
Block a user