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:
2026-06-01 23:44:57 +00:00
parent 3297c00a1c
commit b3a329e81a
+202 -4
View File
@@ -82,9 +82,64 @@ body::after{
/* ── 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)}
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(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 ──────────────────────────────────────────── */
/* ── MINI ARC REACTOR ─────────────────────────────────────────────── */
@@ -197,6 +252,7 @@ body::after{
grid-template-rows:1fr;
gap:10px;padding:10px;
overflow:hidden;
perspective:1200px;
transition:grid-template-columns 0.45s cubic-bezier(0.4,0,0.2,1);
}
#mainLayout.focus-mode{grid-template-columns:0px 1fr 0px}
@@ -678,6 +734,7 @@ body::after{
</head>
<body>
<canvas id="particleCanvas"></canvas>
<div id="alertOverlay"></div>
<div class="scanlines"></div>
<div class="scanline-sweep"></div>
@@ -705,7 +762,7 @@ body::after{
<div id="topBar">
<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>
JARVIS SYSTEM
<span class="tb-logo-text" data-text="JARVIS SYSTEM">JARVIS SYSTEM</span>
</div>
<div class="tb-center">
<div class="tb-stat">LOCAL&nbsp;<span id="tb-cpu">--</span>% CPU</div>
@@ -735,18 +792,21 @@ body::after{
<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>
<!-- Metric bars -->
<!-- Metric bars + sparklines -->
<div class="metric-row">
<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="sparkline-wrap"><canvas id="spark-cpu"></canvas></div>
</div>
<div class="metric-row">
<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="sparkline-wrap"><canvas id="spark-mem"></canvas></div>
</div>
<div class="metric-row">
<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="sparkline-wrap"><canvas id="spark-disk"></canvas></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>
@@ -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 ──────────────────────────────────────────────────────────
let sessionToken = '';
let sessionUser = '';
@@ -1382,6 +1567,17 @@ function renderSystem(s) {
tickTo('cpu-val', cpu);
tickTo('mem-val', mem);
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('load-val').textContent = s.load?.['1m'] || '--';
document.getElementById('host-val').textContent = s.hostname || 'jarvis';
@@ -1735,11 +1931,13 @@ async function loadAlerts() {
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>';
tb.textContent='NO ALERTS'; tb.className='text-green';
setAlertState(false);
return;
}
tb.textContent=alerts.length+' ALERT'+(alerts.length>1?'S':'');
tb.className='text-red';
setAlertState(true);
el.innerHTML = alerts.map(a => {
const ctxKey = 'alert_' + a.id;