Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AutoTech 3D - Détails & Assemblage</title> | |
| <!-- Importation de Three.js et OrbitControls via CDN --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <!-- Importation d'une police moderne et d'icônes --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #3b82f6; | |
| --accent: #06b6d4; | |
| --dark-bg: #0f172a; | |
| --panel-bg: rgba(30, 41, 59, 0.85); | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --border: rgba(255, 255, 255, 0.1); | |
| --glass: blur(12px); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--dark-bg); | |
| color: var(--text-main); | |
| overflow: hidden; /* Empêche le scroll global, on gère par panneaux */ | |
| height: 100vh; | |
| width: 100vw; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* HEADER */ | |
| header { | |
| height: 60px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0 2rem; | |
| background: rgba(15, 23, 42, 0.9); | |
| border-bottom: 1px solid var(--border); | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-weight: 800; | |
| font-size: 1.2rem; | |
| background: linear-gradient(to right, var(--primary), var(--accent)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -0.5px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .anycoder-link { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--primary); | |
| } | |
| /* MAIN LAYOUT */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 1fr 400px; | |
| height: calc(100vh - 60px); | |
| position: relative; | |
| } | |
| /* 3D VIEWER SECTION */ | |
| #scene-container { | |
| position: relative; | |
| background: radial-gradient(circle at center, #1e293b 0%, #0f172a 100%); | |
| overflow: hidden; | |
| cursor: grab; | |
| } | |
| #scene-container:active { | |
| cursor: grabbing; | |
| } | |
| .scene-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 1rem; | |
| background: var(--panel-bg); | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 50px; | |
| backdrop-filter: var(--glass); | |
| border: 1px solid var(--border); | |
| z-index: 10; | |
| } | |
| .btn-control { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-main); | |
| font-size: 1.2rem; | |
| cursor: pointer; | |
| transition: transform 0.2s, color 0.2s; | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .btn-control:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: var(--accent); | |
| transform: scale(1.1); | |
| } | |
| .btn-control.active { | |
| color: var(--primary); | |
| background: rgba(59, 130, 246, 0.2); | |
| } | |
| /* INFO / SIDEBAR SECTION */ | |
| aside { | |
| background: var(--panel-bg); | |
| backdrop-filter: var(--glass); | |
| border-left: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow-y: auto; | |
| padding: 1.5rem; | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-bottom: 1.5rem; | |
| background: rgba(0,0,0,0.2); | |
| padding: 0.25rem; | |
| border-radius: 8px; | |
| } | |
| .tab-btn { | |
| flex: 1; | |
| padding: 0.6rem; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| cursor: pointer; | |
| border-radius: 6px; | |
| transition: all 0.3s; | |
| } | |
| .tab-btn.active { | |
| background: var(--primary); | |
| color: white; | |
| box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); | |
| } | |
| .content-panel { | |
| display: none; | |
| animation: fadeIn 0.4s ease; | |
| } | |
| .content-panel.active { | |
| display: block; | |
| } | |
| h2 { | |
| font-size: 1.5rem; | |
| margin-bottom: 1rem; | |
| color: white; | |
| border-bottom: 2px solid var(--primary); | |
| padding-bottom: 0.5rem; | |
| display: inline-block; | |
| } | |
| .part-list { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .part-item { | |
| background: rgba(255,255,255,0.05); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| border: 1px solid transparent; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .part-item:hover, .part-item.selected { | |
| background: rgba(59, 130, 246, 0.15); | |
| border-color: var(--primary); | |
| } | |
| .part-icon { | |
| color: var(--accent); | |
| width: 24px; | |
| text-align: center; | |
| } | |
| .part-details-view { | |
| margin-top: 1.5rem; | |
| background: rgba(0,0,0,0.2); | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| border: 1px solid var(--border); | |
| } | |
| .detail-label { | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-muted); | |
| margin-bottom: 0.25rem; | |
| } | |
| .detail-value { | |
| font-size: 1.1rem; | |
| margin-bottom: 1rem; | |
| color: var(--text-main); | |
| } | |
| /* TUTORIAL STYLES */ | |
| .step-container { | |
| position: relative; | |
| padding-left: 2rem; | |
| border-left: 2px solid var(--border); | |
| } | |
| .step { | |
| margin-bottom: 2rem; | |
| position: relative; | |
| } | |
| .step::before { | |
| content: ''; | |
| position: absolute; | |
| left: -2.4rem; | |
| top: 0; | |
| width: 1rem; | |
| height: 1rem; | |
| background: var(--dark-bg); | |
| border: 2px solid var(--text-muted); | |
| border-radius: 50%; | |
| transition: all 0.3s; | |
| } | |
| .step.active::before { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| box-shadow: 0 0 10px var(--accent); | |
| } | |
| .step-title { | |
| font-weight: 700; | |
| font-size: 1.1rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .step-desc { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| line-height: 1.5; | |
| } | |
| .tutorial-nav { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 2rem; | |
| } | |
| .btn { | |
| padding: 0.6rem 1.2rem; | |
| border-radius: 6px; | |
| border: none; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| } | |
| .btn-primary { | |
| background: var(--primary); | |
| color: white; | |
| } | |
| .btn-primary:hover { background: #2563eb; } | |
| .btn-secondary { | |
| background: rgba(255,255,255,0.1); | |
| color: white; | |
| } | |
| .btn-secondary:hover { background: rgba(255,255,255,0.2); } | |
| /* RESPONSIVE */ | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: 50vh 1fr; | |
| overflow-y: auto; | |
| } | |
| aside { | |
| border-left: none; | |
| border-top: 1px solid var(--border); | |
| } | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Loading Overlay */ | |
| #loader { | |
| position: fixed; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: var(--dark-bg); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 999; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 4px solid rgba(255,255,255,0.1); | |
| border-left-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { 100% { transform: rotate(360deg); } } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- LOADER --> | |
| <div id="loader"> | |
| <div class="spinner"></div> | |
| <p style="color: var(--text-muted); font-size: 0.9rem;">Chargement du modèle 3D...</p> | |
| </div> | |
| <!-- HEADER --> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-cube"></i> AutoTech 3D | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| <i class="fa-solid fa-code"></i> Built with anycoder | |
| </a> | |
| </header> | |
| <!-- MAIN CONTENT --> | |
| <main> | |
| <!-- 3D SCENE --> | |
| <section id="scene-container"> | |
| <div class="scene-controls"> | |
| <button class="btn-control active" id="btn-rotate" title="Rotation Auto"><i class="fa-solid fa-sync"></i></button> | |
| <button class="btn-control" id="btn-explode" title="Vue Éclatée"><i class="fa-solid fa-arrows-alt-v"></i></button> | |
| <button class="btn-control" id="btn-wireframe" title="Mode Fil de Fer"><i class="fa-solid fa-border-all"></i></button> | |
| </div> | |
| </section> | |
| <!-- SIDEBAR INFO --> | |
| <aside> | |
| <div class="tabs"> | |
| <button class="tab-btn active" data-tab="parts">Pièces</button> | |
| <button class="tab-btn" data-tab="specs">Specs</button> | |
| <button class="tab-btn" data-tab="tutorial">Tutoriel</button> | |
| </div> | |
| <!-- TAB: PARTS --> | |
| <div id="parts" class="content-panel active"> | |
| <h2>Composants</h2> | |
| <p style="color: var(--text-muted); margin-bottom: 1rem; font-size: 0.9rem;"> | |
| Sélectionnez une pièce pour voir les détails techniques et sa localisation. | |
| </p> | |
| <ul class="part-list" id="part-list-container"> | |
| <!-- Javascript will populate this --> | |
| </ul> | |
| <div id="part-detail-box" class="part-details-view" style="display:none;"> | |
| <h3 id="detail-title" style="color: var(--accent); margin-bottom: 0.5rem;">Nom Pièce</h3> | |
| <div class="detail-label">Description</div> | |
| <p id="detail-desc" class="detail-value" style="font-size: 0.95rem; line-height: 1.4;">Description...</p> | |
| <div class="detail-label">Matériau</div> | |
| <p id="detail-material" class="detail-value">Acier / Alliage</p> | |
| <div class="detail-label">Fonction</div> | |
| <p id="detail-function" class="detail-value">Fonction principale</p> | |
| </div> | |
| </div> | |
| <!-- TAB: TUTORIAL --> | |
| <div id="tutorial" class="content-panel"> | |
| <h2>Guide d'Assemblage</h2> | |
| <p style="color: var(--text-muted); margin-bottom: 1.5rem; font-size: 0.9rem;"> | |
| Suivez ces étapes pour comprendre l'architecture du véhicule. | |
| </p> | |
| <div id="steps-container" class="step-container"> | |
| <!-- Steps generated by JS --> | |
| </div> | |
| <div class="tutorial-nav"> | |
| <button class="btn btn-secondary" id="prev-step">Précédent</button> | |
| <span id="step-counter" style="display: flex; align-items: center; color: var(--text-muted);">1 / 5</span> | |
| <button class="btn btn-primary" id="next-step">Suivant</button> | |
| </div> | |
| </div> | |
| <!-- TAB: SPECS --> | |
| <div id="specs" class="content-panel"> | |
| <h2>Spécifications Générales</h2> | |
| <div style="display: grid; gap: 1rem;"> | |
| <div style="background: rgba(255,255,255,0.05); padding: 1rem; border-radius: 8px;"> | |
| <div class="detail-label">Type de Véhicule</div> | |
| <div class="detail-value">Berline Sportive Compacte</div> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.05); padding: 1rem; border-radius: 8px;"> | |
| <div class="detail-label">Moteur</div> | |
| <div class="detail-value">V6 Bi-Turbo 3.0L</div> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.05); padding: 1rem; border-radius: 8px;"> | |
| <div class="detail-label">Puissance</div> | |
| <div class="detail-value">450 Ch</div> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.05); padding: 1rem; border-radius: 8px;"> | |
| <div class="detail-label">Poids</div> | |
| <div class="detail-value">1,450 kg</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </main> | |
| <!-- JAVASCRIPT LOGIC --> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| // --- DATA: Car Parts & Tutorial --- | |
| const carData = { | |
| chassis: { | |
| name: "Châssis & Coque", | |
| icon: "fa-car-side", | |
| desc: "La structure principale qui assure la rigidité et la sécurité des passagers. Fabriqué en acier haute résistance et aluminium.", | |
| material: "Acier/Aluminium", | |
| function: "Supporter tous les composants et protéger les occupants." | |
| }, | |
| engine: { | |
| name: "Moteur", | |
| icon: "fa-cogs", | |
| desc: "Le cœur du véhicule. Il convertit l'énergie chimique du carburant en énergie mécanique pour entraîner les roues.", | |
| material: "Alliage d'aluminium, Fonte", | |
| function: "Générer la puissance motrice." | |
| }, | |
| wheels: { | |
| name: "Roues & Pneus", | |
| icon: "fa-circle", | |
| desc: "Assurent le contact avec la route, la transmission de la traction et le freinage.", | |
| material: "Caucchouc, Jantes Alliage", | |
| function: "Mobilité et adhérence routière." | |
| }, | |
| interior: { | |
| name: "Habitacle", | |
| icon: "fa-chair", | |
| desc: "L'espace passager incluant le tableau de bord, les sièges et les systèmes de contrôle.", | |
| material: "Cuir, Polymères, tissus", | |
| function: "Confort et interface de conduite." | |
| } | |
| }; | |
| const tutorialSteps = [ | |
| { | |
| title: "1. Préparation du Châssis", | |
| desc: "Le montage commence toujours par le châssis. C'est la fondation sur laquelle tout le reste sera boulonné.", | |
| target: "chassis" | |
| }, | |
| { | |
| title: "2. Installation du Groupe Moto-Propulseur", | |
| desc: "Le moteur et la boîte de vitesses sont abaissés avec précision dans le compartiment moteur et fixés aux silent-blocs.", | |
| target: "engine" | |
| }, | |
| { | |
| title: "3. Système de Freinage & Roues", | |
| desc: "Les étriers, disques et roues sont installés. Étape cruciale pour la sécurité dynamique du véhicule.", | |
| target: "wheels" | |
| }, | |
| { | |
| title: "4. Câblage et Électronique", | |
| desc: "Le faisceau électrique est connecté (ECU, capteurs, lumières) avant la fermeture de la carrosserie.", | |
| target: "chassis" // Highlighting chassis as a proxy for wiring hidden inside | |
| }, | |
| { | |
| title: "5. Assemblage de l'Habitacle", | |
| desc: "Pose du tableau de bord, des sièges et des garnitures intérieures pour finaliser le confort.", | |
| target: "interior" | |
| } | |
| ]; | |
| // --- THREE.JS VARIABLES --- | |
| let scene, camera, renderer, controls; | |
| let carGroup = new THREE.Group(); | |
| let partsMeshes = {}; // Map to store mesh references by name | |
| let isAutoRotating = true; | |
| let isExploded = false; | |
| let originalPositions = {}; | |
| // --- INITIALIZATION --- | |
| function init() { | |
| const container = document.getElementById('scene-container'); | |
| // Scene setup | |
| scene = new THREE.Scene(); | |
| // scene.background = new THREE.Color(0x0f172a); // Handled by CSS gradient mostly | |
| // Camera setup | |
| camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 100); | |
| camera.position.set(5, 3, 6); | |
| // Renderer setup | |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.shadowMap.enabled = true; | |
| container.appendChild(renderer.domElement); | |
| // Controls | |
| controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.minDistance = 3; | |
| controls.maxDistance = 15; | |
| // Lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 1); | |
| dirLight.position.set(5, 10, 7); | |
| dirLight.castShadow = true; | |
| scene.add(dirLight); | |
| const blueLight = new THREE.PointLight(0x3b82f6, 2, 10); | |
| blueLight.position.set(-2, 1, -2); | |
| scene.add(blueLight); | |
| // Build the procedural car | |
| buildProceduralCar(); | |
| scene.add(carGroup); | |
| // Remove Loader | |
| document.getElementById('loader').style.display = 'none'; | |
| // Events | |
| window.addEventListener('resize', onWindowResize); | |
| animate(); | |
| // Init UI | |
| initUI(); | |
| } | |
| // --- PROCEDURAL CAR BUILDER --- | |
| function buildProceduralCar() { | |
| const materialBody = new THREE.MeshStandardMaterial({ | |
| color: 0x334155, | |
| roughness: 0.4, | |
| metalness: 0.8 | |
| }); | |
| const materialAccent = new THREE.MeshStandardMaterial({ | |
| color: 0x06b6d4, | |
| roughness: 0.2, | |
| metalness: 0.5, | |
| emissive: 0x06b6d4, | |
| emissiveIntensity: 0.2 | |
| }); | |
| const materialGlass = new THREE.MeshStandardMaterial({ | |
| color: 0x94a3b8, | |
| roughness: 0.1, | |
| metalness: 0.9, | |
| transparent: true, | |
| opacity: 0.7 | |
| }); | |
| const materialInterior = new THREE.MeshStandardMaterial({ | |
| color: 0x1e293b, | |
| roughness: 0.8 | |
| }); | |
| // 1. CHASSIS (Lower Body) | |
| const chassisGeo = new THREE.BoxGeometry(2, 0.5, 4.2); | |
| const chassisMesh = new THREE.Mesh(chassisGeo, materialBody); | |
| chassisMesh.position.y = 0.5; | |
| chassisMesh.castShadow = true; | |
| chassisMesh.name = "chassis"; | |
| carGroup.add(chassisMesh); | |
| partsMeshes['chassis'] = chassisMesh; | |
| // 2. INTERIOR (Cabin) | |
| const cabinGeo = new THREE.BoxGeometry(1.6, 0.6, 2.2); | |
| const cabinMesh = new THREE.Mesh(cabinGeo, materialInterior); | |
| cabinMesh.position.y = 1.0; | |
| cabinMesh.position.z = -0.2; | |
| cabinMesh.name = "interior"; | |
| carGroup.add(cabinMesh); | |
| partsMeshes['interior'] = cabinMesh; | |
| // 3. GLASS (Windshield) | |
| const glassGeo = new THREE.BoxGeometry(1.65, 0.55, 1.4); | |
| // Angle it slightly | |
| const glassMesh = new THREE.Mesh(glassGeo, materialGlass); | |
| glassMesh.position.set(0, 1.35, -0.5); | |
| glassMesh.rotation.x = -0.3; | |
| carGroup.add(glassMesh); | |
| // 4. ENGINE BLOCK (Visible slightly through rear or represented) | |
| const engineGeo = new THREE.BoxGeometry(1.2, 0.6, 1.0); | |
| const engineMesh = new THREE.Mesh(engineGeo, materialAccent); | |
| engineMesh.position.set(0, 0.8, 1.4); | |
| engineMesh.name = "engine"; | |
| carGroup.add(engineMesh); | |
| partsMeshes['engine'] = engineMesh; | |
| // 5. WHEELS (4 wheels) | |
| const wheelGeo = new THREE.CylinderGeometry(0.35, 0.35, 0.25, 32); | |
| const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 }); | |
| const rimGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.26, 16); | |
| const rimMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.9 }); | |
| const wheelPositions = [ | |
| { x: -1.1, z: 1.2 }, { x: 1.1, z: 1.2 }, | |
| { x: -1.1, z: -1.2 }, { x: 1.1, z: -1.2 } | |
| ]; | |
| // Create a group for wheels to handle them as one unit for highlighting | |
| const wheelsGroup = new THREE.Group(); | |
| wheelsGroup.name = "wheels"; | |
| wheelPositions.forEach(pos => { | |
| const wheel = new THREE.Mesh(wheelGeo, wheelMat); | |
| wheel.rotation.z = Math.PI / 2; | |
| wheel.position.set(pos.x, 0.35, pos.z); | |
| const rim = new THREE.Mesh(rimGeo, rimMat); | |
| rim.rotation.z = Math.PI / 2; | |
| rim.position.set(pos.x, 0.35, pos.z); | |
| wheelsGroup.add(wheel); | |
| wheelsGroup.add(rim); | |
| }); | |
| carGroup.add(wheelsGroup); | |
| partsMeshes['wheels'] = wheelsGroup; | |
| // Save original positions for explosion effect | |
| carGroup.traverse((child) => { | |
| if(child.isMesh || child.isGroup) { | |
| originalPositions[child.uuid] = child.position.clone(); | |
| } | |
| }); | |
| } | |
| // --- ANIMATION LOOP --- | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (isAutoRotating) { | |
| carGroup.rotation.y += 0.005; | |
| } | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| // --- WINDOW RESIZE --- | |
| function onWindowResize() { | |
| const container = document.getElementById('scene-container'); | |
| camera.aspect = container.clientWidth / container.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| } | |
| // --- UI LOGIC --- | |
| function initUI() { | |
| // 1. Populate Parts List | |
| const listContainer = document.getElementById('part-list-container'); | |
| Object.keys(carData).forEach(key => { | |
| const part = carData[key]; | |
| const li = document.createElement('li'); | |
| li.className = 'part-item'; | |
| li.innerHTML = ` | |
| <div class="part-icon"><i class="fa-solid ${part.icon}"></i></div> | |
| <span>${part.name}</span> | |
| `; | |
| li.onclick = () => selectPart(key, li); | |
| listContainer.appendChild(li); | |
| }); | |
| // 2. Populate Tutorial | |
| renderTutorialStep(); | |
| // 3. Tab Switching | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| // Remove active class from all tabs and contents | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); | |
| document.querySelectorAll('.content-panel').forEach(c => c.classList.remove('active')); | |
| // Add active to clicked | |
| e.target.classList.add('active'); | |
| document.getElementById(e.target.dataset.tab).classList.add('active'); | |
| }); | |
| }); | |
| // 4. Scene Controls | |
| document.getElementById('btn-rotate').onclick = function() { | |
| isAutoRotating = !isAutoRotating; | |
| this.classList.toggle('active'); | |
| }; | |
| document.getElementById('btn-explode').onclick = function() { | |
| isExploded = !isExploded; | |
| this.classList.toggle('active'); | |
| toggleExplosion(); | |
| }; | |
| document.getElementById('btn-wireframe').onclick = function() { | |
| this.classList.toggle('active'); | |
| carGroup.traverse(child => { | |
| if (child.isMesh) { | |
| child.material.wireframe = !child.material.wireframe; | |
| } | |
| }); | |
| }; | |
| // Tutorial Navigation | |
| document.getElementById('next-step').onclick = nextTutorialStep; | |
| document.getElementById('prev-step').onclick = prevTutorialStep; | |
| } | |
| // --- INTERACTION FUNCTIONS --- | |
| function highlightPart(partKey) { | |
| // Reset all colors | |
| carGroup.traverse(child => { | |
| if(child.isMesh && child.userData.originalHex) { | |
| child.material.emissive.setHex(child.userData.originalHex); | |
| } | |
| }); | |
| const target = partsMeshes[partKey]; | |
| if (target) { | |
| if (target.isGroup) { | |
| target.children.forEach(c => { | |
| if(c.isMesh) { | |
| c.userData.originalHex = c.material.emissive.getHex(); | |
| c.material.emissive.setHex(0x3b82f6); | |
| c.material.emissiveIntensity = 0.8; | |
| } | |
| }); | |
| } else if (target.isMesh) { | |
| target.userData.originalHex = target.material.emissive.getHex(); | |
| target.material.emissive.setHex(0x3b82f6); | |
| target.material.emissiveIntensity = 0.8; | |
| } | |
| } | |
| } | |
| function selectPart(key, domElement) { | |
| // UI Update | |
| document.querySelectorAll('.part-item').forEach(i => i.classList.remove('selected')); | |
| domElement.classList.add('selected'); | |
| // Show Details | |
| const box = document.getElementById('part-detail-box'); | |
| box.style.display = 'block'; | |
| const data = carData[key]; | |
| document.getElementById('detail-title').innerText = data.name; | |
| document.getElementById('detail-desc').innerText = data.desc; | |
| document.getElementById('detail-material').innerText = data.material; | |
| document.getElementById('detail-function').innerText = data.function; | |
| // 3D Highlight | |
| highlightPart(key); | |
| // Stop rotation to let user see | |
| isAutoRotating = false; | |
| document.getElementById('btn-rotate').classList.remove('active'); | |
| } | |
| function toggleExplosion() { | |
| const offset = isExploded ? 0.5 : 0; | |
| // Animate positions | |
| carGroup.traverse((child) => { | |
| if (child.name === 'wheels') { | |
| child.position.x = (child.position.x > 0 ? 1 : -1) * (1.1 + offset * 2); | |
| } else if (child.name === 'engine') { | |
| child.position.z = 1.4 + offset * 2; | |
| } else if (child.name === 'interior') { | |
| child.position.y = 1.0 + offset * 2; | |
| } | |
| }); | |
| } | |
| // --- TUTORIAL LOGIC --- | |
| let currentStepIndex = 0; | |
| function renderTutorialStep() { | |
| const container = document.getElementById('steps-container'); | |
| container.innerHTML = ''; | |
| tutorialSteps.forEach((step, index) => { | |
| const div = document.createElement('div'); | |
| div.className = `step ${index === currentStepIndex ? 'active' : ''}`; | |
| div.innerHTML = ` | |
| <div class="step-title">${step.title}</div> | |
| <div class="step-desc">${step.desc}</div> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| document.getElementById('step-counter').innerText = `${currentStepIndex + 1} / ${tutorialSteps.length}`; | |
| // Highlight relevant part in 3D | |
| const targetPart = tutorialSteps[currentStepIndex].target; | |
| highlightPart(targetPart); | |
| } | |
| function nextTutorialStep() { | |
| if (currentStepIndex < tutorialSteps.length - 1) { | |
| currentStepIndex++; | |
| renderTutorialStep(); | |
| } | |
| } | |
| function prevTutorialStep() { | |
| if (currentStepIndex > 0) { | |
| currentStepIndex--; | |
| renderTutorialStep(); | |
| } | |
| } | |
| // Start App | |
| init(); | |
| </script> | |
| </body> | |
| </html> |