The lunch-time hack: Day 2 - Self organising 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.

Confession

I didn't really get it working at lunch time, but I could see how cool it was going to be, so I finished it off at home. So much for rules ;)

Follow @iblamefish

Aim

To build a particle system where each particle repels its neighbours causing them to self organise

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

It's really rather cool I think! I've been playing with it for a while and can't put it down. A nice unexpected side-effect is that I can now visualise how crystals form in metal.

I'm pleased with the patterns and shapes which it makes when the particles settle down

The code


var WIDTH = 500,
	HEIGHT = 500,
	PARTICLES = [],
	STARTING_COUNT = 100,
	MAX_PARTICLES = 200,
	DAMPING = 0.2,
	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 () {
//	while (STARTING_COUNT--) {
//		addParticle ();
//	}
	_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 = 100-magnitude;

			if ((actingDistance > 0) && (magnitude > 0)) {

				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.