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 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.

Everything about how the goo looks and moves comes from a handful of numbers in the code:
| Value | What it does |
|---|---|
| 1300 | The most drops there can be at once |
| 3 | Drops added each frame while the button is down |
| 5 pixels, 512 | The radius of a drop's bump in the field, and its height in the middle |
| 800 | The field value above which a pixel is drawn green |
| 10 pixels | How far round itself a drop feels the field |
| 5100 | The field value a drop is happiest in |
| 8000 | The most any one pixel can push |
| 0.5 | Gravity, added to the downward speed every frame |
| 0.99 | Friction, before the field takes a little more off |
| 0.02 | How far a drop moves each frame for each unit of speed |
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;
}
}
}
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 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;
}
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:
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 spot | Radius of the green spot |
|---|---|
| 1 | Nothing drawn |
| 2 | About 2.3 pixels |
| 3 | About 3.5 pixels |
| 4 | About 3.9 pixels |
| 10 | About 4.6 pixels |
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);
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:
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 ledge | Push upwards |
|---|---|
| 0 pixels | 7.24 |
| 2 pixels | 6.72 |
| 4 pixels | 4.51 |
| 6 pixels | 2.00 |
| 7 pixels | 1.02 |
| 8 pixels | 0.38 |
| 10 pixels | 0 |
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;
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:
- adds up to three drops at the mouse if the button is down
- moves every drop, using the field from the last frame
- copies the background into both the screen buffer and the field
- draws every drop's bump into the field
- paints green wherever the field is over 800
- 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]);
}
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();
}
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*11714is too big for anintand 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 makesfpositive.Graphics,Color,Image,FontandVectorare imported and not used, andmain()declaresxandyand 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