Clinton Montague

Developer, learner of things, functional programming enthusiast, hacker, and all round inquisitor.

Lunch-time hack: Day 3 – Visualising forces between repelling particles

June 9, 2011

See demo →

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 ();
}

See demo →