webdevpuneet.com

Physics Balls HTML CSS JS — Canvas Gravity Simulation

Physics Balls · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="app">
  <canvas id="canvas"></canvas>
  <div class="controls">
    <label>Gravity <span id="gravVal">0.5</span>
      <input type="range" id="gravSlider" min="0" max="2" step="0.05" value="0.5">
    </label>
    <label>Bounce <span id="bounceVal">0.75</span>
      <input type="range" id="bounceSlider" min="0.3" max="1.0" step="0.05" value="0.75">
    </label>
    <label class="toggle-label">
      <input type="checkbox" id="trailToggle"> Trail
    </label>
    <button id="clearBtn">Clear All</button>
    <span class="hint">Click canvas to spawn balls</span>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

requestAnimationFrame loop: refresh-synced, battery-friendly animation at the display frame rate
Semi-implicit Euler integration: stable gravity simulation with velocity updated before position
Restitution coefficient: adjustable energy loss on wall bounces for realistic settling
Elastic collision response: mass-weighted impulse exchange along the collision normal
Positional overlap correction: separates interpenetrating balls to prevent sinking and jitter
Mass from area: mass = radius squared so big balls realistically push small ones
Trail effect: translucent fillRect overlay fades old frames instead of clearRect
Radial gradient shading: createRadialGradient fakes a light source for a glossy 3D look
HSL color generation: evenly spaced hues produce distinct, vivid ball colors
Click-to-spawn: getBoundingClientRect maps clicks into canvas space with an upward launch velocity

About this UI Snippet

How to Build a Canvas Physics Simulation with Gravity and Collisions

Screenshot of the Physics Balls snippet rendered live

A physics balls simulation drops colorful spheres onto a canvas where they fall under gravity, bounce off the walls, and collide elastically with one another. Click anywhere to launch a new ball upward. It is one of the clearest ways to learn real-time animation, vector math and 2D collision response in the browser using nothing but the HTML5 Canvas 2D API and requestAnimationFrame. This walkthrough explains every piece.

The animation loop

At the heart of the simulation is a fixed-step loop driven by requestAnimationFrame. The loop function calls update() to advance the physics, then draw() to render the frame, then schedules itself again. requestAnimationFrame synchronizes with the display refresh (typically 60Hz), pauses automatically in background tabs to save battery, and gives smooth, tear-free motion. Using a constant per-frame step (rather than scaling by elapsed time) keeps the math simple and stable for a demo, though a production engine would normalize by delta-time.

The ball model

Each ball is a plain object holding position (x, y), velocity (vx, vy), radius r, a hue for color, and a mass. The mass is set to r * r — proportional to area — so larger balls carry more momentum and shove smaller ones aside realistically. When you click, spawnBall reads getBoundingClientRect to convert the click into canvas coordinates, assigns a random radius, a small random horizontal velocity, and a strong upward velocity (vy = -8 - random) so the ball leaps before gravity pulls it down. A MAX_BALLS cap shifts the oldest ball off the array to bound the per-frame collision cost.

Gravity and integration

In update, every ball first has gravity applied: b.vy += GRAVITY. This constant downward acceleration is the discrete form of v = v + a*t. Then position is integrated with b.x += b.vx; b.y += b.vy — semi-implicit (symplectic) Euler integration, which uses the freshly updated velocity and stays stable for gravity-driven motion far better than naive explicit Euler.

Wall collisions and restitution

After moving, each ball is tested against the four canvas edges. If a ball's edge crosses a wall — for example b.x - b.r < 0 — its position is clamped back inside (b.x = b.r) and its velocity component is reflected and scaled by the restitution coefficient BOUNCE: b.vx = Math.abs(b.vx) * BOUNCE. Restitution below 1 means energy is lost on each bounce, so balls gradually settle; a value of 1 would bounce forever. Using Math.abs and forcing the correct sign guarantees the ball moves away from the wall even if floating-point drift left it slightly embedded.

Ball-to-ball elastic collisions

The most educational part is the pairwise collision response. A double loop checks every unique pair (i, j). Two circles overlap when the distance between centers is less than the sum of their radii: sqrt(dx*dx + dy*dy) < a.r + b.r. When they do, resolveCollision runs.

It first computes the collision normal — the unit vector along the line connecting the two centers: nx = dx/dist, ny = dy/dist. It then finds the relative velocity along that normal via the dot product dot = (a.vx - b.vx)*nx + (a.vy - b.vy)*ny. If dot > 0 the balls are already separating, so it returns to avoid sticking. Otherwise it applies the 1D elastic collision impulse projected onto the normal: impulse = 2 * dot / (a.mass + b.mass). Each ball's velocity is adjusted by the impulse weighted by the other ball's mass — a.vx -= impulse * b.mass * nx and b.vx += impulse * a.mass * nx. This is the standard conservation-of-momentum-and-energy formula for equal-restitution collisions, exchanging velocity in proportion to mass.

Finally it resolves overlap. Because discrete steps let balls interpenetrate, the code pushes them apart by half the overlap each along the normal: overlap = (a.r + b.r - dist) / 2. Without this positional correction, balls would jitter or sink into each other.

Rendering and the trail effect

draw offers two modes. Normally it clears the canvas each frame with clearRect and repaints the dark background. With the trail toggle on, instead of clearing it paints a translucent rectangle over the whole canvas: fillStyle = 'rgba(13,13,26,0.15)' then fillRect. Each frame slightly darkens the previous one, so old positions fade out gradually, leaving comet-like motion trails. This is a classic and cheap canvas technique — never clearing, only fading.

Each ball is drawn with a createRadialGradient offset toward the top-left to fake a light source, giving a glossy 3D sphere look, with the two gradient stops derived from the ball's hue in HSL color space. HSL makes it trivial to generate visually distinct, evenly spaced colors by spacing hues — here (hueCounter * 37) % 360 walks around the color wheel.

Controls

Sliders bound to GRAVITY and BOUNCE let you feel how acceleration and restitution change the behavior live, the checkbox toggles trails, and Clear empties the array. Because all state lives in the balls array and two scalars, the controls simply mutate those values and the next animation frame reflects them.

Put together, this simulation demonstrates the core loop of every 2D physics engine: integrate forces, detect collisions, resolve them with impulses and positional correction, and render — all in a couple hundred lines with no dependencies.

Step by step

How to Use

  1. 1
    Spawn ballsClick anywhere on the canvas to launch a new ball upward with a random size and color.
  2. 2
    Adjust gravityDrag the gravity slider to change how strongly balls accelerate downward each frame.
  3. 3
    Tune the bounceDrag the bounce slider to set the restitution coefficient — lower values lose more energy on impact.
  4. 4
    Enable trailsToggle the trail checkbox to fade old frames instead of clearing them, leaving motion trails.
  5. 5
    Watch collisionsSpawn several balls and watch them collide elastically, exchanging velocity based on their mass.
  6. 6
    Clear the sceneClick Clear All to empty the simulation and start fresh.

Real-world uses

Common Use Cases

Game physics prototyping
A starting point for casual games needing gravity and bouncing, alongside other animations like text particles.
Teaching collision math
Demonstrate vectors, dot products, restitution and impulse resolution interactively for physics or graphics students.
Generative backgrounds
Drive ambient, animated hero backgrounds with floating, colliding spheres.
Data visualization
Adapt the engine for bubble simulations or force-directed layouts where nodes repel and settle.
Interactive landing pages
Add a playful click-to-spawn toy that keeps visitors engaged on a marketing page.
Canvas API reference
A compact example of the requestAnimationFrame loop and gradient rendering next to a drawing canvas.

Got questions?

Frequently Asked Questions

Mass proportional to area (radius squared) makes the collision response feel right — larger balls have more momentum and push smaller ones around, while equal-size balls exchange velocity symmetrically.

It sets the restitution coefficient applied on every wall hit. The reflected velocity is multiplied by this value, so below 1 the ball loses energy and eventually settles, and at 1 it would bounce indefinitely.

The dot of relative velocity and the collision normal tells whether the balls are approaching or separating. If they are already moving apart, applying an impulse would incorrectly pull them back together, so the function returns early.

Instead of clearing the canvas each frame, it paints a low-alpha rectangle over everything. Previous frames are darkened a little each time, so older positions fade out smoothly, producing motion trails.

With discrete time steps a fast ball can move past the contact point before collision is detected. The code corrects this by pushing overlapping balls apart by half the penetration depth along the normal each frame.

Yes. Export with the JSX, Vue, Angular, or Tailwind buttons. In React, run the requestAnimationFrame physics loop inside useEffect and cancel it in the cleanup return; keep the ball array in a ref rather than state, since it mutates 60 times per second without needing re-renders.