Visualizing a Solar System with a Base Plane and Elevations

The JavaScript code below renders a solar system in three dimensions. Each planet’s height above or depth below a reference plane is shown by a perpendicular line that connects it to a grid on the base plane. 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/solarsystem.html.

SolarSystem

You can move the camera by using the A, D, S, W, R, and F keys. There’s also a little green square named “Ship” that can be moved from place to place. When you click the ship, a cursor appears. Clicking again fixes the cursor’s position on the XY plane. Clicking yet again fixes the cursor’s Z position, and the ship will start moving toward the specified location.

Despite my intentions to keep things simple, the program kind of ended up being both under- and over-engineered. Some framework stuff from other games projects worked its way in here and there, notably all the stuff in the “Body”, “BodyDefn” and “Category” classes. And there’s still some bugs. For one, if you get the camera too close to the base plane’s grid, it will start rendering incorrectly. The solution to that would probably be to clip the lines in the grid against the camera’s view plane, but I haven’t implemented that yet.

My intent with this, as with the program described in the previous post, was to use it in a video game, perhaps one similar to the old DOS game Ascendancy.

UPDATE 2013/09/25: Well, as you can see in the comments section, the only comment I’ve received on this so far is a complaint of how hard it is to understand.  So here’s a very halfassed and unsatisfying explanation of the architecture:

Basically, this thing is running a simple game loop.  “Globals.Instance” is a singleton object at the root of the tree.  Within the Globals.Instance object, there’s a “Universe” object, which contains a bunch of definitions and an instance of the “Venue” class.

Each instance of Venue (though in this case there’s only one) contains a bunch of “Body” objects.  In this case, the Venue object represents the entire solar system.

Each instance of the Body object would usually be something you can see on the screenshot, like the planets, ship, or the base grid.  Usually.  But the camera is also a Body, and of course you can’t see that.  If I wanted to add an object that, say, generates a new comet every few minutes, the object that does the generating would probably be a Body too, even though that’s not really what the word “body” means in English.

Each Body has a reference to a “BodyDefn”.  Each BodyDefn, in turn, belongs to a bunch of “Categories”.  A Category might be something like “Drawable” (something you can see), “Collidable” (something that can collide with something else), “Actor” (something that does something, like for instance a ship as opposed to a planet), or “Constrainable” (something whose movement is restricted in some way, like how the camera always points at the sun).

The game is started by calling Globals.Instance.initialize().  Every 100 (I think) milliseconds or so, a timer loops through each of the Bodies in the current (and only, in this case) Venue, and for each Category to which that body’s BodyDefn belongs, it performs a chunk of code for the current Body and Venue.

The calculations that figure out where to draw each thing on the screen involve vector math.  I’m going to go ahead and say that a detailed review of vector math is outside the scope of this post.

That’s the gist of it.  Still confused?  Yes, of course you are.

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

// main

function SpaceMovementTest()
{
	this.main = function()
	{
		new Color_Instances();

		var bodyDefns = 
		[
			new BodyDefn
			(
				"Camera",
				[
					"Actor",
					"Camera",
					"Constrainable",
				],
				[
					new ActorDefn
					(
						ActivityDefn.Instances.UserInputAccept
					),
					new CameraDefn
					(
						new Coords(600, 600, 600), // viewSize, 
						600 // focalLength, 
					)
				]

			),

			new BodyDefn
			(
				"Cursor", 
				// categoryNames
				[
					"Constrainable",
					"Cursor",
					"Drawable",
				], 	
				[	
					new DrawableDefn(new VisualRectangle(new Coords(8, 8, 1), Color.Instances.White)),
				]
			),

			new BodyDefn
			(
				"Grid",
				[
					"Drawable",
				],
				[
					new DrawableDefn(new VisualGrid()),
				]
			),

			new BodyDefn
			(
				"Planet:Gas", 	
				// categoryNames
				[
					"Drawable",
				],
				[
					new ClickableDefn(new ColliderSphere(8)), 
					new DrawableDefn(new VisualSphere(8, Color.Instances.Tan, null, 0)),
				]
			),
			new BodyDefn
			(
				"Planet:Greenhouse", 	
				// categoryNames
				[
					"Drawable",
				],
				[
					new ClickableDefn(new ColliderSphere(5)), 
					new DrawableDefn(new VisualSphere(5, Color.Instances.White, null, 0)),
				]
			),
			new BodyDefn
			(
				"Planet:Rusty", 	
				// categoryNames
				[
					"Drawable",
				],
				[
					new ClickableDefn(new ColliderSphere(4)), 
					new DrawableDefn(new VisualSphere(4, Color.Instances.Red, null, 0)),
				]
			),
			new BodyDefn
			(
				"Planet:Selenic", 	
				// categoryNames
				[
					"Drawable",
				],
				[
					new ClickableDefn(new ColliderSphere(3)), 
					new DrawableDefn(new VisualSphere(3, Color.Instances.Gray, null, 0)),
				]
			),
			new BodyDefn
			(
				"Planet:Water", 
				// categoryNames
				[
					"Drawable",
				],
				[
					new ClickableDefn(new ColliderSphere(5)), 
					new DrawableDefn(new VisualSphere(5, Color.Instances.Cyan, null, 0)),
				]
			),

			new BodyDefn
			(
				"Ship", 		
				// categoryNames
				[
					"Actor",
					"Clickable",
					"Drawable",
				],
				[
					new ActorDefn(ActivityDefn.Instances.DoNothing),
					new ClickableDefn
					(
						new ColliderSphere(8),
						// click
						function(bodyClicked, clickPos)
						{
							var universe = Globals.Instance.universe;
							var venue = universe.venue;

							//venue.bodySelected = bodyClicked;

							var bodyDefnCursor = universe.bodyDefns["Cursor"];

							var bodyForCursor = new Body
							(
								"Destination for " + bodyClicked.name, 
								bodyDefnCursor, 
								[
									new CursorData(bodyClicked),
									new Location(clickPos.clone()),
								]
							);

							var constraints = bodyForCursor.constrainableData.constraintAdd
							(
								new Constraint_Cursor()
							);

							venue.bodiesToSpawn.push(bodyForCursor);
						}
					), 
					new DrawableDefn(new VisualRectangle(new Coords(8, 8, 1), Color.Instances.Green)),
				]

			),

			new BodyDefn
			(
				"Sun", 		
				// categoryNames
				[
					"Drawable",
				],
				[
					new ClickableDefn(new ColliderSphere(16)), 
					new DrawableDefn(new VisualSphere(16, Color.Instances.Yellow, null, 0)),
				]
			),
		];

		ArrayHelper.addLookupsToArray(bodyDefns, "name");

		var camera = new Body
		(
			"Camera",
			bodyDefns["Camera"],
			[
				new Location
				(
					new Coords(-600, 0, -300), //pos, 
					new Orientation
					(
						new Coords(1, 0, 0),
						new Coords(0, 0, 1)
					)
				),
			]
		);

		var venue = new Venue
		(
			"System 0",
			// bodyCategoryNamesKnown
			[
				"Actor",
				"Constrainable",
				"Drawable",
			],
			// bodies
			[
				camera, 

				new Body("Grid",	bodyDefns["Grid"],		[ new Location( new Coords(0, 0, 0) ) ] ),

				new Body("Sol", 	bodyDefns["Sun"], 		[ new Location( new Coords(0, 0, 0) ) ] ),

				new Body("Mercury", 	bodyDefns["Planet:Selenic"], 	[ new Location( new Coords(10, 20, 10) ) ] ),
				new Body("Venus", 	bodyDefns["Planet:Greenhouse"],	[ new Location( new Coords(20, -40, 10) ) ] ),
				new Body("Earth", 	bodyDefns["Planet:Water"], 	[ new Location( new Coords(-60, 50, -20) ) ] ),
				new Body("Mars", 	bodyDefns["Planet:Rusty"], 	[ new Location( new Coords(80, -80, -30) ) ] ),
				new Body("Jupiter", 	bodyDefns["Planet:Gas"], 	[ new Location( new Coords(-150, -100, -50) ) ] ),

				new Body("Ship", 	bodyDefns["Ship"], 		[ new Location( new Coords(150, 100, -100) ) ] ),
			]
		);

		var universe = new Universe
		(
			"Universe0",
			Category.Instances._All,
			bodyDefns,
			venue
		);

		var inputs = new Input_Instances();

		var inputToActionBindings = 
		[
			new InputToActionBinding( inputs.A, new Action_CylinderMove_Yaw(-1) ),
			new InputToActionBinding( inputs.D, new Action_CylinderMove_Yaw(1) ),
			new InputToActionBinding( inputs.F, new Action_CylinderMove_DistanceAlongAxis(1) ),
			new InputToActionBinding( inputs.R, new Action_CylinderMove_DistanceAlongAxis(-1) ),
			new InputToActionBinding( inputs.S, new Action_CylinderMove_Radius(1) ),
			new InputToActionBinding( inputs.W, new Action_CylinderMove_Radius(-1) ),
			new InputToActionBinding( inputs.MouseButton, new Action_BodySelect() ),
		];

		Globals.Instance.initialize
		(
			50, // millisecondsPerTick
			inputToActionBindings,
			universe
		);
	}
}

// classes

function Action_BodySelect()
{
	this.ticksToHold = 1;
}
{
	Action_BodySelect.prototype.perform = function(actor)
	{
		var venue = Globals.Instance.universe.venue;
		var cursors = venue.bodiesByCategoryName[Category.Instances.Cursor.name];

		if (cursors == null || cursors.length == 0)
		{
			this.perform_1(actor, venue);
		}
		else
		{
			var cursor = cursors[0];
			var cursorData = cursor.cursorData;

			if (cursorData.hasXYPositionBeenSpecified == false)
			{
				cursorData.hasXYPositionBeenSpecified = true;
			}
			else if (cursor.cursorData.hasZPositionBeenSpecified == false)
			{
				cursorData.hasZPositionBeenSpecified = true;

				cursorData.bodyParent.actorData.activity = new Activity
				(
					ActivityDefn.Instances.MoveTowardTarget,
					cursor.location.pos.clone()
				);

				var venue = Globals.Instance.universe.venue;
				venue.bodiesToRemove.push(cursor);
			}
		}
	}

	Action_BodySelect.prototype.perform_1 = function(actor, venue)
	{
		var camera = actor;
		var cameraDefn = camera.defn.cameraDefn;

		var clickPos = Globals.Instance.inputHelper.mousePos;

		var clickables = venue.bodiesByCategoryName[Category.Instances.Clickable.name];

		var drawPos = new Coords(0, 0, 0);

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

			drawPos.overwriteWith(body.location.pos);
			var bodyViewPos = cameraDefn.convertWorldCoordsToViewCoords(camera, drawPos);
			bodyViewPos.z = 0;

			var clickableDefn = body.defn.clickableDefn;
			var collider = clickableDefn.collider;

			if (collider.doesColliderAtPosCollideWithOther(drawPos, clickPos) == true)
			{
				clickableDefn.clickBody(body, clickPos);
				break;	
			}
		}

	}
}

function Action_CylinderMove_Yaw(sign)
{
	this.sign = sign;
}
{
	Action_CylinderMove_Yaw.prototype.perform = function(actor)
	{
		var constraintCylinder = actor.constrainableData.constraints["PositionOnCylinder"];

		var radiansToMove = this.sign * Math.PI / 90;

		constraintCylinder.yaw += radiansToMove;
	}
}

function Action_CylinderMove_DistanceAlongAxis(sign)
{
	this.sign = sign;
}
{
	Action_CylinderMove_DistanceAlongAxis.prototype.perform = function(actor)
	{
		var constraintCylinder = actor.constrainableData.constraints["PositionOnCylinder"];

		var distanceToMove = this.sign * 10;

		constraintCylinder.distanceFromCenterAlongAxis += distanceToMove;

	}
}

function Action_CylinderMove_Radius(sign)
{
	this.sign = sign;
}
{
	Action_CylinderMove_Radius.prototype.perform = function(actor)
	{
		var constraintCylinder = actor.constrainableData.constraints["PositionOnCylinder"];

		var distanceToMove = this.sign * 10;

		constraintCylinder.radius += distanceToMove;
	}
}

function Action_Message(messageText)
{
	this.messageText = messageText;
}
{
	Action_Message.prototype.perform = function(actor)
	{
		alert(this.messageText);
	}
}

function Activity(defn, target)
{
	this.defn = defn;
	this.target = target;
}
{
	Activity.prototype.perform = function(actor)
	{
		this.defn.perform(actor, this);
	}
}

function ActivityDefn(name, perform)
{
	this.name = name;
	this.perform = perform;
}
{
	function ActivityDefn_Instances()
	{
		this.DoNothing = new ActivityDefn
		(
			"Do Nothing",
			function(actor, activity)
			{
				// do nothing
			}
		);

		this.MoveTowardTarget = new ActivityDefn
		(
			"Move Toward Target",
			function (actor, activity)
			{
				// hack
				// Should do this with an Action
				// rather than an Activity.

				var actorPos = actor.location.pos;
				var targetPos = activity.target;

				var displacementFromActorToTarget = targetPos.clone().subtract
				(
					actorPos
				);

				var distanceFromActorToTarget = displacementFromActorToTarget.magnitude();

				var speed = 2;

				if (distanceFromActorToTarget < speed)
				{
					actorPos.overwriteWith(targetPos);
					activity.defn = ActivityDefn.Instances.DoNothing;
				}
				else
				{
					var directionFromActorToTarget = displacementFromActorToTarget.clone().divideScalar
					(
						distanceFromActorToTarget
					);

					actorPos.add
					(
						directionFromActorToTarget.multiplyScalar
						(
							speed
						)
					);
				}
			}
		);

		this.UserInputAccept = new ActivityDefn
		(
			"Accept User Input",
			function(actor, activity)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var actionsFromInput = inputHelper.actionsForInputsPressed;
				var actionsFromActor = actor.actorData.actions; 

				for (var a = 0; a < actionsFromInput.length; a++)
				{
					var action = actionsFromInput[a];
					var ticksToHold = 
					(
						action.ticksToHold == null 
						? action.ticksSoFar // hold forever
						: action.ticksToHold
					);

					if (action.ticksSoFar <= ticksToHold)
					{
						actionsFromActor.push(action);
					}
				}

			}
		);

	}

	ActivityDefn.Instances = new ActivityDefn_Instances();
}

function ActorData(actorDefn)
{
	this.activity = new Activity(actorDefn.activityDefnDefault);
	this.actions = [];
}

function ActorDefn(activityDefnDefault)
{
	this.activityDefnDefault = activityDefnDefault;
}

function ArrayHelper()
{}
{
	ArrayHelper.addLookupsToArray = function(arrayToAddLookupsTo, propertyNameForKey)
	{
		for (var i = 0; i < arrayToAddLookupsTo.length; i++)
		{
			var arrayItem = arrayToAddLookupsTo[i];
			var key = arrayItem[propertyNameForKey];
			arrayToAddLookupsTo[key] = arrayItem;
		}
	}

	ArrayHelper.getPropertyValueForEachItemInArray = function(propertyName, arrayToGetFrom)
	{
		var returnValues = [];

		for (var i = 0; i < arrayToGetFrom.length; i++)
		{
			returnValues.push(arrayToGetFrom[i][propertyName]);
		}

		return returnValues;
	}
}

function Body(name, defn, properties)
{
	this.name = name;
	this.defn = defn;

	for (var i = 0; i < properties.length; i++)
	{
		var propertyValue = properties[i];
		var propertyName = propertyValue.constructor.name;
		var propertyName = 
			propertyName.substring(0, 1).toLowerCase() 
			+ propertyName.substring(1);

		this[propertyName] = propertyValue;
	}

	var categoryNames = this.defn.categoryNames;

	for (var c = 0; c < categoryNames.length; c++)
	{
		var categoryName = categoryNames[c];
		var category = Category.Instances._All[categoryName];
		if (category.constructBody != null)
		{
			category.constructBody(this);
		}
	}
}

function BodyDefn(name, categoryNames, properties)
{
	this.name = name;
	this.categoryNames = categoryNames;
	for (var i = 0; i < properties.length; i++)
	{
		var propertyValue = properties[i];
		var propertyName = propertyValue.constructor.name;
		var propertyName = 
			propertyName.substring(0, 1).toLowerCase() 
			+ propertyName.substring(1);

		this[propertyName] = propertyValue;
	}
}

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
}

function CameraDefn
(
	viewSize, 
	focalLength 
)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;

	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}
{
	CameraDefn.prototype.convertWorldCoordsToViewCoords = function(cameraBody, coordsToConvert)
	{
		coordsToConvert.subtract
		(
			cameraBody.location.pos
		);

		var orientation = cameraBody.location.orientation;

		coordsToConvert.overwriteWithDimensions
		(
			orientation.right.dotProduct(coordsToConvert),
			orientation.down.dotProduct(coordsToConvert),
			orientation.forward.dotProduct(coordsToConvert)
		);

		var distanceForwardInFocalLengths = coordsToConvert.z / this.focalLength;

		coordsToConvert.x /= distanceForwardInFocalLengths;
		coordsToConvert.y /= distanceForwardInFocalLengths;

		coordsToConvert.x += this.viewSize.x / 2;
		coordsToConvert.y += this.viewSize.y / 2;

		return coordsToConvert;
	}
}

function Category(name, constructBody, updateBodyForVenue)
{
	this.name = name;
	this.constructBody = constructBody;
	this.updateBodyForVenue = updateBodyForVenue;
}
{
	function Category_Instances()
	{
		this.Actor = new Category
		(
			"Actor",

			// construct
			function(body) 
			{ 	
				body.actorData = new ActorData(body.defn.actorDefn); 
			},

			// updateBodyForVenue
			function(venue, body)
			{
				var actorData = body.actorData;

				actorData.activity.perform(body);

				var bodyActions = actorData.actions;

				for (var a = 0; a < bodyActions.length; a++)
				{
					var action = bodyActions[a];
					action.perform(body);
				}

				bodyActions.length = 0;
			}
		);

		this.Camera = new Category
		(
			"Camera",
			null,
			null
		);

		this.Clickable = new Category
		(
			"Clickable",
			null,
			null
		);

		this.Constrainable = new Category
		(
			"Constrainable",
			function(body)
			{
				body.constrainableData = new ConstrainableData();
			},
			function(venue, body)
			{
				var constraints = body.constrainableData.constraints;

				for (var i = 0; i < constraints.length; i++)
				{
					var constraint = constraints[i];
					constraint.applyToBody(body);
				}
			}
		);

		this.Cursor = new Category
		(
			"Cursor",
			null,
			null
		);

		this.Drawable = new Category
		(
			"Drawable",
			null,
			function(venue, body)
			{
				var graphics = venue.graphics;
				var camera = venue.camera;
				var cameraDefn = camera.defn.cameraDefn;
				var drawPos = new Coords(0, 0, 0);
				var bodyPos = body.location.pos;

				drawPos.overwriteWith(bodyPos);
				cameraDefn.convertWorldCoordsToViewCoords(camera, drawPos);

				body.defn.drawableDefn.visual.draw(graphics, camera, drawPos, body);

				var visualColor = body.defn.drawableDefn.visual.color;

				if (visualColor != null)
				{
					graphics.strokeStyle = Color.Instances.Black.systemColor;

					graphics.strokeText
					(
						body.name, drawPos.x, drawPos.y
					);

					graphics.fillStyle = visualColor.systemColor; // hack

					graphics.fillText
					(
						body.name, drawPos.x, drawPos.y
					);
				}

				if (bodyPos.z < 0)
				{
					graphics.strokeStyle = Color.Instances.Green.systemColor;
				}
				else
				{
					graphics.strokeStyle = Color.Instances.Red.systemColor;
				}

				graphics.beginPath();
				graphics.moveTo(drawPos.x, drawPos.y);

				drawPos.overwriteWith(bodyPos);
				drawPos.z = 0;
				cameraDefn.convertWorldCoordsToViewCoords(camera, drawPos);

				graphics.lineTo(drawPos.x, drawPos.y);
				graphics.stroke();
			}	

		);

		this.Locatable = new Category
		(
			"Locatable",
			null,
			null
		);

		this._All = 
		[
			this.Actor,
			this.Camera,
			this.Clickable,
			this.Constrainable,
			this.Cursor,
			this.Drawable,
			this.Locatable,
		];

		ArrayHelper.addLookupsToArray(this._All, "name");
	}

	Category.Instances = new Category_Instances();
}

function ClickableDefn(collider, clickBody)
{
	this.collider = collider;
	this.clickBody = clickBody;
}

function ColliderSphere(radius)
{
	this.radius = radius;
}
{
	ColliderSphere.prototype.doesColliderAtPosCollideWithOther = function(colliderPos, posToCheck)
	{
		var distanceToPos = colliderPos.clone().subtract(posToCheck).magnitude();
		var returnValue = (distanceToPos < this.radius)
		return returnValue;
	}
}

function Collision()
{}
{
	Collision.findIntersectionBetweenRayAndPlane = function(ray, plane)
	{
		var t = 
			(
				plane.distanceFromOrigin 
				- plane.normal.dotProduct(ray.startPos)
			)
			/ plane.normal.dotProduct(ray.direction);

		var collisionPos = ray.direction.clone().multiplyScalar(t).add(ray.startPos);

		return collisionPos;
	}
}

function Color(name, systemColor)
{
	this.name = name;
	this.systemColor = systemColor;
}
{
	function Color_Instances()
	{
		if (Color.Instances == null)
		{
			Color.Instances = this;
		}

		this.Black 	= new Color("Black", "rgb(0, 0, 0)");
		this.Blue 	= new Color("Blue", "rgb(0, 0, 255)");
		this.Cyan 	= new Color("Cyan", "rgb(0, 255, 255)");
		this.CyanHalfTranslucent = new Color("CyanHalfTranslucent", "rgba(0, 128, 128, .5)");
		this.Gray 	= new Color("Gray", "rgb(128, 128, 128)");
		this.Green 	= new Color("Green", "rgb(0, 255, 0)");
		this.Red 	= new Color("Red", "rgb(255, 0, 0)");
		this.Tan 	= new Color("Gray", "rgb(196, 196, 128)");
		this.White 	= new Color("White", "rgb(255, 255, 255)");
		this.Yellow 	= new Color("Yellow", "rgb(255, 255, 0)");
	}

	Color.Instances = new Color_Instances();
}

function Constants()
{}
{
	Constants.Tau = Math.PI * 2;
}

function ConstrainableData()
{
	this.constraints = [];	
}
{
	ConstrainableData.prototype.constraintAdd = function(constraintToAdd)
	{
		this.constraints.push(constraintToAdd);
		this.constraints[constraintToAdd.name] = constraintToAdd;
	}
}

function Constraint_Cursor()
{
	this.name = "Cursor";
}
{
	Constraint_Cursor.prototype.applyToBody = function(body)
	{
		var cursor = body;
		var venue = Globals.Instance.universe.venue;
		var mousePos = Globals.Instance.inputHelper.mousePos.clone();

		var camera = venue.camera;
		var cameraDefn = camera.defn.cameraDefn;
		var cameraOrientation = camera.location.orientation;
		mousePos.subtract(cameraDefn.viewSizeHalf);

		var xyPlaneNormal = new Coords(0, 0, 1);

		var boundsToRestrictTo;
		var cursorPos = cursor.location.pos;

		var displacementFromCameraToMousePosProjected = cameraOrientation.forward.clone().multiplyScalar
		(
			cameraDefn.focalLength
		).add
		(
			cameraOrientation.right.clone().multiplyScalar
			(
				mousePos.x
			)
		).add
		(
			cameraOrientation.down.clone().multiplyScalar
			(
				mousePos.y
			)
		);

		var rayFromCameraToMousePos = new Ray
		(
			camera.location.pos,
			displacementFromCameraToMousePosProjected
		);

		if (cursor.cursorData.hasXYPositionBeenSpecified == false)
		{
			boundsToRestrictTo = new Bounds
			(
				new Coords(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, 0),
				new Coords(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, 0)
			);

			var planeToRestrictTo = new Plane
			(
				xyPlaneNormal,
				0
			);

			var collisionPos = Collision.findIntersectionBetweenRayAndPlane
			(
				rayFromCameraToMousePos,
				planeToRestrictTo
			);

			if (collisionPos != null)
			{
				body.location.pos.overwriteWith(collisionPos);
			}
		}
		else
		{
			boundsToRestrictTo = new Bounds
			(
				new Coords(cursorPos.x, cursorPos.y, Number.NEGATIVE_INFINITY),
				new Coords(cursorPos.x, cursorPos.y, Number.POSITIVE_INFINITY)
			);

			var planeNormal = xyPlaneNormal.crossProduct
			(
				cameraOrientation.right
			);

			cursorPos.z = 0;

			var planeToRestrictTo = new Plane
			(
				planeNormal,
				cursorPos.dotProduct(planeNormal)
			);

			var collisionPos = Collision.findIntersectionBetweenRayAndPlane
			(
				rayFromCameraToMousePos,
				planeToRestrictTo
			);

			if (collisionPos != null)
			{
				cursorPos.z = collisionPos.z;
			}			
		}

		cursorPos.trimToRangeMinMax
		(
			boundsToRestrictTo.min,	
			boundsToRestrictTo.max
		);
	}	
}

function Constraint_LookAtBody
(
	targetBody
)
{
	this.name = "LookAtBody";
	this.targetBody = targetBody;
}
{
	Constraint_LookAtBody.prototype.applyToBody = function(body)
	{
		var bodyOrientationForward = this.targetBody.location.pos.clone().subtract
		(
			body.location.pos
		).normalize();

		body.location.orientation = new Orientation
		(
			bodyOrientationForward,
			body.location.orientation.down	
		);		
	}
}

function Constraint_PositionOnCylinder(center, orientation, yaw, radius, distanceFromCenterAlongAxis)
{
	this.name = "PositionOnCylinder";
	this.center = center;
	this.orientation = orientation;
	this.yaw = yaw;
	this.radius = radius;
	this.distanceFromCenterAlongAxis = distanceFromCenterAlongAxis;
}
{
	Constraint_PositionOnCylinder.prototype.applyToBody = function(body)
	{
		this.yaw = NumberHelper.wrapValueToRangeMinMax(this.yaw, 0, Constants.Tau);

		var bodyPos = body.location.pos;		

		bodyPos.overwriteWith
		(
			this.orientation.down
		).multiplyScalar
		(
			this.distanceFromCenterAlongAxis
		).add
		(
			this.center
		).add
		(
			this.orientation.forward.clone().multiplyScalar
			(
				Math.cos(this.yaw)
			).add
			(
				this.orientation.right.clone().multiplyScalar
				(
					Math.sin(this.yaw)
				)
			).multiplyScalar
			(
				this.radius
			)

		)

		body.location.orientation.overwriteWith(this.orientation);
	}	
}

function Coords(x, y, z)
{
	this.x = x;
	this.y = y;
	this.z = z;
}
{
	Coords.NumberOfDimensions = 3;

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

		return this;
	}

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

	Coords.prototype.crossProduct = function(other)
	{
		return new Coords
		(
			this.y * other.z - this.z * other.y,
			this.z * other.x - this.x * other.z,
			this.x * other.y - this.y * other.x
		);
	}

	Coords.prototype.dimension = function(dimensionIndex)
	{
		var returnValue;

		if (dimensionIndex == 0)
		{
			returnValue = this.x;
		}
		else if (dimensionIndex == 1)
		{
			returnValue = this.y;
		}
		else 
		{
			returnValue = this.z;
		}

		return returnValue;
	}

	Coords.prototype.dimension_Set = function(dimensionIndex, value)
	{
		if (dimensionIndex == 0)
		{
			this.x = value;
		}
		else if (dimensionIndex == 1)
		{
			this.y = value;
		}
		else 
		{
			this.z = value;
		}

		return this;
	}

	Coords.prototype.dotProduct = function(other)
	{
		var returnValue = 
			this.x * other.x 
			+ this.y * other.y
			+ this.z * other.z;

		return returnValue;
	}

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

		return this;
	}

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

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

		return this;
	}

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

		return this;
	}

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

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		this.z = other.z;

		return this;
	}

	Coords.prototype.overwriteWithDimensions = function(x, y, z)
	{
		this.x = x;
		this.y = y;
		this.z = z;

		return this;
	}

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

		return this;
	}

	Coords.prototype.trimToRangeMinMax = function(min, max)
	{
		for (var d = 0; d < Coords.NumberOfDimensions; d++)
		{
			var thisDimension = this.dimension(d);

			var minDimension = min.dimension(d);
			var maxDimension = max.dimension(d);

			if (thisDimension < minDimension)
			{
				thisDimension = minDimension;
			} 
			else if (thisDimension > maxDimension)
			{
				thisDimension = maxDimension;				
			}

			this.dimension_Set(d, thisDimension);
		}
	}
}

function CursorData(bodyParent)
{
	this.bodyParent = bodyParent;
	this.hasXYPositionBeenSpecified = false;
	this.hasZPositionBeenSpecified = false;
}

function DrawableDefn(visual)
{
	this.visual = visual;
}

function Globals()
{
	this.inputHelper = new InputHelper();
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function
	(
		millisecondsPerTimerTick, 
		inputToActionBindings,
		universe
	)
	{
		this.inputHelper.initialize(inputToActionBindings);

		this.universe = universe;

		setInterval
		(
			"Globals.Instance.updateForTimerTick()", 
			millisecondsPerTimerTick
		);

		this.universe.venue.initialize();
	}

	Globals.prototype.updateForTimerTick = function()
	{
		Globals.Instance.inputHelper.updateForTimerTick();
		Globals.Instance.universe.venue.updateForTimerTick();
	}
}

function Gradient(stops)
{
	this.stops = stops;
}
{
	Gradient.prototype.toSystemGraphicsStyle = function(graphics, center, radius)
	{
		var returnValue = graphics.createRadialGradient
		(
			center.x,
			center.y,
			0, // startRadius
			center.x, 
			center.y,
			radius
		);

		for (var i = 0; i < this.stops.length; i++)
		{
			var stop = this.stops[i];

			returnValue.addColorStop(stop.position, stop.color.systemColor);
			returnValue.addColorStop(stop.position, stop.color.systemColor);
		}

		return returnValue;
	}
}

function GradientStop(position, color)
{
	this.position = position;
	this.color = color;
}

function Input(name, keyCode)
{
	this.name = name;
	this.keyCode = keyCode;
}
{
	function Input_Instances()
	{
		if (Input.Instances == null)
		{
			Input.Instances = this;
		}

		this.A = new Input("A", 65);
		this.D = new Input("D", 68);
		this.F = new Input("F", 70);
		this.R = new Input("R", 82);
		this.S = new Input("S", 83);
		this.W = new Input("W", 87);

		this.MouseButton = new Input("MouseButton", "MouseButton");

		this._All = 
		[
			this.A,
			this.D,
			this.F,
			this.R,
			this.S,
			this.W,

			this.MouseButton,
		];

		return Input.Instances;
	}

	Input.Instances = new Input_Instances();
}

function InputToActionBinding(input, action)
{
	this.input = input;
	this.action = action;
}

function InputHelper()
{
	this.mousePos = new Coords(0, 0, 0);
	this.keyCodeToBindingLookup = [];
	this.actionsForInputsPressed = [];
}
{
	// instance methods

	InputHelper.prototype.initialize = function(bindings)
	{
		this.bindings = bindings;
		this.actionsForInputsPressed.length = 0;

		this.keyCodeToBindingLookup.length = 0;
		for (var i = 0; i < this.bindings.length; i++)
		{
			var binding = this.bindings[i];
			this.keyCodeToBindingLookup[binding.input.keyCode] = binding;
		}

		document.body.onkeydown = this.handleKeyDownEvent.bind(this);
		document.body.onkeyup = this.handleKeyUpEvent.bind(this);
		document.body.onmousedown = this.handleMouseDownEvent.bind(this);
		document.body.onmouseup = this.handleMouseUpEvent.bind(this);
		document.body.onmousemove = this.handleMouseMoveEvent.bind(this);
	}

	InputHelper.prototype.updateForTimerTick = function()
	{
		for (var i = 0; i < this.actionsForInputsPressed.length; i++)
		{
			var action = this.actionsForInputsPressed[i];
			action.ticksSoFar++;
		}
	}

	// events

	InputHelper.prototype.handleKeyDownEvent = function(event)
	{
		var keyCodePressed = event.keyCode;
		var binding = this.keyCodeToBindingLookup[keyCodePressed];
		if (binding != null)
		{
			var action = binding.action; 
			var actionName = action.name;

			if (this.actionsForInputsPressed[actionName] == null)
			{
				action.ticksSoFar = 0;
				this.actionsForInputsPressed.push(action);
				this.actionsForInputsPressed[actionName] = action;
			}
		}
	}

	InputHelper.prototype.handleKeyUpEvent = function(event)
	{
		var keyCodeReleased = event.keyCode;
		var binding = this.keyCodeToBindingLookup[keyCodeReleased];
		if (binding != null)
		{
			var action = binding.action; 
			var actionName = action.name;

			if (this.actionsForInputsPressed[actionName] != null)
			{
				this.actionsForInputsPressed.splice
				(
					this.actionsForInputsPressed.indexOf(action),
					1
				);	
				delete this.actionsForInputsPressed[actionName];
			}
		}
	}

	InputHelper.prototype.handleMouseDownEvent = function(event)
	{
		var boundingClientRectangle = event.target.getBoundingClientRect();

		this.mousePos.overwriteWithDimensions
		(
			event.x - boundingClientRectangle.left, 
			event.y - boundingClientRectangle.top, 
			0
		);		

		var keyCodePressed = Input.Instances.MouseButton.keyCode;
		var binding = this.keyCodeToBindingLookup[keyCodePressed];
		if (binding != null)
		{
			var action = binding.action; 
			var actionName = action.name;

			if (this.actionsForInputsPressed[actionName] == null)
			{
				action.ticksSoFar = 0;
				this.actionsForInputsPressed.push(action);
				this.actionsForInputsPressed[actionName] = action;
			}
		}
	}

	InputHelper.prototype.handleMouseUpEvent = function(event)
	{
		var boundingClientRectangle = event.target.getBoundingClientRect();

		this.mousePos.overwriteWithDimensions
		(
			event.x - boundingClientRectangle.left, 
			event.y - boundingClientRectangle.top, 
			0
		);		

		var keyCodeReleased = Input.Instances.MouseButton.keyCode;
		var binding = this.keyCodeToBindingLookup[keyCodeReleased];
		if (binding != null)
		{
			var action = binding.action; 
			var actionName = action.name;

			if (this.actionsForInputsPressed[actionName] != null)
			{
				this.actionsForInputsPressed.splice
				(
					this.actionsForInputsPressed.indexOf(action),
					1
				);	
				delete this.actionsForInputsPressed[actionName];
			}
		}
	}

	InputHelper.prototype.handleMouseMoveEvent = function(event)
	{
		var boundingClientRectangle = event.target.getBoundingClientRect();

		this.mousePos.overwriteWithDimensions
		(
			event.x - boundingClientRectangle.left, 
			event.y - boundingClientRectangle.top, 
			0
		);
	}
}

function Link(namesOfBodiesLinked)
{
	this.namesOfBodiesLinked = namesOfBodiesLinked;
}
{
	Link.prototype.bodiesLinked = function(system)
	{
		var returnValue = 
		[
			system.bodies[this.namesOfBodiesLinked[0]],
			system.bodies[this.namesOfBodiesLinked[1]],
		];

		return returnValue;
	}
}

function Location(pos, orientation)
{
	this.pos = pos;
	this.orientation = orientation;
}

function Venue(name, bodyCategoryNamesKnown, bodiesToSpawn)
{
	this.name = name;
	this.bodyCategoryNamesKnown = bodyCategoryNamesKnown;

	this.bodies = [];
	this.bodiesByCategoryName = [];
	this.bodiesToSpawn = bodiesToSpawn;
	this.bodiesToRemove = [];

	this.update_BodiesSpawn();

	this.camera = this.bodiesByCategoryName[Category.Instances.Camera.name][0];

	var cameraDefn = this.camera.defn.cameraDefn;

	var targetBodyForCamera = this.bodies["Sol"];

	this.camera.constrainableData.constraintAdd
	(
		new Constraint_PositionOnCylinder
		(
			targetBodyForCamera.location.pos, // center
			new Orientation
			(
				new Coords(1, 0, 0), 
				new Coords(0, 0, 1) // axis
			),
			0, // yaw
			cameraDefn.focalLength, // radius
			0 - cameraDefn.focalLength / 2 // distanceFromCenterAlongAxisMax
		)
	);

	this.camera.constrainableData.constraintAdd
	(
		new Constraint_LookAtBody
		(
			targetBodyForCamera
		)
	);
}
{
	Venue.prototype.initialize = function()
	{
		var canvas = this.htmlElement;

		var cameraViewSize = this.camera.defn.cameraDefn.viewSize;

		if (canvas == null)
		{
			canvas = document.createElement("canvas");
			canvas.style.border = "1px solid";
			canvas.width = cameraViewSize.x;
			canvas.height = cameraViewSize.y;
			//canvas.onclick = this.handleClick.bind(this);
			//canvas.onmousemove = this.handleMouseMove.bind(this);

			canvas.style.position = "absolute";
			canvas.style.top = "0px";
			canvas.style.left = "0px";

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

			document.body.appendChild(canvas);

			this.htmlElement = canvas;
		}

		this.graphics.font = "16px Arial";
	}

	Venue.prototype.updateForTimerTick = function()
	{
		this.update_BodiesSpawn();

		for (var c = 0; c < this.bodyCategoryNamesKnown.length; c++)
		{
			var bodyCategoryName = this.bodyCategoryNamesKnown[c];
			var bodyCategory = Category.Instances._All[bodyCategoryName];
			if (bodyCategory.updateBodyForVenue != null)
			{
				var bodiesInCategory = this.bodiesByCategoryName[bodyCategoryName];

				for (var b = 0; b < bodiesInCategory.length; b++)
				{
					var body = bodiesInCategory[b];
					bodyCategory.updateBodyForVenue(this, body);
				}
			}
		}

		this.update_BodiesRemove();
	}

	Venue.prototype.update_BodiesRemove = function()
	{
		for (var b = 0; b < this.bodiesToRemove.length; b++)
		{
			var body = this.bodiesToRemove[b];
			this.bodies.push(body);
			this.bodies[body.name] = body;

			var categoryNames = body.defn.categoryNames;
			for (var c = 0; c < categoryNames.length; c++)
			{
				var categoryName = categoryNames[c];
				if (this.bodiesByCategoryName[categoryName] == null)
				{
					this.bodiesByCategoryName[categoryName] = [];
				}

				var bodiesByCategoryName = this.bodiesByCategoryName[categoryName];

				bodiesByCategoryName.splice
				(
					bodiesByCategoryName.indexOf(body),
					1
				);
			}
		}

		this.bodiesToRemove.length = 0;
	}

	Venue.prototype.update_BodiesSpawn = function()
	{
		for (var b = 0; b < this.bodiesToSpawn.length; b++)
		{
			var body = this.bodiesToSpawn[b];
			this.bodies.push(body);
			this.bodies[body.name] = body;

			var categoryNames = body.defn.categoryNames;
			for (var c = 0; c < categoryNames.length; c++)
			{
				var categoryName = categoryNames[c];
				if (this.bodiesByCategoryName[categoryName] == null)
				{
					this.bodiesByCategoryName[categoryName] = [];
				}

				this.bodiesByCategoryName[categoryName].push(body);
			}
		}

		this.bodiesToSpawn.length = 0;
	}
}

function NumberHelper()
{}
{
	NumberHelper.wrapValueToRangeMinMax = function(value, min, max)
	{
		var range = max - min;

		while (value < min)
		{
			value += range;
		}
		while (value > max)
		{
			value -= range;
		}

		return value;
	}
}

function Orientation(forward, down)
{
	this.forward = forward.clone().normalize();
	this.right = down.crossProduct(this.forward).normalize();
	this.down = this.forward.crossProduct(this.right).normalize();
}
{
	Orientation.prototype.overwriteWith = function(other)
	{
		this.forward.overwriteWith(other.forward);
		this.right.overwriteWith(other.right);
		this.down.overwriteWith(other.down);
	}
}

function Plane(normal, distanceFromOrigin)
{
	this.normal = normal;
	this.distanceFromOrigin = distanceFromOrigin;
}

function Polar(azimuth, elevation, radius)
{
	// values in radians

	this.azimuth = azimuth;
	this.elevation = elevation;
	this.radius = radius;
}
{
	// static methods

	Polar.fromCoords = function(coordsToConvert)
	{
		var azimuth = Math.atan2(coordsToConvert.y, coordsToConvert.x);
		if (azimuth < 0)
		{
			azimuth += Constants.Tau;
		}

		var radius = coordsToConvert.magnitude();

		var elevation = Math.asin(coordsToConvert.z / radius);

		var returnValue = new Polar
		(
			azimuth,
			elevation,
			radius
		);

		return returnValue;
	}

	// instance methods

	Polar.random = function()
	{
		return new Polar
		(
			Math.random() * Constants.Tau,
			Math.random() * Constants.Tau,
			Math.random()
		);
	}

	Polar.prototype.toCoords = function()
	{
		var cosineOfElevation = Math.cos(this.elevation);

		var returnValue = new Coords
		(
			Math.cos(this.azimuth) * cosineOfElevation,
			Math.sin(this.azimuth) * cosineOfElevation,
			Math.sin(this.elevation)
		).multiplyScalar(this.radius);

		return returnValue;
	}
}

function Ray(startPos, direction)
{
	this.startPos = startPos;
	this.direction = direction;
}

function Universe(name, categories, bodyDefns, venue)
{
	this.name = name;
	this.categories = categories;
	this.bodyDefns = bodyDefns;
	this.venue = venue;

	ArrayHelper.addLookupsToArray(this.bodyDefns, "name");
}

function VisualGrid()
{}
{
	VisualGrid.prototype.draw = function(graphics, camera, drawPos, body)
	{
		var cameraDefn = camera.defn.cameraDefn;
		var cameraViewSize = cameraDefn.viewSize;
		var cameraPos = camera.location.pos;

		graphics.fillStyle = Color.Instances.Black.systemColor;
		graphics.fillRect(0, 0, cameraViewSize.x, cameraViewSize.y);

		var drawPos = new Coords(0, 0, 0);
		var drawPosFrom = new Coords(0, 0, 0);
		var drawPosTo = new Coords(0, 0, 0);

		var gridCellSizeInPixels = new Coords(10, 10, 0);
		var gridSizeInCells = new Coords(40, 40, 0); 
		var gridSizeInPixels = gridSizeInCells.clone().multiply
		(
			gridCellSizeInPixels
		);
		var gridSizeInCellsHalf = gridSizeInCells.clone().divideScalar(2);
		var gridSizeInPixelsHalf = gridSizeInPixels.clone().divideScalar(2);

		graphics.strokeStyle = Color.Instances.CyanHalfTranslucent.systemColor;

		for (var d = 0; d < 2; d++)
		{
			var multiplier = new Coords(0, 0, 0);
			multiplier.dimension_Set(d, gridCellSizeInPixels.dimension(d));

			for (var i = 0 - gridSizeInCellsHalf.x; i <= gridSizeInCellsHalf.x; i++)			
			{
				drawPosFrom.overwriteWith(gridSizeInPixelsHalf).multiplyScalar(-1);
				drawPosTo.overwriteWith(gridSizeInPixelsHalf);

				drawPosFrom.dimension_Set(d, 0);
				drawPosTo.dimension_Set(d, 0);

				drawPosFrom.add(multiplier.clone().multiplyScalar(i));
				drawPosTo.add(multiplier.clone().multiplyScalar(i));

				cameraDefn.convertWorldCoordsToViewCoords(camera, drawPosFrom);
				cameraDefn.convertWorldCoordsToViewCoords(camera, drawPosTo);

				graphics.beginPath();
				graphics.moveTo(drawPosFrom.x, drawPosFrom.y);
				graphics.lineTo(drawPosTo.x, drawPosTo.y);
				graphics.stroke();
			}
		}

	}
}

function VisualRectangle(size, color)
{
	this.size = size;
	this.color = color;

	this.sizeHalf = this.size.clone().divideScalar(2);
}
{
	VisualRectangle.prototype.draw = function(graphics, camera, drawPos, body)
	{
		graphics.fillStyle = this.color.systemColor;

		graphics.beginPath();
		graphics.fillRect
		(
			drawPos.x - this.sizeHalf.x, 
			drawPos.y - this.sizeHalf.y, 
			this.size.x, 
			this.size.y 
		);		
	}
}

function VisualSphere(radius, color, gradient, offsetFactor)
{
	this.radius = radius;
	this.color = color;
	this.gradient = gradient;
	this.offsetFactor = offsetFactor;

	if (this.gradient == null)
	{
		this.gradient = new Gradient
		([
			new GradientStop(0, this.color),
			new GradientStop(1, Color.Instances.Black),
		]);
	}
}
{
	VisualSphere.prototype.draw = function(graphics, camera, drawPos, body)
	{
		var radiusApparent = 
			this.radius
			* camera.defn.cameraDefn.focalLength 
			/ drawPos.z;

		var systemGradient = this.gradient.toSystemGraphicsStyle
		(
			graphics,
			drawPos,
			radiusApparent
		);
		graphics.fillStyle = systemGradient;

		graphics.beginPath();
		graphics.arc
		(
			drawPos.x, drawPos.y, 
			radiusApparent, 
			0, 2 * Math.PI, // start and stop angles 
			false // counterClockwise
		);
		graphics.fill();
	}
}

// run

new SpaceMovementTest().main();

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

4 Responses to Visualizing a Solar System with a Base Plane and Elevations

  1. super says:

    none of ur code could be understood from ur website very tough and difficult

    • Sorry you feel that way. Like I said, it kind of got overcomplicated on me. Is there anything in particular you’d like me to explain?

      • super says:

        you are a very professional and talented coder where did u learn this stuff pls share

      • Hey, thanks, I appreciate that. As for where I learned it, I am a professional computer programmer, with a four-year degree in computer science and everything. But no one taught me JavaScript, really–my nonstandard coding style started out as just me trying to make JavaScript look like what I was used to, which was Java. I’m sure my stuff drives real JavaScript guys crazy to look at.

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