Bouncy Bubbles animation

This is my port of the Processing code that you referenced. It doesn't try to optimize, so I didn't try it either, for example I didn't use Nearest to find collisions even though that would be fast since it uses a quadtree. I found that I didn't need those optimizations to recreate the animation on the previously linked to website.

About your questions:

1) To make the animation look smooth you should use RunScheduledTask to make sure the updates happen uniformly. I use

RunScheduledTask[balls = step[balls], 0.02]

which means that I get an FPS of 50 frames per second. Of course I have to make sure that balls = step[balls] can be executed within 0.02 seconds; step[balls] // AbsoluteTiming tells me that one step takes 0.002 seconds to execute, meaning it's fast enough to keep up with the desired FPS.

2) This is what the code is all about.

3) It should be trivial to adapt the code for the three dimensional case, this is left as an exercise.

Code:

numBalls = 12;
spring = 0.05;
gravity = 0.03;
friction = -0.9;
width = 640;
height = 360;

balls = ball @@@ Transpose[{
     RandomReal[{0, width}, numBalls],
     RandomReal[{0, height}, numBalls],
     ConstantArray[0, numBalls],
     ConstantArray[0, numBalls],
     RandomReal[{30, 70}, numBalls]
     }];

ballCollide[ball[x1_, y1_, vx1_, vy1_, d1_], 
  ball[x2_, y2_, vx2_, vy2_, d2_]] := Module[{targetX, targetY}, If[
   Norm[{x2 - x1, y2 - y1}] < (d1/2 + d2/2),
   {targetX, targetY} = AngleVector[{x1, y1},{d1/2 + d2/2, ArcTan[x2 - x1, y2 - y1]}];
   ball[x1, y1, vx1 - (targetX - x2) spring, vy1 - (targetY - y2) spring, d1],
   ball[x1, y1, vx1, vy1, d1]
   ]]

boundaryCollide[ball[x_, y_, vx_, vy_, d_]] := Which[
  x + d/2 > width, ball[width - d/2, y, vx friction, vy, d],
  x - d/2 < 0, ball[d/2, y, vx friction, vy, d],
  y + d/2 > height, ball[x, height - d/2, vx, vy friction, d],
  y - d/2 < 0, ball[x, d/2, vx, vy friction, d],
  True, ball[x, y, vx, vy, d]
  ]

move[ball[x_, y_, vx_, vy_, d_]] := ball[x + vx, y + vy - gravity, vx, vy - gravity, d]

step[balls_] := Module[{new},
  new = Fold[ballCollide, #, Complement[balls, {#}]] & /@ balls;
  new = move /@ new;
  boundaryCollide /@ new
  ]

disk[ball[x_, y_, vx_, vy_, d_]] := Disk[{x, y}, d/2]

draw[balls_] := Graphics[{
   White, Opacity[0.8],
   disk /@ balls
   },
  PlotRange -> {{0, width}, {0, height}},
  Background -> Black,
  ImageSize -> width
  ]

To run it, first create a dynamic cell using

Dynamic@draw[balls]

then schedule updates at a uniform pace:

RunScheduledTask[balls = step[balls], 0.02]

To stop it, simply remove the scheduled task:

RemoveScheduledTask[ScheduledTasks[]]

What it looks like:


data generates n balls, here: 10

Note that it might be wise to make the box larger, if different WhenEvent[]s happen at the same time (both box and ball collision), one can overwrite the other.

data = Table[{RandomReal[{-0.85, 0.85}, {3}], 
RandomReal[{-1, 1}, {3}], 
RGBColor[RandomReal[{-1, 1}], RandomReal[{-1, 1}], 
 RandomReal[{-1, 1}]]}, {i, 10}];

I've adapted this from my n-body simulation:

Generating lists for use in the NDSolve[]

n = Length[data];
positions = Transpose[Table[data[[i, 1]], {i, n}]];
velocities = Transpose[Table[data[[i, 2]], {i, n}]];
(*Setting up stuff for NDSolve[]*)
{xt, yt, 
   zt} = {ToExpression[Table["x" <> ToString[i], {i, n}]], 
   ToExpression[Table["y" <> ToString[i], {i, n}]], 
   ToExpression[Table["z" <> ToString[i], {i, n}]]};
{xm, ym, zm} = {Through[xt[t]], Through[yt[t]], Through[zt[t]]};
{xz, yz, zz} = {Through[xt[0]], Through[yt[0]], Through[zt[0]]};
yt = ToExpression[Table["y" <> ToString[i], {i, n}]];
zt = ToExpression[Table["z" <> ToString[i], {i, n}]];
rm = Flatten[
    Table[If[i != j, 
      Sqrt[(xm[[j]] - xm[[i]])^2 + (ym[[j]] - ym[[i]])^2 + (zm[[j]] - 
           zm[[i]])^2]], {i, n}, {j, n}]] /. Null -> Sequence[];

No friction or gravity:

xf = Thread[D[D[xm, t], t] == ConstantArray[0, n]];
yf = Thread[D[D[ym, t], t] == ConstantArray[0, n]];
zf = Thread[D[D[zm, t], t] == ConstantArray[0, n]];

Friction and gravity:

xf = Thread[D[D[xm, t], t] == -0.01 D[xm, t]];
yf = Thread[D[D[ym, t], t] == -0.01 D[ym, t]];
zf = Thread[D[D[zm, t], t] == -0.01 D[zm, t] - ConstantArray[0.5, n]];

The final equations for the differential equation:

pos = {Thread[xz == positions[[1]]], Thread[yz == positions[[2]]], 
   Thread[zz == positions[[3]]]};
vel = {Thread[D[xm, t] == velocities[[1]]] /. t -> 0, 
   Thread[D[ym, t] == velocities[[2]]] /. t -> 0, 
   Thread[D[zm, t] == velocities[[3]]] /. t -> 0};
col = Table[data[[i, 3]], {i, n}];
we = {};

A list of WhenEvent[]s, note that the ball collisions aren't exactly right, I'm too tired right now to properly add elastic collision equations, feel free to edit the code. The first set is the ball collisions, the second is the boundary box collisions. In these WhenEvent[] collisions we can reduce total energy by little amounts per collision, as in the animation.

Table[AppendTo[we, 
   WhenEvent[
    Sqrt[(xm[[i]] - xm[[j]])^2 + (ym[[i]] - ym[[j]])^2 + (zm[[i]] - 
           zm[[j]])^2] <= 0.2 // Evaluate, {
      D[xm[[i]], t] -> 0.5 (D[xm[[i]], t] + D[xm[[j]], t]), 
      D[xm[[j]], t] -> 0.5 (D[xm[[i]], t] + D[xm[[j]], t]), 
      D[xm[[i]], t] -> 0.5 (D[ym[[i]], t] + D[ym[[j]], t]), 
      D[xm[[j]], t] -> 0.5 (D[ym[[i]], t] + D[ym[[j]], t]), 
      D[xm[[i]], t] -> 0.5 (D[zm[[i]], t] + D[zm[[j]], t]), 
      D[xm[[j]], t] -> 0.5 (D[zm[[i]], t] + D[ym[[j]], t])} // 
     Evaluate]], {i, n - 1}, {j, i + 1, n}];
Table[AppendTo[we, 
   WhenEvent[Abs[xm[[i]]] >= 0.9 // Evaluate, 
    D[xm[[i]], t] -> -D[xm[[i]], t] // Evaluate]], {i, n}];
Table[AppendTo[we, 
   WhenEvent[Abs[ym[[i]]] >= 0.9 // Evaluate, 
    D[ym[[i]], t] -> -D[ym[[i]], t] // Evaluate]], {i, n}];
Table[AppendTo[we, 
   WhenEvent[Abs[zm[[i]]] >= 0.9 // Evaluate, 
    D[zm[[i]], t] -> -D[zm[[i]], t] // Evaluate]], {i, n}];
s = NDSolve[Flatten[{xf, yf, zf,
     vel, pos, we}], Flatten[{xt, yt, zt}], {t, 0, 50}, 
   MaxSteps -> ∞];

Manipulate[
 Graphics3D[
  Transpose[{col, 
    Thread[Sphere[
      Evaluate[
       Flatten[Thread[Transpose[{xm, ym, zm} /. t -> a] /. s], 1]], 
      0.1]]}], PlotRange -> {{-1, 1}, {-1, 1}, {-1, 1}}], {a, 0, 50, 
  0.1}]

enter image description here