Add optional ambient animated background with toggle
Three blurred color blobs (green, blue, purple) slowly drift behind the UI for atmosphere. Toggled via cloud icon in header, persisted in localStorage, off by default. Works with both dark and light themes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,89 @@ body.modal-open {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Ambient animated background ── */
|
||||||
|
#bg-anim-layer {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bg-anim="on"] #bg-anim-layer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bg-anim="on"] body {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bg-anim-layer .bg-blob {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(80px);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bg-anim-layer .bg-blob-a {
|
||||||
|
width: 600px;
|
||||||
|
height: 400px;
|
||||||
|
top: 10%;
|
||||||
|
left: 10%;
|
||||||
|
background: var(--bg-anim-a);
|
||||||
|
animation: blobA 20s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bg-anim-layer .bg-blob-b {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
top: 40%;
|
||||||
|
right: 5%;
|
||||||
|
background: var(--bg-anim-b);
|
||||||
|
animation: blobB 25s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bg-anim-layer .bg-blob-c {
|
||||||
|
width: 450px;
|
||||||
|
height: 550px;
|
||||||
|
bottom: 5%;
|
||||||
|
left: 30%;
|
||||||
|
background: var(--bg-anim-c);
|
||||||
|
animation: blobC 22s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--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 {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
50% { transform: translate(15vw, 10vh) scale(1.15); }
|
||||||
|
100% { transform: translate(-5vw, 20vh) scale(0.9); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blobB {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
50% { transform: translate(-20vw, -10vh) scale(1.1); }
|
||||||
|
100% { transform: translate(5vw, 15vh) scale(0.95); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blobC {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
50% { transform: translate(10vw, -15vh) scale(1.2); }
|
||||||
|
100% { transform: translate(-10vw, -5vh) scale(0.85); }
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"app.connection_lost": "Server unreachable",
|
"app.connection_lost": "Server unreachable",
|
||||||
"app.connection_retrying": "Attempting to reconnect…",
|
"app.connection_retrying": "Attempting to reconnect…",
|
||||||
"theme.toggle": "Toggle theme",
|
"theme.toggle": "Toggle theme",
|
||||||
|
"bg.anim.toggle": "Toggle ambient background",
|
||||||
"accent.title": "Accent color",
|
"accent.title": "Accent color",
|
||||||
"accent.custom": "Custom",
|
"accent.custom": "Custom",
|
||||||
"accent.reset": "Reset",
|
"accent.reset": "Reset",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"app.connection_lost": "Сервер недоступен",
|
"app.connection_lost": "Сервер недоступен",
|
||||||
"app.connection_retrying": "Попытка переподключения…",
|
"app.connection_retrying": "Попытка переподключения…",
|
||||||
"theme.toggle": "Переключить тему",
|
"theme.toggle": "Переключить тему",
|
||||||
|
"bg.anim.toggle": "Анимированный фон",
|
||||||
"accent.title": "Цвет акцента",
|
"accent.title": "Цвет акцента",
|
||||||
"accent.custom": "Свой",
|
"accent.custom": "Свой",
|
||||||
"accent.reset": "Сброс",
|
"accent.reset": "Сброс",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"app.connection_lost": "服务器不可达",
|
"app.connection_lost": "服务器不可达",
|
||||||
"app.connection_retrying": "正在尝试重新连接…",
|
"app.connection_retrying": "正在尝试重新连接…",
|
||||||
"theme.toggle": "切换主题",
|
"theme.toggle": "切换主题",
|
||||||
|
"bg.anim.toggle": "切换动态背景",
|
||||||
"accent.title": "主题色",
|
"accent.title": "主题色",
|
||||||
"accent.custom": "自定义",
|
"accent.custom": "自定义",
|
||||||
"accent.reset": "重置",
|
"accent.reset": "重置",
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body style="visibility: hidden;">
|
<body style="visibility: hidden;">
|
||||||
|
<div id="bg-anim-layer">
|
||||||
|
<div class="bg-blob bg-blob-a"></div>
|
||||||
|
<div class="bg-blob bg-blob-b"></div>
|
||||||
|
<div class="bg-blob bg-blob-c"></div>
|
||||||
|
</div>
|
||||||
<div id="connection-overlay" class="connection-overlay" style="display:none" aria-hidden="true">
|
<div id="connection-overlay" class="connection-overlay" style="display:none" aria-hidden="true">
|
||||||
<div class="connection-overlay-content">
|
<div class="connection-overlay-content">
|
||||||
<div class="connection-spinner-lg"></div>
|
<div class="connection-spinner-lg"></div>
|
||||||
@@ -57,6 +62,9 @@
|
|||||||
<button class="header-btn" onclick="openCommandPalette()" data-i18n-title="search.open" title="Search (Ctrl+K)">
|
<button class="header-btn" onclick="openCommandPalette()" data-i18n-title="search.open" title="Search (Ctrl+K)">
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="header-btn" id="bg-anim-btn" onclick="toggleBgAnim()" data-i18n-title="bg.anim.toggle" title="Toggle ambient background">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>
|
||||||
|
</button>
|
||||||
<button class="header-btn" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
|
<button class="header-btn" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
|
||||||
<span id="theme-icon"><svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg></span>
|
<span id="theme-icon"><svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -205,6 +213,24 @@
|
|||||||
|
|
||||||
<script type="module" src="/static/js/app.js"></script>
|
<script type="module" src="/static/js/app.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Initialize ambient background
|
||||||
|
const savedBgAnim = localStorage.getItem('bgAnim') || 'off';
|
||||||
|
document.documentElement.setAttribute('data-bg-anim', savedBgAnim);
|
||||||
|
updateBgAnimBtn(savedBgAnim);
|
||||||
|
|
||||||
|
function toggleBgAnim() {
|
||||||
|
const cur = document.documentElement.getAttribute('data-bg-anim');
|
||||||
|
const next = cur === 'on' ? 'off' : 'on';
|
||||||
|
document.documentElement.setAttribute('data-bg-anim', next);
|
||||||
|
localStorage.setItem('bgAnim', next);
|
||||||
|
updateBgAnimBtn(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBgAnimBtn(state) {
|
||||||
|
const btn = document.getElementById('bg-anim-btn');
|
||||||
|
if (btn) btn.style.opacity = state === 'on' ? '1' : '0.5';
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize theme
|
// Initialize theme
|
||||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
|||||||
Reference in New Issue
Block a user