Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Neon Velocity: Cyber Racing</title> | |
| <!-- Importing Google Fonts for Cyberpunk Aesthetic --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #00f3ff; /* Cyan */ | |
| --secondary: #ff0055; /* Magenta */ | |
| --accent: #ffee00; /* Yellow */ | |
| --bg-dark: #050510; | |
| --glass: rgba(255, 255, 255, 0.05); | |
| --border: rgba(255, 255, 255, 0.1); | |
| --font-display: 'Orbitron', sans-serif; | |
| --font-body: 'Rajdhani', sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| } | |
| body { | |
| background-color: var(--bg-dark); | |
| color: white; | |
| font-family: var(--font-body); | |
| overflow: hidden; | |
| height: 100vh; | |
| width: 100vw; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* --- Header & Branding --- */ | |
| header { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| z-index: 100; | |
| pointer-events: none; /* Let clicks pass through to canvas if needed */ | |
| } | |
| .brand { | |
| font-family: var(--font-display); | |
| font-size: 1.5rem; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| text-shadow: 0 0 10px var(--primary); | |
| pointer-events: auto; | |
| } | |
| .brand span { | |
| color: var(--secondary); | |
| } | |
| .built-with { | |
| font-size: 0.8rem; | |
| color: rgba(255,255,255,0.5); | |
| text-decoration: none; | |
| border: 1px solid var(--border); | |
| padding: 5px 10px; | |
| border-radius: 20px; | |
| backdrop-filter: blur(5px); | |
| transition: all 0.3s ease; | |
| } | |
| .built-with:hover { | |
| background: var(--primary); | |
| color: var(--bg-dark); | |
| border-color: var(--primary); | |
| box-shadow: 0 0 15px var(--primary); | |
| } | |
| /* --- Game Container --- */ | |
| #game-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| background: radial-gradient(circle at center, #1a1a2e 0%, #000000 100%); | |
| } | |
| canvas { | |
| box-shadow: 0 0 50px rgba(0, 243, 255, 0.1); | |
| max-width: 100%; | |
| max-height: 100%; | |
| } | |
| /* --- UI Overlays --- */ | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(8px); | |
| z-index: 50; | |
| transition: opacity 0.3s ease; | |
| } | |
| .overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| h1 { | |
| font-family: var(--font-display); | |
| font-size: 4rem; | |
| text-transform: uppercase; | |
| background: linear-gradient(to right, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 1rem; | |
| text-align: center; | |
| filter: drop-shadow(0 0 20px rgba(0, 243, 255, 0.5)); | |
| animation: pulse 2s infinite; | |
| } | |
| .instructions { | |
| margin-bottom: 2rem; | |
| text-align: center; | |
| color: #ccc; | |
| font-size: 1.2rem; | |
| line-height: 1.6; | |
| } | |
| .key-badge { | |
| display: inline-block; | |
| background: var(--glass); | |
| border: 1px solid var(--primary); | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| color: var(--primary); | |
| font-weight: bold; | |
| margin: 0 2px; | |
| } | |
| button.btn-primary { | |
| background: transparent; | |
| color: var(--primary); | |
| font-family: var(--font-display); | |
| font-size: 1.5rem; | |
| padding: 1rem 3rem; | |
| border: 2px solid var(--primary); | |
| border-radius: 4px; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| transition: all 0.2s ease; | |
| box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); | |
| } | |
| button.btn-primary:hover { | |
| background: var(--primary); | |
| color: var(--bg-dark); | |
| box-shadow: 0 0 30px var(--primary); | |
| transform: scale(1.05); | |
| } | |
| /* --- HUD (Heads Up Display) --- */ | |
| #hud { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| right: 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| pointer-events: none; | |
| z-index: 40; | |
| display: none; /* Hidden until game starts */ | |
| } | |
| .hud-panel { | |
| background: rgba(0, 0, 0, 0.5); | |
| border-left: 3px solid var(--secondary); | |
| padding: 10px 20px; | |
| backdrop-filter: blur(4px); | |
| } | |
| .hud-label { | |
| font-size: 0.8rem; | |
| color: var(--secondary); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .hud-value { | |
| font-family: var(--font-display); | |
| font-size: 1.5rem; | |
| color: white; | |
| } | |
| /* --- Mobile Controls --- */ | |
| #mobile-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| width: 100%; | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| z-index: 60; | |
| display: none; /* Shown via JS on touch devices */ | |
| } | |
| .control-btn { | |
| width: 80px; | |
| height: 80px; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 2px solid var(--primary); | |
| color: white; | |
| font-size: 2rem; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| backdrop-filter: blur(5px); | |
| touch-action: manipulation; | |
| } | |
| .control-btn:active { | |
| background: var(--primary); | |
| color: black; | |
| } | |
| /* --- Animations --- */ | |
| @keyframes pulse { | |
| 0% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.02); opacity: 0.9; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| @media (max-width: 768px) { | |
| h1 { font-size: 2.5rem; } | |
| .instructions { font-size: 1rem; } | |
| #hud { top: 10px; } | |
| .hud-value { font-size: 1.2rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand">Neon <span>Racer</span></div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with">Built with anycoder</a> | |
| </header> | |
| <main id="game-container"> | |
| <!-- Canvas Layer --> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- Heads Up Display --> | |
| <div id="hud"> | |
| <div class="hud-panel"> | |
| <div class="hud-label">Score</div> | |
| <div class="hud-value" id="scoreDisplay">0</div> | |
| </div> | |
| <div class="hud-panel" style="border-left: none; border-right: 3px solid var(--primary); text-align: right;"> | |
| <div class="hud-label">Speed</div> | |
| <div class="hud-value" id="speedDisplay">0 km/h</div> | |
| </div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="startScreen" class="overlay"> | |
| <h1>Neon Velocity</h1> | |
| <div class="instructions"> | |
| <p>Use <span class="key-badge">←</span> <span class="key-badge">→</span> or <span class="key-badge">A</span> <span class="key-badge">D</span> to steer.</p> | |
| <p>Avoid the red obstacles. Collect yellow energy.</p> | |
| <p style="font-size: 0.8rem; margin-top: 10px; color: #888;">(Mobile: Tap left/right sides of screen)</p> | |
| </div> | |
| <button class="btn-primary" id="startBtn">Start Engine</button> | |
| </div> | |
| <!-- Game Over Screen --> | |
| <div id="gameOverScreen" class="overlay hidden"> | |
| <h1 style="color: var(--secondary); -webkit-text-fill-color: var(--secondary);">CRASHED!</h1> | |
| <div class="instructions"> | |
| <p>Final Score: <span id="finalScore" style="color: white; font-weight: bold;">0</span></p> | |
| <p>High Score: <span id="highScore" style="color: var(--accent);">0</span></p> | |
| </div> | |
| <button class="btn-primary" id="restartBtn">Try Again</button> | |
| </div> | |
| <!-- Mobile Controls --> | |
| <div id="mobile-controls"> | |
| <div class="control-btn" id="btnLeft">←</div> | |
| <div class="control-btn" id="btnRight">→</div> | |
| </div> | |
| </main> | |
| <script> | |
| /** | |
| * AUDIO SYSTEM (Web Audio API) | |
| * Synthesizes sounds locally without external assets. | |
| */ | |
| const AudioSys = { | |
| ctx: null, | |
| init: function() { | |
| window.AudioContext = window.AudioContext || window.webkitAudioContext; | |
| this.ctx = new AudioContext(); | |
| }, | |
| playTone: function(freq, type, duration, vol = 0.1) { | |
| if (!this.ctx) return; | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = type; | |
| osc.frequency.setValueAtTime(freq, this.ctx.currentTime); | |
| gain.gain.setValueAtTime(vol, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration); | |
| osc.connect(gain); | |
| gain.connect(this.ctx.destination); | |
| osc.start(); | |
| osc.stop(this.ctx.currentTime + duration); | |
| }, | |
| playCrash: function() { | |
| this.playTone(100, 'sawtooth', 0.5, 0.3); | |
| this.playTone(50, 'square', 0.5, 0.3); | |
| }, | |
| playCollect: function() { | |
| this.playTone(800, 'sine', 0.1, 0.1); | |
| setTimeout(() => this.playTone(1200, 'sine', 0.2, 0.1), 50); | |
| }, | |
| playEngine: function() { | |
| // Simple hum for engine (simulated by random noise usually, but keeping it simple here) | |
| } | |
| }; | |
| /** | |
| * GAME ENGINE & LOGIC | |
| */ | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // UI Elements | |
| const startScreen = document.getElementById('startScreen'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const hud = document.getElementById('hud'); | |
| const scoreDisplay = document.getElementById('scoreDisplay'); | |
| const speedDisplay = document.getElementById('speedDisplay'); | |
| const finalScoreDisplay = document.getElementById('finalScore'); | |
| const highScoreDisplay = document.getElementById('highScore'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const restartBtn = document.getElementById('restartBtn'); | |
| const mobileControls = document.getElementById('mobile-controls'); | |
| const btnLeft = document.getElementById('btnLeft'); | |
| const btnRight = document.getElementById('btnRight'); | |
| // Game State | |
| let gameRunning = false; | |
| let score = 0; | |
| let highScore = localStorage.getItem('neonRacerHighScore') || 0; | |
| let speed = 5; | |
| let roadOffset = 0; | |
| let animationId; | |
| let lastTime = 0; | |
| // Dimensions | |
| let canvasWidth, canvasHeight; | |
| // Entities | |
| const player = { | |
| x: 0, | |
| y: 0, | |
| width: 40, | |
| height: 70, | |
| color: '#00f3ff', | |
| speedX: 0, | |
| maxSpeedX: 7, | |
| tilt: 0 | |
| }; | |
| let obstacles = []; | |
| let particles = []; | |
| let roadLines = []; | |
| // Inputs | |
| const keys = { | |
| ArrowLeft: false, | |
| ArrowRight: false, | |
| KeyA: false, | |
| KeyD: false | |
| }; | |
| // Resize Handling | |
| function resize() { | |
| canvasWidth = window.innerWidth; | |
| canvasHeight = window.innerHeight; | |
| canvas.width = canvasWidth; | |
| canvas.height = canvasHeight; | |
| // Reposition player on resize if game hasn't started | |
| if (!gameRunning) { | |
| player.x = canvasWidth / 2 - player.width / 2; | |
| player.y = canvasHeight - 150; | |
| } | |
| } | |
| window.addEventListener('resize', resize); | |
| // Input Listeners | |
| window.addEventListener('keydown', (e) => { | |
| if (keys.hasOwnProperty(e.code)) keys[e.code] = true; | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| if (keys.hasOwnProperty(e.code)) keys[e.code] = false; | |
| }); | |
| // Mobile Touch | |
| const handleTouch = (dir) => (e) => { | |
| e.preventDefault(); // Prevent scroll | |
| if (dir === 'left') keys.ArrowLeft = true; | |
| if (dir === 'right') keys.ArrowRight = true; | |
| }; | |
| const handleTouchEnd = (dir) => (e) => { | |
| e.preventDefault(); | |
| if (dir === 'left') keys.ArrowLeft = false; | |
| if (dir === 'right') keys.ArrowRight = false; | |
| }; | |
| if ('ontouchstart' in window) { | |
| mobileControls.style.display = 'flex'; | |
| btnLeft.addEventListener('touchstart', handleTouch('left')); | |
| btnLeft.addEventListener('touchend', handleTouchEnd('left')); | |
| btnRight.addEventListener('touchstart', handleTouch('right')); | |
| btnRight.addEventListener('touchend', handleTouchEnd('right')); | |
| } | |
| // Game Functions | |
| function initGame() { | |
| resize(); | |
| player.x = canvasWidth / 2 - player.width / 2; | |
| player.y = canvasHeight - 150; | |
| obstacles = []; | |
| particles = []; | |
| roadLines = []; | |
| score = 0; | |
| speed = 5; | |
| gameRunning = true; | |
| // Create initial road lines | |
| for(let i=0; i<10; i++) { | |
| roadLines.push({ y: i * 100 }); | |
| } | |
| startScreen.classList.add('hidden'); | |
| gameOverScreen.classList.add('hidden'); | |
| hud.style.display = 'flex'; | |
| AudioSys.init(); | |
| lastTime = performance.now(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function createObstacle() { | |
| const laneWidth = canvasWidth / 4; // 4 lanes roughly | |
| const lane = Math.floor(Math.random() * 4); | |
| const obsWidth = 40; | |
| const obsHeight = 70; | |
| const obsX = (lane * laneWidth) + (laneWidth/2) - (obsWidth/2) + 20; // Center in lane with margin | |
| // 20% chance for bonus coin | |
| const isCoin = Math.random() > 0.8; | |
| obstacles.push({ | |
| x: obsX, | |
| y: -100, | |
| width: obsWidth, | |
| height: obsHeight, | |
| color: isCoin ? '#ffee00' : '#ff0055', | |
| type: isCoin ? 'coin' : 'obstacle', | |
| rotation: 0 | |
| }); | |
| } | |
| function createExplosion(x, y, color) { | |
| for (let i = 0; i < 20; i++) { | |
| particles.push({ | |
| x: x, | |
| y: y, | |
| vx: (Math.random() - 0.5) * 10, | |
| vy: (Math.random() - 0.5) * 10, | |
| life: 1.0, | |
| color: color | |
| }); | |
| } | |
| } | |
| function update(dt) { | |
| // Player Movement | |
| if (keys.ArrowLeft || keys.KeyA) { | |
| player.x -= player.maxSpeedX; | |
| player.tilt = -15; // degrees | |
| } else if (keys.ArrowRight || keys.KeyD) { | |
| player.x += player.maxSpeedX; | |
| player.tilt = 15; | |
| } else { | |
| player.tilt = 0; | |
| } | |
| // Boundaries | |
| if (player.x < 0) player.x = 0; | |
| if (player.x + player.width > canvasWidth) player.x = canvasWidth - player.width; | |
| // Road Animation | |
| roadOffset += speed; | |
| if (roadOffset >= 100) roadOffset = 0; | |
| // Spawn Obstacles | |
| if (Math.random() < 0.02) { // Adjust spawn rate based on speed | |
| createObstacle(); | |
| } | |
| // Update Obstacles | |
| for (let i = obstacles.length - 1; i >= 0; i--) { | |
| let obs = obstacles[i]; | |
| obs.y += speed; | |
| obs.rotation += 0.05; | |
| // Collision Detection (AABB) | |
| if ( | |
| player.x < obs.x + obs.width && | |
| player.x + player.width > obs.x && | |
| player.y < obs.y + obs.height && | |
| player.y + player.height > obs.y | |
| ) { | |
| if (obs.type === 'coin') { | |
| // Collect Coin | |
| score += 100; | |
| createExplosion(obs.x + obs.width/2, obs.y + obs.height/2, '#ffee00'); | |
| AudioSys.playCollect(); | |
| obstacles.splice(i, 1); | |
| } else { | |
| // Crash | |
| createExplosion(player.x + player.width/2, player.y + player.height/2, '#ff0055'); | |
| AudioSys.playCrash(); | |
| gameOver(); | |
| } | |
| } else if (obs.y > canvasHeight) { | |
| obstacles.splice(i, 1); | |
| score += 10; // Points for passing | |
| } | |
| } | |
| // Update Particles | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| let p = particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.life -= 0.02; | |
| if (p.life <= 0) particles.splice(i, 1); | |
| } | |
| // Increase Difficulty | |
| speed = 5 + (score / 500); // Cap speed at reasonable level | |
| score++; | |
| // Update UI | |
| scoreDisplay.innerText = score; | |
| speedDisplay.innerText = Math.floor(speed * 20) + " km/h"; | |
| } | |
| function draw() { | |
| // Clear Screen | |
| ctx.fillStyle = '#050510'; | |
| ctx.fillRect(0, 0, canvasWidth, canvasHeight); | |
| // Draw Road (Perspective effect simplified to top-down for clarity) | |
| ctx.fillStyle = '#111'; | |
| ctx.fillRect(0, 0, canvasWidth, canvasHeight); | |
| // Draw Road Borders | |
| ctx.strokeStyle = '#00f3ff'; | |
| ctx.lineWidth = 4; | |
| ctx.beginPath(); | |
| ctx.moveTo(canvasWidth * 0.1, 0); | |
| ctx.lineTo(canvasWidth * 0.1, canvasHeight); | |
| ctx.moveTo(canvasWidth * 0.9, 0); | |
| ctx.lineTo(canvasWidth * 0.9, canvasHeight); | |
| ctx.stroke(); | |
| // Draw Moving Lines | |
| ctx.strokeStyle = 'rgba(0, 243, 255, 0.3)'; | |
| ctx.lineWidth = 2; | |
| roadLines.forEach(line => { | |
| line.y += speed; | |
| if (line.y > canvasHeight) line.y = -100; | |
| ctx.beginPath(); | |
| ctx.moveTo(canvasWidth * 0.3, line.y); | |
| ctx.lineTo(canvasWidth * 0.3, line.y + 50); | |
| ctx.moveTo(canvasWidth * 0.7, line.y); | |
| ctx.lineTo(canvasWidth * 0.7, line.y + 50); | |
| ctx.stroke(); | |
| }); | |
| // Draw Player Car | |
| ctx.save(); | |
| ctx.translate(player.x + player.width / 2, player.y + player.height / 2); | |
| ctx.rotate(player.tilt * Math.PI / 180); | |
| // Car Body | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = player.color; | |
| ctx.fillStyle = player.color; | |
| ctx.fillRect(-player.width/2, -player.height/2, player.width, player.height); | |
| // Car Detail (Windshield) | |
| ctx.fillStyle = '#000'; | |
| ctx.fillRect(-player.width/2 + 5, -player.height/2 + 10, player.width - 10, 15); | |
| // Engine Glow | |
| ctx.fillStyle = '#ff0055'; | |
| ctx.fillRect(-player.width/2 + 5, player.height/2 - 5, player.width - 10, 5); | |
| ctx.restore(); | |
| // Draw Obstacles | |
| obstacles.forEach(obs => { | |
| ctx.save(); | |
| ctx.translate(obs.x + obs.width/2, obs.y + obs.height/2); | |
| ctx.rotate(obs.rotation); | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = obs.color; | |
| ctx.fillStyle = obs.color; | |
| // Draw shape based on type | |
| if (obs.type === 'coin') { | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, obs.width/2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Shine | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(-5, -5, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } else { | |
| ctx.fillRect(-obs.width/2, -obs.height/2, obs.width, obs.height); | |
| // Inner detail | |
| ctx.fillStyle = 'rgba(0,0,0,0.5)'; | |
| ctx.fillRect(-obs.width/2 + 5, -obs.height/2 + 5, obs.width - 10, obs.height - 10); | |
| } | |
| ctx.restore(); | |
| }); | |
| // Draw Particles | |
| particles.forEach(p => { | |
| ctx.globalAlpha = p.life; | |
| ctx.fillStyle = p.color; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.globalAlpha = 1.0; | |
| }); | |
| } | |
| function gameLoop(timestamp) { | |
| if (!gameRunning) return; | |
| const dt = timestamp - lastTime; | |
| lastTime = timestamp; | |
| update(dt); | |
| draw(); | |
| animationId = requestAnimationFrame(gameLoop); | |
| } | |
| function gameOver() { | |
| gameRunning = false; | |
| cancelAnimationFrame(animationId); | |
| // Update High Score | |
| if (score > highScore) { | |
| highScore = score; | |
| localStorage.setItem('neonRacerHighScore', highScore); | |
| } | |
| finalScoreDisplay.innerText = score; | |
| highScoreDisplay.innerText = highScore; | |
| hud.style.display = 'none'; | |
| gameOverScreen.classList.remove('hidden'); | |
| } | |
| // Button Listeners | |
| startBtn.addEventListener('click', initGame); | |
| restartBtn.addEventListener('click', initGame); | |
| // Initial Draw to show something before start | |
| resize(); | |
| // Draw a static frame | |
| ctx.fillStyle = '#050510'; | |
| ctx.fillRect(0, 0, canvasWidth, canvasHeight); | |
| ctx.fillStyle = '#00f3ff'; | |
| ctx.font = '20px Orbitron'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText("READY TO RACE?", canvasWidth/2, canvasHeight/2); | |
| </script> | |
| </body> | |
| </html> |