A tennis ball spends about a second or less in the air on each shot, and a lot of physics happens in that second. Air drag slows it hard, spin bends its path, the racket's shape decides whether the impact stings your hand, and the court surface changes how it bounces. Three labs: spin in flight, the sweet spots on a racket, and the bounce plus the time a returner has to react to a serve. The ball here follows the ITF rules: 2.0–2.1 oz and 2.57–2.70 in across. The model uses the middle of each range.
Three forces act on a ball in flight. Gravity pulls it down. Drag is air resistance, pushing straight back against the motion; at rally speeds it is stronger than gravity. The third comes from spin. A spinning ball drags a thin layer of air around with it, which bends the air flowing past to one side, and the air pushes the ball the other way. This sideways push is the Magnus force. Topspin (the top of the ball rolls forward) pushes the ball down, so it dips into the court. Slice (backspin, the top rolls backward) pushes it up, so it floats. The court below is drawn to scale: long, net high at the center.
How to read this: everything is drawn at true scale, the same distance per pixel across and up, which is why the arcs look so flat: a rally ball rarely rises more than a few feet over a court. The hitter stands at the left baseline. All three curves start with the same speed, angle and height and differ only in spin. The bold curve is the spin you picked; its numbers are on the right. The strip on top magnifies the outlined box around the net at one uniform zoom (still equal scale across and up), with each ball drawn at its true size where it passes the net and its clearance over the net tape.
Try this: raise the launch angle until the flat ball just lands out, then switch to topspin: the same swing now drops in well short of the baseline.
m is the ball's mass, A = πr² its cross-section, ρ = 1.225 kg/m³ the air density, v the speed and n̂ the direction at right angles to the motion (down for topspin, up for slice). ω is the spin in radians per second (rpm × 2π/60) and r·ω is how fast the ball's surface moves. The CL formula is an approximate fit of the kind published from wind-tunnel and launch tests: more spin compared with speed gives more lift, leveling off near 0.5. RK4 (fourth-order Runge–Kutta) advances the motion in small time steps by averaging four slope estimates per step.
Topspin lets a player swing hard and aim high over the net, because the downward Magnus push brings the ball back into the court. At 2,500 rpm and rally speed it adds an extra downward push of about half to two-thirds of the ball's weight. It also makes the ball come down steeper, which helps it kick up after the bounce (Lab 3). Slice does the opposite: the upward push lets it float long and low.
The Magnus force is lift, the same idea as a wing turning air in E04 · Why planes fly, here made by spin. Spin also keeps the ball's axis pointing steadily in flight, the same gyroscopic stiffness discussed for spinning wheels in E01 · Why bikes stay up.
import math g, rho = 9.81, 1.225 m, r = 0.0577, 0.0335 # ball mass kg, radius m (middle of ITF ranges) A, CD = math.pi*r*r, 0.55 # cross-section m², typical drag coefficient k = 0.5*rho*A/m def accel(vx, vy, w): # w > 0 topspin, w < 0 slice (rad/s) v = math.hypot(vx, vy) CL = 1/(2 + v/(r*abs(w))) if w else 0.0 # approximate empirical fit s = math.copysign(1, w) return (-k*CD*v*vx + s*k*CL*v*vy, -g - k*CD*v*vy - s*k*CL*v*vx) def rk4(q, w, dt): def f(q): ax, ay = accel(q[2], q[3], w) return [q[2], q[3], ax, ay] k1 = f(q) k2 = f([a + dt/2*b for a, b in zip(q, k1)]) k3 = f([a + dt/2*b for a, b in zip(q, k2)]) k4 = f([a + dt*b for a, b in zip(q, k3)]) return [a + dt/6*(b + 2*c + 2*d + e) for a, b, c, d, e in zip(q, k1, k2, k3, k4)] def fly(v0, deg, rpm, h=1.0, dt=0.002): a, w = math.radians(deg), rpm*2*math.pi/60 q, t, peak = [0, h, v0*math.cos(a), v0*math.sin(a)], 0.0, h while True: n = rk4(q, w, dt) if n[1] <= r: # ball touches the ground f = (q[1] - r)/(q[1] - n[1]) return q[0] + f*(n[0] - q[0]), peak, t + f*dt q, t, peak = n, t + dt, max(peak, n[1]) for name, rpm in [("topspin", 2500), ("flat", 0), ("slice", -2500)]: x, peak, t = fly(30, 8, rpm) print(f"{name:8s} lands {x:5.2f} m peak {peak:.2f} m {t:.2f} s (court 23.77 m)")
Players talk about "the sweet spot" as if there were one. Physics finds several, each answering a different question. The center of percussion (COP) is the impact point where the handle does not jerk in your hand: the push from the ball shoves the racket back and also spins it, and at the COP those two motions cancel exactly at the hand. The vibration node is where an impact barely sets the frame ringing, so it feels clean. And the power spot is where the ball comes off fastest. Click anywhere on the strings.
How to read this: the butt of the handle is at the left, the tip at the right. Brighter colors on the strings mean more of whatever the color map shows. The crosshair is where the ball hits; the gold line marks the center of percussion for your hand position and the green dashed line the vibration node.
Try this: set Swing speed to 0 (racket held still, ball thrown at it) and watch the fastest-rebound ring jump down toward the throat; then swing again and watch it climb.
How to read this: each curve is scaled so its largest value on the strings is 1. Hand jolt falls to zero at the COP and grows on either side. Vibration falls to zero at the node. The marked band is the string bed.
Try this: slide the hand toward the butt and watch the COP (where the red curve touches zero) slide down toward the green node.
Treat the racket as a pendulum hanging from the hand: m is its mass, d the distance from the hand to its balance point (center of mass), and I its moment of inertia, which measures how hard it is to spin. b is the COP's distance from the hand. Meff is the effective mass the ball feels at a point s along and x across from the balance point, e = 0.85 is the ball–string bounciness for a clamped racket (typical), and eA is the bounciness the ball sees from a free racket. φ is the shape of the frame's first bending vibration, with ξ the fraction of the length from the butt; its zeros are the nodes.
The COP depends on where you hold the racket; move the hand and the COP moves. The node depends only on how the frame bends: a uniform free beam has nodes at 22.4% and 77.6% of its length. The other node sits in the handle, close to where the hand is, which is part of why a hit at the string node feels so clean. Both the COP and the node land near the middle of the strings on a typical racket, which is why players feel one sweet spot.
Rebound speed is a different story. A racket held still returns the ball fastest near the throat, the part of the strings closest to the balance point, where the ball feels the largest effective mass. In a real swing the head moves faster than the throat, which shifts the fastest rebound up toward the center. Torque about the hand works just like a lever: see E03 · Levers, pulleys & gears.
import math L, m, bal = 0.685, 0.300, 0.33 # length m, mass kg, balance point from butt m I10 = 0.032 # kg·m² about an axis 10 cm from the butt Icm = I10 - m*(bal - 0.10)**2 # parallel-axis theorem Iroll, eS, mb = 0.0014, 0.85, 0.0577 p = 0.08 # hand pivot, m from the butt d = bal - p Ip = Icm + m*d*d cop = p + Ip/(m*d) # center of percussion, from the butt def hand_share(z): # share of the ball's impulse the hand supplies return 1 - (z - p)*m*d/Ip def e_apparent(z, x=0.0): # free racket hit z along, x across M = 1/(1/m + (z - bal)**2/Icm + x*x/Iroll) return (eS*M - mb)/(M + mb) BL = 4.730040745 # free-free beam, first mode sig = (math.cosh(BL) - math.cos(BL))/(math.sinh(BL) - math.sin(BL)) phi = lambda s: math.cosh(BL*s) + math.cos(BL*s) - sig*(math.sinh(BL*s) + math.sin(BL*s)) def node(a, b): # bisection for phi = 0 for _ in range(60): c = (a + b)/2 a, b = (a, c) if phi(a)*phi(c) <= 0 else (c, b) return (a + b)/2 print(f"COP {cop*100:.1f} cm, nodes at {node(0, .5):.4f} L and {node(.5, 1):.4f} L") for z in (0.40, 0.505, 0.60, 0.67): print(f"z={z:.3f} m e_A={e_apparent(z):.3f} hand {hand_share(z):+.2f} vib {abs(phi(z/L))/2:.2f}")
When a ball lands, the court pushes up on it and also grabs the bottom of the ball with friction. Two numbers describe a court. The coefficient of restitution ey is the fraction of the downward speed the ball gets back as upward speed. The friction coefficient μ sets how hard the surface can grab. Friction slows the ball and changes its spin. If friction is strong enough, the bottom of the ball stops sliding and the ball leaves rolling (the "grip" case); otherwise it slides the whole time. Clay grabs hard and plays slow; grass is slick and plays fast and low.
How to read this: balls come in from the left and bounce at the marked point. Height and distance use the same scale. In "Same arrival" all three arrive at the same speed and angle, so only spin differs. In "Shots from Lab 1" each ball arrives the way it landed in Lab 1, steeper for topspin and flatter for slice.
Try this: on Clay, compare the slice and topspin balls: the slice loses more speed and sits up at a steeper angle, while the topspin ball shoots forward. Then switch to Grass and watch every ball stay lower.
How to read this: a right-handed server stands at the left, hitting from about 1.5 times their own height (a rough rule for a full reach with the racket; a 6′1″ player makes contact near ). Every serve is aimed so its first bounce lands 1 ft inside the service line at the chosen spot. The top chart is the side view with a magnified strip around the net; the middle chart looks down on the court from above, so you can see the ball curve sideways in the air and change direction at the bounce. Both are drawn at true scale. The bold path is the serve you picked; the two faint paths are the other serve types at their typical speed and spin, aimed at the same spot. The bar at the bottom is a clock: it starts when the racket hits the ball and ends when the ball reaches the returner.
Try this: pick Slice, serve from Deuce and aim Wide: in the top view the ball bends toward the server's left and reaches the returner outside the singles sideline. Then pick Kick, serve from Ad and aim Wide: it bends left in the air, then jumps right off the court and sits up higher.
vx and vy are the ball's forward and vertical speeds, ω its spin (positive for topspin) and u how fast the bottom of the ball slides forward over the court. F is the friction impulse per unit mass. Topspin makes the bottom of the ball move backward, so it slides less, needs less friction to start rolling and loses less forward speed. Slice does the opposite. Angular momentum about the contact point is the same before and after, a check on the formulas.
A fast pro serve takes roughly 0.4 to 0.6 seconds to reach the returner at the baseline; a slower kick serve gives them closer to a second. A simple visual reaction takes about 0.2 seconds (typical lab value; it varies by person and task), and the swing needs time on top of that. So returners start moving before the ball crosses the net, reading the server's toss and body. Drag takes a big bite: a serve reaches the returner at around half its starting speed.
All three serves spin; what differs is how fast and around which axis. The spin axis is an arrow: the ball turns around it the way a wheel turns around its axle. Only the part of the spin across the direction of flight makes a Magnus force (ω⊥), and that force points at right angles to both the spin axis and the motion. Flat serves are the fastest and spin least, but measured pro "flat" serves still turn at about 1,200 rpm, mostly sideways. A slice serve (about 2,200 rpm) spins around an axis tipped only 22° from upright, like a top, so the push is mostly sideways: for a right-hander the ball curves to the server's left. This is a different spin from a slice groundstroke, which is backspin. A kick serve (about 3,200 rpm) tips the axis further over, which adds topspin: the ball dips into the box, comes down steeper and bounces higher.
The kick's axis also leans toward the net. At the bounce, that lean makes the bottom of the ball rub sideways across the court, and friction throws the ball the other way: to the server's right, opposite to its curve in the air. Spin around an upright axis cannot do this, because the bottom of the ball sits on the axis and just twists in place, so a slice keeps almost the same direction off the bounce.
import math g, rho, CD, alpha = 9.81, 1.225, 0.55, 0.55 m, r = 0.0577, 0.0335 k = 0.5*rho*math.pi*r*r/m L, NET, SVC, NETC, NETP, POST = 23.77, 11.885, 6.40, 0.914, 1.07, 6.40 hard = (0.60, 0.77) # (mu, e_y), approx. # right-handed server: (speed m/s, rpm, axis elevation deg, lean toward the net deg) serves = {"flat": (52.0, 1217, 65.6, 0), "slice": (46.4, 2216, 68.4, 0), "kick": (39.2, 3213, 54.5, 20)} def cross(a, b): return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]] def net_at(y): return NETC + (NETP - NETC)*min(abs(y), POST)/POST def accel(v, w): # x down the court, y to the server's left, z up s = math.sqrt(sum(c*c for c in v)) a = [-k*CD*s*v[0], -k*CD*s*v[1], -g - k*CD*s*v[2]] f = sum(wi*vi for wi, vi in zip(w, v))/(s*s) wp = [wi - f*vi for wi, vi in zip(w, v)] # spin across the flight ws = math.sqrt(sum(c*c for c in wp)) if ws: cl, n = 1/(2 + s/(r*ws)), cross(wp, v) # same C_L fit as Lab 1 a = [ai + k*cl*s*ni/ws for ai, ni in zip(a, n)] return a def rk4(q, w, dt): f = lambda q: q[3:] + accel(q[3:], w) k1 = f(q); k2 = f([a + dt/2*b for a, b in zip(q, k1)]) k3 = f([a + dt/2*b for a, b in zip(q, k2)]); k4 = f([a + dt*b for a, b in zip(q, k3)]) return [a + dt/6*(b + 2*c + 2*d + e) for a, b, c, d, e in zip(q, k1, k2, k3, k4)] def fly(q, w, x_stop=60, dt=0.002): # until the ball touches the ground or passes x_stop t = 0.0; netz = nety = None while True: n = rk4(q, w, dt) if netz is None and q[0] < NET <= n[0]: f = (NET - q[0])/(n[0] - q[0]); netz, nety = q[2] + f*(n[2] - q[2]), q[1] + f*(n[1] - q[1]) for i, lim in ((2, r), (0, x_stop)): if (n[i] <= lim) if i == 2 else (n[i] >= lim): f = (q[i] - lim)/(q[i] - n[i]) return [a + f*(b - a) for a, b in zip(q, n)], t + f*dt, netz, nety q, t = n, t + dt def bounce(v, w, mu, e): # friction against the slip of the contact patch Jn = (1 + e)*(-v[2]) u = [v[0] - r*w[1], v[1] + r*w[0]] F = [-c*alpha/(1 + alpha) for c in u] # impulse that would make it roll if math.hypot(*F) > mu*Jn: um = math.hypot(*u); F = [-mu*Jn*c/um for c in u] return [v[0] + F[0], v[1] + F[1], -e*v[2]], [w[0] + F[1]/(alpha*r), w[1] - F[0]/(alpha*r), w[2]] def spin(rpm, el, lean, ps): # axis in (forward, left, up), turned to aim direction ps e, l = math.radians(el), math.radians(lean) a = [math.sin(l), math.cos(l)*math.cos(e), math.cos(l)*math.sin(e)] c, s, w = math.cos(math.radians(ps)), math.sin(math.radians(ps)), rpm*2*math.pi/60 return [w*(a[0]*c - a[1]*s), w*(a[0]*s + a[1]*c), w*a[2]] def start(V, h, y0, th, ps): t, p = math.radians(th), math.radians(ps) return [0, y0, h, V*math.cos(t)*math.cos(p), V*math.cos(t)*math.sin(p), V*math.sin(t)] def aim(V, h, y0, tx, ty, sv): # Newton's method on (launch angle, aim direction) land = lambda th, ps: fly(start(V, h, y0, th, ps), spin(*sv[1:], ps))[0][:2] th, ps = -5.0, math.degrees(math.atan2(ty - y0, tx)) for _ in range(15): x, y = land(th, ps); ex, ey = x - tx, y - ty if math.hypot(ex, ey) < 1e-4: break d = 0.01; (xa, ya), (xb, yb) = land(th + d, ps), land(th, ps + d) j11, j21, j12, j22 = (xa - x)/d, (ya - y)/d, (xb - x)/d, (yb - y)/d det = j11*j22 - j12*j21; cap = lambda z: max(-4, min(4, z)) th = max(-25, min(15, th - cap((j22*ex - j12*ey)/det))); ps -= cap((-j21*ex + j11*ey)/det) return th, ps h, side, aim_y = 1.5*1.8542, -1, 3.8 # 6'1" server, ad court, aimed wide for name, sv in serves.items(): V = sv[0]; y0, tx, ty = -side*0.6, NET + SVC - 0.3, side*aim_y th, ps = aim(V, h, y0, tx, ty, sv); w = spin(*sv[1:], ps) q, t1, nz, ny = fly(start(V, h, y0, th, ps), w) v2, w2 = bounce(q[3:], w, *hard) q2, t2, _, _ = fly([q[0], q[1], r] + v2, w2, x_stop=L) curve = q[1] - (y0 + math.tan(math.radians(ps))*q[0]) turn = math.degrees(math.atan2(v2[1], v2[0]) - math.atan2(q[4], q[3])) print(f"{name:5s} launch {th:+.2f} deg, clears net by {100*(nz - r - net_at(ny)):.0f} cm, curves {curve:+.2f} m, " f"turns {turn:+.1f} deg at the bounce, reaches the baseline after {t1 + t2:.3f} s at {q2[2]:.2f} m high")
import math r, alpha = 0.0335, 0.55 courts = {"clay": (0.80, 0.80), "hard": (0.60, 0.77), "grass": (0.50, 0.70)} # (mu, e_y), approx. def bounce(vx, vy, w, mu, e): # vy < 0 arriving; w > 0 topspin (rad/s) Jn = (1 + e)*(-vy) # normal impulse per kg u = vx - r*w # slip at the contact point F = -u*alpha/(1 + alpha) # impulse that would stop the slip (rolling) grip = abs(F) <= mu*Jn if not grip: F = -math.copysign(mu*Jn, u) return vx + F, -e*vy, w - F/(alpha*r), grip v, ang = 25, math.radians(16) for name, rpm in [("topspin", 2500), ("flat", 0), ("slice", -2500)]: vx2, vy2, w2, grip = bounce(v*math.cos(ang), -v*math.sin(ang), rpm*2*math.pi/60, *courts["hard"]) print(f"{name:8s} {'grips ' if grip else 'slides'} {math.hypot(vx2, vy2):5.1f} m/s at " f"{math.degrees(math.atan2(vy2, vx2)):4.1f} deg, spin {w2*60/(2*math.pi):5.0f} rpm") # the serve, in 3D with spin, is in the last Code panel