Tutorial · Sports Physics · Aerodynamics · JavaScript
📅 July 2026 ⏱ ≈ 20 min 🎯 Intermediate

Simulating Ball Flight With the Magnus Effect in JavaScript

A curveball in baseball, a banana free-kick in football, and a topspin forehand in tennis all rely on the same piece of physics: the Magnus force generated by a spinning ball moving through air. This tutorial builds a small, accurate ball-flight simulator from the drag equation, the Magnus force, and RK4 integration.

1. The Forces on a Spinning Ball

Once a ball leaves a foot, bat or racket, exactly three forces act on it in the air (ignoring wind):

Kutta–Joukowski intuition: spin drags a thin layer of air around with the ball via friction, speeding up flow on one side and slowing it on the other. By Bernoulli's principle, faster flow means lower pressure — so the ball gets pushed toward the low-pressure (faster-flow) side.

2. Drag Force

The standard quadratic drag equation, direction always opposing the velocity vector:

Fd = 0.5 · Cd · rho · A · v² (direction: −v̂)
function dragForce(vel, Cd, rho, area) {
  const speed = Math.hypot(vel.x, vel.y, vel.z);
  if (speed === 0) return { x: 0, y: 0, z: 0 };
  const mag = 0.5 * Cd * rho * area * speed ** 2;
  // direction opposite to velocity
  return {
    x: -mag * (vel.x / speed),
    y: -mag * (vel.y / speed),
    z: -mag * (vel.z / speed),
  };
}

3. Magnus Force From Spin

The Magnus force is proportional to the cross product of angular velocity (spin) ω and linear velocity v — it's always perpendicular to the plane containing both vectors:

F_Magnus = S · (ω × v) S = ½ · Cl · ρ · A · r (lift coefficient Cl, ball radius r)
function magnusForce(vel, omega, Cl, rho, area, radius) {
  // cross product: omega × vel
  const cross = {
    x: omega.y * vel.z - omega.z * vel.y,
    y: omega.z * vel.x - omega.x * vel.z,
    z: omega.x * vel.y - omega.y * vel.x,
  };
  const S = 0.5 * Cl * rho * area * radius;
  return { x: S * cross.x, y: S * cross.y, z: S * cross.z };
}
Backspin vs topspin vs sidespin: the spin axis determines the curve direction. Backspin (top of ball rotating backward relative to travel) with ω along the horizontal side-axis produces upward Magnus lift — this is why a well-struck golf ball or a chip shot carries further than a no-spin projectile would. Sidespin (ω roughly vertical) curves the ball left or right — the classic "banana kick" in football.

4. State Vector and Equations of Motion

The ball's state is position and velocity; spin ω is treated as approximately constant over the flight (spin decay is slow compared to flight time for most sports):

function acceleration(vel, params) {
  const { mass, Cd, Cl, rho, area, radius, omega, g } = params;
  const fDrag   = dragForce(vel, Cd, rho, area);
  const fMagnus = magnusForce(vel, omega, Cl, rho, area, radius);

  return {
    x: (fDrag.x + fMagnus.x) / mass,
    y: (fDrag.y + fMagnus.y) / mass - g,
    z: (fDrag.z + fMagnus.z) / mass,
  };
}

5. RK4 Integration

Because the drag and Magnus forces both depend on velocity in a curved, nonlinear way, simple Euler integration accumulates visible error over a long flight unless the timestep is tiny. Fourth-order Runge-Kutta (RK4) gives much better accuracy for the same computational budget:

function stepRK4(pos, vel, dt, params) {
  const a1 = acceleration(vel, params);
  const v1 = vel;

  const v2 = addScaled(vel, a1, dt / 2);
  const a2 = acceleration(v2, params);

  const v3 = addScaled(vel, a2, dt / 2);
  const a3 = acceleration(v3, params);

  const v4 = addScaled(vel, a3, dt);
  const a4 = acceleration(v4, params);

  // weighted average of the four slope estimates
  const vAvg = {
    x: (v1.x + 2*v2.x + 2*v3.x + v4.x) / 6,
    y: (v1.y + 2*v2.y + 2*v3.y + v4.y) / 6,
    z: (v1.z + 2*v2.z + 2*v3.z + v4.z) / 6,
  };
  const aAvg = {
    x: (a1.x + 2*a2.x + 2*a3.x + a4.x) / 6,
    y: (a1.y + 2*a2.y + 2*a3.y + a4.y) / 6,
    z: (a1.z + 2*a2.z + 2*a3.z + a4.z) / 6,
  };

  return {
    pos: addScaled(pos, vAvg, dt),
    vel: addScaled(vel, aAvg, dt),
  };
}

function addScaled(v, d, s) {
  return { x: v.x + d.x*s, y: v.y + d.y*s, z: v.z + d.z*s };
}

6. Comparing Spin vs No-Spin Trajectories

Running the simulation twice from the same launch conditions — once with omega = {x:0, y:0, z:0} and once with a realistic spin rate — shows exactly how much curve or extra carry the spin adds:

function simulateTrajectory(initialVel, omega, params, dt = 0.005) {
  let pos = { x: 0, y: 0, z: 0 };
  let vel = initialVel;
  const path = [{ ...pos }];
  params.omega = omega;

  while (pos.y >= 0 && path.length < 5000) {
    const next = stepRK4(pos, vel, dt, params);
    pos = next.pos;
    vel = next.vel;
    path.push({ ...pos });
  }
  return path;
}

// Straight kick vs curved "banana" free kick, same launch speed
const straight = simulateTrajectory(launchVel, { x: 0, y: 0, z: 0 }, params);
const curved   = simulateTrajectory(launchVel, { x: 0, y: 45, z: 0 }, params); // ~7.2 rev/s sidespin
Real-world scale: a football struck with strong sidespin (~8–10 rev/s) over a 20 m free kick can curve sideways by 1–1.5 m compared to a no-spin shot — enough to bend around a wall of defenders into the corner of the goal.

7. Rendering the Trajectory on Canvas

For a simple side-view or top-down render, project the 3D path onto 2D canvas coordinates and draw it as a polyline, updating per animation frame for a live "ball in flight" effect:

function drawTrajectory(ctx, path, scale, originX, originY) {
  ctx.beginPath();
  path.forEach((p, i) => {
    // side view: x = distance downrange, y = height
    const screenX = originX + p.x * scale;
    const screenY = originY - p.y * scale;
    if (i === 0) ctx.moveTo(screenX, screenY);
    else ctx.lineTo(screenX, screenY);
  });
  ctx.strokeStyle = "#4ade80";
  ctx.lineWidth = 2;
  ctx.stroke();
}

8. Tuning Constants for Real Sports

Realistic Cd, lift coefficient and mass/radius values matter more than the integrator for matching real-world trajectories:

BallMass (g)Radius (cm)CdTypical spin
Football (soccer)43011.00.20–0.250–10 rev/s
Tennis ball583.30.55–0.65up to 100 rev/s (topspin)
Baseball1453.60.3–0.420–40 rev/s
Golf ball462.10.24–0.3050–80 rev/s (backspin)

Spin rate is normally measured in RPM in sports science literature — convert to rad/s with ω = RPM · 2π / 60 before feeding it into the model above. Lift coefficient Cl is harder to pin down than Cd and is usually fitted empirically from wind-tunnel or Trackman-style tracking data per ball type; 0.1–0.3 is a reasonable starting range for most spherical sports balls at moderate spin ratios (ωr/v).

See also: for the non-spinning drag-only case (bullets, arrows, dropped objects), see our Ballistics and Aerodynamic Drag article. For how Cd itself is measured across shapes, see Drag Coefficient Cd: Streamlined Shapes.

🌀 Try the Magnus Effect simulation

See backspin and topspin bend a ball's flight in real time, and compare it side-by-side against a no-spin trajectory.

Open simulation →

Frequently Asked Questions

What will I learn in this tutorial?

Build a curved-ball flight simulator from scratch: drag force, Magnus lift from backspin/topspin, RK4 integration, and a comparison of spin vs no-spin trajectories.

What topics are covered in this tutorial?

This tutorial covers: The Forces on a Spinning Ball, Drag Force, Magnus Force From Spin, State Vector and Equations of Motion, RK4 Integration, Comparing Spin vs No-Spin Trajectories, Rendering the Trajectory on Canvas, Tuning Constants for Real Sports.

How long does this tutorial take?

This tutorial takes approximately 20 minutes to complete.

What prerequisites do I need before starting?

This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.