| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Baseball Flight Tracker</title> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
| | <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script> |
| | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> |
| | <style> |
| | #cameraPreview, #3dView { |
| | width: 100%; |
| | height: 300px; |
| | background-color: #1a202c; |
| | border-radius: 12px; |
| | overflow: hidden; |
| | } |
| | #3dView { |
| | position: relative; |
| | } |
| | .ball-marker { |
| | position: absolute; |
| | width: 12px; |
| | height: 12px; |
| | background-color: red; |
| | border-radius: 50%; |
| | transform: translate(-50%, -50%); |
| | z-index: 10; |
| | } |
| | .flight-path { |
| | position: absolute; |
| | background-color: rgba(255, 0, 0, 0.5); |
| | height: 2px; |
| | transform-origin: left center; |
| | z-index: 5; |
| | } |
| | #cameraContainer { |
| | position: relative; |
| | } |
| | .recording-indicator { |
| | position: absolute; |
| | top: 10px; |
| | right: 10px; |
| | background-color: rgba(255, 0, 0, 0.7); |
| | color: white; |
| | padding: 5px 10px; |
| | border-radius: 20px; |
| | font-size: 12px; |
| | display: flex; |
| | align-items: center; |
| | gap: 5px; |
| | z-index: 10; |
| | } |
| | .recording-dot { |
| | width: 8px; |
| | height: 8px; |
| | background-color: white; |
| | border-radius: 50%; |
| | animation: pulse 1.5s infinite; |
| | } |
| | @keyframes pulse { |
| | 0% { opacity: 1; } |
| | 50% { opacity: 0.3; } |
| | 100% { opacity: 1; } |
| | } |
| | </style> |
| | </head> |
| | <body class="bg-gray-100 text-gray-900"> |
| | <div class="container mx-auto px-4 py-6 max-w-md"> |
| | <header class="mb-6"> |
| | <div class="flex items-center justify-between"> |
| | <div> |
| | <h1 class="text-2xl font-bold text-gray-800">Baseball Flight Tracker</h1> |
| | <p class="text-gray-600">Track and visualize baseball trajectories</p> |
| | </div> |
| | <div class="w-12 h-12 bg-red-500 rounded-full flex items-center justify-center text-white"> |
| | <i class="fas fa-baseball-ball text-xl"></i> |
| | </div> |
| | </div> |
| | </header> |
| |
|
| | <div class="bg-white rounded-xl shadow-md p-4 mb-6"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h2 class="text-lg font-semibold">Live Tracking</h2> |
| | <div class="flex space-x-2"> |
| | <button id="recordBtn" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-full flex items-center gap-2 transition"> |
| | <i class="fas fa-circle"></i> Record |
| | </button> |
| | <button class="bg-gray-200 hover:bg-gray-300 text-gray-800 p-2 rounded-full transition"> |
| | <i class="fas fa-cog"></i> |
| | </button> |
| | </div> |
| | </div> |
| |
|
| | <div id="cameraContainer" class="mb-4"> |
| | <div id="cameraPreview" class="relative"> |
| | |
| | <div class="absolute inset-0 flex items-center justify-center text-white"> |
| | <i class="fas fa-camera text-4xl opacity-50"></i> |
| | </div> |
| | |
| | </div> |
| | </div> |
| |
|
| | <div class="grid grid-cols-3 gap-2 mb-4"> |
| | <div class="bg-gray-100 p-3 rounded-lg text-center"> |
| | <p class="text-sm text-gray-500">Speed</p> |
| | <p class="font-bold">-- mph</p> |
| | </div> |
| | <div class="bg-gray-100 p-3 rounded-lg text-center"> |
| | <p class="text-sm text-gray-500">Angle</p> |
| | <p class="font-bold">--°</p> |
| | </div> |
| | <div class="bg-gray-100 p-3 rounded-lg text-center"> |
| | <p class="text-sm text-gray-500">Distance</p> |
| | <p class="font-bold">-- ft</p> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div class="bg-white rounded-xl shadow-md p-4 mb-6"> |
| | <h2 class="text-lg font-semibold mb-4">3D Visualization</h2> |
| | <div id="3dView"> |
| | |
| | <div class="absolute inset-0 flex items-center justify-center text-white"> |
| | <i class="fas fa-baseball-ball text-4xl opacity-50"></i> |
| | </div> |
| | </div> |
| | <div class="mt-3 flex justify-between"> |
| | <button id="viewHome" class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-sm transition"> |
| | Home View |
| | </button> |
| | <button id="viewPitcher" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-3 py-1 rounded text-sm transition"> |
| | Pitcher View |
| | </button> |
| | <button id="viewSide" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-3 py-1 rounded text-sm transition"> |
| | Side View |
| | </button> |
| | </div> |
| | </div> |
| |
|
| | <div class="bg-white rounded-xl shadow-md p-4"> |
| | <h2 class="text-lg font-semibold mb-4">Recent Sessions</h2> |
| | <div class="space-y-3"> |
| | <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> |
| | <div> |
| | <p class="font-medium">Session #245</p> |
| | <p class="text-sm text-gray-500">Today, 3:45 PM</p> |
| | </div> |
| | <div class="text-right"> |
| | <p class="font-medium">92 mph</p> |
| | <p class="text-sm text-gray-500">Fastball</p> |
| | </div> |
| | </div> |
| | <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> |
| | <div> |
| | <p class="font-medium">Session #244</p> |
| | <p class="text-sm text-gray-500">Today, 2:30 PM</p> |
| | </div> |
| | <div class="text-right"> |
| | <p class="font-medium">85 mph</p> |
| | <p class="text-sm text-gray-500">Curveball</p> |
| | </div> |
| | </div> |
| | <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> |
| | <div> |
| | <p class="font-medium">Session #243</p> |
| | <p class="text-sm text-gray-500">Yesterday</p> |
| | </div> |
| | <div class="text-right"> |
| | <p class="font-medium">89 mph</p> |
| | <p class="text-sm text-gray-500">Slider</p> |
| | </div> |
| | </div> |
| | </div> |
| | <button class="w-full mt-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-gray-700 font-medium transition"> |
| | View All Sessions |
| | </button> |
| | </div> |
| | </div> |
| |
|
| | <nav class="fixed bottom-0 left-0 right-0 bg-white shadow-lg"> |
| | <div class="container mx-auto max-w-md px-4"> |
| | <div class="flex justify-around py-3"> |
| | <a href="#" class="flex flex-col items-center text-blue-500"> |
| | <i class="fas fa-home text-xl"></i> |
| | <span class="text-xs mt-1">Home</span> |
| | </a> |
| | <a href="#" class="flex flex-col items-center text-gray-500"> |
| | <i class="fas fa-chart-line text-xl"></i> |
| | <span class="text-xs mt-1">Stats</span> |
| | </a> |
| | <a href="#" class="flex flex-col items-center text-gray-500"> |
| | <i class="fas fa-video text-xl"></i> |
| | <span class="text-xs mt-1">Record</span> |
| | </a> |
| | <a href="#" class="flex flex-col items-center text-gray-500"> |
| | <i class="fas fa-user text-xl"></i> |
| | <span class="text-xs mt-1">Profile</span> |
| | </a> |
| | </div> |
| | </div> |
| | </nav> |
| |
|
| | <script> |
| | |
| | let isRecording = false; |
| | const recordBtn = document.getElementById('recordBtn'); |
| | const cameraPreview = document.getElementById('cameraPreview'); |
| | let recordingIndicator = null; |
| | |
| | recordBtn.addEventListener('click', function() { |
| | isRecording = !isRecording; |
| | |
| | if (isRecording) { |
| | recordBtn.innerHTML = '<i class="fas fa-stop"></i> Stop'; |
| | recordBtn.classList.remove('bg-red-500'); |
| | recordBtn.classList.add('bg-gray-600'); |
| | |
| | |
| | recordingIndicator = document.createElement('div'); |
| | recordingIndicator.className = 'recording-indicator'; |
| | recordingIndicator.innerHTML = ` |
| | <div class="recording-dot"></div> |
| | REC |
| | `; |
| | cameraPreview.appendChild(recordingIndicator); |
| | |
| | |
| | simulateBallTracking(); |
| | } else { |
| | recordBtn.innerHTML = '<i class="fas fa-circle"></i> Record'; |
| | recordBtn.classList.remove('bg-gray-600'); |
| | recordBtn.classList.add('bg-red-500'); |
| | |
| | |
| | if (recordingIndicator) { |
| | cameraPreview.removeChild(recordingIndicator); |
| | } |
| | |
| | |
| | const markers = document.querySelectorAll('.ball-marker, .flight-path'); |
| | markers.forEach(marker => marker.remove()); |
| | } |
| | }); |
| | |
| | function simulateBallTracking() { |
| | if (!isRecording) return; |
| | |
| | |
| | const markers = document.querySelectorAll('.ball-marker, .flight-path'); |
| | markers.forEach(marker => marker.remove()); |
| | |
| | |
| | const flightPath = document.createElement('div'); |
| | flightPath.className = 'flight-path'; |
| | flightPath.style.width = '0'; |
| | flightPath.style.left = '30%'; |
| | flightPath.style.top = '70%'; |
| | cameraPreview.appendChild(flightPath); |
| | |
| | |
| | let position = 0; |
| | const interval = setInterval(() => { |
| | if (!isRecording) { |
| | clearInterval(interval); |
| | return; |
| | } |
| | |
| | position += 2; |
| | |
| | |
| | const marker = document.createElement('div'); |
| | marker.className = 'ball-marker'; |
| | marker.style.left = `${30 + position}%`; |
| | marker.style.top = `${70 - position * 0.7}%`; |
| | cameraPreview.appendChild(marker); |
| | |
| | |
| | flightPath.style.width = `${position}%`; |
| | flightPath.style.transform = `rotate(${-position * 0.5}deg)`; |
| | |
| | |
| | if (position >= 70) { |
| | clearInterval(interval); |
| | updateStats(); |
| | render3DStadium(); |
| | } |
| | }, 50); |
| | } |
| | |
| | function updateStats() { |
| | |
| | document.querySelectorAll('.bg-gray-100 p.font-bold')[0].textContent = '92 mph'; |
| | document.querySelectorAll('.bg-gray-100 p.font-bold')[1].textContent = '24°'; |
| | document.querySelectorAll('.bg-gray-100 p.font-bold')[2].textContent = '380 ft'; |
| | } |
| | |
| | |
| | let scene, camera, renderer, controls, ball, stadium; |
| | |
| | function init3D() { |
| | const container = document.getElementById('3dView'); |
| | |
| | |
| | scene = new THREE.Scene(); |
| | scene.background = new THREE.Color(0xf0f0f0); |
| | |
| | |
| | camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000); |
| | camera.position.set(0, 10, 20); |
| | |
| | |
| | renderer = new THREE.WebGLRenderer({ antialias: true }); |
| | renderer.setSize(container.clientWidth, container.clientHeight); |
| | container.innerHTML = ''; |
| | container.appendChild(renderer.domElement); |
| | |
| | |
| | controls = new THREE.OrbitControls(camera, renderer.domElement); |
| | controls.enableDamping = true; |
| | controls.dampingFactor = 0.25; |
| | |
| | |
| | const ambientLight = new THREE.AmbientLight(0x404040); |
| | scene.add(ambientLight); |
| | |
| | const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); |
| | directionalLight.position.set(1, 1, 1); |
| | scene.add(directionalLight); |
| | |
| | |
| | createStadium(); |
| | |
| | |
| | window.addEventListener('resize', onWindowResize); |
| | |
| | |
| | animate(); |
| | } |
| | |
| | function createStadium() { |
| | |
| | const groundGeometry = new THREE.PlaneGeometry(40, 40); |
| | const groundMaterial = new THREE.MeshStandardMaterial({ |
| | color: 0x4CAF50, |
| | roughness: 0.8, |
| | metalness: 0.2 |
| | }); |
| | const ground = new THREE.Mesh(groundGeometry, groundMaterial); |
| | ground.rotation.x = -Math.PI / 2; |
| | scene.add(ground); |
| | |
| | |
| | const infieldGeometry = new THREE.CircleGeometry(10, 32); |
| | const infieldMaterial = new THREE.MeshStandardMaterial({ |
| | color: 0xD2B48C, |
| | roughness: 0.7 |
| | }); |
| | const infield = new THREE.Mesh(infieldGeometry, infieldMaterial); |
| | infield.rotation.x = -Math.PI / 2; |
| | infield.position.y = 0.01; |
| | scene.add(infield); |
| | |
| | |
| | const baseGeometry = new THREE.BoxGeometry(1, 0.1, 1); |
| | const baseMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); |
| | |
| | const firstBase = new THREE.Mesh(baseGeometry, baseMaterial); |
| | firstBase.position.set(0, 0.02, 10); |
| | scene.add(firstBase); |
| | |
| | const secondBase = new THREE.Mesh(baseGeometry, baseMaterial); |
| | secondBase.position.set(-10, 0.02, 0); |
| | secondBase.rotation.y = Math.PI / 4; |
| | scene.add(secondBase); |
| | |
| | const thirdBase = new THREE.Mesh(baseGeometry, baseMaterial); |
| | thirdBase.position.set(0, 0.02, -10); |
| | scene.add(thirdBase); |
| | |
| | |
| | const homePlateShape = new THREE.Shape(); |
| | homePlateShape.moveTo(0, 0); |
| | homePlateShape.lineTo(-0.7, 0.7); |
| | homePlateShape.lineTo(0, 1.4); |
| | homePlateShape.lineTo(0.7, 0.7); |
| | homePlateShape.lineTo(0, 0); |
| | |
| | const homePlateGeometry = new THREE.ShapeGeometry(homePlateShape); |
| | const homePlateMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); |
| | const homePlate = new THREE.Mesh(homePlateGeometry, homePlateMaterial); |
| | homePlate.rotation.x = -Math.PI / 2; |
| | homePlate.position.set(0, 0.02, 11); |
| | homePlate.scale.set(5, 5, 1); |
| | scene.add(homePlate); |
| | |
| | |
| | const moundGeometry = new THREE.CylinderGeometry(1, 1, 0.2, 32); |
| | const moundMaterial = new THREE.MeshStandardMaterial({ |
| | color: 0xD2B48C, |
| | roughness: 0.7 |
| | }); |
| | const mound = new THREE.Mesh(moundGeometry, moundMaterial); |
| | mound.position.set(0, 0.1, 0); |
| | scene.add(mound); |
| | |
| | |
| | for (let i = 0; i < 4; i++) { |
| | const angle = i * Math.PI / 2; |
| | const x = Math.cos(angle) * 18; |
| | const z = Math.sin(angle) * 18; |
| | |
| | const standGeometry = new THREE.BoxGeometry(15, 5, 3); |
| | const standMaterial = new THREE.MeshStandardMaterial({ color: 0x607D8B }); |
| | const stand = new THREE.Mesh(standGeometry, standMaterial); |
| | stand.position.set(x, 2.5, z); |
| | stand.lookAt(0, 2.5, 0); |
| | scene.add(stand); |
| | } |
| | } |
| | |
| | function render3DStadium() { |
| | if (!scene) init3D(); |
| | |
| | |
| | if (ball) scene.remove(ball); |
| | |
| | const ballGeometry = new THREE.SphereGeometry(0.5, 32, 32); |
| | const ballMaterial = new THREE.MeshStandardMaterial({ |
| | color: 0xCC0000, |
| | roughness: 0.3, |
| | metalness: 0.5 |
| | }); |
| | ball = new THREE.Mesh(ballGeometry, ballMaterial); |
| | scene.add(ball); |
| | |
| | |
| | const startPos = { x: 0, y: 2, z: 5 }; |
| | const endPos = { x: -15, y: 1, z: -10 }; |
| | const controlPos = { x: -5, y: 10, z: -2 }; |
| | |
| | let t = 0; |
| | const ballInterval = setInterval(() => { |
| | t += 0.02; |
| | |
| | if (t > 1) { |
| | clearInterval(ballInterval); |
| | return; |
| | } |
| | |
| | |
| | const x = (1-t)*(1-t)*startPos.x + 2*(1-t)*t*controlPos.x + t*t*endPos.x; |
| | const y = (1-t)*(1-t)*startPos.y + 2*(1-t)*t*controlPos.y + t*t*endPos.y; |
| | const z = (1-t)*(1-t)*startPos.z + 2*(1-t)*t*controlPos.z + t*t*endPos.z; |
| | |
| | ball.position.set(x, y, z); |
| | }, 20); |
| | } |
| | |
| | function onWindowResize() { |
| | const container = document.getElementById('3dView'); |
| | camera.aspect = container.clientWidth / container.clientHeight; |
| | camera.updateProjectionMatrix(); |
| | renderer.setSize(container.clientWidth, container.clientHeight); |
| | } |
| | |
| | function animate() { |
| | requestAnimationFrame(animate); |
| | controls.update(); |
| | renderer.render(scene, camera); |
| | } |
| | |
| | |
| | document.getElementById('viewHome').addEventListener('click', () => { |
| | if (camera) { |
| | camera.position.set(0, 10, 20); |
| | camera.lookAt(0, 0, 0); |
| | } |
| | }); |
| | |
| | document.getElementById('viewPitcher').addEventListener('click', () => { |
| | if (camera) { |
| | camera.position.set(0, 2, 10); |
| | camera.lookAt(0, 1.5, 0); |
| | } |
| | }); |
| | |
| | document.getElementById('viewSide').addEventListener('click', () => { |
| | if (camera) { |
| | camera.position.set(15, 5, 0); |
| | camera.lookAt(0, 2, 0); |
| | } |
| | }); |
| | |
| | |
| | let is3DInitialized = false; |
| | function checkAndInit3D() { |
| | if (!is3DInitialized) { |
| | init3D(); |
| | is3DInitialized = true; |
| | } |
| | } |
| | |
| | |
| | document.querySelectorAll('#viewHome, #viewPitcher, #viewSide').forEach(btn => { |
| | btn.addEventListener('click', checkAndInit3D); |
| | }); |
| | </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=Will-Code/baseball-tracker-000001" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| | </html> |