anycoder-cec97058 / index.html
SAFE44's picture
Upload folder using huggingface_hub
8bbc3f1 verified
Raw History Blame Contribute Delete
32.8 kB
<!DOCTYPE html>
<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>