Java Animation Graphics

This is a liquid made of particles. Each drop adds a soft round bump to a field of numbers, and the goo is drawn green wherever the field is thick enough. The same field also pushes every drop towards a preferred density, so the drops stick together, pile up on the ledges and slide off the ends.

The hard part with a particle liquid is making separate dots look and behave like one sticky mass. Here both come from that one field: drawing it gives the smooth outline, and the way it changes around each drop gives the forces that hold the drops together.

The scene

The applet is 256x256 pixels, with two white ledges: a short one on the left and a longer one lower down. Hold the mouse button down and goo pours out at the pointer. It falls, gathers into blobs on the ledges, and drips off the ends and out of the bottom.

for(i=size-1; i>=0; i--) back[i] = 0;
fillRect(back, 20, 100, 100, 130, 0x7FFFFFFF);
fillRect(back, 50, 190, 190, 220, 0x7FFFFFFF);
The background: black, with the two ledges

The ledges are filled with 0x7FFFFFFF, the biggest number a Java int can hold. That one value does two jobs: it shows as white on the screen, and to the drops it's a wall.

Goo poured onto the two ledges
Goo poured onto the two ledges

Everything about how the goo looks and moves comes from a handful of numbers in the code:

ValueWhat it does
1300The most drops there can be at once
3Drops added each frame while the button is down
5 pixels, 512The radius of a drop's bump in the field, and its height in the middle
800The field value above which a pixel is drawn green
10 pixelsHow far round itself a drop feels the field
5100The field value a drop is happiest in
8000The most any one pixel can push
0.5Gravity, added to the downward speed every frame
0.99Friction, before the field takes a little more off
0.02How far a drop moves each frame for each unit of speed
The constants that make the goo

Pouring

The 1300 drops live in parallel arrays: position, velocity, and a flag saying whether each one is in use. While the button is down, each frame looks for the first three free slots and puts a drop in each, spread over a 4 pixel square round the mouse using a cheap pseudo-random number made from the frame number:

if(mouse_click)
{
  for(j=0; j<3; j++)
  {
    for(i=0; i<num_particles && particle_active[i]; i++);

    if(i < num_particles)
    {
      particle_active[i] = true;

      particle_x[i] = mouse_x - 2 + 4*(((j*6172+frame*frame*11714)%12379)/12379.0);
      particle_y[i] = mouse_y - 2 + 4*(((j*9899+frame*frame*8394)%17121)/17121.0);
      particle_vx[i] = 0;
      particle_vy[i] = 0;
    }
  }
}
Adding up to three drops at the mouse, starting still

At three a frame, all 1300 are out after 434 frames. Then nothing more comes out until some have fallen off the bottom of the applet, which frees their slots.

The field

Nothing about a drop is drawn straight to the screen. Each frame the background is copied into a second array, pot, and every drop is drawn into that. This is the field, and both the picture and the forces come from it.

Drawing a drop

A drop adds a round bump to the field: 512 at its centre, falling off with the square of the distance to nothing at 5 pixels away:

d = (i-px)*(i-px) + (j-py)*(j-py);
d /= 25;
d = 1-d;
if(d < 0) d = 0;
if(d > 1) d = 1;
...
buf[i+width*j] += (int)(d*512);
The bump a drop adds to each pixel within 5 pixels of it (trimmed)

The bumps add up, so wherever drops crowd together the field gets higher.

Where the goo shows

Drawing the goo is one test per pixel. The screen buffer starts as a copy of the background, and anywhere the field is over 800 gets green:

for(i=0; i<size; i++)
{
  if(pot[i] > 800) buf[i] |= 0x00FF00;
}
Painting green wherever the field is over 800

A drop on its own only reaches 512, so you can't see it. It takes at least two close together. Here are two drops 4 pixels apart, at x=100 and x=104 on the same row, and the field along that row:

The field along the row through two drops 4 pixels apart
The field along the row through two drops 4 pixels apart

Neither drop's own centre makes it: 512 from itself and 184 from the other is 696. The pixels between them do, so the pair shows up as a little diamond of five green pixels sitting between the two drops.

Piling more drops onto the same spot makes a bigger spot of goo, but it can never be bigger than the bump's radius of 5:

Drops on one spotRadius of the green spot
1Nothing drawn
2About 2.3 pixels
3About 3.5 pixels
4About 3.9 pixels
10About 4.6 pixels
How big a spot of goo is drawn for drops piled on one point

So a big blob of goo has to be made of drops spread out side by side, each one topping up the field for its neighbours. Getting them to spread out evenly, and to stay together, is the job of the forces. Adding soft bumps into a field and drawing it at a threshold is the same idea as metaballs.

Colour

ORing in 0x00FF00 turns a black pixel pure green and leaves a white one white, so the goo is a flat green with a hard edge, and where it overlaps a ledge it simply doesn't show. The screen is handed over as 32-bit ints, and the colour model only reads the red, green and blue bytes, which is why the ledges' 0x7FFFFFFF comes out plain white.

Forces

Feeling the field

Every frame, each drop looks at the field in a circle of radius 10 round it: a 23x23 square of pixels, each with a weight d that falls from 1 at the drop to 0 at 10 pixels away. Every one of those pixels pushes the drop away from itself:

f = buf[i+width*j] - 5100;
if(f < 0) f =- f;

if(f < 0) f = 0;
if(f > 8000) f = 8000;

f *= d;

particle_friction -= 0.00000001 * f * d;

particle_force_x -= 0.000016 * f * d * (i-px);
particle_force_y -= 0.000016 * f * d * (j-py);
The push from one pixel near the drop, and the friction it adds

How hard a pixel pushes is how far its field value is from 5100, capped at 8000 and multiplied by the weight twice, so the nearest pixels count for much more. The push points from the pixel to the drop. When the pixels all round a drop are alike, their pushes cancel out, and the drop only feels a push when one side is different from the other.

A preferred density

Measuring from 5100 rather than from 0 is what turns the drops into goo. Here is how hard a pixel pushes for each field value:

How hard a pixel pushes for each field value
How hard a pixel pushes for each field value

Empty space has a field of 0, which is 5100 away from 5100, so empty pixels push hard. Pixels where the field is near 5100 hardly push at all. A drop at the edge of a blob has empty space pushing it from outside and goo pushing much less from inside, so it gets pushed inwards. That's what holds the goo together and rounds it off into blobs. A part of a blob that's packed tighter than 5100 pushes harder again, so it spreads out.

5100 is about ten drops' worth of centre (10 x 512 is 5120). Inside settled goo, each pixel is covered by the edges of the bumps of several drops, which adds up to far more than the 800 needed to draw it, so the inside is solid green and the edge is where the field drops away.

Ledges

The ledges are in the field too, because the field starts every frame as a copy of the background. At 0x7FFFFFFF they're as far from 5100 as anything gets, so every ledge pixel pushes with the full 8000, more than empty space does. Working the sums through for a single drop over the middle of a ledge, with only its own bump in the field round it:

Height above the ledgePush upwards
0 pixels7.24
2 pixels6.72
4 pixels4.51
6 pixels2.00
7 pixels1.02
8 pixels0.38
10 pixels0
The ledge's push on one drop at different heights above it

Gravity adds 0.5 a frame, so a lone drop comes to rest just under 8 pixels above the ledge, where the two balance. Nothing ever touches anything: the drops are held up by the field.

Friction

The same loop takes some speed off. Friction starts at 0.99 and every pixel takes off a little more in proportion to its push, so a drop in open space keeps about 0.985 of its speed each frame, and one right against a ledge about 0.983. Inside the goo, where the field is near 5100, almost nothing more is taken off, so the drops keep nearly 0.99 of their speed and slide past each other easily.

The end of calcForce() stops friction going below 0.9, but it never gets there: even with every pixel pushing the full 8000, it only comes down to about 0.982.

Moving a drop

With the push worked out, moving a drop is gravity, the push, friction and a small step:

//- Gravity

particle_vy[i] += 0.5;
...
calcForce(pot, particle_x[i], particle_y[i]);
...
particle_vx[i] += particle_force_x;
particle_vy[i] += particle_force_y;

//- Apply friction

particle_vx[i] *= particle_friction;
particle_vy[i] *= particle_friction;
...
//- Move particle

particle_x[i] += 0.02 * (particle_vx[i] - 0.005 + 0.01*(((frame*frame*3171)%5237)/5237.0));
particle_y[i] += 0.02 * (particle_vy[i] - 0.005 + 0.01*(((frame*frame*4187)%5771)/5771.0));

//- Remove particle if necessary

if(particle_y[i] >= 256) particle_active[i] = false;
Moving one drop for one frame (trimmed)

Speeds are in big units, and each frame a drop only moves 0.02 of its speed, which is what makes it flow like syrup. Falling through open space, gravity and friction balance out at a speed of about 32, so a drop never falls faster than about 0.65 pixels a frame.

The field a drop feels was drawn at the end of the last frame, and it isn't drawn again until every drop has moved. So the order the drops are moved in doesn't matter: they all react to the same picture.

A drop that falls past the bottom of the applet is switched off, and its slot can be used again.

The frame loop

main() sets up the arrays and the background and then loops for ever. Each time round it:

  1. adds up to three drops at the mouse if the button is down
  2. moves every drop, using the field from the last frame
  3. copies the background into both the screen buffer and the field
  4. draws every drop's bump into the field
  5. paints green wherever the field is over 800
  6. hands the screen buffer to TinyPTC to show.
//- Clear buffers

for(i=size-1; i>=0; i--) buf[i] = back[i];
for(i=size-1; i>=0; i--) pot[i] = back[i];

//- Render particles

for(i=0; i<num_particles; i++) if(particle_active[i])
{
  drawParticle(pot, particle_x[i], particle_y[i]);
}
Starting both buffers from the background and drawing the drops into the field

The mouse is tracked with the old Java 1.0 event methods: mouseMove() and mouseDrag() keep the position, and mouseDown() and mouseUp() turn the pouring on and off.

TinyPTC

The pixels get to the screen through TinyPTC, a tiny library by Gaffer for showing an int[] of pixels in a Java applet. Site3D extends it and only has to write main(). TinyPTC calls main() on a thread of its own, and the applet is the producer for its own image, so each update() hands the pixels straight to the image and paints it:

public synchronized void update(Object pixels)
{
    // check consumer
    if (_consumer!=null)
    {
        // copy integer pixel data to image consumer
        _consumer.setPixels(0,0,_width,_height,_model,(int[])pixels,0,_width);

        // notify image consumer that the frame is done
        _consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
    }

    // paint
    paint();
}
TinyPTC showing a frame

The colour model it sets up has masks for red, green and blue and none for alpha, which is where the white ledges come from.

Speed

There's no pause in the loop, so it runs as fast as it can, and it slows down as more drops come out. With all 1300 out, a frame reads 23x23 pixels of the field for every drop, which is 687,700 reads, and draws 13x13 pixels for every bump, another 219,700. The applet's page warns about it: "Warning it can get quite slow." Speeds are per frame rather than per second, so when it's busy the goo moves in slow motion rather than jumping.

On this page the original class files run in CheerpJ, a Java virtual machine for the browser.

Leftovers in the code

  • getPot() is only called from a commented-out test that stopped a drop moving into anything over 1024.
  • Limits on the push (3 either way) and the speed (5 either way) are there but commented out, and so is a first go at drawParticle() that blended white into the screen instead of adding to the field.
  • The random nudge in the move step depends only on the frame number, so every drop gets the same nudge in a frame, and it's never more than a few ten-thousandths of a pixel.
  • From frame 429 on, frame*frame*11714 is too big for an int and wraps round to a negative number, so the remainder can be negative and a new drop can land up to 6 pixels left of the mouse instead of within 2. The same happens to the height from frame 506.
  • A drop's bump drawn over a ledge adds to 0x7FFFFFFF and wraps round to a big negative number. It makes no difference: its distance from 5100 is still capped at 8000, and the pixel is white either way.
  • if(f < 0) f = 0; comes straight after the line that makes f positive.
  • Graphics, Color, Image, Font and Vector are imported and not used, and main() declares x and y and doesn't use them.

Source code

The applet is two Java files: Site3D.java is the goo, and tinyptc.java is TinyPTC. Compile them both with javac and open Site3D.html next to the classes in appletviewer, from a JDK up to version 8.

Applet

  • Site3D.java Java · 333 lines · the drops, the field, the forces and the frame loop
  • tinyptc.java Java · 136 lines · TinyPTC by Gaffer, for showing the pixels

Applet page

  • Site3D.html HTML · 24 lines · the page that runs the applet

See also

  • Java 2D blobs - Metaballs: blobs added into a field and drawn wherever it's over a threshold
  • Cloth simulation - A spring-mass cloth falling and slipping over objects
  • Java fog effects - More Java effects drawn a pixel at a time through TinyPTC