Add futuristic floating UI: particles, panel float, HUD corners, scanline sweep, arc reactor, parallel refresh, animated counters

- Ambient particle canvas: 65 nodes with dynamic connection lines (GPU-accelerated)
- Floating panels: translateY oscillation with staggered phases per panel (pure CSS)
- HUD corner brackets on every panel (4-corner L-brackets via CSS ::after gradients)
- Animated grid drift: background-position keyframes give moving-through-space effect
- Scanline sweep: bright horizontal band that slowly scans the full viewport
- Mini arc reactor in top bar: always-on spinning rings + pulsing core
- Parallel API fetches in refreshAll(): Promise.all cuts refresh latency ~3x
- Animated number counters: CPU/mem/disk roll smoothly to new values (ease-out cubic)
This commit is contained in:
2026-06-01 23:39:25 +00:00
parent 02847d5de3
commit 3297c00a1c
+173 -34
View File
@@ -33,7 +33,8 @@
}
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:var(--font-body)}
/* ── GRID BACKGROUND ──────────────────────────────────────────────── */
/* ── GRID BACKGROUND — animated drift ─────────────────────────────── */
@keyframes gridDrift{from{background-position:0 0,0 0}to{background-position:40px 40px,40px 40px}}
body::before{
content:'';
position:fixed;inset:0;
@@ -43,6 +44,7 @@ body::before{
background-size:40px 40px;
z-index:0;
pointer-events:none;
animation:gridDrift 18s linear infinite;
}
body::after{
content:'';
@@ -60,6 +62,38 @@ body::after{
}
@keyframes scanMove{0%{background-position:0 0}100%{background-position:0 100%}}
/* ── SCANLINE SWEEP ───────────────────────────────────────────────── */
.scanline-sweep{
position:fixed;top:0;left:0;right:0;height:120px;
background:linear-gradient(180deg,transparent 0%,rgba(0,212,255,0.04) 40%,rgba(0,212,255,0.12) 50%,rgba(0,212,255,0.04) 60%,transparent 100%);
pointer-events:none;z-index:2;
animation:sweepDown 7s linear infinite;
box-shadow:0 0 12px rgba(0,212,255,0.15);
}
@keyframes sweepDown{
0%{transform:translateY(-120px);opacity:0}
3%{opacity:1}
97%{opacity:0.7}
100%{transform:translateY(100vh);opacity:0}
}
/* ── PARTICLE CANVAS ──────────────────────────────────────────────── */
#particleCanvas{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:0.7}
/* ── PANEL FLOAT + GLOW ───────────────────────────────────────────── */
@keyframes panelFloat{
0%,100%{transform:translateY(0px);box-shadow:0 4px 20px rgba(0,0,0,0.4),0 0 0px rgba(0,212,255,0)}
50%{transform:translateY(-7px);box-shadow:0 16px 40px rgba(0,0,0,0.5),0 0 30px rgba(0,212,255,0.06),0 0 60px rgba(0,212,255,0.02)}
}
/* ── HUD CORNER BRACKETS ──────────────────────────────────────────── */
/* ── MINI ARC REACTOR ─────────────────────────────────────────────── */
.tb-reactor{width:30px;height:30px;position:relative;flex-shrink:0}
.tbr-ring{position:absolute;border-radius:50%;top:50%;left:50%;transform:translate(-50%,-50%)}
.tbr-r1{width:30px;height:30px;border:1px solid rgba(0,212,255,0.35);animation:spinRing 9s linear infinite}
.tbr-r2{width:20px;height:20px;border:1px solid var(--orange);box-shadow:0 0 6px var(--orange);animation:spinRing 4s linear infinite reverse}
.tbr-core{position:absolute;width:8px;height:8px;border-radius:50%;background:radial-gradient(circle,#fff 0%,var(--cyan) 50%,var(--cyan2) 100%);box-shadow:0 0 10px var(--cyan),0 0 20px rgba(0,212,255,0.5);top:50%;left:50%;transform:translate(-50%,-50%);animation:corePulse 2s ease-in-out infinite}
/* ── LOGIN SCREEN ─────────────────────────────────────────────────── */
#loginScreen{
position:fixed;inset:0;z-index:1000;
@@ -224,11 +258,28 @@ body::after{
overflow:hidden;
position:relative;
backdrop-filter:blur(4px);
animation:panelFloat 7s ease-in-out infinite;
will-change:transform;
}
.panel::before{
content:'';position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--cyan),transparent);
opacity:0.4;
z-index:2;
}
/* HUD corner brackets */
.panel::after{
content:'';position:absolute;inset:0;pointer-events:none;z-index:2;
background:
linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top left / 14px 1px no-repeat,
linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top left / 1px 14px no-repeat,
linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top right / 14px 1px no-repeat,
linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top right / 1px 14px no-repeat,
linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom left / 14px 1px no-repeat,
linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom left / 1px 14px no-repeat,
linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom right / 14px 1px no-repeat,
linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom right / 1px 14px no-repeat;
opacity:0.55;
}
.panel-title{
font-family:var(--font-display);font-size:0.6rem;font-weight:700;
@@ -626,7 +677,9 @@ body::after{
</style>
</head>
<body>
<canvas id="particleCanvas"></canvas>
<div class="scanlines"></div>
<div class="scanline-sweep"></div>
<!-- ── LOGIN ────────────────────────────────────────────────────────── -->
<div id="loginScreen">
@@ -651,7 +704,7 @@ body::after{
<!-- Top Bar -->
<div id="topBar">
<div class="tb-logo">
<div class="tb-logo-dot"></div>
<div class="tb-reactor"><div class="tbr-ring tbr-r1"></div><div class="tbr-ring tbr-r2"></div><div class="tbr-core"></div></div>
JARVIS SYSTEM
</div>
<div class="tb-center">
@@ -900,6 +953,68 @@ body::after{
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js" crossorigin="anonymous"></script>
<script>
// ── PARTICLE CANVAS ───────────────────────────────────────────────────
(function initParticles() {
const canvas = document.getElementById('particleCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const N = 65;
const CONNECT_DIST = 130;
let W, H, particles = [];
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
function spawn() {
particles = [];
for (let i = 0; i < N; i++) {
particles.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.25,
vy: (Math.random() - 0.5) * 0.25,
r: Math.random() * 1.2 + 0.4,
a: Math.random() * 0.35 + 0.08,
});
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
for (let i = 0; i < N; i++) {
const p = particles[i];
for (let j = i + 1; j < N; j++) {
const q = particles[j];
const dx = p.x - q.x, dy = p.y - q.y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < CONNECT_DIST) {
ctx.strokeStyle = `rgba(0,180,255,${0.09 * (1 - d / CONNECT_DIST)})`;
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke();
}
}
ctx.fillStyle = `rgba(0,200,255,${p.a})`;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();
p.x += p.vx; p.y += p.vy;
if (p.x < 0) p.x = W; if (p.x > W) p.x = 0;
if (p.y < 0) p.y = H; if (p.y > H) p.y = 0;
}
requestAnimationFrame(draw);
}
resize(); spawn(); draw();
window.addEventListener('resize', () => { resize(); spawn(); });
})();
// ── PANEL FLOAT STAGGER — different phase per panel ───────────────────
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.panel').forEach((p, i) => {
p.style.animationDelay = `-${(i * 1.37).toFixed(2)}s`;
});
});
// ── GLOBALS ──────────────────────────────────────────────────────────
let sessionToken = '';
let sessionUser = '';
@@ -1199,39 +1314,55 @@ async function refreshAll() {
const el = document.getElementById('last-refresh');
if (el) el.textContent = new Date().toLocaleTimeString('en-US',{hour12:false});
try {
const s = await api('system');
renderSystem(s);
} catch(e) {}
// Fire core calls in parallel — cuts refresh latency from ~3s to ~600ms
const [s, n, d] = await Promise.all([
api('system').catch(() => null),
api('network').catch(() => null),
api('do').catch(() => null),
]);
if (s) renderSystem(s);
if (n) renderNetworkStatus(n);
if (d) renderDO(d);
try {
const n = await api('network');
renderNetworkStatus(n);
} catch(e) {}
// Agent status every tick (fire and forget — doesn't block)
checkAgentStatus().catch(() => {});
try {
const d = await api('do');
renderDO(d);
} catch(e) {}
// Agent status every tick (updates bottom bar badge)
try { await checkAgentStatus(); } catch(e) {}
// Refresh right-panel tabs every 3rd tick (~30s)
// Refresh right-panel tabs every 3rd tick (~30s) — all parallel
if (_refreshTick % 3 === 0) {
try { await loadHA(); } catch(e) {}
try { await loadAlerts(); } catch(e) {}
try { await loadAgents(); } catch(e) {}
try { await loadProxmox(); } catch(e) {}
try { await loadPlannerSummary(); } catch(e) {}
Promise.all([
loadHA().catch(() => {}),
loadAlerts().catch(() => {}),
loadAgents().catch(() => {}),
loadProxmox().catch(() => {}),
loadPlannerSummary().catch(() => {}),
]);
}
// Refresh weather + news every 18th tick (~3 min — cache updates every 30 min)
// Refresh weather + news every 18th tick (~3 min)
if (_refreshTick % 18 === 0) {
try { await loadWeather(); } catch(e) {}
try { await loadNews(); } catch(e) {}
Promise.all([
loadWeather().catch(() => {}),
loadNews().catch(() => {}),
]);
}
}
// ── ANIMATED NUMBER COUNTER ───────────────────────────────────────────
const _prevVals = {};
function tickTo(id, newVal, unit='%', decimals=0) {
const el = document.getElementById(id);
if (!el) return;
const prev = _prevVals[id] ?? newVal;
_prevVals[id] = newVal;
if (Math.abs(newVal - prev) < 0.5) { el.textContent = newVal.toFixed(decimals) + unit; return; }
const start = performance.now(), dur = 700;
(function frame(now) {
const p = Math.min((now - start) / dur, 1);
const ease = 1 - Math.pow(1 - p, 3);
el.textContent = (prev + (newVal - prev) * ease).toFixed(decimals) + unit;
if (p < 1) requestAnimationFrame(frame);
})(performance.now());
}
// ── RENDER: SYSTEM ────────────────────────────────────────────────────
function renderSystem(s) {
if (!s || s.error) return;
@@ -1239,18 +1370,18 @@ function renderSystem(s) {
const mem = s.memory?.percent || 0;
const disk = s.disk?.percent || 0;
// Top bar
document.getElementById('tb-cpu').textContent = cpu;
document.getElementById('tb-mem').textContent = mem;
// Top bar (animated)
tickTo('tb-cpu', cpu, '');
tickTo('tb-mem', mem, '');
// Metric bars
setBar('cpu', cpu);
setBar('mem', mem);
setBar('disk', disk);
document.getElementById('cpu-val').textContent = cpu + '%';
document.getElementById('mem-val').textContent = mem + '%';
document.getElementById('disk-val').textContent = disk + '%';
tickTo('cpu-val', cpu);
tickTo('mem-val', mem);
tickTo('disk-val', disk);
document.getElementById('uptime-val').textContent = s.uptime || '--';
document.getElementById('load-val').textContent = s.load?.['1m'] || '--';
document.getElementById('host-val').textContent = s.hostname || 'jarvis';
@@ -1864,9 +1995,17 @@ function enterVoiceMode() {
voiceMode = true;
voiceMuted = false;
voiceLastCmd = Date.now();
voiceActive = Date.now(); // open free-listen window immediately on wake
voiceActive = Date.now();
updateMicBtn();
speak('Yes, ' + (sessionUser || 'Sir') + '?');
// Bring window to front and maximize when JARVIS wakes
try {
window.focus();
if (!document.fullscreenElement && window.screen) {
window.moveTo(0, 0);
window.resizeTo(window.screen.availWidth, window.screen.availHeight);
}
} catch(e) {}
}
function exitVoiceMode() {