Will-Code's picture
Add 3 files
61cdfa6 verified
<!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">
<!-- Camera preview will be shown here -->
<div class="absolute inset-0 flex items-center justify-center text-white">
<i class="fas fa-camera text-4xl opacity-50"></i>
</div>
<!-- Ball markers and flight path will be added here during tracking -->
</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">
<!-- 3D stadium visualization will be rendered here -->
<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>
// Simulate ball tracking
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');
// Add recording indicator
recordingIndicator = document.createElement('div');
recordingIndicator.className = 'recording-indicator';
recordingIndicator.innerHTML = `
<div class="recording-dot"></div>
REC
`;
cameraPreview.appendChild(recordingIndicator);
// Simulate ball tracking
simulateBallTracking();
} else {
recordBtn.innerHTML = '<i class="fas fa-circle"></i> Record';
recordBtn.classList.remove('bg-gray-600');
recordBtn.classList.add('bg-red-500');
// Remove recording indicator
if (recordingIndicator) {
cameraPreview.removeChild(recordingIndicator);
}
// Clear any existing markers
const markers = document.querySelectorAll('.ball-marker, .flight-path');
markers.forEach(marker => marker.remove());
}
});
function simulateBallTracking() {
if (!isRecording) return;
// Clear previous markers
const markers = document.querySelectorAll('.ball-marker, .flight-path');
markers.forEach(marker => marker.remove());
// Create flight path
const flightPath = document.createElement('div');
flightPath.className = 'flight-path';
flightPath.style.width = '0';
flightPath.style.left = '30%';
flightPath.style.top = '70%';
cameraPreview.appendChild(flightPath);
// Animate ball flight
let position = 0;
const interval = setInterval(() => {
if (!isRecording) {
clearInterval(interval);
return;
}
position += 2;
// Create new ball marker
const marker = document.createElement('div');
marker.className = 'ball-marker';
marker.style.left = `${30 + position}%`;
marker.style.top = `${70 - position * 0.7}%`;
cameraPreview.appendChild(marker);
// Update flight path
flightPath.style.width = `${position}%`;
flightPath.style.transform = `rotate(${-position * 0.5}deg)`;
// End simulation after reaching certain point
if (position >= 70) {
clearInterval(interval);
updateStats();
render3DStadium();
}
}, 50);
}
function updateStats() {
// Update the stats display with simulated data
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';
}
// 3D Stadium Visualization
let scene, camera, renderer, controls, ball, stadium;
function init3D() {
const container = document.getElementById('3dView');
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// Camera
camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 10, 20);
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.innerHTML = '';
container.appendChild(renderer.domElement);
// Controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// Lights
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);
// Create simple stadium
createStadium();
// Handle window resize
window.addEventListener('resize', onWindowResize);
// Start animation loop
animate();
}
function createStadium() {
// Ground
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);
// Infield (diamond)
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);
// Bases
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);
// Home plate (pentagon shape)
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);
// Pitcher's mound
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);
// Simple stands
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();
// Create ball
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);
// Animate ball flight
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;
}
// Quadratic Bezier curve
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);
}
// View buttons
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);
}
});
// Initialize 3D view on first recording
let is3DInitialized = false;
function checkAndInit3D() {
if (!is3DInitialized) {
init3D();
is3DInitialized = true;
}
}
// Demo button to show 3D without recording
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>