feat: Add hardware requirement notice to README, additional Three.js viz components
Add prominent hardware requirements table at top of README documenting the three paths to real CSI data (ESP32, research NIC, commodity WiFi). Include remaining Three.js visualization components for dashboard. https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
This commit is contained in:
429
ui/components/dashboard-hud.js
Normal file
429
ui/components/dashboard-hud.js
Normal file
@@ -0,0 +1,429 @@
|
||||
// Dashboard HUD Overlay - WiFi DensePose 3D Visualization
|
||||
// Connection status, FPS counter, detection confidence, person count, sensing mode
|
||||
|
||||
export class DashboardHUD {
|
||||
constructor(container) {
|
||||
this.container = typeof container === 'string'
|
||||
? document.getElementById(container)
|
||||
: container;
|
||||
|
||||
// State
|
||||
this.state = {
|
||||
connectionStatus: 'disconnected', // connected, disconnected, connecting, error
|
||||
isRealData: false,
|
||||
fps: 0,
|
||||
confidence: 0,
|
||||
personCount: 0,
|
||||
sensingMode: 'Mock', // CSI, RSSI, Mock
|
||||
latency: 0,
|
||||
messageCount: 0,
|
||||
uptime: 0
|
||||
};
|
||||
|
||||
this._fpsFrames = [];
|
||||
this._lastFpsUpdate = 0;
|
||||
|
||||
this._build();
|
||||
}
|
||||
|
||||
_build() {
|
||||
// Create HUD overlay container
|
||||
this.hudElement = document.createElement('div');
|
||||
this.hudElement.id = 'viz-hud';
|
||||
this.hudElement.innerHTML = `
|
||||
<style>
|
||||
#viz-hud {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
font-family: 'Courier New', 'Consolas', monospace;
|
||||
color: #88ccff;
|
||||
}
|
||||
#viz-hud * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Data source banner */
|
||||
.hud-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
z-index: 110;
|
||||
}
|
||||
.hud-banner.mock {
|
||||
background: linear-gradient(90deg, rgba(180,100,0,0.85) 0%, rgba(200,120,0,0.85) 50%, rgba(180,100,0,0.85) 100%);
|
||||
color: #fff;
|
||||
border-bottom: 2px solid #ff8800;
|
||||
}
|
||||
.hud-banner.real {
|
||||
background: linear-gradient(90deg, rgba(0,120,60,0.85) 0%, rgba(0,160,80,0.85) 50%, rgba(0,120,60,0.85) 100%);
|
||||
color: #fff;
|
||||
border-bottom: 2px solid #00ff66;
|
||||
animation: pulse-green 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-green {
|
||||
0%, 100% { border-bottom-color: #00ff66; }
|
||||
50% { border-bottom-color: #00cc44; }
|
||||
}
|
||||
|
||||
/* Top-left: connection info */
|
||||
.hud-top-left {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.hud-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.hud-label {
|
||||
color: #5588aa;
|
||||
min-width: 65px;
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.hud-value {
|
||||
color: #aaddff;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Status dot */
|
||||
.hud-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.hud-status-dot.connected {
|
||||
background: #00ff66;
|
||||
box-shadow: 0 0 6px #00ff66;
|
||||
}
|
||||
.hud-status-dot.disconnected {
|
||||
background: #666;
|
||||
}
|
||||
.hud-status-dot.connecting {
|
||||
background: #ffaa00;
|
||||
box-shadow: 0 0 6px #ffaa00;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
.hud-status-dot.error {
|
||||
background: #ff3344;
|
||||
box-shadow: 0 0 6px #ff3344;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
/* Top-right: performance */
|
||||
.hud-top-right {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
.hud-fps {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #00ff88;
|
||||
line-height: 1;
|
||||
}
|
||||
.hud-fps.low { color: #ff4444; }
|
||||
.hud-fps.mid { color: #ffaa00; }
|
||||
.hud-fps.high { color: #00ff88; }
|
||||
|
||||
/* Bottom-left: detection info */
|
||||
.hud-bottom-left {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.hud-person-count {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
.hud-confidence-bar {
|
||||
width: 120px;
|
||||
height: 6px;
|
||||
background: rgba(20, 30, 50, 0.8);
|
||||
border: 1px solid #223344;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hud-confidence-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease, background 0.3s ease;
|
||||
}
|
||||
|
||||
/* Bottom-right: sensing mode */
|
||||
.hud-bottom-right {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
.hud-mode-badge {
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.hud-mode-badge.csi {
|
||||
background: rgba(0, 100, 200, 0.7);
|
||||
border: 1px solid #0088ff;
|
||||
color: #aaddff;
|
||||
}
|
||||
.hud-mode-badge.rssi {
|
||||
background: rgba(100, 0, 200, 0.7);
|
||||
border: 1px solid #8800ff;
|
||||
color: #ddaaff;
|
||||
}
|
||||
.hud-mode-badge.mock {
|
||||
background: rgba(120, 80, 0, 0.7);
|
||||
border: 1px solid #ff8800;
|
||||
color: #ffddaa;
|
||||
}
|
||||
|
||||
/* Corner brackets decoration */
|
||||
.hud-corner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-color: rgba(100, 150, 200, 0.3);
|
||||
border-style: solid;
|
||||
}
|
||||
.hud-corner.tl { top: 36px; left: 4px; border-width: 1px 0 0 1px; }
|
||||
.hud-corner.tr { top: 36px; right: 4px; border-width: 1px 1px 0 0; }
|
||||
.hud-corner.bl { bottom: 4px; left: 4px; border-width: 0 0 1px 1px; }
|
||||
.hud-corner.br { bottom: 4px; right: 4px; border-width: 0 1px 1px 0; }
|
||||
|
||||
/* Controls hint */
|
||||
.hud-controls-hint {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 10px;
|
||||
color: #445566;
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Data source banner -->
|
||||
<div class="hud-banner mock" id="hud-banner">MOCK DATA</div>
|
||||
|
||||
<!-- Corner decorations -->
|
||||
<div class="hud-corner tl"></div>
|
||||
<div class="hud-corner tr"></div>
|
||||
<div class="hud-corner bl"></div>
|
||||
<div class="hud-corner br"></div>
|
||||
|
||||
<!-- Top-left: connection info -->
|
||||
<div class="hud-top-left">
|
||||
<div class="hud-row">
|
||||
<span class="hud-status-dot disconnected" id="hud-status-dot"></span>
|
||||
<span class="hud-value" id="hud-conn-status">Disconnected</span>
|
||||
</div>
|
||||
<div class="hud-row">
|
||||
<span class="hud-label">Latency</span>
|
||||
<span class="hud-value" id="hud-latency">-- ms</span>
|
||||
</div>
|
||||
<div class="hud-row">
|
||||
<span class="hud-label">Messages</span>
|
||||
<span class="hud-value" id="hud-msg-count">0</span>
|
||||
</div>
|
||||
<div class="hud-row">
|
||||
<span class="hud-label">Uptime</span>
|
||||
<span class="hud-value" id="hud-uptime">0s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top-right: FPS -->
|
||||
<div class="hud-top-right">
|
||||
<div class="hud-fps high" id="hud-fps">-- FPS</div>
|
||||
<div class="hud-row">
|
||||
<span class="hud-label">Frame</span>
|
||||
<span class="hud-value" id="hud-frame-time">-- ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-left: detection info -->
|
||||
<div class="hud-bottom-left">
|
||||
<div class="hud-row">
|
||||
<span class="hud-label">Persons</span>
|
||||
<span class="hud-person-count hud-value" id="hud-person-count">0</span>
|
||||
</div>
|
||||
<div class="hud-row">
|
||||
<span class="hud-label">Confidence</span>
|
||||
<span class="hud-value" id="hud-confidence">0%</span>
|
||||
</div>
|
||||
<div class="hud-confidence-bar">
|
||||
<div class="hud-confidence-fill" id="hud-confidence-fill" style="width: 0%; background: #334455;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-right: sensing mode -->
|
||||
<div class="hud-bottom-right">
|
||||
<div class="hud-mode-badge mock" id="hud-mode-badge">MOCK</div>
|
||||
<div class="hud-row" style="margin-top: 4px;">
|
||||
<span class="hud-label">WiFi DensePose</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls hint -->
|
||||
<div class="hud-controls-hint">
|
||||
Drag to orbit | Scroll to zoom | Right-click to pan
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.container.style.position = 'relative';
|
||||
this.container.appendChild(this.hudElement);
|
||||
|
||||
// Cache DOM references
|
||||
this._els = {
|
||||
banner: this.hudElement.querySelector('#hud-banner'),
|
||||
statusDot: this.hudElement.querySelector('#hud-status-dot'),
|
||||
connStatus: this.hudElement.querySelector('#hud-conn-status'),
|
||||
latency: this.hudElement.querySelector('#hud-latency'),
|
||||
msgCount: this.hudElement.querySelector('#hud-msg-count'),
|
||||
uptime: this.hudElement.querySelector('#hud-uptime'),
|
||||
fps: this.hudElement.querySelector('#hud-fps'),
|
||||
frameTime: this.hudElement.querySelector('#hud-frame-time'),
|
||||
personCount: this.hudElement.querySelector('#hud-person-count'),
|
||||
confidence: this.hudElement.querySelector('#hud-confidence'),
|
||||
confidenceFill: this.hudElement.querySelector('#hud-confidence-fill'),
|
||||
modeBadge: this.hudElement.querySelector('#hud-mode-badge')
|
||||
};
|
||||
}
|
||||
|
||||
// Update state from external data
|
||||
updateState(newState) {
|
||||
Object.assign(this.state, newState);
|
||||
this._render();
|
||||
}
|
||||
|
||||
// Track FPS - call each frame
|
||||
tickFPS() {
|
||||
const now = performance.now();
|
||||
this._fpsFrames.push(now);
|
||||
|
||||
// Keep only last second of frames
|
||||
while (this._fpsFrames.length > 0 && this._fpsFrames[0] < now - 1000) {
|
||||
this._fpsFrames.shift();
|
||||
}
|
||||
|
||||
// Update FPS display at most 4 times per second
|
||||
if (now - this._lastFpsUpdate > 250) {
|
||||
this.state.fps = this._fpsFrames.length;
|
||||
const frameTime = this._fpsFrames.length > 1
|
||||
? (now - this._fpsFrames[0]) / (this._fpsFrames.length - 1)
|
||||
: 0;
|
||||
this._lastFpsUpdate = now;
|
||||
|
||||
// Update FPS elements
|
||||
this._els.fps.textContent = `${this.state.fps} FPS`;
|
||||
this._els.fps.className = 'hud-fps ' + (
|
||||
this.state.fps >= 50 ? 'high' : this.state.fps >= 25 ? 'mid' : 'low'
|
||||
);
|
||||
this._els.frameTime.textContent = `${frameTime.toFixed(1)} ms`;
|
||||
}
|
||||
}
|
||||
|
||||
_render() {
|
||||
const { state } = this;
|
||||
|
||||
// Banner
|
||||
if (state.isRealData) {
|
||||
this._els.banner.textContent = 'REAL DATA - LIVE STREAM';
|
||||
this._els.banner.className = 'hud-banner real';
|
||||
} else {
|
||||
this._els.banner.textContent = 'MOCK DATA - DEMO MODE';
|
||||
this._els.banner.className = 'hud-banner mock';
|
||||
}
|
||||
|
||||
// Connection status
|
||||
this._els.statusDot.className = `hud-status-dot ${state.connectionStatus}`;
|
||||
const statusText = {
|
||||
connected: 'Connected',
|
||||
disconnected: 'Disconnected',
|
||||
connecting: 'Connecting...',
|
||||
error: 'Error'
|
||||
};
|
||||
this._els.connStatus.textContent = statusText[state.connectionStatus] || 'Unknown';
|
||||
|
||||
// Latency
|
||||
this._els.latency.textContent = state.latency > 0 ? `${state.latency.toFixed(0)} ms` : '-- ms';
|
||||
|
||||
// Messages
|
||||
this._els.msgCount.textContent = state.messageCount.toLocaleString();
|
||||
|
||||
// Uptime
|
||||
const uptimeSec = Math.floor(state.uptime);
|
||||
if (uptimeSec < 60) {
|
||||
this._els.uptime.textContent = `${uptimeSec}s`;
|
||||
} else if (uptimeSec < 3600) {
|
||||
this._els.uptime.textContent = `${Math.floor(uptimeSec / 60)}m ${uptimeSec % 60}s`;
|
||||
} else {
|
||||
const h = Math.floor(uptimeSec / 3600);
|
||||
const m = Math.floor((uptimeSec % 3600) / 60);
|
||||
this._els.uptime.textContent = `${h}h ${m}m`;
|
||||
}
|
||||
|
||||
// Person count
|
||||
this._els.personCount.textContent = state.personCount;
|
||||
this._els.personCount.style.color = state.personCount > 0 ? '#00ff88' : '#556677';
|
||||
|
||||
// Confidence
|
||||
const confPct = (state.confidence * 100).toFixed(1);
|
||||
this._els.confidence.textContent = `${confPct}%`;
|
||||
this._els.confidenceFill.style.width = `${state.confidence * 100}%`;
|
||||
// Color temperature: red (low) -> yellow (mid) -> green (high)
|
||||
const confHue = state.confidence * 120; // 0=red, 60=yellow, 120=green
|
||||
this._els.confidenceFill.style.background = `hsl(${confHue}, 100%, 45%)`;
|
||||
|
||||
// Sensing mode
|
||||
const modeLower = (state.sensingMode || 'Mock').toLowerCase();
|
||||
this._els.modeBadge.textContent = state.sensingMode.toUpperCase();
|
||||
this._els.modeBadge.className = `hud-mode-badge ${modeLower}`;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.hudElement && this.hudElement.parentNode) {
|
||||
this.hudElement.parentNode.removeChild(this.hudElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user