A Simple Flashcard System in JavaScript

The JavaScript program below, when run, prompts for a lesson file, and, when one is provided, quizzes the user with the questions from it. When the user answers all questions correctly three times in a row, the lesson is counted as complete.

The lesson file should be a text file. Each line in the file represents a question and its correct response, delimited by a semicolon.

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 https://thiscouldbebetter.neocities.org/flashcards.html.

flashcards


<html>
<body>

	<!-- user interface -->
	<div>
		<input id="inputFileLesson" type="file" onchange="inputFileLesson_Changed(this);"
	</div>

	<div>
		<textarea id="textareaPresentation" readonly="readonly"></textarea>
	</div>

	<div>
		<input id="inputResponse"></input>
	</div>

	<p id="pStatusMessage">Upload a valid file to begin.</p>
	
<script type="text/javascript">

// ui event handlers

function inputFileLesson_Changed(inputFileLesson)
{
	var file = inputFileLesson.files[0];

	var fileReader = new FileReader();
	fileReader.onload = inputFileLesson_Changed_FileLoaded;
	fileReader.readAsText(file);
}

function inputFileLesson_Changed_FileLoaded(event)
{
	var fileContents = event.target.result;

	var questionsAsStrings = fileContents.split("\n");

	var questions = [];

	for (var i = 0; i < questionsAsStrings.length; i++)
	{
		var questionAsString = questionsAsStrings[i];

		var presentationAndResponse = questionAsString.split(";");

		if (presentationAndResponse.length == 2)
		{
			var presentation = presentationAndResponse[0];
			var response = presentationAndResponse[1].trim();

			var question = new Question(presentation, response);

			questions.push(question);
		}
	}

	var lessonDefn = new LessonDefn
	(
		questions, 
		3 // timesCorrectPerQuestion
	);

	Globals.Instance.initialize(lessonDefn);
}

// classes

function DisplayHelper()
{
	// do nothing
}
{
	DisplayHelper.prototype.displayLessonRun = function(lessonRun)
	{
		var textareaPresentation = document.getElementById
		(
			"textareaPresentation"
		);

		var questionCurrent = lessonRun.questionCurrent();

		textareaPresentation.value = questionCurrent.presentation;

		var inputResponse = document.getElementById("inputResponse");
		inputResponse.value = "";
		inputResponse.focus();

		var pStatusMessage = document.getElementById("pStatusMessage");
		pStatusMessage.innerHTML = lessonRun.statusMessage;
	}
}

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

	Globals.Instance = new Globals();

	// methods

	Globals.prototype.initialize = function(lessonDefn)
	{
		this.lessonDefn = lessonDefn;

		this.lessonRun = new LessonRun(this.lessonDefn);

		this.displayHelper = new DisplayHelper();
		this.inputHelper = new InputHelper();

		this.lessonRun.initialize();

		this.inputHelper.initialize();

		this.displayHelper.displayLessonRun(this.lessonRun);
	}
}


function InputHelper()
{
	// do nothing
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeydown.bind(this);
	}

	// event handlers

	InputHelper.prototype.handleEventKeydown = function(event)
	{
		if (event.key == "Enter")
		{
			var inputResponse = document.getElementById("inputResponse");
			var responseActual = inputResponse.value.trim();
			
			var lessonRun = Globals.Instance.lessonRun;
			var responseExpected = lessonRun.questionCurrent().responseCorrect;
	
			var responseRecordCurrent = lessonRun.responseRecordCurrent();

			if (responseActual == responseExpected)
			{
				responseRecordCurrent.timesCorrect++;
				lessonRun.statusMessage = 
					"Correct!  This question has been answered correctly "
					+ responseRecordCurrent.timesCorrect
					+ " times in a row.";
			}
			else
			{
				responseRecordCurrent.timesCorrect = 0;
				lessonRun.statusMessage = 
					"Incorrect!  The correct answer was "
					+ responseExpected
					+ ".  You answered "
					+ responseActual
					+ ".";	
			}

			if (lessonRun.isComplete() == true)
			{
				lessonRun.statusMessage = "Lesson complete!";
				document.body.onkeydown = null;
			}
			else
			{
				lessonRun.questionAdvance();
			}

			Globals.Instance.displayHelper.displayLessonRun
			(
				lessonRun
			);
		}
		
	}
}

function LessonDefn(questions, timesCorrectPerQuestion)
{
	this.questions = questions;
	this.timesCorrectPerQuestion = 
		timesCorrectPerQuestion;
}

function LessonRun(defn)
{
	this.defn = defn;
	this.statusMessage = 
		"Each question must be correctly answered " 
		+ this.defn.timesCorrectPerQuestion
		+ " times in a row.";
}
{
	LessonRun.prototype.initialize = function()
	{
		this.questionIndexCurrent = 0;
		this.responseRecords = [];

		var questions = this.defn.questions;

		for (var i = 0; i < questions.length; i++)
		{
			var question = questions[i];
			var responseRecord = new ResponseRecord();
			this.responseRecords.push(responseRecord);
		}
	}

	LessonRun.prototype.isComplete = function()
	{
		var returnValue = true;

		var timesRequired = this.defn.timesCorrectPerQuestion;

		for (var i = 0; i < this.responseRecords.length; i++)
		{
			var responseRecord = this.responseRecords[i];
			if (responseRecord.timesCorrect < timesRequired)
			{
				returnValue = false;
				break;
			}
		}	

		return returnValue;
	}

	LessonRun.prototype.questionAdvance = function()
	{
		var isFirstTime = true;
		var timesRequired = this.defn.timesCorrectPerQuestion;

		while 
		(
			isFirstTime == true 
			|| this.responseRecordCurrent().timesCorrect >= timesRequired
		)
		{
			isFirstTime = false;

			this.questionIndexCurrent++;

			if (this.questionIndexCurrent >= this.defn.questions.length)
			{
				this.questionIndexCurrent = 0;
			}
		}
	}

	LessonRun.prototype.questionCurrent = function()
	{
		return this.defn.questions[this.questionIndexCurrent];
	}

	LessonRun.prototype.responseRecordCurrent = function()
	{
		return this.responseRecords[this.questionIndexCurrent];
	}

}

function Question(presentation, responseCorrect)
{
	this.presentation = presentation;
	this.responseCorrect = responseCorrect;
}

function ResponseRecord()
{
	this.timesCorrect = 0;
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

Accessing a Google API using OAuth2 in JavaScript

The instructions below describe how to create a JavaScript web application that accesses one of Google’s APIs using OAuth2.

1. If you have not already done so, install and activate a web server. If using Windows, the XAMPP package is recommended. Make a note of the location of the root document folder for the web server, perhaps at “C:\xampp\htdocs”.

2. Open a web browser.

3. If you have not already done so, create a Google account and then log in to that account.

4. In the web browser, navigate to the URL “https://console.developers.google.com“.

5. Click the “Create a project” button. In the dialog that appears, enter the value “OAuth2Test” in the Project name box, then click the “Create” button. Wait for the new project to be created.

6. A new dialog box titled “Credentials” will appear. (If it doesn’t, click on the “Credentials” item in the left-hand menu pane.) Click the “Create credentials” button, then select the entry titled “API key” from the list that appears.

7. A new dialog titled “API key created” will appear. Record the value in the “Your API key” box for later use, then click the “Close” button.

8. Click the “Create credentials” button again, and this time select the entry titled “OAuth client ID” from the list that appears. A warning message will appear stating that, “To create an OAuth client ID, you must first set a product name on the consent screen”.

9. Click the “Configure consent screen” button. On the “OAuth consent screen” tab that appears, enter the value “OAuth2Test” in the “Product name shown to users” box, then click the “Save” button.

10. Back on the “Create client ID” screen, click the “Web application” radio button. Enter the value “Oauth2Test” in the Name box that appears, and the value “http://localhost” in both the “Authorized JavaScript origins” and “Authorized redirect URIs” boxes beneath.

11. Click the “Create” button. Record the client ID and client secret that are displayed in the dialog that appears.

12. Enable the Google People API for the newly created project by navigating to the URL “https://console.developers.google.com/apis/api/people.googleapis.com/overview” and clicking the “Enable” button.

13. In the web server’s document folder (as determined in a previous step), create a new text file named “OAuth2.html”, containing the text from the code listing below. Substitute the API key and client ID obtained in the previous step at the end of the program, where indicated. This code is adapted from a sample found at the URL “https://developers.google.com/api-client-library/javascript/samples/samples“, under the heading “Authorizing and Making Authorized Requests”.


<html>
	
<body>

<button id="authorize-button" onclick="buttonAuthorize_Clicked()">Authorize</button>
<button id="signout-button" onclick="buttonSignout_Clicked()">Sign Out</button>

<script src="https://apis.google.com/js/api.js"></script>

<script type="text/javascript">

// ui event handlers

function buttonAuthorize_Clicked(event) 
{
	googleAPIClient.signIn();
}

function buttonSignout_Clicked(event) 
{
	googleAPIClient.signOut();
}

function GoogleAPIClient(googleAPI, apiKey, clientID)
{
	this.googleAPI = googleAPI;
	this.apiKey = apiKey;
	this.clientID = clientID;

	this.initialize();
}
{
	// public methods

	GoogleAPIClient.prototype.getUserGivenName = function() 
	{
		this.googleAPI.client.people.people.get
		(
			{
				resourceName: "people/me"
			}
		).then
		(
 			this.getUserGivenName_Success,
			this.getUserGivenName_Error
		);
	}

	GoogleAPIClient.prototype.getUserGivenName_Success = function(response) 
	{
		var givenName = 
			response.result.names[0].givenName;

		console.log
		(
			"Hello, " + givenName + "!"
		);
	}

	GoogleAPIClient.prototype.getUserGivenName_Error = function(reason) 
	{
		console.log
		(
			"Error: " + reason.result.error.message
		);
	}

	GoogleAPIClient.prototype.signIn = function()
	{
		this.googleAPI.auth2.getAuthInstance().signIn();
	}

	GoogleAPIClient.prototype.signOut = function()
	{
		this.googleAPI.auth2.getAuthInstance().signOut();
	}

	// initialization

	GoogleAPIClient.prototype.initialize = function() 
	{
		// Load the API client and auth2 library

		this.googleAPI.load
		(
			"client:auth2", 
			this.initialize_2.bind(this)
		);
	}

	GoogleAPIClient.prototype.initialize_2 = function() 
	{
		var initializerObject = 			
		{
			apiKey: this.apiKey,
			discoveryDocs: 
			[
				"https://people.googleapis.com"
					+ "/$discovery/rest?version=v1"
			],
			clientId: this.clientID,
			scope: "profile"
		}

		this.googleAPI.client.init
		(
			initializerObject
		).then
		(
			this.initialize_3.bind(this)
		);
	}

	GoogleAPIClient.prototype.initialize_3 = function() 
	{
		var authInstance = this.googleAPI.auth2.getAuthInstance();

		// Listen for sign-in state changes.
		authInstance.isSignedIn.listen
		(
			this.updateSigninStatus.bind(this)
		);

		// Handle the initial sign-in state.
		this.updateSigninStatus
		(
			authInstance.isSignedIn.get()
		);
	}

	// helper methods

	GoogleAPIClient.prototype.updateSigninStatus = function(isSignedIn) 
	{
		if (isSignedIn == true) 
		{
			this.getUserGivenName();
		}
	}

} // end class GoogleAPIClient

// run 

var googleAPIClient = new GoogleAPIClient
(
	gapi,
	"INSERT_API_KEY_HERE",
	"CLIENT_ID_HERE.apps.googleusercontent.com"
);

</script>

</body>

</html>

14. In the web browser, open the debugging pane (perhaps by pressing the F12 key) and make sure the Console sub-pane is visible.

15. In the web browser, navigate to the URL “http://localhost/OAuth2.html“. A page with two buttons, named “Authorize” and “Sign Out” will appear.

16. In another tab of the web browser, sign out of your Google account.

17. Click the “Authorize” button. On the dialog that appears, enter the username and password of a valid Google account (such as the one you just singed out of). If a dialog appears prompting for permissions, click the “Allow” button.

18. Verify that a customized greeting message is displayed in the debugging console pane.

Posted in Uncategorized | Tagged , , , | Leave a comment

A Network Routing Simulation in JavaScript with the Bellman-Ford Algorithm

The JavaScript code below, when run, simulates routing of packets within a simple computer network using the Bellman-Ford routing algorithm. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The simulation will be updated once per second, as packets are routed towards their destination nodes. New packets can be created by entering the appropriate text in the box and clicking the “Do” button. The nodes will share routing tables among themselves every 5 seconds.

networkroutingsimulator



<html>
<body>

<script type="text/javascript">

// main

function main()
{
	var display = new Display
	(
		new Coords(400, 300), // sizeInPixels
		10, // fontHeightInPixels
		"White", // colorBackground
		"Gray" // colorForeground
	);

	var network = new Network
	(
		new Coords(20, 20), // nodeSizeInPixels
		5, // timerTicksPerRouteShare
		// nodes
		[
			new Node("Node0", new Coords(30, 30)),
			new Node("Node1", new Coords(90, 90)),
			new Node("Node2", new Coords(150, 150)),
			new Node("Node3", new Coords(90, 210)),

		],
		// links
		[
			new Link( ["Node0", "Node1"], 8),
			new Link( ["Node1", "Node2"], 8),
			new Link( ["Node2", "Node3"], 8),
		], 
		// packets
		[
			// todo
		]
	);

	Globals.Instance.initialize
	(
		display,
		1, // timerTicksPerSecond
		network
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}

		return this;
	}

	Array.prototype.remove = function(element)
	{
		var indexOfElement = this.indexOf(element);
		if (indexOfElement >= 0)
		{
			this.splice(indexOfElement, 1);
		}
		return this;
	}
}

// classes

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

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		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.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}

}

function Display(sizeInPixels, fontHeightInPixels, colorBackground, colorBorder)
{
	this.sizeInPixels = sizeInPixels;
	this.fontHeightInPixels = fontHeightInPixels;
	this.colorBackground = colorBackground;
	this.colorBorder = colorBorder;
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			Coords.Instances.Zeroes, 
			this.sizeInPixels, 
			this.colorBackground,
			this.colorBorder
		);
	}

	Display.prototype.drawLine = function(posFrom, posTo, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		this.graphics.moveTo(posFrom.x, posFrom.y);
		this.graphics.lineTo(posTo.x, posTo.y);
		this.graphics.stroke();
	}

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

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

	Display.prototype.drawText = function(text, pos, color)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillText
		(
			text, pos.x, pos.y
		);
	}

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

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

		this.graphics.font = 
			this.fontHeightInPixels + "px sans-serif";

		document.body.appendChild(canvas);
	}

}

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

	Globals.Instance = new Globals();
	
	// methods

	Globals.prototype.initialize = function
	(
		display, timerTicksPerSecond, network
	)
	{
		this.display = display;
		this.display.initialize();

		this.network = network;
		this.network.initialize();

		var millisecondsPerTimerTick = 
			1000 / timerTicksPerSecond;

		this.timerTicksSoFar = 0;

		this.handleEventTimerTick();

		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this),
			millisecondsPerTimerTick
		);
	}

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

		this.timerTicksSoFar++;
	}
}

function Link(namesOfNodesLinked, costToTraverse)
{
	this.namesOfNodesLinked = namesOfNodesLinked;
	this.costToTraverse = costToTraverse;
}
{
	Link.prototype.node0 = function()
	{
		return Globals.Instance.network.nodes[this.namesOfNodesLinked[0]];
	}

	Link.prototype.node1 = function()
	{
		return Globals.Instance.network.nodes[this.namesOfNodesLinked[1]];
	}

	Link.prototype.nodes = function()
	{
		return [ this.node0(), this.node1() ];
	}

	// drawable

	Link.prototype.drawToDisplay = function(display)
	{
		var node0Center = this.node0().center(); 
		var node1Center = this.node1().center();

		display.drawLine
		(
			node0Center, node1Center, "Gray"
		);

		var midpoint = node0Center.add
		(
			node1Center
		).divideScalar(2);

		display.drawText
		(
			"" + this.costToTraverse, 
			midpoint,
			"Gray"
		);
	}	
}

function Network(nodeSizeInPixels, timerTicksPerRouteShare, nodes, links, packets)
{
	this.nodeSizeInPixels = nodeSizeInPixels;
	this.timerTicksPerRouteShare = timerTicksPerRouteShare;
	this.nodes = nodes;
	this.links = links;
	this.packets = packets;

	this.nodes.addLookups("name");
	this.packetsToRemove = [];
}
{
	Network.prototype.initialize = function()
	{
		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			var nodesLinked = link.nodes();
			for (var n = 0; n < nodesLinked.length; n++)
			{
				var nodeSource = nodesLinked[n];
				var nodeTarget = nodesLinked[1 - n]; 

				var linkToNeighbor = new Link
				(
					[
						nodeSource.name, 
						nodeTarget.name,
					],
					link.costToTraverse
				);

				nodeSource.linksToNeighbors.push(linkToNeighbor);				
			}
		}

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.initialize();
		}

		this.domElementUpdate();
	}

	Network.prototype.routesShareAmongNodes = function()
	{
		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.routesShareWithNeighbors();
		}
	}

	Network.prototype.updateForTimerTick = function()
	{
		var timerTicksSoFar = Globals.Instance.timerTicksSoFar;
		if 
		(
			timerTicksSoFar != 0 
			&& timerTicksSoFar % this.timerTicksPerRouteShare == 0
		)
		{
			this.routesShareAmongNodes();	
		}

		this.packetsToRemove.length = 0;

		for (var i = 0; i < this.packets.length; i++)
		{
			var packet = this.packets[i];
			if (packet.isDelivered() == true)
			{
				this.packetsToRemove.push(packet);
				packet.nodeCurrent().packetsDelivered.push(packet);
			}
			else
			{
				packet.updateForTimerTick();
			}
		}

		for (var i = 0; i < this.packetsToRemove.length; i++)
		{
			var packet = this.packetsToRemove[i];
			this.packets.remove(packet);
		}		
	}

	// dom

	Network.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divNetwork = document.createElement("div");

			var divControls = document.createElement("div");

			var labelCommand = document.createElement("label");
			labelCommand.innerHTML = "Command:";
			divControls.appendChild(labelCommand);

			var inputCommandText = document.createElement("input");
			inputCommandText.id = "inputCommandText";
			inputCommandText.value = "packet Node0 Node3 data"
			divControls.appendChild(inputCommandText);
			
			var buttonCommandPerform = document.createElement("button");
			buttonCommandPerform.innerHTML = "Do";
			buttonCommandPerform.onclick = this.buttonCommandPerform_Clicked.bind(this);
			divControls.appendChild(buttonCommandPerform);

			var buttonCommandHelp = document.createElement("button");
			buttonCommandHelp.innerHTML = "Help";
			buttonCommandHelp.onclick = this.buttonCommandHelp_Clicked.bind(this);
			divControls.appendChild(buttonCommandHelp);

			divNetwork.appendChild(divControls);

			document.body.appendChild(divNetwork);

			this.domElement = divNetwork;
		}
	}

	Network.prototype.buttonCommandHelp_Clicked = function()
	{
		var message = "Valid command format: 'packet [from] [to] [data]'";
		alert(message);
	}

	Network.prototype.buttonCommandPerform_Clicked = function()
	{
		var inputCommandText = document.getElementById("inputCommandText");
		var commandText = inputCommandText.value;
		var commandArguments = commandText.split(" ");
		var operationName = commandArguments[0];
		
		if (operationName == "packet")
		{
			if (commandArguments.length != 4)
			{
				alert("Wrong number of arguments!");
			}

			var nodeNameSource = commandArguments[1]; 
			var nodeTargetName = commandArguments[2];
			var payload = commandArguments[3];

			var nodeSource = this.nodes[nodeNameSource];
			var nodeTarget = this.nodes[nodeTargetName];

			if (nodeSource == null)
			{
				alert("Invalid source node name: " + nodeNameSource);
			}
			else if (nodeSource == null)
			{
				alert("Invalid target node name: " + nodeNameSource);
			}
			else
			{
				var packet = new Packet
				(
					nodeNameSource, nodeTargetName, payload
				);
				this.packets.push(packet);

			}
		}
		else
		{
			alert("Unrecognized command!");
		}
	}

	// drawable

	Network.prototype.drawToDisplay = function(display)
	{
		display.clear();

		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			link.drawToDisplay(display);
		}

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.drawToDisplay(display);
		}

		for (var i = 0; i < this.packets.length; i++)
		{
			var packet = this.packets[i];
			packet.drawToDisplay(display);
		}

		display.drawText
		(
			"Time:" + Globals.Instance.timerTicksSoFar, 
			new Coords(10, 10), 
			"Gray"
		);

		display.drawText
		(
			"Routes shared every " 
				+ this.timerTicksPerRouteShare 
				+ " ticks.", 
			new Coords(10, 20), 
			"Gray"
		);
	}
}

function Node(name, pos)
{
	this.name = name;
	this.pos = pos;
	
	this.linksToNeighbors = [];
	this.routes = [];
	this.packetsDelivered = [];
}
{
	Node.prototype.center = function()
	{
		return this.size().clone().divideScalar(2).add(this.pos);
	}

	Node.prototype.initialize = function()
	{
		for (var i = 0; i < this.linksToNeighbors.length; i++)
		{
			var link = this.linksToNeighbors[i];

			var neighborName = link.namesOfNodesLinked[1];

			this.linksToNeighbors[neighborName] = link;

			var route = new Route
			(
				neighborName, // nodeTargetName, 
				link.costToTraverse, // totalCostToTarget, 
				neighborName // nodeNextName
			);
			this.routes.push(route);
		}

		this.routes.addLookups("nodeTargetName");
	}

	Node.prototype.routesShareWithNeighbors = function()
	{
		for (var n = 0; n < this.linksToNeighbors.length; n++)
		{
			var link = this.linksToNeighbors[n];
			var nodeNeighbor = link.node1();
			nodeNeighbor.routesUpdateFromNeighbor
			(
				this.name, 
				link.costToTraverse, 
				this.routes
			);
		}
	}

	Node.prototype.routesUpdateFromNeighbor = function
	(
		neighborName, costToNeighbor, routesFromNeighbor
	)
	{
		for (var r = 0; r < routesFromNeighbor.length; r++)
		{
			var routeFromNeighbor = routesFromNeighbor[r];

			var totalCostToTargetThroughNeighbor = 
				costToNeighbor + routeFromNeighbor.totalCostToTarget;

			var nodeTargetName = routeFromNeighbor.nodeTargetName;

			if (nodeTargetName == this.name)
			{
				// do nothing
			}
			else 
			{
				var routeExisting = this.routes[nodeTargetName];
				if (routeExisting == null)
				{
					var routeNew = new Route
					(
						nodeTargetName, // target
						totalCostToTargetThroughNeighbor, 
						neighborName // nodeNextName
					);
	
					this.routes.push(routeNew);
					this.routes[nodeTargetName] = routeNew;
				}
				else if (routeExisting.totalCostToTarget > totalCostToTargetThroughNeighbor)
				{
					routeExisting.totalCostToTarget = totalCostToTargetThroughNeighbor;
					routeExisting.nodeNextName = neighborName;
				}
			}
		}
	}

	Node.prototype.size = function()
	{
		return Globals.Instance.network.nodeSizeInPixels;
	}

	// drawable

	Node.prototype.drawToDisplay = function(display)
	{
		var network = Globals.Instance.network;

		display.drawRectangle
		(
			this.pos, network.nodeSizeInPixels, "White", "Gray"
		);
		display.drawText(this.name, this.pos, "Gray"); 


		var textPos = this.center();
		textPos.y += display.fontHeightInPixels;

		for (var i = 0; i < this.routes.length; i++)
		{
			var route = this.routes[i];
			display.drawText(route.toString(), textPos, "Blue");
			textPos.y += display.fontHeightInPixels;
		}

		for (var i = 0; i < this.packetsDelivered.length; i++)
		{
			var packet = this.packetsDelivered[i];
			display.drawText(packet.toString(), textPos, "DarkGreen");
			textPos.y += display.fontHeightInPixels;
		}

	}
}

function Packet(nodeSourceName, nodeTargetName, payload)
{
	this.nodeSourceName = nodeSourceName;
	this.nodeCurrentName = this.nodeSourceName;
	this.nodeTargetName = nodeTargetName;
	this.payload = payload;

	this.nodeNextName = null;
	this.ticksTowardNodeNext = 0;
}
{
	Packet.prototype.fractionTowardNodeNext = function()
	{
		var returnValue = 
			this.ticksTowardNodeNext 
			/ this.linkCurrent().costToTraverse;

		return returnValue;		
	}

	Packet.prototype.isDelivered = function()
	{
		return (this.nodeCurrentName == this.nodeTargetName);
	}

	Packet.prototype.linkCurrent = function()
	{
		return this.nodeCurrent().linksToNeighbors[this.nodeNextName];
	}

	Packet.prototype.nodeCurrent = function()
	{
		return Globals.Instance.network.nodes[this.nodeCurrentName];
	}

	Packet.prototype.nodeNext = function()
	{
		return Globals.Instance.network.nodes[this.nodeNextName];
	}

	Packet.prototype.updateForTimerTick = function()
	{
		if (this.nodeNextName == null)
		{
			var nodeCurrent = this.nodeCurrent();
			var route = nodeCurrent.routes[this.nodeTargetName];
			if (route == null)
			{
				// Drop the packet?
			}
			else
			{
				this.nodeNextName = route.nodeNextName;
			}
		}
		else
		{
			var linkCurrent = this.linkCurrent();
			if (linkCurrent != null)
			{
				this.ticksTowardNodeNext++;

				if (this.ticksTowardNodeNext < linkCurrent.costToTraverse)
				{
					// todo
				}
				else
				{
					this.nodeCurrentName = this.nodeNextName;	
					this.nodeNextName = null;
					this.ticksTowardNodeNext = 0;
				}
			}

		}
	}

	// drawable

	Packet.prototype.drawToDisplay = function(display, network)
	{
		var pos = this.nodeCurrent().center().clone();

		if (this.nodeNextName != null)
		{
			var fractionTowardNodeNext = 
				this.fractionTowardNodeNext();

			pos.multiplyScalar
			(
				1 - fractionTowardNodeNext
			).add
			(
				this.nodeNext().center().clone().multiplyScalar
				(
					fractionTowardNodeNext
				)
			);
		}


		display.drawText(this.toString(), pos, "Red"); 
	}

	// string

	Packet.prototype.toString = function()
	{
		var returnValue = 
			"[packet"
			+ " from:" + this.nodeSourceName 
			+ " to:" + this.nodeTargetName
			+ " data:" + this.payload 
			+ "]";

		return returnValue;
	}

}

function Route(nodeTargetName, totalCostToTarget, nodeNextName)
{
	this.nodeTargetName = nodeTargetName;
	this.totalCostToTarget = totalCostToTarget;
	this.nodeNextName = nodeNextName;
}
{
	// string

	Route.prototype.toString = function()
	{
		var returnValue = 
			"[route"
			+ " to:" + this.nodeTargetName
			+ " cost:" + this.totalCostToTarget
			+ "]";

		return returnValue;
	}
}

// run 

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , , | 1 Comment

An Entropy Generator in JavaScript

The JavaScript code below, when run, attempts to gather “entropy” (randomness) from the time between the user’s keypresses, and displays the gathered entropy as bits, hexadecimal digits, and Base64 digits. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

I’m not sure how well it works. With rapid typing, long runs of 1’s or 0’s tend to appear, which doesn’t seem very random. I’m not sure if the problem is just the nature of typing, the limitations of the way JavaScript runs, or just my own misunderstanding of how entropy works.

entropy


<html>
<body>

<div id="divMain"></div>

<script type="text/javascript">

// main
	
function main()
{
	var session = new Session();
	Globals.Instance.initialize(session);	
}

// classes

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(session)
	{
		this.session = session;
		this.session.initialize();
	}
}

function Session()
{
	this.entropyBitsGatheredAsString = "";
	this.timeOfPreviousKeyEvent = new Date();
}
{
	// methods

	Session.prototype.entropyGatherFromEventKey = function(event)
	{
		var now = new Date();

		var millisecondsSincePreviousKeyEvent = 
			now.getTime() 
			- this.timeOfPreviousKeyEvent.getTime();

		var entropyBitGathered = 
			millisecondsSincePreviousKeyEvent % 2;

		this.entropyBitsGatheredAsString += "" + entropyBitGathered; 

		this.domElementUpdate();

		this.timeOfPreviousKeyEvent = now;
	}

	Session.prototype.entropyGatheringReset = function()
	{
		this.entropyBitsGatheredAsString = "";	

		this.domElementUpdate();
	}

	Session.prototype.initialize = function()
	{
		this.domElementUpdate();
		document.body.onkeyup = this.entropyGatherFromEventKey.bind(this);
	}

	// dom

	Session.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divSession = document.createElement("div");

			var divBits = document.createElement("div");

			var labelBitsGatheredSoFar = document.createElement("label");
			labelBitsGatheredSoFar.innerHTML = "Bits Gathered So Far:";
			divBits.appendChild(labelBitsGatheredSoFar);
			
			var inputBitsGatheredSoFar = document.createElement("textarea");
			inputBitsGatheredSoFar.cols = 40;
			inputBitsGatheredSoFar.rows = 10;
			inputBitsGatheredSoFar.disabled = true;
			this.inputBitsGatheredSoFar = inputBitsGatheredSoFar;
			divBits.appendChild(inputBitsGatheredSoFar);
		
			divSession.appendChild(divBits);

			var divHexadecimal = document.createElement("div");

			var labelAsHexadecimal = document.createElement("label");
			labelAsHexadecimal.innerHTML = "As Hexadecimal:";
			divHexadecimal.appendChild(labelAsHexadecimal);

			var inputAsHexadecimal = document.createElement("textarea");
			inputAsHexadecimal.cols = 40;
			inputAsHexadecimal.rows = 10;
			inputAsHexadecimal.readOnly = true;
			this.inputAsHexadecimal = inputAsHexadecimal;
			divHexadecimal.appendChild(inputAsHexadecimal);

			divSession.appendChild(divHexadecimal);

			var divBase64 = document.createElement("div");

			var labelAsBase64 = document.createElement("label");
			labelAsBase64.innerHTML = "As Base64:";
			divBase64.appendChild(labelAsBase64);

			var inputAsBase64 = document.createElement("textarea");
			inputAsBase64.cols = 40;
			inputAsBase64.rows = 10;
			inputAsBase64.readOnly = true;
			this.inputAsBase64 = inputAsBase64;
			divBase64.appendChild(inputAsBase64);

			divSession.appendChild(divBase64);

			var divControls = document.createElement("div");

			var buttonReset = document.createElement("button");
			buttonReset.innerHTML = "Reset";
			buttonReset.onclick = this.entropyGatheringReset.bind(this);
			divControls.appendChild(buttonReset);

			divSession.appendChild(divControls);

			var divMain = document.getElementById("divMain");
			divMain.appendChild(divSession);

			this.domElement = divSession;

		}

		this.inputBitsGatheredSoFar.value = this.entropyBitsGatheredAsString;

		var numberOfBits = this.entropyBitsGatheredAsString.length;

		var bitsPerNibble = 4;
		var numberOfNibbles = Math.floor(numberOfBits / bitsPerNibble);
		var entropyGatheredAsHexadecimal = "";

		for (var i = 0; i < numberOfNibbles; i++)
		{
			var nibbleAsBitString = this.entropyBitsGatheredAsString.substr
			(
				i * bitsPerNibble, bitsPerNibble
			);

			var nibbleValue = parseInt(nibbleAsBitString, 2);

			var nibbleValueAsHexadecimal = nibbleValue.toString(16);

			entropyGatheredAsHexadecimal += nibbleValueAsHexadecimal;
		}

		this.inputAsHexadecimal.value = entropyGatheredAsHexadecimal;

		var base64DigitsAll = 
			"0123456789"
			+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
			+ "abcdefghijklmnopqrstuvwxyz"
			+ "+/";

		var bitsPerSextet = 6;
		var numberOfSextets = Math.floor(numberOfBits / bitsPerSextet);
		var entropyGatheredAsBase64 = "";

		for (var i = 0; i < numberOfSextets; i++)
		{
			var sextetAsBitString = this.entropyBitsGatheredAsString.substr
			(
				i * bitsPerSextet, bitsPerSextet
			);

			var sextetValue = parseInt(sextetAsBitString, 2);

			var sextetValueAsBase64 = base64DigitsAll[sextetValue];

			entropyGatheredAsBase64 += sextetValueAsBase64;
		}

		this.inputAsBase64.value = entropyGatheredAsBase64;

		return this.domElement;
	}

}

// run

main();

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

Posted in Uncategorized | Tagged , , , | Leave a comment

A Simple Hex Editor in JavaScript

The JavaScript code below implements a simple hex editor in JavaScript. To see it 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 https://thiscouldbebetter.neocities.org/hexeditor.html.

hexeditor


<html>
<body>

<div id="divMain"></div>

<script type="text/javascript">

// main

function main()
{
	var session = new Session([]);

	Globals.Instance.initialize(session);
}

// extensions

function StringExtensions()
{
	// extension class
}
{
	String.prototype.padLeft = function(lengthToPadTo, charToPadWith)
	{
		var thisPadded = this;

		while (thisPadded.length < lengthToPadTo)
		{
			thisPadded = 
				charToPadWith + thisPadded;
		}

		return thisPadded;
	}
}

// classes

function Converter()
{
	// static class
}
{
	Converter.PrintableCharacters = 
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		+ "abcdefghijklmnopqrstuvwxyz"
		+ "0123456789";
		// todo - Symbols.

	Converter.bytesToStringASCII = function(bytes)
	{
		var returnValue = "";

		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsCharASCII = String.fromCharCode
			(
				byte
			);

			if (Converter.PrintableCharacters.indexOf(byteAsCharASCII) == -1)
			{
				byteAsCharASCII = ".";	
			}

			returnValue += byteAsCharASCII;
		}

		return returnValue;
	}

	Converter.bytesToStringHexadecimal = function(bytes)
	{
		var returnValue = "";

		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsStringHexadecimal = 
				byte.toString(16).padLeft(2, '0');

			returnValue += byteAsStringHexadecimal;
		}

		return returnValue;
	}

	Converter.stringHexadecimalToBytes = function(stringHexadecimal)
	{
		var returnValues = [];

		var nibblesForByteCurrent = [];

		for (i = 0; i < stringHexadecimal.length; i++)
		{
			var charForNibble = stringHexadecimal[i];
			var nibbleAsInt = parseInt(charForNibble, 16);
			if (isNaN(nibbleAsInt) == false)
			{
				nibblesForByteCurrent.push(nibbleAsInt);
				if (nibblesForByteCurrent.length == 2)
				{
					var byte = 
						(nibblesForByteCurrent[0] << 4) 
						+ nibblesForByteCurrent[1];
					returnValues.push(byte);
					nibblesForByteCurrent.length = 0;
				}
			}			
		}

		return returnValues;
	}	
}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();
	
	Globals.prototype.initialize = function(session)
	{
		this.session = session;
		this.session.domElementUpdate();
	}
}

function Session(bytes)
{
	this.bytes = bytes;
	this.finalNibble = "";
}
{
	// dom

	Session.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divSession = document.createElement("div");

			var textareaHexadecimal = document.createElement("textarea");
			textareaHexadecimal.cols = 40;
			textareaHexadecimal.rows = 10;
			textareaHexadecimal.onkeyup = this.textareaHexadecimal_KeyUp.bind(this);
			textareaHexadecimal.oninput = this.textareaHexadecimal_Changed.bind(this);
			this.textareaHexadecimal = textareaHexadecimal;
			divSession.appendChild(textareaHexadecimal);

			var textareaASCII = document.createElement("textarea");
			textareaASCII.cols = 20;
			textareaASCII.rows = 10;
			textareaASCII.disabled = true;
			this.textareaASCII = textareaASCII;
			divSession.appendChild(textareaASCII);

			var divFileOperations = document.createElement("div");
		
			var buttonSave = document.createElement("button");
			buttonSave.innerHTML = "Save";
			buttonSave.onclick = this.buttonSave_Clicked.bind(this);
			divFileOperations.appendChild(buttonSave);

			var inputFileToLoad = document.createElement("input");
			inputFileToLoad.type = "file";
			inputFileToLoad.onchange = this.inputFileToLoad_Changed.bind(this);
			divFileOperations.appendChild(inputFileToLoad);

			divSession.appendChild(divFileOperations);

			var divCursor = document.createElement("div");
			
			var labelCursorPosition = document.createElement("label");
			labelCursorPosition.innerHTML = "Cursor Position:";
			divCursor.appendChild(labelCursorPosition);

			var inputCursorPosition = document.createElement("input");
			inputCursorPosition.disabled = true;
			this.inputCursorPosition = inputCursorPosition;
			divCursor.appendChild(inputCursorPosition);

			divSession.appendChild(divCursor);

			var divMain = document.getElementById("divMain");
			divMain.appendChild(divSession);

			this.domElement = divSession;
		}

		var bytesAsStringHexadecimal = Converter.bytesToStringHexadecimal
		(
			this.bytes
		);
		this.textareaHexadecimal.value = 
			bytesAsStringHexadecimal + this.finalNibble;

		var bytesAsStringASCII = Converter.bytesToStringASCII
		(
			this.bytes
		);
		this.textareaASCII.value = bytesAsStringASCII;

		var cursorPos = this.textareaHexadecimal.selectionStart;
		var cursorPosAsString = 
			"0d" + cursorPos 
			+ "; 0x" + cursorPos.toString(16)
			+ "; 0b" + cursorPos.toString(2);

		this.inputCursorPosition.value = cursorPosAsString;

		return this.domElement;
	}

	// events

	Session.prototype.buttonSave_Clicked = function()
	{
		var dataAsArrayBuffer = new ArrayBuffer(this.bytes.length);
		var dataAsArrayUnsigned = new Uint8Array(dataAsArrayBuffer);
		for (var i = 0; i < this.bytes.length; i++) 
		{
			dataAsArrayUnsigned[i] = this.bytes[i];
		}
		var dataAsBlob = new Blob([dataAsArrayBuffer], {type:'bytes'});


		var link = document.createElement("a");
		link.href = window.URL.createObjectURL(dataAsBlob);
		link.download = "Data.bin";
		link.click();
	}

	Session.prototype.inputFileToLoad_Changed = function(event)
	{
		var inputFileToLoad = event.target;
		var fileToLoad = inputFileToLoad.files[0];
		if (fileToLoad != null)
		{
	  		var fileReader = new FileReader();
			fileReader.onload = this.inputFileToLoad_Changed_Loaded.bind(this); 
	   		fileReader.readAsBinaryString(fileToLoad);
		}
	}

	Session.prototype.inputFileToLoad_Changed_Loaded = function(fileLoadedEvent) 
	{
		var dataAsBinaryString = fileLoadedEvent.target.result;

		this.bytes = [];

		for (var i = 0; i < dataAsBinaryString.length; i++)
		{
			var byte = dataAsBinaryString.charCodeAt(i);
			this.bytes.push(byte);
		}

		this.domElementUpdate();
	}

	Session.prototype.textareaHexadecimal_Changed = function(event)
	{
		var bytesAsStringHexadecimal = event.target.value;
		this.bytes = Converter.stringHexadecimalToBytes
		(
			bytesAsStringHexadecimal
		);

		if (bytesAsStringHexadecimal.length % 2 == 0)
		{
			this.finalNibble = "";
		}
		else
		{
			this.finalNibble = bytesAsStringHexadecimal.substr
			(
				bytesAsStringHexadecimal.length - 1,
				1
			);

			var finalNibbleAsInt = parseInt(this.finalNibble, 16);
			if (isNaN(finalNibbleAsInt) == true)
			{
				this.finalNibble = "";
			}
		}

		this.domElementUpdate();
	}

	Session.prototype.textareaHexadecimal_KeyUp = function(event)
	{
		if (event.key.indexOf("Arrow") == 0)
		{
			this.domElementUpdate();
		}
	}

}

// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

An Pixel Art Editor in JavaScript

The JavaScript below implements a simple image editor for editing very tiny images pixel-by-pixel. To see it 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 thiscouldbebetter.neocities.org/pixelarteditor.html.

pixelarteditor


<html>
<body>

<div id="divMain"></div>

<script type="text/javascript">

// main

function main()
{
	var session = new Session
	(
		new Coords(16, 16), // imageSizeInPixelsActual
		16, // magnificationFactor
		// colors
		[
			"Black",
			"White",
			"Red",
			"Orange",
			"Yellow",
			"Green",
			"Blue",
			"Violet",
			"Brown",
			"Gray",	
		]
	);

	session.initialize();
}

// classes

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

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		return this;
	}

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

	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;
	}

}

function Display(sizeInPixels)
{
	this.sizeInPixels = sizeInPixels;
}
{
	Display.prototype.drawImage = function(image)
	{
		this.graphics.drawImage
		(
			image,
			0, 0
		);
	}

	Display.prototype.drawImageStretched = function(image)
	{
		this.graphics.drawImage
		(
			image,
			0, 0,
			this.sizeInPixels.x, this.sizeInPixels.y
		);
	}

	Display.prototype.drawPixel = function(color, pos)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillRect
		(
			pos.x, pos.y, 1, 1
		);
	}

	Display.prototype.drawRectangle = function(color, pos, size)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillRect
		(
			pos.x, pos.y, size.x, size.y
		);
	}

	Display.prototype.fillWithColor = function(color)
	{
		this.drawRectangle(color, new Coords(0, 0), this.sizeInPixels);
	}

	Display.prototype.initialize = function(domElementParent)
	{
		this.canvas = document.createElement("canvas");
		this.canvas.style = "border:1px solid;" 
		this.canvas.width = this.sizeInPixels.x;
		this.canvas.height = this.sizeInPixels.y;

		domElementParent.appendChild(this.canvas);
	
		this.graphics = this.canvas.getContext("2d");
		this.graphics.imageSmoothingEnabled = false;

		this.fillWithColor("White");
	}
}

function Session(imageSizeInPixelsActual, magnificationFactor, colors)
{
	this.imageSizeInPixelsActual = imageSizeInPixelsActual;
	this.magnificationFactor = magnificationFactor;
	this.colors = colors;
}
{
	// methods

	Session.prototype.initialize = function()
	{
		this.imageSizeInPixelsMagnified = this.imageSizeInPixelsActual.clone().multiplyScalar
		(
			this.magnificationFactor
		);

		this.cellSizeInPixels = new Coords(1, 1).multiplyScalar
		(
			this.magnificationFactor
		);

		var divMain = document.getElementById("divMain");
		divMain.innerHTML = "";

		var divSize = document.createElement("div");

		var labelWidth = document.createElement("label");
		labelWidth.innerHTML = "Width:";
		divSize.appendChild(labelWidth);

		var inputWidth = document.createElement("input");
		inputWidth.type = "number";
		inputWidth.value = this.imageSizeInPixelsActual.x;
		inputWidth.onchange = this.inputWidth_Changed.bind(this);
		divSize.appendChild(inputWidth);	

		var labelHeight = document.createElement("label");
		labelHeight.innerHTML = "Height:";
		divSize.appendChild(labelHeight);

		var inputHeight = document.createElement("input");
		inputHeight.type = "number";
		inputHeight.value = this.imageSizeInPixelsActual.y;
		inputHeight.onchange = this.inputHeight_Changed.bind(this);
		divSize.appendChild(inputHeight);

		divMain.appendChild(divSize);

		var divImages = document.createElement("div");

		this.displayMagnified = new Display(this.imageSizeInPixelsMagnified);
		this.displayMagnified.initialize(divImages);
		this.displayMagnified.canvas.onmousemove = 
			this.canvasMagnified_MouseMoved.bind(this);
		
		this.displayActualSize = new Display(this.imageSizeInPixelsActual);
		this.displayActualSize.initialize(divImages);

		divMain.appendChild(divImages);

		var divFileOperations = document.createElement("div");

		var inputFileToLoad = document.createElement("input");
		inputFileToLoad.type = "file";
		inputFileToLoad.onchange = this.inputFileToLoad_Changed.bind(this);
		divFileOperations.appendChild(inputFileToLoad);

		var buttonSave = document.createElement("button");
		buttonSave.innerHTML = "Save";
		buttonSave.onclick = this.buttonSave_Clicked.bind(this);
		divFileOperations.appendChild(buttonSave);

		divMain.appendChild(divFileOperations);

		var buttonClear = document.createElement("button");
		buttonClear.innerHTML = "Clear";
		buttonClear.onclick = this.buttonClear_Clicked.bind(this);
		divMain.appendChild(buttonClear);

		this.colorSelected = this.colors[0];

		var divColors = document.createElement("div");

		for (var i = 0; i < this.colors.length; i++)
		{
			var color = this.colors[i];
			var buttonColor = document.createElement("button");
			buttonColor.innerHTML = color;
			buttonColor.style.color = color;
			buttonColor.onclick = this.buttonColor_Clicked.bind(this);
			divColors.appendChild(buttonColor);
		}

		divMain.appendChild(divColors);
	}

	// ui events

	Session.prototype.buttonClear_Clicked = function()
	{
		this.displayMagnified.fillWithColor(this.colorSelected);
		this.displayActualSize.fillWithColor(this.colorSelected);
	}

	Session.prototype.buttonColor_Clicked = function(event)
	{
		var buttonColor = event.target;
		this.colorSelected = buttonColor.innerHTML;
	}

	Session.prototype.buttonSave_Clicked = function()
	{
		var canvas = this.displayActualSize.canvas;

		var imageFromCanvasURL = canvas.toDataURL("image/png");

		var imageAsByteString = atob(imageFromCanvasURL.split(',')[1]);
		var imageAsArrayBuffer = new ArrayBuffer(imageAsByteString.length);
		var imageAsArrayUnsigned = new Uint8Array(imageAsArrayBuffer);
		for (var i = 0; i < imageAsByteString.length; i++) 
		{
			imageAsArrayUnsigned[i] = imageAsByteString.charCodeAt(i);
		}
		var imageAsBlob = new Blob([imageAsArrayBuffer], {type:'image/png'});

		var link = document.createElement("a");
		link.href = window.URL.createObjectURL(imageAsBlob);
		link.download = "Image.png";
		link.click();
	}

	Session.prototype.canvasMagnified_MouseMoved = function(event)
	{
		if (event.buttons == 0)
		{
			return;
		}

		var canvas = event.target;
		var canvasBounds = canvas.getBoundingClientRect();

		var clickPosInPixels = new Coords
		(
			event.clientX - canvasBounds.left, 
			event.clientY - canvasBounds.top
		);

		var clickPosInCells = clickPosInPixels.clone().divide
		(
			this.cellSizeInPixels
		).floor();

		var cellPosInPixels = clickPosInCells.clone().multiply
		(
			this.cellSizeInPixels
		);

		var color = this.colorSelected;

		this.displayMagnified.drawRectangle
		(
			color,
			cellPosInPixels,
			this.cellSizeInPixels
		);

		this.displayActualSize.drawPixel
		(
			color,
			clickPosInCells
		);
	}

	Session.prototype.inputFileToLoad_Changed = function(event)
	{
		var inputFileToLoad = event.target;
		var fileToLoad = inputFileToLoad.files[0];
		if (fileToLoad != null)
		{
			if (fileToLoad.type.match("image.*") != null)
			{
		  		var fileReader = new FileReader();
				fileReader.onload = this.inputFileToLoad_Changed_Loaded.bind(this); 
		   		fileReader.readAsDataURL(fileToLoad);
			}
		}
	}

	Session.prototype.inputFileToLoad_Changed_Loaded = function(fileLoadedEvent) 
	{
		var imageLoaded = document.createElement("img");
		imageLoaded.src = fileLoadedEvent.target.result;

		this.imageSizeInPixelsActual.x = imageLoaded.width;
		this.imageSizeInPixelsActual.y = imageLoaded.height;

		this.initialize();

		this.displayActualSize.drawImage(imageLoaded);
		this.displayMagnified.drawImageStretched(imageLoaded);
	}

	Session.prototype.inputHeight_Changed = function(event)
	{
		var inputHeight = event.target;
		this.imageSizeInPixelsActual.y = inputHeight.value;
		this.initialize();
	}

	Session.prototype.inputWidth_Changed = function(event)
	{
		var inputWidth = event.target;
		this.imageSizeInPixelsActual.x = inputWidth.value;
		this.initialize();
	}


}

// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

A Parallelization-Ready Ray Tracer in JavaScript

The JavaScript code below implements a ray tracer in JavaScript. It is based on an earlier version in a previous post, but has been cleaned up and refactored somewhat, with an eye towards eventually making splitting the view to be rendered into several pieces and rendering them in parallel using the “web workers” feature that major browser manufacturers have relatively recently impemented in JavaScript.

However, this is not feasible at the moment because the texturing depends on the GraphicsContext object, which cannot be used in a web worker, since it is part of the DOM. Specifically, my homegrown attempt to reproduce the functionality of the GetImageData() function met with less-than-attractive results. Some proposed solutions to using HTML5 graphics in web workers are currently under consideration by browser makers, and hopefully someday it may be possible to finish the parallelization effort.

It should be noted that the new version of the ray tracer is somewhat slower than the old. The refactored code, while easier to read and understand, lacks some of the performance optimizations of the older version.

RayTracer


<html>
<body>

<script type="text/javascript">

// main

function main()
{
	var imageRTBang = ImageHelper.buildImageFromStrings	
	(
		"RTBang",
		1, // scaleMultiplier
		[
			"RRRRRRRRRRRRRRRR",
			"RRcccccRcccccRcR",
			"RRcRRRcRRRcRRRcR",
			"RRcRRRcRRRcRRRcR",
			"RRcccccRRRcRRRcR",
			"RRcRRcRRRRcRRRRR",
			"RRcRRRcRRRcRRRcR",
			"RRRRRRRRRRRRRRRR",
		]
	);

	var materialRTBang = new Material
	(
		"RTBang", 
		Color.Instances.White, 
		1, // diffuse
		1, // specular
		.2, // shininess
		10, // diffuse
		new Texture
		(
			"RTBang", 
			imageRTBang
		)
	);

	var meshMonolith = MeshHelper.transformMeshVertexPositions
	(
		MeshHelper.buildCubeUnit("Monolith", materialRTBang),
		new TransformMultiple
		([
			new TransformScale(new Coords(40, 10, 90)),
			new TransformTranslate(new Coords(0, 0, -90)),

		])
	);		

	var meshGround = new Mesh
	(
		"Ground",
		// vertices
		[
			new Vertex(new Coords(-1000, -1000, 0)),
			new Vertex(new Coords(1000, -1000, 0)),
			new Vertex(new Coords(1000, 1000, 0)),
			new Vertex(new Coords(-1000, 1000, 0)),
		],
		// faces
		[
			new Face(Material.Instances.Green.name, [3, 2, 1, 0], null, null)
		]
	);

	var imageEyeball = ImageHelper.buildImageFromStrings
	(
		"Eyeball",
		1, // scaleMultiplier
		[
			"k","b","w","w","w","w","w","w","w","w"
		]
	);

	var materialEyeball = new Material
	(
		"Eyeball", 
		Color.Instances.White, 
		1, // diffuse
		1, // specular
		.2, // shininess
		10, // diffuse
		new Texture
		(
			"Eyeball", 
			imageEyeball
		)
	);

	var materials = 
	[
		materialEyeball, 
		materialRTBang, 
		Material.Instances.Green,
	]; 

	var sphereEyeball = new Sphere
	(
		"Eyeball", 
		materialEyeball.name,
		100, // radius
		new Coords(200, 200, -270),
		new Orientation
		(
			new Coords(1, 0, 0),
			new Coords(1, 1, 0) // down = SE
		)
	);
	
	var displaySize = new Coords(320, 240, 960);

	var scene = new Scene
	(
		"Scene0",
		materials,
		Color.Instances.BlueDark, // backgroundColor
		new Lighting
		(
			// lights
			[
				//new LightAmbient(.1),
				new LightPoint(30000, new Coords(-200, -200, -300)),
				new LightPoint(60000, new Coords(200, -200, -300)),
				new LightPoint(30000, new Coords(200, 200, -300)),
			]
		),
		new Camera
		(
			displaySize.clone(),
			200, // focalLength
			new Coords(-150, -300, -60), // pos
			new Orientation
			(
				new Coords(1, 2, 0), // forward
				new Coords(0, 0, 1) // down
			)
		),
		// collidables
		[
			sphereEyeball,
			meshMonolith,
			meshGround,
		]
	);

	Globals.Instance.initialize
	(
		displaySize,
		scene
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}

		return this;
	}
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
	this.minAndMax = [ this.min, this.max ];
	this.size = new Coords();

	this.recalculateDerivedValues();
}
{	
	Bounds.prototype.overlapsWith = function(other)
	{
		var returnValue = false;

		var bounds = [ this, other ];

		for (var b = 0; b < bounds.length; b++)
		{
			var boundsThis = bounds[b];
			var boundsOther = bounds[1 - b];			

			var doAllDimensionsOverlapSoFar = true;

			for (var d = 0; d < Coords.NumberOfDimensions; d++)
			{
				if 
				(
					boundsThis.max.dimension(d) <= boundsOther.min.dimension(d)
					|| boundsThis.min.dimension(d) >= boundsOther.max.dimension(d)
				)
				{
					doAllDimensionsOverlapSoFar = false;
					break;
				}
			}

			if (doAllDimensionsOverlapSoFar == true)
			{
				returnValue = true;
				break;
			}
		}

		return returnValue;
	}

	Bounds.prototype.recalculateDerivedValues = function()
	{
		this.size.overwriteWith(this.max).subtract(this.min);
	}
}

function Camera(viewSize, focalLength, pos, orientation)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;
	this.pos = pos;
	this.orientation = orientation;
}

function Cloneable()
{}
{
	Cloneable.cloneMany = function(cloneablesToClone)
	{
		var returnValues = [];

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

		return returnValues;
	}

	Cloneable.overwriteManyWithOthers = function(cloneablesToOverwrite, cloneablesToOverwriteWith)
	{
		for (var i = 0; i < cloneablesToClone.length; i++)
		{
			cloneablesToOverwrite[i].overwriteWith
			(
				cloneablesToOverwriteWith[i]
			);
		}		
	}
}

function Collision()
{	
	this.pos = new Coords();
	this.distanceToCollision = null;
	this.colliders = [];
}
{
	// instance methods

	Collision.prototype.rayAndFace = function(ray, mesh, face)
	{
		this.rayAndPlane
		(
			ray,
			face.plane
		);

		if (this.colliders["Plane"] != null)
		{
			if (this.isPosWithinFace(mesh, face) == false)
			{
				this.colliders["Face"] = null;
			}
			else
			{
				this.colliders["Face"] = face;
	
				for (var t = 0; t < face.triangles.length; t++)
				{
					var triangle = face.triangles[t];
					if (this.isPosWithinFace(mesh, triangle) == true)
					{
						this.colliders["Triangle"] = triangle;
						break;
					}
				}
			}
		}

		return this;
	}

	Collision.prototype.rayAndPlane = function(ray, plane)
	{
		this.distanceToCollision = 
			(
				plane.distanceFromOrigin 
				- plane.normal.dotProduct(ray.startPos)
			)
			/ plane.normal.dotProduct(ray.direction);

		if (this.distanceToCollision >= 0)
		{
			this.pos.overwriteWith
			(
				ray.direction
			).multiplyScalar
			(
				this.distanceToCollision
			).add
			(
				ray.startPos
			);

			this.colliders["Plane"] = plane;
		}

		return this;
	}

	Collision.prototype.rayAndSphere = function(ray, sphere)
	{
		var rayDirection = ray.direction;
		var displacementFromSphereCenterToCamera = ray.startPos.clone().subtract
		(
			sphere.centerPos
		);
		var sphereRadius = sphere.radius;
		var sphereRadiusSquared = sphereRadius * sphereRadius;

		var a = rayDirection.dotProduct(rayDirection);

		var b = 2 * rayDirection.dotProduct
		(
			displacementFromSphereCenterToCamera
		);

		var c = displacementFromSphereCenterToCamera.dotProduct
		(
			displacementFromSphereCenterToCamera
		) - sphereRadiusSquared;

		var discriminant = (b * b) - 4 * a * c;

		if (discriminant >= 0)
		{
			var rootOfDiscriminant = Math.sqrt(discriminant);

			var distanceToCollision1 = 
				(rootOfDiscriminant - b) 
				/ (2 * a);

			var distanceToCollision2 = 
				(0 - rootOfDiscriminant - b) 
				/ (2 * a);

			if (distanceToCollision1 >= 0)
			{
				if (distanceToCollision2 >= 0 && distanceToCollision2 < distanceToCollision1)
				{
					this.distanceToCollision = distanceToCollision2;
				}
				else
				{
					this.distanceToCollision = distanceToCollision1;
				}
			}
			else
			{
				this.distanceToCollision = distanceToCollision2;				
			}
	
			this.pos.overwriteWith
			(
				ray.direction
			).multiplyScalar
			(
				this.distanceToCollision
			).add
			(
				ray.startPos
			);

			this.colliders["Sphere"] = sphere;
		}

		return this;
	}

	Collision.prototype.isPosWithinFace = function(mesh, face)
	{
		var displacementFromVertex0ToCollision = new Coords();

		var isPosWithinAllEdgesOfFaceSoFar = true;

		var edges = face.edges;

		for (var i = 0; i < edges.length; i++)
		{
			var edge = edges[i];

			displacementFromVertex0ToCollision.overwriteWith
			(
				this.pos
			).subtract
			(
				edge.vertex(mesh, 0).pos
			);

			var edgeTransverse = edge.direction.clone().crossProduct
			(
				face.plane.normal
			);
					
			// hack?
			var epsilon = .01;

			if (displacementFromVertex0ToCollision.dotProduct(edgeTransverse) >= epsilon)
			{
				isPosWithinAllEdgesOfFaceSoFar = false;
				break;
			}	
		}

		return isPosWithinAllEdgesOfFaceSoFar;
	}

}

function Color(name, codeChar, componentsRGBA)
{
	this.name = name;
	this.codeChar = codeChar;
	this.componentsRGBA = componentsRGBA;
}
{
	// constants

	Color.NumberOfComponentsRGBA = 4;
	Color.ComponentMax = 255;

	// instances

	function Color_Instances()
	{
		this.Transparent = new Color("Transparent", ".", [0, 0, 0, 0]);

		this.Black 	= new Color("Black",	"k", [0, 0, 0, 1]);
		this.Blue 	= new Color("Blue", 	"b", [0, 0, 1, 1]);
		this.BlueDark 	= new Color("BlueDark", "B", [0, 0, .5, 1]);
		this.Cyan 	= new Color("Cyan", 	"c", [0, 1, 1, 1]);
		this.Gray 	= new Color("Gray", 	"a", [.5, .5, .5, 1]);
		this.Green 	= new Color("Green", 	"g", [0, 1, 0, 1]);
		this.GreenDark 	= new Color("GreenDark", "G", [0, .5, 0, 1]);
		this.Orange 	= new Color("Orange", 	"o", [1, .5, 0, 1]);
		this.OrangeDark	= new Color("OrangeDark", "O", [.5, .25, 0, 1]);
		this.Red 	= new Color("Red", 	"r", [1, 0, 0, 1]);
		this.RedDark 	= new Color("RedDark", 	"R", [.5, 0, 0, 1]);
		this.Violet 	= new Color("Violet", 	"v", [1, 0, 1, 1]);
		this.VioletDark	= new Color("VioletDark","V", [.5, 0, .5, 1]);
		this.White 	= new Color("White", 	"w", [1, 1, 1, 1]);
		this.Yellow 	= new Color("Yellow", 	"y", [1, 1, 0, 1]);
		this.YellowDark	= new Color("YellowDark", "Y", [.5, .5, 0, 1]);

		this._All = 
		[
			this.Transparent,

			this.Blue,
			this.BlueDark,
			this.Black,
			this.Cyan,
			this.Gray,
			this.Green,
			this.GreenDark,
			this.Orange,
			this.OrangeDark,
			this.Red,
			this.RedDark,
			this.Violet,
			this.VioletDark,
			this.White,
			this.Yellow,
			this.YellowDark,
		];

		this._All.addLookups("codeChar");

	}

	Color.Instances = new Color_Instances();

	// static methods

	Color.blank = function(name)
	{
		return new Color(name, "x", [0, 0, 0, 0]);
	}

	// instance methods

	Color.prototype.clone = function()
	{
		return new Color
		(
			this.name, 
			this.codeChar, 
			[
				this.componentsRGBA[0],
				this.componentsRGBA[1],
				this.componentsRGBA[2],
				this.componentsRGBA[3],
			]
		);
	}

	Color.prototype.components = function(red, green, blue, alpha)
	{
		this.componentsRGBA[0] = red;
		this.componentsRGBA[1] = green;
		this.componentsRGBA[2] = blue;
		this.componentsRGBA[3] = alpha;
	}

	Color.prototype.multiply = function(scalar)
	{
		for (var i = 0; i < 3; i++)
		{
			this.componentsRGBA[i] *= scalar;
		}

		return this;
	}

	Color.prototype.overwriteWith = function(other)
	{
		this.name = other.name;
		this.codeChar = other.codeChar;
		for (var i = 0; i < this.componentsRGBA.length; i++)
		{
			this.componentsRGBA[i] = other.componentsRGBA[i];
		}

		return this;
	}

	Color.prototype.systemColor = function()
	{
		var returnValue = 
			"rgba(" 
			+ Math.round(Color.ComponentMax * this.componentsRGBA[0]) + ", " 
			+ Math.round(Color.ComponentMax * this.componentsRGBA[1]) + ", " 
			+ Math.round(Color.ComponentMax * this.componentsRGBA[2]) + ", "
			+ this.componentsRGBA[3] 
			+ ")";	

		return returnValue;	
	}
}

function Constants()
{}
{
	Constants.DegreesPerCircle = 360;
	Constants.RadiansPerCircle = 2 * Math.PI;
	Constants.RadiansPerRightAngle = Math.PI / 2;
	Constants.RadiansPerDegree = 
		Constants.RadiansPerCircle
		/ Constants.DegreesPerCircle;
}

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

	Coords.NumberOfDimensions = 3;

	// instance methods

	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)
	{
		this.overwriteWithXYZ
		(
			this.y * other.z - this.z * other.y,
			this.z * other.x - this.x * other.z,
			this.x * other.y - this.y * other.x
		);

		return this;
	}

	Coords.prototype.dimensionValues = function()
	{
		return [ this.x, this.y, this.z ];
	}

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

		return this;
	}

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

		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.magnitude = function()
	{
		var returnValue = Math.sqrt
		(
			this.x * this.x
			+ this.y * this.y
			+ this.z * this.z
		);

		return returnValue;
	}

	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.overwriteWithXYZ = 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.toString = function()
	{
		var returnValue = "(" + this.x + "," + this.y + "," + this.z + ")";

		return returnValue;
	}

	Coords.prototype.trimToRange = function(range)
	{
		if (this.x < 0)
		{
			this.x = 0;
		}
		else if (this.x > range.x)
		{
			this.x = range.x;
		}

		if (this.y < 0)
		{
			this.y = 0;
		}
		else if (this.y > range.y)
		{
			this.y = range.y;
		}

		if (this.z < 0)
		{
			this.z = 0;
		}
		else if (this.z > range.z)
		{
			this.z = range.z;
		}

		return this;
	}
}

function Display()
{}
{
	// static variables

	Display.Collisions = [];
	Display.DirectionFromEyeToPixel = new Coords();
	Display.DisplacementFromEyeToPixel = new Coords();
	Display.Material = new Material("DisplayMaterial", Color.blank("MaterialColor"));
	Display.PixelColor = Color.blank("PixelColor");
	Display.SurfaceNormal = new Coords();
	Display.TexelColor = Color.blank("TexelColor");
	Display.TexelUV = new Coords();
	Display.VertexWeightsAtSurfacePos = [];

	// instance methods

	Display.prototype.drawScene = function(scene)
	{
		this.drawScene_Background(scene);

		var boundsForTiles = [];

		var sizeInTiles = new Coords(1, 1);
		var tileSizeInPixels = this.sizeInPixels.clone().divide
		(
			sizeInTiles
		);

		var tilePosInTiles = new Coords();
	 	var tileBounds = new Bounds
		(
			new Coords(),
			new Coords()
		);
		
		for (var y = 0; y < sizeInTiles.y; y++)
		{
			tilePosInTiles.y = y;

			for (var x = 0; x < sizeInTiles.x; x++)
			{
				tilePosInTiles.x = x;

				tileBounds.min.overwriteWith
				(
					tilePosInTiles
				).multiply
				(
					tileSizeInPixels
				);

				tileBounds.max.overwriteWith
				(
					tileBounds.min
				).add
				(
					tileSizeInPixels
				);

				this.drawScene_PixelsGetAndDrawForBounds
				(
					scene, tileBounds
				);
			}
		}
	}

	Display.prototype.drawScene_Background = function(scene)
	{
		this.graphics.fillStyle = scene.backgroundColor.systemColor();
		this.graphics.fillRect
		(
			0, 0, 
			this.sizeInPixels.x,
			this.sizeInPixels.y
		);
	}

	Display.prototype.drawScene_PixelsGetAndDrawForBounds = function(scene, bounds)
	{
		// todo
		// It's currently impossible to use DOM objects,
		// including Canvas and GraphicsContext objects,
		// within a web worker. Hopefully this will 
		// change in the future.

		var returnValues = [];
		
		var pixelPos = new Coords();
		var pixelColor = Display.PixelColor;

		var boundsMin = bounds.min;
		var boundsMax = bounds.max;

		var sceneBackgroundColor = scene.backgroundColor;

		for (var y = boundsMin.y; y < boundsMax.y; y++)
		{
			pixelPos.y = y;

			for (var x = boundsMin.x; x < boundsMax.x; x++)
			{
				pixelPos.x = x;

				var collisionForPixel = this.drawScene_ColorSetFromPixelAtPos
				(
					scene,
					pixelColor,
					pixelPos
				);

				if (collisionForPixel == null)
				{
					pixelColor.overwriteWith(sceneBackgroundColor);
				}

				this.graphics.fillStyle = 
					pixelColor.systemColor();

				this.graphics.fillRect
				(
					pixelPos.x, 
					pixelPos.y, 
					1, 1
				);
			}
		}
	}

	Display.prototype.drawScene_ColorSetFromPixelAtPos = function
	(
		scene,
		surfaceColor,
		pixelPos
	)
	{
		var collisionClosest = this.drawScene_Pixel_FindClosestCollision
		(
			scene,
			pixelPos
		);	

		if (collisionClosest != null)
		{	
			var collidable = collisionClosest.colliders["Collidable"];

			var surfaceNormal = Display.SurfaceNormal;
			var surfaceMaterial = Display.Material;

			collidable.surfaceMaterialColorAndNormalForCollision
			(
				scene,
				collisionClosest,
				surfaceMaterial,
				surfaceColor,
				surfaceNormal
			);				

			var intensityFromLightsAll = 0;

			var lights = scene.lighting.lights;

			for (var i = 0; i < lights.length; i++)
			{
				var light = lights[i];

				var intensity = light.intensityForCollisionMaterialNormalAndCamera
				(
					collisionClosest,
					surfaceMaterial,
					surfaceNormal,
					scene.camera
				);
	
				intensityFromLightsAll += intensity;						
			}
	
			surfaceColor.multiply
			(
				intensityFromLightsAll 
			);
		}	

		return collisionClosest;
	}

	Display.prototype.drawScene_Pixel_FindClosestCollision = function
	(
		scene,
		pixelPos
	)
	{
		var camera = scene.camera;
		var cameraOrientation = camera.orientation;

		var displacementFromEyeToPixel = Display.DisplacementFromEyeToPixel;
		var cameraOrientationTemp = Orientation.Instances.Camera;
		var cameraForward = cameraOrientationTemp.forward;
		var cameraRight = cameraOrientationTemp.right;
		var cameraDown = cameraOrientationTemp.down;

		displacementFromEyeToPixel.overwriteWith
		(
			cameraForward.overwriteWith
			(
				cameraOrientation.forward
			).multiplyScalar
			(
				camera.focalLength
			)
		).add
		(
			cameraRight.overwriteWith
			(
				cameraOrientation.right
			).multiplyScalar
			(
				pixelPos.x - this.sizeInPixelsHalf.x
			)
		).add
		(
			cameraDown.overwriteWith
			(
				cameraOrientation.down
			).multiplyScalar
			(
				pixelPos.y - this.sizeInPixelsHalf.y
			)
		);	

		var directionFromEyeToPixel = Display.DirectionFromEyeToPixel;
		directionFromEyeToPixel.overwriteWith
		(
			displacementFromEyeToPixel
		).normalize();

		var rayFromEyeToPixel = new Ray
		(
			camera.pos,
			directionFromEyeToPixel
		);		

		var collisions = Display.Collisions;
		collisions.length = 0;

		for (var i = 0; i < scene.collidables.length; i++)
		{
			var collidable = scene.collidables[i];

			collidable.addCollisionsWithRayToList
			(
				rayFromEyeToPixel,
				collisions
			);
		}

		var collisionClosest = null;

		if (collisions.length > 0)
		{
			collisionClosest = collisions[0];

			for (var c = 1; c < collisions.length; c++)
			{
				var collision = collisions[c];
				if (collision.distanceToCollision < collisionClosest.distanceToCollision)
				{
					collisionClosest = collision;
				}
			}
		}

		return collisionClosest;
	}

	Display.prototype.initialize = function(sizeInPixels)
	{
		this.sizeInPixels = sizeInPixels;
		this.sizeInPixelsHalf = this.sizeInPixels.clone().divideScalar(2);

		var canvas = document.createElement("canvas");
		canvas.width = this.sizeInPixels.x;
		canvas.height = this.sizeInPixels.y;
		document.body.appendChild(canvas);

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

function Edge(vertexIndices)
{
	this.vertexIndices = vertexIndices;

	this.vertices = null;
	this.displacement = new Coords();
	this.direction = new Coords();
	this.transverse = new Coords();
}
{
	Edge.prototype.recalculateDerivedValues = function(mesh, face)
	{
		if (this.vertices == null)
		{
			this.vertices = [this.vertex(mesh, 0), this.vertex(mesh, 1)];
		}

		this.displacement.overwriteWith
		(
			this.vertices[1].pos
		).subtract
		(
			this.vertices[0].pos
		);

		this.direction.overwriteWith
		(
			this.displacement
		).normalize();

		this.transverse.overwriteWith
		(
			this.direction
		).crossProduct
		(
			face.plane.normal
		);
	}

	Edge.prototype.vertex = function(mesh, vertexIndexIndex)
	{
		var vertexIndex = this.vertexIndices[vertexIndexIndex];
		var vertex = mesh.vertices[vertexIndex];
		return vertex;
	}
}

function Face(materialName, vertexIndices, textureUVsForVertices, normalsForVertices)
{
	this.materialName = materialName;
	this.vertexIndices = vertexIndices;
	this.textureUVsForVertices = textureUVsForVertices;
	this.normalsForVertices = normalsForVertices;
}
{
	// static variables

	Face.DisplacementFromVertexNextToPos = new Coords();
	Face.VertexValueInterpolated = new Coords();
	Face.VertexValueWeighted = new Coords();

	Face.prototype.buildTriangles = function(mesh)
	{
		// instance variables
	
		if (this.vertexIndices.length == 3)
		{
			this.triangles = [ this ];
		}
		else if (this.vertexIndices.length == 4)
		{
			this.triangles = 
			[
				this.buildTriangle(0, 1, 2).recalculateDerivedValues(mesh),
				this.buildTriangle(2, 3, 0).recalculateDerivedValues(mesh),
			];
		}
		else
		{
			var errorMessage = "A Face may only have 3 or 4 vertices.";
			throw errorMessage;
		}
	}

	Face.prototype.buildTriangle = function(vertexIndexIndex0, vertexIndexIndex1, vertexIndexIndex2)
	{
		var vertexIndex0 = this.vertexIndices[vertexIndexIndex0];
		var vertexIndex1 = this.vertexIndices[vertexIndexIndex1];
		var vertexIndex2 = this.vertexIndices[vertexIndexIndex2];
		
		var returnValue = new Face
		(
			this.materialName, 
			[
				vertexIndex0,
				vertexIndex1,
				vertexIndex2,
			],
			(
				this.textureUVsForVertices == null
				? null
				:
				[
					this.textureUVsForVertices[vertexIndexIndex0],
					this.textureUVsForVertices[vertexIndexIndex1],
					this.textureUVsForVertices[vertexIndexIndex2],
				]
			),
			(
				this.normalsForVertices == null 
				? null
				:
				[
					this.normalsForVertices[vertexIndexIndex0],
					this.normalsForVertices[vertexIndexIndex1],
					this.normalsForVertices[vertexIndexIndex2],
				]
			)
		);

		return returnValue;
	}

	Face.prototype.interpolateVertexValuesWeighted = function(vertexValues, weights)
	{
		var valueInterpolated = Face.VertexValueInterpolated.overwriteWith
		(
			vertexValues[0]
		).multiplyScalar
		(
			weights[0]
		)

		var vertexValueWeighted = Face.VertexValueWeighted;

		for (var i = 1; i < vertexValues.length; i++)
		{
			vertexValueWeighted.overwriteWith
			(
				vertexValues[i]
			).multiplyScalar
			(
				weights[i]
			);

			valueInterpolated.add(vertexValueWeighted);
		}		
	
		return valueInterpolated;
	}

	Face.prototype.material = function(scene)
	{
		return scene.materials[this.materialName];
	}

	Face.prototype.normalForVertexWeights = function(vertexWeights)
	{
		if (this.normalsForVertices == null)
		{
			returnValue = this.plane.normal;
		}
		else
		{
			returnValue = this.interpolateVertexValuesForWeights
			(
				this.normalsForVertices,
				vertexWeights
			);
		}
		
		return returnValue;	
	}

	Face.prototype.recalculateDerivedValues = function(mesh)
	{
		if (this.normalsForVertices != null)
		{
			for (var i = 0; i < this.normalsForVertices.length; i++)
			{
				var normalForVertex = normalsForVertices[i];
				normalForVertex.normalize();
			}
		}

		var vertices = this.vertices(mesh);

		if (this.plane == null)
		{
			this.plane = new Plane
			(
				Vertex.positionsForMany(vertices)
			);
		}
		else
		{
			this.plane.recalculateDerivedValues();
		}


		if (this.triangles == null)
		{	
			this.buildTriangles(mesh);
		}
		else
		{
			if (this.triangles.length > 1)
			{
				for (var t = 0; t < this.triangles.length; t++)
				{
					var triangle = this.triangles[t];
					triangle.recalculateDerivedValues(mesh);
				}
			}
		}

		if (this.edges == null)
		{
			this.edges = [];

			for (var i = 0; i < this.vertexIndices.length; i++)
			{
				var iNext = NumberHelper.wrapValueToRange
				(
					i + 1, this.vertexIndices.length
				);

				var vertexIndex = this.vertexIndices[i];
				var vertexIndexNext = this.vertexIndices[iNext];

				var edge = new Edge([vertexIndex, vertexIndexNext]);
				
				this.edges.push(edge);
			}

		}

		for (var i = 0; i < this.edges.length; i++)
		{
			this.edges[i].recalculateDerivedValues(mesh, this);
		}	

		return this;
	}

	Face.prototype.texelColorForVertexWeights = function(texture, vertexWeights)
	{
		var texelUV = this.interpolateVertexValuesWeighted
		(
			this.textureUVsForVertices,
			vertexWeights
		);	

		var texelColor = Display.TexelColor;

		texture.colorSetFromUV(texelColor, texelUV);

		return texelColor;
	}

	Face.prototype.vertexWeightsAtSurfacePosAddToList = function(mesh, surfacePos, weights)
	{
		var vertices = this.vertices(mesh);
		
		var edges = this.edges;

		var areaOfFace = edges[1].displacement.clone().crossProduct
		(
			edges[0].displacement
		).magnitude() / 2;

		var displacementFromVertexNextToPos = Face.DisplacementFromVertexNextToPos;

		for (var i = 0; i < vertices.length; i++)
		{	
			var iNext = NumberHelper.wrapValueToRange(i + 1, vertices.length);

			var vertex = vertices[i];
			var vertexNext = vertices[iNext];
				
			displacementFromVertexNextToPos.overwriteWith
			(
				surfacePos
			).subtract
			(
				vertexNext.pos
			);

			var displacementOfEdgeNext = edges[iNext].displacement;

			var areaOfTriangleFormedByEdgeNextAndPos = displacementOfEdgeNext.clone().crossProduct
			(
				displacementFromVertexNextToPos
			).magnitude() / 2;
								
			var weightOfVertex = 
				areaOfTriangleFormedByEdgeNextAndPos
				/ areaOfFace;
			
			weights[i] = weightOfVertex;
		}

		return weights;
	}

	Face.prototype.vertex = function(mesh, vertexIndexIndex)
	{
		var vertexIndex = this.vertexIndices[vertexIndexIndex];
		var vertex = mesh.vertices[vertexIndex];
		return vertex;
	}

	Face.prototype.vertices = function(mesh)
	{
		var returnValues = [];

		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = mesh.vertices[vertexIndex];
			returnValues.push(vertex);
		}

		return returnValues;
	}

	// cloneable

	Face.prototype.clone = function()
	{
		// todo - Deep clone.
		return new Face
		(
			this.materialName, 
			this.vertexIndices, 
			this.textureUVsForVertices, 
			this.normalsForVertices
		);
	}

	// strings

	Face.prototype.toString = function(mesh)
	{
		var returnValue = this.vertices(mesh).join("->");
		return returnValue;
	}

}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(displaySize, scene)
	{
		this.display = new Display();
		this.display.initialize(displaySize);

		this.scene = scene;

		this.display.drawScene(this.scene);
	}
}

function Image(name, sizeInPixels, imageData)
{
	this.name = name;
	this.sizeInPixels = sizeInPixels;
	this.imageData = imageData;
}
{
	Image.prototype.systemImage = function()
	{
		if (this._systemImage == null)
		{		
			var canvas = document.createElement("canvas");
			canvas.width = this.sizeInPixels.x;
			canvas.height = this.sizeInPixels.y;

			var graphics = canvas.getContext("2d");
			graphics.putImageData(this.imageData, 0, 0);

			var imageFromCanvasURL = canvas.toDataURL("image/png");

			var systemImage = document.createElement("img");
			systemImage.width = canvas.width;
			systemImage.height = canvas.height;
			systemImage.isLoaded = false;
			systemImage.onload = function(event) 
			{ 
				event.target.isLoaded = true; 
			}
			systemImage.src = imageFromCanvasURL;

			this._systemImage = systemImage;

		}		

		return this._systemImage;
		
	}
}

function ImageHelper()
{}
{
	// static methods

	ImageHelper.buildImageFromStrings = function
	(
		name, 
		scaleMultiplier, 
		stringsForPixels
	)
	{
		var sizeInPixels = new Coords
		(
			stringsForPixels[0].length, stringsForPixels.length
		);

		var canvas = document.createElement("canvas");
		canvas.width = sizeInPixels.x * scaleMultiplier;
		canvas.height = sizeInPixels.y * scaleMultiplier;

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

		var pixelPos = new Coords(0, 0);
		var colorForPixel = Color.Instances.Transparent;

		for (var y = 0; y < sizeInPixels.y; y++)
		{
			var stringForPixelRow = stringsForPixels[y];
			pixelPos.y = y * scaleMultiplier;

			for (var x = 0; x < sizeInPixels.x; x++)
			{
				var charForPixel = stringForPixelRow[x];
				pixelPos.x = x * scaleMultiplier;

				colorForPixel = Color.Instances._All[charForPixel];

				graphics.fillStyle = colorForPixel.systemColor();
				graphics.fillRect
				(
					pixelPos.x, pixelPos.y, 
					scaleMultiplier, scaleMultiplier
				);				
			}
		}
		
		var imageData = graphics.getImageData
		(
			0, 0, sizeInPixels.x, sizeInPixels.y
		);

		var returnValue = new Image
		(
			name, sizeInPixels, imageData
		);

		return returnValue;
	}
}

function LightAmbient(intensity)
{
	this.intensity = intensity;
}
{
	LightAmbient.prototype.intensityForCollisionNormalAndCamera = function(collision, normal, camera)
	{
		return this.intensity;
	}
}

function LightDirectional(intensity, orientation)
{
	this.intensity = intensity;
	this.orientation = orientation;
}
{
	LightDirectional.prototype.intensityForCollisionMaterialNormalAndCamera = function
	(
		collision, material, normal, camera
	)
	{
		return 0; // todo
	}
}

function LightPoint(intensity, pos)
{
	this.intensity = intensity;
	this.pos = pos;
}
{
	LightPoint.prototype.intensityForCollisionMaterialNormalAndCamera = function
	(
		collision, material, normal, camera
	)
	{
		var displacementFromObjectToLight = Lighting.Temp;

		displacementFromObjectToLight.overwriteWith
		(
			this.pos
		).subtract
		(
			collision.pos
		);

		var distanceFromLightToObject = displacementFromObjectToLight.magnitude();
		var distanceFromLightToObjectSquared = Math.pow
		(
			distanceFromLightToObject, 2
		);

		var surfaceNormal = Lighting.Temp2.overwriteWith(normal);

		var directionFromObjectToLight = displacementFromObjectToLight.normalize();

		var directionFromObjectToLightDotSurfaceNormal = directionFromObjectToLight.dotProduct
		(
			surfaceNormal
		);

		var returnValue = 0;

		if (directionFromObjectToLightDotSurfaceNormal > 0)
		{
			var diffuseComponent = 
				material.diffuse
				* directionFromObjectToLightDotSurfaceNormal
				* this.intensity
				/ distanceFromLightToObjectSquared;

			var directionOfReflection = 
				surfaceNormal.multiplyScalar
				(
					2 * directionFromObjectToLightDotSurfaceNormal
				).subtract
				(
					directionFromObjectToLight
				);
	
			var directionFromObjectToViewer = Lighting.Temp3.overwriteWith
			(
				camera.pos
			).subtract
			(
				collision.pos
			).normalize();
	
			var specularComponent = 
				material.specular
				* Math.pow
				(
					directionOfReflection.dotProduct(directionFromObjectToViewer),
					material.shininess
				)
				* this.intensity
				/ distanceFromLightToObjectSquared;

			returnValue = diffuseComponent + specularComponent;
		}
	
		return returnValue;
	}
}

function Lighting(lights)
{
	this.lights = lights;
}
{
	Lighting.Temp = new Coords();
	Lighting.Temp2 = new Coords();
	Lighting.Temp3 = new Coords();
}

function Material(name, color, ambient, diffuse, specular, shininess, texture)
{
	this.name = name;
	this.color = color;
	this.ambient = ambient;
	this.diffuse = diffuse;
	this.specular = specular;
	this.shininess = shininess;
	this.texture = texture;
}
{	
	// instances

	function Material_Instances()
	{
		this.Green = new Material("Green", Color.Instances.Green, 1, 1, .2, 0);
		this.White = new Material("White", Color.Instances.White, 1, 1, .2, 0);
	}

	Material.Instances = new Material_Instances();
	
	// methods

	// cloneable

	Material.prototype.clone = function()
	{
		return new Material
		(
			this.name,
			this.color.clone(),
			this.ambient,
			this.diffuse,
			this.specular,
			this.shininess,
			this.texture
		);
	}

	Material.prototype.overwriteWith = function(other)
	{
		this.name = other.name;
		this.color.overwriteWith(other.color);
		this.ambient = other.ambient;
		this.diffuse = other.diffuse;
		this.specular = other.specular;
		this.shininess = other.shininess;
		this.texture = other.texture;
	}
}

function Mesh
(
	name, 
	vertices, 
	faces
)
{
	this.name = name;
	this.vertices = vertices;
	this.faces = faces;
	this.recalculateDerivedValues();
}
{
	// constants

	Mesh.VerticesInATriangle = 3;

	// methods

	Mesh.prototype.clone = function()
	{
		var returnValue = new Mesh
		(
			this.name,
			Cloneable.cloneMany(this.vertices), 
			Cloneable.cloneMany(this.faces)
		);

		return returnValue;
	}

	Mesh.prototype.overwriteWith = function(other)
	{
		Cloneable.overwriteManyWithOthers(this.vertices, other.vertices);
	}

	Mesh.prototype.recalculateDerivedValues = function()
	{
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			face.recalculateDerivedValues(mesh);
		}
	}

	// collidable

	Mesh.prototype.addCollisionsWithRayToList = function(ray, listToAddTo)
	{	
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
	
			if (face.plane.normal.dotProduct(ray.direction) < 0)
			{
				var collision = new Collision().rayAndFace
				(
					ray,
					this, // mesh
					face	
				);

				if (collision.colliders["Face"] != null)
				{
					collision.colliders["Collidable"] = this;
					listToAddTo.push(collision);
				}
			}
		}

		return listToAddTo;
	}

	Mesh.prototype.recalculateDerivedValues = function()
	{
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			face.recalculateDerivedValues(this);
		}		
	}

	Mesh.prototype.surfaceMaterialColorAndNormalForCollision = function
	(
		scene, 
		collisionClosest,
		surfaceMaterial,
		surfaceColor,
		surfaceNormal
	)
	{
		var face = collisionClosest.colliders["Triangle"];
		var surfacePos = collisionClosest.pos;

		var vertexWeightsAtSurfacePos = face.vertexWeightsAtSurfacePosAddToList
		(
			this, // mesh
			surfacePos,
			Display.VertexWeightsAtSurfacePos
		);

		surfaceMaterial.overwriteWith(face.material(scene));

		if (surfaceMaterial.texture == null)
		{
			surfaceColor.overwriteWith
			(
				surfaceMaterial.color
			);
		}
		else
		{
			var texelColor = face.texelColorForVertexWeights
			(
				surfaceMaterial.texture, 
				vertexWeightsAtSurfacePos
			);
		
			if (texelColor != null)
			{			
				surfaceColor.overwriteWith(texelColor);
			}
		}

		surfaceNormal.overwriteWith
		(
			face.normalForVertexWeights
			(
				vertexWeightsAtSurfacePos
			)
		); 
			
		return surfaceColor;
	}
}

function MeshHelper()
{
	// static class
}
{
	MeshHelper.buildCubeUnit = function(name, material)
	{
		var materialName = material.name;

		var returnValue = new Mesh
		(
			name, 
			// vertices
			[ 
				new Vertex(new Coords(-1, -1, -1)), // 0
				new Vertex(new Coords(1, -1, -1)), // 1
				new Vertex(new Coords(1, 1, -1)), // 2
				new Vertex(new Coords(-1, 1, -1)), // 3
		
				new Vertex(new Coords(-1, -1, 1)), // 4
				new Vertex(new Coords(1, -1, 1)), // 5
				new Vertex(new Coords(1, 1, 1)), // 6
				new Vertex(new Coords(-1, 1, 1)), // 7
			],
			// faces
			[
				new Face(materialName, [3, 2, 1, 0], [ new Coords(0, 1), new Coords(1, 1), new Coords(1, 0), new Coords(0, 0) ], null), // top
				new Face(materialName, [4, 5, 6, 7], [ new Coords(0, 0), new Coords(1, 0), new Coords(1, 1), new Coords(0, 1) ], null), // bottom

				new Face(materialName, [0, 1, 5, 4], [ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ], null), // north
				new Face(materialName, [2, 3, 7, 6], [ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ], null), // south
	
				new Face(materialName, [1, 2, 6, 5], [ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ], null), // east 
				new Face(materialName, [4, 7, 3, 0], [ new Coords(0, 1), new Coords(1, 1), new Coords(1, 0), new Coords(0, 0) ], null), // west				
			]
		);

		return returnValue;
	}

	MeshHelper.transformMeshVertexPositions = function(mesh, transform)
	{
		for (var v = 0; v < mesh.vertices.length; v++)
		{
			var vertex = mesh.vertices[v];
			transform.transformCoords(vertex.pos);
		}

		mesh.recalculateDerivedValues();

		return mesh;
	}
}

function NumberHelper()
{
	// static class
}
{
	NumberHelper.wrapValueToRange = function(valueToWrap, range)
	{
		while (valueToWrap < 0)
		{
			valueToWrap += range;
		}

		while (valueToWrap >= range)
		{
			valueToWrap -= range;
		}

		return valueToWrap;
	}
}

function Orientation(forward, down)
{
	this.forward = new Coords();
	this.right = new Coords();
	this.down = new Coords();

	this.overwriteWithForwardDown(forward, down);
}
{
	// instance methods

	Orientation.prototype.clone = function()
	{
		return new Orientation
		(
			this.forward.clone(), 
			this.down.clone()
		);
	}

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

	// instances

	function Orientation_Instances()
	{
		this.Camera = new Orientation
		(
			new Coords(1, 0, 0),
			new Coords(0, 0, 1)
		);

		this.ForwardXDownZ = new Orientation
		(
			new Coords(1, 0, 0),
			new Coords(0, 0, 1)
		);

		this.ForwardZDownX = new Orientation
		(
			new Coords(0, 0, 1),
			new Coords(1, 0, 0)
		);
	}

	Orientation.Instances = new Orientation_Instances();
}

function Plane(positionsOnPlane)
{
	this.positionsOnPlane = positionsOnPlane;
	this.normal = new Coords(0, 0, 0);
	this.recalculateDerivedValues();
}
{
	Plane.prototype.recalculateDerivedValues = function()
	{
		var pos0 = this.positionsOnPlane[0];
		var displacementFromPos0To1 = this.positionsOnPlane[1].clone().subtract(pos0);
		var displacementFromPos0To2 = this.positionsOnPlane[2].clone().subtract(pos0);
		this.normal.overwriteWith
		(
			displacementFromPos0To1
		).crossProduct
		(
			displacementFromPos0To2
		).normalize();

		this.distanceFromOrigin = this.normal.dotProduct(pos0);
	}
}

function Polar(azimuth, elevation, radius)
{
	this.azimuth = azimuth;
	this.elevation = elevation;
	this.radius = radius;
}
{
	Polar.prototype.fromCoords = function(coordsToConvert)
	{
		this.radius = coordsToConvert.magnitude();

		this.azimuth = Math.atan2
		(
			coordsToConvert.y,
			coordsToConvert.x
		) / Constants.RadiansPerCircle;

		if (this.azimuth < 0)
		{
			this.azimuth += 1;
		}

		this.elevation = Math.asin
		(
			coordsToConvert.z / this.radius
		) / Constants.RadiansPerRightAngle;
	
		return this;
	}

	Polar.prototype.toCoords = function(coordsToOverwrite)
	{
		var azimuthInRadians = this.azimuth * Constants.RadiansPerCircle;
		var elevationInRadians = this.elevation * Constants.RadiansPerRightAngle;
		var cosineOfElevation = Math.cos(elevationInRadians);

		coordsToOverwrite.overwriteWithXYZ
		(
			cosineOfElevation * Math.cos(this.azimuth),
			cosineOfElevation * Math.sin(this.azimuth),
			Math.sin(elevationInRadians)
		).multiplyScalar
		(
			this.radius
		);
	}
}

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

function Scene(name, materials, backgroundColor, lighting, camera, collidables)
{
	this.name = name;
	this.materials = materials;
	this.backgroundColor = backgroundColor;
	this.lighting = lighting;
	this.camera = camera;
	this.collidables = collidables;

	this.materials.addLookups("name");
}

function Sphere(name, materialName, radius, centerPos, orientation)
{
	this.name = name;
	this.materialName = materialName;
	this.radius = radius;
	this.centerPos = centerPos;
	this.orientation = orientation;
}
{
	// collidable

	Sphere.prototype.addCollisionsWithRayToList = function(ray, listToAddTo)
	{	
		var collision = new Collision().rayAndSphere
		(
			ray,
			this
		);

		if (collision.colliders["Sphere"] != null)
		{
			collision.colliders["Collidable"] = this;
			listToAddTo.push(collision);
		}

		return listToAddTo;
	}

	Sphere.prototype.material = function(scene)
	{
		return scene.materials[this.materialName];
	}

	Sphere.prototype.surfaceMaterialColorAndNormalForCollision = function
	(
		scene, 
		collisionClosest,
		surfaceMaterial,
		surfaceColor,
		surfaceNormal
	)
	{
		var sphere = collisionClosest.colliders["Sphere"];
		var surfacePos = collisionClosest.pos;
		surfaceMaterial.overwriteWith(sphere.material(scene));

		surfaceNormal.overwriteWith
		(
			surfacePos
		).subtract
		(
			sphere.centerPos
		).normalize();

		if (surfaceMaterial.texture == null)
		{
			surfaceColor.overwriteWith
			(
				surfaceMaterial.color
			);
		}
		else
		{
			var surfaceNormalInLocalCoords = new TransformOrient
			(
				this.orientation
			).transformCoords
			(
				surfaceNormal.clone()
			);

			var surfaceNormalInLocalCoordsAsPolar = new Polar().fromCoords
			(
				surfaceNormalInLocalCoords
			);

			var texelUV = Display.TexelUV;
			texelUV.overwriteWithXYZ
			(
				surfaceNormalInLocalCoordsAsPolar.azimuth,
				(1 + surfaceNormalInLocalCoordsAsPolar.elevation) / 2
			); // todo

			surfaceMaterial.texture.colorSetFromUV
			(
				surfaceColor,
				texelUV
			);
		}
			
		return surfaceColor;
	}
}

function Texture(name, image)
{
	this.name = name;
	this.image = image;
	
	var canvas = document.createElement("canvas");
	this.graphics = canvas.getContext("2d");
	this.graphics.drawImage(this.image.systemImage(), 0, 0);
}
{
	Texture.prototype.colorSetFromUV = function(texelColor, texelUV)
	{
		var imageSizeInPixels = this.image.sizeInPixels;

		var texelColorComponents = this.graphics.getImageData
		(
			texelUV.x * imageSizeInPixels.x, 
			texelUV.y * imageSizeInPixels.y,
			1, 1
		).data;
	
		texelColor.components
		(
			texelColorComponents[0] / Color.ComponentMax, 
			texelColorComponents[1] / Color.ComponentMax, 
			texelColorComponents[2] / Color.ComponentMax, 
			1 // alpha
		);
	}
}

function TransformMultiple(children)
{
	this.children = children;
}
{
	TransformMultiple.prototype.transformCoords = function(coordsToTransform)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			this.children[i].transformCoords(coordsToTransform);
		}

		return coordsToTransform;
	}
}

function TransformOrient(orientation)
{
	this.orientation = orientation;
}
{
	TransformOrient.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.overwriteWithXYZ
		(
			this.orientation.forward.dotProduct(coordsToTransform),
			this.orientation.right.dotProduct(coordsToTransform),
			this.orientation.down.dotProduct(coordsToTransform)
		);

		return coordsToTransform;
	}
}

function TransformScale(scaleFactors)
{
	this.scaleFactors = scaleFactors;
}
{
	TransformScale.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.multiply(this.scaleFactors);

		return coordsToTransform;
	}

}

function TransformTranslate(offset)
{
	this.offset = offset;
}
{
	TransformTranslate.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.add(this.offset);

		return coordsToTransform;
	}
}

function Vertex(pos)
{
	this.pos = pos;
}
{
	Vertex.prototype.clone = function()
	{
		return new Vertex(this.pos.clone());
	}

	Vertex.positionsForMany = function(vertices)
	{
		var returnValues = [];

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

		return returnValues;
	}

	// strings

	Vertex.prototype.toString = function()
	{
		return this.pos.toString();
	}
}


// run

main();

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

Posted in Uncategorized | Tagged , , , , | Leave a comment