export type BattedBall = { readonly exitVelo: number; // in mph readonly launchAngle: number; // vertical angle in degrees readonly attackAngle: number; // spray angle: pull = -89º, oppo = 89º readonly spinRate: number; // in RPM // calculated using the bat swing from the batter in the at bat. }; export type BallFlightResult = { readonly hangTime: number; readonly distance: number; readonly landingHorizontalVelo: number; }; /** * Calculates baseball flight trajectory including: * 1. Aerodynamic Drag (Sea Level) * 2. Magnus Effect (Lift from spin) * 3. Ground Friction (Impact physics based on spin type) */ export function calculateFullFlightPath( ball: BattedBall, surfaceFriction: number = 0.4 // 0.3 for turf, 0.4 for grass, 0.45 for dirt ): BallFlightResult { // Physical Constants (Imperial: slugs, feet, seconds) const G = 32.174; const RHO = 0.0023769; // Air density at sea level const CD = 0.35; // Drag coefficient const AREA = 0.0458; // Cross-sectional area (sq ft) const MASS = 0.00994; // Mass (slugs) const RADIUS = 0.121; // Radius (ft) const E = 0.5; // Coefficient of restitution (bounciness) const MPH_TO_FPS = 1.46667; const FPS_TO_MPH = 0.681818; const DT = 0.005; // Smaller time step for better accuracy // Initial State let t = 0; let x = 0; let y = 3; // Standard contact height (ft) let z = 0; const v0 = ball.exitVelo * MPH_TO_FPS; const theta = (ball.launchAngle * Math.PI) / 180; const phi = (ball.attackAngle * Math.PI) / 180; const omega = (ball.spinRate * 2 * Math.PI) / 60; // RPM to Rad/s // Velocity Components let vy = v0 * Math.sin(theta); let v_horiz_initial = v0 * Math.cos(theta); let vx = v_horiz_initial * Math.cos(phi); let vz = v_horiz_initial * Math.sin(phi); // 1. Flight Simulation (Numerical integration using Euler's Method) while (y > 0) { const v = Math.sqrt(vx * vx + vy * vy + vz * vz); if (v < 0.1) break; // Aerodynamics: Drag and Magnus (Lift) const spinFactor = (RADIUS * omega) / v; const CL = 1 / (2 + (1 / spinFactor)); const dragAccelFactor = (0.5 * RHO * v * CD * AREA) / MASS; const liftAccelFactor = (0.5 * RHO * v * CL * AREA) / MASS; // Acceleration Vectors // Lift acts perpendicular to velocity; for backspin, it provides upward lift const ax = -dragAccelFactor * vx - liftAccelFactor * (vy / v) * Math.cos(phi); const ay = -G - (dragAccelFactor * vy) + liftAccelFactor * (vx / v); const az = -dragAccelFactor * vz; // Update State vx += ax * DT; vy += ay * DT; vz += az * DT; x += vx * DT; y += vy * DT; z += vz * DT; t += DT; if (t > 15) break; // Safety timeout } // 2. Landing Physics (Impact Friction) // Horizontal velocity at the moment of impact const vx_impact = vx; const vz_impact = vz; const vy_impact = Math.abs(vy); const v_horiz_impact = Math.sqrt(vx_impact * vx_impact + vz_impact * vz_impact); /** * Slip Velocity Calculation: * v_slip = v_horizontal + (omega * radius) * Positive omega (backspin) increases slip, causing friction to act against motion. * Negative omega (topspin) decreases slip, potentially making it negative, * which causes friction to accelerate the ball forward. */ const v_slip = v_horiz_impact + (omega * RADIUS); // Horizontal Impulse (Change in velocity due to friction) const delta_v = surfaceFriction * vy_impact * (1 + E); let finalV_horiz: number; if (v_slip > 0) { // Friction acts as a braking force (Backspin) finalV_horiz = v_horiz_impact - delta_v; } else { // Friction acts as an accelerating force (Topspin) finalV_horiz = v_horiz_impact + delta_v; } // Results return { hangTime: Number(t.toFixed(2)), distance: Number(Math.sqrt(x * x + z * z).toFixed(2)), landingHorizontalVelo: Number(Math.max(0, finalV_horiz * FPS_TO_MPH).toFixed(2)) }; }