APP.core.demo = {}; APP.core.demo.data = null; APP.core.demo.active = false; APP.core.demo.load = async function () { // Demo data is now loaded per-video via loadForVideo() // This function is kept for compatibility but does nothing }; APP.core.demo.getFrameData = function (currentTime) { if (!APP.core.demo.data) return null; // Use interpolation for keyframe format if (APP.core.demo.data.format === "keyframes") { return APP.core.demo.getFrameDataInterpolated(currentTime); } // Original logic for frame-by-frame data const fps = APP.core.demo.data.fps || 30; const frameIdx = Math.floor(currentTime * fps); // Get tracks for this frame // Handle both string and number keys if needed const tracks = APP.core.demo.data.frames[frameIdx] || APP.core.demo.data.frames[frameIdx.toString()]; if (!tracks) return []; return tracks; }; APP.core.demo.enable = function (force = true) { const { log } = APP.ui.logging; APP.core.demo.active = force; if (force) { log("DEMO MODE ACTIVATED", "g"); const chipFeed = document.getElementById("chipFeed"); if (chipFeed) chipFeed.textContent = "MODE:DEMO"; } }; // Keyframe interpolation for smooth radar movement APP.core.demo._keyframeIndices = null; APP.core.demo.getKeyframeIndices = function() { if (!APP.core.demo.data || APP.core.demo.data.format !== "keyframes") return null; if (APP.core.demo._keyframeIndices) return APP.core.demo._keyframeIndices; const indices = Object.keys(APP.core.demo.data.keyframes) .map(k => parseInt(k, 10)) .sort((a, b) => a - b); APP.core.demo._keyframeIndices = indices; return indices; }; APP.core.demo.interpolateTrack = function(trackA, trackB, t) { const { lerp } = APP.core.utils; return { id: trackA.id, label: trackA.label, bbox: { x: lerp(trackA.bbox.x, trackB.bbox.x, t), y: lerp(trackA.bbox.y, trackB.bbox.y, t), w: lerp(trackA.bbox.w, trackB.bbox.w, t), h: lerp(trackA.bbox.h, trackB.bbox.h, t) }, gpt_distance_m: lerp(trackA.gpt_distance_m, trackB.gpt_distance_m, t), angle_deg: trackA.angle_deg, speed_kph: lerp(trackA.speed_kph, trackB.speed_kph, t), depth_valid: true, depth_est_m: lerp(trackA.gpt_distance_m, trackB.gpt_distance_m, t), history: [], predicted_path: [] }; }; APP.core.demo.getFrameDataInterpolated = function(currentTime) { const data = APP.core.demo.data; if (!data || data.format !== "keyframes") return null; const fps = data.fps || 24; const frameIdx = Math.floor(currentTime * fps); const keyframes = APP.core.demo.getKeyframeIndices(); if (!keyframes || keyframes.length === 0) return []; // Find surrounding keyframes let beforeIdx = keyframes[0]; let afterIdx = keyframes[keyframes.length - 1]; for (let i = 0; i < keyframes.length; i++) { if (keyframes[i] <= frameIdx) beforeIdx = keyframes[i]; if (keyframes[i] >= frameIdx) { afterIdx = keyframes[i]; break; } } // Edge cases if (frameIdx <= keyframes[0]) return data.keyframes[keyframes[0]] || []; if (frameIdx >= keyframes[keyframes.length - 1]) return data.keyframes[keyframes[keyframes.length - 1]] || []; // Interpolation factor const t = (beforeIdx === afterIdx) ? 0 : (frameIdx - beforeIdx) / (afterIdx - beforeIdx); const tracksBefore = data.keyframes[beforeIdx] || []; const tracksAfter = data.keyframes[afterIdx] || []; // Match by ID and interpolate const result = []; for (const trackA of tracksBefore) { const trackB = tracksAfter.find(tr => tr.id === trackA.id); if (trackB) { result.push(APP.core.demo.interpolateTrack(trackA, trackB, t)); } } return result; }; // Video-specific loading for demo tracks APP.core.demo.loadForVideo = async function(videoName) { const { log } = APP.ui.logging; if (videoName.toLowerCase().includes("enhance_video_movement")) { // Use global variable injected by helicopter_demo_data.js script tag (CORS-safe) if (window.HELICOPTER_DEMO_DATA) { APP.core.demo.data = window.HELICOPTER_DEMO_DATA; APP.core.demo._keyframeIndices = null; // Reset cache log("Helicopter demo tracks loaded (CORS-safe mode).", "g"); return; } // Fallback to fetch (works when served from HTTP server) try { const resp = await fetch("data/helicopter_demo_tracks.json"); if (resp.ok) { APP.core.demo.data = await resp.json(); APP.core.demo._keyframeIndices = null; // Reset cache log("Helicopter demo tracks loaded.", "g"); } } catch (err) { console.warn("Failed to load helicopter demo tracks:", err); } } };