Pixels2GenAI
Path i Foundations
M 05 · 5.3.1 · hands-on

5.3.1 Bouncing Ball — Gravity and Collisions

Build the smallest possible physics simulation — gravity, integration, and wall collisions — and use it to feel the fundamental loop that powers every later simulation in this module.

Duration14–18 min
Levelbeginner
Load3 core concepts
Prereqs5.1.1 (particle systems)

Overview

A bouncing ball is the smallest non-trivial physics simulation. It has one moving object, one constant force (gravity), and one event type (wall collision). That is enough to require Euler integration, velocity-clamp collision response, and a per-frame loop — and those three primitives recur in every later simulation in the module, from N-body gravity to cloth dynamics. Get the bouncing ball right and you have most of the toolbox. Get it wrong — for example, by forgetting to clamp the position to the wall after a collision — and the same bug will haunt you for ten lessons. This lesson keeps the physics as small as possible so the bugs are easy to see.

Learning objectives

  1. Apply Euler integration with a constant gravity term — velocity += gravity, position += velocity.
  2. Detect wall collisions by checking each side of the canvas and respond by reflecting the velocity component.
  3. Use a damping factor on bounces to model inelastic collisions — energy loss per impact.
  4. Visualise motion with a fading trail of past positions, without storing the full history forever.

Quick start — gravity and bounce

Three lines do the physics. The first two are integration; the third is collision.

python · quick_start.py
self.vy += GRAVITY
self.x += self.vx
self.y += self.vy

if self.y + self.radius > HEIGHT:
    self.y = HEIGHT - self.radius            # clamp to wall
    self.vy = -self.vy * BOUNCE_DAMPING       # reflect velocity
Animated yellow ball bouncing inside a dark blue canvas, with an orange motion trail
Fig. 1 A ball under gravity, bouncing off all four walls, losing 12% of speed per impact. 200 frames at 30 fps.

Core concepts

Concept 1 — Euler integration with constant force

Gravity is a constant force. Every frame, it adds a fixed amount to the downward velocity. Then velocity moves the position. This is Euler’s method for solving a differential equation, and it is the simplest integrator that exists.

python · gravity_demo.py
self.vy += GRAVITY        # 1. constant force → velocity change
self.x += self.vx         # 2. velocity → position change
self.y += self.vy

Order matters slightly. The version above is semi-implicit Euler (velocity first, then position) — slightly more stable than naive Euler for systems with oscillation. Setting GRAVITY = 0.5 does not mean anything physical (it is “pixels per frame²”); we tune until the motion looks right.

Concept 2 — Collision = clamp + reflect

A naive collision check (“if y > HEIGHT, reverse vy”) has a famous bug: it lets the ball penetrate the wall before reflecting. On the next frame the ball is below the wall and vy has flipped — it should be moving up, but the same check triggers again and flips it back. The ball jitters at the wall.

The fix is to clamp first, then reflect:

python · collision_demo.py
if self.y + self.radius > HEIGHT:
    self.y = HEIGHT - self.radius           # 1. clamp position to wall
    self.vy = -self.vy * BOUNCE_DAMPING      # 2. reflect velocity, lose some energy

Now no matter how fast the ball was moving, it ends the frame exactly touching the wall, not penetrating it. The reflect step flips the sign of the component of velocity normal to the wall. For an axis-aligned wall that is just the relevant vx or vy.

Concept 3 — Trails by storing past positions

To show motion paths, store the last N positions in a list and draw each one with a brightness that fades into the past.

python · trail_demo.py
self.trail.append((self.x, self.y))
if len(self.trail) > TRAIL_LEN:
    self.trail.pop(0)

for i, (tx, ty) in enumerate(ball.trail):
    fade = (i + 1) / len(ball.trail)
    colour = tuple(int(c * fade) for c in TRAIL_COLOUR)
    draw.ellipse([tx - r, ty - r, tx + r, ty + r], fill=colour)

The trail is bounded (TRAIL_LEN is constant), the fade is linear in age, and each drawn dot can shrink as it ages too — those three knobs are everything that distinguishes a comet tail from a fairy-dust shimmer.

Exercises

Three exercises in Execute → Modify → Create order.

EXECUTE I.

Run the bouncer

Download bouncing_ball.py and run it. It writes bouncing_ball.gif and a still frame.

bouncing_ball.py — complete reference implementation

Reflection questions

  • The ball never quite stops bouncing in the GIF. With damping = 0.88, why?
  • What is the difference between the ball’s behaviour with INITIAL_VEL = (5.2, 0.0) and (0.0, 5.2)?
  • The trail looks like a comet tail. What would change if you appended to the trail at the start of step() instead of the end?
MODIFY II.

Tune the physics

Goals

  1. Perfectly elastic: BOUNCE_DAMPING = 1.0. The ball bounces forever — useful for diagnosing collision bugs.
  2. No gravity, just inertia: GRAVITY = 0.0. Pure pool-ball motion.
  3. Heavy ball: RADIUS = 40, INITIAL_VEL = (3.0, 0.0). The ball fills the canvas and bounces become more frequent.
  4. Low gravity, slow horizontal: GRAVITY = 0.15, INITIAL_VEL = (2.5, 0.0). Moon-like — long lazy arcs.
CREATE III.

Implement the physics from scratch

Use bouncing_ball_starter.py. The render loop is wired up. Three TODOs inside Ball.step() ask for gravity, integration, and the four wall collisions.

bouncing_ball_starter.py — Exercise 3 scaffold

Implement

  1. TODO 1 — add GRAVITY to self.vy.
  2. TODO 2 — advance self.x by self.vx, self.y by self.vy.
  3. TODO 3 — for each of the four walls (left, right, top, bottom), clamp the position then flip the relevant velocity with BOUNCE_DAMPING.

Make it your own

  • Multiple balls: spawn 50 balls with random initial velocities. With no ball-ball collisions they just bounce off the walls.
  • Ball-ball collisions: add a check that swaps velocities when two balls overlap. Now you have a billiard simulation.
  • Sloped floor: replace the bottom wall with a diagonal line. The reflection becomes 2D (flip the normal component of velocity).
  • Drag in air: self.vx *= 0.998 and self.vy *= 0.998 each frame. The ball slowly stops without any wall-bounce energy loss.

Downloads

bouncing_ball.py — full reference implementation bouncing_ball_starter.py — Exercise 3 scaffold

Summary

Common pitfalls to avoid

  • Reflecting velocity without clamping position. The ball penetrates the wall and the simulation jitters at the boundary.
  • Adding gravity twice — once in step, once in the wall response. The ball accelerates at every bounce.
  • Storing the trail forever. Memory grows linearly with simulation length.
  • Drawing the trail after the ball. The ball is hidden by its own trail tail.
  • Using int for position. Sub-pixel motion vanishes and slow balls freeze.

References

  1. [1] Shiffman, D. (2012). The Nature of Code, Chapter 1: Vectors. natureofcode.com
  2. [2] Witkin, A. (1997). Physically Based Modeling: Principles and Practice. SIGGRAPH Course Notes.
  3. [3] Burden, R. L., & Faires, J. D. (2015). Numerical Analysis (10th ed.), Chapter 5. Cengage Learning.
  4. [4] Hecker, C. (1996). Physics, the next frontier. Game Developer Magazine, October 1996. chrishecker.com/Rigid_Body_Dynamics
  5. [5] Harris, C. R., et al. (2020). Array programming with NumPy. Nature, 585, 357–362. doi:10.1038/s41586-020-2649-2