A Multiple Text Document Editor in JavaScript

Below is a multiple text document editor implemented in JavaScript. 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/multipletextdocumenteditor.html.


<html>

<style>

.bordered {
	border: 1px solid;
}

.number {
	width: 64px;
}
 
</style>
<body>

<! -- ui -->

<div>
	<div id="divProjectFile" class="bordered">
		<label>Project:</label>
		<div>
			<label>Name:</label>
			<input id="inputProjectName" onchange="inputProjectName_Changed(this);"></input>
		</div>
		<button onclick="buttonProjectSave_Clicked();">Save</button>
		<button onclick="buttonProjectLoad_Clicked();">Load:</button>
		<input id="inputFileProjectToLoad" type="file"></input>
		<button onclick="buttonProjectNew_Clicked();">New</button>
	</div>

	<div id="divDocumentList" class="bordered">
		<label>Documents:</label>
		<div>
			<button onclick="buttonDocumentNew_Clicked();">New</button>		
			<button onclick="buttonDocumentExistingAdd_Clicked();">Add Existing:</button>
			<input id="inputFileDocumentExistingToAdd" type="file"></input>
			<button onclick="buttonDocumentSelectedRemove_Clicked();">Remove Selected</button>
		</div>
		<select id="selectDocumentsInProject" style="width:100%" size="10" onchange="selectDocumentsInProject_Changed(this);"></select>
	</div>

	<div id="divDocumentSelected" class="bordered">
		<div><label>Document Selected:</label></div>
		<div>
			<label>Name:</label>
			<input id="inputDocumentSelectedName" onchange="inputDocumentSelectedName_Changed(this);"></input>
			<button onclick="buttonDocumentSelectedSave_Clicked();">Save</button>
			<label>Cursor:</label>
			<label>Row:</label>
			<input id="inputCursorRow" class="number" type="number" onchange="inputCursorColumnOrRow_Changed(this);"></input>
			<label>Column:</label>
			<input id="inputCursorColumn" class="number" type="number" onchange="inputCursorColumnOrRow_Changed(this);"></input>

		</div>
		<div><label>Contents:</label></div>
		<div><textarea id="textareaDocumentSelectedContents" style="width:100%" rows="20" onchange="textareaDocumentSelectedContents_Changed(this);" onkeydown="textareaDocumentSelectedContents_CursorMoved(this);" onmousedown="textareaDocumentSelectedContents_CursorMoved(this);"></textarea></div>
	</div>

	<div id="divSearch" class="bordered">
		<div>
			<button onclick="buttonSearch_Clicked();">Search for:</button>
			<input id="inputTextToSearchFor"></input>
			<input id="checkboxSearchMatchCase" type="checkbox">Match Case</input>
		</div>
		<div>
			<div><label>Results:</label></div>
			<select id="selectSearchResults" size="8" style="width:100%" onchange="selectSearchResults_Changed(this);"></select>
		</div>
		
	</div>

	</div>

</div>

<!-- ui ends -->

<script type="text/javascript">

// ui events

function buttonDocumentExistingAdd_Clicked()
{
	var project = Globals.Instance.session.project;

	var inputFileDocumentExistingToAdd = document.getElementById
	(
		"inputFileDocumentExistingToAdd"
	);
	var fileToLoad = inputFileDocumentExistingToAdd.files[0];
	if (fileToLoad == null)
	{
		alert("Please choose a file with the accompanying 'Choose File' button first.");
	}
	else
	{
		var documentAdded = FileHelper.loadFileAsText
		(
			fileToLoad,
			buttonDocumentExistingAdd_Clicked_FileLoaded // callback
		);
	}
}

function buttonDocumentExistingAdd_Clicked_FileLoaded(fileLoadedName, fileLoadedContents)
{
	var project = Globals.Instance.session.project;

	var documentToAdd = new Document
	(
		fileLoadedName,
		fileLoadedContents
	);
	project.documentAdd(documentToAdd);

	project.domUpdate();
}

function buttonDocumentNew_Clicked()
{
	var project = Globals.Instance.session.project;

	project.documentNew();

	project.domUpdate();
}

function buttonDocumentSelectedRemove_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		project.documentRemove(documentSelected);
	}

	project.domUpdate();
}

function buttonDocumentSelectedSave_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		FileHelper.saveTextAsFile
		(
			documentSelected.contents, documentSelected.name
		);
	}

	project.domUpdate();
}

function buttonProjectLoad_Clicked()
{
	var project = Globals.Instance.session.project;

	var inputFileProjectToLoad = document.getElementById
	(
		"inputFileProjectToLoad"
	);
	var fileToLoad = inputFileProjectToLoad.files[0];
	if (fileToLoad == null)
	{
		alert("Please choose a file with the accompanying 'Choose File' button first.");
	}
	else
	{
		FileHelper.loadFileAsBytes
		(
			fileToLoad,
			buttonProjectLoad_Clicked_FileLoaded // callback
		);
	}
}

function buttonProjectLoad_Clicked_FileLoaded(fileLoadedName, fileLoadedContentsAsBytes)
{
	var projectAsTar = TarFile.fromBytes(fileLoadedName, fileLoadedContentsAsBytes);
	var documentsAsTarEntries = projectAsTar.entries;
	var documents = [];

	for (var i = 0; i < documentsAsTarEntries.length; i++)
	{
		var documentAsTarEntry = documentsAsTarEntries[i];
		var documentName = documentAsTarEntry.header.fileName;
		var documentContentsAsBytes = documentAsTarEntry.dataAsBytes;
		var documentContentsAsString = ByteHelper.bytesToStringUTF8(documentContentsAsBytes);
		var document = new Document(documentName, documentContentsAsString);
		documents.push(document);
	}

	var projectLoaded = new Project
	(
		fileLoadedName,
		documents
	);

	Globals.Instance.session.project = projectLoaded;

	projectLoaded.domUpdate();
}

function buttonProjectNew_Clicked()
{
	var projectNew = new Project
	(
		"Untitled.tar", []
	);

	Globals.Instance.session.project = projectNew;

	projectNew.domUpdate();

}


function buttonProjectSave_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var projectAsTarFile = project.toTarFile();
	var projectAsBytes = projectAsTarFile.toBytes();
	FileHelper.saveBytesAsFile(projectAsBytes, project.name);
}

function buttonSearch_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var inputTextToSearchFor = document.getElementById
	(
		"inputTextToSearchFor"
	);
	var textToSearchFor = inputTextToSearchFor.value;

	var checkboxSearchMatchCase = document.getElementById
	(
		"checkboxSearchMatchCase"
	);
	var matchCase = checkboxSearchMatchCase.checked;

	project.searchForText(textToSearchFor, matchCase);

	project.domUpdate();
}

function inputDocumentSelectedName_Changed(inputDocumentSelectedName)
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var valueFromDOM = inputDocumentSelectedName.value;
		documentSelected.name = valueFromDOM;
	}

	//project.domUpdate();	
}

function inputCursorColumnOrRow_Changed()
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var inputCursorColumn = document.getElementById("inputCursorColumn");
		var inputCursorRow = document.getElementById("inputCursorRow");

		documentSelected.cursorPos.x = Math.floor(inputCursorColumn.value);
		documentSelected.cursorPos.y = Math.floor(inputCursorRow.value);

		project.domUpdate_Cursor_Place();
	}
}

function inputProjectName_Changed(inputProjectName)
{
	var project = Globals.Instance.session.project;

	var valueFromDOM = inputProjectName.value;
	documentSelected.name = valueFromDOM;

	//project.domUpdate();	
}

function selectDocumentsInProject_Changed(selectDocumentsInProject)
{
	var project = Globals.Instance.session.project;

	var documentIndex = selectDocumentsInProject.selectedIndex;
	project.documentIndexSelected = documentIndex;

	project.domUpdate();
}

function selectSearchResults_Changed(selectSearchResults)
{
	var project = Globals.Instance.session.project;

	var searchResultSelectedIndex = selectSearchResults.selectedIndex;
	var searchResultSelected = project.searchResults[searchResultSelectedIndex];
	if (searchResultSelected != null)
	{
		var documentName = searchResultSelected.documentName;
		var documentToSelect = project.documents[documentName];
		var documentIndex = project.documents.indexOf(documentToSelect);
		project.documentIndexSelected = documentIndex;
		documentToSelect.cursorPos.overwriteWith(searchResultSelected.posInDocument);
		project.domUpdate_Cursor_Place();
	}	


}

function textareaDocumentSelectedContents_Changed(textareaDocumentSelectedContents)
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var valueFromDOM = textareaDocumentSelectedContents.value;
		documentSelected.contents = valueFromDOM;
	}

	//project.domUpdate();
}

function textareaDocumentSelectedContents_CursorMoved(textareaDocumentSelectedContents)
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var text = textareaDocumentSelectedContents.value;
		var cursorOffsetInChars = textareaDocumentSelectedContents.selectionEnd;

		var cursorPosNew = Document.stringAndCharOffsetToCursorPos
		(
			text,
			cursorOffsetInChars
		);

		documentSelected.cursorPos.overwriteWith(cursorPosNew);

		project.domUpdate_Cursor();
	}
}

// main

function main()
{
	var welcomeDocumentContents = "";

	for (var i = 0; i < 32; i++)
	{
		welcomeDocumentContents += "Welcome to the multiple document editor!\n";
	}

	var projectDemo = new Project
	(
		"Welcome.tar",
		[
			new Document
			(
				"Welcome.txt", 
				welcomeDocumentContents
			),
			new Document
			(
				"Welcome2.txt", 
				welcomeDocumentContents
			)

		]
	);
	Globals.Instance.initialize(projectDemo);	
}

// 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(elementToRemove)
	{
		this.splice(this.indexOf(elementToRemove), 1);
	}
}

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

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

		return returnValue;
	}


	String.prototype.padRight = function(lengthToPadTo, charToPadWith)
	{
		var returnValue = this;

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

		return returnValue;
	}
}

// classes

function ByteHelper()
{
	// static class
}
{
	ByteHelper.stringUTF8ToBytes = function(stringToConvert)
	{
		var bytes = [];

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

		return bytes;	
	}

	ByteHelper.bytesToStringUTF8 = function(bytesToConvert)
	{
		var returnValue = "";

		for (var i = 0; i < bytesToConvert.length; i++)
		{
			var byte = bytesToConvert[i];
			var byteAsChar = String.fromCharCode(byte);
			returnValue += byteAsChar
		}

		return returnValue;
	}
}

function ByteStream(bytes)
{
	this.bytes = bytes;  

	this.byteIndexCurrent = 0;
}
{
	// constants

	ByteStream.BitsPerByte = 8;
	ByteStream.BitsPerByteTimesTwo = ByteStream.BitsPerByte * 2;
	ByteStream.BitsPerByteTimesThree = ByteStream.BitsPerByte * 3;

	// instance methods

	ByteStream.prototype.hasMoreBytes = function()
	{
		return (this.byteIndexCurrent < this.bytes.length);
	}
	
	ByteStream.prototype.readBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.readByte();
		}

		return returnValue;
	}

	ByteStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteIndexCurrent];

		this.byteIndexCurrent++;

		return returnValue;
	}

	ByteStream.prototype.readString = function(lengthOfString)
	{
		var returnValue = "";

		for (var i = 0; i < lengthOfString; i++)
		{
			var byte = this.readByte();

			if (byte != 0)
			{
				var byteAsChar = String.fromCharCode(byte);
				returnValue += byteAsChar;
			}
		}

		return returnValue;
	}

	ByteStream.prototype.writeBytes = function(bytesToWrite)
	{
		for (var b = 0; b < bytesToWrite.length; b++)
		{
			this.bytes.push(bytesToWrite[b]);
		}

		this.byteIndexCurrent = this.bytes.length;
	}

	ByteStream.prototype.writeByte = function(byteToWrite)
	{
		this.bytes.push(byteToWrite);

		this.byteIndexCurrent++;
	}

	ByteStream.prototype.writeString = function(stringToWrite, lengthPadded)
	{	
		for (var i = 0; i < stringToWrite.length; i++)
		{
			var charAsByte = stringToWrite.charCodeAt(i);
			this.writeByte(charAsByte);
		}
		
		var numberOfPaddingChars = lengthPadded - stringToWrite.length;
		for (var i = 0; i < numberOfPaddingChars; i++)
		{
			this.writeByte(0);
		}
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}

	Coords.prototype.toString = function()
	{
		return this.y + ":" + this.x;
	}
}

function Document(name, contents)
{
	this.name = name;
	this.contents = contents;

	this.cursorPos = new Coords(0, 0);
}
{
	// static methods

	Document.stringAndCharOffsetToCursorPos = function(text, cursorOffsetInChars)
	{
		var newline = "\n";
		var newlinesSoFar = 0;
		var offsetCurrent = null;

		while (true)
		{
			var offsetOfNewline = text.indexOf(newline, offsetCurrent);
			if (offsetOfNewline == -1 || offsetOfNewline >= cursorOffsetInChars)
			{
				break;
			}
			offsetCurrent = offsetOfNewline + 1;
			newlinesSoFar++;
		}

		var returnValue = new Coords
		(
			cursorOffsetInChars - offsetCurrent,
			newlinesSoFar + 1
		);

		return returnValue;
	}

	Document.stringAndCursorPosToCharOffset = function(text, cursorPos)
	{
		var newline = "\n";
		var newlinesSoFar = 0;
		var offsetCurrent = null;
	
		while (offsetCurrent < text.length && newlinesSoFar < cursorPos.y - 1)
		{
			var offsetOfNewline = text.indexOf(newline, offsetCurrent);
			offsetCurrent = offsetOfNewline + 1;
			newlinesSoFar++;
		}

		var returnValue = offsetCurrent + cursorPos.x;

		return returnValue;
	}

}

function FileHelper()
{
	// static class
}
{
    	FileHelper.loadFileAsBytes = function(fileToLoad, callback)
	{   
		var fileReader = new FileReader();
		fileReader.onload = function(fileLoadedEvent)
		{
			var fileLoadedAsBinaryString = 
				fileLoadedEvent.target.result;
			var fileLoadedAsBytes = 
				ByteHelper.stringUTF8ToBytes(fileLoadedAsBinaryString);
			callback(fileToLoad.name, fileLoadedAsBytes);
		}
 
		fileReader.readAsBinaryString(fileToLoad);
	}

	FileHelper.loadFileAsText = function(fileToLoad, callback)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(fileLoadedEvent) 
		{
			var textFromFileLoaded = fileLoadedEvent.target.result;
			callback(fileToLoad.name, textFromFileLoaded);
		};
		fileReader.readAsText(fileToLoad);
	}
 
	FileHelper.saveBytesAsFile = function(bytesToWrite, fileNameToSaveAs)
	{
		var bytesToWriteAsArrayBuffer = new ArrayBuffer(bytesToWrite.length);
		var bytesToWriteAsUIntArray = new Uint8Array(bytesToWriteAsArrayBuffer);
		for (var i = 0; i < bytesToWrite.length; i++) 
		{
			bytesToWriteAsUIntArray[i] = bytesToWrite[i];
		}
 
		var bytesToWriteAsBlob = new Blob
		(
			[ bytesToWriteAsArrayBuffer ], 
			{ type:"application/type" }
		);
 
		var downloadLink = document.createElement("a");
		downloadLink.download = fileNameToSaveAs;
		downloadLink.href = window.URL.createObjectURL(bytesToWriteAsBlob);
		downloadLink.click();
	}

	FileHelper.saveTextAsFile = function(textToSave, fileNameToSaveAs)
	{
		var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"});
		var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob);

		var downloadLink = document.createElement("a");
		downloadLink.download = fileNameToSaveAs;
		downloadLink.href = textToSaveAsURL;
		downloadLink.click();
	}
}

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

	Globals.prototype.initialize = function(project)
	{
		this.session = new Session(project);
		this.domUpdate();
	}

	// dom 
	
	Globals.prototype.domUpdate = function()
	{
		this.session.domUpdate();	
	}
}

function Project(name, documents)
{
	this.name = name;
	this.documents = documents.addLookups("name");

	if (this.documents.length > 0)
	{
		this.documentIndexSelected = 0;
	}

	this.searchResults = [];
}
{
	Project.prototype.documentAdd = function(documentToAdd)
	{
		this.documents.push(documentToAdd);
		this.documents[documentToAdd.name] = documentToAdd;
		this.documentIndexSelected = this.documents.length - 1;
	}

	Project.prototype.documentNew = function()
	{
		var documentNew = new Document("Untitled.txt", "");
		this.documentAdd(documentNew);
	}

	Project.prototype.documentRemove = function(documentToRemove)
	{
		this.documents.remove(documentToRemove);
		delete this.documents[documentToRemove.name];
		if (this.documents.length == 0)
		{
			this.documentIndexSelected = null;
		}
	}

	Project.prototype.documentSelected = function()
	{
		return (this.documentIndexSelected == null ? null : this.documents[this.documentIndexSelected]);
	}

	Project.prototype.searchForText = function(textToSearchFor, matchCase)
	{
		this.searchResults.length = 0;

		if (matchCase == false)
		{
			textToSearchFor = textToSearchFor.toLowerCase();
		}

		for (var i = 0; i < this.documents.length; i++)
		{
			var documentToSearch = this.documents[i];
			var documentContents = documentToSearch.contents;

			if (matchCase == false)
			{
				documentContents = documentContents.toLowerCase();
			}

			var indexOfMatchInContents = -1;
			while (true)
			{
				indexOfMatchInContents = documentContents.indexOf
				(
					textToSearchFor,
					indexOfMatchInContents + 1
				);

				if (indexOfMatchInContents >= 0)
				{
					var matchPos = Document.stringAndCharOffsetToCursorPos
					(
						documentContents,
						indexOfMatchInContents
					);

					var newline = "\n";

					var lineWithMatchStart = documentContents.lastIndexOf(newline, indexOfMatchInContents);
					var lineWithMatchEnd = documentContents.indexOf(newline, indexOfMatchInContents);

					if (lineWithMatchStart == -1)
					{
						lineWithMatchStart = 0;
					}

					if (lineWithMatchEnd == -1)
					{
						lineWithMatchEnd = null;
					}

					var lineWithMatch = documentToSearch.contents.substring 
					(
						// Not the same as ".substr"!
						lineWithMatchStart, lineWithMatchEnd
					);

					var result = new SearchResult
					(
						documentToSearch.name, 
						matchPos,
						lineWithMatch
					)
					this.searchResults.push(result);
				}
				else
				{
					break;
				}
			}
		}
	}

	// dom

	Project.prototype.domUpdate = function()
	{
		var inputProjectName = 
			document.getElementById("inputProjectName");

		inputProjectName.value = this.name;

		var selectDocumentsInProject = 
			document.getElementById("selectDocumentsInProject");

		selectDocumentsInProject.options.length = 0;		

		for (var i = 0; i < this.documents.length; i++)
		{
			var _document = this.documents[i];
			var documentAsOption = document.createElement("option");
			documentAsOption.innerHTML = _document.name;
			selectDocumentsInProject.appendChild(documentAsOption);
		}

		var documentSelected = this.documentSelected();

		var inputDocumentSelectedName = 
			document.getElementById("inputDocumentSelectedName");
		var textareaDocumentSelectedContents = 
			document.getElementById("textareaDocumentSelectedContents");

		if (documentSelected == null)
		{
			inputDocumentSelectedName.value = "";
			textareaDocumentSelectedContents.value = "";
		}
		else
		{
			selectDocumentsInProject.selectedIndex = 
				this.documentIndexSelected;
			inputDocumentSelectedName.value = 
				documentSelected.name;
			textareaDocumentSelectedContents.value 
				= documentSelected.contents;
		}

		this.domUpdate_Cursor();

		this.domUpdate_Search();
	}

	Project.prototype.domUpdate_Cursor = function()
	{
		var inputCursorColumn = 
			document.getElementById("inputCursorColumn");
		var inputCursorRow = 
			document.getElementById("inputCursorRow");

		var documentSelected = this.documentSelected();

		if (documentSelected == null)
		{
			inputCursorRow.value = "";
			inputCursorColumn.value = "";
		}
		else
		{
			inputCursorRow.value = documentSelected.cursorPos.y;
			inputCursorColumn.value = documentSelected.cursorPos.x;
		}
	}

	Project.prototype.domUpdate_Cursor_Place = function()
	{	
		var documentSelected = this.documentSelected();

		if (documentSelected != null)
		{
			var cursorOffsetInChars = Document.stringAndCursorPosToCharOffset
			(
				documentSelected.contents,
				documentSelected.cursorPos
			);

			var textareaDocumentSelectedContents = 
				document.getElementById("textareaDocumentSelectedContents");

			textareaDocumentSelectedContents.selectionStart = cursorOffsetInChars; 
			textareaDocumentSelectedContents.selectionEnd = cursorOffsetInChars;
			textareaDocumentSelectedContents.focus();
		}
	}

	Project.prototype.domUpdate_Search = function()
	{
		var selectSearchResults = document.getElementById("selectSearchResults");
		selectSearchResults.innerHTML = "";

		for (var i = 0; i < this.searchResults.length; i++)
		{
			var searchResult = this.searchResults[i];
			var searchResultAsOption = document.createElement("option");
			searchResultAsOption.innerHTML = searchResult.toString();
			selectSearchResults.appendChild(searchResultAsOption);
		}
	}

	// tar

	Project.prototype.toTarFile = function()
	{
		var returnValue = TarFile.new();

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

			var documentContents = _document.contents;
			var documentContentsAsBytes = ByteHelper.stringUTF8ToBytes
			(
				documentContents
			);

			var documentAsTarFileEntry = TarFileEntry.fileNew
			(
				_document.name,
				documentContentsAsBytes	
			);
			returnValue.entries.push(documentAsTarFileEntry);
		}

		return returnValue;
	}
}

function SearchResult(documentName, posInDocument, lineContainingMatch)
{
	this.documentName = documentName;
	this.posInDocument = posInDocument;
	this.lineContainingMatch = lineContainingMatch;
}
{
	SearchResult.prototype.toString = function()
	{
		return this.documentName 
		+ " - " + this.posInDocument.toString() 
		+ " - " + this.lineContainingMatch;
	}
}


function Session(project)
{
	this.project = project;
}
{
	// dom

	Session.prototype.domUpdate = function()
	{
		this.project.domUpdate();
	}
}

function TarFile(fileName, entries)
{
	this.fileName = fileName;
	this.entries = entries;
}
{
	// constants

	TarFile.ChunkSize = 512;

	// static methods

	TarFile.fromBytes = function(fileName, bytes)
	{
		var reader = new ByteStream(bytes);

		var entries = [];

		var chunkSize = TarFile.ChunkSize;

		var numberOfConsecutiveZeroChunks = 0;

		while (reader.hasMoreBytes() == true)
		{
			var chunkAsBytes = reader.readBytes(chunkSize);

			var areAllBytesInChunkZeroes = true;

			for (var b = 0; b < chunkAsBytes.length; b++)
			{
				if (chunkAsBytes[b] != 0)
				{
					areAllBytesInChunkZeroes = false;
					break;
				}
			}

			if (areAllBytesInChunkZeroes == true)
			{
				numberOfConsecutiveZeroChunks++;

				if (numberOfConsecutiveZeroChunks == 2)
				{
					break;
				}
			}
			else
			{
				numberOfConsecutiveZeroChunks = 0;

				var entry = TarFileEntry.fromBytes(chunkAsBytes, reader);

				entries.push(entry);
			}
		}

		var returnValue = new TarFile
		(
			fileName,
			entries
		);

		return returnValue;
	}
	
	TarFile.new = function(fileName)
	{
		return new TarFile
		(
			fileName,
			[] // entries
		);
	}

	// instance methods
	
	TarFile.prototype.downloadAs = function(fileNameToSaveAs)
	{	
		FileHelper.saveBytesAsFile
		(
			this.toBytes(),
			fileNameToSaveAs
		)
	}	
	
	TarFile.prototype.entriesForDirectories = function()
	{
		var returnValues = [];
		
		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			if (entry.header.typeFlag.name == "Directory")
			{
				returnValues.push(entry);
			}
		}
		
		return returnValues;
	}
	
	TarFile.prototype.toBytes = function()
	{
		var fileAsBytes = [];		

		// hack - For easier debugging.
		var entriesAsByteArrays = [];
		
		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			var entryAsBytes = entry.toBytes();
			entriesAsByteArrays.push(entryAsBytes);
		}		
		
		for (var i = 0; i < entriesAsByteArrays.length; i++)
		{
			var entryAsBytes = entriesAsByteArrays[i];
			fileAsBytes = fileAsBytes.concat(entryAsBytes);
		}
		
		var chunkSize = TarFile.ChunkSize;
		
		var numberOfZeroChunksToWrite = 2;
		
		for (var i = 0; i < numberOfZeroChunksToWrite; i++)
		{
			for (var b = 0; b < chunkSize; b++)
			{
				fileAsBytes.push(0);
			}
		}

		return fileAsBytes;
	}
	
	// strings

	TarFile.prototype.toString = function()
	{
		var newline = "\n";

		var returnValue = "[TarFile]" + newline;

		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			var entryAsString = entry.toString();
			returnValue += entryAsString;
		}

		returnValue += "[/TarFile]" + newline;

		return returnValue;
	}
}

function TarFileEntry(header, dataAsBytes)
{
	this.header = header;
	this.dataAsBytes = dataAsBytes;
}
{
	// methods
	
	// static methods
	
	TarFileEntry.directoryNew = function(directoryName)
	{
		var header = new TarFileEntryHeader.directoryNew(directoryName);
		
		var entry = new TarFileEntry(header, []);
		
		return entry;
	}
	
	TarFileEntry.fileNew = function(fileName, fileContentsAsBytes)
	{
		var header = new TarFileEntryHeader.fileNew(fileName, fileContentsAsBytes);
		
		var entry = new TarFileEntry(header, fileContentsAsBytes);
		
		return entry;
	}
	
	TarFileEntry.fromBytes = function(chunkAsBytes, reader)
	{
		var chunkSize = TarFile.ChunkSize;
	
		var header = TarFileEntryHeader.fromBytes
		(
			chunkAsBytes
		);
	
		var sizeOfDataEntryInBytesUnpadded = header.fileSizeInBytes;	

		var numberOfChunksOccupiedByDataEntry = Math.ceil
		(
			sizeOfDataEntryInBytesUnpadded / chunkSize
		)
	
		var sizeOfDataEntryInBytesPadded = 
			numberOfChunksOccupiedByDataEntry
			* chunkSize;
	
		var dataAsBytes = reader.readBytes
		(
			sizeOfDataEntryInBytesPadded
		).slice
		(
			0, sizeOfDataEntryInBytesUnpadded
		);
	
		var entry = new TarFileEntry(header, dataAsBytes);
		
		return entry;
	}
	
	TarFileEntry.manyFromByteArrays = function
	(
		fileNamePrefix, fileNameSuffix, entriesAsByteArrays
	)
	{
		var returnValues = [];
		
		for (var i = 0; i < entriesAsByteArrays.length; i++)
		{
			var entryAsBytes = entriesAsByteArrays[i];
			var entry = TarFileEntry.fileNew
			(		
				fileNamePrefix + i + fileNameSuffix,
				entryAsBytes
			);
			
			returnValues.push(entry);
		}
		
		return returnValues;
	}
	
	// instance methods

	TarFileEntry.prototype.download = function(event)
	{
		FileHelper.saveBytesAsFile
		(
			this.dataAsBytes,
			this.header.fileName
		);
	}
	
	TarFileEntry.prototype.remove = function(event)
	{
		alert("Not yet implemented!"); // todo
	}
	
	TarFileEntry.prototype.toBytes = function()
	{
		var entryAsBytes = [];
	
		var chunkSize = TarFile.ChunkSize;
	
		var headerAsBytes = this.header.toBytes();
		entryAsBytes = entryAsBytes.concat(headerAsBytes);
		
		entryAsBytes = entryAsBytes.concat(this.dataAsBytes);

		var sizeOfDataEntryInBytesUnpadded = this.header.fileSizeInBytes;	

		var numberOfChunksOccupiedByDataEntry = Math.ceil
		(
			sizeOfDataEntryInBytesUnpadded / chunkSize
		)
	
		var sizeOfDataEntryInBytesPadded = 
			numberOfChunksOccupiedByDataEntry
			* chunkSize;
			
		var numberOfBytesOfPadding = 
			sizeOfDataEntryInBytesPadded - sizeOfDataEntryInBytesUnpadded;
	
		for (var i = 0; i < numberOfBytesOfPadding; i++)
		{
			entryAsBytes.push(0);
		}
		
		return entryAsBytes;
	}	
		
	// strings
	
	TarFileEntry.prototype.toString = function()
	{
		var newline = "\n";

		headerAsString = this.header.toString();

		var dataAsHexadecimalString = ByteHelper.bytesToStringHexadecimal
		(
			this.dataAsBytes
		);

		var returnValue = 
			"[TarFileEntry]" + newline
			+ headerAsString
			+ "[Data]"
			+ dataAsHexadecimalString
			+ "[/Data]" + newline
			+ "[/TarFileEntry]"
			+ newline;

		return returnValue
	}
	
}

function TarFileEntryHeader
(
	fileName,
	fileMode,
	userIDOfOwner,
	userIDOfGroup,
	fileSizeInBytes,
	timeModifiedInUnixFormat,
	checksum,
	typeFlag,
	nameOfLinkedFile,
	uStarIndicator,
	uStarVersion,
	userNameOfOwner,
	groupNameOfOwner,
	deviceNumberMajor,
	deviceNumberMinor,
	filenamePrefix
)
{
	this.fileName = fileName;
	this.fileMode = fileMode;
	this.userIDOfOwner = userIDOfOwner;
	this.userIDOfGroup = userIDOfGroup;
	this.fileSizeInBytes = fileSizeInBytes;
	this.timeModifiedInUnixFormat = timeModifiedInUnixFormat;
	this.checksum = checksum;
	this.typeFlag = typeFlag;
	this.nameOfLinkedFile = nameOfLinkedFile;
	this.uStarIndicator = uStarIndicator;
	this.uStarVersion = uStarVersion;
	this.userNameOfOwner = userNameOfOwner;
	this.groupNameOfOwner = groupNameOfOwner;
	this.deviceNumberMajor = deviceNumberMajor;
	this.deviceNumberMinor = deviceNumberMinor;
	this.filenamePrefix = filenamePrefix;
}
{
	TarFileEntryHeader.SizeInBytes = 500;

	// static methods
	
	TarFileEntryHeader.default = function()
	{
		var returnValue = new TarFileEntryHeader
		(
			"".padRight(100, "\0"), // fileName
			"100777 \0", // fileMode
			"0 \0".padLeft(8, " "), // userIDOfOwner
			"0 \0".padLeft(8, " "), // userIDOfGroup
			0, // fileSizeInBytes
			[49, 50, 55, 50, 49, 49, 48, 55, 53, 55, 52, 32], // hack - timeModifiedInUnixFormat
			0, // checksum
			TarFileTypeFlag.Instances.Normal,		
			"".padRight(100, "\0"), // nameOfLinkedFile,
			"".padRight(6, "\0"), // uStarIndicator,
			"".padRight(2, "\0"), // uStarVersion,
			"".padRight(32, "\0"), // userNameOfOwner,
			"".padRight(32, "\0"), // groupNameOfOwner,
			"".padRight(8, "\0"), // deviceNumberMajor,
			"".padRight(8, "\0"), // deviceNumberMinor,
			"".padRight(155, "\0") // filenamePrefix	
		);		
		
		return returnValue;
	}
	
	TarFileEntryHeader.directoryNew = function(directoryName)
	{
		var header = TarFileEntryHeader.default();
		header.fileName = directoryName;
		header.typeFlag = TarFileTypeFlag.Instances.Directory;
		header.fileSizeInBytes = 0;
		header.checksumCalculate();
		
		return header;
	}
	
	TarFileEntryHeader.fileNew = function(fileName, fileContentsAsBytes)
	{
		var header = TarFileEntryHeader.default();
		header.fileName = fileName;
		header.typeFlag = TarFileTypeFlag.Instances.Normal;
		header.fileSizeInBytes = fileContentsAsBytes.length;
		header.checksumCalculate();
		
		return header;
	}

	TarFileEntryHeader.fromBytes = function(bytes)
	{
		var reader = new ByteStream(bytes);

		var fileName = reader.readString(100).trim();
		var fileMode = reader.readString(8);
		var userIDOfOwner = reader.readString(8);
		var userIDOfGroup = reader.readString(8);
		var fileSizeInBytesAsStringOctal = reader.readString(12);
		var timeModifiedInUnixFormat = reader.readBytes(12);
		var checksumAsStringOctal = reader.readString(8);
		var typeFlagValue = reader.readString(1);
		var nameOfLinkedFile = reader.readString(100);
		var uStarIndicator = reader.readString(6);
		var uStarVersion = reader.readString(2);
		var userNameOfOwner = reader.readString(32);
		var groupNameOfOwner = reader.readString(32);
		var deviceNumberMajor = reader.readString(8);
		var deviceNumberMinor = reader.readString(8);
		var filenamePrefix = reader.readString(155);
		var reserved = reader.readBytes(12);

		var fileSizeInBytes = parseInt
		(
			fileSizeInBytesAsStringOctal.trim(), 8
		);
		
		var checksum = parseInt
		(
			checksumAsStringOctal, 8
		);		
		
		var typeFlags = TarFileTypeFlag.Instances._All;
		var typeFlagID = "_" + typeFlagValue;
		var typeFlag = typeFlags[typeFlagID];

		var returnValue = new TarFileEntryHeader
		(
			fileName,
			fileMode,
			userIDOfOwner,
			userIDOfGroup,
			fileSizeInBytes,
			timeModifiedInUnixFormat,
			checksum,
			typeFlag,
			nameOfLinkedFile,
			uStarIndicator,
			uStarVersion,
			userNameOfOwner,
			groupNameOfOwner,
			deviceNumberMajor,
			deviceNumberMinor,
			filenamePrefix
		);

		return returnValue;
	}

	// instance methods
	
	TarFileEntryHeader.prototype.checksumCalculate = function()
	{	
		var thisAsBytes = this.toBytes();
	
		// The checksum is the sum of all bytes in the header,
		// except we obviously can't include the checksum itself.
		// So it's assumed that all 8 of checksum's bytes are spaces (0x20=32).
		// So we need to set this manually.
						
		var offsetOfChecksumInBytes = 148;
		var numberOfBytesInChecksum = 8;
		var presumedValueOfEachChecksumByte = " ".charCodeAt(0);
		for (var i = 0; i < numberOfBytesInChecksum; i++)
		{
			var offsetOfByte = offsetOfChecksumInBytes + i;
			thisAsBytes[offsetOfByte] = presumedValueOfEachChecksumByte;
		}
		
		var checksumSoFar = 0;

		for (var i = 0; i < thisAsBytes.length; i++)
		{
			var byteToAdd = thisAsBytes[i];
			checksumSoFar += byteToAdd;
		}		

		this.checksum = checksumSoFar;
		
		return this.checksum;
	}
	
	TarFileEntryHeader.prototype.toBytes = function()
	{
		var headerAsBytes = [];
		var writer = new ByteStream(headerAsBytes);
		
		var fileSizeInBytesAsStringOctal = (this.fileSizeInBytes.toString(8) + " ").padLeft(12, " ")
		var checksumAsStringOctal = (this.checksum.toString(8) + " \0").padLeft(8, " ");

		writer.writeString(this.fileName, 100);
		writer.writeString(this.fileMode, 8);
		writer.writeString(this.userIDOfOwner, 8);
		writer.writeString(this.userIDOfGroup, 8);
		writer.writeString(fileSizeInBytesAsStringOctal, 12);
		writer.writeBytes(this.timeModifiedInUnixFormat);
		writer.writeString(checksumAsStringOctal, 8);
		writer.writeString(this.typeFlag.value, 1);		
		writer.writeString(this.nameOfLinkedFile, 100);
		writer.writeString(this.uStarIndicator, 6);
		writer.writeString(this.uStarVersion, 2);
		writer.writeString(this.userNameOfOwner, 32);
		writer.writeString(this.groupNameOfOwner, 32);
		writer.writeString(this.deviceNumberMajor, 8);
		writer.writeString(this.deviceNumberMinor, 8);
		writer.writeString(this.filenamePrefix, 155);
		writer.writeString("".padRight(12, "\0")); // reserved

		return headerAsBytes;
	}		
		
	// strings

	TarFileEntryHeader.prototype.toString = function()
	{		
		var newline = "\n";
	
		var returnValue = 
			"[TarFileEntryHeader "
			+ "fileName='" + this.fileName + "' "
			+ "typeFlag='" + (this.typeFlag == null ? "err" : this.typeFlag.name) + "' "
			+ "fileSizeInBytes='" + this.fileSizeInBytes + "' "
			+ "]"
			+ newline;

		return returnValue;
	}
}	

function TarFileTypeFlag(value, name)
{
	this.value = value;
	this.id = "_" + this.value;
	this.name = name;
}
{
	TarFileTypeFlag.Instances = new TarFileTypeFlag_Instances();

	function TarFileTypeFlag_Instances()
	{
		this.Normal 		= new TarFileTypeFlag("0", "Normal");
		this.HardLink 		= new TarFileTypeFlag("1", "Hard Link");
		this.SymbolicLink 	= new TarFileTypeFlag("2", "Symbolic Link");
		this.CharacterSpecial 	= new TarFileTypeFlag("3", "Character Special");
		this.BlockSpecial 	= new TarFileTypeFlag("4", "Block Special");
		this.Directory		= new TarFileTypeFlag("5", "Directory");
		this.FIFO		= new TarFileTypeFlag("6", "FIFO");
		this.ContiguousFile 	= new TarFileTypeFlag("7", "Contiguous File");

		// Additional types not implemented:
		// 'g' - global extended header with meta data (POSIX.1-2001)
		// 'x' - extended header with meta data for the next file in the archive (POSIX.1-2001)
		// 'A'–'Z' - Vendor specific extensions (POSIX.1-1988)
		// [other values] - reserved for future standardization

		this._All = 
		[
			this.Normal,
			this.HardLink,
			this.SymbolicLink,
			this.CharacterSpecial,
			this.BlockSpecial,
			this.Directory,
			this.FIFO,
			this.ContiguousFile,
		];

		for (var i = 0; i < this._All.length; i++)
		{
			var item = this._All[i];
			this._All[item.id] = item;
		}
	}
}
	
// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

A Word-for-Word Inline Translation Utility in JavaScript

The JavaScript program below, when run, prompts the user for a passage to be translated into a foreign language and a dictionary that provides translations, then presents the translated text interleaved with the original text.

The translation is word-for-word, which obviously is not sufficient to translate meaning in most languages. But it does provide a good basis for further practice in learning to read a foreign language.

inlinetranslation


<html>
<body>

<!-- ui -->

	<div>
		<label>Passage to Translate:</label>
		<input type="file" onchange="inputFilePassageToTranslate_Changed(this);"></input>
		<br />
		<textarea id="textareaPassageToTranslate" cols="40" rows="5">This is a test!</textarea>
	</div>

	<div>
		<label>Dictionary to Translate with:</label>
		<input type="file" onchange="inputFileDictionaryToTranslateWith_Changed(this);"></input>
		<br />
		<textarea id="textareaDictionaryToTranslateWith" cols="40" rows="5">
this=esto
is=es
a=un
test=prueba
		</textarea>
	</div>

	<div>
		<button onclick="buttonTranslate_Clicked();">Translate</button>
	</div>

	<div>	
		<label>Interleaved Translation:</label>
		<br />
		<textarea id="textareaTranslationInterleaved" cols="40" rows="10"></textarea>
	</div>

<!-- ui ends -->

<script type="text/javascript">

// ui events

function inputFilePassageToTranslate_Changed(inputFilePassageToTranslate)
{
	var fileToLoad = inputFilePassageToTranslate.files[0];
	var fileReader = new FileReader();
	fileReader.onload = inputFilePassageToTranslate_Changed_FileLoaded;
	fileReader.readAsText(fileToLoad);
}

function inputFilePassageToTranslate_Changed_FileLoaded(event)
{
	var passageToTranslate = event.target.result;
	var textareaPassageToTranslate = document.getElementById
	(
		"textareaPassageToTranslate"
	);
	textareaPassageToTranslate.value = passageToTranslate;
}

function inputFileDictionaryToTranslateWith_Changed(inputFileDictionaryToTranslateWith)
{
	var fileToLoad = inputFileDictionaryToTranslateWith.files[0];
	var fileReader = new FileReader();
	fileReader.onload = inputFileDictionaryToTranslateWith_Changed_FileLoaded;
	fileReader.readAsText(fileToLoad);		
}

function inputFileDictionaryToTranslateWith_Changed_FileLoaded(event)
{
	var dictionaryAsText = event.target.result;
	var textareaDictionaryToTranslateWith = document.getElementById
	(
		"textareaDictionaryToTranslateWith"
	);
	textareaDictionaryToTranslateWith.value = dictionaryAsText;
}

function buttonTranslate_Clicked()
{
	var textareaPassageToTranslate = document.getElementById
	(
		"textareaPassageToTranslate"
	);
	var passageToTranslate = textareaPassageToTranslate.value;

	var textareaDictionaryToTranslateWith = document.getElementById
	(
		"textareaDictionaryToTranslateWith"
	);
	var dictionaryAsText = textareaDictionaryToTranslateWith.value;

	var dictionaryAsLookup = Translator.dictionaryParse(dictionaryAsText);

	var translator = new Translator(dictionaryAsLookup);

	var textareaTranslationInterleaved = document.getElementById
	(
		"textareaTranslationInterleaved"
	);

	var charsPerLine = textareaTranslationInterleaved.cols;

	var textTranslatedAndInterleaved = translator.translateAndInterleave
	(
		passageToTranslate,
		charsPerLine
	);

	textareaTranslationInterleaved.value = textTranslatedAndInterleaved;
}

// extensions

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

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

		return returnValue;
	}
}

// classes

function Translator(dictionary)
{
	this.dictionary = dictionary;
}
{
	Translator.dictionaryParse = function(dictionaryAsText)
	{
		var returnValue = {};

		var entriesAsStrings = dictionaryAsText.split("\n");
		for (var i = 0; i < entriesAsStrings.length; i++)
		{
			var entryAsString = entriesAsStrings[i];
			var entryAsKeyAndValue = entryAsString.split("=");
			var key = entryAsKeyAndValue[0];
			var value = entryAsKeyAndValue[1];
			returnValue[key] = value;
		}

		return returnValue;
	}

	// instance methods

	Translator.prototype.translateAndInterleave = function(passageToTranslate, charsPerLine)
	{
		var passageInterleaved = "";

		var wordsToTranslate = passageToTranslate.split(" ");

		var charsInCurrentLine = 0;
		var lineToTranslateCurrent = "";
		var lineTranslatedCurrent = "";
	
		for (var i = 0; i < wordsToTranslate.length; i++)
		{
			var wordToTranslate = wordsToTranslate[i];
			var wordToTranslateAsKey = 
				wordToTranslate.toLowerCase().split("!").join("");

			var wordTranslated = this.dictionary[wordToTranslateAsKey];
			if (wordTranslated == null)
			{
				wordTranslated = "[" + wordToTranslate + "]";
			}

			var wordToTranslateLength = wordToTranslate.length;
			var wordTranslatedLength = wordTranslated.length;

			var wordLengthGreater = Math.max
			(
				wordToTranslateLength,
				wordTranslatedLength
			);
		
			var wordToTranslatePadded = wordToTranslate.padRight
			(
				" ", wordLengthGreater
			);

			var wordTranslatedPadded = wordTranslated.padRight
			(
				" ", wordLengthGreater
			);

			charsInCurrentLine += (1 + wordLengthGreater);
			if (charsInCurrentLine >= charsPerLine)
			{
				passageInterleaved += 
					lineToTranslateCurrent + "\n"
					+ lineTranslatedCurrent + "\n"
					+ "\n";

				lineToTranslateCurrent = wordToTranslatePadded;
				lineTranslatedCurrent = wordTranslatedPadded;
				charsInCurrentLine = wordLengthGreater;	
			}
			else
			{
				lineToTranslateCurrent += wordToTranslatePadded + " ";
				lineTranslatedCurrent += wordTranslatedPadded + " ";
			}
		}

		// hack
		passageInterleaved += 
			lineToTranslateCurrent + "\n"
			+ lineTranslatedCurrent + "\n"
			+ "\n";

		return passageInterleaved;		
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , | 1 Comment

Simulating Hardware in VHDL Using GHDL

Follow the steps below to create a simple “Hello, World” program in VHDL using GHDL.

VHDL, or “VHSIC Hardware Description Language”, is a programming language used to simulate the operation of computer hardware in software.  It provides a standardized method of designing and testing electronic hardware, which reduces the difficulty of the design’s eventual translation into real, physical components.

GHDL is an open-source simulator that can compile and run VHDL code.

1. Download and extract GHDL.  As of this writing, the latest Windows version is available at the URL “https://github.com/tgingold/ghdl/releases/tag/v0.33&#8221;.

2. Locate “GHDL.exe” within the “bin” directory of the newly extracted GHDL archive.  Make a note of the path of GHDL.exe. If desired, this path can be added to the system’s PATH environment variable.

3. In any convenient location, create a new directory named “VHDLTest”.

4. In the newly created VHDLTest directory, create a new text file named “hello.vhdl”, containing the following text. This code is taken from the file “ghdl.htm” within the GHDL archive.


     --  Hello world program.
     use std.textio.all; --  Imports the standard textio package.

     --  Defines a design entity, without any ports.
     entity hello_world is
     end hello_world;

     architecture behaviour of hello_world is
     begin
        process
           variable l : line;
        begin
           write (l, String'("Hello world!"));
           writeline (output, l);
           wait;
        end process;
     end behaviour;

5. Open a command prompt window and run the following commands, substituting the full path of the file ghdl.exe where necessary:


ghdl.exe -a hello.vhdl
ghdl.exe -e hello_world
ghdl.exe -r hello_world

6. Verify that the message “Hello world!” appears in the command prompt window.

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

Exploring the PNG Image File Format with a PNG Viewer in JavaScript

The JavaScript program below, when run, prompts the user to upload a PNG file and displays that image. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The code makes use of the “pako” library, by Andrey Tupitsin and Vitaly Puzrin, to uncompress the pixel data using the DEFLATE algorithm. For more information about pako, visit “https://github.com/nodeca/pako“.

Obviously there are easier and better ways to display a PNG in JavaScript, or for that matter within a web browser. This code is intended as a straightforward illustration of the process of decoding a PNG, with an eye toward eventually implementing a PNG viewer in some other language, though obviously another decompression library will needed in that case.

pngviewer


<html>
<body>

	<div><label>File to Load:</label></div>
	<div><input type="file" onchange="inputFile_Changed(this);"></input></div>
	<div><label>Output:</label></div>
	<div><div id="divOutput"></div></div>

<script type="text/javascript" src="https://rawgit.com/nodeca/pako/master/dist/pako.js"></script>

<script type="text/javascript">

// ui events

function inputFile_Changed(inputFile)
{
	var fileSpecified = inputFile.files[0];
	if (fileSpecified != null)
	{
		FileHelper.loadFileAsBytes
		(
			fileSpecified, inputFile_Changed_Loaded
		);
	}
}

function inputFile_Changed_Loaded(fileAsBytes)
{
	var fileAsPNG = PNG.fromBytes(fileAsBytes);
	var fileAsCanvas = fileAsPNG.toCanvas();
	var divOutput = document.getElementById("divOutput");
	divOutput.appendChild(fileAsCanvas);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookupArrays = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			var arrayForKey = this[key];
			if (arrayForKey == null)
			{
				arrayForKey = [];
				this[key] = arrayForKey;
			}
			arrayForKey.push(element);
		}
		return this;
	}
}

// classes

function ByteStream(bytes)
{
	this.bytes = bytes;
	this.byteOffset = 0;
}
{
	// constants

	ByteStream.BitsPerByte = 8;

	// methods

	ByteStream.prototype.hasMoreBytes = function()
	{
		var returnValue = (this.byteOffset < this.bytes.length);
		return returnValue;
	}

	ByteStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteOffset];
		this.byteOffset++;
		return returnValue;
	}

	ByteStream.prototype.readBytes = function(numberOfBytes)
	{
		var returnBytes = [];

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.readByte();
			returnBytes.push(byte);
		}

		return returnBytes;
	}

	ByteStream.prototype.readInteger32 = function()
	{
		var returnValue = 0;

		var numberOfBytes = 4;

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.readByte();
			var iReversed = numberOfBytes - i - 1;
			var valueOfByteInPlace = 
				byte << (iReversed * ByteStream.BitsPerByte);
			returnValue += valueOfByteInPlace;
		}

		return returnValue;
	}

	ByteStream.prototype.readString = function(numberOfBytes)
	{
		var returnString = "";

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.readByte();
			var byteAsChar = String.fromCharCode(byte);
			returnString += byteAsChar;
		}

		return returnString;
	}
}

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

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

function FileHelper()
{
	// static class
}
{
	FileHelper.binaryStringToBytes = function(binaryStringToConvert)
	{
		var returnBytes = [];

		for (var i = 0; i < binaryStringToConvert.length; i++)
		{
			var byte = binaryStringToConvert.charCodeAt(i);
			returnBytes.push(byte);
		}

		return returnBytes;
	}

	FileHelper.loadFileAsBytes = function(fileToLoad, callback)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var fileAsBinaryString = fileReader.result;
			var fileAsBytes = FileHelper.binaryStringToBytes
			(
				fileAsBinaryString
			);
			callback(fileAsBytes);
		}
		fileReader.readAsBinaryString(fileToLoad);
	}
}

function PNG(signature, chunks)
{
	this.signature = signature;
	this.chunks = chunks;

	this.chunks.addLookupArrays("typeCode");
}
{
	PNG.fromBytes = function(pngAsBytes)
	{
		var byteStream = new ByteStream(pngAsBytes);

		var signature = byteStream.readBytes(8);

		var chunks = [];
		
		while (byteStream.hasMoreBytes() == true)
		{
			var chunkPayloadSizeInBytes = byteStream.readInteger32();
			var chunkTypeCode = byteStream.readString(4);
			var chunkPayloadBytes = byteStream.readBytes(chunkPayloadSizeInBytes);
			var chunkChecksum = byteStream.readInteger32();

			var chunk = new PNG_Chunk
			(
				chunkTypeCode,
				chunkPayloadBytes,
				chunkChecksum
			);

			chunks.push(chunk);
		}

		var returnValue = new PNG(signature, chunks);

		return returnValue;
	}

	// dom

	PNG.prototype.toCanvas = function()
	{
		var headerChunk = this.chunks["IHDR"][0];
		var headerReader = new ByteStream
		(
			headerChunk.payloadBytes
		);

		var imageWidth = headerReader.readInteger32();
		var imageHeight = headerReader.readInteger32();
		
		var bitDepth = headerReader.readByte();

		var colorType = headerReader.readByte(); 

		var bytesPerPixel;
		if (colorType == 0) // grayscale without alpha
		{
			throw "Not yet implemented.";
		}
		else if (colorType == 2) // color without alpha
		{
			bytesPerPixel = 3;
		}
		else if (colorType == 4) // grayscale with alpha
		{
			throw "Not yet implemented.";
		}
		else if (colorType == 6) // color with alpha
		{
			bytesPerPixel = 4;
		}	

		var compressionMethod = headerReader.readByte();

		var filterMethod = headerReader.readByte();

		var interlaceMode = headerReader.readByte();
		
		var dataChunks = this.chunks["IDAT"]; // todo

		var pixelsCompressed = [];

		for (var i = 0; i < dataChunks.length; i++)
		{
			var dataChunk = dataChunks[i];

			pixelsCompressed = pixelsCompressed.concat
			(
				dataChunk.payloadBytes
			);
		}

		var pixelsDecompressed = pako.inflate
		(
			pixelsCompressed
		);

		// An alternative to the pako library is available at
		// https://rawgit.com/imaya/zlib.js/master/bin/zlib.min.js

		// var pixelsDecompressed = new Zlib.Inflate(pixelsCompressed).decompress();

		var canvas = document.createElement("canvas");
		canvas.width = imageWidth;
		canvas.height = imageHeight; 

		var pixelsDefilteredSoFar = [];

		var pixelReader = new ByteStream(pixelsDecompressed);

		var pixelRGBAZeroes = [0, 0, 0, 0];

		for (var y = 0; y < imageHeight; y++)
		{
			var filterTypeCode = pixelReader.readByte();

			for (var x = 0; x < imageWidth; x++)
			{
				var pixelRGBAFiltered = pixelReader.readBytes(bytesPerPixel);
				
				var pixelRGBADefiltered = [];

				for (var c = 0; c < pixelRGBAFiltered.length; c++)
				{
					var pixelComponentFiltered = pixelRGBAFiltered[c];

					var pixelComponentDefiltered;

					if (filterTypeCode == 0) // no filter
					{
						pixelComponentDefiltered = pixelComponentFiltered;
					}
					else if (filterTypeCode == 1) // "sub"
					{
						// Difference of this pixel's component
						// and the corresponding component 
						// of the pixel to the left.

						var pixelLeft;
						if (x == 0)
						{
							pixelLeft = pixelRGBAZeroes;
						}
						else
						{
							var pixelLeftIndex = 
								pixelsDefilteredSoFar.length - 1;
							pixelLeft = pixelsDefilteredSoFar[pixelLeftIndex];
						}
						var pixelLeftComponent = pixelLeft[c];
						pixelComponentDefiltered = 
							(pixelComponentFiltered + pixelLeftComponent) % 256;
					}
					else if (filterTypeCode == 2) // "up"
					{
						// Difference of this pixel's component
						// and the corresponding component 
						// of the pixel above.

						var pixelAboveIndex = 
							pixelsDefilteredSoFar.length - imageWidth;
						var pixelAbove = pixelsDefilteredSoFar[pixelAboveIndex];
						var pixelAboveComponent = pixelAbove[c];
						pixelComponentDefiltered = 
							(pixelComponentFiltered + pixelAboveComponent) % 256;
					}
					else if (filterTypeCode == 3) // "average"
					{
						// Average of left and above.

						var pixelLeft;
						var pixelAbove;

						if (x == 0)
						{
							pixelLeft = pixelRGBAZeroes;
						}
						else
						{
							var pixelLeftIndex = 
								pixelsDefilteredSoFar.length - 1;

							pixelLeft = pixelsDefilteredSoFar[pixelLeftIndex];
						}

						var pixelAboveIndex = 
							pixelsDefilteredSoFar.length - imageWidth;
						pixelAbove = pixelsDefilteredSoFar[pixelAboveIndex];

						var pixelLeftComponent = pixelLeft[c];
						var pixelAboveComponent = pixelAbove[c];


						pixelComponentDefiltered = 
							pixelComponentFiltered 
							+ Math.floor
							(
								(pixelLeftComponent + pixelAboveComponent)
								/2
							);

						pixelComponentDefiltered = pixelComponentDefiltered % 256;
					}
					else if (filterTypeCode == 4) // "Paeth"
					{
						// Uses left, above, and above left. 
						var pixelLeft;
						var pixelAbove;
						var pixelAboveLeft;

						if (x == 0)
						{
							pixelLeft = pixelRGBAZeroes;
							pixelAboveLeft = pixelRGBAZeroes;
						}
						else
						{
							var pixelLeftIndex = 
								pixelsDefilteredSoFar.length - 1;

							pixelLeft = pixelsDefilteredSoFar[pixelLeftIndex];

							if (y == 0)
							{
								pixelAboveLeft = pixelRGBAZeroes;
							}
							else
							{

								var pixelAboveLeftIndex = 
									pixelsDefilteredSoFar.length - 1 - imageWidth;

								pixelAboveLeft = pixelsDefilteredSoFar[pixelAboveLeftIndex];
							}
						}

						if (y == 0)
						{
							pixelAbove = pixelRGBAZeroes;
						}
						else
						{
							var pixelAboveIndex = 
								pixelsDefilteredSoFar.length - imageWidth;
							pixelAbove = pixelsDefilteredSoFar[pixelAboveIndex];
						}

						var pixelLeftComponent = pixelLeft[c];
						var pixelAboveLeftComponent = pixelAboveLeft[c];
						var pixelAboveComponent = pixelAbove[c];

						var paethValue = this.paethPredictor
						(
							pixelLeftComponent, 
							pixelAboveComponent,
							pixelAboveLeftComponent 
						);

						pixelComponentDefiltered = 
							(pixelComponentFiltered + paethValue) % 256;
					}
					else
					{
						throw "Unknown filter type."
					}

					pixelRGBADefiltered[c] = pixelComponentDefiltered;
				}

				pixelsDefilteredSoFar.push(pixelRGBADefiltered);
			}
		}

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

		for (var y = 0; y < imageHeight; y++)
		{
			for (var x = 0; x < imageWidth; x++)
			{
				var pixelIndex = y * imageWidth + x;
				var pixelRGBA = pixelsDefilteredSoFar[pixelIndex];

				var pixelColorAsString = 
					"(" + pixelRGBA[0] + "," 
					+ pixelRGBA[1] + "," 
					+ pixelRGBA[2]; 

				if (pixelRGBA.length == 3)
				{
					pixelColorAsString = 
						"rgb" + pixelColorAsString;
				}
				else
				{
					pixelColorAsString = 
						"rgba" 
						+ pixelColorAsString 
						+ "," + (pixelRGBA[3] / 255);
				}

				pixelColorAsString += ")";

				graphics.fillStyle = pixelColorAsString;
				graphics.fillRect(x, y, 1, 1);
			}
		}
	
		return canvas;
	}

	// helper methods

	PNG.prototype.paethPredictor = function(left, above, aboveLeft)
	{
		// Adapted from pseudocode found at the URL
		// https://www.w3.org/TR/PNG-Filters.html

		var estimate = left + above - aboveLeft;

		var differenceFromLeft = Math.abs(estimate - left);
		var differenceFromAbove = Math.abs(estimate - above);
		var differenceFromAboveLeft = Math.abs(estimate - aboveLeft);

		var returnValue;

		if 
		(
			differenceFromLeft <= differenceFromAbove
			&& differenceFromLeft <= differenceFromAboveLeft
		)
		{ 
			returnValue = left;
		}
		else if (differenceFromAbove <= differenceFromAboveLeft)
		{
			returnValue = above;
		}
		else
		{ 
			returnValue = aboveLeft;
		}

		return returnValue;
   	}
}

function PNG_Chunk(typeCode, payloadBytes, checksum)
{
	this.typeCode = typeCode;
	this.payloadBytes = payloadBytes;
	this.checksum = checksum;
}

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

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

Decompressing Data with the DEFLATE Algorithm in JavaScript

The code below, when run, prompts the user to upload a compressed file in GZIP format, which will then be uncompressed, and the uncompressed data will be displayed both as hexadecimal digits and as UTF8 text.

This implementation of DEFLATE is basically a port of a Java implementation created by Nyuki Minase, an edited Java version of which I presented in a previous post. I tried to leave the code in this port as nearly identical as possible, but I had to make a few questionable changes here and there. The most distressing of these is where I commented out the writing of data to the output byte stream in Decompressor.decodeHuffmanBlock(), because it seemed to be appending nonsense to the end of my test data. But no doubt it was there for a reason. It should go without saying that any bugs in this implementation should be assumed to be my own.

I tested the program by creating a simple .txt file and compressing it to a .gz with 7-Zip.

Note that this implementation only does decompression, not compression.

decompressingwithdeflate


<html>
<body>

<!-- ui -->

	<div>
		<div><label>Compressed Bytes as Hexadecimal:</label></div>
		<div><input type="file" onchange="inputCompressedDataLoadFromFile_Changed(this);"></input></div>
		<div><textarea id="textareaCompressed" cols="32" rows="8"></textarea></div>
	</div>

	<div>
		<button onclick="buttonDecompress_Clicked();">Decompress</button>
	</div>

	<div>
		<div><label>Uncompressed Bytes as Hexadecimal:</label></div>
		<div><textarea id="textareaUncompressed" cols="32" rows="8"></textarea></div>

		<div><label>Uncompressed Bytes as Text (UTF8):</label></div>
		<div><textarea id="textareaUncompressedAsUTF8" cols="32" rows="8"></textarea></div>
	</div>

<!-- ui ends -->

<script type="text/javascript">

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.removeAt = function(index)
	{
		this.splice(index, 1);
		return this;
	}
}

function StringExtensions()
{
	// extension class
}
{
	String.prototype.padLeft = function(charToPadWith, lengthToPadTo)
	{
		var returnValue = this;
		while (returnValue.length < lengthToPadTo)
		{
			returnValue = charToPadWith + returnValue;
		}
		return returnValue;
	}
}

// ui events

function buttonDecompress_Clicked()
{
	var textareaCompressed = document.getElementById("textareaCompressed");	
	var bytesCompressedAsHexadecimal = textareaCompressed.value;
	var bytesCompressed = [];
	for (var i = 0; i < bytesCompressedAsHexadecimal.length; i += 2)
	{
		var byteAsHexadecimal = bytesCompressedAsHexadecimal.substr(i, 2);
		var byte = parseInt(byteAsHexadecimal, 16);
		bytesCompressed.push(byte);
	}

	var inflator = new Inflator();
	var bytesDecompressed = inflator.decompressBytes(bytesCompressed);

	var bytesDecompressedAsHexadecimal = "";
	var bytesDecompressedAsUTF8 = "";
	for (var i = 0; i < bytesDecompressed.length; i++)
	{
		var byte = bytesDecompressed[i];

		var byteAsHexadecimal = byte.toString(16).padLeft("0", 2);
		bytesDecompressedAsHexadecimal += byteAsHexadecimal;

		var byteAsUTF8 = String.fromCharCode(byte);
		bytesDecompressedAsUTF8 += byteAsUTF8;
	}

	var textareaUncompressed = document.getElementById("textareaUncompressed");
	textareaUncompressed.value = bytesDecompressedAsHexadecimal;

	var textareaUncompressedAsUTF8 = document.getElementById("textareaUncompressedAsUTF8");
	textareaUncompressedAsUTF8.value = bytesDecompressedAsUTF8;

}

function inputCompressedDataLoadFromFile_Changed(input)
{
	var file = input.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(event)
	{
		var fileContentsAsBinaryString = event.target.result;
		var bytesCompressed = fileContentsAsBinaryString;
		var bytesCompressedAsHexadecimal = "";
		for (var i = 0; i < bytesCompressed.length; i++)
		{
			var byte = bytesCompressed.charCodeAt(i);
			var byteAsHexadecimal = byte.toString(16).padLeft("0", 2);
			bytesCompressedAsHexadecimal += byteAsHexadecimal;
		}
		var textareaCompressed = document.getElementById("textareaCompressed");
		textareaCompressed.value = bytesCompressedAsHexadecimal;
	}
	fileReader.readAsBinaryString(file);
}

// DEFLATE implementation

// Most of the code below is a port 
// of a Java implementation of DEFLATE written by Nyuki Minase.

/*(MIT License)

Copyright © 2012 Nayuki Minase
Copyright © 2017 This Could Be Better

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

* The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

* The Software is provided "as is", without warranty of any kind, express or
  implied, including but not limited to the warranties of merchantability,
  fitness for a particular purpose and noninfringement. In no event shall the
  authors or copyright holders be liable for any claim, damages or other
  liability, whether in an action of contract, tort or otherwise, arising from,
  out of or in connection with the Software or the use or other dealings in the
  Software.

*/

// main

function Inflator()
{
	// do nothing
}
{
	Inflator.prototype.decompressBytes = function(fileContentsAsBytes)
	{	
		var input = new BitStream(fileContentsAsBytes);

		let magicNumberForGZIP = input.readInteger16LE();
		if (magicNumberForGZIP != 35615)
			throw "Invalid GZIP magic number";

		var compressionMethodCode = input.readByte();
		if (compressionMethodCode != 8)
			throw "Unsupported compression method: " + (compressionMethodCode & 0xFF);

		var flags = input.readByte();

		// Reserved flags
		if ((flags & 0xE0) != 0)
			throw "Reserved flags are set";

		// Modification time
		var mtime = input.readInteger32LE();
		if (mtime != 0)
		{
			//console.log("Last modified: " + new DateTime(1970, 1, 1).add(mtime * 1000000L));
		}
		else
		{
			console.log("Last modified: N/A");
		}

		var extraFlags = input.readByte();

		// Extra flags
		switch (extraFlags) 
		{
			case 2:   console.log("Extra flags: Maximum compression");  break;
			case 4:   console.log("Extra flags: Fastest compression");  break;
			default:  console.log("Extra flags: Unknown");  break;
		}

		// Operating system
		var osCode = input.readByte();
		var os;
		switch (osCode & 0xFF) 
		{
			case   0:  os = "FAT";             break;
			case   1:  os = "Amiga";           break;
			case   2:  os = "VMS";             break;
			case   3:  os = "Unix";            break;
			case   4:  os = "VM/CMS";          break;
			case   5:  os = "Atari TOS";       break;
			case   6:  os = "HPFS";            break;
			case   7:  os = "Macintosh";       break;
			case   8:  os = "Z-System";        break;
			case   9:  os = "CP/M";            break;
			case  10:  os = "TOPS-20";         break;
			case  11:  os = "NTFS";            break;
			case  12:  os = "QDOS";            break;
			case  13:  os = "Acorn RISCOS";    break;
			case 255:  os = "Unknown";         break;
			default :  os = "Really unknown";  break;
		}
		console.log("Operating system: " + os);

		// Text flag
		if ((flags & 0x01) != 0)
		{
			console.log("Flag: Text");
		}

		// Extra flag
		if ((flags & 0x04) != 0) 
		{
			console.log("Flag: Extra");
			var len = input.readInteger16LE();
			input.readBytes(len);  // Skip extra data
		}

		// File name flag
		if ((flags & 0x08) != 0) 
		{
			var sb = "";
			while (true) 
			{
				var temp = input.readByte();
				if (input.hasMoreBits() == false)
					throw "EOFException";
				else if (temp == 0)  // Null-terminated string
					break;
				else
					sb += String.fromCharCode(temp);
			}
			console.log("File name: " + sb);
		}

		// Header CRC flag
		if ((flags & 0x02) != 0) {
			
			var crc = input.readInteger16LE(2);
			console.log("Header CRC-16: %04X%n", crc);
		}

		// Comment flag
		if ((flags & 0x10) != 0) {
			var sb = "";
			while (true) {
				var temp = input.readByte();
				if (input.hasMoreBits() == false)
					throw "EOFException";
				else if (temp == 0)  // Null-terminated string
					break;
				else
					sb += String.fromCharCode(temp);
			}
			console.log("Comment: " + sb);
		}

		// Decompress
		var bytesDecompressed = Decompressor.decompress
		(
			input
		);

		return bytesDecompressed;
	}
}

// classes

function BitStream(bytes)
{
	this.bytes = bytes;
	this.byteIndex = 0;
	this.bitOffsetWithinByte = 0;
}
{
	BitStream.prototype.alignWithByteBoundary = function()
	{
		if (this.bitOffsetWithinByte != 0)
		{
			this.bitOffsetWithinByte = 0;
			this.byteIndex++;
		}
	}

	BitStream.prototype.hasMoreBits = function()
	{
		return (this.byteIndex < this.bytes.length);
	}

	BitStream.prototype.readBit = function()
	{
		var byteCurrent = this.bytes[this.byteIndex];
		var returnValue = (byteCurrent >> (8 - this.bitOffsetWithinByte - 1)) & 0x1;
		this.bitOffsetWithinByte++;
		if (this.bitOffsetWithinByte >= 8)
		{
			this.bitOffsetWithinByte = 0;
			this.byteIndex++;
		}
		return returnValue;
	}

	BitStream.prototype.readBits = function(numberOfBits)
	{
		var returnValues = [];	

		for (var i = 0; i < numberOfBits; i++)
		{
			var bit = this.readBit();
			returnValues.push(bit);
		}

		return returnValues;
	}

	BitStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteIndex];
		this.byteIndex++;
		return returnValue;
	}

	BitStream.prototype.readBytes = function(numberOfBytes)
	{
		var returnValues = [];

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.bytes[this.byteIndex];
			returnValues.push(byte);
			this.byteIndex++;
		}

		return returnValues;
	}


	BitStream.prototype.readIntegerOfBitWidthLE = function(numberOfBits)
	{
		var returnValue = 0;

		var bits = this.readBits(numberOfBits);
		for (var i = 0; i < bits.length; i++)
		{
			var bit = bits[i];
			var bitValueInPlace = bit << i;
			returnValue = returnValue | bitValueInPlace;
		}

		return returnValue;
	}

	BitStream.prototype.readInteger16LE = function()
	{
		// 16-bit, little-endian

		var bytes = this.readBytes(2);
		var returnValue = bytes[0] | (bytes[1] << 8);
		return returnValue;
	}

	BitStream.prototype.readInteger32LE = function()
	{
		// 32-bit, little-endian

		var bytes = this.readBytes(4);
		var returnValue = 
			bytes[0] 
			| (bytes[1] << 8) 
			| (bytes[2] << 16) 
			| (bytes[3] << 24);
		return returnValue;
	}

	BitStream.prototype.writeByte = function(byte)
	{
		this.bytes.push(byte);
		this.byteIndex = this.bytes.length;
	}

	BitStream.prototype.writeBytes = function(bytesToWrite)
	{
		for (var i = 0; i < bytesToWrite.length; i++)
		{
			var byte = bytesToWrite[i];
			this.bytes.push(byte);
		}
		this.byteIndex = this.bytes.length;
	}
}

function CanonicalCode(codeLengths)
{

	if (codeLengths == null)
	{
		throw "Argument is null";
	}

	this.codeLengths = codeLengths.slice(0);
	for (var i = 0; i < codeLengths.length; i++) 
	{
		var x = codeLengths[i];
		if (x < 0)
		{
			throw "Illegal code length";
		}
	}
}
{
	// static classes 

	CanonicalCode.constructor2 = function(tree, symbolLimit) 
	{
		var codeLengths = new Array(symbolLimit);
		var returnValue = new CanonicalCode(codeLengths);
		returnValue.buildCodeLengths(tree.root, 0);
	}

	CanonicalCode.prototype.buildCodeLengths = function(node, depth) 
	{
		if (node.constructor.name == "InternalNode") 
		{
			var internalNode = node;
			this.buildCodeLengths(internalNode.leftChild , depth + 1);
			this.buildCodeLengths(internalNode.rightChild, depth + 1);
		} 
		else if (node.constructor.name == "Leaf") 
		{
			var symbol = node.symbol;
			if (codeLengths[symbol] != 0)
			{
				throw "Symbol has more than one code"; 
			}
			if (symbol >= codeLengths.length)
			{
				throw "Symbol exceeds symbol limit";
			}
			this.codeLengths[symbol] = depth;
		} 
		else 
		{
			throw "Illegal node type";
		}
	}

	CanonicalCode.prototype.getSymbolLimit = function() 
	{
		return this.codeLengths.length;
	}

	CanonicalCode.prototype.getCodeLength = function(symbol) 
	{
		if (symbol < 0 || symbol >= this.codeLengths.length)
		{
			throw "Symbol out of range";
		}
		return this.codeLengths[symbol];
	}

	CanonicalCode.prototype.toCodeTree = function() 
	{
		var nodes = [];
		for (var i = this.max(this.codeLengths); i >= 1; i--) 
		{  
			// Descend through positive code lengths
			var newNodes = [];

			// Add leaves for symbols with code length i
			for (var j = 0; j < this.codeLengths.length; j++) {
				if (this.codeLengths[j] == i)
					newNodes.push(new Leaf(j));
			}

			// Merge nodes from the previous deeper layer
			for (var j = 0; j < nodes.length; j += 2)
			{
				newNodes.push(new InternalNode(nodes[j], nodes[j + 1]));
			}

			nodes = newNodes;
			if (nodes.length % 2 != 0)
			{
				throw "This canonical code does not represent a Huffman code tree";
			}
		}

		if (nodes.length != 2)
		{
			throw "This canonical code does not represent a Huffman code tree";
		}
		return new CodeTree(new InternalNode(nodes[0], nodes[1]), this.codeLengths.length);
	}

	CanonicalCode.prototype.max = function(array) 
	{
		var result = array[0];
		for (var i = 0; i < array.length; i++)
		{
			var x = array[i];
			result = Math.max(x, result);
		}
		return result;
	}	
}

function CircularDictionary(size)
{
	this.data = new Array(size);
	this.index = 0;

	if (IntegerMath.isPowerOf2(size))
	{
		this.mask = size - 1;
	}
	else
	{
		this.mask = 0;
	}
}
{
	CircularDictionary.prototype.append = function(b) 
	{
		this.data[this.index] = b;
		if (this.mask != 0)
			this.index = (this.index + 1) & this.mask;
		else
			this.index = (this.index + 1) % this.data.length;
	}

	CircularDictionary.prototype.copy = function(dist, len, out)
	{
		if (len < 0 || dist < 1 || dist > this.data.length)
		{
			throw "IllegalArgumentException";
		}

		if (this.mask != 0) 
		{
			var readIndex = (this.index - dist + this.data.length) & this.mask;
			for (var i = 0; i < len; i++) 
			{
				out.write(this.data[readIndex]);
				this.data[this.index] = this.data[readIndex];
				readIndex = (readIndex + 1) & this.mask;
				this.index = (this.index + 1) & this.mask;
			}
		} 
		else 
		{
			var readIndex = (this.index - dist + this.data.length) % this.data.length;
			for (var i = 0; i < len; i++) 
			{
				out.write(this.data[readIndex]);
				this.data[this.index] = this.data[readIndex];
				readIndex = (readIndex + 1) % this.data.length;
				this.index = (this.index + 1) % this.data.length;
			}
		}
	}	
}

function CodeTree(root, symbolLimit)
{
	// public final InternalNode root;  // Not null

	// Stores the code for each symbol, or null if the symbol has no code.
	// For example, if symbol 5 has code 10011, then codes.get(5) is the list [1, 0, 0, 1, 1].
	// private List<List<Integer>> codes;

	// Every symbol in the tree 'root' must be strictly less than 'symbolLimit'.

	if (root == null)
	{
		throw "Argument is null";
	}
	this.root = root;

	this.codes = [];  // Initially all null
	for (var i = 0; i < symbolLimit; i++)
	{
		this.codes.push(null);
	}

	this.buildCodeList(root, []);  // Fills 'codes' with appropriate data
}
{
	CodeTree.prototype.buildCodeList = function(node, prefix) 
	{
		var nodeConstructorName = node.constructor.name;
		if (nodeConstructorName == "InternalNode") 
		{
			var internalNode = node;

			prefix.push(0);
			this.buildCodeList(internalNode.leftChild , prefix);
			prefix.removeAt(prefix.length - 1);

			prefix.push(1);
			this.buildCodeList(internalNode.rightChild, prefix);
			prefix.removeAt(prefix.length - 1);	
		} 
		else if (nodeConstructorName == "Leaf") 
		{
			var leaf = node;
			if (leaf.symbol >= this.codes.length)
			{
				throw "Symbol exceeds symbol limit";
			}
			if (this.codes[leaf.symbol] != null)
			{
				throw "Symbol has more than one code";
			}
			this.codes[leaf.symbol] = new Array(prefix); // ?
		} 
		else 
		{
			throw "Illegal node type";
		}
	}

	CodeTree.prototype.getCode = function(symbol) 
	{
		if (symbol < 0)
		{
			throw new "Illegal symbol";
		}
		else if (this.codes[symbol] == null)
		{
			throw "No code for given symbol";
		}
		else
		{
			return this.codes[symbol];
		}
	}

	// Returns a string showing all the codes in this tree. The format is subject to change. Useful for debugging.
	CodeTree.prototype.toString = function() 
	{
		var sb = "";
		sb = this.toString2("", root, sb);
		return sb;
	}

	CodeTree.prototype.toString2 = function(prefix, node, sb) 
	{
		var nodeConstructorName = node.constructor.name;
		if (nodeConstructorName == "InternalNode") 
		{
			var internalNode = node;
			sb = this.toString2(prefix + "0", internalNode.leftChild , sb);
			sb = this.toString2(prefix + "1", internalNode.rightChild, sb);
		} 
		else if (nodeConstructorName == "Leaf") 
		{
			sb += String.format("Code %s: Symbol %d%n", prefix, node.symbol);
		} 
		else 
		{
			throw "Illegal node type";
		}

		return sb;
	}	
}

function InternalNode(leftChild, rightChild)
{
	if (leftChild == null || rightChild == null)
	{
		throw "Argument is null";
	}
	this.leftChild = leftChild;
	this.rightChild = rightChild;
}

function Leaf(symbol)
{	
	if (symbol < 0)
		throw "Illegal symbol value";
	this.symbol = symbol;
}

function Decompressor(input)
{
	// These were formerly static variables.

	var llcodelens = new Array(288);
	llcodelens.fill(8, 0, 144);
	llcodelens.fill(9, 144, 256);
	llcodelens.fill(7, 256, 280);
	llcodelens.fill(8, 280, 288);
	this.llcodelens = llcodelens;

	this.fixedLiteralLengthCode = new CanonicalCode(llcodelens).toCodeTree();
	this.distcodelens = new Array(32);

	this.distcodelens.fill(5);
	this.fixedDistanceCode = new CanonicalCode(this.distcodelens).toCodeTree();

	// The original constructor picks up here.

	this.input = input;
	this.output = new BitStream([]);
	this.dictionary = new CircularDictionary(32 * 1024);

	// Process the stream of blocks
	while (true) 
	{
		// Block header
		var isFinal = (input.readBit() == 1);  // bfinal
		var type = input.readIntegerOfBitWidthLE(2); // btype

		// Decompress by type
		if (type == 0)
		{
			this.decompressUncompressedBlock();
		}
		else if (type == 1 || type == 2) 
		{
			var litLenCode, distCode;
			if (type == 1) 
			{
				litLenCode = this.fixedLiteralLengthCode;
				distCode = this.fixedDistanceCode;
			} 
			else 
			{
				var temp = this.decodeHuffmanCodes(input);
				litLenCode = temp[0];
				distCode = temp[1];
			}

			this.decompressHuffmanBlock(litLenCode, distCode);	
		} 
		else if (type == 3)
		{
			throw "Invalid block type";
		}
		else
		{
			throw "AssertionError";
		}

		if (isFinal)
		{
			break;
		}
	}
}
{	
	// static methods

	/* Public method */
	Decompressor.decompress = function(input)
	{
		var decompressor = new Decompressor(input);
		return decompressor.output.bytes;
	}

	// For handling static Huffman codes (btype = 1)
	Decompressor.fixedLiteralLengthCode = null; // todo
	Decompressor.fixedDistanceCode = null; // todo

	// For handling dynamic Huffman codes (btype = 2)
	Decompressor.prototype.decodeHuffmanCodes = function(input)
	{
		var numLitLenCodes = input.readIntegerOfBitWidthLE(5) + 257;  // hlit  + 257
		var numDistCodes = input.readIntegerOfBitWidthLE(5) + 1;      // hdist +   1

		var numCodeLenCodes = input.readIntegerOfBitWidthLE(4) + 4;   // hclen +   4
		var codeLenCodeLen = new Array(19);
		codeLenCodeLen[16] = input.readIntegerOfBitWidthLE(3);
		codeLenCodeLen[17] = input.readIntegerOfBitWidthLE(3);
		codeLenCodeLen[18] = input.readIntegerOfBitWidthLE(3);
		codeLenCodeLen[ 0] = input.readIntegerOfBitWidthLE(3);
		for (var i = 0; i < numCodeLenCodes - 4; i++) 
		{
			if (i % 2 == 0)
				codeLenCodeLen[8 + i / 2] = input.readIntegerOfBitWidthLE(3);
			else
				codeLenCodeLen[7 - i / 2] = input.readIntegerOfBitWidthLE(3);
		}
		var codeLenCode = new CanonicalCode(codeLenCodeLen).toCodeTree();

		var codeLens = new Array(numLitLenCodes + numDistCodes);
		var runVal = -1;
		var runLen = 0;
		for (var i = 0; i < codeLens.length; i++) 
		{
			if (runLen > 0) 
			{
				codeLens[i] = runVal;
				runLen--;	
			} 
			else 
			{
				var sym = this.decodeSymbol(codeLenCode);
				if (sym < 16) 
				{
					codeLens[i] = sym;
					runVal = sym;
				} 
				else 
				{
					if (sym == 16) 
					{
						if (runVal == -1)
						{
							throw "No code length value to copy";
						}
						runLen = input.readIntegerOfBitWidthLE(2) + 3;
					} 
					else if (sym == 17) 
					{
						runVal = 0;
						runLen = input.readIntegerOfBitWidthLE(3) + 3;
					} 
					else if (sym == 18) 
					{
						runVal = 0;
						runLen = input.readIntegerOfBitWidthLE(7) + 11;
					} 
					else
					{
						throw "AssertionError";
					}

					i--;
				}
			}
		}
		if (runLen > 0)
		{
			throw "Run exceeds number of codes";
		}

		// Create code trees
		var litLenCodeLen = codeLens.slice(0, numLitLenCodes); // ?
		var litLenCode = new CanonicalCode(litLenCodeLen).toCodeTree();

		var distCodeLen = codeLens.slice(numLitLenCodes, codeLens.length); // ?
		var distCode;
		if (distCodeLen.length == 1 && distCodeLen[0] == 0)
		{
			distCode = null;  // Empty distance code; the block shall be all literal symbols
		}
		else
		{
			distCode = new CanonicalCode(distCodeLen).toCodeTree();
		}

		return [litLenCode, distCode];
	}

	/* Block decompression methods */

	Decompressor.prototype.decompressUncompressedBlock = function()
	{
		// Discard bits to align to byte boundary
		this.input.alignWithByteBoundary();

		// Read length
		var len  = this.input.readInteger16LE();
		var nlen = this.input.readInteger16LE();
		var nlenCalculated = len ^ 0xFFFF; 
		if (nlenCalculated != nlen) 
		{
			throw "Invalid length in uncompressed block";
		}

		// Copy bytes
		for (var i = 0; i < len; i++) 
		{
			var temp = this.input.readByte();
			if (this.input.hasMoreBits() == false)
			{
				throw "EOFException";
			}
			this.output.writeByte(temp);
			this.dictionary.append(temp);
		}
	}

	Decompressor.prototype.decompressHuffmanBlock = function(litLenCode, distCode)
	{
		if (litLenCode == null)
		{
			throw "NullPointerException";
		}

		while (true) 
		{
			var sym = this.decodeSymbol(litLenCode);
			if (sym == 256)  // End of block
			{
				break;
			}

			if (sym < 256) 
			{  
				// Literal byte

				// Leaving the next line in
				// causes strange junk to appear 
				// at the end of the uncompressed data.
				// Not sure what's going on.
				//this.output.writeByte(sym);

				this.dictionary.append(sym);
			} 
			else 
			{  // Length and distance for copying
				var len = this.decodeRunLength(sym);
				if (distCode == null)
				{
					throw "Length symbol encountered with empty distance code";
				}
				var distSym = this.decodeSymbol(distCode);
				var dist = this.decodeDistance(distSym);
				this.dictionary.copy(dist, len, output);
			}
		}
	}

	/* Symbol decoding methods */
	Decompressor.prototype.decodeSymbol = function(code)
	{
		var currentNode = code.root;
		while (true) 
		{
			var temp = this.input.readBit();
			var nextNode;
			if (temp == 0)
			{			
				nextNode = currentNode.leftChild;
			}
			else if (temp == 1)
			{
				nextNode = currentNode.rightChild;
			}
			else
			{
				throw "AssertionError";
			}

			var nextNodeConstructorName = nextNode.constructor.name;
			if (nextNodeConstructorName == "Leaf")
			{
				return nextNode.symbol;
			}
			else if (nextNodeConstructorName == "InternalNode")
			{
				currentNode = nextNode;
			}
			else
			{
				throw "AssertionError";
			}
		}
	}

	Decompressor.prototype.decodeRunLength = function(sym)
	{
		if (sym < 257 || sym > 285)
		{
			throw "Invalid run length symbol: " + sym;
		}
		else if (sym <= 264)
		{
			return sym - 254;
		}
		else if (sym <= 284) 
		{
			var i = (sym - 261) / 4;  // Number of extra bits to read
			return (((sym - 265) % 4 + 4) << i) + 3 + input.readIntegerOfBitWidthLE(i);
		} 
		else  // sym == 285
		{
			return 258;
		}
	}

	Decompressor.prototype.decodeDistance = function(sym)
	{
		if (sym <= 3)
		{
			return sym + 1;
		}
		else if (sym <= 29) 
		{
			var i = sym / 2 - 1;  // Number of extra bits to read
			return ((sym % 2 + 2) << i) + 1 + input.readIntegerOfBitWidthLE(i);
		} 
		else
		{
			throw "Invalid distance symbol: " + sym;
		}
	}
}

function IntegerMath()
{
	// static class
}
{
	IntegerMath.isPowerOf2 = function(valueToCheck)
	{
		return ((Math.log(valueToCheck) / Math.log(2)) % 0) == 0;
	}
}

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

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

Rendering to Texture with WebGL

The JavaScript program below, when run, will display a simple three-dimensional scene rendered with WebGL. It is nearly identical to a program from a previous post, except that this version demonstrates how to render a scene to a texture, rather than directly to the screen. This texture can then be used to texture another mesh within the scene.

This technique of “rendering to texture” is frequently used to model some advanced effects in a 3D scene, such as representing television screens or mirrors. It is also used as the first step in “shadow mapping”, which is a means of adding shadows to a scene by “rendering” it from the point of view of each light source, into a special texture called a “depth map”, and then using that depth map to determine whether a particular pixel in the camera’s view is in shadow or not. This code does not attempt to perform shadow mapping, though it is intended as a first step in that direction.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Use the W, A, S, D, Z, C, E and R keys to move the camera around, and press the Enter key to create a snapshot of the current view, which will then be added as another object in the scene.

UPDATE 2017/02/10 – Adapting this code to do shadow mapping has proven at least as difficult as I imagined. In the face of this continuing difficulty, I have refactored this code somewhat to make debugging easier. Notably, I have added an option to also render the scene as a simple wireframe without using WebGL for comparison purposes, added some further abstraction around the WebGL shader functionality, and simplified the code that builds the camera’s perspective matrix for WebGL.

webglscenerenderedtotexture


<html>
<body>

<script type="text/javascript">

// main

function WebGLTest()
{
	this.main = function()
	{
		var imageTextGL = ImageHelper.buildImageFromStrings
		(
			"ImageTextGL",
			8, // scaleMultiplier
			[
				"RRRRRRRRRRRRRRRR",
				"RRcccccRcRRRRRcR",
				"RRcRRRRRcRRRRRcR",
				"RRcRRRRRcRRRRRcR",
				"RRcRcccRcRRRRRcR",
				"RRcRRRcRcRRRRRRR",
				"RRcccccRcccccRcR",
				"RRRRRRRRRRRRRRRR",
			]
		);

		Globals.Instance.mediaHelper.loadImages
		(
			[ imageTextGL ],
			this.main2
		);
	}

	this.main2 = function(event)
	{
		var mediaHelper = Globals.Instance.mediaHelper;

		var displaySize = new Coords(320, 240, 2000);

		var scene = new DemoData().scene(mediaHelper, displaySize);

		var displays = 
		[
			//new Display2D(displaySize),
			new DisplayWebGL(displaySize),
		];

		Globals.Instance.initialize
		(
			displays,
			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;
	}

	Array.prototype.append = function(other)
	{
		for (var i = 0; i < other.length; i++)
		{
			var element = other[i];
			this.push(element);
		}

		return this;
	}

	// cloneable

	Array.prototype.clone = function()
	{
		var elementsCloned = [];

		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var elementCloned = element.clone();
			elementsCloned.push(elementCloned);
		}

		return elementsCloned;
	}

	Array.prototype.overwriteWith = function(other)
	{
		for (var i = 0; i < this.length; i++)
		{
			var elementThis = this[i];
			var elementOther = other[i];
			elementThis.overwriteWith(elementOther);
		}

		return this;
	}
}

// classes

function Body(name, defn, pos, orientation)
{
	this.name = name;
	this.defn = defn;
	this.pos = pos;
	this.orientation = orientation;
}
{
	Body.prototype.meshTransformed = function()
	{
		if (this._meshTransformed == null)
		{
			this._meshTransformed = this.defn.mesh.clone();

			this.transformLocate = new TransformMultiple
			([
				new TransformOrient(this.orientation),
				new TransformTranslate(this.pos),
			])
		}
		else
		{
			this._meshTransformed.overwriteWith(this.defn.mesh);
		}

		Transform.transformApplyToCoordsMany
		(
			this.transformLocate,
			this._meshTransformed.vertexPositions
		);

		return this._meshTransformed;
	}
	
	// WebGL

	Body.prototype.drawToWebGLContext = function(webGLContext, scene)
	{
		this.defn.mesh.drawToWebGLContext(webGLContext, scene);
	}
}

function BodyDefn(name, mesh)
{
	this.name = name;
	this.mesh = mesh;
}

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

	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}
{
	Camera.prototype.toString = function()
	{
		var returnValue = "<Camera "
			+ "pos='" + this.pos.toString() + "' "
			+ ">" 
			+ this.orientation.toString()
			+ "</Camera>";

		return returnValue;
	}
}

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

	Color.NumberOfComponentsRGBA = 4;

	// instances

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

		this.Black 	= new Color("Black",	"k", "Black", [0, 0, 0, 1]);
		this.Blue 	= new Color("Blue", 	"b", "Blue", [0, 0, 1, 1]);
		this.Cyan 	= new Color("Cyan", 	"c", "Cyan", [0, 1, 1, 1]);
		this.Gray 	= new Color("Gray", 	"a", "Gray", [.5, .5, .5, 1]);
		this.Green 	= new Color("Green", 	"g", "Green", [0, 1, 0, 1]);
		this.GreenDark 	= new Color("GreenDark", "G", "#008000", [0, .5, 0, 1]);
		this.Orange 	= new Color("Orange", 	"o", "Orange", [1, .5, 0, 1]);
		this.Red 	= new Color("Red", 	"r", "Red", [1, 0, 0, 1]);
		this.RedDark 	= new Color("RedDark", 	"R", "#800000", [.5, 0, 0, 1]);
		this.Violet 	= new Color("Violet", 	"v", "Violet", [1, 0, 1, 1]);
		this.White 	= new Color("White", 	"w", "White", [1, 1, 1, 1]);
		this.Yellow 	= new Color("Yellow", 	"y", "Yellow", [1, 1, 0, 1]);

		this._All = 
		[
			this.Transparent,

			this.Blue,
			this.Black,
			this.Cyan,
			this.Gray,
			this.Green,
			this.GreenDark,
			this.Orange,
			this.Red,
			this.RedDark,
			this.Violet,
			this.White,
			this.Yellow,

		].addLookups("codeChar");
	}

	Color.Instances = new Color_Instances();

}

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

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

	Coords.NumberOfDimensions = 3;

	// instances

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

	Coords.Instances = new Coords_Instances();

	// 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)
	{
		return 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
		);

	}

	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.toWebGLArray = function()
	{
		var returnValues = new Float32Array(this.dimensionValues());

		return returnValues;
	}
}

function Display2D(size)
{
	this.size = size;

	this.colorFore = "LightGray";
	this.colorBack = "White";

	// helper variables

	this.drawPositions = 
	[
		new Coords(), new Coords(), new Coords(), new Coords()
	];
}
{
	Display2D.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		document.body.appendChild(canvas);

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

	Display2D.prototype.sceneInitialize = function(scene)
	{
		// do nothing
	}

	// drawing

	Display2D.prototype.clear = function()
	{
		this.drawRectangle
		(
			Coords.Instances.Zeroes, 
			this.size,
			this.colorBack, // colorFill
			this.colorFore
		)
	}

	Display2D.prototype.drawFace = function(camera, mesh, face)
	{
		var drawPositions = this.drawPositions;
		var cameraPos = camera.pos;
		var cameraOrientation = camera.orientation;
		var cameraFocalLength = camera.focalLength;

		var vertexIndices = face.vertexIndices;
		for (var i = 0; i < vertexIndices.length; i++)
		{
			var vertexIndex = vertexIndices[i];
			var vertex = mesh.vertexPositions[vertexIndex];

			var drawPos = drawPositions[i];

			drawPos.overwriteWith
			(
				vertex
			).subtract
			(
				cameraPos
			);

			cameraOrientation.projectCoords
			(
				drawPos
			);

			if (drawPos.z <= 0)
			{
				return; // hack
			}

			drawPos.multiplyScalar
			(
				cameraFocalLength / drawPos.z
			).add
			(
				camera.viewSizeHalf
			);

			var debug = "debug";
		}

		this.drawPolygon(drawPositions, null, this.colorFore);
	}

	Display2D.prototype.drawMesh = function(camera, mesh)
	{
		var faces = mesh.faces;
		for (var i = 0; i < faces.length; i++)
		{
			var face = faces[i];
			this.drawFace(camera, mesh, face);
		}
	}

	Display2D.prototype.drawPolygon = function(vertices, colorFill, colorBorder)
	{
		this.graphics.beginPath();

		var vertex = vertices[0];
		this.graphics.moveTo(vertex.x, vertex.y);

		for (var i = 1; i < vertices.length; i++)
		{
			vertex = vertices[i];
			this.graphics.lineTo(vertex.x, vertex.y);
		}
	
		this.graphics.closePath();

		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fill();
		}

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorFill;
			this.graphics.stroke();
		}
	}

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

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

	Display2D.prototype.drawScene = function(scene)
	{
		this.clear();

		var camera = scene.camera;
		var bodies = scene.bodies;

		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];
			var mesh = body.meshTransformed();
			this.drawMesh(camera, mesh);
		}
	}

}

function DisplayWebGL(size)
{
	this.size = size;
}
{
	DisplayWebGL.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		document.body.appendChild(canvas);

		this.webGLContext = new WebGLContext(canvas);
	}

	DisplayWebGL.prototype.sceneInitialize = function(scene)
	{
		var textures = scene.textures;

		for (var i = 0; i < textures.length; i++)
		{
			var texture = textures[i];
			this.textureInitialize(texture);
		}
	}

	DisplayWebGL.prototype.textureInitialize = function(texture)
	{
		var gl = this.webGLContext.gl;

		var systemTexture = gl.createTexture();
		texture.systemTexture = systemTexture;

		gl.bindTexture(gl.TEXTURE_2D, systemTexture);
		//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
		gl.texImage2D
		(
			gl.TEXTURE_2D, 
			0, 
			gl.RGBA, 
			gl.RGBA, 
			gl.UNSIGNED_BYTE, 
			texture.image.systemImage
		);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
		gl.bindTexture(gl.TEXTURE_2D, null);
	}

	// drawing

	DisplayWebGL.prototype.drawScene = function(scene)
	{
		scene.drawToWebGLContext
		(
			this.webGLContext
		);
	}
}

function Face(vertexIndices, material, textureUVs, vertexNormals)
{
	this.vertexIndices = vertexIndices;
	this.material = material;
	this.textureUVs = textureUVs;
	this.vertexNormals = vertexNormals;
}
{
	// constants

	Face.VertexIndexIndicesForQuad = [ [ 0, 1, 2 ], [ 0, 2, 3 ] ];
	Face.VertexIndexIndicesForTriangle = [ [ 0, 1, 2 ] ];

	// methods

	Face.prototype.plane = function(mesh)
	{
		var returnValue = new Plane(this.vertices(mesh));
		return returnValue;
	}

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

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

		return returnValues;
	}

	// cloneable

	Face.prototype.clone = function()
	{
		return new Face
		(
			this.vertexIndices, // hack
			this.material, // hack
			(this.textureUVS == null ? null : this.textureUVs.clone()),
			(this.vertexNormals == null ? null : this.vertexNormals.clone())
		);
	}

	// WebGL

	Face.prototype.drawToWebGLContext = function
	(
		webGLContext, 
		scene, 
		mesh,
		vertexPositionsAsFloatArray,
		vertexColorsAsFloatArray,
		vertexNormalsAsFloatArray,
		vertexTextureUVsAsFloatArray,		
		numberOfTrianglesSoFarWrapped
	)
	{
		var face = this;
		var facePlane = face.plane(mesh);
		var faceNormal = facePlane.normal;
		var faceMaterial = face.material;
		var faceVertexNormals = face.vertexNormals;
		var faceColor = (faceMaterial == null ? Color.Instances.Violet : faceMaterial.color);

		var numberOfVerticesInFace = face.vertexIndices.length;

		var vertexIndexIndicesForChildTriangles = 
			(numberOfVerticesInFace == 4) 
			? Face.VertexIndexIndicesForQuad
			: Face.VertexIndexIndicesForTriangle

		for (var t = 0; t < vertexIndexIndicesForChildTriangles.length; t++)
		{
			var vertexIndexIndicesForChildTriangle = 
				vertexIndexIndicesForChildTriangles[t];

			for (var vii = 0; vii < vertexIndexIndicesForChildTriangle.length; vii++)
			{
				var vertexIndexIndex = 
					vertexIndexIndicesForChildTriangle[vii];
				var vertexIndex = face.vertexIndices[vertexIndexIndex];
				var vertexPosition = mesh.vertexPositions[vertexIndex];

				vertexPositionsAsFloatArray.append
				(
					vertexPosition.dimensionValues()
				);

				vertexColorsAsFloatArray.append
				(
					faceColor.componentsRGBA
				);

				var vertexNormal = 
				(
					faceVertexNormals == null 
					? faceNormal
					: faceVertexNormals[vertexIndex]
				);

				vertexNormalsAsFloatArray.append
				(
					vertexNormal.dimensionValues()
				);

				var vertexTextureUV = 
				(
					face.textureUVs == null 
					? Coords.Instances.OnesNegative
					: face.textureUVs[vertexIndexIndex]
				);

				vertexTextureUVsAsFloatArray.append
				(
					[
						vertexTextureUV.x,
						vertexTextureUV.y
					]
				);
			}
		}

		numberOfTrianglesSoFarWrapped.value 
			+= vertexIndexIndicesForChildTriangles.length;
	}
}

function Globals()
{
	this.mediaHelper = new MediaHelper();
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(displays, scene)
	{
		this.displays = displays;
		this.scene = scene;
	
		for (var i = 0; i < this.displays.length; i++)
		{
			var display = this.displays[i];
			display.initialize();
			display.sceneInitialize(scene);
		}

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();

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

function Image(name, systemImage)
{
	this.name = name;
	this.systemImage = systemImage;
	this.filePath = this.systemImage.src;
}
{
}

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 imageFromCanvasURL = canvas.toDataURL("image/png");
		var htmlImageFromCanvas = document.createElement("img");

		htmlImageFromCanvas.width = canvas.width;
		htmlImageFromCanvas.height = canvas.height;
		htmlImageFromCanvas.isLoaded = false;
		htmlImageFromCanvas.onload = function(event) 
		{ 
			event.target.isLoaded = true; 
		}
		htmlImageFromCanvas.src = imageFromCanvasURL;

		var returnValue = new Image(name, htmlImageFromCanvas);

		// hack
		// WebGL doesn't support images from DataURLs?
		returnValue.canvas = canvas;

		return returnValue;
	}
}

function InputHelper()
{
	this.tempCoords = new Coords();
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.processKeyDownEvent.bind(this);
	}

	InputHelper.prototype.processKeyDownEvent = function(event)
	{
		var scene = Globals.Instance.scene
		var camera = scene.camera;

		var key = event.key.toLowerCase();

		var distanceToMove = 10;
		var amountToTurn = .05;
		var cameraOrientation = camera.orientation;

		if (key == "a")
		{
			// move left
			camera.pos.subtract
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.right
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "d")
		{
			// move right
			camera.pos.add
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.right
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "e")
		{
			// roll right
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward,
				cameraOrientation.down.add
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				)
			);

		}
		else if (key == "f")
		{
			// fall
			camera.pos.subtract
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.down
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "q")
		{
			// roll left
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward,
				cameraOrientation.down.subtract
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				)
			);

		}
		else if (key == "r")
		{
			// rise
			camera.pos.add
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.down
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "s")
		{
			// move back
			camera.pos.subtract
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.forward
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "w")
		{
			// move forward
			camera.pos.add
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.forward
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "z")
		{
			// turn left
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward.subtract
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				),
				cameraOrientation.down
			);
		}
		else if (key == "c")
		{
			// turn right
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward.add
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				),
				cameraOrientation.down
			);
		}
		else if (key == "x")
		{
			// cancel roll
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward,
				new Coords(0, 0, 1)
			);
		}
		else if (key == "enter")
		{
			// take a snapshot
			scene.snapshotCreate(scene);
		}

		var displays = Globals.Instance.displays;
		for (var i = 0; i < displays.length; i++)
		{
			var display = displays[i];
			scene.drawToDisplay(display);
		}
	}
}

function Lighting(ambientIntensity, direction, directionalIntensity)
{
	this.ambientIntensity = ambientIntensity;
	this.direction = direction.clone().normalize();
	this.directionalIntensity = directionalIntensity;
}

function Material(name, color)
{
	this.name = name;
	this.color = color;
}
{	
	function Material_Instances()
	{
		var colors = Color.Instances;

		this.Blue 	= new Material("Blue", 	colors.Blue);
		this.Cyan 	= new Material("Cyan", 	colors.Cyan);
		this.Green 	= new Material("Green", colors.Green);
		this.GreenDark 	= new Material("GreenDark", colors.GreenDark);
		this.Orange 	= new Material("Orange", colors.Orange);
		this.Red 	= new Material("Red", 	colors.Red);
		this.Violet 	= new Material("Violet", colors.Violet);
		this.Yellow 	= new Material("Yellow", colors.Yellow);
		this.White 	= new Material("White", colors.White);
	}

	Material.Instances = new Material_Instances();
}

function Matrix(values)
{
	this.values = values;
}
{
	// static methods

	Matrix.buildZeroes = function()
	{
		var returnValue = new Matrix
		([
			0, 0, 0, 0,
			0, 0, 0, 0,
			0, 0, 0, 0,
			0, 0, 0, 0,
		]);

		return returnValue;
	}

	// instance methods

	Matrix.prototype.clone = function()
	{
		var valuesCloned = [];

		for (var i = 0; i < this.values.length; i++)
		{
			valuesCloned[i] = this.values[i];
		}

		var returnValue = new Matrix(valuesCloned);

		return returnValue;
	}

	Matrix.prototype.divideScalar = function(scalar)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] /= scalar;
		}

		return this;
	}

	Matrix.prototype.multiply = function(other)
	{
		// hack
		// Instantiates a new matrix.

		var valuesMultiplied = [];

		for (var y = 0; y < 4; y++)
		{
			for (var x = 0; x < 4; x++)
			{
				var valueSoFar = 0;

				for (var i = 0; i < 4; i++)
				{
					// This appears backwards,
					// but the other way doesn't work?
					valueSoFar += 
						other.values[y * 4 + i] 
						* this.values[i * 4 + x];
				}

				valuesMultiplied[y * 4 + x] = valueSoFar;
			}
		}

		this.overwriteWithValues(valuesMultiplied);

		return this;
	}

	Matrix.prototype.multiplyScalar = function(scalar)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] *= scalar;
		}

		return this;
	}

	Matrix.prototype.overwriteWith = function(other)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] = other.values[i];
		}

		return this;
	}

	Matrix.prototype.overwriteWithOrientationBody = function(orientation)
	{
		var forward = orientation.forward;
		var right = orientation.right;
		var down = orientation.down;

		this.overwriteWithValues
		([
			right.x, 	down.x, 	forward.x, 	0,
			right.y, 	down.y,		forward.y, 	0,
			right.z, 	down.z, 	forward.z, 	0,
			0, 		0, 		0, 		1,
		]);


		return this;
	}

	Matrix.prototype.overwriteWithOrientationCamera = function(orientation)
	{
		var forward = orientation.forward;
		var right = orientation.right;
		var down = orientation.down;

		this.overwriteWithValues
		([

			right.x, 	right.y, 	right.z, 	0,
			0-down.x, 	0-down.y,	0-down.z, 	0,
			0-forward.x, 	0-forward.y, 	0-forward.z, 	0,
			0, 		0, 		0, 		1,
		]);
	

		return this;
	}

	Matrix.prototype.overwriteWithPerspectiveForCamera = function(camera) 
	{
		var viewSize = camera.viewSize;
		var y = camera.focalLength / (viewSize.y / 2);
		var x = camera.focalLength / (viewSize.x / 2);
		var zNear = .001;
		var zFar = viewSize.z;
		var zRange = zNear - zFar; // Yes, it's negative.

		this.overwriteWithValues
		([
			x, 	0,	0,			0,
			0,	y, 	0, 			0,
			0, 	0,	(zNear+zFar)/zRange,	(2*zFar*zNear)/zRange,
			0,	0,	-1,			0,
		]);

		return this;
	}

	Matrix.prototype.overwriteWithRotate = function(axisToRotateAround, radiansToRotate)
	{
		var x = axisToRotateAround.x;
		var y = axisToRotateAround.y;
		var z = axisToRotateAround.z;

		var cosine = Math.cos(radiansToRotate);
		var sine = Math.sin(radiansToRotate);
		var cosineReversed = 1 - cosine;

		var xSine = x * sine;
		var ySine = y * sine;
		var zSine = z * sine;

		var xCosineReversed = x * cosineReversed;
		var yCosineReversed = y * cosineReversed;
		var zCosineReversed = z * cosineReversed;

		var xyCosineReversed = x * yCosineReversed;
		var xzCosineReversed = x * zCosineReversed;
		var yzCosineReversed = y * zCosineReversed;

		this.overwriteWithValues
		([
			(x * xCosineReversed + cosine), 	(xyCosineReversed + z * sine), 		(xzCosineReversed - ySine), 	0,
			(xyCosineReversed - zSine), 		(y * yCosineReversed + cosine), 	(yzCosineReversed + xSine), 	0,
			(xzCosineReversed + ySine), 		(yzCosineReversed - xSine), 		(z * zCosineReversed + cosine), 0,
			0,					0, 					0, 				1,
		]);

		return this;
	}

	Matrix.prototype.overwriteWithScale = function(scaleFactors)
	{
		this.overwriteWithValues
		([
			scaleFactors.x,	0, 		0, 		0,
			0, 		scaleFactors.y, 0, 		0,
			0, 		0, 		scaleFactors.z, 0,
			0, 		0, 		0, 		1,

		]);

		return this;
	}

	Matrix.prototype.overwriteWithTranslate = function(displacement)
	{
		this.overwriteWithValues
		([
			1, 0, 0, displacement.x,
			0, 1, 0, displacement.y,
			0, 0, 1, displacement.z,
			0, 0, 0, 1,

		]);

		return this;
	}

	Matrix.prototype.overwriteWithValues = function(otherValues)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] = otherValues[i];
		}

		return this;
	}

	Matrix.prototype.toWebGLArray = function()
	{
		var returnValues = [];

		for (var x = 0; x < 4; x++)
		{
			for (var y = 0; y < 4; y++)
			{
				returnValues.push(this.values[(y * 4 + x)]);
			}
		}

		var returnValues = new Float32Array(returnValues);

		return returnValues;
	}
}

function MediaHelper()
{
	this.images = [];
}
{
	MediaHelper.prototype.loadImages = function
	(
		imagesToLoad,
		methodToCallWhenAllImagesLoaded
	)
	{
		for (var i = 0; i < imagesToLoad.length; i++)
		{
			var imageToLoad = imagesToLoad[i];

			this.images.push(imageToLoad);
			this.images[imageToLoad.name] = imageToLoad;
		}

		this.methodToCallWhenAllImagesLoaded = methodToCallWhenAllImagesLoaded;	

		setTimeout
		(
			this.checkWhetherAllImagesAreLoaded, 
			100
		);
	}

	MediaHelper.prototype.checkWhetherAllImagesAreLoaded = function()
	{
		var mediaHelper = Globals.Instance.mediaHelper;

		var numberOfImagesLeftToLoad = 0;

		for (var i = 0; i < mediaHelper.images.length; i++)
		{
			var image = mediaHelper.images[i];
			if (image.systemImage.isLoaded == false)
			{
				numberOfImagesLeftToLoad++;
			}
		}	

		if (numberOfImagesLeftToLoad > 0)
		{
			setTimeout
			(
				mediaHelper.checkWhetherAllImagesAreLoaded, 
				100
			);
		}
		else
		{
			mediaHelper.methodToCallWhenAllImagesLoaded();
		}
	}
}

function Mesh
(
	name, 
	vertexPositions, 
	texture,
	faces
)
{
	this.name = name;
	this.vertexPositions = vertexPositions;
	this.texture = texture;
	this.faces = faces;
}
{
	// constants

	Mesh.VerticesInATriangle = 3;
	Mesh.TextureUVsDefault = 
	[ 
		new Coords(0, 1), 
		new Coords(1, 1), 
		new Coords(1, 0), 
		new Coords(0, 0) 
	];

	// instance methods

	// cloneable

	Mesh.prototype.clone = function()
	{
		return new Mesh
		(
			this.name + "_Cloned",
			this.vertexPositions.clone(),
			(this.texture == null ? null : this.texture.clone())	,
			this.faces.clone()
		);
	}

	Mesh.prototype.overwriteWith = function(other)
	{
		this.vertexPositions.overwriteWith(other.vertexPositions);
	}

	// WebGL

	Mesh.prototype.drawToWebGLContext = function(webGLContext, scene)
	{
		var gl = webGLContext.gl;

		var shader = webGLContext.shader;

		var vertexPositionsAsFloatArray = [];
		var vertexColorsAsFloatArray = [];
		var vertexNormalsAsFloatArray = [];
		var vertexTextureUVsAsFloatArray = [];

		var numberOfTrianglesSoFarWrapped = new NumberWrapper(0);

		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			face.drawToWebGLContext
			(
				webGLContext, 
				scene, 
				this, // mesh
				vertexPositionsAsFloatArray,
				vertexColorsAsFloatArray,
				vertexNormalsAsFloatArray,
				vertexTextureUVsAsFloatArray,
				numberOfTrianglesSoFarWrapped
			);
		}

		var colorBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexColorsAsFloatArray), 
			gl.STATIC_DRAW
		);
		gl.vertexAttribPointer
		(
			shader.inputs["aVertexColor"].location, 
			Color.NumberOfComponentsRGBA, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		var normalBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexNormalsAsFloatArray), 
			gl.STATIC_DRAW
		);		
		gl.vertexAttribPointer
		(
			shader.inputs["aVertexNormal"].location, 
			Coords.NumberOfDimensions, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		var positionBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexPositionsAsFloatArray), 
			gl.STATIC_DRAW
		);
		gl.vertexAttribPointer
		(
			shader.inputs["aVertexPosition"].location,
			Coords.NumberOfDimensions, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		// hack - Prevents some texture-related warnings and errors.
		var textureToBind = (this.texture == null ? scene.textures[0] : this.texture);
		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, textureToBind.systemTexture);

		gl.uniform1i(shader.inputs["uSampler"], 0);

		var textureBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexTextureUVsAsFloatArray), 
			gl.STATIC_DRAW
		);
		gl.vertexAttribPointer
		(
			shader.inputs["aVertexTextureUV"].location, 
			2, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		gl.drawArrays
		(
			gl.TRIANGLES,
			0, 
			numberOfTrianglesSoFarWrapped.value 
				* Mesh.VerticesInATriangle
		);
	}
}

function NumberWrapper(value)
{
	this.value = value;
}

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

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

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

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

	Orientation.prototype.projectCoords = function(coordsToProject)
	{
		coordsToProject.overwriteWithXYZ
		(
			coordsToProject.dotProduct(this.right),
			coordsToProject.dotProduct(this.down),
			coordsToProject.dotProduct(this.forward)
		);
	}

	Orientation.prototype.toString = function()
	{
		var returnValue = "<Orientation "
			+ "forward='" + this.forward.toString() + "' "
			+ "right='" + this.right.toString() + "' "
			+ "down='" + this.down.toString() + "' "
			+ " />";

		return returnValue;			
	}
}

function Plane(positionsOnPlane)
{
	var pos0 = positionsOnPlane[0];
	var displacementFromPos0To1 = Plane.TempCoords0.overwriteWith
	(
		positionsOnPlane[1]
	).subtract
	(
		pos0
	);
	var displacementFromPos0To2 = Plane.TempCoords1.overwriteWith
	(
		positionsOnPlane[2]
	).subtract
	(
		pos0
	);
	var normal = displacementFromPos0To1.clone().crossProduct
	(
		displacementFromPos0To2
	).normalize();

	this.normal = normal;
	this.distanceFromOrigin = this.normal.dotProduct(pos0);
}
{
	// helper variables

	Plane.TempCoords0 = new Coords();
	Plane.TempCoords1 = new Coords();
}

function Scene(name, lighting, camera, textures, bodies)
{
	this.name = name;
	this.lighting = lighting;
	this.camera = camera;
	this.textures = textures;
	this.bodies = bodies;

	// helper variables

	this.matrixBody = Matrix.buildZeroes();
	this.matrixCamera = Matrix.buildZeroes();
	this.matrixOrient = Matrix.buildZeroes();
	this.matrixPerspective = Matrix.buildZeroes();
	this.matrixTranslate = Matrix.buildZeroes();
	this.tempCoords = new Coords();
	this.tempMatrix0 = Matrix.buildZeroes();
	this.tempMatrix1 = Matrix.buildZeroes();
}
{
	Scene.prototype.snapshotCreate = function(scene)
	{
		var textureSize = new Coords(512, 512);

		var webGLContext = Globals.Instance.displays[0].webGLContext;
		var gl = webGLContext.gl;

		var framebuffer = gl.createFramebuffer();
		gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

		var glTexture = gl.createTexture();
		gl.bindTexture(gl.TEXTURE_2D, glTexture);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
		//gl.generateMipmap(gl.TEXTURE_2D);
		gl.texImage2D
		(
			gl.TEXTURE_2D, 
			0, 
			gl.RGBA, 
			textureSize.x, textureSize.y, 
			0, 
			gl.RGBA, 
			gl.UNSIGNED_BYTE, 
			null
		);

		var renderbuffer = gl.createRenderbuffer();
		gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
		gl.renderbufferStorage
		(
			gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, textureSize.x, textureSize.y
		);

		gl.framebufferTexture2D
		(
			gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, glTexture, 0
		);
		gl.framebufferRenderbuffer
		(
			gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer
		);

		this.drawToWebGLContext(webGLContext);

		gl.bindTexture(gl.TEXTURE_2D, null);
		gl.bindRenderbuffer(gl.RENDERBUFFER, null);
		gl.bindFramebuffer(gl.FRAMEBUFFER, null);

		var texture = new Texture("Snapshot", null);
		texture.systemTexture = glTexture;

		var camera = this.camera;
		var meshSize = camera.viewSize.clone().divideScalar(16);
		var meshSizeHalf = meshSize.clone().divideScalar(2);
		var cameraViewOverTextureSize = camera.viewSize.clone().divide(textureSize);

		var meshForSnapshot = new Mesh
		(
			"Snapshot", 
			// vertexPositions
			[
				new Coords(-1, -1, 0).multiply(meshSizeHalf),
				new Coords(1, -1, 0).multiply(meshSizeHalf),
				new Coords(1, 1, 0).multiply(meshSizeHalf),
				new Coords(-1, 1, 0).multiply(meshSizeHalf),
			],
			texture,
			// faces
			[
				new Face
				(
					[0, 1, 2, 3],
					null, // material
					[
						new Coords(0, 1).multiply(cameraViewOverTextureSize),
						new Coords(1, 1).multiply(cameraViewOverTextureSize),
						new Coords(1, 0).multiply(cameraViewOverTextureSize),
						new Coords(0, 0).multiply(cameraViewOverTextureSize),
					]
				),
			]
		);

		var cameraOrientation = camera.orientation;

		var bodyForSnapshot = new Body
		(
			"Snapshot", 
			new BodyDefn("Snapshot", meshForSnapshot),
			camera.pos.clone().add
			(
				cameraOrientation.forward.clone().multiplyScalar
				(
					camera.focalLength / 4
				)
			), 
			cameraOrientation.clone()
		);

		scene.bodies.push(bodyForSnapshot);		
	}

	// draw

	Scene.prototype.drawToDisplay = function(display)
	{
		display.drawScene(this);
	}

	Scene.prototype.drawToWebGLContext = function(webGLContext)
	{
		var gl = webGLContext.gl;
		var shader = webGLContext.shader;

		gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
		gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

		var camera = this.camera;

		var cameraMatrix = this.matrixCamera.overwriteWithTranslate
		(
			this.tempCoords.overwriteWith(camera.pos).multiplyScalar(-1)
		).multiply
		(
			this.matrixOrient.overwriteWithOrientationCamera
			(
				camera.orientation
			)
		).multiply
		(
			this.matrixPerspective.overwriteWithPerspectiveForCamera
			(
				camera
			)
		)

		gl.uniform1f
		(
			shader.inputs["uLightAmbientIntensity"].location,
			this.lighting.ambientIntensity
		);

		gl.uniform3fv
		(
			shader.inputs["uLightDirection"].location, 
			this.lighting.direction.toWebGLArray()
		);

		gl.uniform1f
		(
			shader.inputs["uLightDirectionalIntensity"].location,
			this.lighting.directionalIntensity
		);

		gl.uniformMatrix4fv
		(
			shader.inputs["uCameraMatrix"].location, 
			false, // transpose
			cameraMatrix.toWebGLArray()
		);

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

			var normalMatrix = this.matrixOrient.overwriteWithOrientationBody
			(
				body.orientation
			);

			var bodyMatrix = this.matrixBody.overwriteWith
			(
				normalMatrix
			).multiply
			(
				this.matrixTranslate.overwriteWithTranslate
				(
					body.pos
				)
			);

			gl.uniformMatrix4fv
			(
				shader.inputs["uBodyMatrix"].location, 
				false, // transpose
				bodyMatrix.toWebGLArray()
			);

			gl.uniformMatrix4fv
			(
				shader.inputs["uNormalMatrix"].location, 
				false, // transpose
				normalMatrix.multiplyScalar(-1).toWebGLArray()
			);

			body.drawToWebGLContext(webGLContext, this);
		}
	}
}

function Texture(name, image)
{
	this.name = name;
	this.image = image;
}
{
	Texture.prototype.clone = function()
	{
		return this; // hack
	}
}

function Transform()
{
	// static class
}
{
	Transform.transformApplyToCoordsMany = function(transform, coordsMany)
	{
		for (var i = 0; i < coordsMany.length; i++)
		{
			var coordsToTransform = coordsMany[i];
			transform.applyToCoords(coordsToTransform);
		}
	}
}

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

		return coordsToTransform;
	}
}

function TransformOrient(orientation)
{
	this.orientation = orientation;
}
{
	TransformOrient.prototype.applyToCoords = function(coordsToTransform)
	{
		return coordsToTransform.overwriteWith
		(
			this.orientation.forward.clone().multiplyScalar
			(	
				coordsToTransform.z
			).add
			(
				this.orientation.right.clone().multiplyScalar
				(
					coordsToTransform.x
				)
			).add
			(
				this.orientation.down.clone().multiplyScalar
				(
					coordsToTransform.y
				)
			)
		);
	}
}

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

function WebGLContext(canvas)
{
	var vertexShaderInputs = 
	[
		new WebGLShaderInput("Attribute", "aVertexColor"),
		new WebGLShaderInput("Attribute", "aVertexNormal"),
		new WebGLShaderInput("Attribute", "aVertexPosition"),
		new WebGLShaderInput("Attribute", "aVertexTextureUV"),

		new WebGLShaderInput("Uniform", "uBodyMatrix"),
		new WebGLShaderInput("Uniform", "uCameraMatrix"),
		new WebGLShaderInput("Uniform", "uLightAmbientIntensity"),
		new WebGLShaderInput("Uniform", "uLightDirection"),
		new WebGLShaderInput("Uniform", "uLightDirectionalIntensity"),
		new WebGLShaderInput("Uniform", "uNormalMatrix"),
	];

	var vertexShaderCode = document.getElementById
	(
		"divShaderCodeVertex"
	).childNodes[0].nodeValue;

	var fragmentShaderCode = document.getElementById
	(
		"divShaderCodeFragment"
	).childNodes[0].nodeValue;

	this.shader = new WebGLShader
	(
		vertexShaderInputs,
		vertexShaderCode,
		fragmentShaderCode
	);
	
	this.initialize(canvas);
}
{
	WebGLContext.prototype.initialize = function(canvas)
	{
		var gl = canvas.getContext("webgl");
		this.gl = gl;

		gl.viewportWidth = canvas.width;
		gl.viewportHeight = canvas.height;

		var colorBack = Color.Instances.Cyan;
		var colorBackComponentsRGBA = colorBack.componentsRGBA;
		gl.clearColor
		(
			colorBackComponentsRGBA[0], 
			colorBackComponentsRGBA[1], 
			colorBackComponentsRGBA[2], 
			colorBackComponentsRGBA[3]
		);

		gl.enable(gl.DEPTH_TEST);

		this.shader.initialize(gl);
	}
}

function WebGLShader(inputs, vertexShaderCode, fragmentShaderCode)
{
	this.inputs = inputs;
	this.vertexShaderCode = vertexShaderCode;
	this.fragmentShaderCode = fragmentShaderCode;

	this.inputs.addLookups("name");
}
{
	WebGLShader.prototype.initialize = function(gl)
	{
		var vertexShader = gl.createShader(gl.VERTEX_SHADER);
		gl.shaderSource(vertexShader, this.vertexShaderCode);
		gl.compileShader(vertexShader);

		var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
		gl.shaderSource(fragmentShader, this.fragmentShaderCode);
		gl.compileShader(fragmentShader);

		var systemShader = gl.createProgram();
		gl.attachShader(systemShader, vertexShader);
		gl.attachShader(systemShader, fragmentShader);
		gl.linkProgram(systemShader);
		gl.useProgram(systemShader);

		this.systemShader = systemShader;

		for (var i = 0; i < this.inputs.length; i++)
		{
			var input = this.inputs[i];
			input.initialize(gl, this.systemShader);
		}
	}
}

function WebGLShaderInput(typeName, name)
{
	this.typeName = typeName;
	this.name = name;
}
{
	WebGLShaderInput.prototype.initialize = function(gl, systemShader)
	{
		var returnValue;

		if (this.typeName == "Attribute")
		{
			returnValue = gl.getAttribLocation
			(
				systemShader, this.name
			);
			gl.enableVertexAttribArray(returnValue);
		}
		else if (this.typeName == "Uniform")
		{
			returnValue = gl.getUniformLocation
			(
				systemShader, this.name
			);
		}
		else
		{
			throw "Unexpected WebGLShaderInput.typeName.";
		}

		this.location = returnValue;
	}
}

// demo

function DemoData()
{
	// do nothing
}
{
	DemoData.prototype.scene = function(mediaHelper, displaySize)
	{
		var imageTextGL = mediaHelper.images["ImageTextGL"];

		var textureTextGL = new Texture
		(
			"TextureTextGL",
			imageTextGL
		);

		var materials = Material.Instances;

		var meshGround = new Mesh
		(
			"Ground",
			// vertexPositions
			[
				new Coords(1000, -1000, 0),
				new Coords(1000, 1000, 0),
				new Coords(-1000, 1000, 0), 
				new Coords(-1000, -1000, 0),
			],
			null, // texture
			// faces
			[
				new Face
				(
					[0, 1, 2, 3], // vertexIndices
					materials.GreenDark,
					null, // textureUVs
					null // vertexNormals - todo
				),
			]
		);

		var textureUVsForMeshFaces = Mesh.TextureUVsDefault;

		var meshRainbowMonolith = new Mesh
		(
			"RainbowMonolith",
			// vertexPositions
			[
				// back 
				new Coords(-40, 0, -10), 
				new Coords(40, 0, -10),
				new Coords(40, -180, -10),
				new Coords(-40, -180, -10),

				// front
				new Coords(-40, 0, 10),
				new Coords(40, 0, 10),
				new Coords(40, -180, 10),
				new Coords(-40, -180, 10),
			],
			textureTextGL,
			// faces
			[
				new Face
				(
					[5, 4, 7, 6], 
					materials.Red, 
					textureUVsForMeshFaces
				), // front

				new Face
				(
					[0, 1, 2, 3], 
					materials.Orange, 
					textureUVsForMeshFaces
				), // back

				new Face
				(
					[3, 0, 4, 7], 
					materials.Yellow, 
					textureUVsForMeshFaces
				), // left

				new Face
				(
					[5, 6, 2, 1], 
					materials.Green, 
					textureUVsForMeshFaces
				), // right

				new Face
				(
					[6, 7, 3, 2], 
					materials.Blue, 
					textureUVsForMeshFaces
				), // top

				new Face
				(
					[4, 5, 1, 0], 
					materials.Violet, 
					textureUVsForMeshFaces
				), // bottom

			]
		);

		var bodyDefnGround = new BodyDefn
		(
			"Ground",
			meshGround
		);

		var bodyDefnRainbowMonolith = new BodyDefn
		(
			"RainbowMonolith",
			meshRainbowMonolith
		);		

		var bodyGround = new Body
		(
			"Ground0",
			bodyDefnGround,
			new Coords(0, 0, 0), // pos
			new Orientation
			(
				new Coords(0, 0, -1), // forward
				new Coords(1, 0, 0) // down
			)
		);

		var scene = new Scene
		(
			"Scene0",
			new Lighting
			(
				.5, // ambientIntensity
				new Coords(-1, -1, -1), // direction
				2 // directionalIntensity
			),
			new Camera
			(
				displaySize.clone(),
				displaySize.y / 2, // focalLength
				new Coords(-200, 0, -100), // pos
				new Orientation
				(
					new Coords(1, 0, 0), // forward
					new Coords(0, 0, 1) // down
				)
			),
			// textures
			[
				textureTextGL,
			],
			// bodies
			[
				bodyGround, 

				new Body
				(
					"RainbowMonolith0",
					bodyDefnRainbowMonolith,
					new Coords(0, 0, 0), // pos
					new Orientation
					(
						new Coords(1, 0, 0), // forward
						new Coords(0, 0, 1) // down
					)
				),

				new Body
				(
					"RainbowMonolith1",
					bodyDefnRainbowMonolith,
					new Coords(100, 0, 0), // pos
					new Orientation
					(
						new Coords(1, 0, 0), // forward
						new Coords(0, 0, 1) // down
					)
				),

				new Body
				(
					"RainbowMonolith2",
					bodyDefnRainbowMonolith,
					new Coords(0, 100, 0), // pos
					new Orientation
					(
						new Coords(1, 1, 0), // forward
						new Coords(0, 0, 1) // down
					)
				),
			]
		);

		return scene;
	}
}

// run

new WebGLTest().main();

</script>

<!-- WebGL shader programs -->

<div id="divShaderCodeFragment" style="display:none">

	precision mediump float;
	uniform sampler2D uSampler;
	varying vec4 vColor;
	varying vec3 vLight;
	varying vec2 vTextureUV;
	void main(void) 
	{
		if (vTextureUV.x < 0.0)
		{
			gl_FragColor = vColor;
		} 
		else 
		{
			vec4 textureColor = texture2D(uSampler, vec2(vTextureUV.s, vTextureUV.t));
			gl_FragColor = vec4(vLight * textureColor.rgb, textureColor.a);
		}
	}

</div>

<div id="divShaderCodeVertex" style="display:none">

	attribute vec4 aVertexColor;
	attribute vec3 aVertexNormal;
	attribute vec3 aVertexPosition;
	attribute vec2 aVertexTextureUV;
	uniform mat4 uBodyMatrix;
	uniform mat4 uCameraMatrix;
	uniform float uLightAmbientIntensity;
	uniform vec3 uLightDirection;
	uniform float uLightDirectionalIntensity;
	uniform mat4 uNormalMatrix;
	varying vec4 vColor;
	varying vec3 vLight;
	varying vec2 vTextureUV;
	void main(void) 
	{
		vColor = aVertexColor;
		vec4 vertexNormal4 = vec4(aVertexNormal, 0.0);
		vec4 transformedNormal4 = uNormalMatrix * vertexNormal4;
		vec3 transformedNormal = vec3(transformedNormal4.xyz) * -1.0;
		float lightMagnitude = uLightAmbientIntensity;
		lightMagnitude += 
			uLightDirectionalIntensity 
			* max(dot(transformedNormal, uLightDirection), 0.0);
		vLight = vec3(1.0, 1.0, 1.0) * lightMagnitude;
		vTextureUV = aVertexTextureUV;
		vec4 vertexPos = vec4(aVertexPosition, 1.0);
		gl_Position = uCameraMatrix * uBodyMatrix * vertexPos;
	}

</div>


</body>
</html>

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

Finding Collisions of Circles and Line Segments in Javascript

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

To see the code in action, copy it into an .html file and open it in a web browser that runs JavaScript.

collisionsofcirclesandlinesegments


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

// main

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

	Globals.Instance.initialize
	(
		new TimerHelper(10), 
		display,
		World.random
		(
			display.size, 
			16, // numberOfBodies
			20, // bodyDimensionMin
			80, // bodyDimensionMax
			1 // speedMax 
		)
	);
}

// classes

function Body(collider, vel)
{
	this.collider = collider;
	this.vel = vel;
}
{
	Body.prototype.posAddAndWrap = function(offset, rangeMax)
	{
		this.collider.posAddAndWrap(offset, rangeMax);
	}

	// drawable

	Body.prototype.drawToDisplay = function(display)
	{
		this.collider.drawToDisplay(display);
	}
}

function Collision()
{
	this.isActive = null;
	this.colliders = [];
	this.points = [];
}
{	
	Collision.prototype.ofColliders = function(collider0, collider1)
	{
		var collider0TypeName = collider0.constructor.name;
		var collider1TypeName = collider1.constructor.name;
	
		if (collider0TypeName == "ShapeCircle")
		{
			if (collider1TypeName == "ShapeCircle")
			{
				this.ofCircles(collider0, collider1);
			}
			else // if (collider1TypeName == "ShapeLineSegment")
			{
				this.ofCircleAndLineSegment(collider0, collider1);
			}
		}
		else // if (collider0TypeName == "ShapeLineSegment")
		{
			if (collider1TypeName == "ShapeCircle")
			{
				this.ofCircleAndLineSegment(collider1, collider0);	
			}
			else // if (collider1TypeName == "ShapeLineSegment")
			{
				this.ofLineSegments(collider0, collider1);
			}
		}

		return this;
	}

	// shapes

	Collision.prototype.ofCircleAndLineSegment = function(circle, segment)
	{

		// todo

		var segmentRight = segment.right;
		var closestApproachOfSegmentLineToOrigin = segment.right.dotProduct
		(
			segment.start
		);

		var centerOfCircleProjectedOntoRight = circle.center.dotProduct
		(
			segmentRight
		);

		var distanceToRadicalCenter =
			closestApproachOfSegmentLineToOrigin 
			- centerOfCircleProjectedOntoRight;

		if (Math.abs(distanceToRadicalCenter) >= circle.radius)
		{
			this.isActive = false;
			this.colliders.length = 0;
			this.points.length = 0;
		}
		else
		{
			this.isActive = true;
			this.colliders.push(circle);
			this.colliders.push(segment);

			var radicalCenter = circle.center.clone().add
			(
				segment.right.clone().multiplyScalar
				(
					distanceToRadicalCenter
				)
			);
			
			var distanceFromRadicalCenterToIntersection = 
				Math.sqrt
				(
					circle.radius * circle.radius
					- (distanceToRadicalCenter * distanceToRadicalCenter)
				);

			var offsetFromRadicalCenterToIntersection = 
				segment.direction.clone().multiplyScalar
				(
					distanceFromRadicalCenterToIntersection 
				);


			for (var i = 0; i < 2; i++)
			{
				var intersection = radicalCenter.clone().add
				(
					offsetFromRadicalCenterToIntersection
				);

				var distanceAlongSegment = 
					intersection.clone().subtract
					(
						segment.start
					).dotProduct
					(
						segment.direction
					);

				if 
				(
					distanceAlongSegment > 0 
					&& distanceAlongSegment < segment.length
				)
				{
					this.points.push(intersection); 				
				}

				// hack
				offsetFromRadicalCenterToIntersection.multiplyScalar(-1);
			}
		}
	}

	Collision.prototype.ofCircles = function(circle0, circle1)
	{
		var displacementFromCenter0To1 = 
			circle1.center.clone().subtract
			(
				circle0.center	
			);

		var distanceBetweenCenters = 
			displacementFromCenter0To1.magnitude();

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

		var sumOfRadii = circle0Radius + circle1Radius;

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

			this.isActive = true;
			this.colliders.push(circle0);
			this.colliders.push(circle1);

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

			var directionToRadicalCenter = 
				displacementFromCenter0To1.divideScalar
				(
					distanceBetweenCenters
				);

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

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

			var differenceOfRadii = circle1Radius - circle0Radius;

			var directionFromRadicalCenterToIntersection = 
				directionToRadicalCenter.right();

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

			var displacementFromRadicalCenterToIntersection = 
				directionFromRadicalCenterToIntersection.multiplyScalar
				(
					radicalLineLengthHalf
				);

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

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

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

		}

		return this;
	}

	Collision.prototype.ofLineSegments = function(segment0, segment1)
	{
		this.isActive = false;
		this.colliders.length = 0;
		this.points.length = 0;

		var segment1ProjectedOnto0 = segment1.clone().projectOnto
		(
			segment0
		);
		var segmentProjected = segment1ProjectedOnto0;

		var segmentProjectedLength = segmentProjected.length;

		var point0 = segmentProjected.start;
		var point1 = segmentProjected.end();

		var lineProjectedCrossesXAxis = (point0.y / point1.y < 0);

		if (lineProjectedCrossesXAxis == true)
		{
			var lengthAlongSegment1ToIntersection = Math.abs
			(
				point0.y / segmentProjected.direction.y
			);

			var lengthAlongSegment0ToIntersection =  
				segmentProjected.start.x
			 	+ segmentProjected.direction.x
				* lengthAlongSegment1ToIntersection; 

			if 
			(
				lengthAlongSegment0ToIntersection > 0
				&& lengthAlongSegment0ToIntersection < segment0.length
				&& lengthAlongSegment1ToIntersection > 0
				&& lengthAlongSegment1ToIntersection < segment1.length
			)
			{
				this.isActive = true;
				this.colliders.push(segment0);
				this.colliders.push(segment1);

				var collisionPos = segment1.start.clone().add
				(
					segment1.direction.clone().multiplyScalar
					(
						lengthAlongSegment1ToIntersection
					)
				);	

				this.points.push(collisionPos);			
			}
		}

		return this;
	}


	// drawable

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

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

function Constants()
{
	// static class
}
{
	// constants

	Constants.RadiansPerCycle = 2.0 * Math.PI;
}

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

	Coords.Instances = new Coords_Instances();

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

	// methods

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

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

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

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

	Coords.prototype.fromAngle = function(angleInCycles)
	{
		var radiansPerCycle = Constants.RadiansPerCycle;
		var angleInRadians = angleInCycles * radiansPerCycle;
		this.x = Math.cos(angleInRadians);
		this.y = Math.sin(angleInRadians);
		return this;
	}

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

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

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

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

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

	Coords.prototype.projectOntoXY = function(axisX, axisY)
	{
		return this.overwriteWithXY
		(
			this.dotProduct(axisX),
			this.dotProduct(axisY)
		);
	}

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

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

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

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

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

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

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

		return this;
	}
}

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

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

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

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

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

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

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

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

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

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

	Globals.Instance = new Globals();

	// methods

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

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

	// events

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

function ShapeCircle(center, radius)
{
	this.center = center;
	this.radius = radius;
}
{
	ShapeCircle.prototype.posAddAndWrap = function(offset, rangeMax) 
	{ 
		this.center.add(offset).wrapToRangeMax(rangeMax); 
	}

	// drawable

	ShapeCircle.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.center, this.radius);
	}
}

function ShapeLineSegment(start, displacement)
{
	this.start = start;
	this.displacement = displacement;

	this.length = null;
	this.direction = new Coords();
	this.right = new Coords();

	this.recalculateDerivedValues();
}
{
	ShapeLineSegment.prototype.clone = function()
	{
		return new ShapeLineSegment
		(
			this.start.clone(), this.displacement.clone()
		);
	}

	ShapeLineSegment.prototype.end = function()
	{
		return this.start.clone().add(this.displacement);
	}

	ShapeLineSegment.prototype.posAddAndWrap = function(offset, rangeMax) 
	{
		this.start.add(offset).wrapToRangeMax(rangeMax);
	}

	ShapeLineSegment.prototype.projectOnto = function(other)
	{
		this.start.subtract
		(
			other.start
		).projectOntoXY
		(
			other.direction, other.right
		);

		this.displacement.projectOntoXY(other.direction, other.right);

		this.recalculateDerivedValues();

		return this;
	}

	ShapeLineSegment.prototype.recalculateDerivedValues = function()
	{
		this.length = this.displacement.magnitude();

		this.direction.overwriteWith
		(
			this.displacement
		).divideScalar
		(
			this.length
		);

		this.right.overwriteWith(this.direction).right();
	}

	// drawable

	ShapeLineSegment.prototype.drawToDisplay = function(display)
	{
		display.drawLine
		(
			this.start, 
			this.end()
		);
	}

}

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

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

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

		var bodyDimensionRange = bodyDimensionMax - bodyDimensionMin;
		var ones = Coords.Instances.Ones;

		for (var i = 0; i < numberOfBodies; i++)
		{
			var collider;

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

			var bodyDimension = 
				bodyDimensionMin 
				+ bodyDimensionRange * Math.random();	

			if (i % 2 == 0)
			{
				collider = new ShapeCircle
				(
					pos, bodyDimension / 2
				);
			}
			else
			{
				var angle = 
					Math.random() 
					* Constants.RadiansPerCycle;
				var direction = new Coords().fromAngle(angle);
				var displacement = direction.multiplyScalar
				(
					bodyDimension
				);

				collider = new ShapeLineSegment
				(
					pos, displacement
				);
			}

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

			var body = new Body(collider, vel);

			bodies.push(body);
		}

		var returnValue = new World
		(
			size, 
			bodies
		);

		return returnValue;
	}

	World.simpleLines = function(size)
	{
		return new World
		(
			size,
			[
				new Body
				(
					new ShapeLineSegment
					(
						new Coords(size.x / 4, size.y / 2), // start
						new Coords(size.x / 2, 0) // disp
					),
					new Coords(0, 0)
				),

				new Body
				(
					new ShapeLineSegment
					(
						new Coords(size.x / 2, 3 * size.y / 4), // start
						new Coords(-size.x / 4, - size.y / 2) // disp
					),
					new Coords(0, 0)
				),
			]
		);
	}

	// instance methods

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

	// drawable

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

			body.drawToDisplay(display);
		}

		var collision = new Collision();

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

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

				collision.ofColliders(collider, colliderOther);

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

	}
}

// run

main();

</script>
</body>

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