mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
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:
+173
-34
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user