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> |