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.
To take the demo from yesterday and modify it so that it visualises the forces between each of the particles
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.
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.
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.