Skip to content Skip to sidebar Skip to footer

Having Trouble Calculating Direction For Particle Explosion

I'm trying make a particle explosion system, where each particle explodes directly away from a central point. But I'm getting some really funky behavior. I'm using Math.atan2(y2 -

Solution 1:

Further to a comment above, here's a poorly implemented particle system utilizing 2d vectors. I've hacked together an 'animation', using a for loop - obviously, I should use window.requestAnimationFrame(https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) But that's beyond the scope of this example.

To initialize the position and direction of each particle I do the following.

  1. get two random numbers between 0 and 1
  2. subtract 0.5 from each, so I have 2 numbers in the range [-0.5 .. 0.5]
  3. Use the number gained in 1 as the initial position.
  4. Normalize the vector from the origin to this position (by viewing the position as a vector)
  5. Multiply it by a number between 0 and 10. The particles will have random directions and will have a speed that ranges from [0..10]
  6. get a random number [0..25] and add 25 to it - this will be the max age.

function byId(id){return document.getElementById(id)}
window.addEventListener('load', onDocLoaded, false);

var partArray = [], maxParticles=500;


function onDocLoaded(evt)
{
	for (var i=0; i<maxParticles; i++)
	{
		var pos = new vec2_t( Math.random()-0.5, Math.random()-0.5 );		// give them a poition of [-0.5..0.5]
		var vel = pos.normalize();
		vel.scalarMult( Math.random() * 10 );								// give them a velocity of [0..10]
		var maxAge = (Math.random() * 25) + 25;								// age is in range [25..50]
		var newParticle = new part_t(pos, vel, maxAge);						// create the particle
		partArray.push( newParticle );										// and put it in our array
	}
	
	for (var y=0; y<5; y++)
	{
		drawParticles();
		moveParticles();
	}
}

function vec2_t(x,y)
{
	this.x = x;
	this.y = y;
	
	this.normalize = function()
	{
		var result = new vec2_t(0,0);
		var lenSq = (this.x*this.x) + (this.y*this.y);
		var len = Math.sqrt(lenSq);
		result.x = this.x / len;
		result.y = this.y / len;
		return result;
	}
	this.scalarMult = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
	}
	
	return this;
}

function part_t(position, velocity, maxAge)
{
	this.position = position;
	this.velocity = velocity;
	this.maxAge = maxAge;
	this.age = 0;
	return this;
}

function setPixel(x,y,ctx)
{
	var imgData = ctx.getImageData(x,y,1,1);
	imgData.data[ 0 ] = 255;
	imgData.data[ 1 ] = 0;
	imgData.data[ 2 ] = 0;
	imgData.data[ 3 ] = 255;
	ctx.putImageData(imgData,x,y);
//	console.log(x+','+y);
}

function drawParticles()
{
	var can = byId('partSysCanvas');
	var ctx = can.getContext('2d');
	var partNum;
	for (partNum=0; partNum<maxParticles; partNum++)
	{
        // add 256,256 since this is canvas.width/2,canvas.height/2
		setPixel( 256+partArray[partNum].position.x, 256+partArray[partNum].position.y, ctx);
	}
}
function moveParticles()
{
	for (var i=0; i<maxParticles; i++)
	{
		if (partArray[i].age < partArray[i].maxAge)
		{
			partArray[i].age++;
			partArray[i].position.x += partArray[i].velocity.x;
			partArray[i].position.y += partArray[i].velocity.y;
		}
	}
}
<canvas width=512 height=512 id='partSysCanvas'></canvas>

Solution 2:

That was some fun. Again, I've eschewed trigonometry in preference of using vectors. The letters dont explode from the centre - text is drawn and then the letters explode from this point away from the seat of the explosion. I've set a uniform explosion velocity for all of the particles with vel.scalarMult( 10 ); you can randomize this instead with the line above it.

I've also not bothered to scale particle velocity based on it's proximity to the bomb. Everything just explodes away from the seat, where we know the force felt from the explosion reduces as distance increases.

Here's a working demo to play with.

I really should be using requestAnimationFrame

"use strict";
function newEl(tag){return document.createElement(tag)}
function byId(id){return document.getElementById(id)}
// useful for HtmlCollection, NodeList, String types (array-like objects without the forEach method)
function forEach(array, callback, scope){for (var i=0,n=array.length; i<n; i++)callback.call(scope, array[i], i, array);} // passes back stuff we need

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function vec2_t(x,y)
{
	this.x=x;
	this.y=y;
	this.equals = function(vec2){this.x = vec2.x; this.y = vec2.y;}
	this.addVec = function(vec2){this.x += vec2.x; this.y += vec2.y;}
	this.scalarMult = function(scalar){this.x *= scalar; this.y *= scalar;}
	this.vecLen = function(){return Math.sqrt( this.x*this.x + this.y*this.y );}
	this.normalize = function(){ let k = 1.0 / this.vecLen(); this.scalarMult(k); }
	this.vecSub = function(vec2){this.x-=vec2.x;this.y-=vec2.y;}
	this.toString = function(){return"<"+this.x+","+this.y+">"}
	return this;
}

function part_t(vec2pos, vec2vel, domElem)
{
	this.pos = vec2pos;
	this.vel = vec2vel;
	this.domElem = domElem;
	return this;
}



var particleArray, timerId;
let explosionOrigin = new vec2_t(156,110);

window.addEventListener('load', onDocLoaded, false);



function onDocLoaded(evt)
{
	particleArray = createPartSys('textInput', 'tgtContainer');
	byId('stepBtn').addEventListener('click', onStepBtnClick);
	byId('resetBtn').addEventListener('click', onResetBtnClick);
	byId('animateBtn').addEventListener('click', onAnimateBtnClick);
	
	byId('pauseBtn').addEventListener('click', onPauseBtnClick);
	
	byId('tgtContainer').addEventListener('click', onClick);
}

function onStepBtnClick(evt)
{
	updatePartSys(particleArray);
}

function onAnimateBtnClick(evt)
{
	timerId = setInterval(function(){updatePartSys(particleArray);}, 100);
	this.setAttribute('disabled', 'true');
	byId('pauseBtn').removeAttribute('disabled');
}

function onPauseBtnClick(evt)
{
	clearInterval(timerId);
	this.setAttribute('disabled', 'true');
	byId('animateBtn').removeAttribute('disabled');
}

function onResetBtnClick(evt)
{
	var bombImg = byId('bomb');
	byId('tgtContainer').innerHTML = '';
	byId('tgtContainer').appendChild(bombImg);
	
	particleArray = createPartSys('textInput', 'tgtContainer');
	
	byId('animateBtn').removeAttribute('disabled');
	clearInterval(timerId);
}

function createPartSys(srcElemId, tgtElemId)
{
	let	elem = byId(srcElemId);
	var str = elem.value, len=str.length;

	let result = [];
	let parent = elem;
	let curX = elem.offsetLeft - parent.offsetLeft;
	let curY = elem.offsetTop - parent.offsetTop;

	let bombImg = byId('bomb');
	bombImg.style = 'position: absolute';
	bombImg.style.left = (explosionOrigin.x - (bombImg.clientWidth/2))+'px';
	bombImg.style.top = (explosionOrigin.y - (bombImg.clientHeight/2))+'px';
	byId(tgtElemId).appendChild(bombImg);
		
	curY = 50;
	curX = 50;
	forEach(str,
			function(letter)
			{
				var span = newEl('span');
				span.className = 'particle';
				if (letter == ' ') letter = '&nbsp;'
				let h1 = newEl('h1');
				h1.innerHTML = letter;
				span.appendChild(h1);
				span.style.left = curX + 'px';
				span.style.top = curY + 'px';
				byId(tgtElemId).appendChild(span);
				
				var pos = new vec2_t(curX,curY);
				
				curX += span.offsetWidth;

				var vel = new vec2_t(0,0);
				
				let letterOrigin = getCenter(span);
				
				vel.equals(letterOrigin);
				vel.vecSub(explosionOrigin);
				vel.normalize();
			//	vel.scalarMult( (Math.random()*1) + 4 );
				vel.scalarMult( 10 );
				
				var newPart = new part_t(pos,vel,span);
				result.push(newPart);
			}
		);
	return result;
}

function updatePartSys(partSys)
{
	forEach(
			partSys,
			function(part, index, array)
			{
				part.pos.addVec(part.vel);						// position += velocity
				var gravity = new vec2_t(0,0.98/3);				// arbitrary value chosen
				part.vel.scalarMult(0.95);						// velocity *= 0.95	- needs to be quite high. it simulates wind resistance
				part.vel.addVec(gravity);						// velocity += gravity
				part.domElem.style.left = part.pos.x + "px";
				part.domElem.style.top = part.pos.y + "px";
			}
		);
}

function onClick(evt)
{
	let elemRect = byId('tgtContainer').getBoundingClientRect();
	let posX = evt.clientX - elemRect.left, posY = evt.clientY-elemRect.top;
	explosionOrigin.x = posX;
	explosionOrigin.y = posY;
	onResetBtnClick();
}

function getCenter(elem)
{
	let x = elem.offsetLeft + (elem.offsetWidth/2);
	let y = elem.offsetTop + (elem.offsetHeight/2);
	let result = new vec2_t(x,y);
	return result;
}
#tgtContainer
{
	position: relative;
	height: 256px;
	width: 512px;
	border: solid 1px black;
	overflow: hidden;
	background-color: white;
}
.particle
{
	display: inline-block;
	position: absolute;
}
.panel
{
	display: inline-block;
	border: solid 1px #113;
	border-radius: 8px;
	margin: 8px;
	padding: 8px;
	background-image: url(https://www.gravatar.com/avatar/97c2d181ef6bbb9eee0c4033561c3891?s=48&d=identicon&r=PG);
	background-size: 100% 100%;
}

#textContainer
{
	display: block;
}

#textContainer textarea
{ 
	width: 100%; 
	padding: 0;
	margin: 1px 0px;
}
<div class='panel'>
		<div id='textContainer'><textarea id='textInput'>click to set bomb position</textarea></div>
		<hr>
		<button id='resetBtn'>Reset</button><button id='stepBtn'>Single Step</button> | <button id='animateBtn'>Animate</button><button id='pauseBtn' disabled>Pause</button>
		<hr>
		<div id='tgtContainer'>
		</div>
	</div>
	<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" id='bomb'>
		<g transform="translate(0,-1020.3622)">
			<path d="m23.23,15.84a10.55,10.55,0,1,1,-21.11,0,10.55,10.55,0,1,1,21.11,0z" transform="matrix(1.1875635,0,0,1.1875635,0.68612298,1020.367)" fill="#26201e"/>
			<path d="m23.23,15.84a10.55,10.55,0,1,1,-21.11,0,10.55,10.55,0,1,1,21.11,0z" transform="matrix(0.86603158,0,0,0.86603158,2.4299747,1024.1874)" fill="#333"/>
			<path d="m-13.04,19.32a1.964,1.964,0,1,1,-3.929,0,1.964,1.964,0,1,1,3.929,0z" transform="matrix(1.924285,1.1058108,-1.1908732,2.0723069,62.314757,1012.6494)" fill="#CCC"/>
			<path d="m15.69,1026c0.02518-5.037,7.647-7.396,8.907-2.969,0.7936,2.761,1.349,5.666,4.877,6.786" stroke="#888" stroke-width="1.5px" fill="none"/>
			<rect height="2.399" width="4.798" y="1026" x="13.31" stroke-width="0" fill="#26201e"/>
			<path fill="#F00" transform="translate(2.0203051,1022.13)" d="M29.8,10.53,27.1,9.62,24.82,11.32,24.86,8.477,22.54,6.833,25.25,5.989,26.1,3.271,27.74,5.595,30.59,5.558,28.89,7.839z"/>
		</g>
	</svg>

Post a Comment for "Having Trouble Calculating Direction For Particle Explosion"