window.ISR = window.ISR || {}; /* ================================================================ * 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(); } // Track which boxes have been seen, for fade-in animation const boxFirstSeen = {}; let boxAnimTime = 0; function renderDetections(ctx, w, h, frame) { const STATE = ISR.STATE; ctx.clearRect(0, 0, w, h); const tracks = ISR.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 = (ISR.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; ISR.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; ISR.roundRect(ctx, bx, by, bw, bh, 3); ctx.stroke(); ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; // Faint fill ctx.fillStyle = t.color + (isHighlighted ? '1A' : '0D'); ISR.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'; ISR.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 STATE = ISR.STATE; const el = document.getElementById('frameCounter'); if (el) el.textContent = `FRM ${STATE.playheadFrame} / ${STATE.totalFrames}`; } function mainRenderLoop(timestamp) { const STATE = ISR.STATE; 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); const svgOverlay = document.getElementById('detectionOverlay'); if (STATE.current !== 'ready') { // Use real detection rendering when we have a jobId in analysis/inspect/playing if (STATE.jobId && (STATE.current === 'analysis' || STATE.current === 'playing' || STATE.current === 'inspect')) { // Real detection rendering is driven by video timeupdate, NOT the render loop // Only update SVG pointer events here if (svgOverlay) svgOverlay.style.pointerEvents = 'all'; } else { renderDetections(overlayCtx, w, h, STATE.playheadFrame); // Hide SVG overlay in mock mode if (svgOverlay) { while (svgOverlay.firstChild) svgOverlay.removeChild(svgOverlay.firstChild); svgOverlay.style.pointerEvents = 'none'; } } } else { overlayCtx.clearRect(0, 0, w, h); // Clear and disable SVG overlay in ready state if (svgOverlay) { while (svgOverlay.firstChild) svgOverlay.removeChild(svgOverlay.firstChild); svgOverlay.style.pointerEvents = 'none'; } } // Advance playhead if playing (mock playback only — video element handles real playback) if (STATE.isPlaying && !STATE.jobId && (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(); ISR.updatePlayheadPosition(); ISR.updateTimeDisplay(); if (STATE.playheadFrame >= STATE.totalFrames) { STATE.isPlaying = false; STATE.playheadFrame = STATE.totalFrames; document.getElementById('playPauseBtn').innerHTML = '▶'; } } } requestAnimationFrame(mainRenderLoop); } // ── Export to namespace ───────────────────────────────────────── Object.assign(window.ISR, { updateClock, initCanvases, resizeCanvases, renderVideoBackground, renderDetections, updateFrameCounter, mainRenderLoop, boxFirstSeen, get videoCanvas() { return videoCanvas; }, get videoCtx() { return videoCtx; }, get overlayCanvas() { return overlayCanvas; }, get overlayCtx() { return overlayCtx; }, get playbackSpeed() { return playbackSpeed; }, set playbackSpeed(v) { playbackSpeed = v; }, get lastFrameTime() { return lastFrameTime; }, set lastFrameTime(v) { lastFrameTime = v; }, });