Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Car Parts Customizer - Fantasy Rally</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js", | |
| "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <style> | |
| :root { | |
| --primary-bg: #0d0c1d; | |
| --secondary-bg: #131224; | |
| --accent-color: #6c63ff; | |
| --accent-glow: rgba(108, 99, 255, 0.5); | |
| --text-primary: #e0e0e0; | |
| --text-secondary: #a0a0a0; | |
| --border-color: #3c3b53; | |
| } | |
| body { | |
| font-family: 'Space Mono', monospace; | |
| background-color: var(--primary-bg); | |
| color: var(--text-primary); | |
| overflow-x: hidden; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| /* === CRT Effect === */ | |
| .crt-effect::before { | |
| content: " "; | |
| display: block; | |
| position: fixed; | |
| top: 0; left: 0; bottom: 0; right: 0; | |
| background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); | |
| background-size: 100% 2px, 3px 100%; | |
| z-index: 9999; | |
| pointer-events: none; | |
| opacity: 0.2; | |
| } | |
| /* === Header & Buttons === */ | |
| .header-btn { | |
| background-color: rgba(108, 99, 255, 0.2); | |
| border: 1px solid var(--accent-glow); | |
| transition: all 0.3s ease; | |
| } | |
| .header-btn:hover { | |
| background-color: rgba(108, 99, 255, 0.4); | |
| box-shadow: 0 0 15px var(--accent-glow); | |
| } | |
| /* === 3D Viewer === */ | |
| #viewer-container { | |
| width: 100%; | |
| height: 500px; | |
| position: relative; | |
| border: 2px solid var(--accent-glow); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| background: rgba(19, 18, 36, 0.8); | |
| } | |
| #threed-canvas { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| #loading-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(13, 12, 29, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 10; | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 5px solid rgba(108, 99, 255, 0.3); | |
| border-top: 5px solid var(--accent-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-bottom: 20px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* === Parts Panel === */ | |
| .parts-panel { | |
| background: rgba(19, 18, 36, 0.8); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| .part-category { | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .part-category:last-child { | |
| border-bottom: none; | |
| } | |
| .part-item { | |
| padding: 10px; | |
| border-bottom: 1px solid rgba(60, 59, 83, 0.5); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .part-item:last-child { | |
| border-bottom: none; | |
| } | |
| .part-item:hover { | |
| background: rgba(108, 99, 255, 0.2); | |
| } | |
| .part-item.active { | |
| background: rgba(108, 99, 255, 0.4); | |
| border-left: 3px solid var(--accent-color); | |
| } | |
| /* === Controls Panel === */ | |
| .controls-panel { | |
| background: rgba(19, 18, 36, 0.8); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| } | |
| .control-group { | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .control-group:last-child { | |
| border-bottom: none; | |
| } | |
| .color-picker { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| border: 2px solid var(--border-color); | |
| cursor: pointer; | |
| } | |
| /* === Modal Styles === */ | |
| .modal-overlay { | |
| background-color: rgba(13, 12, 29, 0.9); | |
| backdrop-filter: blur(10px); | |
| } | |
| .modal-content { | |
| background-color: var(--secondary-bg); | |
| border: 1px solid var(--border-color); | |
| } | |
| </style> | |
| </head> | |
| <body class="crt-effect"> | |
| <header class="p-4 flex flex-wrap justify-between items-center gap-4 bg-secondary-bg border-b border-border-color sticky top-0 z-20"> | |
| <h1 id="header-title" class="text-2xl font-bold tracking-wider text-accent">3D CAR PARTS CUSTOMIZER</h1> | |
| <div class="flex items-center gap-2"> | |
| <button id="back-to-hub-btn" class="header-btn text-white font-bold py-2 px-4 rounded-lg"><i class="fas fa-arrow-left mr-2"></i>Back to Hub</button> | |
| </div> | |
| </header> | |
| <main class="p-4 sm:p-6"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6"> | |
| <!-- 3D Viewer --> | |
| <div class="lg:col-span-2"> | |
| <div class="card-bg rounded-lg p-4"> | |
| <h2 class="text-xl font-bold mb-4">3D Viewer</h2> | |
| <div id="viewer-container"> | |
| <div id="loading-overlay"> | |
| <div class="spinner"></div> | |
| <p>Loading 3D Model...</p> | |
| </div> | |
| <canvas id="threed-canvas"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls Panel --> | |
| <div class="controls-panel rounded-lg p-4"> | |
| <h2 class="text-xl font-bold mb-4">Customization Controls</h2> | |
| <div class="control-group mb-4 pb-4"> | |
| <h3 class="font-bold mb-2">Color Customization</h3> | |
| <div class="grid grid-cols-3 gap-3"> | |
| <div> | |
| <label class="block text-sm mb-1">Primary</label> | |
| <input type="color" id="primary-color" class="color-picker w-full" value="#6c63ff"> | |
| </div> | |
| <div> | |
| <label class="block text-sm mb-1">Secondary</label> | |
| <input type="color" id="secondary-color" class="color-picker w-full" value="#ff6b6b"> | |
| </div> | |
| <div> | |
| <label class="block text-sm mb-1">Accent</label> | |
| <input type="color" id="accent-color-picker" class="color-picker w-full" value="#4ecdc4"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="control-group mb-4 pb-4"> | |
| <h3 class="font-bold mb-2">Material</h3> | |
| <select id="material-select" class="w-full p-2 bg-primary-bg border border-border-color rounded"> | |
| <option value="metal">Metallic</option> | |
| <option value="matte">Matte</option> | |
| <option value="chrome">Chrome</option> | |
| <option value="carbon">Carbon Fiber</option> | |
| </select> | |
| </div> | |
| <div class="control-group mb-4 pb-4"> | |
| <h3 class="font-bold mb-2">View Controls</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button id="reset-view-btn" class="header-btn py-2 px-3 rounded text-sm">Reset View</button> | |
| <button id="toggle-wireframe-btn" class="header-btn py-2 px-3 rounded text-sm">Wireframe</button> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <h3 class="font-bold mb-2">Export</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button id="export-png-btn" class="header-btn py-2 px-3 rounded text-sm">Export PNG</button> | |
| <button id="export-glb-btn" class="header-btn py-2 px-3 rounded text-sm">Export GLB</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Parts Selection Panel --> | |
| <div class="parts-panel rounded-lg p-4"> | |
| <h2 class="text-xl font-bold mb-4">Car Parts</h2> | |
| <div class="part-category mb-4 pb-4"> | |
| <h3 class="font-bold mb-2">Body</h3> | |
| <div class="part-item active" data-part="body">Main Body</div> | |
| <div class="part-item" data-part="hood">Hood</div> | |
| <div class="part-item" data-part="roof">Roof</div> | |
| <div class="part-item" data-part="trunk">Trunk</div> | |
| </div> | |
| <div class="part-category mb-4 pb-4"> | |
| <h3 class="font-bold mb-2">Wheels</h3> | |
| <div class="part-item" data-part="wheel_fl">Front Left Wheel</div> | |
| <div class="part-item" data-part="wheel_fr">Front Right Wheel</div> | |
| <div class="part-item" data-part="wheel_rl">Rear Left Wheel</div> | |
| <div class="part-item" data-part="wheel_rr">Rear Right Wheel</div> | |
| <div class="part-item" data-part="rim">Rim Style</div> | |
| </div> | |
| <div class="part-category mb-4 pb-4"> | |
| <h3 class="font-bold mb-2">Accessories</h3> | |
| <div class="part-item" data-part="spoiler">Spoiler</div> | |
| <div class="part-item" data-part="side_skirt">Side Skirts</div> | |
| <div class="part-item" data-part="front_bumper">Front Bumper</div> | |
| <div class="part-item" data-part="rear_bumper">Rear Bumper</div> | |
| </div> | |
| <div class="part-category"> | |
| <h3 class="font-bold mb-2">Lighting</h3> | |
| <div class="part-item" data-part="headlight">Headlights</div> | |
| <div class="part-item" data-part="taillight">Taillights</div> | |
| <div class="part-item" data-part="neon">Neon Kit</div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Confirmation Modal --> | |
| <div id="confirmation-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 modal-overlay hidden"> | |
| <div class="modal-content rounded-lg shadow-2xl max-w-md w-full p-6"> | |
| <h3 class="text-xl font-bold mb-4">Export Ready</h3> | |
| <p class="mb-6">Your customized car model has been prepared for export. What would you like to do next?</p> | |
| <div class="flex justify-end space-x-3"> | |
| <button id="cancel-export-btn" class="header-btn py-2 px-4 rounded">Cancel</button> | |
| <button id="confirm-export-btn" class="header-btn py-2 px-4 rounded bg-accent-color">Download</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| // DOM Elements | |
| const backToHubBtn = document.getElementById('back-to-hub-btn'); | |
| const viewerContainer = document.getElementById('viewer-container'); | |
| const loadingOverlay = document.getElementById('loading-overlay'); | |
| const primaryColorPicker = document.getElementById('primary-color'); | |
| const secondaryColorPicker = document.getElementById('secondary-color'); | |
| const accentColorPicker = document.getElementById('accent-color-picker'); | |
| const materialSelect = document.getElementById('material-select'); | |
| const resetViewBtn = document.getElementById('reset-view-btn'); | |
| const toggleWireframeBtn = document.getElementById('toggle-wireframe-btn'); | |
| const exportPngBtn = document.getElementById('export-png-btn'); | |
| const exportGlbBtn = document.getElementById('export-glb-btn'); | |
| const partItems = document.querySelectorAll('.part-item'); | |
| const confirmationModal = document.getElementById('confirmation-modal'); | |
| const cancelExportBtn = document.getElementById('cancel-export-btn'); | |
| const confirmExportBtn = document.getElementById('confirm-export-btn'); | |
| // Three.js variables | |
| let scene, camera, renderer, controls; | |
| let carModel = null; | |
| let selectedPart = 'body'; | |
| let wireframeMode = false; | |
| // Initialize Three.js | |
| function init() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0d0c1d); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(45, viewerContainer.clientWidth / viewerContainer.clientHeight, 0.1, 1000); | |
| camera.position.set(0, 0, 5); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ | |
| canvas: document.getElementById('threed-canvas'), | |
| antialias: true, | |
| alpha: true | |
| }); | |
| renderer.setSize(viewerContainer.clientWidth, viewerContainer.clientHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| // Add orbit controls | |
| controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.autoRotate = true; | |
| controls.autoRotateSpeed = 0.5; | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(5, 5, 5); | |
| scene.add(directionalLight); | |
| // Create a simple car model | |
| createCarModel(); | |
| // Hide loading overlay | |
| loadingOverlay.style.display = 'none'; | |
| // Start animation loop | |
| animate(); | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| // Create a simple car model | |
| function createCarModel() { | |
| // Car body | |
| const bodyGeometry = new THREE.BoxGeometry(2, 0.5, 1); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ | |
| color: primaryColorPicker.value, | |
| metalness: 0.5, | |
| roughness: 0.5 | |
| }); | |
| const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| body.position.y = 0.5; | |
| scene.add(body); | |
| // Car top | |
| const topGeometry = new THREE.BoxGeometry(1, 0.4, 0.8); | |
| const topMaterial = new THREE.MeshStandardMaterial({ | |
| color: secondaryColorPicker.value, | |
| metalness: 0.3, | |
| roughness: 0.7 | |
| }); | |
| const top = new THREE.Mesh(topGeometry, topMaterial); | |
| top.position.y = 1; | |
| top.position.z = 0.1; | |
| scene.add(top); | |
| // Wheels | |
| const wheelGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.1, 16); | |
| wheelGeometry.rotateZ(Math.PI / 2); | |
| const wheelMaterial = new THREE.MeshStandardMaterial({ | |
| color: accentColorPicker.value, | |
| metalness: 0.8, | |
| roughness: 0.2 | |
| }); | |
| const wheelPositions = [ | |
| { x: 0.8, y: 0.2, z: 0.6 }, | |
| { x: -0.8, y: 0.2, z: 0.6 }, | |
| { x: 0.8, y: 0.2, z: -0.6 }, | |
| { x: -0.8, y: 0.2, z: -0.6 } | |
| ]; | |
| wheelPositions.forEach(pos => { | |
| const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
| wheel.position.set(pos.x, pos.y, pos.z); | |
| scene.add(wheel); | |
| }); | |
| carModel = new THREE.Group(); | |
| carModel.add(body); | |
| carModel.add(top); | |
| // Wheels are added directly to scene for simplicity | |
| scene.add(carModel); | |
| } | |
| // Update materials based on selections | |
| function updateMaterials() { | |
| if (!carModel) return; | |
| const primaryColor = new THREE.Color(primaryColorPicker.value); | |
| const secondaryColor = new THREE.Color(secondaryColorPicker.value); | |
| const accentColor = new THREE.Color(accentColorPicker.value); | |
| carModel.traverse((child) => { | |
| if (child.isMesh) { | |
| // Determine which material to apply based on object name or position | |
| let color; | |
| if (child.geometry.parameters.height === 0.4) { | |
| color = secondaryColor; // Top | |
| } else { | |
| color = primaryColor; // Body | |
| } | |
| // Create material based on selection | |
| let material; | |
| switch(materialSelect.value) { | |
| case 'metal': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: color, | |
| metalness: 0.8, | |
| roughness: 0.2 | |
| }); | |
| break; | |
| case 'matte': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: color, | |
| metalness: 0.1, | |
| roughness: 0.9 | |
| }); | |
| break; | |
| case 'chrome': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: color, | |
| metalness: 1, | |
| roughness: 0 | |
| }); | |
| break; | |
| case 'carbon': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: color, | |
| metalness: 0.4, | |
| roughness: 0.3 | |
| }); | |
| break; | |
| default: | |
| material = new THREE.MeshStandardMaterial({ color: color }); | |
| } | |
| // Apply wireframe if enabled | |
| material.wireframe = wireframeMode; | |
| child.material = material; | |
| } | |
| }); | |
| // Update wheel colors | |
| scene.traverse((child) => { | |
| if (child.isMesh && child.geometry.type === 'CylinderGeometry') { | |
| let material; | |
| switch(materialSelect.value) { | |
| case 'metal': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: accentColor, | |
| metalness: 0.8, | |
| roughness: 0.2 | |
| }); | |
| break; | |
| case 'matte': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: accentColor, | |
| metalness: 0.1, | |
| roughness: 0.9 | |
| }); | |
| break; | |
| case 'chrome': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: accentColor, | |
| metalness: 1, | |
| roughness: 0 | |
| }); | |
| break; | |
| case 'carbon': | |
| material = new THREE.MeshStandardMaterial({ | |
| color: accentColor, | |
| metalness: 0.4, | |
| roughness: 0.3 | |
| }); | |
| break; | |
| default: | |
| material = new THREE.MeshStandardMaterial({ color: accentColor }); | |
| } | |
| material.wireframe = wireframeMode; | |
| child.material = material; | |
| } | |
| }); | |
| } | |
| // Handle window resize | |
| function onWindowResize() { | |
| camera.aspect = viewerContainer.clientWidth / viewerContainer.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(viewerContainer.clientWidth, viewerContainer.clientHeight); | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| // Event Listeners | |
| backToHubBtn.addEventListener('click', () => { | |
| window.location.href = 'index.html'; | |
| }); | |
| // Part selection | |
| partItems.forEach(item => { | |
| item.addEventListener('click', () => { | |
| partItems.forEach(i => i.classList.remove('active')); | |
| item.classList.add('active'); | |
| selectedPart = item.dataset.part; | |
| }); | |
| }); | |
| // Color pickers | |
| primaryColorPicker.addEventListener('input', updateMaterials); | |
| secondaryColorPicker.addEventListener('input', updateMaterials); | |
| accentColorPicker.addEventListener('input', updateMaterials); | |
| // Material selection | |
| materialSelect.addEventListener('change', updateMaterials); | |
| // Reset view | |
| resetViewBtn.addEventListener('click', () => { | |
| controls.reset(); | |
| }); | |
| // Toggle wireframe | |
| toggleWireframeBtn.addEventListener('click', () => { | |
| wireframeMode = !wireframeMode; | |
| toggleWireframeBtn.textContent = wireframeMode ? 'Solid' : 'Wireframe'; | |
| updateMaterials(); | |
| }); | |
| // Export PNG | |
| exportPngBtn.addEventListener('click', () => { | |
| // In a real implementation, this would trigger the export process | |
| confirmationModal.classList.remove('hidden'); | |
| }); | |
| // Export GLB | |
| exportGlbBtn.addEventListener('click', () => { | |
| // In a real implementation, this would trigger the export process | |
| confirmationModal.classList.remove('hidden'); | |
| }); | |
| // Modal controls | |
| cancelExportBtn.addEventListener('click', () => { | |
| confirmationModal.classList.add('hidden'); | |
| }); | |
| confirmExportBtn.addEventListener('click', () => { | |
| // Simulate download | |
| const link = document.createElement('a'); | |
| link.href = 'data:text/plain;charset=utf-8,Car customization export would be available here'; | |
| link.download = 'custom_car.txt'; | |
| link.click(); | |
| confirmationModal.classList.add('hidden'); | |
| }); | |
| // Close modal when clicking outside | |
| confirmationModal.addEventListener('click', (e) => { | |
| if (e.target === confirmationModal) { | |
| confirmationModal.classList.add('hidden'); | |
| } | |
| }); | |
| // Initialize the application | |
| init(); | |
| </script> | |
| </body> | |
| </html> |