Files
jarvis/public_html/assets/js/jarvis-effects.js
myron 462ce257a8 Modularize JARVIS frontend into separate CSS/JS files
Split monolithic 261KB index.html into maintainable modules:
- assets/css/jarvis.css (65KB, 1103 lines) — all styles
- assets/js/jarvis-effects.js (23KB) — particle canvas, sparklines, panel float, face tracking, glitch
- assets/js/jarvis-overlays.js (17KB) — sleep mode, network map
- assets/js/jarvis-app.js (60KB) — globals, init, login, API, panels, chat, voice, alerts, weather, news, planner
- assets/js/jarvis-protocols.js (69KB) — arc reactor, intel/comms/guardian/mission/directives/clearance/sites/vision, history search, suggestions, mobile

index.html is now a 25KB thin HTML shell with link/script tags.
Load order preserved; all cross-file dependencies resolve at runtime after window.load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 02:55:35 +00:00

591 lines
23 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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`;
});
});
// ── 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) ov.style.display = hasAlerts ? 'block' : 'none';
const vg = document.getElementById('vignetteOverlay');
if (vg) vg.classList.toggle('alert-vignette', hasAlerts);
}
function setSystemHealth(level) {
// level: 'ok' | 'warning' | 'critical'
const reactor = document.getElementById('arcReactor');
if (!reactor) return;
reactor.classList.remove('health-warning', 'health-critical');
if (level === 'warning') reactor.classList.add('health-warning');
if (level === 'critical') reactor.classList.add('health-critical');
// Also update topbar logo dot
const dot = document.querySelector('.tb-logo-dot');
if (dot) {
dot.style.background = level === 'critical' ? 'var(--red)' : level === 'warning' ? '#f5a623' : 'var(--cyan)';
dot.style.boxShadow = level === 'critical' ? '0 0 8px var(--red)' : level === 'warning' ? '0 0 8px #f5a623' : '0 0 8px var(--cyan)';
}
}
// ── FACE TRACKING — reactor follows face position ─────────────────────
let _faceTargetX = 0, _faceTargetY = 0; // normalized -0.5 to 0.5
let _faceCurrX = 0, _faceCurrY = 0;
let _faceVisible = false;
let _faceTrackRaf = null;
const FACE_MAX_X = 48; // max px left/right travel
const FACE_MAX_Y = 10; // max px up/down travel
const FACE_LERP = 0.07; // smoothing (lower = slower/smoother)
function _faceTrackLoop() {
_faceTrackRaf = requestAnimationFrame(_faceTrackLoop);
// Lerp toward target (or back to 0 when no face)
const targetX = _faceVisible ? _faceTargetX : 0;
const targetY = _faceVisible ? _faceTargetY : 0;
_faceCurrX += (targetX - _faceCurrX) * FACE_LERP;
_faceCurrY += (targetY - _faceCurrY) * FACE_LERP;
const tx = _faceCurrX * FACE_MAX_X;
const ty = _faceCurrY * FACE_MAX_Y;
const logo = document.querySelector('.tb-logo');
if (logo) logo.style.transform = `translateX(${tx.toFixed(2)}px) translateY(${ty.toFixed(2)}px)`;
}
function updateFaceTarget(box, videoW, videoH) {
// Face center, normalized — flip X because front camera is mirrored
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
_faceTargetX = 0.5 - (cx / videoW); // flipped: move right when face is left
_faceTargetY = (cy / videoH) - 0.5; // positive = face low = logo drops slightly
_faceVisible = true;
// Position the scan overlay over the detected face in the viewport.
// The video feed is 320×240 but hidden; map to viewport coords.
const scaleX = window.innerWidth / videoW;
const scaleY = window.innerHeight / videoH;
const faceVx = box.x * scaleX;
const faceVy = box.y * scaleY;
const faceVw = box.width * scaleX;
const faceVh = box.height * scaleY;
const ov = document.getElementById('faceScanOverlay');
if (ov) {
ov.style.display = 'block';
// Center overlay on face, flipped X to match mirror
const ovX = window.innerWidth - faceVx - faceVw / 2 - 30;
const ovY = faceVy + faceVh / 2 - 30;
ov.style.left = Math.max(0, Math.min(window.innerWidth - 60, ovX)) + 'px';
ov.style.top = Math.max(0, Math.min(window.innerHeight - 80, ovY)) + 'px';
}
}
function clearFaceTarget() {
_faceVisible = false;
const ov = document.getElementById('faceScanOverlay');
if (ov) ov.style.display = 'none';
}
function startFaceTracking() {
const logo = document.querySelector('.tb-logo');
if (logo) logo.classList.add('face-tracking');
if (!_faceTrackRaf) _faceTrackLoop();
}
function stopFaceTracking() {
clearFaceTarget();
const logo = document.querySelector('.tb-logo');
if (logo) { logo.classList.remove('face-tracking'); logo.style.transform = ''; }
// Let the loop coast to zero naturally rather than snapping — cancel after settling
setTimeout(() => {
if (!_faceVisible && _faceTrackRaf) {
cancelAnimationFrame(_faceTrackRaf);
_faceTrackRaf = null;
}
}, 1500);
}
// ── 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);
setTimeout(triggerGlitch, 35000 + Math.random() * 25000);
}
setTimeout(triggerGlitch, 20000);
})();
// ① HUD CORNER RINGS ──────────────────────────────────────────────────
(function initHudCorners() {
const canvas = document.getElementById('hudCornersCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H, t = 0;
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
function drawCorner(cx, cy, a0, a1) {
const R = 62, R2 = R + 16;
// Edge lines
ctx.strokeStyle = 'rgba(0,212,255,0.35)'; ctx.lineWidth = 1;
const edgeLen = 28;
// two short lines along the screen edges from the corner point
const midA = (a0 + a1) / 2;
const ax = Math.cos(a0), ay = Math.sin(a0);
const bx = Math.cos(a1), by = Math.sin(a1);
ctx.beginPath(); ctx.moveTo(cx + ax*(R+6), cy + ay*(R+6)); ctx.lineTo(cx + ax*(R+6+edgeLen), cy + ay*(R+6+edgeLen)); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx + bx*(R+6), cy + by*(R+6)); ctx.lineTo(cx + bx*(R+6+edgeLen), cy + by*(R+6+edgeLen)); ctx.stroke();
// Primary arc
ctx.beginPath(); ctx.arc(cx, cy, R, a0, a1);
ctx.strokeStyle = 'rgba(0,212,255,0.5)'; ctx.lineWidth = 1.2; ctx.stroke();
// Outer arc
ctx.beginPath(); ctx.arc(cx, cy, R2, a0 + 0.12, a1 - 0.12);
ctx.strokeStyle = 'rgba(0,212,255,0.18)'; ctx.lineWidth = 0.6; ctx.stroke();
// Tick marks
const ticks = 14;
for (let i = 0; i <= ticks; i++) {
const a = a0 + (a1 - a0) * (i / ticks);
const big = i % 7 === 0;
const len = big ? 9 : (i % 2 === 0 ? 5 : 3);
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * (R - 2), cy + Math.sin(a) * (R - 2));
ctx.lineTo(cx + Math.cos(a) * (R - 2 - len), cy + Math.sin(a) * (R - 2 - len));
ctx.strokeStyle = big ? 'rgba(0,212,255,0.8)' : 'rgba(0,212,255,0.3)';
ctx.lineWidth = big ? 1 : 0.5; ctx.stroke();
}
// Animated scanning dot
const dotA = a0 + ((a1 - a0) * ((t * 0.35) % 1));
ctx.beginPath(); ctx.arc(cx + Math.cos(dotA) * R, cy + Math.sin(dotA) * R, 2.5, 0, Math.PI*2);
ctx.fillStyle = 'rgba(0,212,255,1)';
ctx.shadowColor = 'rgba(0,212,255,0.9)'; ctx.shadowBlur = 8; ctx.fill(); ctx.shadowBlur = 0;
// Small numeric labels
ctx.font = '7px Share Tech Mono,monospace'; ctx.fillStyle = 'rgba(0,212,255,0.55)';
ctx.fillText(Math.round((Math.abs(a0) / (Math.PI*2)) * 360) + '°',
cx + Math.cos(a0) * (R + 20), cy + Math.sin(a0) * (R + 20));
}
function draw() {
ctx.clearRect(0, 0, W, H); t += 0.01;
drawCorner(0, 0, 0, Math.PI*0.5);
drawCorner(W, 0, Math.PI*0.5, Math.PI);
drawCorner(0, H, Math.PI*1.5, Math.PI*2);
drawCorner(W, H, Math.PI, Math.PI*1.5);
requestAnimationFrame(draw);
}
resize(); draw();
window.addEventListener('resize', resize);
})();
// ② DATA STREAM COLUMNS ───────────────────────────────────────────────
(function initDataStream() {
const canvas = document.getElementById('dataStreamCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H;
const CHARS = '0123456789ABCDEF◈◉▸▹⟩⟨⬡░▒';
const COL_COUNT = 22;
let cols = [];
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
cols = [];
for (let i = 0; i < COL_COUNT; i++) {
const x = (i / COL_COUNT) * W + Math.random() * (W / COL_COUNT) * 0.6;
cols.push({ x, y: Math.random() * H, speed: Math.random() * 0.7 + 0.25,
len: Math.floor(Math.random() * 14 + 5), chars: [],
alpha: Math.random() * 0.035 + 0.015, tick: 0 });
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.font = '11px Share Tech Mono,monospace';
for (const c of cols) {
c.y += c.speed;
if (c.y - c.len * 14 > H) { c.y = -c.len * 14; c.alpha = Math.random() * 0.035 + 0.015; }
if (++c.tick > 7) { c.tick = 0; c.chars[0] = CHARS[Math.floor(Math.random() * CHARS.length)]; }
for (let i = 0; i < c.len; i++) {
if (!c.chars[i]) c.chars[i] = CHARS[Math.floor(Math.random() * CHARS.length)];
const cy = c.y - i * 14;
if (cy < -14 || cy > H + 14) continue;
const a = c.alpha * (1 - i / c.len);
ctx.fillStyle = i === 0 ? `rgba(180,240,255,${Math.min(a*4,0.15)})` : `rgba(0,185,225,${a})`;
ctx.fillText(c.chars[i], c.x, cy);
}
}
requestAnimationFrame(draw);
}
resize(); draw();
window.addEventListener('resize', resize);
})();
// ③ NETWORK TOPOLOGY ──────────────────────────────────────────────────
let _topoNodes = [], _topoT = 0, _topoRunning = false;
function renderTopology(devices) {
const canvas = document.getElementById('topoCanvas');
if (!canvas) return;
const W = canvas.parentElement?.clientWidth || 260;
canvas.width = W; canvas.height = 118;
_topoNodes = devices.slice(0, 18).map((d, i, arr) => {
const angle = (i / arr.length) * Math.PI * 2 - Math.PI / 2;
const rx = W * 0.36, ry = 36;
return {
x: W/2 + Math.cos(angle) * rx * (0.6 + (i%3)*0.18),
y: 52 + Math.sin(angle) * ry * (0.65 + (i%2)*0.25),
label: (d.name || d.ip || '?').split('.')[0].substring(0, 9),
on: !!(d.alive || d.status === 'online'),
agent: d.source === 'agent',
phase: Math.random() * Math.PI * 2,
};
});
if (!_topoRunning) { _topoRunning = true; _drawTopo(); }
}
function _drawTopo() {
requestAnimationFrame(_drawTopo);
const canvas = document.getElementById('topoCanvas');
if (!canvas || !_topoNodes.length) return;
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
_topoT += 0.018;
ctx.clearRect(0, 0, W, H);
const floatY = n => Math.sin(_topoT * 0.9 + n.phase) * 2.5;
// Connections
for (let i = 0; i < _topoNodes.length; i++) {
for (let j = i+1; j < _topoNodes.length; j++) {
const a = _topoNodes[i], b = _topoNodes[j];
if (!a.on || !b.on) continue;
const ax = a.x, ay = a.y + floatY(a), bx = b.x, by = b.y + floatY(b);
const dist = Math.hypot(bx-ax, by-ay);
if (dist > W * 0.55) continue;
const lg = ctx.createLinearGradient(ax, ay, bx, by);
const col = a.agent ? '0,255,136' : '0,212,255';
lg.addColorStop(0, `rgba(${col},0.25)`); lg.addColorStop(0.5, `rgba(${col},0.1)`); lg.addColorStop(1, `rgba(${col},0.25)`);
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by);
ctx.strokeStyle = lg; ctx.lineWidth = 0.8; ctx.stroke();
// travelling pulse
const p = (_topoT * 0.4 + a.phase) % 1;
ctx.beginPath(); ctx.arc(ax+(bx-ax)*p, ay+(by-ay)*p, 1.8, 0, Math.PI*2);
ctx.fillStyle = 'rgba(0,212,255,0.85)'; ctx.fill();
}
}
// Bubble nodes
for (const n of _topoNodes) {
const pulse = 0.5 + Math.sin(_topoT * 1.4 + n.phase) * 0.3;
const col = n.on ? (n.agent ? '0,255,136' : '0,212,255') : '255,50,80';
const r = n.agent ? 10 : 7;
const nx = n.x, ny = n.y + floatY(n);
if (n.on) {
// Ambient bloom
const bloom = ctx.createRadialGradient(nx, ny, r*0.4, nx, ny, r*3);
bloom.addColorStop(0, `rgba(${col},${(pulse*0.18).toFixed(3)})`);
bloom.addColorStop(1, `rgba(${col},0)`);
ctx.beginPath(); ctx.arc(nx, ny, r*3, 0, Math.PI*2); ctx.fillStyle = bloom; ctx.fill();
}
// Frosted glass fill
const fg = ctx.createRadialGradient(nx, ny - r*0.3, 0, nx, ny, r);
const fa = n.on ? 0.18 + pulse*0.1 : 0.07;
fg.addColorStop(0, `rgba(${col},${(fa*1.8).toFixed(3)})`);
fg.addColorStop(0.65, `rgba(${col},${fa.toFixed(3)})`);
fg.addColorStop(1, `rgba(${col},${(fa*0.2).toFixed(3)})`);
ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2); ctx.fillStyle = fg; ctx.fill();
// Border
ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2);
ctx.strokeStyle = `rgba(${col},${n.on ? (0.5 + pulse*0.32).toFixed(3) : '0.2'})`;
ctx.lineWidth = 1; ctx.stroke();
// Label below bubble
ctx.font = '6px Share Tech Mono,monospace';
ctx.textAlign = 'center';
ctx.fillStyle = n.on ? `rgba(${col},0.82)` : 'rgba(255,100,100,0.5)';
ctx.fillText(n.label, nx, ny + r + 7);
ctx.textAlign = 'left';
}
}
// ④ EKG HEARTBEAT ────────────────────────────────────────────────────
(function initEKG() {
const canvas = document.getElementById('ekgCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H, data = [], phase = 0;
function resize() {
const wrap = document.getElementById('ekgWrap');
W = canvas.width = wrap ? Math.max(100, wrap.clientWidth) : 180;
H = canvas.height = 22;
data = new Array(W).fill(0);
}
function ekgVal(p) {
const c = p % 1;
if (c < 0.28) return 0;
if (c < 0.38) return Math.sin((c-0.28)/0.10*Math.PI) * 0.22; // P bump
if (c < 0.43) return 0; // PR flat
if (c < 0.45) return -(c-0.43)/0.02 * 0.18; // Q dip
if (c < 0.465){ const f=(c-0.45)/0.015; return f<0.5?f*2*0.95:(2-f*2)*0.95; } // R spike
if (c < 0.49) return -(c-0.465)/0.025 * 0.22; // S dip
if (c < 0.52) return -(0.22-(c-0.49)/0.03*0.22); // back to baseline
if (c < 0.70) return Math.sin((c-0.52)/0.18*Math.PI) * 0.28; // T wave
return 0;
}
function draw() {
phase += 0.0038;
data.shift(); data.push(ekgVal(phase));
ctx.clearRect(0, 0, W, H);
// Glow layer
ctx.beginPath();
data.forEach((v,i) => { const y=H*0.5-v*H*0.44; i===0?ctx.moveTo(i,y):ctx.lineTo(i,y); });
ctx.strokeStyle='rgba(0,220,100,0.2)'; ctx.lineWidth=4; ctx.stroke();
// Line
ctx.beginPath();
data.forEach((v,i) => { const y=H*0.5-v*H*0.44; i===0?ctx.moveTo(i,y):ctx.lineTo(i,y); });
ctx.strokeStyle='rgba(0,255,120,0.85)'; ctx.lineWidth=1.2; ctx.stroke();
requestAnimationFrame(draw);
}
resize(); draw();
window.addEventListener('resize', resize);
})();
// ⑤ AUDIO WAVEFORM RING ───────────────────────────────────────────────
(function initAudioRing() {
const canvas = document.getElementById('audioRingCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const CX = 30, CY = 30, BARS = 28, INNER = 20;
let t = 0;
function draw() {
t += 0.055;
ctx.clearRect(0, 0, 60, 60);
const listening = window.isListening;
const speaking = window.isSpeaking;
if (!listening && !speaking) { requestAnimationFrame(draw); return; }
for (let i = 0; i < BARS; i++) {
const angle = (i / BARS) * Math.PI * 2;
let amp;
if (speaking) {
amp = 0.35 + Math.abs(Math.sin(t*4.1+i*0.9))*0.5 + Math.abs(Math.sin(t*9+i*1.7))*0.15;
} else {
amp = 0.08 + Math.abs(Math.sin(t*1.1+i*0.6))*0.18;
}
const barLen = 3 + amp * 11;
ctx.beginPath();
ctx.moveTo(CX+Math.cos(angle)*INNER, CY+Math.sin(angle)*INNER);
ctx.lineTo(CX+Math.cos(angle)*(INNER+barLen), CY+Math.sin(angle)*(INNER+barLen));
const alpha = speaking ? 0.55+amp*0.45 : 0.25+amp*0.35;
ctx.strokeStyle = speaking ? `rgba(0,255,175,${alpha})` : `rgba(0,212,255,${alpha})`;
ctx.lineWidth = 1.8; ctx.stroke();
}
requestAnimationFrame(draw);
}
draw();
})();
// ⑥ STATIC NOISE BURSTS ───────────────────────────────────────────────
(function initStaticBursts() {
function burst() {
const panels = document.querySelectorAll('#app .panel');
if (panels.length) {
const p = panels[Math.floor(Math.random() * panels.length)];
const n = document.createElement('div');
n.className = 'panel-noise-layer';
p.appendChild(n);
setTimeout(() => n.remove(), 320);
}
setTimeout(burst, 75000 + Math.random() * 55000);
}
setTimeout(burst, 40000 + Math.random() * 30000);
})();
// ⑦ AMBIENT COLOR CYCLE ───────────────────────────────────────────────
(function initAmbientColor() {
let t = 0;
const root = document.documentElement;
function tick() {
t += 0.00025;
const g = Math.round(210 + Math.sin(t) * 22);
const b = Math.round(248 + Math.cos(t * 1.15) * 28);
root.style.setProperty('--cyan', `rgb(0,${g},${b})`);
root.style.setProperty('--cyan2', `rgb(0,${Math.round(g*.82)},${Math.round(b*.87)})`);
requestAnimationFrame(tick);
}
tick();
})();