mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Add 6 more alive-feeling effects: parallax, sparklines, flash, glitch, alert pulse, hover rise
- Mouse parallax: panels tilt in 3D toward cursor + columns shift depth (perspective:1200px on layout) - Sparklines: mini 25-point line charts under CPU/mem/disk bars (cyan/green/orange per metric) - Panel flash: system panel border pulses bright cyan on each data refresh - Glitch: JARVIS title text does chromatic aberration glitch every 35-60s (random interval) - Alert pulse: red radial overlay slowly breathes when there are unresolved alerts - Hover rise: panels lift extra 4px + brighter border on mouse hover - Bonus: metric bar shimmer (animated light sweep through filled bars)
This commit is contained in:
+202
-4
@@ -82,9 +82,64 @@ body::after{
|
|||||||
|
|
||||||
/* ── PANEL FLOAT + GLOW ───────────────────────────────────────────── */
|
/* ── PANEL FLOAT + GLOW ───────────────────────────────────────────── */
|
||||||
@keyframes panelFloat{
|
@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)}
|
0%,100%{transform:translateY(var(--pty,0px)) rotateX(var(--prx,0deg)) rotateY(var(--pry,0deg));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)}
|
50%{transform:translateY(calc(var(--pty,0px) - 7px)) rotateX(var(--prx,0deg)) rotateY(var(--pry,0deg));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)}
|
||||||
}
|
}
|
||||||
|
/* Panel flash when data updates */
|
||||||
|
@keyframes panelFlash{0%{box-shadow:0 0 0 1px rgba(0,212,255,0.8),0 0 20px rgba(0,212,255,0.3)}100%{box-shadow:none}}
|
||||||
|
.panel.data-flash{animation:panelFlash 0.5s ease-out,panelFloat 7s ease-in-out infinite}
|
||||||
|
|
||||||
|
/* Alert pulse — red ambient glow on body when alerts active */
|
||||||
|
@keyframes alertPulse{0%,100%{opacity:0}50%{opacity:1}}
|
||||||
|
#alertOverlay{position:fixed;inset:0;pointer-events:none;z-index:1;background:radial-gradient(ellipse at 50% 50%,rgba(255,30,60,0.07) 0%,transparent 70%);animation:alertPulse 3s ease-in-out infinite;display:none}
|
||||||
|
|
||||||
|
/* Glitch keyframes */
|
||||||
|
@keyframes glitch1{
|
||||||
|
0%,100%{clip-path:inset(0 0 100% 0);transform:translate(0)}
|
||||||
|
10%{clip-path:inset(10% 0 60% 0);transform:translate(-3px,1px)}
|
||||||
|
20%{clip-path:inset(40% 0 30% 0);transform:translate(3px,-1px)}
|
||||||
|
30%{clip-path:inset(70% 0 10% 0);transform:translate(-2px,2px)}
|
||||||
|
40%{clip-path:inset(0 0 100% 0);transform:translate(0)}
|
||||||
|
}
|
||||||
|
@keyframes glitch2{
|
||||||
|
0%,100%{clip-path:inset(0 0 100% 0);transform:translate(0)}
|
||||||
|
10%{clip-path:inset(60% 0 10% 0);transform:translate(3px,-1px)}
|
||||||
|
25%{clip-path:inset(20% 0 50% 0);transform:translate(-3px,1px)}
|
||||||
|
35%{clip-path:inset(80% 0 5% 0);transform:translate(2px,2px)}
|
||||||
|
45%{clip-path:inset(0 0 100% 0);transform:translate(0)}
|
||||||
|
}
|
||||||
|
.tb-logo-text{position:relative;display:inline-block}
|
||||||
|
.tb-logo-text::before,.tb-logo-text::after{
|
||||||
|
content:attr(data-text);position:absolute;top:0;left:0;
|
||||||
|
color:var(--cyan);font-family:var(--font-display);font-size:inherit;font-weight:inherit;letter-spacing:inherit;
|
||||||
|
pointer-events:none;opacity:0;
|
||||||
|
}
|
||||||
|
.tb-logo-text::before{color:rgba(255,0,80,0.8);text-shadow:2px 0 rgba(255,0,80,0.5)}
|
||||||
|
.tb-logo-text::after{color:rgba(0,255,255,0.8);text-shadow:-2px 0 rgba(0,255,255,0.5)}
|
||||||
|
.tb-logo-text.glitching::before{animation:glitch1 0.25s steps(1) forwards;opacity:1}
|
||||||
|
.tb-logo-text.glitching::after{animation:glitch2 0.25s steps(1) forwards;opacity:1}
|
||||||
|
|
||||||
|
/* Metric bar shimmer */
|
||||||
|
@keyframes barShimmer{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}}
|
||||||
|
.metric-bar-fill::after{
|
||||||
|
content:'';position:absolute;top:0;left:0;width:30%;height:100%;
|
||||||
|
background:linear-gradient(90deg,transparent,rgba(255,255,255,0.25),transparent);
|
||||||
|
animation:barShimmer 2.5s ease-in-out infinite;
|
||||||
|
border-radius:inherit;
|
||||||
|
}
|
||||||
|
.metric-bar-fill{position:relative;overflow:hidden}
|
||||||
|
|
||||||
|
/* Panel hover rise */
|
||||||
|
.panel:hover{
|
||||||
|
transform:translateY(calc(var(--pty,0px) - 11px)) !important;
|
||||||
|
border-color:rgba(0,212,255,0.45) !important;
|
||||||
|
box-shadow:0 20px 50px rgba(0,0,0,0.55),0 0 40px rgba(0,212,255,0.1) !important;
|
||||||
|
transition:transform 0.3s ease,border-color 0.3s ease,box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sparkline canvas */
|
||||||
|
.sparkline-wrap{margin:4px 0 8px;height:32px;position:relative}
|
||||||
|
.sparkline-wrap canvas{display:block;width:100%;height:32px}
|
||||||
|
|
||||||
/* ── HUD CORNER BRACKETS ──────────────────────────────────────────── */
|
/* ── HUD CORNER BRACKETS ──────────────────────────────────────────── */
|
||||||
/* ── MINI ARC REACTOR ─────────────────────────────────────────────── */
|
/* ── MINI ARC REACTOR ─────────────────────────────────────────────── */
|
||||||
@@ -197,6 +252,7 @@ body::after{
|
|||||||
grid-template-rows:1fr;
|
grid-template-rows:1fr;
|
||||||
gap:10px;padding:10px;
|
gap:10px;padding:10px;
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
|
perspective:1200px;
|
||||||
transition:grid-template-columns 0.45s cubic-bezier(0.4,0,0.2,1);
|
transition:grid-template-columns 0.45s cubic-bezier(0.4,0,0.2,1);
|
||||||
}
|
}
|
||||||
#mainLayout.focus-mode{grid-template-columns:0px 1fr 0px}
|
#mainLayout.focus-mode{grid-template-columns:0px 1fr 0px}
|
||||||
@@ -678,6 +734,7 @@ body::after{
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="particleCanvas"></canvas>
|
<canvas id="particleCanvas"></canvas>
|
||||||
|
<div id="alertOverlay"></div>
|
||||||
<div class="scanlines"></div>
|
<div class="scanlines"></div>
|
||||||
<div class="scanline-sweep"></div>
|
<div class="scanline-sweep"></div>
|
||||||
|
|
||||||
@@ -705,7 +762,7 @@ body::after{
|
|||||||
<div id="topBar">
|
<div id="topBar">
|
||||||
<div class="tb-logo">
|
<div class="tb-logo">
|
||||||
<div class="tb-reactor"><div class="tbr-ring tbr-r1"></div><div class="tbr-ring tbr-r2"></div><div class="tbr-core"></div></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
|
<span class="tb-logo-text" data-text="JARVIS SYSTEM">JARVIS SYSTEM</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tb-center">
|
<div class="tb-center">
|
||||||
<div class="tb-stat">LOCAL <span id="tb-cpu">--</span>% CPU</div>
|
<div class="tb-stat">LOCAL <span id="tb-cpu">--</span>% CPU</div>
|
||||||
@@ -735,18 +792,21 @@ body::after{
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-title">JARVIS SERVER <span style="font-size:0.5rem;color:var(--text-dim)">165.22.1.228</span><div class="indicator"></div></div>
|
<div class="panel-title">JARVIS SERVER <span style="font-size:0.5rem;color:var(--text-dim)">165.22.1.228</span><div class="indicator"></div></div>
|
||||||
|
|
||||||
<!-- Metric bars -->
|
<!-- Metric bars + sparklines -->
|
||||||
<div class="metric-row">
|
<div class="metric-row">
|
||||||
<div class="metric-label">CPU <span id="cpu-val">--%</span></div>
|
<div class="metric-label">CPU <span id="cpu-val">--%</span></div>
|
||||||
<div class="metric-bar"><div class="metric-bar-fill" id="cpu-bar" style="width:0%"></div></div>
|
<div class="metric-bar"><div class="metric-bar-fill" id="cpu-bar" style="width:0%"></div></div>
|
||||||
|
<div class="sparkline-wrap"><canvas id="spark-cpu"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-row">
|
<div class="metric-row">
|
||||||
<div class="metric-label">MEMORY <span id="mem-val">--%</span></div>
|
<div class="metric-label">MEMORY <span id="mem-val">--%</span></div>
|
||||||
<div class="metric-bar"><div class="metric-bar-fill" id="mem-bar" style="width:0%"></div></div>
|
<div class="metric-bar"><div class="metric-bar-fill" id="mem-bar" style="width:0%"></div></div>
|
||||||
|
<div class="sparkline-wrap"><canvas id="spark-mem"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-row">
|
<div class="metric-row">
|
||||||
<div class="metric-label">DISK <span id="disk-val">--%</span></div>
|
<div class="metric-label">DISK <span id="disk-val">--%</span></div>
|
||||||
<div class="metric-bar"><div class="metric-bar-fill" id="disk-bar" style="width:0%"></div></div>
|
<div class="metric-bar"><div class="metric-bar-fill" id="disk-bar" style="width:0%"></div></div>
|
||||||
|
<div class="sparkline-wrap"><canvas id="spark-disk"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="val-row"><div class="lbl">UPTIME</div><div class="val" id="uptime-val">--</div></div>
|
<div class="val-row"><div class="lbl">UPTIME</div><div class="val" id="uptime-val">--</div></div>
|
||||||
<div class="val-row"><div class="lbl">LOAD</div><div class="val" id="load-val">--</div></div>
|
<div class="val-row"><div class="lbl">LOAD</div><div class="val" id="load-val">--</div></div>
|
||||||
@@ -1015,6 +1075,131 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── MOUSE PARALLAX — panels tilt toward cursor ────────────────────────
|
||||||
|
(function initParallax() {
|
||||||
|
const MAX_TILT = 3.5; // degrees
|
||||||
|
let mouseX = 0, mouseY = 0, raf = null;
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', e => {
|
||||||
|
mouseX = e.clientX / window.innerWidth - 0.5; // -0.5 to 0.5
|
||||||
|
mouseY = e.clientY / window.innerHeight - 0.5;
|
||||||
|
if (!raf) raf = requestAnimationFrame(applyTilt);
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyTilt() {
|
||||||
|
raf = null;
|
||||||
|
const rx = mouseY * MAX_TILT;
|
||||||
|
const ry = -mouseX * MAX_TILT;
|
||||||
|
document.querySelectorAll('.panel').forEach(p => {
|
||||||
|
p.style.setProperty('--prx', rx.toFixed(2) + 'deg');
|
||||||
|
p.style.setProperty('--pry', ry.toFixed(2) + 'deg');
|
||||||
|
});
|
||||||
|
// Column-level parallax (skip in focus mode)
|
||||||
|
if (!document.getElementById('mainLayout')?.classList.contains('focus-mode')) {
|
||||||
|
const lp = document.getElementById('leftPanel');
|
||||||
|
const rp = document.getElementById('rightPanel');
|
||||||
|
const cp = document.getElementById('centerPanel');
|
||||||
|
if (lp) lp.style.transform = `translateX(${mouseX * 5}px) translateY(${mouseY * 3}px)`;
|
||||||
|
if (rp) rp.style.transform = `translateX(${-mouseX * 5}px) translateY(${mouseY * 3}px)`;
|
||||||
|
if (cp) cp.style.transform = `translateX(${mouseX * 2}px)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── SPARKLINES ────────────────────────────────────────────────────────
|
||||||
|
const _sparkData = {cpu: [], mem: [], disk: []};
|
||||||
|
const SPARK_MAX = 25;
|
||||||
|
|
||||||
|
function pushSparkData(key, val) {
|
||||||
|
_sparkData[key].push(val);
|
||||||
|
if (_sparkData[key].length > SPARK_MAX) _sparkData[key].shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSparkline(canvasId, data, color) {
|
||||||
|
const canvas = document.getElementById(canvasId);
|
||||||
|
if (!canvas || !data.length) return;
|
||||||
|
const wrap = canvas.parentElement;
|
||||||
|
canvas.width = wrap.clientWidth || 240;
|
||||||
|
canvas.height = 32;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const W = canvas.width, H = canvas.height;
|
||||||
|
const min = 0, max = 100;
|
||||||
|
const step = W / (SPARK_MAX - 1);
|
||||||
|
|
||||||
|
// Fill area under line
|
||||||
|
const grad = ctx.createLinearGradient(0, 0, 0, H);
|
||||||
|
grad.addColorStop(0, color.replace(')', ',0.35)').replace('rgb','rgba'));
|
||||||
|
grad.addColorStop(1, color.replace(')', ',0)').replace('rgb','rgba'));
|
||||||
|
ctx.beginPath();
|
||||||
|
data.forEach((v, i) => {
|
||||||
|
const x = i * step;
|
||||||
|
const y = H - ((v - min) / (max - min)) * H;
|
||||||
|
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
||||||
|
});
|
||||||
|
ctx.lineTo((data.length - 1) * step, H);
|
||||||
|
ctx.lineTo(0, H);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = grad;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Line
|
||||||
|
ctx.beginPath();
|
||||||
|
data.forEach((v, i) => {
|
||||||
|
const x = i * step;
|
||||||
|
const y = H - ((v - min) / (max - min)) * H;
|
||||||
|
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
||||||
|
});
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Current value dot
|
||||||
|
if (data.length > 1) {
|
||||||
|
const last = data[data.length - 1];
|
||||||
|
const x = (data.length - 1) * step;
|
||||||
|
const y = H - ((last - min) / (max - min)) * H;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PANEL DATA FLASH ──────────────────────────────────────────────────
|
||||||
|
function flashPanel(panelEl) {
|
||||||
|
if (!panelEl) return;
|
||||||
|
panelEl.classList.remove('data-flash');
|
||||||
|
void panelEl.offsetWidth; // reflow to restart animation
|
||||||
|
panelEl.classList.add('data-flash');
|
||||||
|
setTimeout(() => panelEl.classList.remove('data-flash'), 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ALERT PULSE ───────────────────────────────────────────────────────
|
||||||
|
function setAlertState(hasAlerts) {
|
||||||
|
const ov = document.getElementById('alertOverlay');
|
||||||
|
if (!ov) return;
|
||||||
|
ov.style.display = hasAlerts ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GLITCH EFFECT ─────────────────────────────────────────────────────
|
||||||
|
(function initGlitch() {
|
||||||
|
function triggerGlitch() {
|
||||||
|
const el = document.querySelector('.tb-logo-text');
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.add('glitching');
|
||||||
|
setTimeout(() => el.classList.remove('glitching'), 280);
|
||||||
|
// Schedule next glitch: 35-60s random interval
|
||||||
|
setTimeout(triggerGlitch, 35000 + Math.random() * 25000);
|
||||||
|
}
|
||||||
|
// First glitch after 20s
|
||||||
|
setTimeout(triggerGlitch, 20000);
|
||||||
|
})();
|
||||||
|
|
||||||
// ── GLOBALS ──────────────────────────────────────────────────────────
|
// ── GLOBALS ──────────────────────────────────────────────────────────
|
||||||
let sessionToken = '';
|
let sessionToken = '';
|
||||||
let sessionUser = '';
|
let sessionUser = '';
|
||||||
@@ -1382,6 +1567,17 @@ function renderSystem(s) {
|
|||||||
tickTo('cpu-val', cpu);
|
tickTo('cpu-val', cpu);
|
||||||
tickTo('mem-val', mem);
|
tickTo('mem-val', mem);
|
||||||
tickTo('disk-val', disk);
|
tickTo('disk-val', disk);
|
||||||
|
|
||||||
|
// Sparklines
|
||||||
|
pushSparkData('cpu', cpu);
|
||||||
|
pushSparkData('mem', mem);
|
||||||
|
pushSparkData('disk', disk);
|
||||||
|
drawSparkline('spark-cpu', _sparkData.cpu, 'rgb(0,212,255)');
|
||||||
|
drawSparkline('spark-mem', _sparkData.mem, 'rgb(0,255,136)');
|
||||||
|
drawSparkline('spark-disk', _sparkData.disk, 'rgb(255,166,0)');
|
||||||
|
|
||||||
|
// Flash the system panel on data arrival
|
||||||
|
flashPanel(document.querySelector('#leftPanel .panel'));
|
||||||
document.getElementById('uptime-val').textContent = s.uptime || '--';
|
document.getElementById('uptime-val').textContent = s.uptime || '--';
|
||||||
document.getElementById('load-val').textContent = s.load?.['1m'] || '--';
|
document.getElementById('load-val').textContent = s.load?.['1m'] || '--';
|
||||||
document.getElementById('host-val').textContent = s.hostname || 'jarvis';
|
document.getElementById('host-val').textContent = s.hostname || 'jarvis';
|
||||||
@@ -1735,11 +1931,13 @@ async function loadAlerts() {
|
|||||||
if (!alerts.length) {
|
if (!alerts.length) {
|
||||||
el.innerHTML = '<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--green);text-align:center;margin-top:20px">✓ NO ACTIVE ALERTS</div>';
|
el.innerHTML = '<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--green);text-align:center;margin-top:20px">✓ NO ACTIVE ALERTS</div>';
|
||||||
tb.textContent='NO ALERTS'; tb.className='text-green';
|
tb.textContent='NO ALERTS'; tb.className='text-green';
|
||||||
|
setAlertState(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tb.textContent=alerts.length+' ALERT'+(alerts.length>1?'S':'');
|
tb.textContent=alerts.length+' ALERT'+(alerts.length>1?'S':'');
|
||||||
tb.className='text-red';
|
tb.className='text-red';
|
||||||
|
setAlertState(true);
|
||||||
|
|
||||||
el.innerHTML = alerts.map(a => {
|
el.innerHTML = alerts.map(a => {
|
||||||
const ctxKey = 'alert_' + a.id;
|
const ctxKey = 'alert_' + a.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user