Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SlitherSphere 3D</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
| body { | |
| font-family: 'Press Start 2P', cursive; | |
| overflow: hidden; | |
| touch-action: none; | |
| } | |
| #game-container { | |
| perspective: 1000px; | |
| transform-style: preserve-3d; | |
| } | |
| .snake-segment { | |
| box-shadow: 0 0 15px rgba(110, 231, 183, 0.7); | |
| transition: all 0.2s ease-out; | |
| } | |
| .food { | |
| animation: pulse 1.5s infinite alternate; | |
| box-shadow: 0 0 20px rgba(236, 72, 153, 0.8); | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1) translateZ(0); } | |
| 100% { transform: scale(1.2) translateZ(10px); } | |
| } | |
| #vanta-bg { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| opacity: 0.3; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-teal-300 min-h-screen flex flex-col items-center justify-center"> | |
| <div id="vanta-bg"></div> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-4xl md:text-6xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-teal-400 to-pink-500"> | |
| SLITHERSPHERE 3D | |
| </h1> | |
| <div class="flex justify-center gap-8 text-xl"> | |
| <div> | |
| <p class="text-pink-400">SCORE</p> | |
| <p id="score" class="text-teal-300">0</p> | |
| </div> | |
| <div> | |
| <p class="text-pink-400">HIGH SCORE</p> | |
| <p id="high-score" class="text-teal-300">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="game-container" class="relative w-full max-w-2xl aspect-square bg-gray-800 rounded-xl overflow-hidden shadow-2xl shadow-black/50"> | |
| <canvas id="game-canvas" class="w-full h-full"></canvas> | |
| <div id="game-over" class="absolute inset-0 bg-black/80 flex flex-col items-center justify-center hidden"> | |
| <h2 class="text-4xl text-pink-500 mb-6">GAME OVER</h2> | |
| <p class="text-xl text-teal-300 mb-8">Score: <span id="final-score">0</span></p> | |
| <button id="restart-btn" class="px-8 py-3 bg-gradient-to-r from-teal-500 to-pink-500 rounded-full font-bold hover:scale-105 transition-transform"> | |
| PLAY AGAIN | |
| </button> | |
| </div> | |
| <div id="start-screen" class="absolute inset-0 bg-black/80 flex flex-col items-center justify-center"> | |
| <h2 class="text-4xl text-teal-300 mb-6">SLITHERSPHERE</h2> | |
| <p class="text-pink-400 mb-8 text-center px-4">Use arrow keys or swipe to control the snake</p> | |
| <button id="start-btn" class="px-8 py-3 bg-gradient-to-r from-teal-500 to-pink-500 rounded-full font-bold hover:scale-105 transition-transform"> | |
| START GAME | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-8 flex gap-4"> | |
| <button id="sound-btn" class="p-3 bg-gray-800 rounded-full hover:bg-gray-700 transition-colors"> | |
| <i data-feather="volume-2" class="text-teal-400"></i> | |
| </button> | |
| <button id="fullscreen-btn" class="p-3 bg-gray-800 rounded-full hover:bg-gray-700 transition-colors"> | |
| <i data-feather="maximize" class="text-teal-400"></i> | |
| </button> | |
| </div> | |
| <audio id="eat-sound" src="https://assets.mixkit.co/sfx/preview/mixkit-arcade-game-jump-coin-216.mp3" preload="auto"></audio> | |
| <audio id="gameover-sound" src="https://assets.mixkit.co/sfx/preview/mixkit-retro-arcade-lose-2027.mp3" preload="auto"></audio> | |
| <audio id="bg-music" loop src="https://assets.mixkit.co/music/preview/mixkit-game-show-suspense-waiting-668.mp3" preload="auto"></audio> | |
| <script> | |
| // Initialize Vanta.js background | |
| VANTA.GLOBE({ | |
| el: "#vanta-bg", | |
| mouseControls: true, | |
| touchControls: true, | |
| gyroControls: false, | |
| minHeight: 200.00, | |
| minWidth: 200.00, | |
| scale: 1.00, | |
| scaleMobile: 1.00, | |
| color: 0x3b82f6, | |
| backgroundColor: 0x111827, | |
| size: 0.8 | |
| }); | |
| // Game variables | |
| const canvas = document.getElementById('game-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const scoreElement = document.getElementById('score'); | |
| const highScoreElement = document.getElementById('high-score'); | |
| const finalScoreElement = document.getElementById('final-score'); | |
| const gameOverScreen = document.getElementById('game-over'); | |
| const startScreen = document.getElementById('start-screen'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| const soundBtn = document.getElementById('sound-btn'); | |
| const fullscreenBtn = document.getElementById('fullscreen-btn'); | |
| const eatSound = document.getElementById('eat-sound'); | |
| const gameoverSound = document.getElementById('gameover-sound'); | |
| const bgMusic = document.getElementById('bg-music'); | |
| let gridSize = 20; | |
| let tileSize; | |
| let snake = []; | |
| let food = {}; | |
| let direction = 'right'; | |
| let nextDirection = 'right'; | |
| let gameSpeed = 150; | |
| let score = 0; | |
| let highScore = localStorage.getItem('highScore') || 0; | |
| let gameInterval; | |
| let isPaused = false; | |
| let isSoundOn = true; | |
| let touchStartX = 0; | |
| let touchStartY = 0; | |
| // Set canvas size | |
| function resizeCanvas() { | |
| const container = document.getElementById('game-container'); | |
| const size = Math.min(container.clientWidth, container.clientHeight); | |
| canvas.width = size; | |
| canvas.height = size; | |
| tileSize = canvas.width / gridSize; | |
| } | |
| // Initialize game | |
| function initGame() { | |
| resizeCanvas(); | |
| // Create snake | |
| snake = []; | |
| for (let i = 3; i >= 0; i--) { | |
| snake.push({ x: i, y: 0 }); | |
| } | |
| // Create food | |
| generateFood(); | |
| // Reset score | |
| score = 0; | |
| scoreElement.textContent = score; | |
| highScoreElement.textContent = highScore; | |
| // Reset direction | |
| direction = 'right'; | |
| nextDirection = 'right'; | |
| // Hide game over screen | |
| gameOverScreen.classList.add('hidden'); | |
| // Start game loop | |
| if (gameInterval) clearInterval(gameInterval); | |
| gameInterval = setInterval(gameLoop, gameSpeed); | |
| // Play background music | |
| if (isSoundOn) { | |
| bgMusic.currentTime = 0; | |
| bgMusic.play().catch(e => console.log("Autoplay prevented:", e)); | |
| } | |
| } | |
| // Generate food at random position | |
| function generateFood() { | |
| let validPosition = false; | |
| while (!validPosition) { | |
| food = { | |
| x: Math.floor(Math.random() * gridSize), | |
| y: Math.floor(Math.random() * gridSize) | |
| }; | |
| validPosition = true; | |
| // Check if food is on snake | |
| for (let segment of snake) { | |
| if (segment.x === food.x && segment.y === food.y) { | |
| validPosition = false; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // Main game loop | |
| function gameLoop() { | |
| if (isPaused) return; | |
| // Move snake | |
| direction = nextDirection; | |
| let head = { ...snake[0] }; | |
| switch (direction) { | |
| case 'up': | |
| head.y--; | |
| break; | |
| case 'down': | |
| head.y++; | |
| break; | |
| case 'left': | |
| head.x--; | |
| break; | |
| case 'right': | |
| head.x++; | |
| break; | |
| } | |
| // Check wall collision | |
| if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) { | |
| gameOver(); | |
| return; | |
| } | |
| // Check self collision | |
| for (let segment of snake) { | |
| if (segment.x === head.x && segment.y === head.y) { | |
| gameOver(); | |
| return; | |
| } | |
| } | |
| // Add new head | |
| snake.unshift(head); | |
| // Check food collision | |
| if (head.x === food.x && head.y === food.y) { | |
| score++; | |
| scoreElement.textContent = score; | |
| if (score > highScore) { | |
| highScore = score; | |
| highScoreElement.textContent = highScore; | |
| localStorage.setItem('highScore', highScore); | |
| } | |
| if (isSoundOn) eatSound.play(); | |
| // Increase speed every 5 points | |
| if (score % 5 === 0) { | |
| clearInterval(gameInterval); | |
| gameSpeed = Math.max(50, gameSpeed - 10); | |
| gameInterval = setInterval(gameLoop, gameSpeed); | |
| } | |
| generateFood(); | |
| } else { | |
| // Remove tail if no food eaten | |
| snake.pop(); | |
| } | |
| // Draw game | |
| drawGame(); | |
| } | |
| // Draw game elements | |
| function drawGame() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid | |
| ctx.strokeStyle = 'rgba(75, 85, 99, 0.3)'; | |
| ctx.lineWidth = 0.5; | |
| for (let i = 0; i < gridSize; i++) { | |
| // Vertical lines | |
| ctx.beginPath(); | |
| ctx.moveTo(i * tileSize, 0); | |
| ctx.lineTo(i * tileSize, canvas.height); | |
| ctx.stroke(); | |
| // Horizontal lines | |
| ctx.beginPath(); | |
| ctx.moveTo(0, i * tileSize); | |
| ctx.lineTo(canvas.width, i * tileSize); | |
| ctx.stroke(); | |
| } | |
| // Draw food with 3D effect | |
| const foodX = food.x * tileSize + tileSize / 2; | |
| const foodY = food.y * tileSize + tileSize / 2; | |
| const foodRadius = tileSize * 0.4; | |
| // Food glow | |
| const foodGradient = ctx.createRadialGradient( | |
| foodX, foodY, 0, | |
| foodX, foodY, foodRadius * 1.5 | |
| ); | |
| foodGradient.addColorStop(0, 'rgba(236, 72, 153, 0.8)'); | |
| foodGradient.addColorStop(1, 'rgba(236, 72, 153, 0)'); | |
| ctx.fillStyle = foodGradient; | |
| ctx.beginPath(); | |
| ctx.arc(foodX, foodY, foodRadius * 1.5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Food main | |
| const foodMainGradient = ctx.createRadialGradient( | |
| foodX, foodY - foodRadius * 0.2, 0, | |
| foodX, foodY, foodRadius | |
| ); | |
| foodMainGradient.addColorStop(0, '#ec4899'); | |
| foodMainGradient.addColorStop(1, '#be185d'); | |
| ctx.fillStyle = foodMainGradient; | |
| ctx.beginPath(); | |
| ctx.arc(foodX, foodY, foodRadius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Food highlight | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| foodX - foodRadius * 0.3, | |
| foodY - foodRadius * 0.3, | |
| foodRadius * 0.2, | |
| 0, Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| // Draw snake with 3D effect | |
| for (let i = 0; i < snake.length; i++) { | |
| const segment = snake[i]; | |
| const x = segment.x * tileSize; | |
| const y = segment.y * tileSize; | |
| const size = tileSize * 0.9; | |
| const offset = (tileSize - size) / 2; | |
| const isHead = i === 0; | |
| // Snake segment shadow | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
| ctx.beginPath(); | |
| ctx.roundRect( | |
| x + offset + 3, | |
| y + offset + 3, | |
| size, | |
| size, | |
| [size * 0.2] | |
| ); | |
| ctx.fill(); | |
| // Snake segment main | |
| const segmentGradient = ctx.createLinearGradient( | |
| x, y, | |
| x + tileSize, y + tileSize | |
| ); | |
| if (isHead) { | |
| segmentGradient.addColorStop(0, '#2dd4bf'); | |
| segmentGradient.addColorStop(1, '#0d9488'); | |
| } else { | |
| const intensity = 1 - (i / snake.length) * 0.5; | |
| segmentGradient.addColorStop(0, `rgba(45, 212, 191, ${intensity})`); | |
| segmentGradient.addColorStop(1, `rgba(13, 148, 136, ${intensity})`); | |
| } | |
| ctx.fillStyle = segmentGradient; | |
| ctx.beginPath(); | |
| ctx.roundRect( | |
| x + offset, | |
| y + offset, | |
| size, | |
| size, | |
| [size * 0.2] | |
| ); | |
| ctx.fill(); | |
| // Snake segment highlight | |
| if (isHead) { | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| x + offset + size * 0.3, | |
| y + offset + size * 0.3, | |
| size * 0.15, | |
| 0, Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| // Eyes | |
| const eyeOffsetX = direction === 'left' ? -0.2 : | |
| direction === 'right' ? 0.2 : 0; | |
| const eyeOffsetY = direction === 'up' ? -0.2 : | |
| direction === 'down' ? 0.2 : 0; | |
| // Left eye | |
| ctx.fillStyle = 'white'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| x + offset + size * 0.5 + eyeOffsetX * size * 0.3, | |
| y + offset + size * 0.4 + eyeOffsetY * size * 0.3, | |
| size * 0.08, | |
| 0, Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| // Right eye | |
| ctx.beginPath(); | |
| ctx.arc( | |
| x + offset + size * 0.7 + eyeOffsetX * size * 0.3, | |
| y + offset + size * 0.4 + eyeOffsetY * size * 0.3, | |
| size * 0.08, | |
| 0, Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| // Pupils | |
| ctx.fillStyle = 'black'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| x + offset + size * 0.5 + eyeOffsetX * size * 0.35, | |
| y + offset + size * 0.4 + eyeOffsetY * size * 0.35, | |
| size * 0.04, | |
| 0, Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| ctx.beginPath(); | |
| ctx.arc( | |
| x + offset + size * 0.7 + eyeOffsetX * size * 0.35, | |
| y + offset + size * 0.4 + eyeOffsetY * size * 0.35, | |
| size * 0.04, | |
| 0, Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| } | |
| } | |
| } | |
| // Game over | |
| function gameOver() { | |
| clearInterval(gameInterval); | |
| gameOverScreen.classList.remove('hidden'); | |
| finalScoreElement.textContent = score; | |
| if (isSoundOn) { | |
| bgMusic.pause(); | |
| gameoverSound.play(); | |
| } | |
| } | |
| // Event listeners | |
| window.addEventListener('resize', () => { | |
| resizeCanvas(); | |
| drawGame(); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| switch (e.key) { | |
| case 'ArrowUp': | |
| if (direction !== 'down') nextDirection = 'up'; | |
| break; | |
| case 'ArrowLeft': | |
| if (direction !== 'right') nextDirection = 'left'; | |
| break; | |
| case 'ArrowRight': | |
| if (direction !== 'left') nextDirection = 'right'; | |
| break; | |
| case 'ArrowDown': | |
| if (direction !== 'up') nextDirection = 'down'; | |
| break; | |
| case ' ': | |
| isPaused = !isPaused; | |
| if (isPaused) { | |
| bgMusic.pause(); | |
| } else if (isSoundOn) { | |
| bgMusic.play().catch(e => console.log("Autoplay prevented:", e)); | |
| } | |
| break; | |
| } | |
| }); | |
| // Touch controls for mobile | |
| canvas.addEventListener('touchstart', (e) => { | |
| touchStartX = e.touches[0].clientX; | |
| touchStartY = e.touches[0].clientY; | |
| }, { passive: false }); | |
| canvas.addEventListener('touchmove', (e) => { | |
| if (!touchStartX || !touchStartY) return; | |
| const touchEndX = e.touches[0].clientX; | |
| const touchEndY = e.touches[0].clientY; | |
| const diffX = touchStartX - touchEndX; | |
| const diffY = touchStartY - touchEndY; | |
| if (Math.abs(diffX) > Math.abs(diffY)) { | |
| // Horizontal swipe | |
| if (diffX > 0 && direction !== 'right') { | |
| nextDirection = 'left'; | |
| } else if (diffX < 0 && direction !== 'left') { | |
| nextDirection = 'right'; | |
| } | |
| } else { | |
| // Vertical swipe | |
| if (diffY > 0 && direction !== 'down') { | |
| nextDirection = 'up'; | |
| } else if (diffY < 0 && direction !== 'up') { | |
| nextDirection = 'down'; | |
| } | |
| } | |
| touchStartX = 0; | |
| touchStartY = 0; | |
| e.preventDefault(); | |
| }, { passive: false }); | |
| // Button event listeners | |
| startBtn.addEventListener('click', () => { | |
| startScreen.classList.add('hidden'); | |
| initGame(); | |
| }); | |
| restartBtn.addEventListener('click', () => { | |
| gameOverScreen.classList.add('hidden'); | |
| initGame(); | |
| }); | |
| soundBtn.addEventListener('click', () => { | |
| isSoundOn = !isSoundOn; | |
| if (isSoundOn) { | |
| soundBtn.innerHTML = '<i data-feather="volume-2"></i>'; | |
| bgMusic.play().catch(e => console.log("Autoplay prevented:", e)); | |
| } else { | |
| soundBtn.innerHTML = '<i data-feather="volume-x"></i>'; | |
| bgMusic.pause(); | |
| } | |
| feather.replace(); | |
| }); | |
| fullscreenBtn.addEventListener('click', () => { | |
| if (!document.fullscreenElement) { | |
| canvas.requestFullscreen().catch(err => { | |
| alert(`Error attempting to enable fullscreen: ${err.message}`); | |
| }); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| }); | |
| // Initialize | |
| window.addEventListener('load', () => { | |
| resizeCanvas(); | |
| drawGame(); | |
| feather.replace(); | |
| // Try to autoplay music with user interaction | |
| document.addEventListener('click', () => { | |
| if (isSoundOn) { | |
| bgMusic.play().catch(e => console.log("Autoplay prevented:", e)); | |
| } | |
| }, { once: true }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |