Redfire-1234 commited on
Commit
052979d
·
verified ·
1 Parent(s): f06644b

Upload 4 files

Browse files
Files changed (4) hide show
  1. frontend/app.js +137 -0
  2. frontend/index.html +83 -0
  3. frontend/styles.css +235 -0
  4. frontend/visualizer.js +244 -0
frontend/app.js ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let selectedFile = null;
2
+ let currentTrajectories = null;
3
+
4
+ // DOM elements
5
+ const uploadBox = document.getElementById('uploadBox');
6
+ const videoInput = document.getElementById('videoInput');
7
+ const uploadBtn = document.getElementById('uploadBtn');
8
+ const loadingSection = document.getElementById('loadingSection');
9
+ const resultsSection = document.getElementById('resultsSection');
10
+ const playBtn = document.getElementById('playBtn');
11
+ const resetBtn = document.getElementById('resetBtn');
12
+ const showTrails = document.getElementById('showTrails');
13
+
14
+ // Upload box click
15
+ uploadBox.addEventListener('click', () => {
16
+ videoInput.click();
17
+ });
18
+
19
+ // File selection
20
+ videoInput.addEventListener('change', (e) => {
21
+ const file = e.target.files[0];
22
+ if (file) {
23
+ selectedFile = file;
24
+ uploadBox.querySelector('h3').textContent = `Selected: ${file.name}`;
25
+ uploadBox.querySelector('p').textContent = `Size: ${(file.size / 1024 / 1024).toFixed(2)} MB`;
26
+ uploadBtn.style.display = 'block';
27
+ }
28
+ });
29
+
30
+ // Drag and drop
31
+ uploadBox.addEventListener('dragover', (e) => {
32
+ e.preventDefault();
33
+ uploadBox.classList.add('dragover');
34
+ });
35
+
36
+ uploadBox.addEventListener('dragleave', () => {
37
+ uploadBox.classList.remove('dragover');
38
+ });
39
+
40
+ uploadBox.addEventListener('drop', (e) => {
41
+ e.preventDefault();
42
+ uploadBox.classList.remove('dragover');
43
+
44
+ const file = e.dataTransfer.files[0];
45
+ if (file && file.type.startsWith('video/')) {
46
+ selectedFile = file;
47
+ uploadBox.querySelector('h3').textContent = `Selected: ${file.name}`;
48
+ uploadBox.querySelector('p').textContent = `Size: ${(file.size / 1024 / 1024).toFixed(2)} MB`;
49
+ uploadBtn.style.display = 'block';
50
+ } else {
51
+ alert('Please drop a valid video file');
52
+ }
53
+ });
54
+
55
+ // Upload and process
56
+ uploadBtn.addEventListener('click', async () => {
57
+ if (!selectedFile) return;
58
+
59
+ const formData = new FormData();
60
+ formData.append('file', selectedFile);
61
+
62
+ // Show loading
63
+ document.querySelector('.upload-section').style.display = 'none';
64
+ loadingSection.style.display = 'block';
65
+
66
+ try {
67
+ const response = await fetch('/api/upload', {
68
+ method: 'POST',
69
+ body: formData
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error('Upload failed');
74
+ }
75
+
76
+ const data = await response.json();
77
+ currentTrajectories = data.trajectories;
78
+
79
+ // Display results
80
+ displayResults(data.trajectories);
81
+
82
+ loadingSection.style.display = 'none';
83
+ resultsSection.style.display = 'block';
84
+
85
+ } catch (error) {
86
+ console.error('Error:', error);
87
+ alert('Error processing video: ' + error.message);
88
+ loadingSection.style.display = 'none';
89
+ document.querySelector('.upload-section').style.display = 'block';
90
+ }
91
+ });
92
+
93
+ // Display results
94
+ function displayResults(data) {
95
+ const metadata = data.metadata;
96
+ const trajectories = data.trajectories;
97
+
98
+ // Update stats
99
+ document.getElementById('statObjects').textContent = metadata.num_objects;
100
+ document.getElementById('statFrames').textContent = metadata.frame_count;
101
+ document.getElementById('statFPS').textContent = metadata.fps.toFixed(1);
102
+ document.getElementById('statDuration').textContent =
103
+ (metadata.frame_count / metadata.fps).toFixed(1) + 's';
104
+
105
+ // Initialize 3D visualization
106
+ if (window.visualizer) {
107
+ window.visualizer.destroy();
108
+ }
109
+ window.visualizer = new TrajectoryVisualizer('canvas3d');
110
+ window.visualizer.loadTrajectories(trajectories);
111
+ }
112
+
113
+ // Control buttons
114
+ playBtn.addEventListener('click', () => {
115
+ if (window.visualizer) {
116
+ if (window.visualizer.isPlaying) {
117
+ window.visualizer.pause();
118
+ playBtn.textContent = '▶ Play';
119
+ } else {
120
+ window.visualizer.play();
121
+ playBtn.textContent = '⏸ Pause';
122
+ }
123
+ }
124
+ });
125
+
126
+ resetBtn.addEventListener('click', () => {
127
+ if (window.visualizer) {
128
+ window.visualizer.reset();
129
+ playBtn.textContent = '▶ Play';
130
+ }
131
+ });
132
+
133
+ showTrails.addEventListener('change', (e) => {
134
+ if (window.visualizer) {
135
+ window.visualizer.setShowTrails(e.target.checked);
136
+ }
137
+ });
frontend/index.html ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>3D Trajectory Tracker</title>
7
+ <link rel="stylesheet" href="/static/styles.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <header>
12
+ <h1>🎯 3D Trajectory Tracker</h1>
13
+ <p>Upload a video to track and visualize object movements in 3D</p>
14
+ </header>
15
+
16
+ <div class="upload-section">
17
+ <div class="upload-box" id="uploadBox">
18
+ <input type="file" id="videoInput" accept="video/*" hidden>
19
+ <div class="upload-content">
20
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
21
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
22
+ <polyline points="17 8 12 3 7 8"></polyline>
23
+ <line x1="12" y1="3" x2="12" y2="15"></line>
24
+ </svg>
25
+ <h3>Drop video here or click to upload</h3>
26
+ <p>Supports MP4, AVI, MOV, MKV</p>
27
+ </div>
28
+ </div>
29
+ <button id="uploadBtn" class="btn primary" style="display: none;">Process Video</button>
30
+ </div>
31
+
32
+ <div id="loadingSection" class="loading-section" style="display: none;">
33
+ <div class="spinner"></div>
34
+ <p>Processing video... This may take a few moments.</p>
35
+ </div>
36
+
37
+ <div id="resultsSection" class="results-section" style="display: none;">
38
+ <div class="stats-panel">
39
+ <h3>📊 Tracking Statistics</h3>
40
+ <div class="stats-grid">
41
+ <div class="stat-item">
42
+ <span class="stat-label">Objects Tracked:</span>
43
+ <span class="stat-value" id="statObjects">0</span>
44
+ </div>
45
+ <div class="stat-item">
46
+ <span class="stat-label">Total Frames:</span>
47
+ <span class="stat-value" id="statFrames">0</span>
48
+ </div>
49
+ <div class="stat-item">
50
+ <span class="stat-label">FPS:</span>
51
+ <span class="stat-value" id="statFPS">0</span>
52
+ </div>
53
+ <div class="stat-item">
54
+ <span class="stat-label">Duration:</span>
55
+ <span class="stat-value" id="statDuration">0s</span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="visualization-panel">
61
+ <h3>🎬 3D Trajectory Visualization</h3>
62
+ <div class="controls">
63
+ <button id="playBtn" class="btn">▶ Play</button>
64
+ <button id="resetBtn" class="btn">↻ Reset</button>
65
+ <label>
66
+ <input type="checkbox" id="showTrails" checked>
67
+ Show Trails
68
+ </label>
69
+ </div>
70
+ <div id="canvas3d"></div>
71
+ </div>
72
+ </div>
73
+
74
+ <footer>
75
+ <p>Built with FastAPI, Three.js, and OpenCV</p>
76
+ </footer>
77
+ </div>
78
+
79
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
80
+ <script src="/static/visualizer.js"></script>
81
+ <script src="/static/app.js"></script>
82
+ </body>
83
+ </html>
frontend/styles.css ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ padding: 20px;
12
+ color: #333;
13
+ }
14
+
15
+ .container {
16
+ max-width: 1200px;
17
+ margin: 0 auto;
18
+ }
19
+
20
+ header {
21
+ text-align: center;
22
+ color: white;
23
+ margin-bottom: 40px;
24
+ }
25
+
26
+ header h1 {
27
+ font-size: 2.5rem;
28
+ margin-bottom: 10px;
29
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
30
+ }
31
+
32
+ header p {
33
+ font-size: 1.1rem;
34
+ opacity: 0.9;
35
+ }
36
+
37
+ .upload-section {
38
+ background: white;
39
+ border-radius: 16px;
40
+ padding: 30px;
41
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
42
+ margin-bottom: 30px;
43
+ }
44
+
45
+ .upload-box {
46
+ border: 3px dashed #667eea;
47
+ border-radius: 12px;
48
+ padding: 60px 20px;
49
+ text-align: center;
50
+ cursor: pointer;
51
+ transition: all 0.3s ease;
52
+ background: #f8f9ff;
53
+ }
54
+
55
+ .upload-box:hover {
56
+ background: #f0f2ff;
57
+ border-color: #764ba2;
58
+ }
59
+
60
+ .upload-box.dragover {
61
+ background: #e8ebff;
62
+ border-color: #764ba2;
63
+ transform: scale(1.02);
64
+ }
65
+
66
+ .upload-content svg {
67
+ color: #667eea;
68
+ margin-bottom: 20px;
69
+ }
70
+
71
+ .upload-content h3 {
72
+ font-size: 1.3rem;
73
+ margin-bottom: 10px;
74
+ color: #333;
75
+ }
76
+
77
+ .upload-content p {
78
+ color: #666;
79
+ font-size: 0.9rem;
80
+ }
81
+
82
+ .btn {
83
+ padding: 12px 30px;
84
+ border: none;
85
+ border-radius: 8px;
86
+ font-size: 1rem;
87
+ cursor: pointer;
88
+ transition: all 0.3s ease;
89
+ font-weight: 600;
90
+ margin: 10px 5px;
91
+ }
92
+
93
+ .btn.primary {
94
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
95
+ color: white;
96
+ width: 100%;
97
+ margin-top: 20px;
98
+ }
99
+
100
+ .btn.primary:hover {
101
+ transform: translateY(-2px);
102
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
103
+ }
104
+
105
+ .btn:hover {
106
+ transform: translateY(-2px);
107
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
108
+ }
109
+
110
+ .loading-section {
111
+ background: white;
112
+ border-radius: 16px;
113
+ padding: 60px;
114
+ text-align: center;
115
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
116
+ }
117
+
118
+ .spinner {
119
+ width: 50px;
120
+ height: 50px;
121
+ border: 5px solid #f3f3f3;
122
+ border-top: 5px solid #667eea;
123
+ border-radius: 50%;
124
+ animation: spin 1s linear infinite;
125
+ margin: 0 auto 20px;
126
+ }
127
+
128
+ @keyframes spin {
129
+ 0% { transform: rotate(0deg); }
130
+ 100% { transform: rotate(360deg); }
131
+ }
132
+
133
+ .results-section {
134
+ background: white;
135
+ border-radius: 16px;
136
+ padding: 30px;
137
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
138
+ }
139
+
140
+ .stats-panel {
141
+ margin-bottom: 30px;
142
+ padding: 20px;
143
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
144
+ border-radius: 12px;
145
+ }
146
+
147
+ .stats-panel h3 {
148
+ margin-bottom: 20px;
149
+ color: #333;
150
+ }
151
+
152
+ .stats-grid {
153
+ display: grid;
154
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
155
+ gap: 15px;
156
+ }
157
+
158
+ .stat-item {
159
+ background: white;
160
+ padding: 15px;
161
+ border-radius: 8px;
162
+ display: flex;
163
+ justify-content: space-between;
164
+ align-items: center;
165
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
166
+ }
167
+
168
+ .stat-label {
169
+ font-weight: 600;
170
+ color: #666;
171
+ }
172
+
173
+ .stat-value {
174
+ font-size: 1.5rem;
175
+ font-weight: bold;
176
+ color: #667eea;
177
+ }
178
+
179
+ .visualization-panel h3 {
180
+ margin-bottom: 15px;
181
+ color: #333;
182
+ }
183
+
184
+ .controls {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 10px;
188
+ margin-bottom: 20px;
189
+ padding: 15px;
190
+ background: #f8f9fa;
191
+ border-radius: 8px;
192
+ }
193
+
194
+ .controls label {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 8px;
198
+ cursor: pointer;
199
+ margin-left: auto;
200
+ }
201
+
202
+ .controls input[type="checkbox"] {
203
+ width: 18px;
204
+ height: 18px;
205
+ cursor: pointer;
206
+ }
207
+
208
+ #canvas3d {
209
+ width: 100%;
210
+ height: 600px;
211
+ border-radius: 12px;
212
+ background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
213
+ overflow: hidden;
214
+ }
215
+
216
+ footer {
217
+ text-align: center;
218
+ color: white;
219
+ margin-top: 40px;
220
+ opacity: 0.8;
221
+ }
222
+
223
+ @media (max-width: 768px) {
224
+ header h1 {
225
+ font-size: 2rem;
226
+ }
227
+
228
+ .stats-grid {
229
+ grid-template-columns: 1fr;
230
+ }
231
+
232
+ #canvas3d {
233
+ height: 400px;
234
+ }
235
+ }
frontend/visualizer.js ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class TrajectoryVisualizer {
2
+ constructor(containerId) {
3
+ this.container = document.getElementById(containerId);
4
+ this.trajectories = [];
5
+ this.objects = [];
6
+ this.trails = [];
7
+ this.isPlaying = false;
8
+ this.currentFrame = 0;
9
+ this.maxFrames = 0;
10
+ this.showTrails = true;
11
+
12
+ this.init();
13
+ }
14
+
15
+ init() {
16
+ // Scene
17
+ this.scene = new THREE.Scene();
18
+ this.scene.fog = new THREE.Fog(0x1a1a2e, 5, 15);
19
+
20
+ // Camera
21
+ this.camera = new THREE.PerspectiveCamera(
22
+ 75,
23
+ this.container.clientWidth / this.container.clientHeight,
24
+ 0.1,
25
+ 1000
26
+ );
27
+ this.camera.position.set(3, 3, 3);
28
+ this.camera.lookAt(0, 0, 0);
29
+
30
+ // Renderer
31
+ this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
32
+ this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
33
+ this.renderer.setClearColor(0x1a1a2e);
34
+ this.container.appendChild(this.renderer.domElement);
35
+
36
+ // Lighting
37
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
38
+ this.scene.add(ambientLight);
39
+
40
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
41
+ directionalLight.position.set(5, 5, 5);
42
+ this.scene.add(directionalLight);
43
+
44
+ const pointLight = new THREE.PointLight(0x667eea, 1, 100);
45
+ pointLight.position.set(0, 3, 0);
46
+ this.scene.add(pointLight);
47
+
48
+ // Grid helper
49
+ const gridHelper = new THREE.GridHelper(4, 20, 0x444444, 0x222222);
50
+ gridHelper.position.y = -1;
51
+ this.scene.add(gridHelper);
52
+
53
+ // Axes helper
54
+ const axesHelper = new THREE.AxesHelper(2);
55
+ this.scene.add(axesHelper);
56
+
57
+ // Handle window resize
58
+ window.addEventListener('resize', () => this.onWindowResize());
59
+
60
+ // Mouse controls
61
+ this.setupControls();
62
+
63
+ // Start animation loop
64
+ this.animate();
65
+ }
66
+
67
+ setupControls() {
68
+ let isDragging = false;
69
+ let previousMousePosition = { x: 0, y: 0 };
70
+
71
+ this.container.addEventListener('mousedown', (e) => {
72
+ isDragging = true;
73
+ });
74
+
75
+ this.container.addEventListener('mousemove', (e) => {
76
+ if (isDragging) {
77
+ const deltaX = e.offsetX - previousMousePosition.x;
78
+ const deltaY = e.offsetY - previousMousePosition.y;
79
+
80
+ const rotationSpeed = 0.005;
81
+ this.camera.position.applyAxisAngle(
82
+ new THREE.Vector3(0, 1, 0),
83
+ deltaX * rotationSpeed
84
+ );
85
+
86
+ const lookAt = new THREE.Vector3(0, 0, 0);
87
+ this.camera.lookAt(lookAt);
88
+ }
89
+
90
+ previousMousePosition = { x: e.offsetX, y: e.offsetY };
91
+ });
92
+
93
+ this.container.addEventListener('mouseup', () => {
94
+ isDragging = false;
95
+ });
96
+
97
+ // Zoom with mouse wheel
98
+ this.container.addEventListener('wheel', (e) => {
99
+ e.preventDefault();
100
+ const zoomSpeed = 0.1;
101
+ const direction = new THREE.Vector3();
102
+ this.camera.getWorldDirection(direction);
103
+
104
+ if (e.deltaY < 0) {
105
+ this.camera.position.addScaledVector(direction, zoomSpeed);
106
+ } else {
107
+ this.camera.position.addScaledVector(direction, -zoomSpeed);
108
+ }
109
+ });
110
+ }
111
+
112
+ loadTrajectories(trajectories) {
113
+ this.trajectories = trajectories;
114
+ this.currentFrame = 0;
115
+ this.isPlaying = false;
116
+
117
+ // Find max frames
118
+ this.maxFrames = 0;
119
+ trajectories.forEach(traj => {
120
+ const maxFrame = Math.max(...traj.points.map(p => p.frame));
121
+ if (maxFrame > this.maxFrames) this.maxFrames = maxFrame;
122
+ });
123
+
124
+ // Clear existing objects
125
+ this.objects.forEach(obj => this.scene.remove(obj));
126
+ this.trails.forEach(line => this.scene.remove(line));
127
+ this.objects = [];
128
+ this.trails = [];
129
+
130
+ // Create objects for each trajectory
131
+ const colors = [0xff6b6b, 0x4ecdc4, 0xffe66d, 0x95e1d3, 0xf38181, 0xaa96da, 0xfcbad3, 0xa8e6cf];
132
+
133
+ trajectories.forEach((traj, index) => {
134
+ const color = colors[index % colors.length];
135
+ const points = traj.points;
136
+
137
+ // Create trail line
138
+ const positions = new Float32Array(points.length * 3);
139
+ points.forEach((point, i) => {
140
+ positions[i * 3] = point.x;
141
+ positions[i * 3 + 1] = point.y;
142
+ positions[i * 3 + 2] = point.z;
143
+ });
144
+
145
+ const geometry = new THREE.BufferGeometry();
146
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
147
+
148
+ const material = new THREE.LineBasicMaterial({
149
+ color: color,
150
+ linewidth: 2
151
+ });
152
+
153
+ const line = new THREE.Line(geometry, material);
154
+ line.geometry.setDrawRange(0, 0);
155
+ this.scene.add(line);
156
+ this.trails.push(line);
157
+
158
+ // Create sphere for current position
159
+ const sphereGeometry = new THREE.SphereGeometry(0.05, 16, 16);
160
+ const sphereMaterial = new THREE.MeshPhongMaterial({
161
+ color: color,
162
+ emissive: color,
163
+ emissiveIntensity: 0.5
164
+ });
165
+ const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
166
+ sphere.visible = false;
167
+ this.scene.add(sphere);
168
+ this.objects.push(sphere);
169
+ });
170
+ }
171
+
172
+ play() {
173
+ this.isPlaying = true;
174
+ }
175
+
176
+ pause() {
177
+ this.isPlaying = false;
178
+ }
179
+
180
+ reset() {
181
+ this.currentFrame = 0;
182
+ this.isPlaying = false;
183
+ this.objects.forEach(obj => obj.visible = false);
184
+ this.trails.forEach(trail => {
185
+ trail.geometry.setDrawRange(0, 0);
186
+ });
187
+ }
188
+
189
+ setShowTrails(show) {
190
+ this.showTrails = show;
191
+ this.trails.forEach(trail => trail.visible = show);
192
+ }
193
+
194
+ update() {
195
+ if (this.isPlaying && this.trajectories.length > 0) {
196
+ this.currentFrame++;
197
+
198
+ // Update each trajectory
199
+ this.trajectories.forEach((traj, idx) => {
200
+ const points = traj.points;
201
+ const currentPoint = points.find(p => p.frame === this.currentFrame);
202
+
203
+ if (currentPoint) {
204
+ const obj = this.objects[idx];
205
+ obj.position.set(currentPoint.x, currentPoint.y, currentPoint.z);
206
+ obj.visible = true;
207
+
208
+ // Update trail
209
+ if (this.showTrails) {
210
+ const trail = this.trails[idx];
211
+ const visiblePoints = points.filter(p => p.frame <= this.currentFrame);
212
+ trail.geometry.setDrawRange(0, visiblePoints.length);
213
+ }
214
+ }
215
+ });
216
+
217
+ // Loop animation
218
+ if (this.currentFrame >= this.maxFrames) {
219
+ this.currentFrame = 0;
220
+ }
221
+ }
222
+ }
223
+
224
+ animate() {
225
+ requestAnimationFrame(() => this.animate());
226
+
227
+ this.update();
228
+ this.renderer.render(this.scene, this.camera);
229
+ }
230
+
231
+ onWindowResize() {
232
+ this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
233
+ this.camera.updateProjectionMatrix();
234
+ this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
235
+ }
236
+
237
+ destroy() {
238
+ window.removeEventListener('resize', () => this.onWindowResize());
239
+ this.renderer.dispose();
240
+ while(this.container.firstChild) {
241
+ this.container.removeChild(this.container.firstChild);
242
+ }
243
+ }
244
+ }