Files
softball/types/BattedBall.ts
George Powell 9b779598e9 initial commit
2026-03-26 12:11:43 -04:00

120 lines
3.8 KiB
TypeScript

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))
};
}