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.

Orrery

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

function Planetarium()
{
    this.main = function()
    {
        var solarSystem0 = new SolarSystem
        (
            "SolarSystem0",
            new Array
            (
                // new Body(name, color, massInKg, radiusInKm, periapsisInKm, apoapsisInKm)
                new Body("Sun",     "#ffff00", 1.9891 * Math.pow(10, 30), 695500, 0, 0),
                new Body("Mercury",     "#aaaaaa", 3.3022 * Math.pow(10, 23), 2439.7, 46001200, 69816900),
                new Body("Venus",     "#ffffff", 4.8685 * Math.pow(10, 24), 6051.8, 107476259, 108942109),
                new Body("Earth",     "#00ffff", 5.9736 * Math.pow(10, 24), 6371.0, 147098290, 152098232),
                new Body("Mars",     "#ff0000", 6.4185 * Math.pow(10, 23), 3396.2, 206669000, 227939100),
                new Body("Jupiter",     "#ffccaa", 1.8986 * Math.pow(10, 27), 71492,  740573600, 778547200)
                // Saturn
                // Uranus
                // Neptune
                // Pluto
            )
        );

        Globals.Instance = new Globals();

        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 viewSizeInPixels = new Coords(1080, 1080); // viewSizeInPixels;
        var dateStart = new Date(2012, 0, 1, 0, 0, 0, 0); // January 1, 2012

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

// classes

function Coords(x, y)
{
    this.x = x;
    this.y = y;
}
{
    var prototype = Coords.prototype;

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

        return this;
    }

    prototype.clear = function()
    {
        this.x = 0;
        this.y = 0;

        return this;
    }

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

        return returnValue;
    }

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

        return this;
    }

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

        return returnValue;
    }

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

        return this;
    }

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

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

        return this;
    }

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

        return this;
    }

    prototype.toString = function()
    {
        return "<Coords x='" + this.x + "' y='" + this.y + "' />"
    }
}

function Globals()
{
    this.initialize = function
    (
        gravityConstant, 
        kilometersPerPixel, 
        simulationMillisecondsPerTick, 
        realWorldMillisecondsPerTick, 
        viewSizeInPixels, 
        dateStart, 
        solarSystem
    )    
    {
        this.gravityConstant = gravityConstant;
        this.kilometersPerPixel = kilometersPerPixel;        
        this.simulationMillisecondsPerTick = simulationMillisecondsPerTick;
        this.realWorldMillisecondsPerTick = realWorldMillisecondsPerTick;
        this.viewSizeInPixels = viewSizeInPixels;
        this.viewSizeInPixelsHalf = this.viewSizeInPixels.clone().divideScalar(2);
        this.dateCurrent = dateStart;

        this.solarSystem = solarSystem;

        this.solarSystem.initialize();

        var solarSystemAsHtmlElement = this.solarSystem.htmlElementConvert();

        document.body.appendChild(solarSystemAsHtmlElement);

        setInterval("Globals.Instance.processTick()", this.realWorldMillisecondsPerTick);
    }

    this.processTick = function()
    {
        this.solarSystem.processTick();
    }
}

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.planetText = "Planet";

    var prototype = Body.prototype;

    prototype.htmlElementConvert = function()
    {
        var planetBodyAsHtmlElement = document.createElement("div");

        var kmPerPixel = Globals.Instance.kilometersPerPixel;
        var radiusInPixels = Math.ceil(this.radiusInKm / kmPerPixel);
        var diameterInPixels = radiusInPixels * 2;
        var offsetToViewCenter = Globals.Instance.viewSizeInPixelsHalf;
        var posInPixels = this.posInKm.clone().divideScalar(kmPerPixel).round().add(offsetToViewCenter);
        var boundingBoxSizeInPixels = new Coords(100, 100);
        var boundingBoxSizeHalfInPixels = boundingBoxSizeInPixels.clone().divideScalar(2);

        planetBodyAsHtmlElement.style.backgroundColor = this.color;
        planetBodyAsHtmlElement.style.width = "" + diameterInPixels + "px";
        planetBodyAsHtmlElement.style.height = "" + diameterInPixels + "px";
        planetBodyAsHtmlElement.style.position = "absolute";
        planetBodyAsHtmlElement.style.left = (boundingBoxSizeHalfInPixels.x - radiusInPixels) + "px";
        planetBodyAsHtmlElement.style.top = (boundingBoxSizeHalfInPixels.y - radiusInPixels) + "px";

        var planetNameAsHtmlElement = document.createElement("p");
        planetNameAsHtmlElement.innerHTML = this.name;
        planetNameAsHtmlElement.style.color = this.color;
        planetNameAsHtmlElement.style.position = "absolute";
        planetNameAsHtmlElement.style.left = planetBodyAsHtmlElement.style.left;
        planetNameAsHtmlElement.style.top = planetBodyAsHtmlElement.style.top;

        var planetBodyAndNameAsHtmlElement = document.createElement("div");
        planetBodyAndNameAsHtmlElement.id = Body.planetText + this.name;
        planetBodyAndNameAsHtmlElement.style.width = boundingBoxSizeInPixels.x + "px";
        planetBodyAndNameAsHtmlElement.style.height = boundingBoxSizeInPixels.y + "px";
        planetBodyAndNameAsHtmlElement.style.position = "absolute";
        planetBodyAndNameAsHtmlElement.style.left = (posInPixels.x - boundingBoxSizeHalfInPixels.x) + "px";
        planetBodyAndNameAsHtmlElement.style.top = (posInPixels.y - boundingBoxSizeHalfInPixels.y) + "px";
        planetBodyAndNameAsHtmlElement.appendChild(planetBodyAsHtmlElement);
        planetBodyAndNameAsHtmlElement.appendChild(planetNameAsHtmlElement);

        this.htmlElement = planetBodyAndNameAsHtmlElement;

        return this.htmlElement;
    }

    prototype.htmlElementUpdate = function()
    {
        var kmPerPixel = Globals.Instance.kilometersPerPixel;
        var offsetToViewCenter = Globals.Instance.viewSizeInPixelsHalf;
        var posInPixels = this.posInKm.clone().divideScalar(kmPerPixel).round().add(offsetToViewCenter);
        var radiusInPixels = Math.ceil(this.radiusInKm / kmPerPixel);
        var boundingBoxSizeHalfInPixels = new Coords(50, 50);

        this.htmlElement.style.left = (posInPixels.x - boundingBoxSizeHalfInPixels.x) + "px";
        this.htmlElement.style.top = (posInPixels.y - boundingBoxSizeHalfInPixels.y) + "px";
    }

    prototype.processTick = 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();

        this.htmlElementUpdate();
    }
}

function SolarSystem(name, planets)
{
    this.name = name;
    this.planets = planets;    
}
{
    SolarSystem.solarSystemText = "SolarSystem";

    var prototype = SolarSystem.prototype;

    prototype.htmlElementConvert = function()
    {
        var viewSizeInPixels = Globals.Instance.viewSizeInPixels;

        var returnValue = document.createElement("div");
        returnValue.id = SolarSystem.solarSystemText + name;
        returnValue.style.backgroundColor = "#000000";
        returnValue.style.width = viewSizeInPixels.x + "px";
        returnValue.style.height = viewSizeInPixels.y + "px";

        var dateCurrentAsHtmlElement = document.createElement("p");
        dateCurrentAsHtmlElement.id = "pDateCurrent";
        dateCurrentAsHtmlElement.style.color = "#ffffff";
        dateCurrentAsHtmlElement.style.position = "absolute";
        dateCurrentAsHtmlElement.style.left = "16px";
        dateCurrentAsHtmlElement.style.top = "16px";
        dateCurrentAsHtmlElement.innerHTML = "[dateCurrent]";
        returnValue.appendChild(dateCurrentAsHtmlElement);
        this.htmlElementForDateCurrent = dateCurrentAsHtmlElement;

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

            returnValue.appendChild(planet.htmlElementConvert());
        }

        this.htmlElement = returnValue;

        return this.htmlElement;
    }

    prototype.htmlElementUpdate = function()
    {
        this.htmlElementForDateCurrent.innerHTML = Globals.Instance.dateCurrent.toString();
    }

    prototype.initialize = function()
    {
        var sun = this.planets[0];
        var sunMassInKg = sun.massInKg;
        var sunPosInKm = sun.posInKm;

        var gravityConstant = Globals.Instance.gravityConstant;

        var numberOfPlanets = this.planets.length;
        for (var i = 1; i < numberOfPlanets; i++)
        {
            var planet = this.planets[i];
            var distanceOfPlanetFromSunInMeters = planet.posInKm.clone().subtract(sunPosInKm).magnitude() * 1000;

            var orbitalSpeedInKps = Math.sqrt(gravityConstant * sunMassInKg / (distanceOfPlanetFromSunInMeters)) / 1000;
            planet.velInKmPerSec.y = orbitalSpeedInKps;
        }
    }

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

        var sun = this.planets[0];

        var numberOfPlanets = this.planets.length;
        for (var i = 1; i < numberOfPlanets; 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.processTick();

        }
        this.htmlElementUpdate();
    }
}

new Planetarium().main();

</script>
</body>
</html>
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

6 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.

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