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>Interactive Physics Ball</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script> | |
| <style> | |
| body { | |
| overflow: hidden; | |
| touch-action: none; | |
| background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); | |
| } | |
| #ball { | |
| position: absolute; | |
| border-radius: 50%; | |
| box-shadow: 0 0 30px rgba(255, 255, 255, 0.5); | |
| transition: transform 0.1s ease-out; | |
| background: radial-gradient(circle at 30% 30%, #fff, #f0f0f0 40%, #d9d9d9 60%, #bfbfbf 80%); | |
| } | |
| #instructions { | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(5px); | |
| transition: all 0.3s ease; | |
| } | |
| #instructions.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .bounce { | |
| animation: bounce 0.5s cubic-bezier(0.28, 0.84, 0.42, 1); | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { transform: scale(1, 1) translateY(0); } | |
| 10% { transform: scale(1.1, 0.9) translateY(0); } | |
| 30% { transform: scale(0.9, 1.1) translateY(-20px); } | |
| 50% { transform: scale(1.05, 0.95) translateY(0); } | |
| 57% { transform: scale(1, 1) translateY(-7px); } | |
| 64% { transform: scale(1, 1) translateY(0); } | |
| } | |
| </style> | |
| </head> | |
| <body class="h-screen w-full relative"> | |
| <div id="ball" class="w-16 h-16"></div> | |
| <div id="instructions" class="fixed inset-0 flex flex-col items-center justify-center text-white p-6 z-10"> | |
| <div class="max-w-md bg-black bg-opacity-50 rounded-xl p-8 text-center"> | |
| <div class="text-4xl mb-6"> | |
| <i class="fas fa-mobile-alt animate-pulse"></i> | |
| </div> | |
| <h1 class="text-2xl font-bold mb-4">Physics Ball</h1> | |
| <p class="mb-6">Shake or tilt your device to interact with the ball. The ball will bounce realistically based on physics simulation.</p> | |
| <button id="startBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full transition-all transform hover:scale-105"> | |
| Start Experience | |
| </button> | |
| </div> | |
| </div> | |
| <div class="absolute bottom-4 left-0 right-0 flex justify-center"> | |
| <div class="bg-black bg-opacity-50 text-white px-4 py-2 rounded-full text-sm flex items-center"> | |
| <i class="fas fa-info-circle mr-2"></i> | |
| <span>Shake harder for more intense reactions!</span> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const ball = document.getElementById('ball'); | |
| const instructions = document.getElementById('instructions'); | |
| const startBtn = document.getElementById('startBtn'); | |
| let ballX = window.innerWidth / 2; | |
| let ballY = window.innerHeight / 2; | |
| let ballSize = 64; | |
| let velocityX = 0; | |
| let velocityY = 0; | |
| let gravity = 0.2; | |
| let friction = 0.99; | |
| let bounce = 0.7; | |
| let isSimulating = false; | |
| let lastShakeTime = 0; | |
| let shakeThreshold = 15; | |
| let lastAcceleration = { x: 0, y: 0, z: 0 }; | |
| // Set initial ball position | |
| ball.style.left = `${ballX - ballSize/2}px`; | |
| ball.style.top = `${ballY - ballSize/2}px`; | |
| // Start button handler | |
| startBtn.addEventListener('click', () => { | |
| instructions.classList.add('hidden'); | |
| startSimulation(); | |
| }); | |
| function startSimulation() { | |
| if (isSimulating) return; | |
| isSimulating = true; | |
| // Request permission for iOS 13+ devices | |
| if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') { | |
| DeviceMotionEvent.requestPermission() | |
| .then(response => { | |
| if (response === 'granted') { | |
| window.addEventListener('devicemotion', handleMotion); | |
| } | |
| }) | |
| .catch(console.error); | |
| } else { | |
| window.addEventListener('devicemotion', handleMotion); | |
| } | |
| animate(); | |
| } | |
| function handleMotion(event) { | |
| const acceleration = event.accelerationIncludingGravity; | |
| const now = Date.now(); | |
| // Calculate difference from previous acceleration | |
| const deltaX = Math.abs(acceleration.x - lastAcceleration.x); | |
| const deltaY = Math.abs(acceleration.y - lastAcceleration.y); | |
| const deltaZ = Math.abs(acceleration.z - lastAcceleration.z); | |
| // Check if it's a shake | |
| if ((deltaX > shakeThreshold || deltaY > shakeThreshold || deltaZ > shakeThreshold) && | |
| now - lastShakeTime > 1000) { | |
| lastShakeTime = now; | |
| // Add random velocity for shake effect | |
| velocityX += (Math.random() - 0.5) * 20; | |
| velocityY += (Math.random() - 0.5) * 20; | |
| // Visual feedback | |
| ball.classList.add('bounce'); | |
| setTimeout(() => ball.classList.remove('bounce'), 500); | |
| } | |
| // Normal tilt handling | |
| if (acceleration.x !== null) { | |
| velocityX += acceleration.x * 0.5; | |
| } | |
| if (acceleration.y !== null) { | |
| velocityY += -acceleration.y * 0.5; | |
| } | |
| lastAcceleration = { | |
| x: acceleration.x, | |
| y: acceleration.y, | |
| z: acceleration.z | |
| }; | |
| } | |
| function animate() { | |
| if (!isSimulating) return; | |
| // Apply gravity | |
| velocityY += gravity; | |
| // Apply friction | |
| velocityX *= friction; | |
| velocityY *= friction; | |
| // Update position | |
| ballX += velocityX; | |
| ballY += velocityY; | |
| // Boundary collision - X axis | |
| if (ballX - ballSize/2 < 0) { | |
| ballX = ballSize/2; | |
| velocityX = -velocityX * bounce; | |
| } else if (ballX + ballSize/2 > window.innerWidth) { | |
| ballX = window.innerWidth - ballSize/2; | |
| velocityX = -velocityX * bounce; | |
| } | |
| // Boundary collision - Y axis | |
| if (ballY - ballSize/2 < 0) { | |
| ballY = ballSize/2; | |
| velocityY = -velocityY * bounce; | |
| } else if (ballY + ballSize/2 > window.innerHeight) { | |
| ballY = window.innerHeight - ballSize/2; | |
| velocityY = -velocityY * bounce; | |
| // Extra effect when hitting the bottom | |
| if (Math.abs(velocityY) > 2) { | |
| ball.classList.add('bounce'); | |
| setTimeout(() => ball.classList.remove('bounce'), 500); | |
| } | |
| } | |
| // Apply new position | |
| ball.style.left = `${ballX - ballSize/2}px`; | |
| ball.style.top = `${ballY - ballSize/2}px`; | |
| // Color change based on velocity | |
| const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); | |
| const hue = (speed * 2) % 360; | |
| ball.style.background = `radial-gradient(circle at 30% 30%, hsl(${hue}, 100%, 90%), hsl(${hue}, 80%, 80%) 40%, hsl(${hue}, 60%, 70%) 60%, hsl(${hue}, 40%, 60%) 80%)`; | |
| requestAnimationFrame(animate); | |
| } | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| // Keep ball on screen | |
| if (ballX > window.innerWidth - ballSize/2) { | |
| ballX = window.innerWidth - ballSize/2; | |
| } | |
| if (ballY > window.innerHeight - ballSize/2) { | |
| ballY = window.innerHeight - ballSize/2; | |
| } | |
| }); | |
| // Click/tap to add random impulse | |
| window.addEventListener('click', (e) => { | |
| if (!isSimulating) return; | |
| // Calculate direction from ball to click | |
| const dx = e.clientX - ballX; | |
| const dy = e.clientY - ballY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| // Add velocity away from click point | |
| if (distance < 200) { | |
| velocityX += (ballX - e.clientX) / 20; | |
| velocityY += (ballY - e.clientY) / 20; | |
| } | |
| }); | |
| }); | |
| </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=amirpoorazima/virtual-ball" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |