aaurelions/3dquadrocopter
0
1<!DOCTYPE html>2<html lang="en">3 4<head>5 <meta charset="UTF-8">6 <meta name="viewport" content="width=device-width, initial-scale=1.0">7 <title>Quadcopter Interceptor Simulation - 3D</title>8 <!-- THREE.JS CDN -->9 <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>10 <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>11 <script src="https://cdn.tailwindcss.com"></script>12 <style>13 /* --- Base & Theming --- */14 :root {15 --bg-start: #f4f6f9;16 --bg-end: #e2e8f0;17 --card-bg: rgba(255, 255, 255, 0.7);18 --text-primary: #1f2937;19 --text-secondary: #4b5563;20 --border-color: rgba(0, 0, 0, 0.1);21 --shadow: 0 10px 30px rgba(0, 0, 0, 0.07);22 --primary-gradient: linear-gradient(90deg, #3b82f6, #8b5cf6);23 --primary-hover-gradient: linear-gradient(90deg, #2563eb, #7c3aed);24 --detail-bg: rgba(0, 0, 0, 0.04);25 --propeller-color: #64748b;26 }27 28 html.dark {29 --bg-start: #111827;30 --bg-end: #1f2937;31 --card-bg: rgba(31, 41, 55, 0.7);32 --text-primary: #f9fafb;33 --text-secondary: #d1d5db;34 --border-color: rgba(255, 255, 255, 0.1);35 --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);36 --detail-bg: rgba(255, 255, 255, 0.06);37 --propeller-color: #94a3b8;38 }39 40 body {41 background: linear-gradient(-45deg, var(--bg-start), var(--bg-end));42 transition: background 0.5s;43 font-family: 'Inter', sans-serif;44 overflow: hidden;45 /* Prevent body scroll */46 }47 48 /* Helpers */49 .text-text-primary {50 color: var(--text-primary);51 }52 53 .text-text-secondary {54 color: var(--text-secondary);55 }56 57 .border-border-color {58 border-color: var(--border-color);59 }60 61 .bg-card-bg {62 background-color: var(--card-bg);63 }64 65 .bg-detail-bg {66 background-color: var(--detail-bg);67 }68 69 .backdrop-blur-xl {70 backdrop-filter: blur(16px);71 -webkit-backdrop-filter: blur(16px);72 }73 74 /* Custom canvas styling */75 #simulation-canvas {76 display: block;77 width: 100%;78 height: 100%;79 cursor: grab;80 }81 82 #simulation-canvas:active {83 cursor: grabbing;84 }85 86 /* Custom scrollbar for results & gallery */87 .custom-scrollbar::-webkit-scrollbar {88 height: 6px;89 width: 6px;90 }91 92 .custom-scrollbar::-webkit-scrollbar-track {93 background: transparent;94 }95 96 .custom-scrollbar::-webkit-scrollbar-thumb {97 background: rgba(0, 0, 0, 0.2);98 border-radius: 3px;99 }100 101 html.dark .custom-scrollbar::-webkit-scrollbar-thumb {102 background: rgba(255, 255, 255, 0.2);103 }104 </style>105 <link rel="preconnect" href="https://fonts.googleapis.com">106 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>107 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">108 <script>109 tailwind.config = {110 darkMode: 'class',111 theme: {112 extend: {113 fontFamily: {114 sans: ['Inter', 'sans-serif'],115 },116 }117 }118 }119 </script>120</head>121 122<body class="min-h-screen flex items-center justify-center p-2 sm:p-4 antialiased">123 124 <!-- Model Loading Overlay -->125 <div id="model-loading-overlay"126 class="fixed inset-0 bg-black/70 flex flex-col items-center justify-center z-50 transition-opacity duration-300">127 <div class="flex flex-col items-center gap-4">128 <div class="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-400"></div>129 <span class="text-white text-2xl font-bold tracking-wide">Loading AI Model...</span>130 <span class="text-blue-200 text-sm">This may take a few seconds on first load.</span>131 </div>132 </div>133 134 <!-- Main Application Card -->135 <div136 class="w-full max-w-7xl mx-auto bg-card-bg border border-border-color rounded-2xl sm:rounded-3xl shadow-2xl backdrop-blur-xl p-4 md:p-6 transition-all duration-500">137 <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">138 139 <!-- Left Panel: 3D Quadcopter Simulation -->140 <div class="flex flex-col gap-4">141 <header class="flex justify-between items-center">142 <h1 class="text-lg sm:text-xl md:text-2xl font-bold text-text-primary">3D Quadcopter Simulation</h1>143 <button id="theme-switcher"144 class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition"145 aria-label="Switch theme" title="Switch theme">146 <svg id="sun-icon" class="w-6 h-6 text-text-secondary" fill="none" stroke="currentColor"147 viewBox="0 0 24 24">148 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"149 d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z">150 </path>151 </svg>152 <svg id="moon-icon" class="w-6 h-6 text-text-secondary hidden" fill="none" stroke="currentColor"153 viewBox="0 0 24 24">154 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"155 d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z">156 </path>157 </svg>158 </button>159 </header>160 161 <div id="simulation-container"162 class="relative w-full aspect-square bg-detail-bg rounded-2xl overflow-hidden">163 <!-- Three.js canvas will be inserted here -->164 </div>165 166 <div>167 <h2 class="text-base sm:text-lg font-semibold text-text-primary mb-2">Flight Telemetry</h2>168 <div class="grid grid-cols-2 gap-2 text-left">169 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">170 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">171 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"172 d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1V10a1 1 0 00-1-1H7a1 1 0 00-1 1v10a1 1 0 001 1h2z">173 </path>174 </svg>175 <div>176 <p class="text-xs text-text-secondary font-medium">TARGET</p>177 <p id="telemetry-target" class="text-sm sm:text-base font-bold text-text-primary">-</p>178 </div>179 </div>180 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">181 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">182 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"183 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>184 </svg>185 <div>186 <p class="text-xs text-text-secondary font-medium">STATUS</p>187 <p id="telemetry-status" class="text-sm sm:text-base font-bold text-text-primary">IDLE188 </p>189 </div>190 </div>191 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">192 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">193 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"194 d="M4 8h16M4 16h16"></path>195 </svg>196 <div>197 <p class="text-xs text-text-secondary font-medium">X-VECTOR (ROLL)</p>198 <p id="telemetry-x" class="text-sm sm:text-base font-bold text-text-primary">0.00</p>199 </div>200 </div>201 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">202 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">203 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"204 d="M8 4v16M16 4v16"></path>205 </svg>206 <div>207 <p class="text-xs text-text-secondary font-medium">Y-VECTOR (PITCH)</p>208 <p id="telemetry-y" class="text-sm sm:text-base font-bold text-text-primary">0.00</p>209 </div>210 </div>211 </div>212 </div>213 </div>214 215 <!-- Right Panel: AI Detector -->216 <div id="detector-card"217 class="bg-card-bg/50 dark:bg-card-bg/50 border border-border-color rounded-2xl p-4 md:p-6 flex flex-col">218 <header class="flex justify-between items-center mb-4">219 <h1 class="text-lg sm:text-xl md:text-2xl font-bold text-text-primary">AI Vision System</h1>220 </header>221 222 <div id="content-area" class="flex-grow flex flex-col min-h-0">223 <div id="uploader-view" class="flex-grow flex flex-col">224 <div id="drop-zone"225 class="flex-grow flex flex-col items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-2xl p-4 text-center cursor-pointer hover:border-blue-500 dark:hover:border-blue-400 hover:scale-[1.01] transition-all duration-300"226 tabindex="0">227 <svg class="w-12 h-12 sm:w-16 sm:h-16 opacity-50 text-text-secondary" fill="none"228 stroke="currentColor" viewBox="0 0 24 24">229 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"230 d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">231 </path>232 </svg>233 <p class="mt-3 font-semibold text-text-primary">Drag & Drop Sky Image</p>234 <p class="text-xs sm:text-sm text-text-secondary mt-1">or</p>235 <label for="image-upload"236 class="mt-3 px-5 py-2 rounded-lg font-semibold text-white cursor-pointer transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 text-sm"237 style="background: var(--primary-gradient);"238 onmouseover="this.style.background='var(--primary-hover-gradient)'"239 onmouseout="this.style.background='var(--primary-gradient)'">240 Select File241 </label>242 <input type="file" id="image-upload" class="hidden" accept="image/*">243 </div>244 <div class="mt-4">245 <h3 class="text-sm font-semibold text-text-secondary text-center mb-2">Or Select a Preset246 </h3>247 <div id="preset-gallery" class="custom-scrollbar flex gap-2 overflow-x-auto pb-2">248 </div>249 </div>250 </div>251 252 <div id="results-view" class="hidden flex-grow flex flex-col min-h-0">253 <div class="relative mb-2 group">254 <canvas id="output-canvas" class="w-full h-auto rounded-xl shadow-md"></canvas>255 <button id="reset-button"256 class="absolute top-2 right-2 w-9 h-9 bg-black/40 hover:bg-black/60 backdrop-blur-sm rounded-full flex items-center justify-center transition opacity-0 group-hover:opacity-100"257 title="Analyze another image">258 <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">259 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"260 d="M19 12H5M12 19l-7-7 7-7"></path>261 </svg>262 </button>263 </div>264 <div id="results-summary" class="flex-grow overflow-y-auto space-y-2 pr-2 custom-scrollbar">265 </div>266 </div>267 </div>268 <footer id="status-footer" class="mt-4 text-center text-sm text-text-secondary h-5"></footer>269 </div>270 </div>271 </div>272 273 <!-- Toast Notification -->274 <div id="toast"275 class="fixed bottom-6 right-6 bg-red-600 text-white px-4 py-2 rounded-lg shadow-lg opacity-0 transform translate-y-4 pointer-events-none transition-all duration-300 z-50">276 </div>277 278 279 <script>280 // --- DOM Elements ---281 const uploaderView = document.getElementById('uploader-view');282 const resultsView = document.getElementById('results-view');283 const dropZone = document.getElementById('drop-zone');284 const imageUpload = document.getElementById('image-upload');285 const outputCanvas = document.getElementById('output-canvas');286 const resultsSummary = document.getElementById('results-summary');287 const statusFooter = document.getElementById('status-footer');288 const resetButton = document.getElementById('reset-button');289 const themeSwitcher = document.getElementById('theme-switcher');290 const sunIcon = document.getElementById('sun-icon');291 const moonIcon = document.getElementById('moon-icon');292 const modelLoadingOverlay = document.getElementById('model-loading-overlay');293 const toast = document.getElementById('toast');294 const presetGallery = document.getElementById('preset-gallery');295 const simulationContainer = document.getElementById('simulation-container');296 297 const telemetry = {298 target: document.getElementById('telemetry-target'),299 status: document.getElementById('telemetry-status'),300 x: document.getElementById('telemetry-x'),301 y: document.getElementById('telemetry-y'),302 };303 304 let session;305 306 // --- Constants ---307 const MODEL_URL = "https://huggingface.co/aaurelions/yolo11n.onnx/resolve/main/yolo11n.onnx";308 const COCO_CLASSES = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'];309 const TARGET_CLASS = 'airplane';310 const COLORS = ["#3b82f6", "#ef4444", "#22c55e", "#f97316", "#8b5cf6", "#fde047", "#ec4899", "#14b8a6", "#64748b"];311 312 // --- 3D Simulation Globals ---313 let scene, camera, renderer, quadcopter, targetAim, clock;314 let targetRotation = { x: 0, y: 0 };315 let motorSpeeds3D = { fl: 0, fr: 0, rl: 0, rr: 0 };316 317 // --- UI & Initialization Logic ---318 function showToast(message, duration = 3000) {319 toast.textContent = message;320 toast.classList.remove('opacity-0', 'translate-y-4');321 setTimeout(() => {322 toast.classList.add('opacity-0', 'translate-y-4');323 }, duration);324 }325 326 function showModelLoading(show) {327 modelLoadingOverlay.style.display = show ? 'flex' : 'none';328 modelLoadingOverlay.classList.toggle('opacity-0', !show);329 }330 331 function setDarkMode(isDark) {332 document.documentElement.classList.toggle('dark', isDark);333 sunIcon.classList.toggle('hidden', isDark);334 moonIcon.classList.toggle('hidden', !isDark);335 localStorage.setItem('theme', isDark ? 'dark' : 'light');336 // Update 3D materials if scene exists337 if (scene) {338 const newGridColor = isDark ? 0x444444 : 0xcccccc;339 scene.getObjectByName("grid").material.color.setHex(newGridColor);340 }341 }342 343 themeSwitcher.addEventListener('click', () => setDarkMode(!document.documentElement.classList.contains('dark')));344 345 resetButton.addEventListener('click', () => {346 resultsView.classList.add('hidden');347 uploaderView.classList.remove('hidden');348 statusFooter.textContent = 'Model is ready. Select an image.';349 imageUpload.value = '';350 resetSimulation();351 });352 353 // --- Drag & Drop Logic ---354 ['dragenter', 'dragover'].forEach(eventName => {355 dropZone.addEventListener(eventName, (e) => {356 e.preventDefault();357 dropZone.classList.add('ring-4', 'ring-blue-400', 'scale-105', 'border-solid');358 }, false);359 });360 ['dragleave', 'drop'].forEach(eventName => {361 dropZone.addEventListener(eventName, (e) => {362 e.preventDefault();363 dropZone.classList.remove('ring-4', 'ring-blue-400', 'scale-105', 'border-solid');364 }, false);365 });366 dropZone.addEventListener('drop', (e) => {367 const file = e.dataTransfer.files[0];368 if (file) handleFile(file);369 });370 imageUpload.addEventListener('change', (e) => { if (e.target.files[0]) handleFile(e.target.files[0]); });371 372 // --- 3D Scene Initialization ---373 function init3DScene() {374 scene = new THREE.Scene();375 clock = new THREE.Clock();376 377 // Camera378 camera = new THREE.PerspectiveCamera(75, simulationContainer.clientWidth / simulationContainer.clientHeight, 0.1, 1000);379 camera.position.set(0, 1.5, 4);380 camera.lookAt(0, 0, 0);381 382 // Renderer383 renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });384 renderer.setSize(simulationContainer.clientWidth, simulationContainer.clientHeight);385 renderer.setPixelRatio(window.devicePixelRatio);386 simulationContainer.appendChild(renderer.domElement);387 388 // Lighting389 const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);390 scene.add(ambientLight);391 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);392 directionalLight.position.set(5, 10, 7.5);393 scene.add(directionalLight);394 395 // Ground Grid396 const gridColor = document.documentElement.classList.contains('dark') ? 0x444444 : 0xcccccc;397 const grid = new THREE.GridHelper(20, 20, gridColor, gridColor);398 grid.name = "grid";399 grid.position.y = -2;400 scene.add(grid);401 402 // Build Quadcopter from Primitives403 quadcopter = new THREE.Group();404 const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.6 });405 const armMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.7 });406 const motorMaterial = new THREE.MeshStandardMaterial({ color: 0x777777, roughness: 0.5 });407 const propMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, transparent: true, opacity: 0.8, side: THREE.DoubleSide });408 409 // Body410 const body = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.4, 0.3, 16), bodyMaterial);411 quadcopter.add(body);412 413 // Arms414 const armGeometry = new THREE.BoxGeometry(2, 0.08, 0.08);415 const arm1 = new THREE.Mesh(armGeometry, armMaterial);416 arm1.rotation.y = Math.PI / 4;417 quadcopter.add(arm1);418 const arm2 = new THREE.Mesh(armGeometry, armMaterial);419 arm2.rotation.y = -Math.PI / 4;420 quadcopter.add(arm2);421 422 // Motors & Propellers423 const motorPositions = [424 { x: 0.707, z: 0.707, name: 'fr' }, { x: -0.707, z: 0.707, name: 'fl' },425 { x: 0.707, z: -0.707, name: 'rr' }, { x: -0.707, z: -0.707, name: 'rl' }426 ];427 const propGeometry = new THREE.BoxGeometry(0.6, 0.02, 0.1);428 motorPositions.forEach(pos => {429 const motor = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.2, 8), motorMaterial);430 motor.position.set(pos.x * 1.1, 0.1, pos.z * 1.1);431 motor.rotation.x = Math.PI / 2;432 quadcopter.add(motor);433 434 const propellerGroup = new THREE.Group();435 const blade1 = new THREE.Mesh(propGeometry, propMaterial);436 const blade2 = new THREE.Mesh(propGeometry, propMaterial);437 blade2.rotation.y = Math.PI / 2;438 propellerGroup.add(blade1, blade2);439 propellerGroup.position.set(pos.x * 1.1, 0.2, pos.z * 1.1);440 propellerGroup.name = `prop_${pos.name}`;441 quadcopter.add(propellerGroup);442 });443 scene.add(quadcopter);444 445 // Target Aim446 const targetGeometry = new THREE.TorusGeometry(0.5, 0.05, 16, 100);447 const targetMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });448 targetAim = new THREE.Mesh(targetGeometry, targetMaterial);449 targetAim.visible = false;450 scene.add(targetAim);451 452 // Camera Controls (simple drag)453 let isDragging = false;454 let previousMousePosition = { x: 0, y: 0 };455 renderer.domElement.addEventListener('mousedown', () => isDragging = true);456 renderer.domElement.addEventListener('mouseup', () => isDragging = false);457 renderer.domElement.addEventListener('mouseleave', () => isDragging = false);458 renderer.domElement.addEventListener('mousemove', e => {459 if (!isDragging) return;460 const deltaMove = {461 x: e.offsetX - previousMousePosition.x,462 y: e.offsetY - previousMousePosition.y463 };464 const rotationSpeed = 0.005;465 quadcopter.rotation.y += deltaMove.x * rotationSpeed;466 quadcopter.rotation.x += deltaMove.y * rotationSpeed;467 previousMousePosition = { x: e.offsetX, y: e.offsetY };468 });469 renderer.domElement.addEventListener('wheel', e => {470 camera.position.z += e.deltaY * 0.01;471 camera.position.z = Math.max(2, Math.min(10, camera.position.z));472 });473 window.addEventListener('resize', onWindowResize, false);474 475 animate3D();476 }477 478 function onWindowResize() {479 if (!renderer || !camera) return;480 const width = simulationContainer.clientWidth;481 const height = simulationContainer.clientHeight;482 camera.aspect = width / height;483 camera.updateProjectionMatrix();484 renderer.setSize(width, height);485 }486 487 // --- 3D Animation Loop ---488 function animate3D() {489 requestAnimationFrame(animate3D);490 const delta = clock.getDelta();491 const lerpFactor = delta * 5; // Smoothing factor for rotation492 493 // Smoothly interpolate quadcopter rotation towards the target494 quadcopter.rotation.x = THREE.MathUtils.lerp(quadcopter.rotation.x, targetRotation.x, lerpFactor);495 quadcopter.rotation.z = THREE.MathUtils.lerp(quadcopter.rotation.z, targetRotation.y, lerpFactor); // Map roll to Z-axis496 497 // Animate propellers based on motor speeds498 const rotationSpeedMultiplier = 0.5;499 Object.keys(motorSpeeds3D).forEach(key => {500 const prop = scene.getObjectByName(`prop_${key}`);501 if (prop) {502 const speed = motorSpeeds3D[key];503 // Counter-rotate pairs for torque compensation visual504 const direction = (key === 'fr' || key === 'rl') ? 1 : -1;505 prop.rotation.y += (speed / 100) * rotationSpeedMultiplier * direction;506 }507 });508 509 renderer.render(scene, camera);510 }511 512 // --- Quadcopter Simulation Logic ---513 function resetSimulation() {514 telemetry.target.textContent = '-';515 telemetry.status.textContent = 'IDLE';516 telemetry.x.textContent = '0.00';517 telemetry.y.textContent = '0.00';518 telemetry.status.classList.remove('text-green-500', 'text-red-500', 'text-yellow-500');519 520 // Reset 3D state521 targetRotation.x = 0;522 targetRotation.y = 0;523 motorSpeeds3D = { fl: 0, fr: 0, rl: 0, rr: 0 };524 if (targetAim) targetAim.visible = false;525 }526 527 function calculateMotorSpeeds(target, imageWidth, imageHeight) {528 const [x1, y1, x2, y2] = target.box;529 const targetX = x1 + (x2 - x1) / 2;530 const targetY = y1 + (y2 - y1) / 2;531 const normX = (targetX / imageWidth - 0.5) * 2;532 const normY = (targetY / imageHeight - 0.5) * 2;533 534 const BASE_SPEED = 60;535 const MANEUVER_INTENSITY = 25;536 const pitch_adjustment = normY * MANEUVER_INTENSITY;537 const roll_adjustment = normX * MANEUVER_INTENSITY;538 539 // Update motor speeds for 3D animation540 motorSpeeds3D = {541 fl: BASE_SPEED - pitch_adjustment - roll_adjustment,542 fr: BASE_SPEED - pitch_adjustment + roll_adjustment,543 rl: BASE_SPEED + pitch_adjustment - roll_adjustment,544 rr: BASE_SPEED + pitch_adjustment + roll_adjustment,545 };546 547 // Update 3D target rotation (pitch = x-axis, roll = z-axis)548 const PITCH_FACTOR = Math.PI / 6; // Max 30 degrees pitch549 const ROLL_FACTOR = Math.PI / 6; // Max 30 degrees roll550 targetRotation.x = -normY * PITCH_FACTOR; // Invert Y for 3D coordinate system551 targetRotation.y = -normX * ROLL_FACTOR;552 553 // Position the 3D target aim helper554 const TARGET_DISTANCE = 10;555 const viewPlaneHeight = 2 * TARGET_DISTANCE * Math.tan(camera.fov * 0.5 * (Math.PI / 180));556 const viewPlaneWidth = viewPlaneHeight * camera.aspect;557 targetAim.position.set(normX * (viewPlaneWidth / 2), -normY * (viewPlaneHeight / 2), -TARGET_DISTANCE);558 targetAim.lookAt(camera.position);559 targetAim.visible = true;560 561 // Update telemetry UI562 telemetry.target.textContent = 'AIRPLANE';563 telemetry.status.textContent = 'TRACKING';564 telemetry.status.classList.remove('text-red-500', 'text-yellow-500');565 telemetry.status.classList.add('text-green-500');566 telemetry.x.textContent = normX.toFixed(2);567 telemetry.y.textContent = normY.toFixed(2);568 }569 570 // --- Core AI & Rendering Logic ---571 async function handleFile(file) {572 if (!session) { showToast("AI Model is not loaded yet."); return; }573 if (!file || !file.type.startsWith('image/')) { showToast("Please select a valid image file."); return; }574 const image = new Image();575 image.src = URL.createObjectURL(file);576 image.onload = () => {577 runAnalysis(image);578 URL.revokeObjectURL(image.src);579 };580 image.onerror = () => {581 showToast("Could not load the selected image file.");582 statusFooter.textContent = "Error: Invalid image.";583 }584 }585 586 async function handleUrl(url) {587 if (!session) { showToast("AI Model is not loaded yet."); return; }588 const image = new Image();589 image.crossOrigin = "anonymous";590 image.src = url;591 image.onload = () => runAnalysis(image);592 image.onerror = () => {593 showToast(`Could not load image from URL: ${url}`);594 statusFooter.textContent = "Error: Invalid image URL.";595 };596 }597 598 async function runAnalysis(image) {599 resetSimulation();600 statusFooter.innerHTML = '<div class="flex items-center justify-center gap-2"><div class="animate-spin rounded-full h-4 w-4 border-b-2 border-text-primary"></div><span>Analyzing image...</span></div>';601 uploaderView.classList.add('hidden');602 resultsView.classList.remove('hidden');603 try {604 const inputTensor = preprocess(image);605 const results = await session.run({ 'images': inputTensor });606 const boxes = postprocess(results.output0, image.width, image.height);607 const finalBoxes = nonMaxSuppression(boxes, 0.5, 0.45);608 const airplanes = finalBoxes.filter(b => COCO_CLASSES[b.classId] === TARGET_CLASS);609 let primaryTarget = null;610 if (airplanes.length > 0) {611 primaryTarget = airplanes.sort((a, b) => b.confidence - a.confidence)[0];612 calculateMotorSpeeds(primaryTarget, image.naturalWidth, image.naturalHeight);613 } else {614 telemetry.target.textContent = 'NONE';615 telemetry.status.textContent = 'SCANNING';616 telemetry.status.classList.add('text-yellow-500');617 }618 drawDetections(image, finalBoxes, primaryTarget);619 displaySummary(finalBoxes);620 statusFooter.textContent = `Found ${finalBoxes.length} object(s). ${airplanes.length > 0 ? 'Target acquired.' : 'No target found.'}`;621 } catch (e) {622 console.error(e);623 showToast(`An error occurred: ${e.message}`);624 statusFooter.textContent = "Error during analysis.";625 resetButton.click();626 }627 }628 629 function preprocess(img) {630 const canvas = document.createElement('canvas');631 const modelWidth = 640, modelHeight = 640;632 canvas.width = modelWidth;633 canvas.height = modelHeight;634 const ctx = canvas.getContext('2d');635 ctx.drawImage(img, 0, 0, modelWidth, modelHeight);636 const imageData = ctx.getImageData(0, 0, modelWidth, modelHeight);637 const float32Data = new Float32Array(1 * 3 * modelHeight * modelWidth);638 for (let i = 0, j = 0; i < imageData.data.length; i += 4, j++) {639 float32Data[j] = imageData.data[i] / 255.0;640 float32Data[modelWidth * modelHeight + j] = imageData.data[i + 1] / 255.0;641 float32Data[2 * modelWidth * modelHeight + j] = imageData.data[i + 2] / 255.0;642 }643 return new ort.Tensor('float32', float32Data, [1, 3, modelHeight, modelWidth]);644 }645 646 function postprocess(tensor, originalWidth, originalHeight) {647 const data = tensor.data;648 const boxes = [];649 const modelWidth = 640, modelHeight = 640;650 const xRatio = originalWidth / modelWidth;651 const yRatio = originalHeight / modelHeight;652 for (let i = 0; i < tensor.dims[2]; i++) {653 const classScores = [];654 for (let j = 0; j < COCO_CLASSES.length; j++) {655 classScores.push(data[(4 + j) * tensor.dims[2] + i]);656 }657 const maxScore = Math.max(...classScores);658 if (maxScore > 0.25) {659 const classId = classScores.indexOf(maxScore);660 const [cx, cy, w, h] = [data[i], data[tensor.dims[2] + i], data[2 * tensor.dims[2] + i], data[3 * tensor.dims[2] + i]];661 const x1 = (cx - w / 2) * xRatio;662 const y1 = (cy - h / 2) * yRatio;663 const x2 = (cx + w / 2) * xRatio;664 const y2 = (cy + h / 2) * yRatio;665 boxes.push({ classId, confidence: maxScore, box: [x1, y1, x2, y2] });666 }667 }668 return boxes;669 }670 671 function nonMaxSuppression(boxes, iouThreshold, scoreThreshold) {672 boxes.sort((a, b) => b.confidence - a.confidence);673 const finalBoxes = [];674 while (boxes.length > 0) {675 const current = boxes.shift();676 if (current.confidence < scoreThreshold) continue;677 finalBoxes.push(current);678 boxes = boxes.filter(box => {679 if (box.classId !== current.classId) return true;680 const iou = calculateIoU(current.box, box.box);681 return iou <= iouThreshold;682 });683 }684 return finalBoxes;685 }686 687 function calculateIoU(box1, box2) {688 const [x1, y1, x2, y2] = box1;689 const [x1_b, y1_b, x2_b, y2_b] = box2;690 const interX1 = Math.max(x1, x1_b);691 const interY1 = Math.max(y1, y1_b);692 const interX2 = Math.min(x2, x2_b);693 const interY2 = Math.min(y2, y2_b);694 const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);695 const box1Area = (x2 - x1) * (y2 - y1);696 const box2Area = (x2_b - x1_b) * (y2_b - y1_b);697 const unionArea = box1Area + box2Area - interArea;698 return unionArea > 0 ? interArea / unionArea : 0;699 }700 701 function drawDetections(img, boxes, primaryTarget) {702 const ctx = outputCanvas.getContext('2d');703 outputCanvas.width = img.naturalWidth;704 outputCanvas.height = img.naturalHeight;705 ctx.drawImage(img, 0, 0, outputCanvas.width, outputCanvas.height);706 boxes.forEach(det => {707 const isTarget = (det === primaryTarget);708 const [x1, y1, x2, y2] = det.box;709 const w = x2 - x1;710 const h = y2 - y1;711 const color = isTarget ? '#ef4444' : COLORS[det.classId % COLORS.length];712 ctx.strokeStyle = color;713 ctx.lineWidth = isTarget ? Math.max(4, outputCanvas.width * 0.005) : Math.max(2, outputCanvas.width * 0.003);714 ctx.strokeRect(x1, y1, w, h);715 ctx.fillStyle = color;716 const label = `${COCO_CLASSES[det.classId]} (${(det.confidence * 100).toFixed(1)}%)`;717 const fontSize = Math.max(14, outputCanvas.width * 0.015);718 ctx.font = `bold ${fontSize}px sans-serif`;719 const textWidth = ctx.measureText(label).width;720 ctx.fillRect(x1 - (ctx.lineWidth / 2), y1 - (fontSize + 8), textWidth + 10, fontSize + 8);721 ctx.fillStyle = '#ffffff';722 ctx.fillText(label, x1 + 5, y1 - 5);723 });724 if (primaryTarget) {725 const [x1, y1, x2, y2] = primaryTarget.box;726 const targetX = x1 + (x2 - x1) / 2;727 const targetY = y1 + (y2 - y1) / 2;728 const centerX = outputCanvas.width / 2;729 const centerY = outputCanvas.height / 2;730 ctx.beginPath();731 ctx.moveTo(centerX, centerY);732 ctx.lineTo(targetX, targetY);733 ctx.strokeStyle = 'rgba(239, 68, 68, 0.8)';734 ctx.lineWidth = 3;735 ctx.setLineDash([10, 5]);736 ctx.stroke();737 ctx.setLineDash([]);738 ctx.beginPath();739 ctx.arc(centerX, centerY, 10, 0, 2 * Math.PI);740 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';741 ctx.fill();742 ctx.strokeStyle = 'rgba(239, 68, 68, 0.9)';743 ctx.stroke();744 }745 }746 747 function displaySummary(boxes) {748 resultsSummary.innerHTML = '';749 if (boxes.length === 0) {750 resultsSummary.innerHTML = '<p class="text-text-secondary text-center">No objects detected.</p>';751 return;752 }753 boxes.forEach(box => {754 const wrapper = document.createElement('div');755 wrapper.className = 'bg-gray-500/10 rounded-lg p-2 flex items-center gap-3';756 const color = COLORS[box.classId % COLORS.length];757 const className = COCO_CLASSES[box.classId];758 wrapper.innerHTML = `759 <div class="w-1 h-8 rounded" style="background-color: ${color}"></div>760 <div class="flex-grow">761 <div class="flex justify-between items-center">762 <span class="font-semibold text-sm text-text-primary">${className}</span>763 <span class="font-mono text-xs text-text-primary bg-white/50 dark:bg-black/20 px-1.5 py-0.5 rounded">${(box.confidence * 100).toFixed(1)}%</span>764 </div>765 <p class="text-xs text-text-secondary font-mono">766 Pos: ${Math.round(box.box[0])},${Math.round(box.box[1])}767 </p>768 </div>`;769 resultsSummary.appendChild(wrapper);770 });771 }772 773 function populatePresetGallery() {774 for (let i = 1; i <= 10; i++) {775 const thumbSrc = `https://huggingface.co/spaces/aaurelions/quadrocopter/resolve/main/images/thumbnails/${i}.jpg`;776 const fullSrc = `https://huggingface.co/spaces/aaurelions/quadrocopter/resolve/main/images/${i}.jpg`;777 778 const img = document.createElement('img');779 img.src = thumbSrc;780 img.className = "w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-transparent hover:border-blue-500 transition-all duration-200 flex-shrink-0";781 img.loading = "lazy";782 img.alt = `Preset Image ${i}`;783 img.onclick = () => handleUrl(fullSrc);784 img.onerror = () => {785 img.src = `https://placehold.co/80x80/e2e8f0/4b5563?text=Img+${i}`;786 img.onclick = null;787 };788 presetGallery.appendChild(img);789 }790 }791 792 // --- App Initialization ---793 async function initialize() {794 setDarkMode(localStorage.getItem('theme') === 'dark' || (window.matchMedia('(prefers-color-scheme: dark)').matches && !localStorage.getItem('theme')));795 init3DScene();796 populatePresetGallery();797 resetSimulation();798 statusFooter.textContent = 'Initializing AI engine...';799 showModelLoading(true);800 try {801 session = await ort.InferenceSession.create(MODEL_URL, { executionProviders: ['wasm'] });802 statusFooter.textContent = 'Model is ready. Select an image.';803 } catch (e) {804 console.error(e);805 statusFooter.textContent = `Error loading model: ${e.message}`;806 showToast(`Failed to load the AI model. Please refresh. ${e.message}`);807 } finally {808 showModelLoading(false);809 }810 }811 812 document.addEventListener('DOMContentLoaded', initialize);813 </script>814</body>815 816</html>