ISR / isr-command-demo.html
Zhen Ye
fix(demo): add missing updateTimeDisplay calls on event marker and log clicks
7b80bd1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ISR Command Center</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ── CSS Design System ─────────────────────────────────────────── */
:root {
--base: #020617;
--accent: #3b82f6;
--accent-light: #60a5fa;
--danger: #ef4444;
--warning: #f59e0b;
--success: #22c55e;
--text-primary: rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.58);
--text-tertiary: rgba(255, 255, 255, 0.35);
--panel-bg: rgba(255, 255, 255, 0.02);
--panel-border: rgba(255, 255, 255, 0.06);
}
/* ── Reset ──────────────────────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 13px;
color: var(--text-primary);
background: var(--base);
overflow: hidden;
width: 100vw;
height: 100vh;
display: grid;
grid-template-rows: 44px 1fr auto auto;
grid-template-columns: 1fr;
position: relative;
}
/* ── Ambient Background ────────────────────────────────────────── */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(ellipse 80% 50% at 20% 40%, rgba(59, 130, 246, 0.04) 0%, transparent 70%),
radial-gradient(ellipse 60% 60% at 80% 60%, rgba(96, 165, 250, 0.03) 0%, transparent 70%),
radial-gradient(ellipse 90% 40% at 50% 90%, rgba(59, 130, 246, 0.02) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
body > * {
position: relative;
z-index: 1;
}
/* ── Panel Base ────────────────────────────────────────────────── */
.panel {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 10px;
transition: border-color 0.2s ease, background 0.2s ease;
}
.panel:hover {
border-color: rgba(255, 255, 255, 0.1);
}
/* ── Typography ────────────────────────────────────────────────── */
.label {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--text-tertiary);
font-weight: 500;
}
.value {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 13px;
color: var(--text-primary);
}
.heading {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
letter-spacing: 0.3px;
}
/* ── Indicators ────────────────────────────────────────────────── */
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.dot--lg {
width: 8px;
height: 8px;
}
.dot--glow {
box-shadow: 0 0 6px currentColor, 0 0 12px currentColor;
}
.pill {
font-size: 8px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
padding: 2px 6px;
border-radius: 4px;
display: inline-flex;
align-items: center;
gap: 4px;
line-height: 1;
}
/* ── Utility ───────────────────────────────────────────────────── */
.hidden {
display: none !important;
}
/* ── Scrollbar ─────────────────────────────────────────────────── */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.02);
border-radius: 2px;
}
::-webkit-scrollbar-thumb {
background: var(--accent);
border-radius: 2px;
opacity: 0.5;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-light);
}
/* ── Layout: Top Bar ───────────────────────────────────────────── */
#topBar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid var(--panel-border);
background: rgba(2, 6, 23, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* ── Layout: Main Area ─────────────────────────────────────────── */
#mainArea {
display: flex;
gap: 12px;
padding: 12px;
min-height: 0;
overflow: hidden;
}
#videoFeed {
flex: 7;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
#drawer {
flex: 3;
min-width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
overflow: hidden;
}
/* ── Layout: Timeline ──────────────────────────────────────────── */
#timeline {
margin: 0 12px;
padding: 10px 16px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
min-height: 48px;
position: relative;
}
/* ── Layout: Command Bar ───────────────────────────────────────── */
#commandBar {
margin: 8px 12px 12px;
padding: 10px 16px;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
background: transparent;
border: 1px solid var(--panel-border);
border-radius: 10px;
min-height: 44px;
}
/* ── Placeholder Labels ────────────────────────────────────────── */
.placeholder-label {
color: var(--text-tertiary);
font-size: 11px;
letter-spacing: 1px;
text-transform: uppercase;
user-select: none;
}
/* ── Transitions ───────────────────────────────────────────────── */
a, button, input, select, textarea {
transition: all 0.2s ease;
}
/* ── Top Bar Internals ────────────────────────────────────────── */
.topbar-left, .topbar-right {
display: flex;
align-items: center;
gap: 10px;
}
.topbar-sep {
color: var(--text-tertiary);
font-weight: 300;
font-size: 16px;
margin: 0 2px;
}
.topbar-mission {
color: var(--success);
letter-spacing: 2.5px;
}
.topbar-pulse {
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% { box-shadow: 0 0 4px currentColor, 0 0 8px currentColor; opacity: 1; }
50% { box-shadow: 0 0 8px currentColor, 0 0 18px currentColor; opacity: 0.7; }
}
.topbar-indicator {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 10px;
color: var(--text-secondary);
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 5px;
}
.topbar-clock {
min-width: 72px;
text-align: right;
}
/* ── Video Feed ───────────────────────────────────────────────── */
#videoFeed {
position: relative;
background: #0a0f1a;
}
#videoCanvas, #overlayCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#overlayCanvas {
pointer-events: auto;
}
#videoBadges {
position: absolute;
top: 10px;
left: 10px;
display: flex;
gap: 6px;
z-index: 2;
}
#frameCounter {
position: absolute;
bottom: 10px;
left: 10px;
font-size: 10px;
color: var(--text-secondary);
z-index: 2;
background: rgba(0,0,0,0.45);
padding: 2px 6px;
border-radius: 4px;
}
#videoControls {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
align-items: center;
gap: 6px;
z-index: 2;
}
.vc-btn {
background: rgba(255,255,255,0.06);
border: 1px solid var(--panel-border);
color: var(--text-primary);
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.vc-select {
background: rgba(255,255,255,0.06);
border: 1px solid var(--panel-border);
color: var(--text-primary);
font-size: 10px;
padding: 4px 6px;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
}
.vc-select option {
background: #0a0f1a;
}
/* Progress Overlay */
#progressOverlay {
position: absolute;
inset: 0;
background: rgba(2,6,23,0.75);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 5;
gap: 8px;
}
.progress-ring circle {
fill: none;
stroke-width: 3;
}
.progress-ring__bg {
stroke: rgba(255,255,255,0.06);
}
.progress-ring__fg {
stroke: var(--accent);
stroke-linecap: round;
stroke-dasharray: 213.628;
stroke-dashoffset: 213.628;
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 0.3s ease;
}
.progress-pct {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
/* Start Button */
#startBtn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 3;
padding: 14px 36px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
letter-spacing: 1.5px;
text-transform: uppercase;
color: #fff;
background: linear-gradient(135deg, var(--accent), #2563eb);
border: none;
border-radius: 10px;
cursor: pointer;
box-shadow: 0 0 20px rgba(59,130,246,0.3), 0 4px 12px rgba(0,0,0,0.3);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
#startBtn:hover {
transform: translate(-50%, -50%) scale(1.04);
box-shadow: 0 0 30px rgba(59,130,246,0.45), 0 6px 16px rgba(0,0,0,0.35);
}
#startBtn:active {
transform: translate(-50%, -50%) scale(0.97);
}
/* ── Drawer ───────────────────────────────────────────────────── */
.drawer-tabs {
display: flex;
border-bottom: 1px solid var(--panel-border);
flex-shrink: 0;
}
.drawer-tab {
flex: 1;
background: none;
border: none;
color: var(--text-tertiary);
font-family: inherit;
font-size: 9px;
font-weight: 600;
letter-spacing: 1.5px;
text-transform: uppercase;
padding: 10px 0;
cursor: pointer;
}
.drawer-tab:hover {
color: var(--text-secondary);
}
.drawer-tab.active {
color: var(--accent-light);
}
.drawer-section {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
}
/* Config Panel */
#configPanel {
display: flex;
flex-direction: column;
gap: 12px;
}
.config-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.config-toggle {
display: flex;
gap: 4px;
}
.config-toggle-btn {
flex: 1;
background: rgba(255,255,255,0.03);
border: 1px solid var(--panel-border);
color: var(--text-secondary);
font-family: inherit;
font-size: 9px;
font-weight: 600;
letter-spacing: 1px;
padding: 6px 0;
border-radius: 6px;
cursor: pointer;
}
.config-toggle-btn.active {
background: rgba(59,130,246,0.12);
border-color: var(--accent);
color: var(--accent-light);
}
.config-toggle-btn:hover:not(.active) {
background: rgba(255,255,255,0.06);
}
.config-select, .config-input {
background: rgba(255,255,255,0.03);
border: 1px solid var(--panel-border);
color: var(--text-primary);
font-family: inherit;
font-size: 11px;
padding: 7px 10px;
border-radius: 6px;
outline: none;
}
.config-select option {
background: #0a0f1a;
}
.config-select:focus, .config-input:focus {
border-color: var(--accent);
}
.config-row {
flex-direction: row;
align-items: center;
}
.config-checkbox-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-secondary);
cursor: pointer;
}
.config-checkbox-label input[type="checkbox"] {
accent-color: var(--accent);
}
/* Track Cards */
.track-card {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 6px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.04);
margin-bottom: 6px;
cursor: pointer;
}
.track-card.selected {
border-color: var(--accent);
background: rgba(59,130,246,0.06);
}
.track-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.track-info {
flex: 1;
min-width: 0;
}
.track-label {
font-size: 11px;
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track-meta {
display: flex;
gap: 8px;
font-size: 9px;
color: var(--text-tertiary);
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
margin-top: 2px;
}
.track-conf {
font-size: 10px;
font-weight: 600;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
color: var(--text-secondary);
flex-shrink: 0;
}
/* ── Timeline Internals ───────────────────────────────────────── */
.timeline-bar {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.timeline-time {
font-size: 9px;
color: var(--text-tertiary);
flex-shrink: 0;
min-width: 32px;
}
.timeline-waveform-wrap {
flex: 1;
height: 32px;
position: relative;
cursor: pointer;
}
#waveformCanvas {
width: 100%;
height: 100%;
border-radius: 4px;
}
#playhead {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: #fff;
left: 0;
z-index: 2;
pointer-events: auto;
cursor: ew-resize;
}
.playhead-handle {
width: 8px;
height: 8px;
border-radius: 50%;
background: #fff;
position: absolute;
top: -4px;
left: -3px;
box-shadow: 0 0 4px rgba(255,255,255,0.4);
}
#eventMarkers {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
}
.event-marker {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: auto;
cursor: pointer;
opacity: 0.8;
}
.timeline-legend {
display: flex;
gap: 14px;
font-size: 9px;
color: var(--text-tertiary);
padding: 0 42px;
}
.timeline-legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.timeline-log-toggle {
background: none;
border: none;
color: var(--text-tertiary);
font-family: inherit;
font-size: 8px;
font-weight: 600;
letter-spacing: 1.5px;
text-transform: uppercase;
cursor: pointer;
padding: 2px 0;
align-self: flex-start;
margin-left: 42px;
}
.timeline-log-toggle:hover {
color: var(--text-secondary);
}
#eventLog {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background: rgba(2,6,23,0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--panel-border);
border-radius: 8px 8px 0 0;
padding: 8px;
z-index: 10;
}
.event-log-entry {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 5px 6px;
border-radius: 4px;
font-size: 10px;
transition: background 0.15s;
cursor: pointer;
}
.event-log-entry:hover {
background: rgba(255,255,255,0.04);
}
.event-log-time {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 9px;
color: var(--text-tertiary);
flex-shrink: 0;
min-width: 52px;
}
.event-log-type {
font-size: 8px;
font-weight: 600;
letter-spacing: 0.8px;
text-transform: uppercase;
padding: 1px 5px;
border-radius: 3px;
flex-shrink: 0;
}
.event-log-label {
color: var(--text-primary);
font-weight: 500;
flex-shrink: 0;
}
.event-log-desc {
color: var(--text-tertiary);
flex: 1;
min-width: 0;
}
/* ── Command Bar Internals ────────────────────────────────────── */
.cmd-icon {
color: var(--text-tertiary);
font-size: 16px;
flex-shrink: 0;
}
.cmd-input {
flex: 1;
background: none;
border: none;
color: var(--text-primary);
font-family: inherit;
font-size: 12px;
outline: none;
}
.cmd-input::placeholder {
color: var(--text-tertiary);
}
.cmd-badge {
font-size: 9px;
font-weight: 600;
color: var(--text-tertiary);
background: rgba(255,255,255,0.04);
border: 1px solid var(--panel-border);
padding: 2px 6px;
border-radius: 4px;
flex-shrink: 0;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
}
/* ── Toast ────────────────────────────────────────────────────── */
#toastContainer {
position: fixed;
bottom: 80px;
right: 20px;
z-index: 100;
display: flex;
flex-direction: column-reverse;
gap: 8px;
pointer-events: none;
}
.toast {
background: rgba(2,6,23,0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 10px 14px;
max-width: 400px;
font-size: 11px;
color: var(--text-primary);
line-height: 1.5;
pointer-events: auto;
display: flex;
align-items: flex-start;
gap: 8px;
animation: toast-in 0.3s ease forwards;
}
.toast-close {
background: none;
border: none;
color: var(--text-tertiary);
font-size: 14px;
cursor: pointer;
padding: 0;
line-height: 1;
flex-shrink: 0;
}
.toast-close:hover {
color: var(--text-primary);
}
.toast.toast-out {
animation: toast-out 0.25s ease forwards;
}
@keyframes toast-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(12px); }
}
/* ── State-Driven Visibility ─────────────────────────────────── */
/* Start button: visible only in ready */
#startBtn { display: none; }
body[data-state="ready"] #startBtn { display: block; }
/* Progress overlay: visible only in processing */
#progressOverlay { display: none !important; }
body[data-state="processing"] #progressOverlay {
display: flex !important;
background: rgba(0,0,0,0.6);
}
/* Drawer tabs: hidden in ready (config panel shown instead) */
body[data-state="ready"] .drawer-tabs { display: none; }
/* Processing: only TRACKS tab available */
body[data-state="processing"] .drawer-tab[data-tab="inspect"],
body[data-state="processing"] .drawer-tab[data-tab="metrics"] {
opacity: 0.3;
pointer-events: none;
}
/* Analysis: TRACKS + METRICS available, INSPECT disabled */
body[data-state="analysis"] .drawer-tab[data-tab="inspect"] {
opacity: 0.3;
pointer-events: none;
}
/* Inspect state: all tabs enabled, INSPECT forced active */
body[data-state="inspect"] .drawer-tab[data-tab="inspect"] {
opacity: 1;
pointer-events: auto;
}
/* Video controls: hidden in ready */
body[data-state="ready"] #videoControls { display: none; }
body[data-state="ready"] #videoBadges { display: none; }
body[data-state="ready"] #frameCounter { display: none; }
/* Processing pulse for top bar dot */
.topbar-dot-processing {
animation: processing-pulse 0.8s ease-in-out infinite !important;
}
@keyframes processing-pulse {
0%, 100% { box-shadow: 0 0 4px currentColor, 0 0 8px currentColor; opacity: 1; }
50% { box-shadow: 0 0 12px currentColor, 0 0 24px currentColor; opacity: 0.5; }
}
/* Inspect panel styling */
.inspect-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--panel-border);
}
.inspect-back-btn {
background: none;
border: 1px solid var(--panel-border);
color: var(--text-secondary);
font-family: inherit;
font-size: 9px;
font-weight: 600;
letter-spacing: 1px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
.inspect-back-btn:hover {
background: rgba(255,255,255,0.06);
color: var(--text-primary);
}
.inspect-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.inspect-label {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-tertiary);
}
/* ── Inspect Quad Grid ────────────────────────────────────────── */
#inspectPanel {
display: flex;
flex-direction: column;
}
#inspectPanel.hidden {
display: none !important;
}
#inspectHeader {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--panel-border);
flex-shrink: 0;
}
#inspectBack {
background: none;
border: 1px solid var(--panel-border);
color: var(--text-secondary);
font-family: inherit;
font-size: 9px;
font-weight: 600;
letter-spacing: 1px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
}
#inspectBack:hover {
background: rgba(255,255,255,0.06);
color: var(--text-primary);
}
#inspectLabel {
font-size: 11px;
font-weight: 600;
letter-spacing: 1.5px;
color: var(--text-primary);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#inspectConf {
font-size: 11px;
flex-shrink: 0;
}
#quadGrid {
display: grid;
grid-template: 1fr 1fr / 1fr 1fr;
gap: 6px;
flex: 1;
min-height: 0;
}
.quadrant {
position: relative;
border-radius: 8px;
overflow: hidden;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
cursor: pointer;
}
.quad-label {
position: absolute;
top: 6px;
left: 8px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
z-index: 2;
pointer-events: none;
}
.quad-canvas {
width: 100%;
height: 100%;
display: block;
}
.quad-metric {
position: absolute;
bottom: 6px;
left: 8px;
font-size: 9px;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
color: var(--text-secondary);
z-index: 2;
pointer-events: none;
}
.quadrant.expanded {
grid-column: 1 / -1;
grid-row: 1 / -1;
z-index: 5;
}
#threeContainer {
width: 100%;
height: 100%;
}
#threeContainer canvas {
width: 100% !important;
height: 100% !important;
}
#inspectMetrics {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
gap: 6px;
padding-top: 8px;
border-top: 1px solid var(--panel-border);
margin-top: 8px;
}
.inspect-metric-item {
flex: 1 1 45%;
min-width: 0;
}
.inspect-metric-item .label {
font-size: 7px;
}
.inspect-metric-item .value {
font-size: 11px;
}
/* ── Metrics Mode ─────────────────────────────────────────────── */
.metric-hero {
text-align: center;
padding: 16px 0 12px;
border-bottom: 1px solid var(--panel-border);
margin-bottom: 12px;
}
.metric-big-number {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 48px;
font-weight: 300;
color: var(--accent-light);
line-height: 1;
}
.metric-hero .label {
margin-top: 6px;
}
.metric-breakdown {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 14px;
}
.metric-bar-row {
display: flex;
align-items: center;
gap: 8px;
}
.metric-bar-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text-secondary);
min-width: 72px;
text-align: right;
}
.metric-bar-track {
flex: 1;
height: 4px;
background: rgba(255,255,255,0.04);
border-radius: 2px;
overflow: hidden;
}
.metric-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.6s ease;
}
.metric-bar-count {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 10px;
color: var(--text-primary);
min-width: 16px;
}
.metric-stats {
display: flex;
flex-direction: column;
gap: 0;
}
.metric-stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.metric-stat-row:last-child {
border-bottom: none;
}
.metric-stat-label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-tertiary);
}
.metric-stat-value {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 12px;
color: var(--text-primary);
font-weight: 500;
}
.inspect-value {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 12px;
color: var(--text-primary);
}
/* Mission report styling */
.mission-report {
display: flex;
flex-direction: column;
gap: 14px;
}
.report-header {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 1.5px;
color: var(--accent-light);
padding-bottom: 8px;
border-bottom: 1px solid var(--panel-border);
}
.report-section {
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.04);
border-radius: 6px;
padding: 10px 12px;
}
.report-section-title {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--accent-light);
margin-bottom: 8px;
font-weight: 600;
}
.report-stat {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 11px;
}
.report-stat-label {
color: var(--text-tertiary);
}
.report-stat-value {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
color: var(--text-primary);
font-weight: 500;
}
.report-threat {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
background: rgba(239,68,68,0.06);
border: 1px solid rgba(239,68,68,0.12);
margin-bottom: 6px;
}
.report-threat-time {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 9px;
color: var(--text-tertiary);
flex-shrink: 0;
}
.report-threat-label {
font-size: 10px;
color: var(--danger);
font-weight: 500;
}
.report-rec {
font-size: 10px;
color: var(--text-secondary);
padding: 4px 0 4px 12px;
border-left: 2px solid var(--accent);
margin-bottom: 6px;
line-height: 1.5;
}
/* Metrics panel styling */
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
.metric-card {
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.04);
border-radius: 6px;
padding: 10px;
text-align: center;
}
.metric-value {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
}
.metric-label {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-tertiary);
margin-top: 4px;
}
/* ── Polish: Global Text Rendering ────────────────────────────── */
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background: rgba(59,130,246,0.3);
}
/* ── Polish: Drawer Content Transitions ───────────────────────── */
.drawer-section {
animation: drawer-fade-in 0.3s ease both;
}
@keyframes drawer-fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Polish: Progress Overlay Fade ────────────────────────────── */
#progressOverlay {
transition: opacity 0.5s ease;
}
body[data-state="processing"] #progressOverlay {
opacity: 1;
}
/* ── Polish: Start Button Fade Out ────────────────────────────── */
#startBtn.fading-out {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
/* ── Polish: Track Card Stagger Entrance ──────────────────────── */
@keyframes track-card-enter {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Polish: Toast Slide Up ───────────────────────────────────── */
@keyframes toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Polish: Track Card Hover ─────────────────────────────────── */
.track-card {
transition: background 0.15s ease, border-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
}
.track-card:hover {
background: rgba(255,255,255,0.05);
border-color: rgba(255,255,255,0.08);
transform: scale(1.01);
}
.track-card.highlighted {
border-color: var(--accent-light) !important;
background: rgba(59,130,246,0.12) !important;
box-shadow: 0 0 8px rgba(59,130,246,0.15);
transform: scale(1.01);
}
/* Type-colored glow on hover */
.track-card:hover .track-dot {
transition: box-shadow 0.2s ease;
}
/* ── Polish: Quadrant Hover ───────────────────────────────────── */
.quadrant {
transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.quadrant:hover {
border-color: rgba(255,255,255,0.15);
transform: scale(1.02);
box-shadow: 0 0 12px rgba(59,130,246,0.1);
}
.quadrant.expanded:hover {
transform: none;
}
/* ── Polish: Event Marker Hover Tooltip ────────────────────────── */
.event-marker {
transition: opacity 0.15s, transform 0.15s;
z-index: 2;
}
.event-marker:hover {
opacity: 1;
transform: translate(-50%, -50%) scale(1.5);
z-index: 3;
}
.event-marker-tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: rgba(2,6,23,0.95);
border: 1px solid var(--panel-border);
border-radius: 4px;
padding: 4px 8px;
font-size: 9px;
color: var(--text-primary);
white-space: nowrap;
pointer-events: none;
z-index: 20;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* ── Polish: Command Bar Focus ────────────────────────────────── */
#commandBar {
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
#commandBar:focus-within {
border-color: rgba(59,130,246,0.4);
box-shadow: inset 0 0 12px rgba(59,130,246,0.06);
}
/* ── Polish: Tab Button Underline Slide ────────────────────────── */
.drawer-tab {
position: relative;
border-bottom: 2px solid transparent;
transition: color 0.2s ease, border-color 0.3s ease;
}
.drawer-tab::after {
content: '';
position: absolute;
bottom: -2px;
left: 50%;
right: 50%;
height: 2px;
background: var(--accent);
transition: left 0.25s ease, right 0.25s ease;
}
.drawer-tab.active::after {
left: 0;
right: 0;
}
.drawer-tab.active {
border-bottom-color: transparent;
}
/* ── Polish: Button Lift on Hover ─────────────────────────────── */
.vc-btn:hover {
background: rgba(255,255,255,0.12);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.inspect-back-btn:hover {
transform: translateY(-1px);
}
.config-toggle-btn {
transition: all 0.15s ease;
}
.config-toggle-btn:hover {
transform: translateY(-1px);
}
/* ── Polish: Detection Box Animations ─────────────────────────── */
@keyframes box-dash-march {
to { stroke-dashoffset: -20; }
}
@keyframes box-pulse-glow {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
/* ── Polish: Top Bar Processing Progress Bar ──────────────────── */
#topBar {
position: relative;
overflow: hidden;
}
#topBarProgress {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 0%;
background: linear-gradient(90deg, var(--accent), var(--accent-light));
transition: width 0.3s ease;
z-index: 10;
opacity: 0;
}
body[data-state="processing"] #topBarProgress {
opacity: 1;
}
/* FPS flicker */
@keyframes fps-flicker {
0% { color: var(--text-secondary); }
50% { color: var(--accent-light); }
100% { color: var(--text-secondary); }
}
.fps-updating {
animation: fps-flicker 0.3s ease;
}
/* ── Polish: Keyboard Shortcuts Hint ──────────────────────────── */
#shortcutsHint {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: rgba(2,6,23,0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 10px 14px;
z-index: 50;
display: flex;
gap: 16px;
font-size: 9px;
color: var(--text-secondary);
white-space: nowrap;
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
opacity: 0;
animation: shortcuts-fade 3s ease forwards;
pointer-events: none;
}
#shortcutsHint .shortcut-item {
display: flex;
align-items: center;
gap: 4px;
}
#shortcutsHint kbd {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 8px;
background: rgba(255,255,255,0.06);
border: 1px solid var(--panel-border);
border-radius: 3px;
padding: 1px 4px;
color: var(--text-primary);
}
@keyframes shortcuts-fade {
0% { opacity: 0; transform: translateX(-50%) translateY(4px); }
10% { opacity: 1; transform: translateX(-50%) translateY(0); }
80% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(-4px); }
}
/* ── Polish: Glass Effect on Panels ───────────────────────────── */
#eventLog {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
.toast {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
#drawer {
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
}
#timeline {
box-shadow: 0 2px 16px rgba(0,0,0,0.15);
}
/* ── Polish: z-index Stacking ─────────────────────────────────── */
#toastContainer { z-index: 100; }
#eventLog { z-index: 50; }
#progressOverlay { z-index: 5; }
#shortcutsHint { z-index: 60; }
/* ── Polish: Smooth Scroll ────────────────────────────────────── */
.drawer-section {
scroll-behavior: smooth;
}
#eventLog {
scroll-behavior: smooth;
}
/* ── Min Width Guard ───────────────────────────────────────────── */
@media (max-width: 1280px) {
body {
min-width: 1280px;
}
}
</style>
</head>
<body>
<!-- ── Top Bar ─────────────────────────────────────────────────── -->
<header id="topBar">
<div class="topbar-left">
<span class="dot dot--lg topbar-pulse" style="color: var(--accent); background: var(--accent);"></span>
<span class="heading">ISR COMMAND</span>
<span class="topbar-sep">|</span>
<span class="label topbar-mission">MISSION ACTIVE</span>
</div>
<div class="topbar-right">
<span class="topbar-indicator"><span class="dot" style="background: var(--success); color: var(--success);"></span> GPU&times;4</span>
<span class="topbar-indicator" id="fpsDisplay">48 FPS</span>
<span class="topbar-indicator">YOLO11</span>
<span class="topbar-indicator topbar-clock" id="liveClock">00:00:00Z</span>
</div>
<div id="topBarProgress"></div>
</header>
<!-- ── Main Area ───────────────────────────────────────────────── -->
<main id="mainArea">
<div id="videoFeed" class="panel">
<canvas id="videoCanvas"></canvas>
<canvas id="overlayCanvas"></canvas>
<div id="videoBadges">
<span class="pill" style="background: rgba(59,130,246,0.15); color: var(--accent-light);">
<span class="dot" style="background: var(--accent);"></span> YOLO11
</span>
<span class="pill" style="background: rgba(34,197,94,0.15); color: var(--success);">
<span class="dot" style="background: var(--success);"></span> TRACKING
</span>
</div>
<div id="frameCounter" class="value">FRM 0 / 1847</div>
<div id="videoControls">
<button id="playPauseBtn" class="vc-btn" title="Play / Pause">&#9654;</button>
<select id="speedSelect" class="vc-select">
<option value="0.5">0.5x</option>
<option value="1" selected>1x</option>
<option value="2">2x</option>
<option value="4">4x</option>
</select>
</div>
<div id="progressOverlay" class="hidden">
<svg class="progress-ring" width="80" height="80">
<circle class="progress-ring__bg" cx="40" cy="40" r="34" />
<circle class="progress-ring__fg" id="progressCircle" cx="40" cy="40" r="34" />
</svg>
<span class="progress-pct" id="progressPct">0%</span>
</div>
<button id="startBtn">START DETECTION</button>
</div>
<aside id="drawer" class="panel">
<div class="drawer-tabs">
<button class="drawer-tab active" data-tab="tracks">TRACKS</button>
<button class="drawer-tab" data-tab="inspect">INSPECT</button>
<button class="drawer-tab" data-tab="metrics">METRICS</button>
</div>
<div id="configPanel" class="drawer-section">
<div class="config-group">
<span class="label">MODE</span>
<div class="config-toggle">
<button class="config-toggle-btn active" data-mode="object_detection">DETECT</button>
<button class="config-toggle-btn" data-mode="segmentation">SEGMENT</button>
<button class="config-toggle-btn" data-mode="drone_detection">DRONE</button>
</div>
</div>
<div class="config-group">
<span class="label">MODEL</span>
<select class="config-select" id="modelSelect">
<option value="yolo11" selected>YOLO11</option>
<option value="detr_resnet50">DETR ResNet-50</option>
<option value="grounding_dino">Grounding DINO</option>
<option value="yolov8_visdrone">YOLOv8 VisDrone</option>
</select>
</div>
<div class="config-group">
<span class="label">QUERIES</span>
<input type="text" class="config-input" id="queryInput" placeholder="person, vehicle, drone..." value="person, vehicle, drone">
</div>
<div class="config-group config-row">
<label class="config-checkbox-label">
<input type="checkbox" id="depthCheck"> <span>Enable Depth</span>
</label>
</div>
</div>
<div id="tracksPanel" class="drawer-section"></div>
<div id="inspectPanel" class="drawer-section hidden"></div>
<div id="metricsPanel" class="drawer-section hidden"></div>
</aside>
</main>
<!-- ── Timeline ────────────────────────────────────────────────── -->
<div id="timeline" class="panel">
<div class="timeline-bar">
<span class="timeline-time value" id="timeStart">00:00</span>
<div class="timeline-waveform-wrap">
<canvas id="waveformCanvas"></canvas>
<div id="playhead"><div class="playhead-handle"></div></div>
<div id="eventMarkers"></div>
</div>
<span class="timeline-time value" id="timeEnd">01:02</span>
</div>
<div class="timeline-legend" id="timelineLegend"></div>
<button class="timeline-log-toggle" id="eventLogToggle">EVENT LOG &#9660;</button>
<div id="eventLog" class="hidden"></div>
</div>
<!-- ── Command Bar ─────────────────────────────────────────────── -->
<div id="commandBar">
<span class="cmd-icon">&#8984;</span>
<input type="text" id="commandInput" class="cmd-input" placeholder="Ask a question or issue a command..." autocomplete="off">
<span class="cmd-badge">&#8984;K</span>
</div>
<!-- ── Toast Container ───────────────────────────────────────── -->
<div id="toastContainer"></div>
<script>
/* ================================================================
* ISR COMMAND CENTER — Mock Data Layer
* ================================================================ */
// ── State ───────────────────────────────────────────────────────
const STATE = {
current: 'ready',
selectedTrackId: null,
expandedQuadrant: null,
playheadFrame: 0,
totalFrames: 1847,
fps: 30,
isPlaying: false,
drawerTab: 'config',
eventLogOpen: false,
trackFilter: null,
};
// ── Color Map ───────────────────────────────────────────────────
const TYPE_COLORS = {
person: '#3b82f6',
vehicle: '#f59e0b',
drone: '#ef4444',
stationary: '#22c55e',
};
function getColorForType(type) {
return TYPE_COLORS[type] || '#8b5cf6';
}
// ── Track Schema ────────────────────────────────────────────────
function createTrack(id, label, type, confidence, speed, depth, area, keyframes) {
return {
id,
label,
type,
color: getColorForType(type),
confidence,
speed, // kph
depth, // meters
area, // percentage of frame
keyframes, // [{ frame, bbox: { x, y, w, h } }] — values as %
active: true,
};
}
// ── 20 Mock Tracks ──────────────────────────────────────────────
const MOCK_TRACKS = [
// 8 persons
createTrack('P-001', 'Person Alpha', 'person', 0.94, 5.2, 12.4, 1.8, [
{ frame: 0, bbox: { x: 10, y: 30, w: 4, h: 10 } },
{ frame: 400, bbox: { x: 20, y: 32, w: 4, h: 10 } },
{ frame: 800, bbox: { x: 35, y: 34, w: 4.5, h: 11 } },
{ frame: 1200, bbox: { x: 48, y: 33, w: 4, h: 10 } },
{ frame: 1600, bbox: { x: 60, y: 31, w: 4, h: 10 } },
]),
createTrack('P-002', 'Person Bravo', 'person', 0.91, 8.7, 18.1, 1.5, [
{ frame: 100, bbox: { x: 55, y: 45, w: 3.5, h: 9 } },
{ frame: 500, bbox: { x: 50, y: 43, w: 3.5, h: 9 } },
{ frame: 900, bbox: { x: 42, y: 40, w: 4, h: 10 } },
{ frame: 1300, bbox: { x: 33, y: 38, w: 4, h: 10 } },
{ frame: 1700, bbox: { x: 25, y: 36, w: 3.5, h: 9 } },
]),
createTrack('P-003', 'Person Charlie', 'person', 0.88, 3.1, 8.5, 2.1, [
{ frame: 200, bbox: { x: 70, y: 55, w: 5, h: 12 } },
{ frame: 600, bbox: { x: 68, y: 54, w: 5, h: 12 } },
{ frame: 1000, bbox: { x: 65, y: 52, w: 5, h: 12 } },
{ frame: 1400, bbox: { x: 62, y: 50, w: 5, h: 12 } },
]),
createTrack('P-004', 'Person Delta', 'person', 0.86, 12.4, 22.3, 1.2, [
{ frame: 50, bbox: { x: 15, y: 60, w: 3, h: 8 } },
{ frame: 450, bbox: { x: 25, y: 55, w: 3.5, h: 9 } },
{ frame: 850, bbox: { x: 40, y: 48, w: 4, h: 10 } },
{ frame: 1250, bbox: { x: 55, y: 42, w: 4, h: 10 } },
{ frame: 1650, bbox: { x: 70, y: 38, w: 3.5, h: 9 } },
{ frame: 1847, bbox: { x: 80, y: 35, w: 3, h: 8 } },
]),
createTrack('P-005', 'Person Echo', 'person', 0.92, 1.8, 6.2, 2.5, [
{ frame: 300, bbox: { x: 45, y: 70, w: 5.5, h: 13 } },
{ frame: 700, bbox: { x: 44, y: 69, w: 5.5, h: 13 } },
{ frame: 1100, bbox: { x: 43, y: 68, w: 5.5, h: 13 } },
{ frame: 1500, bbox: { x: 42, y: 67, w: 5.5, h: 13 } },
]),
createTrack('P-006', 'Person Foxtrot', 'person', 0.79, 22.1, 30.5, 0.9, [
{ frame: 0, bbox: { x: 80, y: 25, w: 3, h: 7 } },
{ frame: 350, bbox: { x: 65, y: 30, w: 3.5, h: 8 } },
{ frame: 700, bbox: { x: 45, y: 38, w: 4, h: 10 } },
{ frame: 1050, bbox: { x: 28, y: 44, w: 4, h: 10 } },
{ frame: 1400, bbox: { x: 12, y: 50, w: 3.5, h: 8 } },
]),
createTrack('P-007', 'Person Golf', 'person', 0.83, 6.5, 14.8, 1.6, [
{ frame: 150, bbox: { x: 30, y: 20, w: 4, h: 10 } },
{ frame: 550, bbox: { x: 35, y: 22, w: 4, h: 10 } },
{ frame: 950, bbox: { x: 38, y: 25, w: 4, h: 10 } },
{ frame: 1350, bbox: { x: 40, y: 28, w: 4, h: 10 } },
{ frame: 1750, bbox: { x: 42, y: 30, w: 4, h: 10 } },
]),
createTrack('P-008', 'Person Hotel', 'person', 0.77, 15.3, 25.0, 1.1, [
{ frame: 250, bbox: { x: 88, y: 40, w: 3, h: 8 } },
{ frame: 650, bbox: { x: 75, y: 42, w: 3.5, h: 9 } },
{ frame: 1050, bbox: { x: 58, y: 45, w: 4, h: 10 } },
{ frame: 1450, bbox: { x: 40, y: 48, w: 4, h: 10 } },
]),
// 5 vehicles
createTrack('V-001', 'Vehicle Alpha', 'vehicle', 0.96, 45.0, 35.2, 4.8, [
{ frame: 0, bbox: { x: 5, y: 50, w: 10, h: 8 } },
{ frame: 400, bbox: { x: 25, y: 48, w: 10, h: 8 } },
{ frame: 800, bbox: { x: 50, y: 46, w: 11, h: 9 } },
{ frame: 1200, bbox: { x: 72, y: 44, w: 10, h: 8 } },
{ frame: 1600, bbox: { x: 90, y: 42, w: 9, h: 7 } },
]),
createTrack('V-002', 'Vehicle Bravo', 'vehicle', 0.93, 62.3, 42.1, 5.2, [
{ frame: 200, bbox: { x: 90, y: 55, w: 9, h: 7 } },
{ frame: 550, bbox: { x: 70, y: 53, w: 10, h: 8 } },
{ frame: 900, bbox: { x: 48, y: 50, w: 11, h: 9 } },
{ frame: 1250, bbox: { x: 25, y: 48, w: 10, h: 8 } },
{ frame: 1600, bbox: { x: 5, y: 46, w: 9, h: 7 } },
]),
createTrack('V-003', 'Vehicle Charlie', 'vehicle', 0.89, 38.7, 28.9, 3.9, [
{ frame: 100, bbox: { x: 40, y: 62, w: 9, h: 7 } },
{ frame: 500, bbox: { x: 50, y: 60, w: 9.5, h: 7.5 } },
{ frame: 900, bbox: { x: 60, y: 58, w: 10, h: 8 } },
{ frame: 1300, bbox: { x: 68, y: 56, w: 10, h: 8 } },
{ frame: 1700, bbox: { x: 75, y: 54, w: 9, h: 7 } },
]),
createTrack('V-004', 'Vehicle Delta', 'vehicle', 0.85, 78.5, 55.0, 3.2, [
{ frame: 0, bbox: { x: 92, y: 35, w: 7, h: 6 } },
{ frame: 300, bbox: { x: 68, y: 38, w: 8, h: 7 } },
{ frame: 600, bbox: { x: 42, y: 42, w: 10, h: 8 } },
{ frame: 900, bbox: { x: 18, y: 45, w: 10, h: 8 } },
]),
createTrack('V-005', 'Vehicle Echo', 'vehicle', 0.91, 31.2, 20.7, 4.5, [
{ frame: 500, bbox: { x: 10, y: 68, w: 10, h: 8 } },
{ frame: 900, bbox: { x: 30, y: 65, w: 10.5, h: 8.5 } },
{ frame: 1300, bbox: { x: 52, y: 62, w: 11, h: 9 } },
{ frame: 1700, bbox: { x: 72, y: 60, w: 10, h: 8 } },
]),
// 2 drones
createTrack('D-001', 'Drone Alpha', 'drone', 0.97, 42.0, 85.0, 0.6, [
{ frame: 400, bbox: { x: 60, y: 8, w: 2.5, h: 2 } },
{ frame: 700, bbox: { x: 50, y: 12, w: 3, h: 2.5 } },
{ frame: 1000, bbox: { x: 38, y: 10, w: 3, h: 2.5 } },
{ frame: 1300, bbox: { x: 28, y: 6, w: 2.5, h: 2 } },
{ frame: 1600, bbox: { x: 18, y: 8, w: 2, h: 1.8 } },
{ frame: 1847, bbox: { x: 10, y: 5, w: 2, h: 1.5 } },
]),
createTrack('D-002', 'Drone Bravo', 'drone', 0.93, 55.8, 120.0, 0.4, [
{ frame: 800, bbox: { x: 20, y: 5, w: 2, h: 1.5 } },
{ frame: 1050, bbox: { x: 35, y: 8, w: 2.5, h: 2 } },
{ frame: 1300, bbox: { x: 52, y: 6, w: 2.5, h: 2 } },
{ frame: 1550, bbox: { x: 68, y: 10, w: 3, h: 2.5 } },
{ frame: 1800, bbox: { x: 82, y: 7, w: 2.5, h: 2 } },
]),
// 5 stationary
createTrack('S-001', 'Stationary Alpha', 'stationary', 0.95, 0.0, 10.2, 3.8, [
{ frame: 0, bbox: { x: 75, y: 72, w: 8, h: 6 } },
{ frame: 600, bbox: { x: 75, y: 72, w: 8, h: 6 } },
{ frame: 1200, bbox: { x: 75, y: 72, w: 8, h: 6 } },
{ frame: 1847, bbox: { x: 75, y: 72, w: 8, h: 6 } },
]),
createTrack('S-002', 'Stationary Bravo', 'stationary', 0.90, 0.2, 15.8, 5.1, [
{ frame: 0, bbox: { x: 22, y: 78, w: 10, h: 7 } },
{ frame: 600, bbox: { x: 22, y: 78, w: 10, h: 7 } },
{ frame: 1200, bbox: { x: 22, y: 78, w: 10, h: 7 } },
{ frame: 1847, bbox: { x: 22, y: 78, w: 10, h: 7 } },
]),
createTrack('S-003', 'Stationary Charlie', 'stationary', 0.87, 0.1, 8.3, 2.4, [
{ frame: 0, bbox: { x: 50, y: 82, w: 6, h: 5 } },
{ frame: 900, bbox: { x: 50, y: 82, w: 6, h: 5 } },
{ frame: 1847, bbox: { x: 50, y: 82, w: 6, h: 5 } },
]),
createTrack('S-004', 'Stationary Delta', 'stationary', 0.82, 0.5, 22.1, 6.2, [
{ frame: 0, bbox: { x: 88, y: 65, w: 11, h: 8 } },
{ frame: 500, bbox: { x: 88, y: 65, w: 11, h: 8 } },
{ frame: 1000, bbox: { x: 88, y: 65.5, w: 11, h: 8 } },
{ frame: 1500, bbox: { x: 88, y: 65, w: 11, h: 8 } },
{ frame: 1847, bbox: { x: 88, y: 65, w: 11, h: 8 } },
]),
createTrack('S-005', 'Stationary Echo', 'stationary', 0.78, 0.3, 5.6, 1.9, [
{ frame: 200, bbox: { x: 35, y: 58, w: 5, h: 4 } },
{ frame: 700, bbox: { x: 35, y: 58, w: 5, h: 4 } },
{ frame: 1200, bbox: { x: 35, y: 58, w: 5, h: 4 } },
{ frame: 1700, bbox: { x: 35, y: 58, w: 5, h: 4 } },
]),
];
// ── 18 Mock Timeline Events ─────────────────────────────────────
const MOCK_EVENTS = [
{ frame: 0, time: '00:00:00', type: 'system', label: 'Mission Start', description: 'ISR feed initialized. All sensors nominal.', priority: 'low' },
{ frame: 85, time: '00:00:03', type: 'detection', label: 'First Detection', description: 'Person Alpha acquired at sector 2.', priority: 'medium' },
{ frame: 210, time: '00:00:07', type: 'detection', label: 'Vehicle Spotted', description: 'Vehicle Alpha entering FOV from west.', priority: 'medium' },
{ frame: 400, time: '00:00:13', type: 'alert', label: 'Drone Detected', description: 'Drone Alpha detected at high altitude. Tracking.', priority: 'high' },
{ frame: 520, time: '00:00:17', type: 'tracking', label: 'Track Merge', description: 'Tracks P-003 and P-005 proximity alert.', priority: 'low' },
{ frame: 650, time: '00:00:22', type: 'detection', label: 'New Vehicle', description: 'Vehicle Delta — high speed approach detected.', priority: 'medium' },
{ frame: 780, time: '00:00:26', type: 'alert', label: 'Speed Warning', description: 'Vehicle Delta exceeding 75 kph in restricted zone.', priority: 'high' },
{ frame: 800, time: '00:00:27', type: 'alert', label: 'Second Drone', description: 'Drone Bravo detected. Multiple aerial contacts.', priority: 'high' },
{ frame: 920, time: '00:00:31', type: 'tracking', label: 'Track Lost', description: 'Vehicle Delta exited FOV at sector 1.', priority: 'medium' },
{ frame: 1050, time: '00:00:35', type: 'detection', label: 'Crowd Formation', description: '5 persons converging at grid reference 4-C.', priority: 'medium' },
{ frame: 1180, time: '00:00:39', type: 'system', label: 'Depth Map Update', description: 'Depth estimation recalibrated. Accuracy +12%.', priority: 'low' },
{ frame: 1300, time: '00:00:43', type: 'alert', label: 'Drone Maneuver', description: 'Drone Alpha executing erratic flight pattern.', priority: 'high' },
{ frame: 1380, time: '00:00:46', type: 'tracking', label: 'Re-ID Confirmed', description: 'Person Hotel re-identified after occlusion.', priority: 'low' },
{ frame: 1450, time: '00:00:48', type: 'detection', label: 'Vehicle Convoy', description: 'Vehicles Bravo and Echo moving in formation.', priority: 'medium' },
{ frame: 1550, time: '00:00:52', type: 'alert', label: 'Perimeter Breach', description: 'Drone Bravo approaching restricted airspace.', priority: 'high' },
{ frame: 1650, time: '00:00:55', type: 'tracking', label: 'Person Foxtrot Exit', description: 'Person Foxtrot exited FOV. Track archived.', priority: 'low' },
{ frame: 1750, time: '00:00:58', type: 'system', label: 'Frame Drop Warning', description: 'Processing latency spike: 45ms → 120ms.', priority: 'medium' },
{ frame: 1847, time: '00:01:02', type: 'system', label: 'Mission Complete', description: 'ISR feed terminated. 20 tracks catalogued.', priority: 'low' },
];
// ── Mock Detection Density (100 values, 0-1) ────────────────────
const MOCK_DENSITY = [];
const DENSITY_TYPES = [];
(function generateDensity() {
const segments = 100;
for (let i = 0; i < segments; i++) {
const t = i / segments;
// Base density with peaks at key event areas
let d = 0.15 + Math.random() * 0.15;
// Peak near frame 400 (drone entry)
d += 0.35 * Math.exp(-Math.pow((t - 0.22) / 0.05, 2));
// Peak near frame 800 (second drone + speed warning)
d += 0.45 * Math.exp(-Math.pow((t - 0.43) / 0.06, 2));
// Peak near frame 1050 (crowd)
d += 0.30 * Math.exp(-Math.pow((t - 0.57) / 0.04, 2));
// Peak near frame 1300-1550 (drone maneuver + perimeter breach)
d += 0.50 * Math.exp(-Math.pow((t - 0.75) / 0.08, 2));
// Gentle ramp at end
d += 0.10 * Math.exp(-Math.pow((t - 0.95) / 0.06, 2));
MOCK_DENSITY.push(Math.min(1.0, d));
// Dominant type per segment
if (t > 0.20 && t < 0.28) DENSITY_TYPES.push('drone');
else if (t > 0.40 && t < 0.48) DENSITY_TYPES.push('drone');
else if (t > 0.70 && t < 0.82) DENSITY_TYPES.push('drone');
else if (t > 0.30 && t < 0.38) DENSITY_TYPES.push('vehicle');
else if (t > 0.55 && t < 0.62) DENSITY_TYPES.push('person');
else DENSITY_TYPES.push(Math.random() > 0.6 ? 'vehicle' : 'person');
}
})();
// ── Mock Placeholder Data for Depth / Mask / PointCloud ─────────
const MOCK_MASKS = {};
MOCK_TRACKS.forEach(t => {
// Each mask is a placeholder canvas-like descriptor
MOCK_MASKS[t.id] = {
trackId: t.id,
type: 'binary-mask',
width: 640,
height: 480,
data: null, // would be Uint8Array in real impl
};
});
const MOCK_DEPTH = {
type: 'depth-map',
width: 640,
height: 480,
min: 0.5,
max: 150.0,
data: null, // would be Float32Array in real impl
};
const MOCK_POINTCLOUD = {
type: 'point-cloud',
numPoints: 12000,
bounds: { minX: -10, maxX: 10, minY: -5, maxY: 5, minZ: 0, maxZ: 50 },
positions: null, // would be Float32Array(numPoints * 3) in real impl
colors: null, // would be Float32Array(numPoints * 3) in real impl
};
// ── Mock AI Responses ───────────────────────────────────────────
const MOCK_AI_RESPONSES = {
'threat assessment': {
text: 'THREAT ASSESSMENT — 2 aerial contacts (Drone Alpha, Drone Bravo) classified HIGH PRIORITY. Drone Alpha exhibiting erratic flight pattern near restricted airspace. Drone Bravo approaching perimeter. 1 ground vehicle (Vehicle Delta) flagged for excessive speed (78.5 kph) in restricted zone. Recommend immediate action on aerial contacts.',
action: null,
},
'show all drones': {
text: 'Filtering 2 drone tracks: Drone Alpha (D-001, confidence 0.97) and Drone Bravo (D-002, confidence 0.93). Both classified as HIGH PRIORITY aerial threats.',
action: 'filter-drones',
},
'generate report': {
text: 'Generating mission report... Report includes 20 tracked objects across 1847 frames (61.6 seconds). 5 high-priority alerts logged. Report ready for download.',
action: 'show-report',
},
'summarize mission': {
text: 'MISSION SUMMARY — Duration: 61.6s (1847 frames at 30fps). Total tracks: 20 (8 persons, 5 vehicles, 2 drones, 5 stationary). High-priority events: 5 (2 drone detections, 1 speed violation, 1 erratic maneuver, 1 perimeter breach). All tracks catalogued. Recommend review of aerial contact timeline.',
action: null,
},
'count vehicles': {
text: '5 vehicle tracks detected: Vehicle Alpha (45.0 kph), Vehicle Bravo (62.3 kph), Vehicle Charlie (38.7 kph), Vehicle Delta (78.5 kph), Vehicle Echo (31.2 kph). Average speed: 51.1 kph. Vehicle Delta flagged for excessive speed.',
action: null,
},
};
// ── Async Data Functions (mock backend API) ─────────────────────
async function fetchTracks(jobId) {
return MOCK_TRACKS;
}
async function fetchFrame(jobId, frameIdx) {
// In production, returns image blob from MJPEG stream
return null;
}
async function fetchMask(jobId, frameIdx, trackId) {
return MOCK_MASKS[trackId] || null;
}
async function fetchDepth(jobId, frameIdx, trackId = null) {
return MOCK_DEPTH;
}
async function fetchPointCloud(jobId, frameIdx, trackId) {
return MOCK_POINTCLOUD;
}
async function fetchTimelineSummary(jobId) {
return MOCK_DENSITY;
}
async function startDetection(video, config) {
return { jobId: 'demo-001' };
}
async function pollStatus(jobId) {
return {
status: STATE.current,
progress: STATE.playheadFrame / STATE.totalFrames,
};
}
async function askAI(question, context) {
const key = question.toLowerCase().trim();
if (MOCK_AI_RESPONSES[key]) {
return MOCK_AI_RESPONSES[key];
}
// Partial matching — check if the question contains any known keyword
for (const [keyword, response] of Object.entries(MOCK_AI_RESPONSES)) {
if (key.includes(keyword)) {
return response;
}
}
return {
text: 'Command not recognized. Try: threat assessment, show all drones, generate report, summarize mission, count vehicles',
action: null,
};
}
// ── getTracksAtFrame — with linear bbox interpolation ───────────
function getTracksAtFrame(frameIdx) {
const visible = [];
for (const track of MOCK_TRACKS) {
const kf = track.keyframes;
if (kf.length === 0) continue;
const firstFrame = kf[0].frame;
const lastFrame = kf[kf.length - 1].frame;
// Track not visible outside its keyframe range
if (frameIdx < firstFrame || frameIdx > lastFrame) continue;
// Find surrounding keyframes for interpolation
let before = null;
let after = null;
for (let i = 0; i < kf.length; i++) {
if (kf[i].frame <= frameIdx) {
before = kf[i];
}
if (kf[i].frame >= frameIdx && after === null) {
after = kf[i];
}
}
let bbox;
if (!before || !after || before.frame === after.frame) {
// Exact keyframe or edge case
bbox = { ...(before || after).bbox };
} else {
// Linear interpolation
const t = (frameIdx - before.frame) / (after.frame - before.frame);
bbox = {
x: before.bbox.x + (after.bbox.x - before.bbox.x) * t,
y: before.bbox.y + (after.bbox.y - before.bbox.y) * t,
w: before.bbox.w + (after.bbox.w - before.bbox.w) * t,
h: before.bbox.h + (after.bbox.h - before.bbox.h) * t,
};
}
visible.push({
...track,
bbox,
interpolatedFrame: frameIdx,
});
}
return visible;
}
// ── Boot Log ────────────────────────────────────────────────────
console.log('[ISR Command Center] Mock data layer initialized.');
console.log(` Tracks: ${MOCK_TRACKS.length}`);
console.log(` Events: ${MOCK_EVENTS.length}`);
console.log(` Density segments: ${MOCK_DENSITY.length}`);
console.log(` AI commands: ${Object.keys(MOCK_AI_RESPONSES).length}`);
/* ================================================================
* TASK 3: Top Bar — Clock
* ================================================================ */
function updateClock() {
const el = document.getElementById('liveClock');
if (!el) return;
const now = new Date();
const h = String(now.getUTCHours()).padStart(2, '0');
const m = String(now.getUTCMinutes()).padStart(2, '0');
const s = String(now.getUTCSeconds()).padStart(2, '0');
el.textContent = `${h}:${m}:${s}Z`;
}
/* ================================================================
* TASK 4: Video Feed — Rendering
* ================================================================ */
let videoCanvas, videoCtx, overlayCanvas, overlayCtx;
let scanLineY = 0;
let animFrame = 0;
let playbackSpeed = 1;
let lastFrameTime = 0;
function initCanvases() {
videoCanvas = document.getElementById('videoCanvas');
overlayCanvas = document.getElementById('overlayCanvas');
videoCtx = videoCanvas.getContext('2d');
overlayCtx = overlayCanvas.getContext('2d');
resizeCanvases();
}
function resizeCanvases() {
const container = document.getElementById('videoFeed');
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
[videoCanvas, overlayCanvas].forEach(c => {
c.width = rect.width * dpr;
c.height = rect.height * dpr;
c.style.width = rect.width + 'px';
c.style.height = rect.height + 'px';
c.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0);
});
}
function renderVideoBackground(ctx, w, h, frame) {
ctx.fillStyle = '#0a0f1a';
ctx.fillRect(0, 0, w, h);
// Grid lines
ctx.strokeStyle = 'rgba(255,255,255,0.02)';
ctx.lineWidth = 0.5;
for (let x = 0; x < w; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
}
for (let y = 0; y < h; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
// Scan line — slow horizontal sweep
scanLineY = (scanLineY + 0.5) % h;
const scanGrad = ctx.createLinearGradient(0, scanLineY - 30, 0, scanLineY + 30);
scanGrad.addColorStop(0, 'rgba(255,255,255,0)');
scanGrad.addColorStop(0.5, 'rgba(255,255,255,0.015)');
scanGrad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = scanGrad;
ctx.fillRect(0, scanLineY - 30, w, 60);
// Vignette effect — darker at edges
const vignetteGrad = ctx.createRadialGradient(w * 0.5, h * 0.5, Math.min(w, h) * 0.25, w * 0.5, h * 0.5, Math.max(w, h) * 0.75);
vignetteGrad.addColorStop(0, 'rgba(0,0,0,0)');
vignetteGrad.addColorStop(1, 'rgba(0,0,0,0.35)');
ctx.fillStyle = vignetteGrad;
ctx.fillRect(0, 0, w, h);
// Corner crosshair markers — military tactical HUD
const crossLen = 18;
const crossOff = 12;
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
// Top-left
ctx.beginPath();
ctx.moveTo(crossOff, crossOff); ctx.lineTo(crossOff + crossLen, crossOff);
ctx.moveTo(crossOff, crossOff); ctx.lineTo(crossOff, crossOff + crossLen);
ctx.stroke();
// Top-right
ctx.beginPath();
ctx.moveTo(w - crossOff, crossOff); ctx.lineTo(w - crossOff - crossLen, crossOff);
ctx.moveTo(w - crossOff, crossOff); ctx.lineTo(w - crossOff, crossOff + crossLen);
ctx.stroke();
// Bottom-left
ctx.beginPath();
ctx.moveTo(crossOff, h - crossOff); ctx.lineTo(crossOff + crossLen, h - crossOff);
ctx.moveTo(crossOff, h - crossOff); ctx.lineTo(crossOff, h - crossOff - crossLen);
ctx.stroke();
// Bottom-right
ctx.beginPath();
ctx.moveTo(w - crossOff, h - crossOff); ctx.lineTo(w - crossOff - crossLen, h - crossOff);
ctx.moveTo(w - crossOff, h - crossOff); ctx.lineTo(w - crossOff, h - crossOff - crossLen);
ctx.stroke();
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
}
// Track which boxes have been seen, for fade-in animation
const boxFirstSeen = {};
let boxAnimTime = 0;
function renderDetections(ctx, w, h, frame) {
ctx.clearRect(0, 0, w, h);
const tracks = getTracksAtFrame(frame);
boxAnimTime = performance.now();
for (const t of tracks) {
const bx = (t.bbox.x / 100) * w;
const by = (t.bbox.y / 100) * h;
const bw = (t.bbox.w / 100) * w;
const bh = (t.bbox.h / 100) * h;
// Fade-in: track when boxes first appear
if (!boxFirstSeen[t.id]) {
boxFirstSeen[t.id] = boxAnimTime;
}
const age = boxAnimTime - boxFirstSeen[t.id];
const fadeAlpha = Math.min(1, age / 300); // 300ms fade-in
const isSelected = (STATE.selectedTrackId === t.id);
const isHighlighted = (highlightedTrackId === t.id) || isSelected;
const lineWidth = isHighlighted ? 2.5 : 1.5;
ctx.globalAlpha = fadeAlpha;
// Pulsing glow for highlighted boxes
if (isHighlighted) {
const pulse = 0.6 + 0.4 * Math.sin(boxAnimTime / 300);
ctx.shadowColor = t.color;
ctx.shadowBlur = 8 + 8 * pulse;
}
// Selected box: animated dashed border
if (isSelected) {
ctx.setLineDash([6, 4]);
ctx.lineDashOffset = -(boxAnimTime / 50); // marching ants
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
roundRect(ctx, bx, by, bw, bh, 3);
ctx.stroke();
ctx.setLineDash([]);
ctx.lineDashOffset = 0;
}
// Rounded rect border
ctx.strokeStyle = isHighlighted ? '#fff' : t.color;
ctx.lineWidth = lineWidth;
roundRect(ctx, bx, by, bw, bh, 3);
ctx.stroke();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
// Faint fill
ctx.fillStyle = t.color + (isHighlighted ? '1A' : '0D');
roundRect(ctx, bx, by, bw, bh, 3);
ctx.fill();
// Label badge above
const labelText = `${t.label.split(' ').pop()} ${t.confidence.toFixed(2)}`;
ctx.font = '500 9px Inter, sans-serif';
const tm = ctx.measureText(labelText);
const lw = tm.width + 8;
const lh = 14;
const lx = bx;
const ly = by - lh - 3;
ctx.fillStyle = t.color + 'CC';
roundRect(ctx, lx, ly, lw, lh, 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.textBaseline = 'middle';
ctx.fillText(labelText, lx + 4, ly + lh / 2);
ctx.globalAlpha = 1;
}
}
function updateFrameCounter() {
const el = document.getElementById('frameCounter');
if (el) el.textContent = `FRM ${STATE.playheadFrame} / ${STATE.totalFrames}`;
}
function mainRenderLoop(timestamp) {
if (!videoCanvas) { requestAnimationFrame(mainRenderLoop); return; }
const container = document.getElementById('videoFeed');
const rect = container.getBoundingClientRect();
const w = rect.width;
const h = rect.height;
renderVideoBackground(videoCtx, w, h, STATE.playheadFrame);
if (STATE.current !== 'ready') {
renderDetections(overlayCtx, w, h, STATE.playheadFrame);
} else {
overlayCtx.clearRect(0, 0, w, h);
}
// Advance playhead if playing
if (STATE.isPlaying && (STATE.current === 'playing' || STATE.current === 'analysis')) {
if (!lastFrameTime) lastFrameTime = timestamp;
const elapsed = timestamp - lastFrameTime;
const framesPerMs = (STATE.fps * playbackSpeed) / 1000;
const framesToAdvance = Math.floor(elapsed * framesPerMs);
if (framesToAdvance > 0) {
STATE.playheadFrame = Math.min(STATE.playheadFrame + framesToAdvance, STATE.totalFrames);
lastFrameTime = timestamp;
updateFrameCounter();
updatePlayheadPosition();
updateTimeDisplay();
if (STATE.playheadFrame >= STATE.totalFrames) {
STATE.isPlaying = false;
STATE.playheadFrame = STATE.totalFrames;
document.getElementById('playPauseBtn').innerHTML = '&#9654;';
}
}
}
requestAnimationFrame(mainRenderLoop);
}
/* ================================================================
* TASK 8: State Machine
* ================================================================ */
let processingInterval = null;
let playbackAnimFrame = null;
let highlightedTrackId = null;
function setState(newState) {
const prev = STATE.current;
STATE.current = newState;
document.body.setAttribute('data-state', newState);
onStateChange(prev, newState);
}
function onStateChange(prev, newState) {
const topDot = document.querySelector('.topbar-left .dot');
const missionLabel = document.querySelector('.topbar-mission');
// Update top bar status
if (newState === 'ready') {
if (topDot) {
topDot.style.color = 'var(--accent)';
topDot.style.background = 'var(--accent)';
topDot.classList.remove('topbar-dot-processing');
}
if (missionLabel) missionLabel.textContent = 'MISSION ACTIVE';
} else if (newState === 'processing') {
if (topDot) {
topDot.style.color = 'var(--warning)';
topDot.style.background = 'var(--warning)';
topDot.classList.add('topbar-dot-processing');
}
if (missionLabel) {
missionLabel.style.color = 'var(--warning)';
missionLabel.textContent = 'PROCESSING';
}
} else if (newState === 'analysis' || newState === 'playing') {
if (topDot) {
topDot.style.color = 'var(--success)';
topDot.style.background = 'var(--success)';
topDot.classList.remove('topbar-dot-processing');
}
if (missionLabel) {
missionLabel.style.color = 'var(--success)';
missionLabel.textContent = 'ANALYSIS';
}
} else if (newState === 'inspect') {
if (topDot) {
topDot.style.color = 'var(--accent-light)';
topDot.style.background = 'var(--accent-light)';
topDot.classList.remove('topbar-dot-processing');
}
if (missionLabel) {
missionLabel.style.color = 'var(--accent-light)';
missionLabel.textContent = 'INSPECT';
}
}
// Drawer tab availability managed by CSS, but switch to appropriate tab
if (newState === 'ready') {
switchDrawerTab('tracks');
} else if (newState === 'processing') {
switchDrawerTab('tracks');
} else if (newState === 'analysis' || newState === 'playing') {
switchDrawerTab('tracks');
} else if (newState === 'inspect') {
switchDrawerTab('inspect');
}
}
/* ================================================================
* TASK 8: Processing Simulation
* ================================================================ */
function startProcessingSimulation() {
// Fade out start button before transitioning
const startBtn = document.getElementById('startBtn');
if (startBtn) startBtn.classList.add('fading-out');
// Clear box animation tracking for fresh detection appearances
Object.keys(boxFirstSeen).forEach(k => delete boxFirstSeen[k]);
setTimeout(() => {
setState('processing');
const circumference = 2 * Math.PI * 34;
const circle = document.getElementById('progressCircle');
const pctEl = document.getElementById('progressPct');
const waveformCanvas = document.getElementById('waveformCanvas');
const tracksPanel = document.getElementById('tracksPanel');
const topBarProgress = document.getElementById('topBarProgress');
const fpsDisplay = document.getElementById('fpsDisplay');
STATE.playheadFrame = 0;
let startTime = performance.now();
const duration = 10000; // 10 seconds
let lastTrackIndex = 0;
const trackStaggerInterval = 500; // one every 0.5s
let lastFpsFlicker = 0;
// Clear track list
tracksPanel.innerHTML = '';
processingInterval = setInterval(() => {
const elapsed = performance.now() - startTime;
const progress = Math.min(elapsed / duration, 1.0);
// Update playhead frame linearly
STATE.playheadFrame = Math.round(progress * STATE.totalFrames);
updateFrameCounter();
updatePlayheadPosition();
// Update time display
updateTimeDisplay();
// Update progress ring
const offset = circumference - progress * circumference;
circle.style.strokeDashoffset = offset;
pctEl.textContent = Math.round(progress * 100) + '%';
// Update top bar progress bar
if (topBarProgress) {
topBarProgress.style.width = (progress * 100) + '%';
}
// FPS flicker effect — every ~800ms
if (fpsDisplay && elapsed - lastFpsFlicker > 800) {
lastFpsFlicker = elapsed;
const fpsVal = 45 + Math.floor(Math.random() * 8);
fpsDisplay.textContent = fpsVal + ' FPS';
fpsDisplay.classList.add('fps-updating');
setTimeout(() => fpsDisplay.classList.remove('fps-updating'), 300);
}
// Progressively fill waveform — render partial density
if (waveformCanvas) {
const visibleBars = Math.round(progress * MOCK_DENSITY.length);
const partialDensity = MOCK_DENSITY.map((v, i) => i < visibleBars ? v : 0);
renderWaveform(waveformCanvas, partialDensity);
}
// Stagger track cards
const tracksToShow = Math.min(Math.floor(elapsed / trackStaggerInterval), MOCK_TRACKS.length);
while (lastTrackIndex < tracksToShow) {
appendTrackCard(MOCK_TRACKS[lastTrackIndex], tracksPanel);
lastTrackIndex++;
}
// Completion
if (progress >= 1.0) {
clearInterval(processingInterval);
processingInterval = null;
// Reset top bar progress
if (topBarProgress) {
topBarProgress.style.width = '100%';
setTimeout(() => {
topBarProgress.style.opacity = '0';
setTimeout(() => { topBarProgress.style.width = '0%'; }, 500);
}, 300);
}
// Reset start button state and FPS display
if (startBtn) startBtn.classList.remove('fading-out');
if (fpsDisplay) fpsDisplay.textContent = '48 FPS';
// Brief pause then transition to analysis
setTimeout(() => {
enterAnalysisState();
}, 400);
}
}, 1000 / 60); // ~60fps
}, 300); // Wait for start button fade
}
function appendTrackCard(track, container) {
const card = document.createElement('div');
card.className = 'track-card';
card.dataset.trackId = track.id;
card.style.opacity = '0';
card.style.transform = 'translateY(8px)';
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
card.innerHTML = `
<div class="track-dot" style="background: ${track.color}; box-shadow: 0 0 4px ${track.color};"></div>
<div class="track-info">
<div class="track-label">${track.label}</div>
<div class="track-meta">
<span>${track.speed.toFixed(1)} kph</span>
<span>${track.depth.toFixed(1)}m</span>
<span>${track.id}</span>
</div>
</div>
<span class="track-conf">${track.confidence.toFixed(2)}</span>
`;
wireTrackCard(card, track);
container.appendChild(card);
// Trigger animation
requestAnimationFrame(() => {
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
});
}
function wireTrackCard(card, track) {
card.addEventListener('click', () => {
if (STATE.current === 'analysis' || STATE.current === 'playing') {
enterInspectState(track.id);
} else if (STATE.current === 'inspect') {
enterInspectState(track.id);
} else {
STATE.selectedTrackId = STATE.selectedTrackId === track.id ? null : track.id;
document.querySelectorAll('.track-card').forEach(c => {
c.classList.toggle('selected', c.dataset.trackId === STATE.selectedTrackId);
});
}
});
// Hover sync: card -> canvas
card.addEventListener('mouseenter', () => {
highlightedTrackId = track.id;
card.classList.add('highlighted');
});
card.addEventListener('mouseleave', () => {
highlightedTrackId = null;
card.classList.remove('highlighted');
});
}
/* ================================================================
* TASK 8: Analysis State Entry
* ================================================================ */
function enterAnalysisState() {
setState('analysis');
// Reset playhead to 0
STATE.playheadFrame = 0;
STATE.isPlaying = false;
lastFrameTime = 0;
updateFrameCounter();
updatePlayheadPosition();
updateTimeDisplay();
document.getElementById('playPauseBtn').innerHTML = '&#9654;';
// Render full waveform
const waveformCanvas = document.getElementById('waveformCanvas');
if (waveformCanvas) renderWaveform(waveformCanvas, MOCK_DENSITY);
// Show full track list with interaction wiring
renderTrackListFull(MOCK_TRACKS, STATE.trackFilter);
// Render metrics panel
renderMetricsPanel();
showToast('Processing complete. 20 tracks detected across 1847 frames. Use timeline to review.', 6000);
}
function renderTrackListFull(tracks, filter) {
const panel = document.getElementById('tracksPanel');
panel.innerHTML = '';
const filtered = filter ? tracks.filter(t => t.type === filter) : tracks;
if (filtered.length === 0) {
panel.innerHTML = '<div style="color: var(--text-tertiary); font-size: 10px; text-align: center; padding: 20px;">No tracks match the current filter.</div>';
return;
}
// If there's a filter, add a clear filter button
if (filter) {
const clearBtn = document.createElement('button');
clearBtn.className = 'inspect-back-btn';
clearBtn.style.marginBottom = '8px';
clearBtn.style.width = '100%';
clearBtn.textContent = 'CLEAR FILTER — SHOW ALL';
clearBtn.addEventListener('click', () => {
STATE.trackFilter = null;
renderTrackListFull(MOCK_TRACKS, null);
});
panel.appendChild(clearBtn);
}
for (const t of filtered) {
const card = document.createElement('div');
card.className = 'track-card' + (STATE.selectedTrackId === t.id ? ' selected' : '');
card.dataset.trackId = t.id;
card.innerHTML = `
<div class="track-dot" style="background: ${t.color}; box-shadow: 0 0 4px ${t.color};"></div>
<div class="track-info">
<div class="track-label">${t.label}</div>
<div class="track-meta">
<span>${t.speed.toFixed(1)} kph</span>
<span>${t.depth.toFixed(1)}m</span>
<span>${t.id}</span>
</div>
</div>
<span class="track-conf">${t.confidence.toFixed(2)}</span>
`;
wireTrackCard(card, t);
panel.appendChild(card);
}
}
function renderMetricsPanel() {
const panel = document.getElementById('metricsPanel');
if (!panel) return;
const personCount = MOCK_TRACKS.filter(t => t.type === 'person').length;
const vehicleCount = MOCK_TRACKS.filter(t => t.type === 'vehicle').length;
const droneCount = MOCK_TRACKS.filter(t => t.type === 'drone').length;
const stationaryCount = MOCK_TRACKS.filter(t => t.type === 'stationary').length;
const totalCount = MOCK_TRACKS.length;
const avgConf = (MOCK_TRACKS.reduce((s, t) => s + t.confidence, 0) / totalCount).toFixed(2);
const breakdown = [
{ label: 'Person', count: personCount, color: TYPE_COLORS.person },
{ label: 'Vehicle', count: vehicleCount, color: TYPE_COLORS.vehicle },
{ label: 'Drone', count: droneCount, color: TYPE_COLORS.drone },
{ label: 'Stationary', count: stationaryCount, color: TYPE_COLORS.stationary },
];
const maxCount = Math.max(...breakdown.map(b => b.count));
panel.innerHTML = `
<div class="metric-hero">
<div class="metric-big-number">${totalCount}</div>
<div class="label">TOTAL DETECTIONS</div>
</div>
<div class="metric-breakdown">
${breakdown.map(b => `
<div class="metric-bar-row">
<span class="metric-bar-label">${b.label}</span>
<div class="metric-bar-track">
<div class="metric-bar-fill" style="width: ${maxCount > 0 ? (b.count / maxCount) * 100 : 0}%; background: ${b.color};"></div>
</div>
<span class="metric-bar-count" style="color: ${b.color};">${b.count}</span>
</div>
`).join('')}
</div>
<div class="metric-stats">
<div class="metric-stat-row"><span class="metric-stat-label">AVG CONFIDENCE</span><span class="metric-stat-value">${avgConf}</span></div>
<div class="metric-stat-row"><span class="metric-stat-label">PROCESSING FPS</span><span class="metric-stat-value">48</span></div>
<div class="metric-stat-row"><span class="metric-stat-label">GPU UTILIZATION</span><span class="metric-stat-value">94%</span></div>
<div class="metric-stat-row"><span class="metric-stat-label">TOTAL FRAMES</span><span class="metric-stat-value">${STATE.totalFrames}</span></div>
<div class="metric-stat-row"><span class="metric-stat-label">PROCESS TIME</span><span class="metric-stat-value">38.5s</span></div>
</div>
`;
}
/* ================================================================
* TASK 10: Inspect State — 2x2 Quad View
* ================================================================ */
let threeScene = null;
let threeCamera = null;
let threeRenderer = null;
let threeMesh = null;
let threeAnimId = null;
function enterInspectState(trackId) {
// Clean up previous 3D viewer
destroy3DViewer();
STATE.selectedTrackId = trackId;
STATE.expandedQuadrant = null;
setState('inspect');
const track = MOCK_TRACKS.find(t => t.id === trackId);
if (!track) return;
const panel = document.getElementById('inspectPanel');
panel.innerHTML = `
<div id="inspectHeader">
<button id="inspectBack">&larr; Back</button>
<span id="inspectLabel" style="color: ${track.color};">${track.label.toUpperCase()}</span>
<span id="inspectConf" class="value">${track.confidence.toFixed(2)}</span>
</div>
<div id="quadGrid">
<div class="quadrant" data-quad="seg">
<div class="quad-label" style="color: var(--accent)">SEGMENTATION</div>
<canvas class="quad-canvas"></canvas>
<div class="quad-metric"></div>
</div>
<div class="quadrant" data-quad="edge">
<div class="quad-label" style="color: #94a3b8">EDGE</div>
<canvas class="quad-canvas"></canvas>
</div>
<div class="quadrant" data-quad="depth">
<div class="quad-label" style="color: var(--warning)">DEPTH</div>
<canvas class="quad-canvas"></canvas>
<div class="quad-metric"></div>
</div>
<div class="quadrant" data-quad="3d">
<div class="quad-label" style="color: var(--success)">3D MESH</div>
<div id="threeContainer"></div>
<div class="quad-metric"></div>
</div>
</div>
<div id="inspectMetrics"></div>
`;
// Render all 4 quadrants
requestAnimationFrame(() => {
const segCanvas = panel.querySelector('[data-quad="seg"] .quad-canvas');
const edgeCanvas = panel.querySelector('[data-quad="edge"] .quad-canvas');
const depthCanvas = panel.querySelector('[data-quad="depth"] .quad-canvas');
const threeContainer = document.getElementById('threeContainer');
if (segCanvas) renderSegmentation(segCanvas, track);
if (edgeCanvas) renderEdgeDetection(edgeCanvas, track);
if (depthCanvas) renderDepthHeatmap(depthCanvas, track);
if (threeContainer) init3DViewer(threeContainer, track);
// Update quad metrics
const segMetric = panel.querySelector('[data-quad="seg"] .quad-metric');
const depthMetric = panel.querySelector('[data-quad="depth"] .quad-metric');
const meshMetric = panel.querySelector('[data-quad="3d"] .quad-metric');
if (segMetric) segMetric.textContent = `AREA ${track.area.toFixed(1)}%`;
if (depthMetric) depthMetric.textContent = `${track.depth.toFixed(1)}m`;
if (meshMetric) meshMetric.textContent = '12,847 pts';
});
// Inspect metrics strip
const metricsStrip = document.getElementById('inspectMetrics');
if (metricsStrip) {
metricsStrip.innerHTML = `
<div class="inspect-metric-item"><div class="label">VELOCITY</div><div class="value">${track.speed.toFixed(1)} kph</div></div>
<div class="inspect-metric-item"><div class="label">DEPTH</div><div class="value">${track.depth.toFixed(1)}m</div></div>
<div class="inspect-metric-item"><div class="label">AREA</div><div class="value">${track.area.toFixed(1)}%</div></div>
<div class="inspect-metric-item"><div class="label">CONFIDENCE</div><div class="value">${track.confidence.toFixed(2)}</div></div>
`;
}
// Wire back button
document.getElementById('inspectBack').addEventListener('click', exitInspectState);
// Wire quadrant expand/collapse
panel.querySelectorAll('.quadrant').forEach(q => {
q.addEventListener('click', () => {
const quadName = q.dataset.quad;
if (STATE.expandedQuadrant === quadName) {
// Collapse
q.classList.remove('expanded');
STATE.expandedQuadrant = null;
// Show other quadrants
panel.querySelectorAll('.quadrant').forEach(oq => oq.style.display = '');
} else {
// Collapse any previous
panel.querySelectorAll('.quadrant.expanded').forEach(eq => eq.classList.remove('expanded'));
panel.querySelectorAll('.quadrant').forEach(oq => oq.style.display = '');
// Hide others, expand this one
panel.querySelectorAll('.quadrant').forEach(oq => {
if (oq.dataset.quad !== quadName) oq.style.display = 'none';
});
q.classList.add('expanded');
STATE.expandedQuadrant = quadName;
}
// Re-render canvases after layout change
requestAnimationFrame(() => {
const segCanvas = panel.querySelector('[data-quad="seg"] .quad-canvas');
const edgeCanvas = panel.querySelector('[data-quad="edge"] .quad-canvas');
const depthCanvas = panel.querySelector('[data-quad="depth"] .quad-canvas');
if (segCanvas && segCanvas.offsetParent !== null) renderSegmentation(segCanvas, track);
if (edgeCanvas && edgeCanvas.offsetParent !== null) renderEdgeDetection(edgeCanvas, track);
if (depthCanvas && depthCanvas.offsetParent !== null) renderDepthHeatmap(depthCanvas, track);
// Resize 3D if visible
if (threeRenderer && document.getElementById('threeContainer') && document.getElementById('threeContainer').offsetParent !== null) {
const tc = document.getElementById('threeContainer');
threeRenderer.setSize(tc.clientWidth, tc.clientHeight);
if (threeCamera) {
threeCamera.aspect = tc.clientWidth / tc.clientHeight;
threeCamera.updateProjectionMatrix();
}
}
});
});
});
// Pause playback, jump to track midpoint
STATE.isPlaying = false;
document.getElementById('playPauseBtn').innerHTML = '&#9654;';
const midFrame = Math.round((track.keyframes[0].frame + track.keyframes[track.keyframes.length - 1].frame) / 2);
STATE.playheadFrame = midFrame;
updatePlayheadPosition();
updateFrameCounter();
updateTimeDisplay();
}
function exitInspectState() {
destroy3DViewer();
STATE.selectedTrackId = null;
STATE.expandedQuadrant = null;
setState('analysis');
switchDrawerTab('tracks');
renderTrackListFull(MOCK_TRACKS, STATE.trackFilter);
}
/* ── Segmentation Renderer ────────────────────────────────────── */
function renderSegmentation(canvas, track) {
const parent = canvas.parentElement;
const rect = parent.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const w = rect.width;
const h = rect.height;
// Dark background
ctx.fillStyle = '#0a0f1a';
ctx.fillRect(0, 0, w, h);
// Grid
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
ctx.lineWidth = 0.5;
for (let x = 0; x < w; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); }
for (let y = 0; y < h; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
// Draw mask silhouette based on type
const cx = w * 0.5;
const cy = h * 0.5;
ctx.beginPath();
if (track.type === 'person') {
// Organic blob for person
const s = Math.min(w, h) * 0.3;
ctx.moveTo(cx, cy - s);
ctx.bezierCurveTo(cx + s * 0.6, cy - s, cx + s * 0.7, cy - s * 0.3, cx + s * 0.5, cy);
ctx.bezierCurveTo(cx + s * 0.7, cy + s * 0.4, cx + s * 0.4, cy + s, cx, cy + s);
ctx.bezierCurveTo(cx - s * 0.4, cy + s, cx - s * 0.7, cy + s * 0.4, cx - s * 0.5, cy);
ctx.bezierCurveTo(cx - s * 0.7, cy - s * 0.3, cx - s * 0.6, cy - s, cx, cy - s);
} else if (track.type === 'vehicle') {
// Angular shape for vehicle
const sw = Math.min(w, h) * 0.4;
const sh = Math.min(w, h) * 0.25;
ctx.moveTo(cx - sw, cy - sh * 0.3);
ctx.lineTo(cx - sw * 0.6, cy - sh);
ctx.lineTo(cx + sw * 0.6, cy - sh);
ctx.lineTo(cx + sw, cy - sh * 0.3);
ctx.lineTo(cx + sw, cy + sh);
ctx.lineTo(cx - sw, cy + sh);
} else if (track.type === 'drone') {
// Triangular / cross shape for drone
const s = Math.min(w, h) * 0.25;
ctx.moveTo(cx, cy - s);
ctx.lineTo(cx + s * 1.2, cy + s * 0.3);
ctx.lineTo(cx + s * 0.3, cy + s * 0.1);
ctx.lineTo(cx, cy + s);
ctx.lineTo(cx - s * 0.3, cy + s * 0.1);
ctx.lineTo(cx - s * 1.2, cy + s * 0.3);
} else {
// Generic rectangle for stationary
const sw = Math.min(w, h) * 0.35;
const sh = Math.min(w, h) * 0.25;
ctx.rect(cx - sw, cy - sh, sw * 2, sh * 2);
}
ctx.closePath();
// Semi-transparent fill
ctx.fillStyle = 'rgba(59,130,246,0.3)';
ctx.fill();
// Glowing contour
ctx.shadowColor = '#3b82f6';
ctx.shadowBlur = 8;
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
ctx.stroke();
ctx.shadowBlur = 0;
}
/* ── Edge Detection Renderer ──────────────────────────────────── */
function renderEdgeDetection(canvas, track) {
const parent = canvas.parentElement;
const rect = parent.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const w = rect.width;
const h = rect.height;
// Black background
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, w, h);
const cx = w * 0.5;
const cy = h * 0.5;
function drawSilhouette(lineWidth, opacity) {
ctx.beginPath();
if (track.type === 'person') {
const s = Math.min(w, h) * 0.3;
ctx.moveTo(cx, cy - s);
ctx.bezierCurveTo(cx + s * 0.6, cy - s, cx + s * 0.7, cy - s * 0.3, cx + s * 0.5, cy);
ctx.bezierCurveTo(cx + s * 0.7, cy + s * 0.4, cx + s * 0.4, cy + s, cx, cy + s);
ctx.bezierCurveTo(cx - s * 0.4, cy + s, cx - s * 0.7, cy + s * 0.4, cx - s * 0.5, cy);
ctx.bezierCurveTo(cx - s * 0.7, cy - s * 0.3, cx - s * 0.6, cy - s, cx, cy - s);
} else if (track.type === 'vehicle') {
const sw = Math.min(w, h) * 0.4;
const sh = Math.min(w, h) * 0.25;
ctx.moveTo(cx - sw, cy - sh * 0.3);
ctx.lineTo(cx - sw * 0.6, cy - sh);
ctx.lineTo(cx + sw * 0.6, cy - sh);
ctx.lineTo(cx + sw, cy - sh * 0.3);
ctx.lineTo(cx + sw, cy + sh);
ctx.lineTo(cx - sw, cy + sh);
} else if (track.type === 'drone') {
const s = Math.min(w, h) * 0.25;
ctx.moveTo(cx, cy - s);
ctx.lineTo(cx + s * 1.2, cy + s * 0.3);
ctx.lineTo(cx + s * 0.3, cy + s * 0.1);
ctx.lineTo(cx, cy + s);
ctx.lineTo(cx - s * 0.3, cy + s * 0.1);
ctx.lineTo(cx - s * 1.2, cy + s * 0.3);
} else {
const sw = Math.min(w, h) * 0.35;
const sh = Math.min(w, h) * 0.25;
ctx.rect(cx - sw, cy - sh, sw * 2, sh * 2);
}
ctx.closePath();
ctx.strokeStyle = `rgba(255,255,255,${opacity})`;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
// Outer edge — strong
drawSilhouette(2.5, 0.9);
// Inner detail lines — weaker
drawSilhouette(1, 0.35);
// Secondary inner detail: offset slightly inward
ctx.save();
ctx.translate(0, 0);
ctx.scale(0.88, 0.88);
ctx.translate(w * 0.5 * (1 - 1/0.88), h * 0.5 * (1 - 1/0.88));
drawSilhouette(0.8, 0.2);
ctx.restore();
}
/* ── Depth Heatmap Renderer ───────────────────────────────────── */
function renderDepthHeatmap(canvas, track) {
const parent = canvas.parentElement;
const rect = parent.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const w = rect.width;
const h = rect.height;
// Viridis-style gradient background
const bgGrad = ctx.createLinearGradient(0, 0, w, h);
bgGrad.addColorStop(0, '#440154'); // deep purple
bgGrad.addColorStop(0.25, '#31688e'); // blue
bgGrad.addColorStop(0.5, '#35b779'); // green
bgGrad.addColorStop(0.75, '#90d743'); // lime
bgGrad.addColorStop(1, '#fde725'); // yellow
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, w, h);
// Warm "closer" region at object center
const cx = w * 0.5;
const cy = h * 0.5;
const maxR = Math.min(w, h) * 0.45;
const radGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxR);
radGrad.addColorStop(0, 'rgba(253,231,37,0.7)'); // warm yellow
radGrad.addColorStop(0.3, 'rgba(253,231,37,0.4)');
radGrad.addColorStop(0.6, 'rgba(144,215,67,0.2)');
radGrad.addColorStop(1, 'rgba(68,1,84,0)'); // transparent at edges
ctx.fillStyle = radGrad;
ctx.fillRect(0, 0, w, h);
// Second warm spot offset
const ox = cx + w * 0.15;
const oy = cy - h * 0.1;
const rad2 = ctx.createRadialGradient(ox, oy, 0, ox, oy, maxR * 0.5);
rad2.addColorStop(0, 'rgba(253,231,37,0.35)');
rad2.addColorStop(1, 'rgba(68,1,84,0)');
ctx.fillStyle = rad2;
ctx.fillRect(0, 0, w, h);
}
/* ── 3D Mesh Viewer ───────────────────────────────────────────── */
function init3DViewer(container, track) {
if (!window.THREE) return;
const w = container.clientWidth || 150;
const h = container.clientHeight || 120;
threeScene = new THREE.Scene();
threeScene.background = new THREE.Color('#0a0f1a');
threeCamera = new THREE.PerspectiveCamera(45, w / h, 0.1, 100);
threeCamera.position.set(3, 2, 3);
threeCamera.lookAt(0, 0, 0);
threeRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
threeRenderer.setSize(w, h);
threeRenderer.setPixelRatio(window.devicePixelRatio || 1);
container.appendChild(threeRenderer.domElement);
// Geometry by type
let geometry;
if (track.type === 'vehicle') {
geometry = new THREE.BoxGeometry(1.6, 0.8, 1);
} else if (track.type === 'person') {
geometry = new THREE.CylinderGeometry(0.4, 0.4, 1.6, 12);
} else if (track.type === 'drone') {
geometry = new THREE.ConeGeometry(0.6, 1.2, 6);
} else {
geometry = new THREE.BoxGeometry(1.2, 0.6, 1.2);
}
const color = new THREE.Color(track.color);
const material = new THREE.MeshBasicMaterial({
color: color,
wireframe: true,
transparent: true,
opacity: 0.7,
});
threeMesh = new THREE.Mesh(geometry, material);
threeScene.add(threeMesh);
// Lighting
const ambient = new THREE.AmbientLight(0x404040);
threeScene.add(ambient);
const point = new THREE.PointLight(0xffffff, 0.8, 50);
point.position.set(5, 5, 5);
threeScene.add(point);
// Auto-rotation animation
let angle = 0;
function animate() {
threeAnimId = requestAnimationFrame(animate);
angle += 0.01;
threeCamera.position.x = 3 * Math.cos(angle);
threeCamera.position.z = 3 * Math.sin(angle);
threeCamera.lookAt(0, 0, 0);
threeMesh.rotation.y += 0.005;
threeRenderer.render(threeScene, threeCamera);
}
animate();
}
function destroy3DViewer() {
if (threeAnimId) {
cancelAnimationFrame(threeAnimId);
threeAnimId = null;
}
if (threeRenderer) {
threeRenderer.dispose();
if (threeRenderer.domElement && threeRenderer.domElement.parentElement) {
threeRenderer.domElement.parentElement.removeChild(threeRenderer.domElement);
}
threeRenderer = null;
}
if (threeMesh) {
if (threeMesh.geometry) threeMesh.geometry.dispose();
if (threeMesh.material) threeMesh.material.dispose();
threeMesh = null;
}
threeScene = null;
threeCamera = null;
}
/* ================================================================
* TASK 9: Mission Report
* ================================================================ */
function renderMissionReport() {
const panel = document.getElementById('tracksPanel');
panel.innerHTML = '';
const report = document.createElement('div');
report.className = 'mission-report';
const highPriorityEvents = MOCK_EVENTS.filter(e => e.priority === 'high');
const personCount = MOCK_TRACKS.filter(t => t.type === 'person').length;
const vehicleCount = MOCK_TRACKS.filter(t => t.type === 'vehicle').length;
const droneCount = MOCK_TRACKS.filter(t => t.type === 'drone').length;
const stationaryCount = MOCK_TRACKS.filter(t => t.type === 'stationary').length;
report.innerHTML = `
<div class="report-header">MISSION REPORT — ISR-2026-03-20</div>
<div class="report-section">
<div class="report-section-title">SUMMARY STATISTICS</div>
<div class="report-stat"><span class="report-stat-label">Duration</span><span class="report-stat-value">61.6s (1847 frames)</span></div>
<div class="report-stat"><span class="report-stat-label">Frame Rate</span><span class="report-stat-value">30 fps</span></div>
<div class="report-stat"><span class="report-stat-label">Total Tracks</span><span class="report-stat-value">${MOCK_TRACKS.length}</span></div>
<div class="report-stat"><span class="report-stat-label">Persons</span><span class="report-stat-value">${personCount}</span></div>
<div class="report-stat"><span class="report-stat-label">Vehicles</span><span class="report-stat-value">${vehicleCount}</span></div>
<div class="report-stat"><span class="report-stat-label">Drones</span><span class="report-stat-value" style="color: var(--danger);">${droneCount}</span></div>
<div class="report-stat"><span class="report-stat-label">Stationary</span><span class="report-stat-value">${stationaryCount}</span></div>
<div class="report-stat"><span class="report-stat-label">High-Priority Alerts</span><span class="report-stat-value" style="color: var(--danger);">${highPriorityEvents.length}</span></div>
</div>
<div class="report-section">
<div class="report-section-title">THREAT ASSESSMENT</div>
${highPriorityEvents.map(evt => `
<div class="report-threat">
<span class="report-threat-time">${evt.time}</span>
<span class="report-threat-label">${evt.label}${evt.description}</span>
</div>
`).join('')}
</div>
<div class="report-section">
<div class="report-section-title">TACTICAL RECOMMENDATIONS</div>
<div class="report-rec">Deploy counter-UAS measures for aerial contacts D-001 and D-002. Erratic flight patterns suggest reconnaissance activity.</div>
<div class="report-rec">Establish speed enforcement at western approach. Vehicle Delta exceeded restricted zone speed limit by 3.5 kph.</div>
<div class="report-rec">Monitor crowd formation at grid 4-C. Five persons converging may indicate planned gathering.</div>
<div class="report-rec">Reinforce perimeter surveillance at sector where Drone Bravo approached restricted airspace.</div>
<div class="report-rec">Increase sensor coverage at FOV edges — two tracks lost due to exit from field of view.</div>
</div>
`;
// Back to tracks button
const backBtn = document.createElement('button');
backBtn.className = 'inspect-back-btn';
backBtn.style.marginTop = '8px';
backBtn.style.width = '100%';
backBtn.textContent = '\u2190 Back to Tracks';
backBtn.addEventListener('click', () => {
renderTrackListFull(MOCK_TRACKS, STATE.trackFilter);
});
panel.appendChild(report);
panel.appendChild(backBtn);
}
/* ================================================================
* TASK 9: Command Bar Actions
* ================================================================ */
function executeAction(action) {
if (action === 'filter-drones') {
STATE.trackFilter = 'drone';
renderTrackListFull(MOCK_TRACKS, 'drone');
// Highlight drone events on timeline
document.querySelectorAll('.event-marker').forEach(m => {
m.style.opacity = '0.3';
});
MOCK_EVENTS.forEach((evt, i) => {
if (evt.type === 'alert' || evt.label.toLowerCase().includes('drone')) {
const markers = document.querySelectorAll('.event-marker');
if (markers[i]) {
markers[i].style.opacity = '1';
markers[i].style.transform = 'translate(-50%, -50%) scale(1.4)';
}
}
});
} else if (action === 'show-report') {
renderMissionReport();
switchDrawerTab('tracks');
}
}
/* ================================================================
* TASK 8/9: Play/Pause + Keyboard Controls
* ================================================================ */
function togglePlayPause() {
if (STATE.current === 'ready') return;
if (STATE.current === 'processing') return;
STATE.isPlaying = !STATE.isPlaying;
lastFrameTime = 0;
const btn = document.getElementById('playPauseBtn');
btn.innerHTML = STATE.isPlaying ? '&#9646;&#9646;' : '&#9654;';
if (STATE.isPlaying && STATE.playheadFrame >= STATE.totalFrames) {
STATE.playheadFrame = 0;
}
// Ensure we're in a playable state
if (STATE.isPlaying && STATE.current === 'analysis') {
STATE.current = 'playing';
document.body.setAttribute('data-state', 'analysis'); // keep analysis CSS
}
if (!STATE.isPlaying && STATE.current === 'playing') {
STATE.current = 'analysis';
document.body.setAttribute('data-state', 'analysis');
}
}
function updateTimeDisplay() {
const totalSeconds = STATE.playheadFrame / STATE.fps;
const min = Math.floor(totalSeconds / 60);
const sec = Math.floor(totalSeconds % 60);
const el = document.getElementById('timeStart');
if (el) el.textContent = String(min).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
}
function seekFrames(delta) {
if (STATE.current === 'ready' || STATE.current === 'processing') return;
STATE.playheadFrame = Math.max(0, Math.min(STATE.totalFrames, STATE.playheadFrame + delta));
updatePlayheadPosition();
updateFrameCounter();
updateTimeDisplay();
}
function handleKeyboardControls(e) {
// Don't capture when typing in command bar or other inputs
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
// Escape priority chain
if (e.code === 'Escape') {
e.preventDefault();
// 1. Expanded quadrant -> collapse
if (STATE.expandedQuadrant) {
const panel = document.getElementById('inspectPanel');
if (panel) {
panel.querySelectorAll('.quadrant.expanded').forEach(eq => eq.classList.remove('expanded'));
panel.querySelectorAll('.quadrant').forEach(oq => oq.style.display = '');
}
STATE.expandedQuadrant = null;
return;
}
// 2. Event log open -> close
if (STATE.eventLogOpen) {
toggleEventLog();
return;
}
// 3. In inspect state -> return to analysis
if (STATE.current === 'inspect') {
exitInspectState();
return;
}
return;
}
if (e.code === 'Space') {
e.preventDefault();
togglePlayPause();
} else if (e.code === 'ArrowRight') {
e.preventDefault();
seekFrames(STATE.fps * 5); // 5 seconds forward
} else if (e.code === 'ArrowLeft') {
e.preventDefault();
seekFrames(-STATE.fps * 5); // 5 seconds backward
} else if (e.code === 'Period') {
e.preventDefault();
seekFrames(1); // next frame
} else if (e.code === 'Comma') {
e.preventDefault();
seekFrames(-1); // previous frame
}
}
/* ================================================================
* TASK 9: Canvas Hit-Testing + Hover Sync
* ================================================================ */
function hitTestBoxes(clientX, clientY) {
const container = document.getElementById('videoFeed');
const rect = container.getBoundingClientRect();
const relX = clientX - rect.left;
const relY = clientY - rect.top;
const w = rect.width;
const h = rect.height;
const tracks = getTracksAtFrame(STATE.playheadFrame);
// Check in reverse order (top-most drawn last)
for (let i = tracks.length - 1; i >= 0; i--) {
const t = tracks[i];
const bx = (t.bbox.x / 100) * w;
const by = (t.bbox.y / 100) * h;
const bw = (t.bbox.w / 100) * w;
const bh = (t.bbox.h / 100) * h;
if (relX >= bx && relX <= bx + bw && relY >= by && relY <= by + bh) {
return t;
}
}
return null;
}
function handleCanvasMouseMove(e) {
if (STATE.current === 'ready' || STATE.current === 'processing') return;
const hit = hitTestBoxes(e.clientX, e.clientY);
const overlay = document.getElementById('overlayCanvas');
if (hit) {
overlay.style.cursor = 'pointer';
highlightedTrackId = hit.id;
// Highlight matching card
document.querySelectorAll('.track-card').forEach(c => {
c.classList.toggle('highlighted', c.dataset.trackId === hit.id);
});
} else {
overlay.style.cursor = 'default';
highlightedTrackId = null;
document.querySelectorAll('.track-card.highlighted').forEach(c => c.classList.remove('highlighted'));
}
}
function handleCanvasClick(e) {
if (STATE.current === 'ready' || STATE.current === 'processing') return;
const hit = hitTestBoxes(e.clientX, e.clientY);
if (hit) {
enterInspectState(hit.id);
}
}
/* ================================================================
* TASK 5: Drawer — Tabs + Track List
* ================================================================ */
function switchDrawerTab(tabName) {
STATE.drawerTab = tabName;
const tabs = document.querySelectorAll('.drawer-tab');
tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
// Hide all panels first
document.getElementById('configPanel').classList.add('hidden');
document.getElementById('tracksPanel').classList.add('hidden');
document.getElementById('inspectPanel').classList.add('hidden');
document.getElementById('metricsPanel').classList.add('hidden');
if (tabName === 'tracks' || tabName === 'config') {
// Config panel only shows in ready state
if (STATE.current === 'ready') {
document.getElementById('configPanel').classList.remove('hidden');
}
document.getElementById('tracksPanel').classList.remove('hidden');
} else if (tabName === 'inspect') {
document.getElementById('inspectPanel').classList.remove('hidden');
} else if (tabName === 'metrics') {
document.getElementById('metricsPanel').classList.remove('hidden');
}
}
function renderTrackList(tracks, filter) {
const panel = document.getElementById('tracksPanel');
panel.innerHTML = '';
const filtered = filter ? tracks.filter(t => t.type === filter) : tracks;
if (filtered.length === 0) {
panel.innerHTML = '<div style="color: var(--text-tertiary); font-size: 10px; text-align: center; padding: 20px;">No tracks match the current filter.</div>';
return;
}
for (const t of filtered) {
const card = document.createElement('div');
card.className = 'track-card' + (STATE.selectedTrackId === t.id ? ' selected' : '');
card.dataset.trackId = t.id;
card.innerHTML = `
<div class="track-dot" style="background: ${t.color}; box-shadow: 0 0 4px ${t.color};"></div>
<div class="track-info">
<div class="track-label">${t.label}</div>
<div class="track-meta">
<span>${t.speed.toFixed(1)} kph</span>
<span>${t.depth.toFixed(1)}m</span>
<span>${t.id}</span>
</div>
</div>
<span class="track-conf">${t.confidence.toFixed(2)}</span>
`;
card.addEventListener('click', () => {
STATE.selectedTrackId = STATE.selectedTrackId === t.id ? null : t.id;
renderTrackList(tracks, filter);
});
panel.appendChild(card);
}
}
/* ================================================================
* TASK 6: Timeline — Waveform, Events, Playhead
* ================================================================ */
function renderWaveform(canvas, densityData) {
const rect = canvas.parentElement.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const w = rect.width;
const h = rect.height;
const barW = w / densityData.length;
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < densityData.length; i++) {
const val = densityData[i];
const barH = val * h * 0.85;
const x = i * barW;
const y = h - barH;
const color = getColorForType(DENSITY_TYPES[i]);
ctx.fillStyle = color + '88';
ctx.fillRect(x, y, Math.max(barW - 1, 1), barH);
}
}
function renderEventMarkers(container, events) {
container.innerHTML = '';
const EVENT_TYPE_COLORS = {
system: 'var(--text-tertiary)',
detection: 'var(--accent)',
tracking: 'var(--success)',
alert: 'var(--danger)',
};
for (const evt of events) {
const pct = (evt.frame / STATE.totalFrames) * 100;
const marker = document.createElement('div');
marker.className = 'event-marker';
marker.style.left = pct + '%';
marker.style.background = EVENT_TYPE_COLORS[evt.type] || 'var(--text-tertiary)';
if (evt.priority === 'high') {
marker.style.boxShadow = `0 0 4px ${EVENT_TYPE_COLORS[evt.type]}`;
}
marker.title = `${evt.time}${evt.label}`;
marker.addEventListener('click', () => {
STATE.playheadFrame = evt.frame;
updatePlayheadPosition();
updateFrameCounter();
updateTimeDisplay();
});
container.appendChild(marker);
}
}
function updatePlayheadPosition() {
const playhead = document.getElementById('playhead');
if (!playhead) return;
const pct = (STATE.playheadFrame / STATE.totalFrames) * 100;
playhead.style.left = pct + '%';
}
function initPlayheadDrag() {
const wrap = document.querySelector('.timeline-waveform-wrap');
if (!wrap) return;
let dragging = false;
function setFrameFromX(clientX) {
const rect = wrap.getBoundingClientRect();
let pct = (clientX - rect.left) / rect.width;
pct = Math.max(0, Math.min(1, pct));
STATE.playheadFrame = Math.round(pct * STATE.totalFrames);
updatePlayheadPosition();
updateFrameCounter();
}
wrap.addEventListener('mousedown', (e) => {
dragging = true;
setFrameFromX(e.clientX);
});
document.addEventListener('mousemove', (e) => {
if (dragging) setFrameFromX(e.clientX);
});
document.addEventListener('mouseup', () => {
dragging = false;
});
}
function renderTimelineLegend() {
const legend = document.getElementById('timelineLegend');
if (!legend) return;
const counts = { system: 0, detection: 0, tracking: 0, alert: 0 };
const colors = {
system: 'var(--text-tertiary)',
detection: 'var(--accent)',
tracking: 'var(--success)',
alert: 'var(--danger)',
};
MOCK_EVENTS.forEach(e => { if (counts[e.type] !== undefined) counts[e.type]++; });
legend.innerHTML = Object.entries(counts).map(([type, count]) =>
`<span class="timeline-legend-item"><span class="dot" style="background: ${colors[type]};"></span> ${type} (${count})</span>`
).join('');
}
function toggleEventLog() {
STATE.eventLogOpen = !STATE.eventLogOpen;
const log = document.getElementById('eventLog');
const toggle = document.getElementById('eventLogToggle');
if (STATE.eventLogOpen) {
log.classList.remove('hidden');
toggle.innerHTML = 'EVENT LOG &#9650;';
renderEventLog();
} else {
log.classList.add('hidden');
toggle.innerHTML = 'EVENT LOG &#9660;';
}
}
function renderEventLog() {
const log = document.getElementById('eventLog');
if (!log) return;
const EVENT_TYPE_COLORS = {
system: { bg: 'rgba(255,255,255,0.05)', color: 'var(--text-tertiary)' },
detection: { bg: 'rgba(59,130,246,0.12)', color: 'var(--accent-light)' },
tracking: { bg: 'rgba(34,197,94,0.12)', color: 'var(--success)' },
alert: { bg: 'rgba(239,68,68,0.12)', color: 'var(--danger)' },
};
log.innerHTML = MOCK_EVENTS.map(evt => {
const tc = EVENT_TYPE_COLORS[evt.type] || EVENT_TYPE_COLORS.system;
return `
<div class="event-log-entry" data-frame="${evt.frame}">
<span class="event-log-time">${evt.time}</span>
<span class="event-log-type" style="background: ${tc.bg}; color: ${tc.color};">${evt.type}</span>
<span class="event-log-label">${evt.label}</span>
<span class="event-log-desc">${evt.description}</span>
</div>`;
}).join('');
log.querySelectorAll('.event-log-entry').forEach(entry => {
entry.addEventListener('click', () => {
STATE.playheadFrame = parseInt(entry.dataset.frame, 10);
updatePlayheadPosition();
updateFrameCounter();
updateTimeDisplay();
});
});
}
/* ================================================================
* TASK 7: Command Bar + Toast System
* ================================================================ */
function handleCommand(text) {
if (!text.trim()) return;
askAI(text).then(response => {
showToast(response.text);
if (response.action) {
executeAction(response.action);
}
});
}
function showToast(text, duration = 8000) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = 'toast';
const msg = document.createElement('span');
msg.style.flex = '1';
msg.textContent = text;
const close = document.createElement('button');
close.className = 'toast-close';
close.innerHTML = '&times;';
close.addEventListener('click', () => dismissToast(toast));
toast.appendChild(msg);
toast.appendChild(close);
container.appendChild(toast);
setTimeout(() => dismissToast(toast), duration);
}
function dismissToast(toast) {
if (!toast || !toast.parentElement) return;
toast.classList.add('toast-out');
toast.addEventListener('animationend', () => {
if (toast.parentElement) toast.parentElement.removeChild(toast);
});
}
/* ================================================================
* TASK 12: Keyboard Shortcuts Hint
* ================================================================ */
function showShortcutsHint() {
// Remove existing hint if any
const existing = document.getElementById('shortcutsHint');
if (existing) existing.remove();
const commandBar = document.getElementById('commandBar');
const hint = document.createElement('div');
hint.id = 'shortcutsHint';
hint.innerHTML = `
<span class="shortcut-item"><kbd>Space</kbd> Play/Pause</span>
<span class="shortcut-item"><kbd>&larr;</kbd><kbd>&rarr;</kbd> Seek 5s</span>
<span class="shortcut-item"><kbd>,</kbd><kbd>.</kbd> Frame step</span>
<span class="shortcut-item"><kbd>Esc</kbd> Back/Close</span>
`;
commandBar.style.position = 'relative';
commandBar.appendChild(hint);
// Remove after animation completes (3s)
setTimeout(() => {
if (hint.parentElement) hint.remove();
}, 3100);
}
/* ================================================================
* TASK 12: Event Marker Tooltips
* ================================================================ */
function wireEventMarkerTooltips() {
document.querySelectorAll('.event-marker').forEach((marker, idx) => {
const evt = MOCK_EVENTS[idx];
if (!evt) return;
marker.addEventListener('mouseenter', () => {
// Remove old tooltips
document.querySelectorAll('.event-marker-tooltip').forEach(t => t.remove());
const tooltip = document.createElement('div');
tooltip.className = 'event-marker-tooltip';
tooltip.textContent = `${evt.time}${evt.label}: ${evt.description}`;
marker.style.position = 'absolute'; // ensure positioning context
marker.appendChild(tooltip);
});
marker.addEventListener('mouseleave', () => {
marker.querySelectorAll('.event-marker-tooltip').forEach(t => t.remove());
});
});
}
/* ================================================================
* Initialization
* ================================================================ */
document.addEventListener('DOMContentLoaded', () => {
// Task 8: Set initial state
setState('ready');
// Task 3: Clock
updateClock();
setInterval(updateClock, 1000);
// Task 4: Canvases + render loop
initCanvases();
window.addEventListener('resize', resizeCanvases);
requestAnimationFrame(mainRenderLoop);
// Task 8: Start button wired to processing simulation
document.getElementById('startBtn').addEventListener('click', () => {
startProcessingSimulation();
});
// Play/Pause
document.getElementById('playPauseBtn').addEventListener('click', togglePlayPause);
// Speed
document.getElementById('speedSelect').addEventListener('change', (e) => {
playbackSpeed = parseFloat(e.target.value);
});
// Task 5: Drawer tabs
document.querySelectorAll('.drawer-tab').forEach(tab => {
tab.addEventListener('click', () => {
// Respect state-driven tab restrictions via CSS pointer-events
switchDrawerTab(tab.dataset.tab);
});
});
// Mode toggle
document.querySelectorAll('.config-toggle-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.config-toggle-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Init drawer to tracks tab
switchDrawerTab('tracks');
// Task 6: Timeline
const waveformCanvas = document.getElementById('waveformCanvas');
if (waveformCanvas) {
renderWaveform(waveformCanvas, MOCK_DENSITY);
window.addEventListener('resize', () => renderWaveform(waveformCanvas, MOCK_DENSITY));
}
renderEventMarkers(document.getElementById('eventMarkers'), MOCK_EVENTS);
wireEventMarkerTooltips();
renderTimelineLegend();
initPlayheadDrag();
updatePlayheadPosition();
// Event log toggle
document.getElementById('eventLogToggle').addEventListener('click', toggleEventLog);
// Task 7: Command bar — wired with executeAction support
const cmdInput = document.getElementById('commandInput');
cmdInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const text = cmdInput.value.trim();
if (!text) return;
cmdInput.value = '';
askAI(text).then(response => {
showToast(response.text);
if (response.action) {
executeAction(response.action);
}
});
}
});
// Cmd+K / Ctrl+K shortcut with keyboard hints
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
cmdInput.focus();
showShortcutsHint();
}
});
// Task 9: Keyboard controls
document.addEventListener('keydown', handleKeyboardControls);
// Task 9: Canvas mouse interactions (hover + click)
const overlayEl = document.getElementById('overlayCanvas');
overlayEl.addEventListener('mousemove', handleCanvasMouseMove);
overlayEl.addEventListener('click', handleCanvasClick);
console.log('[ISR Command Center] UI initialized. State:', STATE.current);
});
</script>
</body>
</html>