The lunch-time hack: Day 3 - Visualising forces between repelling particles

This demo is part of a series call 'the lunch-time hack' where I have 15 minutes to create a cool demo. Read more about the project. Read the blog post for this demo.

Follow @iblamefish

Aim

To take the demo from yesterday and modify it so that it visualises the forces between each of the particles

Description

100 particles will go onto the canvas in the first few seconds. You can add new particles by clicking. All the particles on the canvas will repel eachother to form the pattern where the forces between them all cancel out.

Outcome

Stronger forces are in red, green are weaker. When the particles are too far apart for the forces to act, the lines disappear.

It's quite interesting to see how the forces are continuously flicking on-and-off and how they mostly form a triangle lattice, but with tears where there appear to be force triangles, but are not. The changing of the colours really helps to show how the force shockwaves travel through the system after a new particle has been added.

The code


var WIDTH = 500,
	HEIGHT = 500,
	PARTICLES = [],
	STARTING_COUNT = 100,
	MAX_PARTICLES = 200,
	DAMPING = 0.05,
	CURRENT_FRAME = 0;

var canvas = document.getElementsByTagName ('canvas')[0];
canvas.width = WIDTH;
canvas.height = HEIGHT;

var ctx = canvas.getContext ('2d');

init ();

function random (min, max) {
	return (Math.random() * (max - min) + min);
}


function _particleTimer () {
	addParticle (random(0, HEIGHT), random(0, HEIGHT));
	if (PARTICLES.length < STARTING_COUNT) setTimeout (_particleTimer, 50);
}


function init () {
	_particleTimer();
	setInterval (animate, 1000 / 50);
	canvas.addEventListener ('mousedown', onMouseDown, false);
	animate ();
}


function onMouseDown (e) {
	addParticle (e.offsetX, e.offsetY);
}


function addParticle (x, y) {
	PARTICLES.push ({
		position: {
			x: x || random(0, WIDTH),
			y: y || random (0, HEIGHT)
		},
		force : {
			x: 0,
			y: 0
		},
		vel : {
			x: 0,
			y: 0
		}
	});
}

function animate () {
	while (PARTICLES.length > MAX_PARTICLES) PARTICLES.shift ();


	CURRENT_FRAME++;
	ctx.save ();
	ctx.fillStyle = "rgba(0,0,0,0.3)"; 
	ctx.fillRect (0, 0, WIDTH, HEIGHT);
	ctx.fillStyle = 'rgba(0,0,0,0)';
	ctx.lineWidth = 1;
	ctx.strokeStyle = '#ffffff';
	ctx.beginPath();
	ctx.moveTo (20, 20);
	ctx.lineTo (20, HEIGHT-20);
	ctx.lineTo (WIDTH-20, HEIGHT-20);
	ctx.lineTo (WIDTH-20, 20);
	ctx.closePath();
	ctx.stroke();
	ctx.restore();
	
	
	var force = {x : 0, y: 0};
	for (var i = 0; i < PARTICLES.length; i++) {
		var p1 = PARTICLES[i];
	

		for (var j = i + 1; j < PARTICLES.length; j++) {
			var p2 = PARTICLES[j];

			// length vector
			force.x = p2.position.x - p1.position.x;
			force.y = p2.position.y - p1.position.y;

			var magnitude = mag (force);

			var actingDistance = 50-magnitude;

			if ((actingDistance > 0) && (magnitude > 0)) {
				var red = 0;
				var green = 120;

				var color = green - actingDistance * (green / 2);
				if (color < 0) color = 0;
				if (color > 120) color = 120;

				ctx.save ();
				ctx.strokeStyle = 'hsla(' + color + ', 100%, 50%, 1)';
				ctx.strokeWidth = 1;
				ctx.beginPath ();
				ctx.moveTo (p1.position.x, p1.position.y);
				ctx.lineTo (p2.position.x, p2.position.y);
				ctx.stroke();
				ctx.closePath();
				ctx.restore ();

				force.x *= DAMPING * actingDistance / magnitude;
				force.y *= DAMPING * actingDistance / magnitude;
				p1.force.x -= force.x;
				p1.force.y -= force.y;

				p2.force.x += force.x;
				p2.force.y += force.y;
			}
		}
		draw (p1);
		update (p1);
	}
}

function mag (vector) {
	return Math.sqrt (vector.x * vector.x + vector.y * vector.y);
}

function update (particle) {
	with (particle) {
		vel.x += force.x;
		vel.y += force.y;
		vel.x *= 0.8;
		vel.y *= 0.8;

		position.x += vel.x;
		position.y += vel.y;

		force.x = 0;
		force.y = 0;

		if (position.x > WIDTH - 26) {
			position.x = WIDTH - 26;
		}
		if (position.x < 21) {
			position.x = 21;
		}
		if (position.y > HEIGHT - 26) {
			position.y = HEIGHT - 26;
		}
		if (position.y < 21) {
			position.y = 21;
		}
	}
}

function draw (particle) {
	ctx.save ();
	ctx.translate (particle.position.x, particle.position.y);
	ctx.fillStyle = "hsla("+CURRENT_FRAME%360 +",50%,50%,1)";

	ctx.fillRect (0, 0, 5, 5);
	ctx.restore ();
}

Created by Clinton Montague (@iblamefish) as part of the lunch-time hack series

Read more about the project. Read the blog post for this demo.