Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Voice-Controlled Free Diving Game</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.min.js"></script> | |
| <style> | |
| .water-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(to bottom, rgba(0, 120, 255, 0.2), rgba(0, 60, 200, 0.6)); | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| .volume-indicator { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 80%; | |
| height: 10px; | |
| background: rgba(255, 255, 255, 0.3); | |
| border-radius: 5px; | |
| overflow: hidden; | |
| z-index: 100; | |
| } | |
| .volume-level { | |
| height: 100%; | |
| background: linear-gradient(to right, #4facfe, #00f2fe); | |
| width: 0%; | |
| transition: width 0.1s; | |
| } | |
| .depth-meter { | |
| position: absolute; | |
| right: 20px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 30px; | |
| height: 200px; | |
| background: rgba(0, 0, 0, 0.5); | |
| border-radius: 15px; | |
| overflow: hidden; | |
| z-index: 100; | |
| } | |
| .depth-fill { | |
| position: absolute; | |
| bottom: 0; | |
| width: 100%; | |
| height: 0%; | |
| background: linear-gradient(to top, #4facfe, #00f2fe); | |
| transition: height 0.2s; | |
| } | |
| .depth-label { | |
| position: absolute; | |
| right: 60px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: white; | |
| font-size: 24px; | |
| text-shadow: 0 0 5px black; | |
| z-index: 100; | |
| } | |
| .bubble { | |
| position: absolute; | |
| background: rgba(255, 255, 255, 0.6); | |
| border-radius: 50%; | |
| filter: blur(1px); | |
| z-index: 5; | |
| } | |
| .fish { | |
| position: absolute; | |
| z-index: 8; | |
| transition: transform 0.5s; | |
| } | |
| .game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| #videoElement { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| z-index: 1; | |
| } | |
| #canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| z-index: 2; | |
| } | |
| .start-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.7); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 200; | |
| color: white; | |
| } | |
| .start-btn { | |
| margin-top: 20px; | |
| padding: 15px 30px; | |
| background: linear-gradient(to right, #4facfe, #00f2fe); | |
| border: none; | |
| border-radius: 30px; | |
| color: white; | |
| font-size: 18px; | |
| cursor: pointer; | |
| box-shadow: 0 0 15px rgba(0, 242, 254, 0.5); | |
| transition: all 0.3s; | |
| } | |
| .start-btn:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 20px rgba(0, 242, 254, 0.8); | |
| } | |
| .title { | |
| font-size: 3rem; | |
| margin-bottom: 20px; | |
| text-shadow: 0 0 10px #00f2fe; | |
| background: linear-gradient(to right, #4facfe, #00f2fe); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .instructions { | |
| text-align: center; | |
| max-width: 80%; | |
| margin-bottom: 30px; | |
| line-height: 1.6; | |
| } | |
| .silence-warning { | |
| position: absolute; | |
| bottom: 50px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 20px; | |
| z-index: 150; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .volume-threshold { | |
| position: absolute; | |
| left: 0; | |
| width: 15%; | |
| height: 100%; | |
| background-color: rgba(255, 255, 255, 0.2); | |
| z-index: 101; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-black overflow-hidden"> | |
| <div class="game-container"> | |
| <video id="videoElement" autoplay playsinline></video> | |
| <canvas id="canvas"></canvas> | |
| <div class="water-overlay"></div> | |
| <div class="volume-indicator"> | |
| <div class="volume-threshold"></div> | |
| <div class="volume-level" id="volumeLevel"></div> | |
| </div> | |
| <div class="depth-meter"> | |
| <div class="depth-fill" id="depthFill"></div> | |
| </div> | |
| <div class="depth-label" id="depthLabel">0m</div> | |
| <div class="silence-warning" id="silenceWarning"> | |
| Take a deep breath and make noise to dive deeper! | |
| </div> | |
| <div class="start-screen" id="startScreen"> | |
| <h1 class="title">VOICE DIVE</h1> | |
| <p class="instructions"> | |
| Welcome to Voice Dive! Use your voice to control your descent into the deep blue.<br> | |
| The louder you are, the faster you'll dive. Try to reach the deepest point!<br> | |
| Stay silent and you'll start floating back up. Make sure to allow camera and microphone access when prompted. | |
| </p> | |
| <button class="start-btn" id="startBtn">START DIVING</button> | |
| </div> | |
| </div> | |
| <script> | |
| // Game variables | |
| let video; | |
| let canvas; | |
| let ctx; | |
| let bubbles = []; | |
| let fishes = []; | |
| let depth = 0; | |
| let maxDepth = 1000; // in meters | |
| let volume = 0; | |
| let isPlaying = false; | |
| let animationId; | |
| let lastBubbleTime = 0; | |
| let silenceTimer = 0; | |
| let isSilent = false; | |
| // Fish images | |
| const fishImages = [ | |
| '🐠', '🐟', '🐡', '🦈', '🐋', '🦑', '🐙', '🦀', '🦐', '🐚' | |
| ]; | |
| // Initialize the game | |
| function initGame() { | |
| video = document.getElementById('videoElement'); | |
| canvas = document.getElementById('canvas'); | |
| ctx = canvas.getContext('2d'); | |
| // Set canvas size | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Setup camera | |
| setupCamera(); | |
| // Create initial fishes | |
| createFishes(10); | |
| // Start game loop | |
| gameLoop(); | |
| } | |
| // Setup camera | |
| function setupCamera() { | |
| if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { | |
| navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: "user", | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 } | |
| } | |
| }) | |
| .then(function(stream) { | |
| video.srcObject = stream; | |
| }) | |
| .catch(function(error) { | |
| console.error("Camera error: ", error); | |
| }); | |
| } | |
| } | |
| // Setup microphone | |
| function setupMicrophone() { | |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| const analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 32; | |
| navigator.mediaDevices.getUserMedia({ audio: true, video: false }) | |
| .then(function(stream) { | |
| const microphone = audioContext.createMediaStreamSource(stream); | |
| microphone.connect(analyser); | |
| const dataArray = new Uint8Array(analyser.frequencyBinCount); | |
| function analyzeVolume() { | |
| analyser.getByteFrequencyData(dataArray); | |
| let sum = 0; | |
| for (let i = 0; i < dataArray.length; i++) { | |
| sum += dataArray[i]; | |
| } | |
| volume = sum / dataArray.length; | |
| // Check for no breath (volume below 15%) | |
| if (volume < 15) { | |
| silenceTimer++; | |
| if (silenceTimer > 30 && !isSilent) { // 0.5 seconds of silence | |
| isSilent = true; | |
| document.getElementById('silenceWarning').style.opacity = '1'; | |
| } | |
| } else { | |
| silenceTimer = 0; | |
| if (isSilent) { | |
| isSilent = false; | |
| document.getElementById('silenceWarning').style.opacity = '0'; | |
| } | |
| } | |
| // Update volume indicator | |
| document.getElementById('volumeLevel').style.width = `${volume}%`; | |
| if (isPlaying) { | |
| // Change depth based on volume | |
| if (volume >= 15) { // Normal breathing - descend | |
| depth += volume / 15; // Adjusted for better control | |
| } else { // No breath - ascend | |
| depth = Math.max(0, depth - 1.5); // Faster ascent | |
| } | |
| // Update depth meter | |
| const depthPercentage = Math.min(100, (depth / maxDepth) * 100); | |
| document.getElementById('depthFill').style.height = `${depthPercentage}%`; | |
| document.getElementById('depthLabel').textContent = `${Math.floor(depth)}m`; | |
| // Create bubbles when breathing out | |
| const now = Date.now(); | |
| if (volume >= 15 && now - lastBubbleTime > 100 - volume) { | |
| createBubbles(); | |
| lastBubbleTime = now; | |
| } | |
| } | |
| animationId = requestAnimationFrame(analyzeVolume); | |
| } | |
| analyzeVolume(); | |
| }) | |
| .catch(function(error) { | |
| console.error("Microphone error: ", error); | |
| }); | |
| } | |
| // Create bubbles with physics | |
| function createBubbles() { | |
| const bubbleCount = Math.floor(1 + volume / 15); | |
| for (let i = 0; i < bubbleCount; i++) { | |
| const size = 5 + Math.random() * 15; | |
| const x = window.innerWidth / 2 + (Math.random() - 0.5) * 200; | |
| const y = window.innerHeight - 50 + (Math.random() - 0.5) * 20; | |
| const baseSpeed = 0.5 + Math.random() * 2 + volume / 50; | |
| // Physics properties | |
| const horizontalSpeed = (Math.random() - 0.5) * 0.5; | |
| const rotationSpeed = (Math.random() - 0.5) * 0.02; | |
| const rotationRadius = 5 + Math.random() * 20; | |
| bubbles.push({ | |
| x, | |
| y, | |
| size, | |
| baseSpeed, | |
| horizontalSpeed, | |
| rotationSpeed, | |
| rotationRadius, | |
| angle: Math.random() * Math.PI * 2, | |
| opacity: 0.3 + Math.random() * 0.7, | |
| time: 0 | |
| }); | |
| } | |
| } | |
| // Create fishes | |
| function createFishes(count) { | |
| for (let i = 0; i < count; i++) { | |
| const fishType = fishImages[Math.floor(Math.random() * fishImages.length)]; | |
| const size = 30 + Math.random() * 50; | |
| const x = Math.random() * window.innerWidth; | |
| const y = Math.random() * window.innerHeight; | |
| const speed = 0.1 + Math.random() * 0.5; | |
| const direction = Math.random() > 0.5 ? 1 : -1; | |
| fishes.push({ | |
| type: fishType, | |
| x, | |
| y, | |
| size, | |
| speed, | |
| direction, | |
| startX: x, | |
| startY: y, | |
| amplitude: 50 + Math.random() * 100, | |
| frequency: 0.01 + Math.random() * 0.03 | |
| }); | |
| } | |
| } | |
| // Update bubbles with physics | |
| function updateBubbles() { | |
| for (let i = bubbles.length - 1; i >= 0; i--) { | |
| const bubble = bubbles[i]; | |
| // Update physics properties | |
| bubble.time += 0.1; | |
| bubble.angle += bubble.rotationSpeed; | |
| // Calculate movement | |
| const verticalSpeed = bubble.baseSpeed * (1 + 0.2 * Math.sin(bubble.time)); | |
| bubble.y -= verticalSpeed; | |
| bubble.x += bubble.horizontalSpeed + Math.sin(bubble.angle) * bubble.rotationRadius; | |
| // Remove bubbles that are off screen | |
| if (bubble.y + bubble.size < 0 || | |
| bubble.x + bubble.size < 0 || | |
| bubble.x - bubble.size > window.innerWidth) { | |
| bubbles.splice(i, 1); | |
| } | |
| } | |
| } | |
| // Update fishes | |
| function updateFishes() { | |
| fishes.forEach(fish => { | |
| fish.x += fish.speed * fish.direction; | |
| fish.y = fish.startY + Math.sin(fish.x * fish.frequency) * fish.amplitude; | |
| // Reverse direction at screen edges | |
| if (fish.x > window.innerWidth + fish.size) { | |
| fish.x = -fish.size; | |
| } else if (fish.x < -fish.size) { | |
| fish.x = window.innerWidth + fish.size; | |
| } | |
| }); | |
| } | |
| // Draw video frame | |
| function drawVideo() { | |
| ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | |
| } | |
| // Draw bubbles with physics effects | |
| function drawBubbles() { | |
| bubbles.forEach(bubble => { | |
| // Add slight distortion based on movement | |
| const distortionX = Math.sin(bubble.time * 2) * bubble.size * 0.1; | |
| const distortionY = Math.cos(bubble.time * 1.5) * bubble.size * 0.1; | |
| ctx.beginPath(); | |
| ctx.ellipse( | |
| bubble.x + distortionX, | |
| bubble.y + distortionY, | |
| bubble.size, | |
| bubble.size * (0.8 + 0.2 * Math.sin(bubble.time)), | |
| 0, 0, Math.PI * 2 | |
| ); | |
| ctx.fillStyle = `rgba(255, 255, 255, ${bubble.opacity})`; | |
| ctx.fill(); | |
| // Add highlight for more realism | |
| ctx.beginPath(); | |
| ctx.ellipse( | |
| bubble.x - bubble.size * 0.3 + distortionX, | |
| bubble.y - bubble.size * 0.3 + distortionY, | |
| bubble.size * 0.3, | |
| bubble.size * 0.15, | |
| 0, 0, Math.PI * 2 | |
| ); | |
| ctx.fillStyle = `rgba(255, 255, 255, ${bubble.opacity * 0.8})`; | |
| ctx.fill(); | |
| }); | |
| } | |
| // Draw fishes | |
| function drawFishes() { | |
| ctx.font = '30px Arial'; | |
| fishes.forEach(fish => { | |
| ctx.save(); | |
| ctx.translate(fish.x, fish.y); | |
| if (fish.direction < 0) { | |
| ctx.scale(-1, 1); | |
| } | |
| ctx.fillText(fish.type, 0, 0); | |
| ctx.restore(); | |
| }); | |
| } | |
| // Game loop | |
| function gameLoop() { | |
| if (!isPlaying) { | |
| requestAnimationFrame(gameLoop); | |
| return; | |
| } | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw elements | |
| drawVideo(); | |
| drawBubbles(); | |
| drawFishes(); | |
| // Update elements | |
| updateBubbles(); | |
| updateFishes(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Start game | |
| document.getElementById('startBtn').addEventListener('click', function() { | |
| document.getElementById('startScreen').style.display = 'none'; | |
| isPlaying = true; | |
| setupMicrophone(); | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', function() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }); | |
| // Initialize game when page loads | |
| window.addEventListener('load', initGame); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Tingchenliang/voice-dive" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |