Programming A Primitive Orrery in JavaScript

My intent with this was to build a simple physical model of the solar system that, while it might not be exactly right in terms of particulars, still got the physics more-or-less correct. Without using any differential equations, just brute force math.

It’s obviously still not finished–Saturn, Neptune, Uranus and Pluto aren’t included, and the planets weren’t in conjunction on January 1, 2012, as this simulation purports–but each of the planets shown does take approximately the right amount of time to complete an orbit around the Sun, so that’s good enough for me, for now at least.

UPDATE 2017/01/26: Several years on, I have refactored this code to draw to an HTML5 canvas rather than giving each of the planets its own DOM Element. I’ve also cleaned up and modernized the code in general. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit “http://thiscouldbebetter.neocities.org/planetarium.html“.

Orrery


<html>
<body>
<script type="text/javascript">

// main

function Planetarium()
{
	this.main = function()
	{
		var display = new Display
		(
			new Coords(1080, 1080), // sizeInPixels
			10, // fontHeightInPixels
			"LightGray", //colorFore
			"Black" // colorBack
		);
	
		var solarSystem0 = new SolarSystem
		(
			"SolarSystem0",
			// bodies
			[
				// new Body(name, color, massInKg, radiusInKm, periapsisInKm, apoapsisInKm)
				new Body("Sun", "Yellow", 1.9891 * Math.pow(10, 30), 695500, 0, 0),
				new Body("Mercury", "LightGray", 3.3022 * Math.pow(10, 23), 2439.7, 46001200, 69816900),
				new Body("Venus", "White", 4.8685 * Math.pow(10, 24), 6051.8, 107476259, 108942109),
				new Body("Earth", "Cyan", 5.9736 * Math.pow(10, 24), 6371.0, 147098290, 152098232),
				new Body("Mars", "Red", 6.4185 * Math.pow(10, 23), 3396.2, 206669000, 227939100),
				new Body("Jupiter", "Orange", 1.8986 * Math.pow(10, 27), 71492,  740573600, 778547200)
				// Saturn
				// Uranus
				// Neptune
				// Pluto
			]
		);

		var gravityConstant = 6.67384 * Math.pow(10, -11); // in m^3 kg^-1 s^-2
		var kilometersPerPixel = 2000000;
		var simulationMillisecondsPerTick = 14400000; // 4 hours
		var realWorldMillisecondsPerTick = 100; // 10 ticks per second
		var dateStart = new Date(2012, 0, 1, 0, 0, 0, 0); // January 1, 2012

		Globals.Instance.initialize
		(
			display,
			gravityConstant, 
			kilometersPerPixel, 
			simulationMillisecondsPerTick, 
			realWorldMillisecondsPerTick, 
			dateStart,
			solarSystem0
		);
	}
}

// classes

function Body(name, color, massInKg, radiusInKm, periapsisInKm, apoapsisInKm)
{
	this.name = name;
	this.color = color;
	this.massInKg = massInKg;
	this.radiusInKm = radiusInKm;

	this.periapsisInKm = periapsisInKm;
	this.apoapsisInKm = apoapsisInKm;
	this.semimajorAxisInKm = (this.periapsisInKm + this.apoapsisInKm) / 2;
	this.eccentricity = (this.apoapsisInKm - this.periapsisInKm) / (this.apoapsisInKm + this.periapsisInKm);
	this.posInKm = new Coords(this.semimajorAxisInKm, 0);

	this.velInKmPerSec = new Coords(0, 0);
	this.accelInKmPerSecSquared = new Coords(0, 0);
	this.forceInNewtons = new Coords(0, 0); // 1 N = 1 kg * m / s^2
}
{
	Body.prototype.drawToDisplay = function(display)
	{
		var kmPerPixel = Globals.Instance.kilometersPerPixel;
		var radiusInPixels = Math.ceil(this.radiusInKm / kmPerPixel);		
		var offsetToViewCenter = display.sizeInPixelsHalf;
		var posInPixels = this.posInKm.clone().divideScalar(kmPerPixel).round().add(offsetToViewCenter);
		display.drawRectangle(posInPixels, Coords.Instances.Ones, this.color, null);
		display.drawText(this.name, posInPixels);
	}
	
	Body.prototype.initialize = function(gravityConstant, sun)
	{
		var distanceOfPlanetFromSunInMeters = this.posInKm.clone().subtract(sun.posInKm).magnitude() * 1000;
		var orbitalSpeedInKps = Math.sqrt(gravityConstant * sun.massInKg / (distanceOfPlanetFromSunInMeters)) / 1000;
		this.velInKmPerSec.y = orbitalSpeedInKps;	
	}

	Body.prototype.updateForTimerTick = function()
	{
		var secondsPerTick = .001 * Globals.Instance.simulationMillisecondsPerTick;
		var metersPerKilometer = 1000;

		this.accelInKmPerSecSquared.add
		(
			this.forceInNewtons.clone().divideScalar(this.massInKg).divideScalar(metersPerKilometer)
		);
		this.velInKmPerSec.add(this.accelInKmPerSecSquared.clone().multiplyScalar(secondsPerTick));
		this.posInKm.add(this.velInKmPerSec.clone().multiplyScalar(secondsPerTick));

		this.forceInNewtons.clear();
		this.accelInKmPerSecSquared.clear();
	}
}

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, 1);
		this.Zeroes = new Coords(0, 0, 0);
	}

	// methods

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

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

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

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

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

	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

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

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

function Display(sizeInPixels, fontHeightInPixels, colorFore, colorBack)
{
	this.sizeInPixels = sizeInPixels;
	this.fontHeightInPixels = fontHeightInPixels;
	this.colorFore = colorFore;
	this.colorBack = colorBack;
	
	this.sizeInPixelsHalf = this.sizeInPixels.clone().divideScalar(2);
}
{
	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.sizeInPixels.x;
		this.canvas.height = this.sizeInPixels.y;
		
		this.graphics = this.canvas.getContext("2d");

		this.graphics.font = this.fontHeightInPixels + "px sans-serif";
		
		document.body.appendChild(this.canvas);
	}
	
	// drawing
	
	Display.prototype.clear = function()
	{
		this.drawRectangle(Coords.Instances.Zeroes, this.sizeInPixels, this.colorBack, this.colorFore);
	}
	
	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.drawText = function(textToDraw, pos)
	{
		this.graphics.fillStyle = this.colorFore;
		this.graphics.fillText(textToDraw, pos.x, pos.y + this.fontHeightInPixels);
	}
}

function Globals()
{
	// do nothing
}
{
	// instance
	
	Globals.Instance = new Globals();	
	
	// methods
	
	Globals.prototype.initialize = function
	(
		display,
		gravityConstant, 
		kilometersPerPixel, 
		simulationMillisecondsPerTick, 
		realWorldMillisecondsPerTick,
		dateStart, 
		solarSystem
	)	
	{	
		this.display = display;
		this.gravityConstant = gravityConstant;
		this.kilometersPerPixel = kilometersPerPixel;		
		this.simulationMillisecondsPerTick = simulationMillisecondsPerTick;
		this.realWorldMillisecondsPerTick = realWorldMillisecondsPerTick;
		this.dateCurrent = dateStart;

		this.solarSystem = solarSystem;

		this.display.initialize();		
		this.solarSystem.initialize();

		setInterval(this.updateForTimerTick.bind(this), this.realWorldMillisecondsPerTick);
	}

	Globals.prototype.updateForTimerTick = function()
	{
		this.solarSystem.updateForTimerTick();
	}
}

function SolarSystem(name, planets)
{
	this.name = name;
	this.planets = planets;	
}
{
	SolarSystem.prototype.drawToDisplay = function(display)
	{
		display.drawRectangle
		(
			Coords.Instances.Zeroes, display.sizeInPixels, 
			display.colorBack, display.colorFore
		);
	
		for (var i = 0; i < this.planets.length; i++)
		{
			var planet = this.planets[i];
			planet.drawToDisplay(display);
		}

		var dateCurrentAsString = Globals.Instance.dateCurrent.toString();
		display.drawText(dateCurrentAsString, Coords.Instances.Zeroes);
	}

	SolarSystem.prototype.initialize = function()
	{
		var sun = this.planets[0];
		var gravityConstant = Globals.Instance.gravityConstant;

		var numberOfPlanets = this.planets.length;
		for (var i = 1; i < numberOfPlanets; i++)
		{
			var planet = this.planets[i];
			planet.initialize(gravityConstant, sun);
		}
	}

	SolarSystem.prototype.updateForTimerTick = function()
	{	
		var dateCurrent = Globals.Instance.dateCurrent;
		dateCurrent.setMilliseconds(dateCurrent.getMilliseconds() + Globals.Instance.simulationMillisecondsPerTick);

		var sun = this.planets[0];

		for (var i = 1; i < this.planets.length; i++)
		{
			var planet = this.planets[i];

			var displacementFromSunToPlanetInKm = sun.posInKm.clone().subtract(planet.posInKm);
			var distanceFromSunToPlanetInKm = displacementFromSunToPlanetInKm.magnitude();
			var directionFromSunToPlanet = displacementFromSunToPlanetInKm.divideScalar(distanceFromSunToPlanetInKm);

			var distanceFromSunToPlanetInMeters = distanceFromSunToPlanetInKm * 1000;

			var magnitudeOfForceOnPlanet = 
				Globals.Instance.gravityConstant 
				* sun.massInKg
				* planet.massInKg
				/ (distanceFromSunToPlanetInMeters * distanceFromSunToPlanetInMeters);

			var forceOnPlanet = directionFromSunToPlanet.clone().multiplyScalar(magnitudeOfForceOnPlanet);

			planet.forceInNewtons.add(forceOnPlanet);

			planet.updateForTimerTick();
		}
		
		this.drawToDisplay(Globals.Instance.display);
	}
}

// run

new Planetarium().main();

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

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

7 Responses to Programming A Primitive Orrery in JavaScript

  1. This is a pretty cool idea, but sadly the massive amount of code drowns out my ability to find the problem.

    • For the sake of posterity: The problem that After Hours Programming was referring to was that, in the line of code where I was calculating the force on a planet due to the Sun’s gravity, I had to multiply the results by a fudge factor of 0.001 to prevent the planets from dropping directly towards the Sun and then slingshotting out into interstellar space. I have since located the actual problem (which was occurring in the calculation of the acceleration on the planet based on the force on it), fixed it, and removed the fudge factor.

  2. ANDREW OSBORN says:

    Nice. Mind if I borrow some of the gravitational code. I can’t seem to find it anywhere else.

  3. khaled says:

    really cool…

  4. gwistix says:

    Very cool. FYI, all the planets except Venus and Uranus (and Pluto) actually orbit in a counter-clockwise direction. In any case, I love that you actually did the gravity and stuff, instead of just having simple rotators.

    • gwistix says:

      Correction: All of the planets orbit in a counter-clockwise direction. They also all spin in a counter-clockwise direction, except for Venus, Uranus, and (dwarf planet) Pluto.

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