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>Color Toss - Premium</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;700;900&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #ff0055; | |
| --bg: #0f172a; | |
| } | |
| body { | |
| margin: 0; padding: 0; | |
| background-color: var(--bg); | |
| color: white; | |
| font-family: 'Outfit', sans-serif; | |
| overflow: hidden; | |
| touch-action: none; | |
| user-select: none; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| background: radial-gradient(circle at center, #1e293b 0%, #0f172a 100%); | |
| } | |
| canvas { | |
| display: block; | |
| background: transparent; | |
| } | |
| .ui-screen { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(15, 23, 42, 0.85); | |
| backdrop-filter: blur(12px); | |
| z-index: 50; | |
| transition: opacity 0.3s ease; | |
| } | |
| .menu-card { | |
| background: linear-gradient(145deg, #1e293b, #0f172a); | |
| padding: 2.5rem 2rem; | |
| border-radius: 2.5rem; | |
| text-align: center; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| width: 85%; | |
| max-width: 360px; | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); | |
| } | |
| .btn { | |
| width: 100%; | |
| padding: 16px; | |
| border-radius: 1rem; | |
| font-weight: 700; | |
| margin-bottom: 0.75rem; | |
| transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .btn-primary { | |
| background: var(--primary); | |
| color: white; | |
| box-shadow: 0 10px 20px -5px rgba(255, 0, 85, 0.4); | |
| } | |
| .btn-secondary { | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| color: #cbd5e1; | |
| } | |
| .btn:active { transform: scale(0.95); opacity: 0.9; } | |
| .diff-btn { | |
| flex: 1; | |
| padding: 10px 5px; | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 0.75rem; | |
| margin: 0 4px; | |
| background: rgba(255, 255, 255, 0.03); | |
| color: #94a3b8; | |
| } | |
| .diff-btn.active { | |
| background: white; | |
| color: black; | |
| border-color: white; | |
| } | |
| #hud { | |
| position: absolute; | |
| top: env(safe-area-inset-top, 20px); | |
| left: 0; | |
| width: 100%; | |
| padding: 0 24px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| .hud-btn { | |
| pointer-events: auto; | |
| background: rgba(255,255,255,0.05); | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 14px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .tap-hint { | |
| position: absolute; | |
| bottom: 20%; | |
| width: 100%; | |
| text-align: center; | |
| color: rgba(255,255,255,0.4); | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| animation: pulse 1.5s infinite; | |
| pointer-events: none; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 0.3; transform: scale(1); } | |
| 50% { opacity: 0.8; transform: scale(1.05); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="hud" style="display: none;"> | |
| <div id="score-display" class="text-5xl font-black italic tracking-tighter">0</div> | |
| <button class="hud-btn" onclick="togglePause()"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="white"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg> | |
| </button> | |
| </div> | |
| <div id="tap-to-start" class="tap-hint" style="display: none;">TAP TO JUMP</div> | |
| <!-- Home Menu --> | |
| <div id="home-menu" class="ui-screen"> | |
| <div class="menu-card"> | |
| <h1 class="text-6xl font-black italic mb-1 tracking-tighter leading-none">COLOR<br><span style="color: var(--primary)">TOSS</span></h1> | |
| <p class="text-slate-500 mb-10 text-sm font-medium">PREMIUM EDITION</p> | |
| <div class="mb-8"> | |
| <p class="text-[10px] uppercase text-slate-400 mb-3 font-black tracking-widest">Difficulty Level</p> | |
| <div class="flex bg-black/20 p-1 rounded-xl"> | |
| <button class="diff-btn" onclick="setDifficulty('easy')" id="diff-easy">EASY</button> | |
| <button class="diff-btn active" onclick="setDifficulty('normal')" id="diff-normal">NORMAL</button> | |
| <button class="diff-btn" onclick="setDifficulty('hard')" id="diff-hard">HARD</button> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary" onclick="startNewGame()"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg> | |
| New Game | |
| </button> | |
| <button class="btn btn-secondary" id="resume-btn" onclick="loadSavedGame()" style="display: none;"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg> | |
| Continue | |
| </button> | |
| </div> | |
| <p class="absolute bottom-8 text-slate-600 text-[10px] font-bold tracking-widest uppercase">Best Score: <span id="high-score-text">0</span></p> | |
| </div> | |
| <!-- Pause Menu --> | |
| <div id="pause-menu" class="ui-screen" style="display: none;"> | |
| <div class="menu-card"> | |
| <h2 class="text-3xl font-black mb-8 italic tracking-tight">PAUSED</h2> | |
| <button class="btn btn-primary" onclick="togglePause()">Resume Game</button> | |
| <button class="btn btn-secondary" onclick="saveAndExit()">Save & Exit</button> | |
| </div> | |
| </div> | |
| <!-- Game Over --> | |
| <div id="game-over" class="ui-screen" style="display: none;"> | |
| <div class="menu-card"> | |
| <div class="text-pink-500 mb-2"> | |
| <svg class="mx-auto" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> | |
| </div> | |
| <h2 class="text-3xl font-black mb-1">CRASHED!</h2> | |
| <p id="final-score" class="text-slate-400 font-bold mb-8">Score: 0</p> | |
| <button class="btn btn-primary" onclick="startNewGame()">Try Again</button> | |
| <button class="btn btn-secondary" onclick="showHome()">Main Menu</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const COLORS = ['#FF0055', '#00DDFF', '#FFCC00', '#AA00FF']; | |
| const CANVAS_WIDTH = 400; | |
| const CANVAS_HEIGHT = 700; | |
| let state = { | |
| mode: 'MENU', | |
| waitingForFirstTap: true, | |
| difficulty: 'normal', | |
| score: 0, | |
| highScore: parseInt(localStorage.getItem('ct_best') || 0), | |
| cameraY: 0, | |
| player: { x: 200, y: 550, vy: 0, radius: 12, color: COLORS[0], floatOffset: 0 }, | |
| obstacles: [], | |
| switchers: [] | |
| }; | |
| let audioCtx; | |
| const difficultySettings = { | |
| easy: { speed: 0.02, gravity: 0.22, jump: -5.2 }, | |
| normal: { speed: 0.038, gravity: 0.28, jump: -6.0 }, | |
| hard: { speed: 0.06, gravity: 0.34, jump: -6.8 } | |
| }; | |
| function initAudio() { | |
| if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| function playSound(f, t, d, v = 0.06) { | |
| if (!audioCtx) return; | |
| const o = audioCtx.createOscillator(); | |
| const g = audioCtx.createGain(); | |
| o.type = t; o.frequency.value = f; | |
| g.gain.setValueAtTime(v, audioCtx.currentTime); | |
| g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + d); | |
| o.connect(g); g.connect(audioCtx.destination); | |
| o.start(); o.stop(audioCtx.currentTime + d); | |
| } | |
| class Obstacle { | |
| constructor(y, angle = 0, passed = false) { | |
| this.y = y; | |
| this.radius = 85; | |
| this.angle = angle; | |
| this.thickness = 18; | |
| this.passed = passed; | |
| } | |
| draw() { | |
| const speed = difficultySettings[state.difficulty].speed; | |
| this.angle += speed; | |
| const seg = (Math.PI * 2) / 4; | |
| ctx.lineWidth = this.thickness; | |
| ctx.lineCap = 'round'; | |
| for (let i = 0; i < 4; i++) { | |
| ctx.beginPath(); | |
| ctx.strokeStyle = COLORS[i]; | |
| // Added a small gap for a cleaner look | |
| const start = this.angle + i*seg + 0.05; | |
| const end = this.angle + (i+1)*seg - 0.05; | |
| ctx.arc(CANVAS_WIDTH/2, this.y - state.cameraY, this.radius, start, end); | |
| ctx.stroke(); | |
| } | |
| } | |
| check(px, py, pr, pc) { | |
| const dx = px - CANVAS_WIDTH/2, dy = py - this.y; | |
| const d = Math.sqrt(dx*dx + dy*dy); | |
| if (d + pr > this.radius - this.thickness/2 && d - pr < this.radius + this.thickness/2) { | |
| let a = Math.atan2(dy, dx) - this.angle; | |
| while(a < 0) a += Math.PI * 2; | |
| const index = Math.floor((a % (Math.PI*2)) / (Math.PI/2)); | |
| if (COLORS[index] !== pc) return true; | |
| } | |
| return false; | |
| } | |
| } | |
| function setDifficulty(d) { | |
| state.difficulty = d; | |
| document.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active')); | |
| document.getElementById('diff-' + d).classList.add('active'); | |
| playSound(400, 'sine', 0.1); | |
| } | |
| function startNewGame() { | |
| initAudio(); | |
| state.score = 0; | |
| state.cameraY = 0; | |
| state.waitingForFirstTap = true; | |
| state.player = { | |
| x: 200, y: 550, vy: 0, radius: 12, | |
| color: COLORS[Math.floor(Math.random()*4)], | |
| floatOffset: 0 | |
| }; | |
| state.obstacles = []; | |
| state.switchers = []; | |
| for(let i=0; i<3; i++) spawn(250 - i*350); | |
| launch(); | |
| } | |
| function spawn(y) { | |
| state.obstacles.push(new Obstacle(y)); | |
| state.switchers.push({ y: y - 175, radius: 16, active: true, angle: 0 }); | |
| } | |
| function launch() { | |
| state.mode = 'PLAYING'; | |
| document.querySelectorAll('.ui-screen').forEach(s => s.style.display = 'none'); | |
| document.getElementById('hud').style.display = 'flex'; | |
| document.getElementById('tap-to-start').style.display = 'block'; | |
| document.getElementById('score-display').innerText = state.score; | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function togglePause() { | |
| if (state.mode === 'PLAYING') { | |
| state.mode = 'PAUSED'; | |
| document.getElementById('pause-menu').style.display = 'flex'; | |
| } else if (state.mode === 'PAUSED') { | |
| state.mode = 'PLAYING'; | |
| document.getElementById('pause-menu').style.display = 'none'; | |
| requestAnimationFrame(gameLoop); | |
| } | |
| } | |
| function saveAndExit() { | |
| const saveData = { | |
| score: state.score, | |
| cameraY: state.cameraY, | |
| player: state.player, | |
| difficulty: state.difficulty, | |
| waitingForFirstTap: state.waitingForFirstTap, | |
| obstacles: state.obstacles.map(o => ({y: o.y, angle: o.angle, passed: o.passed})), | |
| switchers: state.switchers | |
| }; | |
| localStorage.setItem('ct_save', JSON.stringify(saveData)); | |
| showHome(); | |
| } | |
| function loadSavedGame() { | |
| const raw = localStorage.getItem('ct_save'); | |
| if (!raw) return; | |
| const d = JSON.parse(raw); | |
| state = {...state, ...d}; | |
| state.obstacles = d.obstacles.map(o => new Obstacle(o.y, o.angle, o.passed)); | |
| localStorage.removeItem('ct_save'); | |
| launch(); | |
| } | |
| function showHome() { | |
| state.mode = 'MENU'; | |
| document.querySelectorAll('.ui-screen').forEach(s => s.style.display = 'none'); | |
| document.getElementById('home-menu').style.display = 'flex'; | |
| document.getElementById('hud').style.display = 'none'; | |
| document.getElementById('tap-to-start').style.display = 'none'; | |
| document.getElementById('high-score-text').innerText = state.highScore; | |
| const hasSave = localStorage.getItem('ct_save'); | |
| document.getElementById('resume-btn').style.display = hasSave ? 'flex' : 'none'; | |
| } | |
| function gameLoop() { | |
| if (state.mode !== 'PLAYING') return; | |
| ctx.clearRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); | |
| const config = difficultySettings[state.difficulty]; | |
| // Physics & Start Logic | |
| if (state.waitingForFirstTap) { | |
| // Floating effect | |
| state.player.floatOffset += 0.05; | |
| const floatY = state.player.y + Math.sin(state.player.floatOffset) * 10; | |
| drawPlayer(state.player.x, floatY); | |
| } else { | |
| document.getElementById('tap-to-start').style.display = 'none'; | |
| state.player.vy += config.gravity; | |
| state.player.y += state.player.vy; | |
| if (state.player.y < state.cameraY + 380) state.cameraY = state.player.y - 380; | |
| if (state.player.y - state.cameraY > CANVAS_HEIGHT + 50) { endGame(); return; } | |
| drawPlayer(state.player.x, state.player.y); | |
| } | |
| // Draw Obstacles | |
| state.obstacles.forEach((o, i) => { | |
| o.draw(); | |
| if (!state.waitingForFirstTap) { | |
| if (!o.passed && state.player.y < o.y) { | |
| o.passed = true; state.score++; | |
| document.getElementById('score-display').innerText = state.score; | |
| playSound(900, 'sine', 0.1); | |
| } | |
| if (o.check(state.player.x, state.player.y, state.player.radius, state.player.color)) endGame(); | |
| } | |
| if (o.y - state.cameraY > CANVAS_HEIGHT + 150) { | |
| state.obstacles.splice(i, 1); | |
| spawn(state.obstacles[state.obstacles.length-1].y - 350); | |
| } | |
| }); | |
| // Color Switchers | |
| state.switchers.forEach((s, i) => { | |
| if (!s.active) return; | |
| s.angle += 0.02; | |
| const seg = (Math.PI*2)/4; | |
| ctx.save(); | |
| ctx.translate(CANVAS_WIDTH/2, s.y - state.cameraY); | |
| ctx.rotate(s.angle); | |
| for(let j=0; j<4; j++){ | |
| ctx.beginPath(); ctx.fillStyle = COLORS[j]; ctx.moveTo(0,0); | |
| ctx.arc(0, 0, s.radius, j*seg, (j+1)*seg); ctx.fill(); | |
| } | |
| ctx.restore(); | |
| if (!state.waitingForFirstTap) { | |
| const dx = state.player.x - CANVAS_WIDTH/2, dy = state.player.y - s.y; | |
| if (Math.sqrt(dx*dx + dy*dy) < state.player.radius + s.radius) { | |
| s.active = false; | |
| let nc; do { nc = COLORS[Math.floor(Math.random()*4)]; } while(nc === state.player.color); | |
| state.player.color = nc; | |
| playSound(600, 'triangle', 0.2, 0.1); | |
| } | |
| } | |
| }); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function drawPlayer(x, y) { | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.fillStyle = state.player.color; | |
| ctx.shadowBlur = 25; ctx.shadowColor = state.player.color; | |
| ctx.arc(x, y - state.cameraY, state.player.radius, 0, Math.PI*2); | |
| ctx.fill(); | |
| // Inner detail | |
| ctx.beginPath(); | |
| ctx.fillStyle = 'rgba(255,255,255,0.3)'; | |
| ctx.arc(x - 3, (y - state.cameraY) - 3, 4, 0, Math.PI*2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function endGame() { | |
| state.mode = 'GAMEOVER'; | |
| playSound(150, 'sawtooth', 0.3); | |
| document.getElementById('game-over').style.display = 'flex'; | |
| document.getElementById('final-score').innerText = "Score: " + state.score; | |
| if (state.score > state.highScore) { | |
| state.highScore = state.score; | |
| localStorage.setItem('ct_best', state.highScore); | |
| } | |
| } | |
| const handleInput = (e) => { | |
| if (state.mode === 'PLAYING') { | |
| if (state.waitingForFirstTap) { | |
| state.waitingForFirstTap = false; | |
| } | |
| state.player.vy = difficultySettings[state.difficulty].jump; | |
| playSound(500, 'sine', 0.06); | |
| } | |
| }; | |
| window.addEventListener('mousedown', e => { if(e.target.closest('button')) return; handleInput(); }); | |
| window.addEventListener('touchstart', e => { if(e.target.closest('button')) return; e.preventDefault(); handleInput(); }, {passive:false}); | |
| window.addEventListener('keydown', e => { if(e.code === 'Space') handleInput(); }); | |
| function resize() { | |
| const s = Math.min(window.innerWidth/CANVAS_WIDTH, window.innerHeight/CANVAS_HEIGHT); | |
| canvas.width = CANVAS_WIDTH; canvas.height = CANVAS_HEIGHT; | |
| canvas.style.width = (CANVAS_WIDTH*s)+'px'; canvas.style.height = (CANVAS_HEIGHT*s)+'px'; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| showHome(); | |
| </script> | |
| </body> | |
| </html> |