Spaces:
Running
Running
Download index.html from User-2468/anycoder-5e9ef7d6: direct link, hf CLI and curl.
- Browser
- Download file 40.4 kB
-
https://huggingface.co/spaces/User-2468/anycoder-5e9ef7d6/resolve/main/index.html
- Command line
-
hf download hf://spaces/User-2468/anycoder-5e9ef7d6/index.html
-
curl -L -o index.html https://huggingface.co/spaces/User-2468/anycoder-5e9ef7d6/resolve/main/index.html
40.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FORZA HORIZON - WebGL Racing</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap'); | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary: #ff3e3e; | |
| --secondary: #3eff3e; | |
| --accent: #3e8eff; | |
| --dark: #0a0a0f; | |
| --light: #f0f0f5; | |
| --glass: rgba(255, 255, 255, 0.05); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| } | |
| body { | |
| overflow: hidden; | |
| background: var(--dark); | |
| font-family: 'Rajdhani', sans-serif; | |
| color: var(--light); | |
| } | |
| #gameCanvas { | |
| display: block; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| /* Loading Screen */ | |
| #loadingScreen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| transition: opacity 0.8s ease; | |
| } | |
| #loadingScreen.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .loading-title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(2rem, 6vw, 4rem); | |
| font-weight: 900; | |
| background: linear-gradient(90deg, var(--primary), var(--accent)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-shadow: 0 0 60px rgba(255, 62, 62, 0.5); | |
| margin-bottom: 2rem; | |
| letter-spacing: 0.2em; | |
| } | |
| .loading-bar-container { | |
| width: 300px; | |
| height: 6px; | |
| background: var(--glass); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| border: 1px solid var(--glass-border); | |
| } | |
| .loading-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--accent)); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| box-shadow: 0 0 20px var(--primary); | |
| } | |
| .loading-text { | |
| margin-top: 1rem; | |
| font-size: 0.9rem; | |
| color: rgba(255, 255, 255, 0.5); | |
| letter-spacing: 0.3em; | |
| } | |
| /* HUD */ | |
| #hud { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 100; | |
| } | |
| .hud-top { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 1.5rem 2rem; | |
| background: linear-gradient(180deg, rgba(0,0,0,0.6) 0%, transparent 100%); | |
| } | |
| .brand { | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 900; | |
| font-size: 1.2rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .brand-icon { | |
| width: 30px; | |
| height: 30px; | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); | |
| } | |
| .speed-display { | |
| text-align: right; | |
| } | |
| .speed-value { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(2rem, 5vw, 3.5rem); | |
| font-weight: 700; | |
| color: var(--light); | |
| line-height: 1; | |
| text-shadow: 0 0 20px rgba(255, 255, 255, 0.3); | |
| } | |
| .speed-unit { | |
| font-size: 0.9rem; | |
| color: rgba(255, 255, 255, 0.5); | |
| letter-spacing: 0.2em; | |
| } | |
| .hud-bottom { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| padding: 2rem; | |
| background: linear-gradient(0deg, rgba(0,0,0,0.7) 0%, transparent 100%); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-end; | |
| } | |
| .lap-info { | |
| background: var(--glass); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 8px; | |
| padding: 1rem 1.5rem; | |
| } | |
| .lap-label { | |
| font-size: 0.75rem; | |
| color: rgba(255, 255, 255, 0.5); | |
| letter-spacing: 0.2em; | |
| margin-bottom: 0.25rem; | |
| } | |
| .lap-value { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--accent); | |
| } | |
| .time-display { | |
| text-align: center; | |
| } | |
| .time-label { | |
| font-size: 0.75rem; | |
| color: rgba(255, 255, 255, 0.5); | |
| letter-spacing: 0.2em; | |
| } | |
| .time-value { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: var(--secondary); | |
| text-shadow: 0 0 15px rgba(62, 255, 62, 0.5); | |
| } | |
| .gear-indicator { | |
| display: flex; | |
| gap: 0.25rem; | |
| margin-top: 0.5rem; | |
| } | |
| .gear { | |
| width: 24px; | |
| height: 30px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| font-size: 0.9rem; | |
| background: var(--glass); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 4px; | |
| transition: all 0.15s ease; | |
| } | |
| .gear.active { | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| border-color: transparent; | |
| box-shadow: 0 0 15px rgba(255, 62, 62, 0.5); | |
| } | |
| /* Mini Map */ | |
| .mini-map { | |
| width: 150px; | |
| height: 150px; | |
| background: var(--glass); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 8px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .mini-map canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Controls Panel */ | |
| #controls { | |
| position: fixed; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: var(--glass); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 12px; | |
| padding: 1rem 2rem; | |
| display: flex; | |
| gap: 1.5rem; | |
| pointer-events: auto; | |
| z-index: 100; | |
| } | |
| .control-key { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .key { | |
| width: 45px; | |
| height: 45px; | |
| background: linear-gradient(180deg, #2a2a35 0%, #1a1a25 100%); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| font-size: 0.8rem; | |
| color: var(--light); | |
| transition: all 0.1s ease; | |
| } | |
| .key.active { | |
| background: linear-gradient(180deg, var(--primary), #cc2020); | |
| box-shadow: 0 0 20px rgba(255, 62, 62, 0.5); | |
| transform: scale(0.95); | |
| } | |
| .key-label { | |
| font-size: 0.65rem; | |
| color: rgba(255, 255, 255, 0.4); | |
| letter-spacing: 0.1em; | |
| } | |
| /* Start Screen */ | |
| #startScreen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(10, 10, 15, 0.95); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 500; | |
| transition: opacity 0.5s ease; | |
| } | |
| #startScreen.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .start-title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(3rem, 10vw, 6rem); | |
| font-weight: 900; | |
| background: linear-gradient(90deg, var(--primary), #ff8c00, var(--accent)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 1rem; | |
| animation: titlePulse 2s ease-in-out infinite; | |
| } | |
| @keyframes titlePulse { | |
| 0%, 100% { filter: brightness(1); } | |
| 50% { filter: brightness(1.2); } | |
| } | |
| .start-subtitle { | |
| font-size: 1.2rem; | |
| color: rgba(255, 255, 255, 0.5); | |
| letter-spacing: 0.5em; | |
| margin-bottom: 3rem; | |
| } | |
| .start-btn { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| padding: 1rem 3rem; | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| border: none; | |
| border-radius: 8px; | |
| color: white; | |
| cursor: pointer; | |
| letter-spacing: 0.2em; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 0 30px rgba(255, 62, 62, 0.3); | |
| } | |
| .start-btn:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 50px rgba(255, 62, 62, 0.5); | |
| } | |
| /* Shaders */ | |
| .vignette { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.6) 100%); | |
| z-index: 50; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .hud-bottom { | |
| flex-direction: column; | |
| gap: 1rem; | |
| align-items: center; | |
| } | |
| .mini-map { | |
| display: none; | |
| } | |
| .controls { | |
| bottom: 1rem; | |
| padding: 0.75rem 1rem; | |
| gap: 1rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="gameCanvas"></canvas> | |
| <div class="vignette"></div> | |
| <!-- Loading Screen --> | |
| <div id="loadingScreen"> | |
| <div class="loading-title">FORZA HORIZON</div> | |
| <div class="loading-bar-container"> | |
| <div class="loading-bar" id="loadingBar"></div> | |
| </div> | |
| <div class="loading-text">INITIALIZING ENGINE...</div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="startScreen" class="hidden"> | |
| <div class="start-title">FORZA HORIZON</div> | |
| <div class="start-subtitle">WEBGL RACING</div> | |
| <button class="start-btn" id="startBtn">START RACE</button> | |
| </div> | |
| <!-- HUD --> | |
| <div id="hud"> | |
| <div class="hud-top"> | |
| <div class="brand"> | |
| <div class="brand-icon"></div> | |
| <span>FORZA HORIZON</span> | |
| </div> | |
| <div class="speed-display"> | |
| <div class="speed-value" id="speedValue">0</div> | |
| <div class="speed-unit">KM/H</div> | |
| </div> | |
| </div> | |
| <div class="hud-bottom"> | |
| <div class="lap-info"> | |
| <div class="lap-label">LAP</div> | |
| <div class="lap-value" id="lapValue">1/3</div> | |
| </div> | |
| <div class="time-display"> | |
| <div class="time-label">TIME</div> | |
| <div class="time-value" id="timeValue">00:00.000</div> | |
| <div class="gear-indicator" id="gearIndicator"></div> | |
| </div> | |
| <div class="mini-map"> | |
| <canvas id="miniMapCanvas"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls --> | |
| <div id="controls"> | |
| <div class="control-key"> | |
| <div class="key" id="keyW">W</div> | |
| <div class="key-label">ACCEL</div> | |
| </div> | |
| <div class="control-key"> | |
| <div class="key" id="keyA">A</div> | |
| <div class="key-label">LEFT</div> | |
| </div> | |
| <div class="control-key"> | |
| <div class="key" id="keyS">S</div> | |
| <div class="key-label">BRAKE</div> | |
| </div> | |
| <div class="control-key"> | |
| <div class="key" id="keyD">D</div> | |
| <div class="key-label">RIGHT</div> | |
| </div> | |
| </div> | |
| <!-- Vertex Shader --> | |
| <script id="vertexShader" type="x-shader/x-vertex"> | |
| varying vec3 vNormal; | |
| varying vec3 vPosition; | |
| varying vec2 vUv; | |
| void main() { | |
| vNormal = normalize(normalMatrix * normal); | |
| vPosition = (modelMatrix * vec4(position, 1.0)).xyz; | |
| vUv = uv; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| </script> | |
| <!-- Fragment Shader - Car --> | |
| <script id="carFragmentShader" type="x-shader/x-fragment"> | |
| precision highp float; | |
| uniform vec3 uColor; | |
| uniform vec3 uLightPosition; | |
| uniform vec3 uCameraPosition; | |
| uniform float uTime; | |
| varying vec3 vNormal; | |
| varying vec3 vPosition; | |
| varying vec2 vUv; | |
| void main() { | |
| vec3 normal = normalize(vNormal); | |
| vec3 lightDir = normalize(uLightPosition - vPosition); | |
| vec3 viewDir = normalize(uCameraPosition - vPosition); | |
| vec3 halfDir = normalize(lightDir + viewDir); | |
| // Ambient | |
| float ambient = 0.15; | |
| // Diffuse | |
| float diff = max(dot(normal, lightDir), 0.0); | |
| // Specular | |
| float spec = pow(max(dot(normal, halfDir), 0.0), 64.0); | |
| // Fresnel | |
| float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 3.0); | |
| vec3 color = uColor * (ambient + diff * 0.8) + vec3(1.0) * spec * 0.6 + vec3(0.3, 0.5, 1.0) * fresnel * 0.3; | |
| // Metallic shine | |
| color += vec3(0.1) * sin(uTime * 2.0); | |
| gl_FragColor = vec4(color, 1.0); | |
| } | |
| </script> | |
| <!-- Fragment Shader - Ground --> | |
| <script id="groundFragmentShader" type="x-shader/x-fragment"> | |
| precision highp float; | |
| uniform vec3 uColor1; | |
| uniform vec3 uColor2; | |
| uniform float uTime; | |
| varying vec3 vNormal; | |
| varying vec3 vPosition; | |
| varying vec2 vUv; | |
| float hash(vec2 p) { | |
| return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); | |
| } | |
| float noise(vec2 p) { | |
| vec2 i = floor(p); | |
| vec2 f = fract(p); | |
| f = f * f * (3.0 - 2.0 * f); | |
| float a = hash(i); | |
| float b = hash(i + vec2(1.0, 0.0)); | |
| float c = hash(i + vec2(0.0, 1.0)); | |
| float d = hash(i + vec2(1.0, 1.0)); | |
| return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); | |
| } | |
| void main() { | |
| vec3 normal = normalize(vNormal); | |
| // Grid pattern | |
| vec2 grid = fract(vPosition.xz * 0.1); | |
| float gridLine = step(0.95, grid.x) + step(0.95, grid.y); | |
| gridLine = min(gridLine, 1.0); | |
| // Noise for texture | |
| float n = noise(vPosition.xz * 0.5) * 0.2; | |
| // Distance fade | |
| float dist = length(vPosition.xz) * 0.005; | |
| float fade = 1.0 - clamp(dist, 0.0, 1.0); | |
| vec3 color = mix(uColor1, uColor2, gridLine * 0.3 + n); | |
| color *= fade; | |
| // Fog | |
| float fog = 1.0 - exp(-dist * 2.0); | |
| color = mix(color, vec3(0.1, 0.1, 0.15), fog * 0.7); | |
| gl_FragColor = vec4(color, 1.0); | |
| } | |
| </script> | |
| <!-- Sky Shader --> | |
| <script id="skyFragmentShader" type="x-shader/x-fragment"> | |
| precision highp float; | |
| uniform float uTime; | |
| varying vec3 vPosition; | |
| void main() { | |
| vec3 dir = normalize(vPosition); | |
| // Gradient sky | |
| float y = dir.y * 0.5 + 0.5; | |
| vec3 skyColor = mix(vec3(0.1, 0.15, 0.3), vec3(0.02, 0.05, 0.15), y); | |
| // Stars | |
| vec2 starUv = dir.xz / (dir.y + 0.001) * 10.0; | |
| float stars = step(0.998, fract(sin(dot(floor(starUv * 50.0), vec2(12.9898, 78.233))) * 43758.5453)); | |
| stars *= smoothstep(0.3, 0.8, dir.y); | |
| skyColor += vec3(stars) * 0.8; | |
| // Horizon glow | |
| float horizonGlow = exp(-abs(dir.y) * 5.0); | |
| skyColor += vec3(0.3, 0.1, 0.2) * horizonGlow * 0.3; | |
| gl_FragColor = vec4(skyColor, 1.0); | |
| } | |
| </script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // Game State | |
| const GameState = { | |
| LOADING: 'loading', | |
| START: 'start', | |
| PLAYING: 'playing', | |
| PAUSED: 'paused', | |
| FINISHED: 'finished' | |
| }; | |
| let currentState = GameState.LOADING; | |
| // Three.js Setup | |
| let scene, camera, renderer, clock; | |
| let car, carBody, wheels = []; | |
| let track; | |
| let gameTime = 0; | |
| let lapTime = 0; | |
| let currentLap = 1; | |
| let totalLaps = 3; | |
| // Physics | |
| const carPhysics = { | |
| position: new THREE.Vector3(0, 0.5, 0), | |
| velocity: new THREE.Vector3(), | |
| rotation: 0, | |
| speed: 0, | |
| maxSpeed: 120, | |
| acceleration: 40, | |
| braking: 60, | |
| friction: 10, | |
| turnSpeed: 2.5, | |
| wheelAngle: 0 | |
| }; | |
| // Controls | |
| const keys = { | |
| forward: false, | |
| backward: false, | |
| left: false, | |
| right: false | |
| }; | |
| // Track Data | |
| let trackPoints = []; | |
| let checkpoints = []; | |
| let currentCheckpoint = 0; | |
| // Mini Map | |
| let miniMapCanvas, miniMapCtx; | |
| // Initialize | |
| async function init() { | |
| updateLoadingBar(10); | |
| // Scene | |
| scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0x0a0a15, 0.008); | |
| // Camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 5, -10); | |
| // Renderer | |
| renderer = new THREE.WebGLRenderer({ | |
| canvas: document.getElementById('gameCanvas'), | |
| antialias: true, | |
| powerPreference: 'high-performance' | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 1.2; | |
| updateLoadingBar(30); | |
| // Clock | |
| clock = new THREE.Clock(); | |
| // Create World | |
| createSky(); | |
| updateLoadingBar(40); | |
| createGround(); | |
| updateLoadingBar(50); | |
| createTrack(); | |
| updateLoadingBar(70); | |
| createCar(); | |
| updateLoadingBar(90); | |
| createLights(); | |
| // Mini Map | |
| setupMiniMap(); | |
| // Gear Indicator | |
| setupGearIndicator(); | |
| updateLoadingBar(100); | |
| // Event Listeners | |
| setupEventListeners(); | |
| // Start Game Loop | |
| setTimeout(() => { | |
| document.getElementById('loadingScreen').classList.add('hidden'); | |
| document.getElementById('startScreen').classList.remove('hidden'); | |
| currentState = GameState.START; | |
| }, 500); | |
| animate(); | |
| } | |
| function updateLoadingBar(percent) { | |
| document.getElementById('loadingBar').style.width = percent + '%'; | |
| } | |
| // Sky | |
| function createSky() { | |
| const skyGeo = new THREE.SphereGeometry(500, 32, 32); | |
| const skyMat = new THREE.ShaderMaterial({ | |
| vertexShader: document.getElementById('vertexShader').textContent, | |
| fragmentShader: document.getElementById('skyFragmentShader').textContent, | |
| uniforms: { | |
| uTime: { value: 0 } | |
| }, | |
| side: THREE.BackSide | |
| }); | |
| const sky = new THREE.Mesh(skyGeo, skyMat); | |
| scene.add(sky); | |
| } | |
| // Ground | |
| function createGround() { | |
| const groundGeo = new THREE.PlaneGeometry(1000, 1000, 100, 100); | |
| const groundMat = new THREE.ShaderMaterial({ | |
| vertexShader: document.getElementById('vertexShader').textContent, | |
| fragmentShader: document.getElementById('groundFragmentShader').textContent, | |
| uniforms: { | |
| uColor1: { value: new THREE.Color(0x1a1a2e) }, | |
| uColor2: { value: new THREE.Color(0x16213e) }, | |
| uTime: { value: 0 } | |
| } | |
| }); | |
| const ground = new THREE.Mesh(groundGeo, groundMat); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.receiveShadow = true; | |
| scene.add(ground); | |
| } | |
| // Track Generation | |
| function createTrack() { | |
| // Generate track points in a loop | |
| const trackRadius = 80; | |
| const numPoints = 32; | |
| for (let i = 0; i < numPoints; i++) { | |
| const angle = (i / numPoints) * Math.PI * 2; | |
| const wobble = Math.sin(angle * 3) * 20 + Math.cos(angle * 2) * 15; | |
| const r = trackRadius + wobble; | |
| trackPoints.push(new THREE.Vector3( | |
| Math.cos(angle) * r, | |
| 0, | |
| Math.sin(angle) * r | |
| )); | |
| } | |
| // Create track mesh | |
| const trackShape = new THREE.Shape(); | |
| const trackWidth = 12; | |
| // Track surface | |
| const trackGeo = new THREE.BufferGeometry(); | |
| const vertices = []; | |
| const normals = []; | |
| const uvs = []; | |
| for (let i = 0; i < trackPoints.length; i++) { | |
| const p1 = trackPoints[i]; | |
| const p2 = trackPoints[(i + 1) % trackPoints.length]; | |
| const dir = new THREE.Vector3().subVectors(p2, p1).normalize(); | |
| const perp = new THREE.Vector3(-dir.z, 0, dir.x); | |
| const p1Left = new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(trackWidth / 2)); | |
| const p1Right = new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(-trackWidth / 2)); | |
| const p2Left = new THREE.Vector3().addVectors(p2, perp.clone().multiplyScalar(trackWidth / 2)); | |
| const p2Right = new THREE.Vector3().addVectors(p2, perp.clone().multiplyScalar(-trackWidth / 2)); | |
| // Triangle 1 | |
| vertices.push(p1Left.x, 0.02, p1Left.z); | |
| vertices.push(p1Right.x, 0.02, p1Right.z); | |
| vertices.push(p2Left.x, 0.02, p2Left.z); | |
| // Triangle 2 | |
| vertices.push(p1Right.x, 0.02, p1Right.z); | |
| vertices.push(p2Right.x, 0.02, p2Right.z); | |
| vertices.push(p2Left.x, 0.02, p2Left.z); | |
| for (let j = 0; j < 6; j++) { | |
| normals.push(0, 1, 0); | |
| uvs.push(0, 0); | |
| } | |
| } | |
| trackGeo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); | |
| trackGeo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); | |
| trackGeo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); | |
| const trackMat = new THREE.MeshStandardMaterial({ | |
| color: 0x2a2a35, | |
| roughness: 0.8, | |
| metalness: 0.2 | |
| }); | |
| track = new THREE.Mesh(trackGeo, trackMat); | |
| track.receiveShadow = true; | |
| scene.add(track); | |
| // Track borders | |
| createTrackBorders(); | |
| // Checkpoints | |
| createCheckpoints(); | |
| } | |
| function createTrackBorders() { | |
| const borderRadius = 0.3; | |
| const borderHeight = 0.5; | |
| const innerBorderPoints = []; | |
| const outerBorderPoints = []; | |
| const trackWidth = 12; | |
| for (let i = 0; i < trackPoints.length; i++) { | |
| const p1 = trackPoints[i]; | |
| const p2 = trackPoints[(i + 1) % trackPoints.length]; | |
| const dir = new THREE.Vector3().subVectors(p2, p1).normalize(); | |
| const perp = new THREE.Vector3(-dir.z, 0, dir.x); | |
| innerBorderPoints.push(new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(trackWidth / 2 - 0.5))); | |
| outerBorderPoints.push(new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(-trackWidth / 2 + 0.5))); | |
| } | |
| // Create curb meshes | |
| const curbMat = new THREE.MeshStandardMaterial({ color: 0xff3e3e }); | |
| const curbMat2 = new THREE.MeshStandardMaterial({ color: 0xf0f0f0 }); | |
| // Inner curb | |
| for (let i = 0; i < innerBorderPoints.length; i++) { | |
| const p1 = innerBorderPoints[i]; | |
| const p2 = innerBorderPoints[(i + 1) % innerBorderPoints.length]; | |
| const curbGeo = new THREE.BoxGeometry(0.5, borderHeight, p1.distanceTo(p2) + 0.5); | |
| const curb = new THREE.Mesh(curbGeo, i % 2 === 0 ? curbMat : curbMat2); | |
| curb.position.copy(p1).add(p2).multiplyScalar(0.5); | |
| curb.position.y = borderHeight / 2; | |
| curb.lookAt(p2); | |
| curb.rotateY(Math.PI / 2); | |
| scene.add(curb); | |
| } | |
| // Outer curb | |
| for (let i = 0; i < outerBorderPoints.length; i++) { | |
| const p1 = outerBorderPoints[i]; | |
| const p2 = outerBorderPoints[(i + 1) % outerBorderPoints.length]; | |
| const curbGeo = new THREE.BoxGeometry(0.5, borderHeight, p1.distanceTo(p2) + 0.5); | |
| const curb = new THREE.Mesh(curbGeo, i % 2 === 0 ? curbMat : curbMat2); | |
| curb.position.copy(p1).add(p2).multiplyScalar(0.5); | |
| curb.position.y = borderHeight / 2; | |
| curb.lookAt(p2); | |
| curb.rotateY(Math.PI / 2); | |
| scene.add(curb); | |
| } | |
| } | |
| function createCheckpoints() { | |
| const numCheckpoints = 8; | |
| for (let i = 0; i < numCheckpoints; i++) { | |
| const idx = Math.floor((i / numCheckpoints) * trackPoints.length); | |
| const p1 = trackPoints[idx]; | |
| const p2 = trackPoints[(idx + 2) % trackPoints.length]; | |
| const center = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5); | |
| const dir = new THREE.Vector3().subVectors(p2, p1).normalize(); | |
| checkpoints.push({ | |
| position: center, | |
| direction: dir, | |
| passed: false | |
| }); | |
| } | |
| } | |
| // Car | |
| function createCar() { | |
| car = new THREE.Group(); | |
| // Car body shader material | |
| const carMat = new THREE.ShaderMaterial({ | |
| vertexShader: document.getElementById('vertexShader').textContent, | |
| fragmentShader: document.getElementById('carFragmentShader').textContent, | |
| uniforms: { | |
| uColor: { value: new THREE.Color(0xff3e3e) }, | |
| uLightPosition: { value: new THREE.Vector3(50, 100, 50) }, | |
| uCameraPosition: { value: new THREE.Vector3() }, | |
| uTime: { value: 0 } | |
| } | |
| }); | |
| // Main body | |
| const bodyGeo = new THREE.BoxGeometry(2, 0.6, 4); | |
| carBody = new THREE.Mesh(bodyGeo, carMat); | |
| carBody.position.y = 0.5; | |
| carBody.castShadow = true; | |
| car.add(carBody); | |
| // Cabin | |
| const cabinGeo = new THREE.BoxGeometry(1.6, 0.5, 1.8); | |
| 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.2); | |
| cabin.castShadow = true; | |
| car.add(cabin); | |
| // Hood | |
| const hoodGeo = new THREE.BoxGeometry(1.8, 0.2, 1.2); | |
| const hood = new THREE.Mesh(hoodGeo, carMat); | |
| hood.position.set(0, 0.8, 1.2); | |
| hood.castShadow = true; | |
| car.add(hood); | |
| // Spoiler | |
| const spoilerGeo = new THREE.BoxGeometry(1.8, 0.1, 0.3); | |
| const spoiler = new THREE.Mesh(spoilerGeo, carMat); | |
| spoiler.position.set(0, 1.1, -1.8); | |
| spoiler.castShadow = true; | |
| car.add(spoiler); | |
| // Spoiler stands | |
| const standGeo = new THREE.BoxGeometry(0.1, 0.3, 0.1); | |
| const stand1 = new THREE.Mesh(standGeo, carMat); | |
| stand1.position.set(0.7, 0.95, -1.8); | |
| car.add(stand1); | |
| const stand2 = new THREE.Mesh(standGeo, carMat); | |
| stand2.position.set(-0.7, 0.95, -1.8); | |
| car.add(stand2); | |
| // Wheels | |
| const wheelGeo = new THREE.CylinderGeometry(0.35, 0.35, 0.25, 16); | |
| const wheelMat = new THREE.MeshStandardMaterial({ | |
| color: 0x1a1a1a, | |
| metalness: 0.3, | |
| roughness: 0.7 | |
| }); | |
| const wheelPositions = [ | |
| { x: -1, y: 0.35, z: 1.2 }, | |
| { x: 1, y: 0.35, z: 1.2 }, | |
| { x: -1, y: 0.35, z: -1.2 }, | |
| { x: 1, y: 0.35, z: -1.2 } | |
| ]; | |
| wheelPositions.forEach(pos => { | |
| const wheel = new THREE.Mesh(wheelGeo, wheelMat); | |
| wheel.rotation.z = Math.PI / 2; | |
| wheel.position.set(pos.x, pos.y, pos.z); | |
| wheel.castShadow = true; | |
| wheels.push(wheel); | |
| car.add(wheel); | |
| // Rim | |
| const rimGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.26, 6); | |
| const rimMat = new THREE.MeshStandardMaterial({ | |
| color: 0xcccccc, | |
| metalness: 0.9, | |
| roughness: 0.2 | |
| }); | |
| const rim = new THREE.Mesh(rimGeo, rimMat); | |
| rim.rotation.z = Math.PI / 2; | |
| rim.position.set(pos.x, pos.y, pos.z); | |
| car.add(rim); | |
| }); | |
| // Headlights | |
| const headlightGeo = new THREE.SphereGeometry(0.15, 8, 8); | |
| const headlightMat = new THREE.MeshBasicMaterial({ color: 0xffffcc }); | |
| const hl1 = new THREE.Mesh(headlightGeo, headlightMat); | |
| hl1.position.set(0.6, 0.5, 2); | |
| car.add(hl1); | |
| const hl2 = new THREE.Mesh(headlightGeo, headlightMat); | |
| hl2.position.set(-0.6, 0.5, 2); | |
| car.add(hl2); | |
| // Taillights | |
| const taillightMat = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
| const tl1 = new THREE.Mesh(headlightGeo, taillightMat); | |
| tl1.position.set(0.6, 0.5, -2); | |
| car.add(tl1); | |
| const tl2 = new THREE.Mesh(headlightGeo, taillightMat); | |
| tl2.position.set(-0.6, 0.5, -2); | |
| car.add(tl2); | |
| // Position car | |
| car.position.copy(trackPoints[0]); | |
| car.position.y = 0; | |
| // Face correct direction | |
| const firstDir = new THREE.Vector3().subVectors(trackPoints[1], trackPoints[0]).normalize(); | |
| car.rotation.y = Math.atan2(firstDir.x, firstDir.z); | |
| carPhysics.position.copy(car.position); | |
| carPhysics.rotation = car.rotation.y; | |
| scene.add(car); | |
| } | |
| // Lights | |
| function createLights() { | |
| // Ambient | |
| const ambient = new THREE.AmbientLight(0x404060, 0.5); | |
| scene.add(ambient); | |
| // Main directional light (sun) | |
| const sun = new THREE.DirectionalLight(0xffeedd, 1); | |
| sun.position.set(50, 100, 50); | |
| sun.castShadow = true; | |
| sun.shadow.mapSize.width = 2048; | |
| sun.shadow.mapSize.height = 2048; | |
| sun.shadow.camera.near = 0.5; | |
| sun.shadow.camera.far = 500; | |
| sun.shadow.camera.left = -100; | |
| sun.shadow.camera.right = 100; | |
| sun.shadow.camera.top = 100; | |
| sun.shadow.camera.bottom = -100; | |
| scene.add(sun); | |
| // Fill light | |
| const fill = new THREE.DirectionalLight(0x8888ff, 0.3); | |
| fill.position.set(-50, 50, -50); | |
| scene.add(fill); | |
| // Hemisphere | |
| const hemi = new THREE.HemisphereLight(0x606080, 0x404040, 0.5); | |
| scene.add(hemi); | |
| } | |
| // Mini Map | |
| function setupMiniMap() { | |
| miniMapCanvas = document.getElementById('miniMapCanvas'); | |
| miniMapCanvas.width = 150; | |
| miniMapCanvas.height = 150; | |
| miniMapCtx = miniMapCanvas.getContext('2d'); | |
| } | |
| function updateMiniMap() { | |
| const ctx = miniMapCtx; | |
| const w = miniMapCanvas.width; | |
| const h = miniMapCanvas.height; | |
| const scale = 0.8; | |
| const offsetX = w / 2; | |
| const offsetY = h / 2; | |
| // Clear | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
| ctx.fillRect(0, 0, w, h); | |
| // Track | |
| ctx.strokeStyle = '#2a2a35'; | |
| ctx.lineWidth = 8; | |
| ctx.beginPath(); | |
| for (let i = 0; i < trackPoints.length; i++) { | |
| const x = (trackPoints[i].x - carPhysics.position.x) * scale + offsetX; | |
| const y = (trackPoints[i].z - carPhysics.position.z) * scale + offsetY; | |
| if (i === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| } | |
| ctx.closePath(); | |
| ctx.stroke(); | |
| // Car position | |
| ctx.fillStyle = '#ff3e3e'; | |
| ctx.beginPath(); | |
| ctx.arc(offsetX, offsetY, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Direction indicator | |
| ctx.strokeStyle = '#ff3e3e'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(offsetX, offsetY); | |
| ctx.lineTo( | |
| offsetX + Math.sin(carPhysics.rotation) * 8, | |
| offsetY - Math.cos(carPhysics.rotation) * 8 | |
| ); | |
| ctx.stroke(); | |
| } | |
| // Gear Indicator | |
| function setupGearIndicator() { | |
| const container = document.getElementById('gearIndicator'); | |
| for (let i = 1; i <= 6; i++) { | |
| const gear = document.createElement('div'); | |
| gear.className = 'gear'; | |
| gear.textContent = i; | |
| gear.dataset.gear = i; | |
| container.appendChild(gear); | |
| } | |
| } | |
| function updateGearIndicator() { | |
| const speed = carPhysics.speed; | |
| let gear; | |
| if (speed < 10) gear = 1; | |
| else if (speed < 25) gear = 2; | |
| else if (speed < 45) gear = 3; | |
| else if (speed < 70) gear = 4; | |
| else if (speed < 95) gear = 5; | |
| else gear = 6; | |
| document.querySelectorAll('.gear').forEach(g => { | |
| g.classList.toggle('active', parseInt(g.dataset.gear) === gear); | |
| }); | |
| } | |
| // Event Listeners | |
| function setupEventListeners() { | |
| // Keyboard | |
| document.addEventListener('keydown', (e) => { | |
| if (currentState !== GameState.PLAYING) return; | |
| switch(e.key.toLowerCase()) { | |
| case 'w': | |
| case 'arrowup': | |
| keys.forward = true; | |
| document.getElementById('keyW').classList.add('active'); | |
| break; | |
| case 's': | |
| case 'arrowdown': | |
| keys.backward = true; | |
| document.getElementById('keyS').classList.add('active'); | |
| break; | |
| case 'a': | |
| case 'arrowleft': | |
| keys.left = true; | |
| document.getElementById('keyA').classList.add('active'); | |
| break; | |
| case 'd': | |
| case 'arrowright': | |
| keys.right = true; | |
| document.getElementById('keyD').classList.add('active'); | |
| break; | |
| } | |
| }); | |
| document.addEventListener('keyup', (e) => { | |
| switch(e.key.toLowerCase()) { | |
| case 'w': | |
| case 'arrowup': | |
| keys.forward = false; | |
| document.getElementById('keyW').classList.remove('active'); | |
| break; | |
| case 's': | |
| case 'arrowdown': | |
| keys.backward = false; | |
| document.getElementById('keyS').classList.remove('active'); | |
| break; |