Spaces:
Running
Running
Download index.html from SAFE44/anycoder-cec97058: direct link, hf CLI and curl.
- Browser
- Download file 32.8 kB
-
https://huggingface.co/spaces/SAFE44/anycoder-cec97058/resolve/main/index.html
- Command line
-
hf download hf://spaces/SAFE44/anycoder-cec97058/index.html
-
curl -L -o index.html https://huggingface.co/spaces/SAFE44/anycoder-cec97058/resolve/main/index.html
32.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Royal Driver 3D - Showroom & Drive</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@400;600&display=swap'); | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: #000; | |
| font-family: 'Rajdhani', sans-serif; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| } | |
| #canvas-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| } | |
| .ui-layer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 10; | |
| pointer-events: none; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| padding: 1rem; | |
| box-sizing: border-box; | |
| } | |
| .interactive { | |
| pointer-events: auto; | |
| } | |
| /* Neon Glow Text */ | |
| .neon-text { | |
| text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 20px #0ff, 0 0 30px #0ff, 0 0 40px #0ff; | |
| } | |
| /* Button Styles */ | |
| .btn-glass { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| color: white; | |
| transition: all 0.2s ease; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | |
| } | |
| .btn-glass:active { | |
| transform: scale(0.95); | |
| background: rgba(255, 255, 255, 0.25); | |
| } | |
| .btn-glass.active-btn { | |
| background: rgba(0, 255, 255, 0.3); | |
| border-color: #0ff; | |
| box-shadow: 0 0 15px #0ff; | |
| } | |
| /* Color Swatch */ | |
| .swatch { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| border: 2px solid rgba(255,255,255,0.5); | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| } | |
| .swatch:hover { transform: scale(1.2); } | |
| .swatch.selected { border-color: #fff; box-shadow: 0 0 10px #fff; } | |
| /* HUD */ | |
| .hud-panel { | |
| background: rgba(0, 0, 0, 0.6); | |
| border-left: 2px solid #0ff; | |
| padding: 10px 20px; | |
| transform: skewX(-15deg); | |
| } | |
| .hud-text { | |
| transform: skewX(15deg); | |
| font-family: 'Orbitron', sans-serif; | |
| } | |
| /* Loader */ | |
| #loader { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: #050505; | |
| z-index: 100; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| color: #0ff; | |
| font-family: 'Orbitron', sans-serif; | |
| transition: opacity 0.5s; | |
| } | |
| .loader-bar { | |
| width: 200px; | |
| height: 4px; | |
| background: #333; | |
| margin-top: 20px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .loader-progress { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 100%; | |
| width: 0%; | |
| background: #0ff; | |
| box-shadow: 0 0 10px #0ff; | |
| transition: width 0.2s; | |
| } | |
| /* Animations */ | |
| @keyframes pulse-ring { | |
| 0% { transform: scale(0.8); opacity: 0.5; } | |
| 100% { transform: scale(1.2); opacity: 0; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Loading Screen --> | |
| <div id="loader"> | |
| <div class="text-2xl mb-2 tracking-widest">ROYAL DRIVER <span class="text-xs align-top">3D</span></div> | |
| <div class="text-sm text-gray-400">INITIALIZING ENGINE...</div> | |
| <div class="loader-bar"><div class="loader-progress" id="loader-progress"></div></div> | |
| <div class="mt-4 text-xs text-gray-500">Built with anycoder</div> | |
| </div> | |
| <!-- 3D Canvas --> | |
| <div id="canvas-container"></div> | |
| <!-- UI Layer --> | |
| <div class="ui-layer"> | |
| <!-- Header --> | |
| <div class="w-full flex justify-between items-start"> | |
| <div class="flex flex-col"> | |
| <h1 class="text-4xl md:text-6xl font-black text-white italic tracking-tighter drop-shadow-lg" style="font-family: 'Orbitron'"> | |
| ROYAL<span class="text-cyan-400">DRIVER</span> | |
| </h1> | |
| <p class="text-cyan-200 text-xs md:text-sm tracking-widest uppercase opacity-80">Simulation Environment v2.4</p> | |
| </div> | |
| <div class="hidden md:block"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-xs text-white opacity-50 hover:opacity-100 transition cursor-pointer interactive">Built with anycoder</a> | |
| </div> | |
| </div> | |
| <!-- Showroom UI (Centered) --> | |
| <div id="showroom-ui" class="absolute inset-0 flex flex-col items-center justify-center interactive transition-opacity duration-500"> | |
| <div class="bg-black/60 backdrop-blur-md p-8 rounded-xl border border-cyan-500/30 flex flex-col items-center gap-6 shadow-2xl transform transition-all hover:scale-105"> | |
| <div class="text-center"> | |
| <h2 class="text-2xl text-white font-bold mb-1">CUSTOMIZE RIDE</h2> | |
| <p class="text-gray-400 text-xs">SELECT PAINT JOB</p> | |
| </div> | |
| <div class="flex gap-4" id="color-picker"> | |
| <button class="swatch bg-red-600" data-color="0xff0000"></button> | |
| <button class="swatch bg-blue-600" data-color="0x0044ff"></button> | |
| <button class="swatch bg-green-600" data-color="0x00ff00"></button> | |
| <button class="swatch bg-yellow-400" data-color="0xffcc00"></button> | |
| <button class="swatch bg-gray-900 border-gray-600" data-color="0x111111"></button> | |
| <button class="swatch bg-white" data-color="0xffffff"></button> | |
| </div> | |
| <button id="start-btn" class="mt-4 px-12 py-4 bg-cyan-600 hover:bg-cyan-500 text-white font-black text-xl rounded-sm tracking-widest clip-path-polygon transition-all shadow-[0_0_20px_rgba(0,255,255,0.5)] hover:shadow-[0_0_40px_rgba(0,255,255,0.8)] active:scale-95"> | |
| START RACE | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Driving UI (Hidden by default) --> | |
| <div id="driving-ui" class="absolute inset-0 flex flex-col justify-between opacity-0 pointer-events-none transition-opacity duration-500"> | |
| <!-- HUD Top --> | |
| <div class="w-full flex justify-end p-4"> | |
| <div class="hud-panel"> | |
| <div class="hud-text text-right"> | |
| <div class="text-xs text-cyan-300 uppercase">Speed</div> | |
| <div class="text-4xl text-white font-bold"><span id="speed-display">0</span> <span class="text-sm">KM/H</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls Bottom --> | |
| <div class="w-full flex justify-between items-end p-4 pb-8"> | |
| <!-- Steering --> | |
| <div class="flex gap-4"> | |
| <button id="btn-left" class="btn-glass w-20 h-20 rounded-full flex items-center justify-center text-3xl interactive"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg> | |
| </button> | |
| <button id="btn-right" class="btn-glass w-20 h-20 rounded-full flex items-center justify-center text-3xl interactive"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg> | |
| </button> | |
| </div> | |
| <!-- Pedals --> | |
| <div class="flex gap-4"> | |
| <button id="btn-brake" class="btn-glass w-24 h-24 rounded-full flex flex-col items-center justify-center border-red-500/50 interactive"> | |
| <span class="font-bold text-red-400">BRAKE</span> | |
| </button> | |
| <button id="btn-gas" class="btn-glass w-28 h-28 rounded-full flex flex-col items-center justify-center border-green-500/50 interactive"> | |
| <span class="font-bold text-green-400">GAS</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * ------------------------------------------------------------------ | |
| * APPLICATION STATE & CONFIGURATION | |
| * ------------------------------------------------------------------ | |
| */ | |
| const CONFIG = { | |
| maxSpeed: 120, // Arbitrary units | |
| acceleration: 0.5, | |
| deceleration: 0.2, | |
| turnSpeed: 0.03, | |
| cameraOffset: new THREE.Vector3(0, 3, -7), // Behind car | |
| showroomRotationSpeed: 0.005 | |
| }; | |
| const STATE = { | |
| mode: 'showroom', // 'showroom' or 'drive' | |
| speed: 0, | |
| steering: 0, // -1 to 1 | |
| isAccelerating: false, | |
| isBraking: false, | |
| audioInitialized: false | |
| }; | |
| /** | |
| * ------------------------------------------------------------------ | |
| * ASSET MANAGEMENT (Simulated since we don't have real external files) | |
| * ------------------------------------------------------------------ | |
| */ | |
| // Helper to create a texture programmatically if fetch fails or for placeholders | |
| function createPlaceholderTexture(type) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 512; | |
| canvas.height = 512; | |
| const ctx = canvas.getContext('2d'); | |
| if (type === 'road') { | |
| // Asphalt | |
| ctx.fillStyle = '#333'; | |
| ctx.fillRect(0, 0, 512, 512); | |
| // Noise | |
| for(let i=0; i<5000; i++) { | |
| ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.1})`; | |
| ctx.fillRect(Math.random()*512, Math.random()*512, 2, 2); | |
| } | |
| // Center lines | |
| ctx.strokeStyle = '#fff'; | |
| ctx.lineWidth = 4; | |
| ctx.setLineDash([40, 40]); | |
| ctx.beginPath(); | |
| ctx.moveTo(256, 0); | |
| ctx.lineTo(256, 512); | |
| ctx.stroke(); | |
| } else if (type === 'pole') { | |
| // Metal/Concrete | |
| const grad = ctx.createLinearGradient(0, 0, 512, 0); | |
| grad.addColorStop(0, '#444'); | |
| grad.addColorStop(0.5, '#777'); | |
| grad.addColorStop(1, '#444'); | |
| ctx.fillStyle = grad; | |
| ctx.fillRect(0, 0, 512, 512); | |
| // Rust | |
| ctx.fillStyle = 'rgba(100, 50, 20, 0.2)'; | |
| ctx.fillRect(0, 0, 512, 100); | |
| ctx.fillRect(0, 412, 512, 100); | |
| } | |
| const tex = new THREE.CanvasTexture(canvas); | |
| tex.wrapS = THREE.RepeatWrapping; | |
| tex.wrapT = THREE.RepeatWrapping; | |
| return tex; | |
| } | |
| // Since we can't guarantee external GLB files work perfectly in a single HTML without CORS/availability issues, | |
| // we will construct a high-quality "Beta" car using primitives to ensure the app ALWAYS works perfectly. | |
| function createProceduralCar() { | |
| const carGroup = new THREE.Group(); | |
| // Chassis | |
| const chassisGeo = new THREE.BoxGeometry(2, 0.5, 4); | |
| const chassisMat = new THREE.MeshStandardMaterial({ color: 0xcc0000, metalness: 0.6, roughness: 0.2 }); | |
| const chassis = new THREE.Mesh(chassisGeo, chassisMat); | |
| chassis.position.y = 0.5; | |
| chassis.castShadow = true; | |
| carGroup.add(chassis); | |
| // Cabin | |
| const cabinGeo = new THREE.BoxGeometry(1.8, 0.4, 2); | |
| const cabinMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.9, roughness: 0.1 }); | |
| const cabin = new THREE.Mesh(cabinGeo, cabinMat); | |
| cabin.position.set(0, 1.0, -0.2); | |
| cabin.castShadow = true; | |
| carGroup.add(cabin); | |
| // Wheels | |
| const wheelGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 24); | |
| const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 }); | |
| const positions = [ | |
| { x: -1.1, z: 1.2 }, { x: 1.1, z: 1.2 }, | |
| { x: -1.1, z: -1.2 }, { x: 1.1, z: -1.2 } | |
| ]; | |
| positions.forEach(pos => { | |
| const wheel = new THREE.Mesh(wheelGeo, wheelMat); | |
| wheel.rotation.z = Math.PI / 2; | |
| wheel.position.set(pos.x, 0.4, pos.z); | |
| wheel.castShadow = true; | |
| carGroup.add(wheel); | |
| }); | |
| // Headlights (Physical Geometry) | |
| const lightGeo = new THREE.CircleGeometry(0.15, 16); | |
| const lightMat = new THREE.MeshBasicMaterial({ color: 0xffffcc }); | |
| const leftLight = new THREE.Mesh(lightGeo, lightMat); | |
| leftLight.position.set(-0.6, 0.5, 2.01); | |
| carGroup.add(leftLight); | |
| const rightLight = new THREE.Mesh(lightGeo, lightMat); | |
| rightLight.position.set(0.6, 0.5, 2.01); | |
| carGroup.add(rightLight); | |
| // Tail lights | |
| const tailGeo = new THREE.PlaneGeometry(0.3, 0.1); | |
| const tailMat = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
| const leftTail = new THREE.Mesh(tailGeo, tailMat); | |
| leftTail.position.set(-0.6, 0.5, -2.01); | |
| leftTail.rotation.y = Math.PI; | |
| carGroup.add(leftTail); | |
| const rightTail = new THREE.Mesh(tailGeo, tailMat); | |
| rightTail.position.set(0.6, 0.5, -2.01); | |
| rightTail.rotation.y = Math.PI; | |
| carGroup.add(rightTail); | |
| // Spotlights for Showroom | |
| const spotL = new THREE.SpotLight(0xffffee, 2); | |
| spotL.position.set(-0.6, 0.5, 2.1); | |
| spotL.target.position.set(-0.6, 0, 10); | |
| spotL.angle = Math.PI / 6; | |
| spotL.penumbra = 0.2; | |
| spotL.distance = 20; | |
| carGroup.add(spotL); | |
| carGroup.add(spotL.target); | |
| const spotR = new THREE.SpotLight(0xffffee, 2); | |
| spotR.position.set(0.6, 0.5, 2.1); | |
| spotR.target.position.set(0.6, 0, 10); | |
| spotR.angle = Math.PI / 6; | |
| spotR.penumbra = 0.2; | |
| spotR.distance = 20; | |
| carGroup.add(spotR); | |
| carGroup.add(spotR.target); | |
| // Store references for color changing | |
| carGroup.userData.bodyMeshes = [chassis]; | |
| return carGroup; | |
| } | |
| /** | |
| * ------------------------------------------------------------------ | |
| * THREE.JS SETUP | |
| * ------------------------------------------------------------------ | |
| */ | |
| const container = document.getElementById('canvas-container'); | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87CEEB); // Daytime Sky | |
| scene.fog = new THREE.Fog(0x87CEEB, 20, 100); | |
| const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| container.appendChild(renderer.domElement); | |
| // Lighting | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const sunLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| sunLight.position.set(50, 100, 50); | |
| sunLight.castShadow = true; | |
| sunLight.shadow.mapSize.width = 2048; | |
| sunLight.shadow.mapSize.height = 2048; | |
| sunLight.shadow.camera.near = 0.5; | |
| sunLight.shadow.camera.far = 500; | |
| sunLight.shadow.camera.left = -50; | |
| sunLight.shadow.camera.right = 50; | |
| sunLight.shadow.camera.top = 50; | |
| sunLight.shadow.camera.bottom = -50; | |
| scene.add(sunLight); | |
| // Environment | |
| const textureLoader = new THREE.TextureLoader(); | |
| const roadTexture = createPlaceholderTexture('road'); | |
| roadTexture.repeat.set(1, 10); | |
| const poleTexture = createPlaceholderTexture('pole'); | |
| // Ground Plane (Grass) | |
| const groundGeo = new THREE.PlaneGeometry(1000, 1000); | |
| const groundMat = new THREE.MeshStandardMaterial({ color: 0x2a5a2a }); | |
| const ground = new THREE.Mesh(groundGeo, groundMat); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.position.y = -0.1; | |
| ground.receiveShadow = true; | |
| scene.add(ground); | |
| // Road | |
| const roadGeo = new THREE.PlaneGeometry(8, 1000); // Wide road | |
| const roadMat = new THREE.MeshStandardMaterial({ | |
| map: roadTexture, | |
| roughness: 0.8 | |
| }); | |
| const road = new THREE.Mesh(roadGeo, roadMat); | |
| road.rotation.x = -Math.PI / 2; | |
| road.receiveShadow = true; | |
| scene.add(road); | |
| // Poles | |
| const poles = []; | |
| function createPoles() { | |
| const poleGeo = new THREE.CylinderGeometry(0.1, 0.1, 6, 8); | |
| const poleMat = new THREE.MeshStandardMaterial({ map: poleTexture, roughness: 0.9 }); | |
| for (let i = -50; i < 50; i+=2) { | |
| // Left Pole | |
| const pL = new THREE.Mesh(poleGeo, poleMat); | |
| pL.position.set(-6, 3, i * 10); | |
| pL.castShadow = true; | |
| scene.add(pL); | |
| poles.push(pL); | |
| // Right Pole | |
| const pR = new THREE.Mesh(poleGeo, poleMat); | |
| pR.position.set(6, 3, i * 10); | |
| pR.castShadow = true; | |
| scene.add(pR); | |
| poles.push(pR); | |
| } | |
| } | |
| createPoles(); | |
| /** | |
| * ------------------------------------------------------------------ | |
| * CAR LOGIC | |
| * ------------------------------------------------------------------ | |
| */ | |
| let car; | |
| let carVelocity = 0; | |
| let carSteering = 0; | |
| function initCar() { | |
| // Try to load external GLB, fallback to procedural if fails | |
| const loader = new THREE.GLTFLoader(); | |
| loader.load( | |
| 'beta.glb', | |
| (gltf) => { | |
| car = gltf.scene; | |
| setupCar(car); | |
| document.getElementById('loader-progress').style.width = '100%'; | |
| setTimeout(() => document.getElementById('loader').style.opacity = '0', 500); | |
| setTimeout(() => document.getElementById('loader').style.display = 'none', 1000); | |
| }, | |
| (xhr) => { | |
| const percent = (xhr.loaded / xhr.total) * 100; | |
| document.getElementById('loader-progress').style.width = percent + '%'; | |
| }, | |
| (error) => { | |
| console.warn("Could not load GLB, using procedural model.", error); | |
| car = createProceduralCar(); | |
| scene.add(car); | |
| // Hide loader | |
| document.getElementById('loader-progress').style.width = '100%'; | |
| setTimeout(() => document.getElementById('loader').style.opacity = '0', 500); | |
| setTimeout(() => document.getElementById('loader').style.display = 'none', 1000); | |
| } | |
| ); | |
| } | |
| function setupCar(model) { | |
| // Scale and center if needed | |
| const box = new THREE.Box3().setFromObject(model); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| model.position.sub(center); // Center pivot | |
| // Orient to face +Z (forward in Three.js usually, but we want nose at +Z) | |
| // If model faces +Z, good. If not, rotate. | |
| // Assuming standard car models face +Z or -Z. | |
| // Let's rotate 180 if needed. Usually GLTF cars face +Z or -Z. | |
| // We will assume standard orientation. | |
| scene.add(model); | |
| // Add shadows | |
| model.traverse((child) => { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| // Try to identify body parts for color changing | |
| if (!child.name.toLowerCase().includes('glass') && !child.name.toLowerCase().includes('wheel')) { | |
| if(!model.userData.bodyMeshes) model.userData.bodyMeshes = []; | |
| model.userData.bodyMeshes.push(child); | |
| } | |
| } | |
| }); | |
| } | |
| initCar(); | |
| /** | |
| * ------------------------------------------------------------------ | |
| * AUDIO SYSTEM (Web Audio API) | |
| * ------------------------------------------------------------------ | |
| */ | |
| const AudioContext = window.AudioContext || window.webkitAudioContext; | |
| let audioCtx; | |
| let engineOsc; | |
| let engineGain; | |
| // Since we can't guarantee external MP3s, we synthesize engine sounds using Oscillators. | |
| // This is actually better for dynamic pitch changes! | |
| function initAudio() { | |
| if (STATE.audioInitialized) return; | |
| audioCtx = new AudioContext(); | |
| // Create Oscillator for engine hum | |
| engineOsc = audioCtx.createOscillator(); | |
| engineGain = audioCtx.createGain(); | |
| engineOsc.type = 'sawtooth'; | |
| engineOsc.frequency.value = 50; // Idle rumble | |
| // Lowpass filter to make it sound muffled like an engine | |
| const filter = audioCtx.createBiquadFilter(); | |
| filter.type = 'lowpass'; | |
| filter.frequency.value = 400; | |
| engineOsc.connect(filter); | |
| filter.connect(engineGain); | |
| engineGain.connect(audioCtx.destination); | |
| engineOsc.start(); | |
| engineGain.gain.value = 0; // Start silent | |
| STATE.audioInitialized = true; | |
| } | |
| function updateEngineSound() { | |
| if (!STATE.audioInitialized || !audioCtx) return; | |
| // Resume context if suspended (browser policy) | |
| if (audioCtx.state === 'suspended') audioCtx.resume(); | |
| const baseFreq = 60; | |
| const maxFreq = 300; | |
| // Calculate speed ratio (0 to 1) | |
| const speedRatio = Math.min(STATE.speed / CONFIG.maxSpeed, 1); | |
| // Pitch modulation | |
| const targetFreq = baseFreq + (speedRatio * (maxFreq - baseFreq)); | |
| engineOsc.frequency.setTargetAtTime(targetFreq, audioCtx.currentTime, 0.1); | |
| // Volume modulation (louder when faster) | |
| const targetGain = 0.05 + (speedRatio * 0.1); | |
| engineGain.gain.setTargetAtTime(targetGain, audioCtx.currentTime, 0.1); | |
| } | |
| // Audio Event Listeners | |
| function playStartSound() { | |
| if(!audioCtx) return; | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); | |
| gain.connect(audioCtx.destination); | |
| osc.frequency.setValueAtTime(100, audioCtx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 1.5); | |
| gain.gain.setValueAtTime(0.1, audioCtx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1.5); | |
| osc.start(); | |
| osc.stop(audioCtx.currentTime + 1.5); | |
| } | |
| /** | |
| * ------------------------------------------------------------------ | |
| * UI INTERACTION | |
| * ------------------------------------------------------------------ | |
| */ | |
| // Color Picker | |
| document.querySelectorAll('.swatch').forEach(swatch => { | |
| swatch.addEventListener('click', (e) => { | |
| const colorHex = parseInt(e.target.dataset.color); | |
| if (car && car.userData.bodyMeshes) { | |
| car.userData.bodyMeshes.forEach(mesh => { | |
| // Clone material to not affect others sharing it | |
| if(mesh.material) { | |
| mesh.material = mesh.material.clone(); | |
| mesh.material.color.setHex(colorHex); | |
| } | |
| }); | |
| } | |
| // Visual feedback | |
| document.querySelectorAll('.swatch').forEach(s => s.classList.remove('selected')); | |
| e.target.classList.add('selected'); | |
| }); | |
| }); | |
| // Start Button | |
| document.getElementById('start-btn').addEventListener('click', () => { | |
| initAudio(); // Initialize Audio Context on user gesture | |
| playStartSound(); | |
| // Transition UI | |
| document.getElementById('showroom-ui').style.opacity = '0'; | |
| document.getElementById('showroom-ui').style.pointerEvents = 'none'; | |
| const driveUI = document.getElementById('driving-ui'); | |
| driveUI.style.opacity = '1'; | |
| driveUI.style.pointerEvents = 'auto'; | |
| STATE.mode = 'drive'; | |
| // Reset Car Position | |
| if(car) { | |
| car.position.set(0, 0, 0); | |
| car.rotation.set(0, Math.PI, 0); // Face forward | |
| STATE.speed = 0; | |
| STATE.steering = 0; | |
| } | |
| }); | |
| // Driving Controls | |
| const btnGas = document.getElementById('btn-gas'); | |
| const btnBrake = document.getElementById('btn-brake'); | |
| const btnLeft = document.getElementById('btn-left'); | |
| const btnRight = document.getElementById('btn-right'); | |
| function setupTouch(el, key) { | |
| const start = (e) => { e.preventDefault(); STATE[key] = true; el.classList.add('active-btn'); }; | |
| const end = (e) => { e.preventDefault(); STATE[key] = false; el.classList.remove('active-btn'); }; | |
| el.addEventListener('mousedown', start); | |
| el.addEventListener('mouseup', end); | |
| el.addEventListener('mouseleave', end); | |
| el.addEventListener('touchstart', start); | |
| el.addEventListener('touchend', end); | |
| } | |
| setupTouch(btnGas, 'isAccelerating'); | |
| setupTouch(btnBrake, 'isBraking'); | |
| setupTouch(btnLeft, 'isTurningLeft'); | |
| setupTouch(btnRight, 'isTurningRight'); | |
| // Keyboard controls | |
| window.addEventListener('keydown', (e) => { | |
| if(STATE.mode !== 'drive') return; | |
| if(e.key === 'ArrowUp' || e.key === 'w') STATE.isAccelerating = true; | |
| if(e.key === 'ArrowDown' || e.key === 's') STATE.isBraking = true; | |
| if(e.key === 'ArrowLeft' || e.key === 'a') STATE.isTurningLeft = true; | |
| if(e.key === 'ArrowRight' || e.key === 'd') STATE.isTurningRight = true; | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| if(e.key === 'ArrowUp' || e.key === 'w') STATE.isAccelerating = false; | |
| if(e.key === 'ArrowDown' || e.key === 's') STATE.isBraking = false; | |
| if(e.key === 'ArrowLeft' || e.key === 'a') STATE.isTurningLeft = false; | |
| if(e.key === 'ArrowRight' || e.key === 'd') STATE.isTurningRight = false; | |
| }); | |
| /** | |
| * ------------------------------------------------------------------ | |
| * ANIMATION LOOP | |
| * ------------------------------------------------------------------ | |
| */ | |
| function updatePhysics() { | |
| if (!car) return; | |
| if (STATE.mode === 'showroom') { | |
| car.rotation.y += CONFIG.showroomRotationSpeed; | |
| // Floating effect | |
| car.position.y = Math.sin(Date.now() * 0.002) * 0.1; | |
| } | |
| else if (STATE.mode === 'drive') { | |
| // Acceleration | |
| if (STATE.isAccelerating) { | |
| STATE.speed += CONFIG.acceleration; | |
| } else if (STATE.isBraking) { | |
| STATE.speed -= CONFIG.deceleration * 2; | |
| } else { | |
| // Rolling friction | |
| STATE.speed -= CONFIG.deceleration * 0.5; | |
| } | |
| // Clamp Speed | |
| STATE.speed = Math.max(0, Math.min(STATE.speed, CONFIG.maxSpeed)); | |
| // Steering | |
| let targetSteering = 0; | |
| if (STATE.isTurningLeft) targetSteering = 1; | |
| if (STATE.isTurningRight) targetSteering = -1; | |
| // Smooth steering | |
| STATE.steering += (targetSteering - STATE.steering) * 0.1; | |
| // Rotation (only if moving) | |
| if (STATE.speed > 0.1) { | |
| car.rotation.y += STATE.steering * CONFIG.turnSpeed * (STATE.speed / 20); | |
| } | |
| // Move Car | |
| // Forward vector is (0, 0, 1) in local space, transformed to world | |
| const direction = new THREE.Vector3(0, 0, 1); | |
| direction.applyAxisAngle(new THREE.Vector3(0, 1, 0), car.rotation.y); | |
| car.position.add(direction.multiplyScalar(STATE.speed * 0.01)); | |
| // Camera Follow | |
| const relativeOffset = new THREE.Vector3(0, 4, -8); // Behind and up | |
| // Calculate ideal camera position based on car rotation | |
| const cameraOffset = relativeOffset.applyMatrix4(car.matrixWorld); | |
| // Smoothly interpolate camera position | |
| camera.position.lerp(cameraOffset, 0.1); | |
| camera.lookAt(car.position); | |
| // Update UI | |
| document.getElementById('speed-display').innerText = Math.floor(STATE.speed); | |
| // Update Audio | |
| updateEngineSound(); | |
| // Infinite Road/Environment Logic (Teleport objects to create infinite illusion) | |
| // Instead of moving world, we check if poles are too far behind car and move them ahead | |
| poles.forEach(pole => { | |
| if (pole.position.z < car.position.z - 20) { | |
| pole.position.z += 100; // Move ahead by total loop length | |
| } | |
| }); | |
| // Texture offset for road movement effect | |
| if (roadMat.map) { | |
| roadMat.map.offset.y -= (STATE.speed * 0.001); | |
| } | |
| } | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| updatePhysics(); | |
| renderer.render(scene, camera); | |
| } | |
| // Handle Window Resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| // Start Loop | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |