Zhen Ye Claude Opus 4.5 commited on
Commit
d496e6d
·
1 Parent(s): 7a7588e

Add military-style radar with helicopter demo mode

Browse files

- Redesign radar with military tactical aesthetic (green phosphor CRT look)
- Add 4 helicopter tracks with keyframe interpolation for smooth movement
- H01 moves away (triangle points up), H02-H04 approach (triangles point down)
- First frame radar shows static snapshot after Reason clicked
- Live radar shows animated view after Engage clicked
- Remove old unused demo data files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

frontend/data/helicopter_demo_data.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ window.HELICOPTER_DEMO_DATA = {
2
+ "fps": 24,
3
+ "totalFrames": 192,
4
+ "video": "Enhance_Video_Movement.mp4",
5
+ "format": "keyframes",
6
+ "keyframes": {
7
+ "0": [
8
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 200, "y": 260, "w": 180, "h": 120}, "gpt_distance_m": 800, "angle_deg": -90, "speed_kph": 180},
9
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 420, "y": 240, "w": 150, "h": 100}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 180},
10
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 620, "y": 250, "w": 100, "h": 68}, "gpt_distance_m": 1200, "angle_deg": 90, "speed_kph": 185},
11
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 850, "y": 235, "w": 90, "h": 62}, "gpt_distance_m": 1300, "angle_deg": 90, "speed_kph": 190}
12
+ ],
13
+ "48": [
14
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 150, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 700, "angle_deg": -90, "speed_kph": 190},
15
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 400, "y": 255, "w": 160, "h": 105}, "gpt_distance_m": 850, "angle_deg": 90, "speed_kph": 185},
16
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 640, "y": 255, "w": 115, "h": 78}, "gpt_distance_m": 1050, "angle_deg": 90, "speed_kph": 188},
17
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 870, "y": 245, "w": 105, "h": 72}, "gpt_distance_m": 1150, "angle_deg": 90, "speed_kph": 192}
18
+ ],
19
+ "96": [
20
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 80, "y": 300, "w": 220, "h": 145}, "gpt_distance_m": 600, "angle_deg": -90, "speed_kph": 200},
21
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 380, "y": 265, "w": 155, "h": 102}, "gpt_distance_m": 820, "angle_deg": 90, "speed_kph": 182},
22
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 660, "y": 262, "w": 135, "h": 92}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 190},
23
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 890, "y": 258, "w": 125, "h": 85}, "gpt_distance_m": 980, "angle_deg": 90, "speed_kph": 195}
24
+ ],
25
+ "144": [
26
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 50, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 650, "angle_deg": -90, "speed_kph": 195},
27
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 360, "y": 270, "w": 148, "h": 98}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 178},
28
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 680, "y": 272, "w": 158, "h": 108}, "gpt_distance_m": 750, "angle_deg": 90, "speed_kph": 192},
29
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 910, "y": 270, "w": 148, "h": 100}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 198}
30
+ ],
31
+ "191": [
32
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 30, "y": 260, "w": 190, "h": 125}, "gpt_distance_m": 680, "angle_deg": -90, "speed_kph": 190},
33
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 340, "y": 275, "w": 142, "h": 94}, "gpt_distance_m": 780, "angle_deg": 90, "speed_kph": 175},
34
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 700, "y": 280, "w": 180, "h": 122}, "gpt_distance_m": 620, "angle_deg": 90, "speed_kph": 195},
35
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 930, "y": 282, "w": 172, "h": 116}, "gpt_distance_m": 650, "angle_deg": 90, "speed_kph": 200}
36
+ ]
37
+ }
38
+ };
frontend/data/helicopter_demo_tracks.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fps": 24,
3
+ "totalFrames": 192,
4
+ "video": "Enhance_Video_Movement.mp4",
5
+ "format": "keyframes",
6
+ "keyframes": {
7
+ "0": [
8
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 200, "y": 260, "w": 180, "h": 120}, "gpt_distance_m": 800, "angle_deg": -90, "speed_kph": 180},
9
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 420, "y": 240, "w": 150, "h": 100}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 180},
10
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 620, "y": 250, "w": 100, "h": 68}, "gpt_distance_m": 1200, "angle_deg": 90, "speed_kph": 185},
11
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 850, "y": 235, "w": 90, "h": 62}, "gpt_distance_m": 1300, "angle_deg": 90, "speed_kph": 190}
12
+ ],
13
+ "48": [
14
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 150, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 700, "angle_deg": -90, "speed_kph": 190},
15
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 400, "y": 255, "w": 160, "h": 105}, "gpt_distance_m": 850, "angle_deg": 90, "speed_kph": 185},
16
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 640, "y": 255, "w": 115, "h": 78}, "gpt_distance_m": 1050, "angle_deg": 90, "speed_kph": 188},
17
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 870, "y": 245, "w": 105, "h": 72}, "gpt_distance_m": 1150, "angle_deg": 90, "speed_kph": 192}
18
+ ],
19
+ "96": [
20
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 80, "y": 300, "w": 220, "h": 145}, "gpt_distance_m": 600, "angle_deg": -90, "speed_kph": 200},
21
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 380, "y": 265, "w": 155, "h": 102}, "gpt_distance_m": 820, "angle_deg": 90, "speed_kph": 182},
22
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 660, "y": 262, "w": 135, "h": 92}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 190},
23
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 890, "y": 258, "w": 125, "h": 85}, "gpt_distance_m": 980, "angle_deg": 90, "speed_kph": 195}
24
+ ],
25
+ "144": [
26
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 50, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 650, "angle_deg": -90, "speed_kph": 195},
27
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 360, "y": 270, "w": 148, "h": 98}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 178},
28
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 680, "y": 272, "w": 158, "h": 108}, "gpt_distance_m": 750, "angle_deg": 90, "speed_kph": 192},
29
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 910, "y": 270, "w": 148, "h": 100}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 198}
30
+ ],
31
+ "191": [
32
+ {"id": "H01", "label": "helicopter", "bbox": {"x": 30, "y": 260, "w": 190, "h": 125}, "gpt_distance_m": 680, "angle_deg": -90, "speed_kph": 190},
33
+ {"id": "H02", "label": "helicopter", "bbox": {"x": 340, "y": 275, "w": 142, "h": 94}, "gpt_distance_m": 780, "angle_deg": 90, "speed_kph": 175},
34
+ {"id": "H03", "label": "helicopter", "bbox": {"x": 700, "y": 280, "w": 180, "h": 122}, "gpt_distance_m": 620, "angle_deg": 90, "speed_kph": 195},
35
+ {"id": "H04", "label": "helicopter", "bbox": {"x": 930, "y": 282, "w": 172, "h": 116}, "gpt_distance_m": 650, "angle_deg": 90, "speed_kph": 200}
36
+ ]
37
+ }
38
+ }
frontend/index.html CHANGED
@@ -533,7 +533,6 @@
533
  <script src="./js/ui/intel.js"></script>
534
  <script src="./js/ui/cursor.js"></script>
535
  <script src="./js/ui/trade.js"></script>
536
- <script src="./data/demo_data.js"></script>
537
  <script src="./data/helicopter_demo_data.js"></script>
538
  <script src="./js/core/demo.js"></script>
539
  <script src="./js/main.js"></script>
 
533
  <script src="./js/ui/intel.js"></script>
534
  <script src="./js/ui/cursor.js"></script>
535
  <script src="./js/ui/trade.js"></script>
 
536
  <script src="./data/helicopter_demo_data.js"></script>
537
  <script src="./js/core/demo.js"></script>
538
  <script src="./js/main.js"></script>
frontend/js/core/demo.js ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ APP.core.demo = {};
2
+
3
+ APP.core.demo.data = null;
4
+ APP.core.demo.active = false;
5
+
6
+ APP.core.demo.load = async function () {
7
+ // Demo data is now loaded per-video via loadForVideo()
8
+ // This function is kept for compatibility but does nothing
9
+ };
10
+
11
+ APP.core.demo.getFrameData = function (currentTime) {
12
+ if (!APP.core.demo.data) return null;
13
+
14
+ // Use interpolation for keyframe format
15
+ if (APP.core.demo.data.format === "keyframes") {
16
+ return APP.core.demo.getFrameDataInterpolated(currentTime);
17
+ }
18
+
19
+ // Original logic for frame-by-frame data
20
+ const fps = APP.core.demo.data.fps || 30;
21
+ const frameIdx = Math.floor(currentTime * fps);
22
+
23
+ // Get tracks for this frame
24
+ // Handle both string and number keys if needed
25
+ const tracks = APP.core.demo.data.frames[frameIdx] || APP.core.demo.data.frames[frameIdx.toString()];
26
+
27
+ if (!tracks) return [];
28
+ return tracks;
29
+ };
30
+
31
+ APP.core.demo.enable = function (force = true) {
32
+ const { log } = APP.ui.logging;
33
+ APP.core.demo.active = force;
34
+ if (force) {
35
+ log("DEMO MODE ACTIVATED", "g");
36
+ const chipFeed = document.getElementById("chipFeed");
37
+ if (chipFeed) chipFeed.textContent = "MODE:DEMO";
38
+ }
39
+ };
40
+
41
+ // Keyframe interpolation for smooth radar movement
42
+ APP.core.demo._keyframeIndices = null;
43
+
44
+ APP.core.demo.getKeyframeIndices = function() {
45
+ if (!APP.core.demo.data || APP.core.demo.data.format !== "keyframes") return null;
46
+ if (APP.core.demo._keyframeIndices) return APP.core.demo._keyframeIndices;
47
+
48
+ const indices = Object.keys(APP.core.demo.data.keyframes)
49
+ .map(k => parseInt(k, 10))
50
+ .sort((a, b) => a - b);
51
+
52
+ APP.core.demo._keyframeIndices = indices;
53
+ return indices;
54
+ };
55
+
56
+ APP.core.demo.interpolateTrack = function(trackA, trackB, t) {
57
+ const { lerp } = APP.core.utils;
58
+
59
+ return {
60
+ id: trackA.id,
61
+ label: trackA.label,
62
+ bbox: {
63
+ x: lerp(trackA.bbox.x, trackB.bbox.x, t),
64
+ y: lerp(trackA.bbox.y, trackB.bbox.y, t),
65
+ w: lerp(trackA.bbox.w, trackB.bbox.w, t),
66
+ h: lerp(trackA.bbox.h, trackB.bbox.h, t)
67
+ },
68
+ gpt_distance_m: lerp(trackA.gpt_distance_m, trackB.gpt_distance_m, t),
69
+ angle_deg: trackA.angle_deg,
70
+ speed_kph: lerp(trackA.speed_kph, trackB.speed_kph, t),
71
+ depth_valid: true,
72
+ depth_est_m: lerp(trackA.gpt_distance_m, trackB.gpt_distance_m, t),
73
+ history: [],
74
+ predicted_path: []
75
+ };
76
+ };
77
+
78
+ APP.core.demo.getFrameDataInterpolated = function(currentTime) {
79
+ const data = APP.core.demo.data;
80
+ if (!data || data.format !== "keyframes") return null;
81
+
82
+ const fps = data.fps || 24;
83
+ const frameIdx = Math.floor(currentTime * fps);
84
+ const keyframes = APP.core.demo.getKeyframeIndices();
85
+
86
+ if (!keyframes || keyframes.length === 0) return [];
87
+
88
+ // Find surrounding keyframes
89
+ let beforeIdx = keyframes[0];
90
+ let afterIdx = keyframes[keyframes.length - 1];
91
+
92
+ for (let i = 0; i < keyframes.length; i++) {
93
+ if (keyframes[i] <= frameIdx) beforeIdx = keyframes[i];
94
+ if (keyframes[i] >= frameIdx) { afterIdx = keyframes[i]; break; }
95
+ }
96
+
97
+ // Edge cases
98
+ if (frameIdx <= keyframes[0]) return data.keyframes[keyframes[0]] || [];
99
+ if (frameIdx >= keyframes[keyframes.length - 1]) return data.keyframes[keyframes[keyframes.length - 1]] || [];
100
+
101
+ // Interpolation factor
102
+ const t = (beforeIdx === afterIdx) ? 0 : (frameIdx - beforeIdx) / (afterIdx - beforeIdx);
103
+
104
+ const tracksBefore = data.keyframes[beforeIdx] || [];
105
+ const tracksAfter = data.keyframes[afterIdx] || [];
106
+
107
+ // Match by ID and interpolate
108
+ const result = [];
109
+ for (const trackA of tracksBefore) {
110
+ const trackB = tracksAfter.find(tr => tr.id === trackA.id);
111
+ if (trackB) {
112
+ result.push(APP.core.demo.interpolateTrack(trackA, trackB, t));
113
+ }
114
+ }
115
+
116
+ return result;
117
+ };
118
+
119
+ // Video-specific loading for demo tracks
120
+ APP.core.demo.loadForVideo = async function(videoName) {
121
+ const { log } = APP.ui.logging;
122
+ if (videoName.toLowerCase().includes("enhance_video_movement")) {
123
+ // Use global variable injected by helicopter_demo_data.js script tag (CORS-safe)
124
+ if (window.HELICOPTER_DEMO_DATA) {
125
+ APP.core.demo.data = window.HELICOPTER_DEMO_DATA;
126
+ APP.core.demo._keyframeIndices = null; // Reset cache
127
+ log("Helicopter demo tracks loaded (CORS-safe mode).", "g");
128
+ return;
129
+ }
130
+ // Fallback to fetch (works when served from HTTP server)
131
+ try {
132
+ const resp = await fetch("data/helicopter_demo_tracks.json");
133
+ if (resp.ok) {
134
+ APP.core.demo.data = await resp.json();
135
+ APP.core.demo._keyframeIndices = null; // Reset cache
136
+ log("Helicopter demo tracks loaded.", "g");
137
+ }
138
+ } catch (err) {
139
+ console.warn("Failed to load helicopter demo tracks:", err);
140
+ }
141
+ }
142
+ };
frontend/js/ui/radar.js CHANGED
@@ -635,6 +635,12 @@ APP.ui.radar.render = function (canvasId, trackSource, options = {}) {
635
  APP.ui.radar.renderFrameRadar = function () {
636
  const { state } = APP.core;
637
 
 
 
 
 
 
 
638
  // In demo mode, use demo data for first frame (time=0) to match video radar initial state
639
  let trackSource = state.detections;
640
  if (APP.core.demo.active && APP.core.demo.data) {
@@ -650,6 +656,13 @@ APP.ui.radar.renderFrameRadar = function () {
650
 
651
  APP.ui.radar.renderLiveRadar = function () {
652
  const { state } = APP.core;
 
 
 
 
 
 
 
653
  // Live radar has sweep animation
654
  APP.ui.radar.render("radarCanvas", state.tracker.tracks, { static: false });
655
  };
 
635
  APP.ui.radar.renderFrameRadar = function () {
636
  const { state } = APP.core;
637
 
638
+ // Only show tracks after Reason has been clicked
639
+ if (!state.hasReasoned) {
640
+ APP.ui.radar.render("frameRadar", [], { static: true });
641
+ return;
642
+ }
643
+
644
  // In demo mode, use demo data for first frame (time=0) to match video radar initial state
645
  let trackSource = state.detections;
646
  if (APP.core.demo.active && APP.core.demo.data) {
 
656
 
657
  APP.ui.radar.renderLiveRadar = function () {
658
  const { state } = APP.core;
659
+
660
+ // Only show tracks after Engage has been clicked (tracker running)
661
+ if (!state.tracker.running) {
662
+ APP.ui.radar.render("radarCanvas", [], { static: false });
663
+ return;
664
+ }
665
+
666
  // Live radar has sweep animation
667
  APP.ui.radar.render("radarCanvas", state.tracker.tracks, { static: false });
668
  };