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):
-
Gravity: constant, straight down,
F_g = m·g. - Drag: opposes velocity, magnitude grows with v², shape-dependent via Cd.
- Magnus force: perpendicular to both the velocity vector and the spin axis. This is what bends a curving free kick sideways, or makes topspin dip a tennis ball down faster than gravity alone would.
2. Drag Force
The standard quadratic drag equation, direction always opposing the velocity vector:
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:
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 };
}
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
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:
| Ball | Mass (g) | Radius (cm) | Cd | Typical spin |
|---|---|---|---|---|
| Football (soccer) | 430 | 11.0 | 0.20–0.25 | 0–10 rev/s |
| Tennis ball | 58 | 3.3 | 0.55–0.65 | up to 100 rev/s (topspin) |
| Baseball | 145 | 3.6 | 0.3–0.4 | 20–40 rev/s |
| Golf ball | 46 | 2.1 | 0.24–0.30 | 50–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).
🌀 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.