Lunch-time hack: Day 2 – self organising particles
June 8, 2011
In today’s hack I wanted to play with a self organising particle system, so I set about writing a loop which worked out the distance between each particle and generated a force based on that. It was a lot to think about so I didn’t quite get it working in 15 minutes and decided to be naughty and finish it off this evening.
One of the best things about it is that it’s a truly chaotic system – the rules are 100% known, but you can’t be sure exactly what will happen when you add a new particle.
Have a play, go on, it’s really cool!
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 ();
}