Finding Collisions of Circles in JavaScript

The JavaScript program below creates a field full of circles of random size, position, and velocity, and calculates and displays the collisions between them.

To see the code in action, open it in a web browser that runs JavaScript.

collisionsofcircles


<html>
<body>
<div id="divMain"></div>
<script type="text/javascript">

// main

function main()
{
	var display = new Display
	(
		new Coords(200, 200),
		"LightGray", "White"
	);

	Globals.Instance.initialize
	(
		new TimerHelper(10), 
		display,
		World.random
		(
			display.size, 
			16, // numberOfBodies
			10, // radiusMin
			40, // radiusMax
			1 // speedMax 
		)
	);
}

// classes

function Body(radius, pos, vel)
{
	this.radius = radius;
	this.pos = pos;
	this.vel = vel;

	this.collider = new ShapeCircle(this.pos, this.radius);
}
{
	// drawable

	Body.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.radius);
	}
}

function Collision()
{
	this.isActive = null;
	this.colliders = [];
	this.points = [];
}
{
	Collision.prototype.ofCircles = function(circle0, circle1)
	{
		var displacementFromCenter0To1 = 
			circle1.center.clone().subtract
			(
				circle0.center	
			);

		var distanceBetweenCenters = 
			displacementFromCenter0To1.magnitude();

		var circle0Radius = circle0.radius;
		var circle1Radius = circle1.radius;

		var sumOfRadii = circle0Radius + circle1Radius;

		if (distanceBetweenCenters > sumOfRadii)
		{
			this.isActive = false;
			this.colliders.length = 0;
			this.points.length = 0;
		}
		else
		{
			this.isActive = true;
			this.colliders.push(circle0);
			this.colliders.push(circle1);

			// Adapted from formulas derived at the URL
			// http://mathworld.wolfram.com/Circle-CircleIntersection.html
			
			var distanceToRadicalCenter = 
			(
				distanceBetweenCenters * distanceBetweenCenters
				+ circle0Radius * circle0Radius
				- circle1Radius * circle1Radius
			)
			/ (2 * distanceBetweenCenters);

			var directionToRadicalCenter = 
				displacementFromCenter0To1.divideScalar
				(
					distanceBetweenCenters
				);

			var displacementToRadicalCenter = 
				directionToRadicalCenter.clone().multiplyScalar
				(
					distanceToRadicalCenter
				)

			var radicalCenter = circle0.center.clone().add
			(
				displacementToRadicalCenter
			);

			var differenceOfRadii = circle1Radius - circle0Radius;

			var directionFromRadicalCenterToIntersection = 
				directionToRadicalCenter.right();

			var radicalLineLengthHalf = Math.sqrt
			(
				(-distanceBetweenCenters + differenceOfRadii)
				* (-distanceBetweenCenters - differenceOfRadii)
				* (-distanceBetweenCenters + sumOfRadii)
				* (distanceBetweenCenters + sumOfRadii)
			) / (2 * distanceBetweenCenters);

			var displacementFromRadicalCenterToIntersection = 
				directionFromRadicalCenterToIntersection.multiplyScalar
				(
					radicalLineLengthHalf
				);

			var intersection0 = radicalCenter.clone().add
			(
				displacementFromRadicalCenterToIntersection
			);			
		

			var intersection1 = radicalCenter.clone().subtract
			(
				displacementFromRadicalCenterToIntersection
			);

			// this.points.push(radicalCenter);
			this.points.push(intersection0);
			this.points.push(intersection1);			

		}

		return this;
	}

	// drawable

	Collision.prototype.drawToDisplay = function(display)
	{
		for (var i = 0; i < this.points.length; i++)
		{
			var point = this.points[i];

			display.drawCircle(point, 3);
		}
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	// instances

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Ones = new Coords(1, 1);
		this.Zeroes = new Coords(0, 0);
	}

	// methods

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}

	Coords.prototype.randomize = function()
	{
		this.x = Math.random();
		this.y = Math.random();
		return this;
	}

	Coords.prototype.right = function()
	{
		var temp = this.y;
		this.y = this.x;
		this.x = 0 - temp;
		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}

	Coords.prototype.wrapToRangeMax = function(max)
	{
		while (this.x < 0)
		{
			this.x += max.x;
		}

		while (this.x >= max.x)
		{
			this.x -= max.x;
		}

		while (this.y < 0)
		{
			this.y += max.y;
		}

		while (this.y >= max.y)
		{
			this.y -= max.y;
		}

		return this;
	}
}

function Display(size, colorFore, colorBack)
{
	this.size = size;
	this.colorFore = colorFore;
	this.colorBack = colorBack;
}
{
	// constants

	Display.RadiansPerCycle = 2.0 * Math.PI;

	// methods

	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			Coords.Instances.Zeroes,
			this.size,
			this.colorBack, this.colorFore
		);
	}

	Display.prototype.drawCircle = function(center, radius)
	{
		this.graphics.beginPath();
		this.graphics.arc
		(
			center.x, center.y,
			radius,
			0, Display.RadiansPerCycle
		);
		this.graphics.strokeStyle = this.colorFore;
		this.graphics.stroke();
	}

	Display.prototype.drawLine = function(fromPos, toPos)
	{
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.strokeStyle = this.colorFore;
		this.graphics.stroke();	
	}

	Display.prototype.drawRectangle = function
	(
		pos, size, colorFill, colorBorder
	)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fillRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.strokeRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}
	}

	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.size.x;
		this.canvas.height = this.size.y;

		this.graphics = this.canvas.getContext("2d");

		var divMain = document.getElementById("divMain");
		divMain.appendChild(this.canvas);
		
	}
}

function Globals()
{
	// do nothing
}
{
	// instance

	Globals.Instance = new Globals();

	// methods

	Globals.prototype.initialize = function(timerHelper, display, world)
	{
		this.timerHelper = timerHelper;
		this.display = display;
		this.world = world;

		this.display.initialize();
		this.timerHelper.initialize(this.handleEventTimerTick.bind(this));
	}

	// events

	Globals.prototype.handleEventTimerTick = function()
	{
		this.world.updateForTimerTick();
		this.world.drawToDisplay(this.display);
	}
}

function ShapeCircle(center, radius)
{
	this.center = center;
	this.radius = radius;
}

function TimerHelper(ticksPerSecond)
{
	this.ticksPerSecond = ticksPerSecond;
}
{
	TimerHelper.prototype.initialize = function(tickEventHandler)
	{
		var millisecondsPerTick = 1000 / this.ticksPerSecond;
		this.timer = setInterval
		(
			tickEventHandler,
			millisecondsPerTick
		);
	}
}

function World(size, bodies)
{
	this.size = size;
	this.bodies = bodies;
}
{
	// static methods

	World.random = function(size, numberOfBodies, radiusMin, radiusMax, speedMax)
	{
		var bodies = [];

		var radiusRange = radiusMax - radiusMin;
		var ones = Coords.Instances.Ones;

		for (var i = 0; i < numberOfBodies; i++)
		{
			var radius = radiusMin + radiusRange * Math.random();

			var pos = new Coords().randomize().multiply
			(
				size
			);

			var vel = new Coords().randomize().multiplyScalar
			(
				2
			).subtract
			(
				ones
			).multiplyScalar
			(
				speedMax
			);

			var body = new Body(radius, pos, vel);

			bodies.push(body);
		}

		var returnValue = new World
		(
			size, 
			bodies
		);

		return returnValue;
	}

	// instance methods

	World.prototype.updateForTimerTick = function()
	{
		for (var i = 0; i < this.bodies.length; i++)
		{
			var body = this.bodies[i];
			body.pos.add(body.vel).wrapToRangeMax
			(
				this.size
			);
		}
	}

	// drawable

	World.prototype.drawToDisplay = function(display)
	{
		display.clear();
		for (var i = 0; i < this.bodies.length; i++)
		{
			var body = this.bodies[i];

			body.drawToDisplay(display);
		}

		var collision = new Collision();

		for (var i = 0; i < this.bodies.length; i++)
		{
			var body = this.bodies[i];
			var collider = body.collider;

			for (var j = i + 1; j < this.bodies.length; j++)
			{
				var bodyOther = this.bodies[j];
				var colliderOther = bodyOther.collider;

				collision.ofCircles(collider, colliderOther);

				if (collision.isActive == true)
				{
					collision.drawToDisplay(display);
				}				
			}
		}

	}
}

// run

main();

</script>
</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s