CoolFace
Apppublic

aaurelions/3dquadrocopter

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
index.html816 linesDownload Raw Back to root
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>