Drag and Drop with External Files in HTML5 and JavaScript

The JavaScript code below, when run, allows the user to drag and drop image files onto a box, upon which the image and its file name will be displayed. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

DragAndDropImageViewer.png


<html>
<body>

<div 
	id="divDropTarget" 
	style="border:1px solid" 
	ondragover="divDropTarget_DraggedOver(event);" 
	ondrop="divDropTarget_DroppedOnto(event);" 
>
	<label>Drag an image file into this box to display it.</label>
</div>

<script type="text/javascript">

// UI event handlers.

function divDropTarget_DraggedOver(event)
{
	event.preventDefault();	
}

function divDropTarget_DroppedOnto(eventDropped)
{
	eventDropped.preventDefault();

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

	var filesDropped = eventDropped.dataTransfer.files;
	for (var i = 0; i < filesDropped.length; i++) 
	{
		var file = filesDropped[i];
		var fileType = file.type;
		var isFileAnImage = fileType.startsWith("image/");
		if (isFileAnImage == true)
		{
			var fileReader = new FileReader();
			fileReader.onload = function(eventFileLoaded) 
			{
				var divFile = document.createElement("div");
				divFile.style.border = "1px solid";

				var fileAsDataURL = eventFileLoaded.target.result;
				var imgContent = document.createElement("img");
				imgContent.src = fileAsDataURL;
				divFile.appendChild(imgContent);

				var spanFileName = document.createElement("span");
				spanFileName.innerHTML = file.name;
				divFile.appendChild(spanFileName);

				divDropTarget.appendChild(divFile);
			}
			fileReader.readAsDataURL(file);
		}
	}
}

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


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

Exploring the ISO File Format in JavaScript

The JavaScript code below allows the user to specify a file in .ISO format, and displays the structure and contents of that file as text. To see it in action, copy it into an .html file and open it in a web browser that runs JavaScript.

The extension .ISO is commonly used to denote a file that contains an instance of the ISO 9660 filesystem. For details on this filesystem, visit the URLs “https://en.wikipedia.org/wiki/ISO_9660” or “http://wiki.osdev.org/ISO_9660“.

Currently, this program only shows the top level of the directory structure, as attempting to recurse over the entire directory tree results in a stack overflow. Also, some of the longer byte fields are being inappropriately concatenated into semicolon-delimited strings as an aid to readability.

ISOViewer.png


<html>
<body>
<div id="divUI">
	<label>ISO File to Load:</label>

	<input type="file" onchange="inputFile_Changed(this);"></input>

	<label>File Contents:</label>

	<textarea id="textareaFileContents" cols="80" rows="25"></textarea></div>
<script type="text/javascript">

// ui event handers

function inputFile_Changed(inputFile)
{
	var file = inputFile.files[0];
	var fileReader = new FileReaderLarge();
	fileReader.readFileAsBinaryStrings(file, inputFile_Changed_FileLoaded);
}

function inputFile_Changed_FileLoaded(fileAsBinaryStrings)
{
	var fileAsISO = ISOFile.fromBinaryStrings(fileAsBinaryStrings);
	var fileAsJSON = JSON.stringify(fileAsISO, null, 4); 
	var textareaFileContents = document.getElementById("textareaFileContents");
	textareaFileContents.value = fileAsJSON;
}

// classes 

function ByteStream(bytes)
{
	this.bytes = bytes;
	this.byteIndexCurrent = 0;
}
{
	ByteStream.prototype.peekByte = function()
	{
		return this.bytes[this.byteIndex];
	}

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

	ByteStream.prototype.readBytes = function(numberOfBytes)
	{
		var bytesRead = [];
		for (var i = 0; i < numberOfBytes; i++)
		{
			var byteRead = this.readByte();
			bytesRead.push(byteRead);
		}
		return bytesRead;
	}
	
	ByteStream.prototype.readInt16BE = function()
	{
		var bytes = this.readBytes(2);
		var returnValue = 
			bytes[0] << 8
			| bytes[1];
		return returnValue;
	}
	
	ByteStream.prototype.readInt16LE = function()
	{
		var bytes = this.readBytes(2);
		var returnValue = 
			bytes[0] 
			| bytes[1] << 8;
		return returnValue;
	}

	ByteStream.prototype.readInt32BE = function()
	{
		var bytes = this.readBytes(4);
		var returnValue = 
			bytes[0] << 24
			| bytes[1] << 16
			| bytes[2] << 8
			| bytes[3];
		return returnValue;
	}
	
	ByteStream.prototype.readInt32LE = function()
	{
		var bytes = this.readBytes(4);
		var returnValue = 
			bytes[0] 
			| bytes[1] << 8
			| bytes[2] << 16
			| bytes[3] << 24;
		return returnValue;
	}	
	
	ByteStream.prototype.seek = function(byteIndex)
	{
		this.byteIndexCurrent = byteIndex;
	}
}


function ByteStreamBinaryStrings(bytesAsBinaryStrings)
{
	this.bytesAsStringChunked = new StringChunked(bytesAsBinaryStrings);
	this.byteIndexCurrent = 0;
}
{
	ByteStreamBinaryStrings.prototype.peekByte = function()
	{
		return this.bytesAsStringChunked.charCodeAt(this.byteIndexCurrent);
	}

	ByteStreamBinaryStrings.prototype.readByte = function()
	{
		var returnValue = this.bytesAsStringChunked.charCodeAt(this.byteIndexCurrent);
		this.byteIndexCurrent++;
		return returnValue;
	}

	ByteStreamBinaryStrings.prototype.readBytes = function(numberOfBytes)
	{
		var bytesRead = [];
		for (var i = 0; i < numberOfBytes; i++)
		{
			var byteRead = this.readByte();
			bytesRead.push(byteRead);
		}
		return bytesRead;
	}
	
	ByteStreamBinaryStrings.prototype.readInt16BE = function()
	{
		var bytes = this.readBytes(2);
		var returnValue = 
			bytes[0] << 8
			| bytes[1];
		return returnValue;
	}
	
	ByteStreamBinaryStrings.prototype.readInt16LE = function()
	{
		var bytes = this.readBytes(2);
		var returnValue = 
			bytes[0] 
			| bytes[1] << 8;
		return returnValue;
	}

	ByteStreamBinaryStrings.prototype.readInt32BE = function()
	{
		var bytes = this.readBytes(4);
		var returnValue = 
			bytes[0] << 24
			| bytes[1] << 16
			| bytes[2] << 8
			| bytes[3];
		return returnValue;
	}
	
	ByteStreamBinaryStrings.prototype.readInt32LE = function()
	{
		var bytes = this.readBytes(4);
		var returnValue = 
			bytes[0] 
			| bytes[1] << 8
			| bytes[2] << 16
			| bytes[3] << 24;
		return returnValue;
	}
	
	ByteStreamBinaryStrings.prototype.readInt64LE = function()
	{
		var bytes = this.readBytes(8);
		var returnValue = 
			bytes[0] 
			| bytes[1] << 8
			| bytes[2] << 16
			| bytes[3] << 24
			| bytes[4] << 32
			| bytes[5] << 40
			| bytes[6] << 48
			| bytes[7] << 56; 			 		return returnValue; 	}	 	 	ByteStreamBinaryStrings.prototype.seek = function(byteIndex) 	{ 		this.byteIndexCurrent = byteIndex; 	}	 } function FileReaderLarge() { 	this.systemFileReader = new FileReader(); 	this.bytesPerChunk = 2 * 1024 * 1024; // 2 MiB. } { 	FileReaderLarge.prototype.readFileAsBinaryStrings = function(fileToRead, callback) 	{ 		this.numberOfChunks = Math.ceil(fileToRead.size / this.bytesPerChunk); 		this.fileChunksAsBinaryStrings = []; 		this.readFileAsBinaryStrings_Chunk(fileToRead, 0, callback); 	} 	 	FileReaderLarge.prototype.readFileAsBinaryStrings_Chunk = function(file, chunkIndex, callback) 	{ 		if (chunkIndex >= this.numberOfChunks)
		{
			callback(this.fileChunksAsBinaryStrings);
		}
		else
		{
			var chunkOffsetInBytes = this.bytesPerChunk * chunkIndex;
			var chunkEndInBytes = chunkOffsetInBytes + this.bytesPerChunk;
			var fileChunk = file.slice(chunkOffsetInBytes, chunkEndInBytes);
			var thisAsVariable = this;
			this.systemFileReader.onload = function(event)
			{
				var fileChunkAsBinaryString = event.target.result;
				thisAsVariable.fileChunksAsBinaryStrings[chunkIndex] = fileChunkAsBinaryString;
				
				thisAsVariable.readFileAsBinaryStrings_Chunk(file, chunkIndex + 1, callback)
			}
			this.systemFileReader.readAsBinaryString(fileChunk);
		}
	}
}

function ISOFile(systemAreaAsBytes, volumeDescriptors)
{
	// Format specifications at http://wiki.osdev.org/ISO_9660.
	// Field assignments re-ordered for clearer serialization.

	this.volumeDescriptors = volumeDescriptors;
	
	var volumeDescriptorPrimary = this.volumeDescriptorPrimary();
	this.directoryRoot = volumeDescriptorPrimary.directoryEntryRoot.clone();
	
	this.systemAreaAsBytes = systemAreaAsBytes.join(";"); // Usually bootable assembly?	
}
{
	// Static methods.

	ISOFile.fromBinaryStrings = function(fileAsBinaryStrings)
	{
		var fileAsByteStream = new ByteStreamBinaryStrings
		(
			fileAsBinaryStrings
		);	

		var systemAreaAsBytes = fileAsByteStream.readBytes(32768);

		var volumeDescriptors = [];
		var volumeDescriptorTypeTerminator = 
			ISOFileVolumeDescriptorType.Instances.Terminator;
		while (true)
		{
			var volumeDescriptor = ISOFileVolumeDescriptor.fromByteStream
			(
				fileAsByteStream
			);
			
			volumeDescriptors.push(volumeDescriptor);
			
			if (volumeDescriptor.type == volumeDescriptorTypeTerminator)
			{
				break;
			}
		}

		var returnValue = new ISOFile
		(
			systemAreaAsBytes,
			volumeDescriptors
		);
		
		returnValue.directoryTreeBuild(fileAsByteStream);
		
		return returnValue;
	}
	
	// Instance methods.
	
	ISOFile.prototype.directoryTreeBuild = function(fileAsByteStream)
	{
		this.directoryRoot.directoryContentsBuild(fileAsByteStream);
	}
	
	ISOFile.prototype.volumeDescriptorPrimary = function()
	{
		return this.volumeDescriptors[0]; // todo
	}
}

function ISOFileDateTimeLong()
{
	// Static class.
}
{
	ISOFileDateTimeLong.fromBytes = function(bytes)
	{
		var year = parseInt(ISOFileString.fromBytes(bytes.slice(0, 4)));
		var month = parseInt(ISOFileString.fromBytes(bytes.slice(4, 6)));
		var day = parseInt(ISOFileString.fromBytes(bytes.slice(6, 8)));
		var hour = parseInt(ISOFileString.fromBytes(bytes.slice(8, 10)));
		var minute = parseInt(ISOFileString.fromBytes(bytes.slice(10, 12)));
		var second = parseInt(ISOFileString.fromBytes(bytes.slice(12, 14)));
		var centisecond = parseInt(ISOFileString.fromBytes(bytes.slice(14, 16)));
		var timeZoneOffset = bytes[16];
		var returnValue = new Date(year, month, day, hour, minute, second); 
		return returnValue;
	}
}

function ISOFileDateTimeShort()
{
	// Static class.
}
{
	ISOFileDateTimeShort.fromBytes = function(bytes)
	{
		var byteStream = new ByteStream(bytes);
		var yearsSince1900 = byteStream.readByte();
		var year = 1900 + yearsSince1900;
		var month = byteStream.readByte();
		var day = byteStream.readByte();
		var hour = byteStream.readByte();
		var minute = byteStream.readByte();
		var second = byteStream.readByte();
		var timeZoneOffset = byteStream.readByte();
		var returnValue = new Date(year, month, day, hour, minute, second); 
		return returnValue;
	}
}

function ISOFileDirectoryEntry
(
	entryLengthInBytes,
	extendedAttributeRecordLength,
	dataLocationInSectors,
	dataLengthInBytes,
	recordingTime,
	flags,
	interleaveUnitSize,
	interleaveGapSize,
	volumeSequenceNumber,
	fileName
)
{
	this.entryLengthInBytes = entryLengthInBytes;
	this.extendedAttributeRecordLength = extendedAttributeRecordLength;
	this.dataLocationInSectors = dataLocationInSectors;
	this.dataLengthInBytes = dataLengthInBytes;
	this.recordingTime = recordingTime;
	this.flags = flags;
	this.interleaveUnitSize = interleaveUnitSize;
	this.interleaveGapSize = interleaveGapSize;
	this.volumeSequenceNumber = volumeSequenceNumber;
	this.fileName = fileName;
}
{
	// Static methods.

	ISOFileDirectoryEntry.fromBytes = function(bytes)
	{
		var byteStream = new ByteStream(bytes);
		
		var entryLengthInBytes = byteStream.readByte();
		var extendedAttributeRecordLength = byteStream.readByte();
		var dataLocationInSectorsLE = byteStream.readInt32LE();
		var dataLocationInSectorsBE = byteStream.readInt32BE();
		var dataLengthInBytesLE = byteStream.readInt32LE();
		var dataLengthInBytesBE = byteStream.readInt32BE();
		var recordingTime = ISOFileDateTimeShort.fromBytes(byteStream.readBytes(7));
		var flags = byteStream.readByte();
		var interleaveUnitSize = byteStream.readByte();
		var interleaveGapSize = byteStream.readByte();
		var volumeSequenceNumberLE = byteStream.readInt16LE();
		var volumeSequenceNumberBE = byteStream.readInt16BE();
		var fileNameLength = byteStream.readByte();
		var fileName = ISOFileString.fromBytes(byteStream.readBytes(fileNameLength));
		if (fileNameLength % 2 == 0)
		{
			var padding = byteStream.readByte();
		}
		//var reserved = byteStream.readToEnd();
		
		var returnValue = new ISOFileDirectoryEntry
		(
			entryLengthInBytes,
			extendedAttributeRecordLength,
			dataLocationInSectorsLE,
			dataLengthInBytesLE,
			recordingTime,
			flags,
			interleaveUnitSize,
			interleaveGapSize,
			volumeSequenceNumberLE,
			fileName			
		);
		
		return returnValue;
	}
	
	// Instance methods.
	
	ISOFileDirectoryEntry.prototype.clone = function()
	{
		return new ISOFileDirectoryEntry
		(
			this.entryLengthInBytes,
			this.extendedAttributeRecordLength,
			this.dataLocationInSectors,
			this.dataLengthInBytes,
			this.recordingTime,
			this.flags,
			this.interleaveUnitSize,
			this.interleaveGapSize,
			this.volumeSequenceNumber,
			this.fileName			
		);
	
	}

	ISOFileDirectoryEntry.prototype.directoryContentsBuild = function(fileAsByteStream)
	{
		if (this.flags.isDirectory == false)
		{
			return;
		}
	
		var dataOffsetInBytes = 
			this.dataLocationInSectors 
			* this.dataLengthInBytes;
		fileAsByteStream.seek(dataOffsetInBytes);

		var childEntries = [];
		
		while (true)
		{
			var entryLengthAsBytes = fileAsByteStream.peekByte();
			if (entryLengthAsBytes == 0)
			{
				break;
			}
			else
			{
				var entryAsBytes = 
					fileAsByteStream.readBytes(entryLengthAsBytes);
				var entry = 
					ISOFileDirectoryEntry.fromBytes(entryAsBytes);	
				childEntries.push(entry);
			}
		}
		
		this.childEntries = childEntries;
		for (var i = 0; i < this.childEntries.length; i++) 		{ 			var child = this.childEntries[i]; 			// todo - Leads to stack overflow right now. 			//child.directoryContentsBuild(fileAsByteStream); 		} 	} } function ISOFileDirectoryEntryFlags ( 	isHidden, 	isDirectory, 	isAssociatedFile, // ? 	doesExtendedAttributeRecordHaveFormatInfo, // ? 	doesExtendedAttributeRecordHavePermissions, // ? 	isNotFinalEntryInFile ) { 	this.isHidden = isHidden; 	this.isDirectory = isDirectory; 	this.isAssociatedFile = isAssociatedFile; 	this.doesExtendedAttributeRecordHaveFormatInfo =  		doesExtendedAttributeRecordHaveFormatInfo; 	this.doesExtendedAttributeRecordHavePermissions =  		doesExtendedAttributeRecordHavePermissions; 	this.isNotFinalEntryInFile = isNotFinalEntryInFile; } { 	ISOFileDirectoryEntryFlags.fromByte = function(flagsAsByte) 	{ 		return new ISOFileDirectoryEntryFlags 		( 			((flagsAsByte >> 0) & 1) == 1,
			((flagsAsByte >> 1) & 1) == 1,
			((flagsAsByte >> 2) & 1) == 1,
			((flagsAsByte >> 3) & 1) == 1,
			((flagsAsByte >> 4) & 1) == 1,
			// Bits 5 and 6 reserved.
			((flagsAsByte >> 7) & 1) == 1
		);
	}
}


function ISOFileString()
{
	// Static class
}
{
	ISOFileString.fromBytes = function(bytes)
	{
		var returnValue = "";
		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			returnValue += String.fromCharCode(byte);
		}
		returnValue = returnValue.trim();
		return returnValue;
	}
}


function ISOFileVolumeDescriptor()
{
	// Statis class.
}
{
	// static methods

	ISOFileVolumeDescriptor.fromByteStream = function(byteStream)
	{
		var typeAsByte = byteStream.readByte();
		var type = ISOFileVolumeDescriptorType.fromCode(typeAsByte);
		var identifier = byteStream.readBytes(5);
		var version = byteStream.readByte();
		
		var returnValue = type.volumeDescriptorReadFromByteStream
		(
			byteStream
		);
				
		return returnValue;
	}
}

function ISOFileVolumeDescriptor_BootRecord(bootSystemID, bootID, data)
{
	this.type = ISOFileVolumeDescriptorType.Instances.BootRecord;

	this.bootSystemID = bootSystemID;
	this.bootID = bootID;
	this.data = data.join(";");
}
{
	ISOFileVolumeDescriptor_BootRecord.fromByteStream = function(byteStream)
	{
		var bootSystemID = ISOFileString.fromBytes(byteStream.readBytes(32));
		var bootID = ISOFileString.fromBytes(byteStream.readBytes(32));
		var data = byteStream.readBytes(1977);
		var returnValue = new ISOFileVolumeDescriptor_BootRecord
		(
			bootSystemID, bootID, data
		);
		return returnValue;
	}
}

function ISOFileVolumeDescriptor_Partition()
{
	this.type = ISOFileVolumeDescriptorType.Instances.Partition;
	// todo
}

function ISOFileVolumeDescriptor_Primary
(
	systemID,
	volumeID,
	volumeSpaceSize,
	volumeSetSize,
	volumeSequenceNumber,
	logicalBlockSize,
	pathTableSize,
	locationOfPathTable,
	locationOfOptionalPathTable,
	directoryEntryRoot,
	volumeSetID,
	publisherID,
	dataPreparerID,
	applicationID,
	copyrightFileID,
	abstractFileID,
	bibliographicFileID,
	volumeCreateTime,
	volumeModifyTime,
	volumeExpireTime,
	volumeEffectiveTime,
	fileStructureVersion,
	applicationUsed
)
{
	this.type = ISOFileVolumeDescriptorType.Instances.Primary;

	this.systemID = systemID;
	this.volumeID = volumeID;
	this.volumeSpaceSize = volumeSpaceSize;
	this.volumeSetSize = volumeSetSize;
	this.volumeSequenceNumber = volumeSequenceNumber;
	this.logicalBlockSize = logicalBlockSize;
	this.pathTableSize = pathTableSize;
	this.locationOfPathTable = locationOfPathTable;
	this.locationOfOptionalPathTable = locationOfOptionalPathTable;
	this.directoryEntryRoot = directoryEntryRoot;
	this.volumeSetID = volumeSetID;
	this.publisherID = publisherID;
	this.dataPreparerID = dataPreparerID;
	this.applicationID = applicationID;
	this.copyrightFileID = copyrightFileID;
	this.abstractFileID = abstractFileID;
	this.bibliographicFileID = bibliographicFileID;
	this.volumeCreateTime = volumeCreateTime;
	this.volumeModifyTime = volumeModifyTime;
	this.volumeExpireTime = volumeExpireTime;
	this.volumeEffectiveTime = volumeEffectiveTime;
	this.fileStructureVersion = fileStructureVersion;
	this.applicationUsed = applicationUsed;
}
{
	ISOFileVolumeDescriptor_Primary.fromByteStream = function(byteStream)
	{
		var unused = byteStream.readByte();
		var systemID = ISOFileString.fromBytes(byteStream.readBytes(32));
		var volumeID = ISOFileString.fromBytes(byteStream.readBytes(32));
		var unused2 = byteStream.readBytes(8);
		var volumeSpaceSizeLE = byteStream.readInt32LE();
		var volumeSpaceSizeBE = byteStream.readInt32BE();		
		var unused3 = byteStream.readBytes(32);
		var volumeSetSizeLE = byteStream.readInt16LE();
		var volumeSetSizeBE = byteStream.readInt16BE();
		var volumeSequenceNumberLE = byteStream.readInt16LE();
		var volumeSequenceNumberBE = byteStream.readInt16BE();
		var logicalBlockSizeLE = byteStream.readInt16LE();
		var logicalBlockSizeBE = byteStream.readInt16BE();
		var pathTableSizeLE = byteStream.readInt32LE();
		var pathTableSizeBE = byteStream.readInt32BE();		
		var locationOfPathTableLE = byteStream.readInt32LE(); // "LE" = "Little-Endian".
		var locationOfOptionalPathTableLE = byteStream.readInt32LE();
		var locationOfPathTableBE = byteStream.readInt32BE(); // "BE" = "Big-Endian".
		var locationOfOptionalPathTableBE = byteStream.readInt32LE();
		var directoryEntryRoot = ISOFileDirectoryEntry.fromBytes(byteStream.readBytes(34));
		var volumeSetID = ISOFileString.fromBytes(byteStream.readBytes(128));
		var publisherID = ISOFileString.fromBytes(byteStream.readBytes(128));
		var dataPreparerID = ISOFileString.fromBytes(byteStream.readBytes(128));
		var applicationID = ISOFileString.fromBytes(byteStream.readBytes(128));
		var copyrightFileID = ISOFileString.fromBytes(byteStream.readBytes(38));
		var abstractFileID = ISOFileString.fromBytes(byteStream.readBytes(36));
		var bibliographicFileID = ISOFileString.fromBytes(byteStream.readBytes(37));
		var volumeCreateTime = ISOFileDateTimeLong.fromBytes(byteStream.readBytes(17));
		var volumeModifyTime = ISOFileDateTimeLong.fromBytes(byteStream.readBytes(17));
		var volumeExpireTime = ISOFileDateTimeLong.fromBytes(byteStream.readBytes(17));
		var volumeEffectiveTime = ISOFileDateTimeLong.fromBytes(byteStream.readBytes(17));
		var fileStructureVersion = byteStream.readByte();
		var unused4 = byteStream.readByte();
		var applicationUsed = ISOFileString.fromBytes(byteStream.readBytes(512));
		var reserved = byteStream.readBytes(653);
		
		var returnValue = new ISOFileVolumeDescriptor_Primary
		(
			systemID,
			volumeID,
			volumeSpaceSizeLE,
			volumeSetSizeLE,
			volumeSequenceNumberLE,
			logicalBlockSizeLE,
			pathTableSizeLE,
			locationOfPathTableLE,
			locationOfOptionalPathTableLE,
			directoryEntryRoot,
			volumeSetID,
			publisherID,
			dataPreparerID,
			applicationID,
			copyrightFileID,
			abstractFileID,
			bibliographicFileID,
			volumeCreateTime,
			volumeModifyTime,
			volumeExpireTime,
			volumeEffectiveTime,
			fileStructureVersion,
			applicationUsed			
		);
		
		return returnValue;
	}
}

function ISOFileVolumeDescriptor_Supplemental()
{
	this.type = ISOFileVolumeDescriptorType.Instances.Supplemental;	
	// todo	
}

function ISOFileVolumeDescriptor_Terminator()
{
	this.type = ISOFileVolumeDescriptorType.Instances.Terminator;
}

function ISOFileVolumeDescriptorType(name, code, volumeDescriptorReadFromByteStream)
{
	this.name = name;
	this.code = code;
	this.volumeDescriptorReadFromByteStream = volumeDescriptorReadFromByteStream;
}
{
	ISOFileVolumeDescriptorType.Instances = new ISOFileVolumeDescriptorType_Instances();
	
	function ISOFileVolumeDescriptorType_Instances()
	{
		this.BootRecord = new ISOFileVolumeDescriptorType
		(
			"BootRecord", 
			0, // code
			ISOFileVolumeDescriptor_BootRecord.fromByteStream
		);
		
		this.Primary = new ISOFileVolumeDescriptorType
		(
			"Primary", 
			1,
			ISOFileVolumeDescriptor_Primary.fromByteStream
		);
		
		this.Supplemental = new ISOFileVolumeDescriptorType
		(
			"Supplemental", 
			2,
			function volumeDescriptorReadFromByteStream(byteStream)
			{
				byteStream.readBytes(2041); // todo
				return new ISOFileVolumeDescriptor_Supplemental();
			}			
		);
		
		this.Partition = new ISOFileVolumeDescriptorType
		(
			"Partition", 
			3,
			function volumeDescriptorReadFromByteStream(byteStream)
			{
				byteStream.readBytes(2041); // todo
				return new ISOFileVolumeDescriptor_Partition();
			}			
		);
		
		this.Terminator = new ISOFileVolumeDescriptorType
		(
			"Terminator", 
			255,
			function volumeDescriptorReadFromByteStream(byteStream)
			{
				byteStream.readBytes(2041);
				return new ISOFileVolumeDescriptor_Terminator();
			}			
		);
		
		this._All = 
		[
			this.BootRecord,
			this.Primary,
			this.Supplemental,
			this.Partition,
			this.Terminator
		];
		
		for (var i = 0; i < this._All.length; i++)
		{
			var element = this._All[i];
			var key = "_" + element.code;
			this._All[key] = element;
		}
	}
	
	// static methods
	
	ISOFileVolumeDescriptorType.fromCode = function(code)
	{
		var codeEscaped = "_" + code;
		var instances = ISOFileVolumeDescriptorType.Instances._All;
		var returnValue = instances[codeEscaped];
		return returnValue;
	}
}

function StringChunked(chunks)
{
	this.chunks = chunks;
	this.charsPerChunk = this.chunks[0].length;
}
{
	StringChunked.prototype.charCodeAt = function(charIndex)
	{
		var chunkIndex = Math.floor(charIndex / this.charsPerChunk);
		var charOffsetWithinChunk = charIndex % this.charsPerChunk;
		
		var chunk = this.chunks[chunkIndex];
		var returnValue = chunk.charCodeAt(charOffsetWithinChunk);
		return returnValue;
	}

	StringChunked.prototype.length = function()
	{
		var returnValue =  
			((this.chunks.length - 1) * this.charsPerChunk)
			+ this.chunks[this.chunks.length - 1].length;
			
		return returnValue;
	}
}

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

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

A Key Sequence Recorder in JavaScript

The JavaScript code below implements a key sequence recorder in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

When the Record button is clicked, the program will begin recording the times which certain keys (up to five distinct keys) on the keyboard are pressed and released. The recording continues until the Stop button is pressed. This information is displayed graphically as it is recorded or played back, and can also be exported and imported to text.

This code is intended as a first step towards using a keyboard as the user interface for a musical instrument. It might also conceivably be useful for recording sequences of key presses for testing of real-time software, though it would probably need to be adapted somewhat for that purpose.

KeySequenceRecorder.png


<html>
<body>
<div id="divUI">

	<b>Sequence Recorder</b>
	<div>
		<button onclick="buttonRecord_Clicked();">Record</button>
		<button onclick="buttonStop_Clicked();">Stop</button>
		<button onclick="buttonPlay_Clicked();">Play</button></div>
	<div id="divDisplay"></div>
	<div>
		<label>Sequence Serialized:</label>
		<button onclick="buttonExport_Clicked();">Export</button>
		<button onclick="buttonImport_Clicked();">Import</button>

		<textarea id="textareaSequenceSerialized" cols="60" rows="10"></textarea>
	</div>
</div>
<script type="text/javascript">

// event handlers

function buttonImport_Clicked()
{
	var textareaSequenceSerialized = 
		document.getElementById("textareaSequenceSerialized");
	var sequenceSerialized = textareaSequenceSerialized.value;
	var sequence = Sequence.deserialize(sequenceSerialized);
	Session.Instance.sequenceRecorded = sequence;
	Session.Instance.draw();
}

function buttonExport_Clicked()
{
	var sequence = Session.Instance.sequenceRecorded;
	var sequenceSerialized = sequence.serialize();
	var textareaSequenceSerialized = 
		document.getElementById("textareaSequenceSerialized");
	textareaSequenceSerialized.value = sequenceSerialized;
}

function buttonPlay_Clicked()
{
	Session.Instance.play();
}

function buttonRecord_Clicked()
{
	Session.Instance.record();
}

function buttonStop_Clicked()
{
	Session.Instance.stop();
}

// extensions

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

	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex != -1)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

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

function Display(size)
{
	this.size = size;
}
{
	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 divDisplay = document.getElementById("divDisplay");
		divDisplay.appendChild(this.canvas);
	}
	
	// drawing
	
	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = "White";
		this.graphics.fillRect
		(
			0, 0, this.size.x, this.size.y
		);
		
		this.graphics.strokeStyle = "Gray";
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);		
	}
	
	Display.prototype.drawCircle = function(pos, radius, colorFill, colorBorder)
	{
		this.graphics.beginPath();
		this.graphics.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
	
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fill();
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.stroke();
		}
	}
	
	Display.prototype.drawLine = function(fromPos, toPos, color)
	{
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.strokeStyle = color;		
		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.drawText = function(text, pos, color)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y);
	}
}

function Event(offsetInSeconds, isPressNotRelease)
{
	this.offsetInSeconds = offsetInSeconds;
	this.isPressNotRelease = isPressNotRelease;
}
{	
	Event.prototype.offsetInMilliseconds = function()
	{
		return Math.round(this.offsetInSeconds * 1000);
	}

	// drawable

	Event.MarkerRadius = 3;
	Event.MarkerSize = new Coords(5, 5);
	
	Event.prototype.draw = function(display, pixelsPerSecond, viewOffsetInSecondsMin, yPos)
	{
		var xPos = this.draw_XPos(pixelsPerSecond, viewOffsetInSecondsMin);
		var drawPos = new Coords(xPos, yPos);
	
		if (this.isPressNotRelease == true)
		{
			display.drawCircle(drawPos, Event.MarkerRadius, null, "Gray");
		}
		else
		{
			var markerSize = Event.MarkerSize;
			drawPos.x -= markerSize.x / 2;
			drawPos.y -= markerSize.x / 2;			
			display.drawRectangle(drawPos, markerSize, null, "Gray");
		}
	}
	
	Event.prototype.draw_XPos = function(pixelsPerSecond, viewOffsetInSecondsMin)
	{
		var returnValue = 
			(this.offsetInSeconds - viewOffsetInSecondsMin) 
			* pixelsPerSecond;
			
		return returnValue;
	}
}

function InputHelper()
{
	this.inputsPressed = [];
	this.inputsActive = [];
	this.inputsReleased = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}
		
	// events
	
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var input = event.key;
		
		if (this.inputsPressed[input] == null)
		{
			this.inputsPressed.push(input);		
			this.inputsPressed[input] = input;
			
			this.inputsActive.push(input);
			this.inputsActive[input] = input;
		}
	}
	
	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		var input = event.key;
		if (this.inputsPressed[input] != null)
		{
			this.inputsPressed.remove(input);
			delete this.inputsPressed[input];

			this.inputsActive.remove(input);
			delete this.inputsActive[input];
			
			this.inputsReleased.push(input);
			this.inputsReleased[input] = input;
		}
	}	
}

function Sequence(tracks)
{
	this.tracks = tracks.addLookups("input");
}
{
	Sequence.prototype.draw = function(display, pixelsPerSecond, cursorOffsetInSeconds)
	{
		var secondsPerDisplay = display.size.x / pixelsPerSecond;	
		var secondsPerDisplayHalf = secondsPerDisplay / 2;
		var viewOffsetMinInSeconds = cursorOffsetInSeconds - secondsPerDisplayHalf;
		var viewOffsetMaxInSeconds = viewOffsetMinInSeconds + secondsPerDisplay;

		display.clear();
		
		for (var t = 0; t < this.tracks.length; t++)
		{
			var track = this.tracks[t];
			var trackPosY = 
				display.size.y / (this.tracks.length + 1) * (t + 1);
			track.draw
			(
				display, 
				pixelsPerSecond, 
				cursorOffsetInSeconds,
				viewOffsetMinInSeconds, 
				viewOffsetMaxInSeconds,
				trackPosY
			);
		}
		
		if (viewOffsetMinInSeconds <= 0)
		{
			var eventDummy = new Event(0, null);
			var zeroPosX = eventDummy.draw_XPos
			(
				pixelsPerSecond, viewOffsetMinInSeconds
			);
		
			display.drawLine
			(
				new Coords(zeroPosX, 0),
				new Coords(zeroPosX, display.size.y),
				"Gray"
			)
		}
		
		var cursorPosX = display.size.x / 2;
		display.drawLine
		(
			new Coords(cursorPosX, 0), 
			new Coords(cursorPosX, display.size.y),
			"Gray"
		);
		
		var cursorOffsetInMilliseconds = Math.round
		(
			cursorOffsetInSeconds * 1000
		);
		
		display.drawText
		(
			"" + cursorOffsetInMilliseconds, 
			new Coords(cursorPosX, display.size.y), 
			"Gray"
		);
	}
	
	// serialization
	
	Sequence.deserialize = function(sequenceSerialized)
	{
		var tracksSerialized = sequenceSerialized.split("\n");
		var tracks = [];
		for (var i = 0; i < tracksSerialized.length; i++)
		{
			var trackSerialized = tracksSerialized[i];
			var track = Track.deserialize(trackSerialized);
			tracks.push(track);
		}
		
		var returnValue = new Sequence(tracks);
		return returnValue;
	}
	
	Sequence.prototype.serialize = function()
	{
		var tracksSerialized = [];
		for (var i = 0; i < this.tracks.length; i++)
		{
			var track = this.tracks[i];
			var trackSerialized = track.serialize();
			tracksSerialized.push(trackSerialized);
		}
		
		var returnValue = tracksSerialized.join("\n");
		return returnValue;
	}
}

function Session(displaySize, pixelsPerSecond)
{
	this.pixelsPerSecond = pixelsPerSecond;

	this.display = new Display(displaySize);	
	this.display.initialize();
	this.display.clear();
	
	this.inputHelper = new InputHelper();
	this.inputHelper.initialize();
	
	this.status = SessionStatus.Stopped;
	
	this.millisecondsPerTimerTick = 50;
	this.numberOfTracksMax = 5;
	this.cursorOffsetInSeconds = 0;
}
{
	Session.Instance = new Session
	(
		new Coords(500, 100), // displaySize
		100 // pixelsPerSecond
	);
	
	Session.prototype.play = function()
	{
		this.status = SessionStatus.Playing;
		this.timeStarted = new Date();
		this.timer = setInterval
		(
			this.play_Output.bind(this),
			this.millisecondsPerTimerTick
		);
	}	
	
	Session.prototype.play_Output = function(event)
	{
		var now = new Date();
		var cursorOffsetInMilliseconds = now - this.timeStarted;
		this.cursorOffsetInSeconds = cursorOffsetInMilliseconds / 1000;
		this.draw();
	}
	
	Session.prototype.record = function()
	{
		this.status = SessionStatus.Recording;
		this.timeStarted = new Date();
		this.sequenceRecorded = new Sequence([]);

		this.timer = setInterval
		(
			this.record_Input.bind(this),
			this.millisecondsPerTimerTick
		);
	}
	
	Session.prototype.record_Input = function()
	{
		var now = new Date();
		var offsetInMilliseconds = now - this.timeStarted;
		this.cursorOffsetInSeconds = offsetInMilliseconds / 1000;
	
		var tracks = this.sequenceRecorded.tracks;

		var inputsActive = this.inputHelper.inputsActive;
		for (var i = 0; i < inputsActive.length; i++)
		{
			var input = inputsActive[i];
			
			var trackForInput = tracks[input];
			if (trackForInput == null)
			{
				if (tracks.length < this.numberOfTracksMax)
				{
					trackForInput = new Track(input, []);
					tracks.push(trackForInput);
					tracks[input] = trackForInput;
				}
				else
				{
					continue;
				}
			}
			
			var event = new Event(this.cursorOffsetInSeconds, true);
			trackForInput.events.push(event);			
			
			delete inputsActive[input];
			inputsActive.remove(input);
		}
		
		var inputsReleased = this.inputHelper.inputsReleased;
		for (var i = 0; i < inputsReleased.length; i++)
		{
			var input = inputsReleased[i];
			var trackForInput = tracks[input];
			
			if (trackForInput != null)
			{
				var event = new Event(this.cursorOffsetInSeconds, false);
				trackForInput.events.push(event);
			}
				
			inputsReleased.remove(input);
			delete inputsReleased[input];
		}
		
		this.draw();
	}
	
	Session.prototype.stop = function()
	{
		this.status = SessionStatus.Stopped;
		if (this.timer != null)
		{
			clearInterval(this.timer);
		}
	}
	
	// drawing
	
	Session.prototype.draw = function()
	{
		this.sequenceRecorded.draw
		(
			this.display, this.pixelsPerSecond, this.cursorOffsetInSeconds
		);	
	}
}

function SessionStatus(name)
{
	this.name = name;
}
{
	SessionStatus.Playing = new SessionStatus("Playing");
	SessionStatus.Recording = new SessionStatus("Recording");
	SessionStatus.Stopped = new SessionStatus("Stopped");	
}

function Span(eventStart, eventStop)
{
	this.eventStart = eventStart;
	this.eventStop = eventStop;
}
{
	Span.prototype.draw = function(display, pixelsPerSecond, viewOffsetMinInSeconds, trackPosY)
	{
		this.eventStart.draw(display, pixelsPerSecond, viewOffsetMinInSeconds, trackPosY);

		var xPosStart = this.eventStart.draw_XPos(pixelsPerSecond, viewOffsetMinInSeconds);
		var xPosStop = this.eventStop.draw_XPos(pixelsPerSecond, viewOffsetMinInSeconds);
		
		display.drawLine
		(
			new Coords(xPosStart, trackPosY),
			new Coords(xPosStop, trackPosY),
			"Gray"
		);
		
		this.eventStop.draw(display, pixelsPerSecond, viewOffsetMinInSeconds, trackPosY);
	}
	
	// serialization
	
	Span.deserialize = function(spanSerialized)
	{
		var parts = spanSerialized.split("_");
		var eventStart = new Event((parseFloat(parts[0]) / 1000), true);
		var eventStop = new Event((parseFloat(parts[1]) / 1000), false);
		var returnValue = new Span(eventStart, eventStop);
		return returnValue;
	}
	
	Span.prototype.serialize = function()
	{
		var returnValue = 
			this.eventStart.offsetInMilliseconds() + "_" 
			+ this.eventStop.offsetInMilliseconds();
		return returnValue;
	}
}

function Track(input, events)
{
	this.input = input;
	this.events = events;
}
{
	// drawing

	Track.prototype.draw = function
	(
		display, 
		pixelsPerSecond, 
		cursorOffsetInSeconds, 
		viewOffsetMinInSeconds, 
		viewOffsetMaxInSeconds, 
		yPos
	)
	{	
		display.drawLine
		(
			new Coords(0, yPos),
			new Coords(display.size.x, yPos),			
			"LightGray"
		);
	
		display.drawText
		(
			this.input, new Coords(0, yPos), "Gray"
		);
				
		var eventPrev = null;
	
		for (var i = 0; i < this.events.length; i++) 		
		{
 			var event = this.events[i];
 			if (event.offsetInSeconds >= viewOffsetMinInSeconds)
			{
				if (event.offsetInSeconds >= viewOffsetMaxInSeconds)
				{
					break;
				}
				else
				{					
					var span = null;
					if (event.isPressNotRelease == true)
					{
						var eventNext = this.events[i + 1];
						if (eventNext == null)
						{
							var eventDummy = new Event
							(
								cursorOffsetInSeconds, false
							);
							span = new Span(event, eventDummy)
						}
						else
						{
							span = new Span(event, eventNext);
						}
					}
					else if (eventPrev != null)
					{
						span = new Span(eventPrev, event);	
					}
					
					if (span != null)
					{
						span.draw(display, pixelsPerSecond, viewOffsetMinInSeconds, yPos);
					}
				}
			}
			
			eventPrev = event;
			
		} // end for each event
	}
	
	// serialization
	
	Track.deserialize = function(trackSerialized)
	{
		var parts = trackSerialized.split("=");
		var input = parts[0];
		var spansSerialized = parts[1].split(",");
		var events = [];
		for (var i = 0; i < spansSerialized.length; i++)
		{
			var spanSerialized = spansSerialized[i];
			var span = Span.deserialize(spanSerialized);
			events.push(span.eventStart);
			events.push(span.eventStop);
		}
		
		var returnValue = new Track(input, events);
		return returnValue;
	}
	
	Track.prototype.serialize = function()
	{
		var spansSerialized = [];
		for (var e = 0; e < this.events.length; e += 2)
		{
			var eventStart = this.events[e];
			var eventStop = this.events[e + 1];
			var span = new Span(eventStart, eventStop);
			var spanSerialized = span.serialize();
			spansSerialized.push(spanSerialized);
		}
		
		var returnValue = 
			this.input + "=" + spansSerialized.join(",");
		return returnValue;
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , | Leave a comment

A Scrolling Map Tile Engine in JavaScript

The JavaScript below implements a simple engine for allowing an animated character to move around a map made of tiles. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This code was originally intended to be the first steps in implementing the exploration engine for a role-playing game.

ScrollableMapWithAnimatedCharacter.png


<html>
<body>

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

<script type="text/javascript">

// main

function main()
{
	var display = new Display(new Coords(200, 200));
	var mapCellSizeInPixels = new Coords(16, 16);
	var world = World.demo(display.sizeInPixels, mapCellSizeInPixels);
	var universe = new Universe
	(
		10, // timerTicksPerSecond
		display, 
		world
	);
	universe.start();
}

// 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.clone = function()
	{
		var returnValues = [];
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var elementCloned = element.clone();
			returnValues.push(elementCloned);
		}
		return returnValues;
	}
	
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Activity(defnName, target)
{
	this.defnName = defnName;
	this.target = target;
}
{
	Activity.prototype.defn = function(world)
	{
		return world.activityDefns[this.defnName];
	}
	
	Activity.prototype.perform = function(universe, world, venue, actor)
	{
		this.defn(world).perform(universe, world, venue, actor, this);
	}
}

function ActivityDefn(name, perform)
{
	this.name = name;
	this.perform = perform;
}

function Camera(viewSize, pos)
{
	this.viewSize = viewSize;
	this.pos = pos;
	
	this.viewSizeHalf = this.viewSize.clone().half();
}

function Color(name, code, componentsRGBA)
{
	this.name = name;
	this.code = code;
	this.componentsRGBA = componentsRGBA;
	
	this.systemColor = 
		"rgba(" 
		+ Math.floor(this.componentsRGBA[0] * Color.ComponentMax) + ","
		+ Math.floor(this.componentsRGBA[1] * Color.ComponentMax) + ","
		+ Math.floor(this.componentsRGBA[2] * Color.ComponentMax) + ","
		+ this.componentsRGBA[3] // ?
		+ ")";	
}
{
	Color.ComponentMax = 255;

	Color.Instances = function()
	{
		if (Color._instances == null)
		{
			Color._instances = new Color_Instances();
		}
		return Color._instances;
	}
	
	function Color_Instances()
	{
		this._Transparent = new Color("Transparent", ".", [0, 0, 0, 0]);
		this.Black 	= new Color("Black", "k", [0, 0, 0, 1]);
		this.Blue 	= new Color("Blue", "b", [0, 0, 1, 1]);
		this.Cyan 	= new Color("Cyan", "c", [0, 1, 1, 1]);	
		this.Gray 	= new Color("Gray", "a", [.5, .5, .5, 1]);
		this.GrayDark = new Color("GrayDark", "A", [.25, .25, .25, 1]);
		this.GrayLight = new Color("GrayLight", "-", [.75, .75, .75, 1]);		
		this.Green 	= new Color("Green", "g", [0, 1, 0, 1]);
		this.Orange = new Color("Orange", "o", [1, .5, 0, 1]);
		this.Red 	= new Color("Red", "r", [1, 0, 0, 1]);
		this.Violet = new Color("Violet", "v", [1, 0, 1, 1]);
		this.White 	= new Color("White", "w", [1, 1, 1, 1]);
		this.Yellow = new Color("Yellow", "y", [1, 1, 0, 1]);		

		this._All = 
		[
			this._Transparent,
			this.Black,
			this.Blue,
			this.Cyan,
			this.Gray,
			this.GrayDark,
			this.GrayLight,
			this.Green,
			this.Orange,
			this.Red,
			this.Violet,
			this.White,
			this.Yellow,
		].addLookups("code");
	}
}

function Constraint_Follow(target)
{
	this.target = target;
}
{
	Constraint_Follow.prototype.apply = function(constrainable)
	{
		constrainable.pos.overwriteWith(this.target.pos);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}
	
	Coords.prototype.addXY = function(x, y)
	{
		this.x += x;
		this.y += y;
		return this;
	}
	
	Coords.prototype.ceiling = function()
	{
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);
		return this;
	}
		
	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;
		return this;
	}
	
	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}
	
	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		return this;
	}
		
	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}
	
	Coords.prototype.floor = function()
	{
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);
		return this;
	}
		
	Coords.prototype.half = function()
	{
		return this.divideScalar(2);
	}
	
	Coords.prototype.isInRangeMax = function(max)
	{
		var returnValue = 
		(
			this.x >= 0
			&& this.x <= max.x
			&& this.y >= 0
			&& this.y <= max.y
		);
		
		return returnValue;
	}
		
	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.round = function()
	{
		this.x = Math.round(this.x);
		this.y = Math.round(this.y);
		return this;
	}
			
	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}
	
	Coords.prototype.trimToRangeMax = function(max)
	{
		if (this.x < 0)
		{
			this.x = 0;
		}
		else if (this.x > max.x)
		{
			this.x = max.x;
		}
		
		if (this.y < 0)
		{
			this.y = 0;
		}
		else if (this.y > max.y)
		{
			this.y = max.y;
		}
		
		return this;
	}
}

function Display(sizeInPixels)
{
	this.sizeInPixels = sizeInPixels;
	this.fontSizeInPixels = Math.floor(this.sizeInPixels.y / 32);
	
	this.sizeInPixelsHalf = this.sizeInPixels.clone().half();
	
	this.drawPos = new Coords();
}
{
	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.sizeInPixels.x;
		this.canvas.height = this.sizeInPixels.y;
		this.graphics = this.canvas.getContext("2d");
		this.graphics.font = this.fontSizeInPixels + "px sans-serif";
		return this;
	}
	
	Display.prototype.toImage = function(name)
	{
		var dataURL = this.canvas.toDataURL();
		var systemImage = document.createElement("img");
		systemImage.src = dataURL;
		var returnValue = new Image(name, this.sizeInPixels, systemImage);
		return returnValue;
	}
	
	// drawing
	
	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = "White";
		this.graphics.fillRect(0, 0, this.sizeInPixels.x, this.sizeInPixels.y);
		this.graphics.strokeStyle = "Gray";
		this.graphics.strokeRect(0, 0, this.sizeInPixels.x, this.sizeInPixels.y);

		return this;		
	}
		
	Display.prototype.clearRectangle = function(pos, size)
	{
		this.graphics.clearRect(pos.x, pos.y, size.x, size.y);
		return this; 
	}
	
	Display.prototype.drawCircle = function(center, radius, colorFill, colorBorder)
	{
		this.graphics.beginPath();		
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);

		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fill();	
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.stroke();	
		}
		
		return this;
	}
	
	Display.prototype.drawImage = function(image, pos)
	{
		this.graphics.drawImage(image.systemImage, pos.x, pos.y);
		
		return this;
	}
	
	Display.prototype.drawImageRegion = function(image, sourcePos, sourceSize, targetPos)
	{
		var targetSize = sourceSize;
	
		this.graphics.drawImage
		(
			image.systemImage, 
			sourcePos.x, sourcePos.y,
			sourceSize.x, sourceSize.y,
			targetPos.x, targetPos.y,
			targetSize.x, targetSize.y
		);
		
		return this;
	}
		
	Display.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 = colorBorder;
			this.graphics.stroke();	
		}
		
		return this;
	}
		
	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);
		}
		
		return this;
	}
	
	Display.prototype.drawText = function(text, pos, colorFill, colorBorder)
	{
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = color;
			this.graphics.strokeString(text, pos.x, pos.y);
		}
	
		if (colorFill != null)
		{
			this.graphics.fillStyle = color;
			this.graphics.fillString(text, pos.x, pos.y);
		}
		
		return this;
	}
}

function Image(name, size, systemImage)
{
	this.name = name;
	this.size = size;
	this.systemImage = systemImage;
	
	this.sizeHalf = this.size.clone().half();
}
{
	Image.fromStrings = function(name, colors, pixelsAsStrings)
	{
		var size = new Coords
		(
			pixelsAsStrings[0].length, pixelsAsStrings.length
		);
		var canvas = document.createElement("canvas");
		canvas.width = size.x;
		canvas.height = size.y;
		var graphics = canvas.getContext("2d");
		
		for (var y = 0; y < size.y; y++)
		{
			var pixelRowAsString = pixelsAsStrings[y];
			
			for (var x = 0; x < size.x; x++)
			{
				var pixelColorCode = pixelRowAsString[x];
				var pixelColor = colors[pixelColorCode];
				graphics.fillStyle = pixelColor.systemColor;
				graphics.fillRect(x, y, 1, 1);
			}
		}
		
		var systemImage = document.createElement("img");
		systemImage.src = canvas.toDataURL();
		
		var returnValue = new Image
		(
			name, size, systemImage
		);
		
		return returnValue;
	}
	
	Image.prototype.toDisplay = function()
	{
		return new Display(this.size).initialize().drawImage(this, new Coords(0, 0));
	}
}

function InputHelper()
{
	this.inputsPressed = [];
	this.inputsActive = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}
	
	InputHelper.prototype.inputActivate = function(input)
	{
		if (this.inputsActive[input] == null)
		{
			this.inputsActive[input] = input;
			this.inputsActive.push(input);
		}	
	}	
	
	InputHelper.prototype.inputAdd = function(input)
	{
		if (this.inputsPressed[input] == null)
		{
			this.inputsPressed[input] = input;
			this.inputsPressed.push(input);
			this.inputActivate(input);
		}	
	}
	
	InputHelper.prototype.inputInactivate = function(input)
	{
		if (this.inputsActive[input] != null)
		{
			delete this.inputsActive[input];
			this.inputsActive.remove(input);
		}
	}
	
	InputHelper.prototype.inputRemove = function(input)
	{
		this.inputInactivate(input);
		if (this.inputsPressed[input] != null)
		{
			delete this.inputsPressed[input];
			this.inputsPressed.remove(input);
		}
	}
	
	// events
	
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.inputAdd(event.key);
	}
	
	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.inputRemove(event.key);
	}	
}

function Map(cellSizeInPixels, terrains, cellsAsStrings)
{
	this.cellSizeInPixels = cellSizeInPixels;
	this.terrains = terrains.addLookups("code");
	this.cellsAsStrings = cellsAsStrings;
	
	this.sizeInCells = new Coords
	(
		cellsAsStrings[0].length, cellsAsStrings.length
	);
	
	this.sizeInCellsMinusOnes = this.sizeInCells.clone().addXY
	(
		-1, -1
	);
	
	this.cellPos = new Coords();
	this.drawPos = new Coords();
}
{
	Map.prototype.terrainAtPosInCells = function(posInCells)
	{
		var terrainCode = this.cellsAsStrings[posInCells.y][posInCells.x];
		var terrain = this.terrains[terrainCode];
		return terrain;
	}

	// drawable

	Map.prototype.draw = function(universe, world, display, visualCamera)
	{
		var cellPos = this.cellPos;
		var drawPos = this.drawPos;
		var cell = {};
		cell.pos = drawPos;
		cell.velInCellsPerTick = new Coords(0, 0); // hack
		var halves = new Coords(.5, .5);
		var ones = new Coords(1, 1);
		
		var camera = visualCamera.camera;
		var cameraPos = camera.pos;
		var cameraViewSizeHalf = camera.viewSizeHalf;
		var cellPosMin = cameraPos.clone().subtract
		(
			cameraViewSizeHalf
		).divide
		(
			this.cellSizeInPixels
		).floor().trimToRangeMax
		(
			this.sizeInCellsMinusOnes
		);
		var cellPosMax = cameraPos.clone().add
		(
			cameraViewSizeHalf
		).divide
		(
			this.cellSizeInPixels
		).ceiling().trimToRangeMax
		(
			this.sizeInCellsMinusOnes
		);
						
		for (var y = cellPosMin.y; y <= cellPosMax.y; y++)
		{
			cellPos.y = y;
			
			for (var x = cellPosMin.x; x <= cellPosMax.x; x++)
			{
				cellPos.x = x;
				
				drawPos.overwriteWith
				(
					cellPos
				).add(halves).multiply
				(
					this.cellSizeInPixels
				);
				
				var terrain = this.terrainAtPosInCells(cellPos);
				var terrainVisual = terrain.visual;
				visualCamera.child = terrainVisual;				
				visualCamera.draw
				(
					universe, world, display, cell
				);
			}
		}

		var sizeDiminished = this.sizeInCellsMinusOnes.clone().addXY(-1, -1);
		
		var cornerPosMin = cellPosMin.clone().addXY(-1, -1).trimToRangeMax(this.sizeInCells);
		var cornerPosMax = cellPosMax.trimToRangeMax(sizeDiminished);
		var cornerPos = new Coords();
		var neighborOffsets = 
		[
			new Coords(0, 0),		
			new Coords(1, 0),
			new Coords(0, 1),			
			new Coords(1, 1),
		];
		var neighborPos = new Coords();
		var neighborTerrains = [];
				
		for (var y = cornerPosMin.y; y <= cornerPosMax.y; y++)
		{
			cornerPos.y = y;
			
			for (var x = cornerPosMin.x; x <= cornerPosMax.x; x++)
			{
				cornerPos.x = x;
				
				var neighborOffset = neighborOffsets[0];
				neighborPos.overwriteWith(cornerPos).add(neighborOffset);
				var terrainHighestSoFar = this.terrainAtPosInCells(neighborPos);
				
				for (var n = 1; n < neighborOffsets.length; n++)
				{
					var neighborOffset = neighborOffsets[n];
					neighborPos.overwriteWith(cornerPos).add(neighborOffset);
					var neighborTerrain = this.terrainAtPosInCells(neighborPos);
					var zLevelDifference = 
						neighborTerrain.zLevelForOverlays 
						- terrainHighestSoFar.zLevelForOverlays;
					if (zLevelDifference > 0)
					{
						terrainHighestSoFar = neighborTerrain;
					}
				}
				
				var terrainHighest = terrainHighestSoFar;
				var visualChildIndexSoFar = 0;
		
				for (var n = 0; n < neighborOffsets.length; n++)
				{
					var neighborOffset = neighborOffsets[n];
					neighborPos.overwriteWith(cornerPos).add(neighborOffset);
					var neighborTerrain = this.terrainAtPosInCells(neighborPos);
					if (neighborTerrain != terrainHighest)
					{
						visualChildIndexSoFar |= (1 << n);
					}
				}
				
				if (visualChildIndexSoFar > 0)
				{
					drawPos.overwriteWith
					(
						cornerPos
					).add(ones).multiply
					(
						this.cellSizeInPixels
					);
					
					var terrainVisual = terrainHighest.visual.children[visualChildIndexSoFar];
					if (terrainVisual != null) // hack
					{
						visualCamera.child = terrainVisual;				
						visualCamera.draw
						(
							universe, world, display, cell
						);
					}
				}
			}
		}
	}
}

function MapTerrain(name, code, blocksMovement, zLevelForOverlays, visual)
{
	this.name = name;
	this.code = code;
	this.blocksMovement = blocksMovement;
	this.zLevelForOverlays = zLevelForOverlays;
	this.visual = visual;
}

function MapTerrainVisual(children)
{
	this.children = children;
	
	var childNames = MapTerrainVisual.ChildNames;
		
	for (var i = 0; i < childNames.length; i++)
	{
		var childName = childNames[i];
		var child = this.children[i];
		this.children[childName] = child;
	}
}
{
	MapTerrainVisual.TestInstance = function()
	{
		// Helpful for debugging.	
		var radius = 3;
		var size = new Coords(5, 5);
		return new MapTerrainVisual
		(
			[
				new VisualRectangle(size, null, "Black"), // 0000 - center
				new VisualCircle(radius, "Red", "Black"), // 0001 - inside se
				new VisualCircle(radius, "Orange", "Black"), // 0010 - inside sw
				new VisualCircle(radius, "Yellow","Black"), // 0011 - edge n
				new VisualCircle(radius, "Green", "Black"), // 0100 - inside ne
				new VisualCircle(radius, "Blue", "Black"),  // 0101 - edge w
				new VisualCircle(radius, "Violet", "Black"), // 0110 - diagonal
				new VisualCircle(radius, "Gray", "Black"),  // 0111 - outside se
				new VisualRectangle(size, "Red", "Black"), // 1000 - inside nw
				new VisualRectangle(size, "Orange", "Black"), // 1001 - diagonal?
				new VisualRectangle(size, "Yellow", "Black"), // 1010 - edge e
				new VisualRectangle(size, "Green", "Black"), // 1011 - outside sw
				new VisualRectangle(size, "Blue", "Black"), // 1100 - edge s
				new VisualRectangle(size, "Violet", "Black"), // 1101 - outside ne
				new VisualRectangle(size, "Gray", "Black"), // 1110 - outside nw
				new VisualRectangle(size, null, "Red"), // 1111 // Never
			]
		);
	}
	
	MapTerrainVisual.ChildNames = 
	[
		"Center",
		"InsideSE",
		"InsideSW",	
		"EdgeN",		
		"InsideNE",
		"EdgeW",
		"DiagonalSlash",
		"OutsideSE",
		"InsideNW",
		"DiagonalBackslash",
		"EdgeE",
		"OutsideSW",
		"EdgeS",
		"OutsideNE",
		"OutsideNW"
	]


	MapTerrainVisual.prototype.draw = function(universe, world, display, drawable)
	{
		this.children["Center"].draw(universe, world, display, drawable);
	}
}

function Mover(name, visual, activity, posInCells)
{
	this.name = name;
	this.visual = visual;
	this.activity = activity;
	this.posInCells = posInCells;

	this.pos = new Coords();
	this.posInCellsNext = new Coords();
	this.posInCellsNextFloor = new Coords();	
	this.velInCellsPerTick = new Coords(0, 0);
}
{
	Mover.prototype.updateForTimerTick = function(universe, world, venue)
	{		
		this.posInCellsNext.overwriteWith
		(
			this.posInCells
		).add
		(
			this.velInCellsPerTick
		);
		
		var map = venue.map;		
		this.posInCellsNextFloor.overwriteWith(this.posInCellsNext).floor();		
		var mapTerrain = map.terrainAtPosInCells(this.posInCellsNextFloor);
		if (mapTerrain.blocksMovement == false)
		{
			this.posInCells.overwriteWith(this.posInCellsNext);
		}
		
		this.pos.overwriteWith
		(
			this.posInCells
		).multiply
		(
			map.cellSizeInPixels
		);
		
		this.activity.perform(universe, world, venue, this);		
	}
	
	// drawable
	
	Mover.prototype.draw = function(universe, world, visualCamera)
	{
		visualCamera.child = this.visual;
		visualCamera.draw(universe, world, universe.display, this);
	}	
}

function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2.0;

	Polar.prototype.fromCoords = function(coords)
	{
		var azimuthInRadians = Math.atan2(coords.y, coords.x);
		var azimuthInTurns = azimuthInRadians / Polar.RadiansPerTurn;
		if (azimuthInTurns < 0)
		{
			azimuthInTurns += 1;
		}
		this.azimuthInTurns = azimuthInTurns;
		this.radius = coords.magnitude();
		return this;
	}
}

function Portal(defnName, posInCells, destinationVenueName, destinationPosInCells)
{
	this.defnName = defnName;
	this.posInCells = posInCells.addXY(.5, .5);
	this.destinationVenueName = destinationVenueName;
	this.destinationPosInCells = destinationPosInCells;
	
	this.pos = new Coords();
}
{
	Portal.prototype.defn = function(world)
	{
		return world.portalDefns[this.defnName];
	}

	Portal.prototype.activate = function(universe, world, venue, actor)
	{
		var mover = actor;
		venue.moversToRemove.push(mover);
		var venueNext = world.venues[this.destinationVenueName];
		venueNext.movers.splice(0, 0, mover);
		mover.posInCells.overwriteWith(this.destinationPosInCells);
		world.venueNext = venueNext;
	}
	
	Portal.prototype.updateForTimerTick = function(universe, world, venue)
	{
		this.pos.overwriteWith(this.posInCells).multiply(venue.map.cellSizeInPixels);	
	}
	
	// drawable
		
	Portal.prototype.draw = function(universe, world, visualCamera)
	{
		var defn = this.defn(world);
		var visual = defn.visual;
		visualCamera.child = visual;
		visualCamera.draw(universe, world, universe.display, this);
	}
}

function PortalDefn(name, visual)
{
	this.name = name;
	this.visual = visual;
}

function Universe(timerTicksPerSecond, display, world)
{
	this.timerTicksPerSecond = timerTicksPerSecond;
	this.display = display;
	this.world = world;
	
	this.secondsPerTimerTick = 1 / this.timerTicksPerSecond;
}
{
	Universe.prototype.start = function()
	{
		var divMain = document.getElementById("divMain");
		divMain.appendChild(this.display.initialize().canvas);
				
		var timerTicksPerSecond = 10;
		var msPerSecond = 1000;
		var msPerTimerTick = Math.floor(msPerSecond / timerTicksPerSecond);
		this.timer = setInterval
		(
			this.updateForTimerTick.bind(this),
			msPerTimerTick
		);

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();
		
		this.world.initialize(this);		
	}
	
	Universe.prototype.updateForTimerTick = function()
	{
		this.world.draw(this);		
		this.world.updateForTimerTick(this);
	}
}

function VisualAnimation(framesPerSecond, frames)
{
	this.framesPerSecond = framesPerSecond;
	this.frames = frames;
	
	this.durationInSeconds = this.frames.length / this.framesPerSecond;
}
{
	VisualAnimation.prototype.draw = function(universe, world, display, drawable)
	{
		if (drawable.secondsSinceAnimationStarted == null)
		{
			drawable.secondsSinceAnimationStarted = 0;
		}
		
		var frameIndexCurrent = Math.floor
		(
			drawable.secondsSinceAnimationStarted * this.framesPerSecond
		);
		
		var frameCurrent = this.frames[frameIndexCurrent];
		frameCurrent.draw(universe, world, display, drawable);
		
		drawable.secondsSinceAnimationStarted += universe.secondsPerTimerTick;
		if (drawable.secondsSinceAnimationStarted >= this.durationInSeconds)
		{
			drawable.secondsSinceAnimationStarted -= this.durationInSeconds;
		}
	}
}

function VisualCamera(camera, child)
{
	this.camera = camera;
	this.child = child;
	
	this.drawablePosOriginal = new Coords();
}
{
	VisualCamera.prototype.draw = function(universe, world, display, drawable)
	{
		this.drawablePosOriginal.overwriteWith(drawable.pos);
		drawable.pos.subtract
		(
			this.camera.pos
		).add
		(
			display.sizeInPixelsHalf
		);
		this.child.draw(universe, world, display, drawable);
		drawable.pos.overwriteWith(this.drawablePosOriginal);
	}
}

function VisualCircle(radius, colorFill, colorBorder)
{
	this.radius = radius;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;
}
{
	VisualCircle.prototype.draw = function(universe, world, display, drawable)
	{
		display.drawCircle(drawable.pos, this.radius, this.colorFill, this.colorBorder);
	}
}

function VisualDirectional(visualAtRest, visualsForDirections)
{
	this.visualAtRest = visualAtRest;
	this.visualsForDirections = visualsForDirections;
	
	this.polar = new Polar();
}
{
	VisualDirectional.prototype.draw = function(universe, world, display, drawable)
	{
		var visualToDraw = null;
		var vel = drawable.velInCellsPerTick;
		if (vel.magnitude() == 0)
		{
			visualToDraw = this.visualAtRest;
		}
		else
		{
			this.polar.fromCoords(vel);
			var azimuthInTurns = this.polar.azimuthInTurns;
			var directionIndex = Math.floor(azimuthInTurns * this.visualsForDirections.length);
			visualToDraw = this.visualsForDirections[directionIndex];
		}
		visualToDraw.draw(universe, world, display, drawable);
	}
}

function VisualGroup(children)
{
	this.children = children;
}
{
	VisualGroup.prototype.draw = function(universe, world, display, drawable)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.draw(universe, world, display, drawable);
		}
	}
}

function VisualImage(image, size)
{
	this.image = image;
	this.size = (size == null ? this.image.size : size);
	
	this.sizeHalf = this.size.clone().half();
	
	this.drawPos = new Coords();
}
{
	VisualImage.manyFromImages = function(images)
	{
		var returnValues = [];
		for (var i = 0; i < images.length; i++)
		{
			var image = images[i];
			var visual = (image == null ? null : new VisualImage(image));
			returnValues.push(visual);
		}
		return returnValues;
	}

	VisualImage.prototype.draw = function(universe, world, display, drawable)
	{		
		var drawPos = this.drawPos.overwriteWith
		(
			drawable.pos
		).subtract
		(
			this.sizeHalf
		);
	
		display.drawImage(this.image, drawPos);
	}
}

function VisualImageRegion(image, offset, size)
{
	this.image = image;
	this.offset = offset;
	this.size = size;
	
	this.sizeHalf = this.size.clone().half();
	
	this.drawPos = new Coords();
}
{
	VisualImageRegion.prototype.draw = function(universe, world, display, drawable)
	{		
		var drawPos = this.drawPos.overwriteWith
		(
			drawable.pos
		).subtract
		(
			this.sizeHalf
		);
	
		display.drawImageRegion(this.image, this.offset, this.size, drawPos);
	}
}

function VisualOffset(offset, child)
{
	this.offset = offset;
	this.child = child;
	
	this.drawablePosOriginal = new Coords();
}
{
	VisualOffset.prototype.draw = function(universe, world, display, drawable)
	{
		this.drawablePosOriginal.overwriteWith(drawable.pos);
		drawable.pos.add(this.offset);
		this.child.draw(universe, world, display, drawable);
		drawable.pos.overwriteWith(this.drawablePosOriginal);
	}
}

function VisualPolygon(vertices, colorFill, colorBorder)
{
	this.vertices = vertices;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;
	
	this.verticesTransformed = this.vertices.clone();
}
{
	VisualPolygon.prototype.draw = function(universe, world, display, drawable)
	{
		for (var i = 0; i < this.vertices.length; i++)
		{
			var vertex = this.vertices[i];
			var vertexTransformed = this.verticesTransformed[i];
			
			vertexTransformed.overwriteWith
			(
				vertex
			).add
			(
				drawable.pos
			);
		}
		
		display.drawPolygon(this.verticesTransformed, this.colorFill, this.colorBorder);
	}
}

function VisualRectangle(size, colorFill, colorBorder)
{
	this.size = size;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;	
	
	this.sizeHalf = this.size.clone().half();
	
	this.drawPos = new Coords();
}
{
	VisualRectangle.prototype.draw = function(universe, world, display, drawable)
	{
		var drawPos = this.drawPos.overwriteWith
		(
			drawable.pos
		).subtract
		(
			this.sizeHalf
		);
		
		display.drawRectangle(drawPos, this.size, this.colorFill, this.colorBorder);
	}
}

function VisualText(text)
{
	this.text = text;
}
{
	VisualText.prototype.draw = function(universe, world, display, drawable)
	{
		display.drawText(this.text, drawable.pos);
	}
}

function Venue(name, camera, map, portals, movers)
{
	this.name = name;
	this.camera = camera;
	this.map = map;
	this.portals = portals;
	this.movers = movers;
	
	this.moversToRemove = [];	
}
{
	Venue.prototype.initialize = function(universe, world)
	{
		this.constraintCameraFollowPlayer = new Constraint_Follow
		(
			this.movers[0]
		);

		this.updateForTimerTick(universe, world);
	}

	Venue.prototype.updateForTimerTick = function(universe, world)
	{			
		for (var i = 0; i < this.portals.length; i++)
		{
			var portal = this.portals[i];
			portal.updateForTimerTick(universe, world, this);
		}
	
		for (var i = 0; i < this.movers.length; i++)
		{
			var mover = this.movers[i];
			mover.updateForTimerTick(universe, world, this);	
		}
		
		for (var i = 0; i < this.moversToRemove.length; i++)
		{
			var mover = this.moversToRemove[i];
			if (mover == this.constraintCameraFollowPlayer.target)
			{
				this.constraintCameraFollowPlayer.target = this.camera;
			}
			this.movers.remove(mover);
		}		
		this.moversToRemove.length = 0;
	}
	
	// drawable
	
	Venue.prototype.draw = function(universe, world)
	{
		this.constraintCameraFollowPlayer.apply(this.camera);
	
		universe.display.clear();
	
		var visualCamera = new VisualCamera(this.camera);
		
		this.map.draw(universe, this, universe.display, visualCamera);
				
		for (var i = 0; i < this.portals.length; i++)
		{
			var portal = this.portals[i];
			portal.draw(universe, world, visualCamera);
		}
		
		for (var i = 0; i < this.movers.length; i++)
		{
			var mover = this.movers[i];
			mover.draw(universe, world, visualCamera);
		}
	}

}

function World(name, portalDefns, activityDefns, venues)
{
	this.name = name;
	this.portalDefns = portalDefns.addLookups("name");
	this.activityDefns = activityDefns.addLookups("name");
	this.venues = venues.addLookups("name");
	
	this.venueNext = this.venues[0];
}
{
	World.demo = function(displaySizeInPixels, cellSizeInPixels)
	{				
		var portalSize = cellSizeInPixels.clone().multiplyScalar(.75);;
		var portalDefns = 
		[
			new PortalDefn
			(
				"PortalTown",
				new VisualPolygon
				(
					[
						new Coords(-.5, 0).multiply(portalSize),
						new Coords(.5, 0).multiply(portalSize),
						new Coords(.5, -.5).multiply(portalSize),
						new Coords(0, -1).multiply(portalSize),
						new Coords(-.5, -.5).multiply(portalSize),
					],
					"LightGreen", "Green"
				)
			),
			new PortalDefn
			(
				"PortalExit",
				new VisualPolygon
				(
					[
						new Coords(-.5, 0).multiply(portalSize),
						new Coords(0, -.5).multiply(portalSize),
						new Coords(0, -.25).multiply(portalSize),
						new Coords(.5, -.25).multiply(portalSize),
						new Coords(.5, .25).multiply(portalSize),
						new Coords(0, .25).multiply(portalSize),
						new Coords(0, .5).multiply(portalSize),
					],
					"LightGreen", "Green"
				)
			)
			
		];
		
		var activityDefns = 
		[
			new ActivityDefn
			(
				"DoNothing", 
				function perform(universe, world, venue, actor, activity) 
				{
					// Do nothing.
				} 
			),
			
			new ActivityDefn
			(
				"MoveRandomly",
				function perform(universe, world, venue, actor, activity) 
				{
					while (activity.target == null)
					{
						actor.posInCells.round();
						var directionToMove = new Coords();
						var heading = Math.floor(4 * Math.random());
						if (heading == 0)
						{
							directionToMove.overwriteWithXY(0, 1);
						}
						else if (heading == 1)
						{
							directionToMove.overwriteWithXY(-1, 0);
						}
						else if (heading == 2)
						{
							directionToMove.overwriteWithXY(1, 0);
						}
						else if (heading == 3)
						{
							directionToMove.overwriteWithXY(0, -1);
						}
						
						var target = actor.posInCells.clone().add
						(
							directionToMove
						);
						
						if (target.isInRangeMax(venue.map.sizeInCells) == true)
						{
							var terrainAtTarget = venue.map.terrainAtPosInCells(target);
							if (terrainAtTarget.blocksMovement == false)
							{
								activity.target = target;
							}
						}
					}
					
					var target = activity.target;
					
					var displacementToTarget = target.clone().subtract
					(
						actor.posInCells
					);
					
					var distanceToTarget = displacementToTarget.magnitude();

					var speedInCellsPerTick = 0.1;
					
					if (distanceToTarget <= speedInCellsPerTick)
					{
						actor.posInCells.overwriteWith(target);
						activity.target = null;
					}
					else
					{
						var directionToTarget = displacementToTarget.divideScalar
						(
							distanceToTarget
						);
						actor.velInCellsPerTick.overwriteWith
						(
							directionToTarget
						).multiplyScalar
						(
							speedInCellsPerTick
						);
					}
				}
			),
			
			new ActivityDefn
			(
				"UserInputAccept", 
				function perform(universe, world, venue, actor, activity) 
				{
					var actorVel = actor.velInCellsPerTick;
					actorVel.clear();
					var inputHelper = universe.inputHelper;
					var inputsActive = inputHelper.inputsActive;
					for (var i = 0; i < inputsActive.length; i++)
					{
						var input = inputsActive[i];
						if (input == null)
						{
							// do nothing
						}
						else if (input.startsWith("Arrow") == true)
						{
							if (input == "ArrowDown")
							{
								actorVel.overwriteWithXY(0, 1);
							}
							else if (input == "ArrowLeft")
							{
								actorVel.overwriteWithXY(-1, 0);
							}
							else if (input == "ArrowRight")
							{
								actorVel.overwriteWithXY(1, 0);
							}
							else if (input == "ArrowUp")
							{
								actorVel.overwriteWithXY(0, -1);
							}
							
							var speedInCellsPerTick = 0.1;
							actorVel.multiplyScalar(speedInCellsPerTick);							
						}
						else if (input == "Enter")
						{
							inputHelper.inputInactivate(input);
							var displacement = new Coords();
							var portals = venue.portals;
							for (var i = 0; i < portals.length; i++)
							{
								var portal = portals[i];
								var distance = displacement.overwriteWith
								(
									portal.pos
								).subtract
								(
									actor.pos
								).magnitude();
								var distanceMax = venue.map.cellSizeInPixels.x;
								if (distance <= distanceMax)
								{
									portal.activate(universe, world, venue, actor);
								}
							}
						}
					}
				} 
			),
			
		];
		
		var colors = Color.Instances()._All;

		var mapTerrainVisualDesert = new MapTerrainVisual(VisualImage.manyFromImages
		([
			Image.fromStrings
			(
				"DesertCenter", 
				colors, 
				[ 
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
				]
			),
			Image.fromStrings
			(
				"DesertInsideSE", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"aaaawyyw........",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"DesertInsideSW", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywaaaa",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"DesertEdgeN", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaaaaa",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"DesertInsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"aaaayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
				]
			),
			Image.fromStrings
			(
				"DesertEdgeW", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
				]
			),			
			Image.fromStrings
			(
				"DesertDiagonalBackslash", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywaaaa",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"aaaayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
				]
			),
			Image.fromStrings
			(
				"DesertOutsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"...aaaaaaaaaaaaa",
					"...ayywwyywwyyww",
					"...aywwyywwyywwy",
					"...awwyywwyywwyy",
					"...awyywwyywwyyw",
					"...ayywwy.......",
					"...aywwyy.......",
					"...awwyyw.......",
					"...awyyww.......",
					"...ayywwy.......",
					"...aywwyy.......",
					"...awwyyw.......",
					"...awyyww.......",
				]
			),
			Image.fromStrings
			(
				"DesertInsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"........yywwaaaa",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),
			Image.fromStrings
			(
				"DesertDiagonalSlash", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"aaaawyyw........",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"........yywwaaaa",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),
			Image.fromStrings
			(
				"DesertEdgeE", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),			
			Image.fromStrings
			(
				"DesertOutsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaa...",
					"yywwyywwyywwa...",
					"ywwyywwyywwya...",
					"wwyywwyywwyya...",
					"wyywwyywwyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),
			Image.fromStrings
			(
				"DesertEdgeS", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"aaaaaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"DesertOutsideSW", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayywwyywwyyww",
					"...aywwyywwyywwy",
					"...awwyywwyywwyy",
					"...awyywwyywwyyw",
					"...aaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"DesertOutsideSE", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"yywwyywwyywwa...",
					"ywwyywwyywwya...",
					"wwyywwyywwyya...",
					"wyywwyywwyywa...",
					"aaaaaaaaaaaaa...",
					"................",
					"................",
					"................",
				]
			),
		]));
		
		var mapTerrainVisualRock = new MapTerrainVisual(VisualImage.manyFromImages
		([
			Image.fromStrings
			(
				"RockCenter", 
				colors, 
				[ 
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
				]
			),
			Image.fromStrings
			(
				"RockInsideSE", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"aaaaA--A........",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"RockInsideSW", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aaaaa",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"RockEdgeN", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaaaaa",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"RockInsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"aaaa--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
				]
			),
			Image.fromStrings
			(
				"RockEdgeW", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
				]
			),			
			Image.fromStrings
			(
				"RockDiagonalBackslash", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aaaaa",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"aaaa--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
				]
			),
			Image.fromStrings
			(
				"RockOutsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"...aaaaaaaaaaaaa",
					"...a--AA--AA--AA",
					"...a-AA--AA--AA-",
					"...aAA--AA--AA--",
					"...aA--AA--AA--A",
					"...a--AA-.......",
					"...a-AA--.......",
					"...aAA--A.......",
					"...aA--AA.......",
					"...a--AA-.......",
					"...a-AA--.......",
					"...aAA--A.......",
					"...aA--AA.......",
				]
			),
			Image.fromStrings
			(
				"RockInsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"........--AAaaaa",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),
			Image.fromStrings
			(
				"RockDiagonalSlash", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"aaaaA--A........",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"........--AAaaaa",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),
			Image.fromStrings
			(
				"RockEdgeE", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),			
			Image.fromStrings
			(
				"RockOutsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaa...",
					"--AA--AA--AAa...",
					"-AA--AA--AA-a...",
					"AA--AA--AA--a...",
					"A--AA--AA--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),
			Image.fromStrings
			(
				"RockEdgeS", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"aaaaaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"RockOutsideSW", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA--AA--AA",
					"...a-AA--AA--AA-",
					"...aAA--AA--AA--",
					"...aA--AA--AA--A",
					"...aaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"RockOutsideSE", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"--AA--AA--AAa...",
					"-AA--AA--AA-a...",
					"AA--AA--AA--a...",
					"A--AA--AA--Aa...",
					"aaaaaaaaaaaaa...",
					"................",
					"................",
					"................",
				]
			),
		]));
		
		//mapTerrainVisualDesert = MapTerrainVisual.TestInstance();
				
		var mapTerrains = 
		[
			new MapTerrain
			(
				"Desert", 
				".", 
				false, // blocksMovement
				1, // zLevelForOverlays
				mapTerrainVisualDesert
			),
			new MapTerrain
			(
				"Rocks", 
				"x", 
				true, // blocksMovement 
				2, // zLevelForOverlays
				mapTerrainVisualRock
			),
			
			new MapTerrain
			(
				"Water", 
				"~", 
				true, // blocksMovement 
				0, // zLevelForOverlays
				new MapTerrainVisual([new VisualImage
				(
					Image.fromStrings
					(
						"Water", 
						colors, 
						[ 
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",								
						]
					)
				)])
			)
		];
					
		var moverVisual = new VisualDirectional
		(
			// visualAtRest
			new VisualGroup
			([
				new VisualPolygon
				(
					[
						new Coords(0, -1).multiply(cellSizeInPixels),
						new Coords(-.5, 0).multiply(cellSizeInPixels),
						new Coords(.5, 0).multiply(cellSizeInPixels),
					],
					"Gray", null
				),
				new VisualOffset
				(
					new Coords(0, -cellSizeInPixels.y / 2),
					new VisualCircle(cellSizeInPixels.x / 4, "Tan", null)
				)
			]),
			// visualsForDirections
			[
				// east
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualPolygon
						(
							[
								new Coords(.4, -1).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),
						new VisualPolygon
						(
							[
								new Coords(.5, -.9).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),						
					]
				),
				
				// south
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualGroup
						([
							new VisualPolygon
							(
								[
									new Coords(0, -1).multiply(cellSizeInPixels),
									new Coords(-.5, 0).multiply(cellSizeInPixels),
									new Coords(.5, 0).multiply(cellSizeInPixels),
								],
								"Gray", null
							),
							new VisualOffset
							(
								new Coords(0, -cellSizeInPixels.y * .5),
								new VisualCircle(cellSizeInPixels.x / 4, "Tan", null)
							)
						]),

						new VisualGroup
						([
							new VisualPolygon
							(
								[
									new Coords(0, -.9).multiply(cellSizeInPixels),
									new Coords(-.5, 0).multiply(cellSizeInPixels),
									new Coords(.5, 0).multiply(cellSizeInPixels),
								],
								"Gray", null
							),
							new VisualOffset
							(
								new Coords(0, -cellSizeInPixels.y * .4),							
								new VisualCircle(cellSizeInPixels.x / 4, "Tan", null)
							)
						]),							
					]
				),

				// west
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualPolygon
						(
							[
								new Coords(-.4, -1).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),
						new VisualPolygon
						(
							[
								new Coords(-.5, -.9).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),						
					]
				),

				// north
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualPolygon
						(
							[
								new Coords(0, -1).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),
						
						new VisualPolygon
						(
							[
								new Coords(0, -.9).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),							
					]
				),
			]
		);
						
		var venues = 
		[ 
			new Venue
			(
				"Overworld",
				new Camera(displaySizeInPixels, new Coords(0, 0)),
				new Map
				(
					cellSizeInPixels,
					mapTerrains,
					[
						"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
						"~....x.........................~",
						"~..........~~..................~",
						"~.........~~~~.................~",
						"~.........~~~~...xx............~",
						"~..........~~....x...xx........~",
						"~..............xxxx.x.x........~",
						"~................x.............~",
						"~.........~.~.......x..........~",
						"~..........~...................~",
						"~..............................~",
						"~..............................~",
						"~..............................~",
						"~..............................~",
						"~..............................~",
						"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
					]
				),
				[
					new Portal
					(
						"PortalTown", 
						new Coords(17, 9),
						"Lonelytown", // destinationVenueName
						new Coords(1, 4) // destinationPosInCells
					)
				],
				[
					new Mover
					(
						"Player",
						moverVisual,
						new Activity("UserInputAccept", null), 
						new Coords(16, 8) // posInCells
					),
				]
			),
			
			new Venue
			(
				"Lonelytown",
				new Camera(displaySizeInPixels, new Coords(0, 0)),
				new Map
				(
					cellSizeInPixels,
					mapTerrains,
					[
						"xxxxxxxxxxxxxxxx",
						"x..............x",
						"x..............x",
						"x..............x",
						"x..............x",
						"x..............x",
						"x..............x",
						"xxxxxxxxxxxxxxxx",
					]
				),
				[
					new Portal
					(
						"PortalExit", 
						new Coords(1, 4), // posInCells
						"Overworld", // destinationVenueName
						new Coords(17, 9) // destinationPosInCells
					)
				],
				[
					new Mover
					(
						"Stranger",
						moverVisual,
						new Activity("MoveRandomly", null), 
						new Coords(4, 4) // posInCells
					),
				]
			),
			
		];
	
		var returnValue = new World
		(
			"WorldDemo",
			portalDefns,
			activityDefns,
			venues
		);
		
		return returnValue;
	}
	
	// instance methods
	
	World.prototype.initialize = function(universe)
	{
		this.updateForTimerTick(universe);
	}
		
	World.prototype.updateForTimerTick = function(universe)
	{
		if (this.venueNext != null)
		{
			this.venueCurrent = this.venueNext;
			this.venueCurrent.initialize(universe, this);
			this.venueNext = null;
		}
		this.venueCurrent.updateForTimerTick(universe, this);
	}
	
	World.prototype.draw = function(universe)
	{
		this.venueCurrent.draw(universe, this, universe.display);
	}
		
}

// run

main();

</script>

</body>
</html>

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

A Brick-Breaking Game in JavaScript

Below is a simple brick-breaking game implemented in JavaScript. Its gameplay is similar to the classic games Breakout and Arkanoid. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2018/01/16 – An online version of this code is now available at the URL “https://thiscouldbebetter.neocities.org/brickbreakinggame.html“.

BrickBreakingGame.png


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

// main

function main()
{
	var displaySize = new Coords(100, 200);

	var display = new Display(displaySize);

	var world = World.random(displaySize);

	Globals.Instance.initialize
	(
		10, // ticksPerSecond
		display,
		world
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Activity(perform)
{
	this.perform = perform;
}
{
	Activity.Instances = new Activity_Instances()
	
	function Activity_Instances()
	{
		this.DoNothing = new Activity(function perform() {});
		this.UserInputAccept = new Activity
		(
			function perform(world, actor)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var inputsActive = inputHelper.keysPressed;

				for (var i = 0; i < inputsActive.length; i++)
				{
					var inputActive = inputsActive[i];
					if (inputActive == "ArrowLeft")
					{
						actor.vel.x -= actor.accelPerTick;
					}
					else if (inputActive == "ArrowRight")
					{
						actor.vel.x += actor.accelPerTick;
					}
				}
			}
		);
	}
}

function Actor(pos, activity)
{
	this.pos = pos;
	this.activity = activity;

	this.color = "Gray";
	this.radius = 8;

	this.vel = new Coords(0, 0);

	this.accelPerTick = .005;
	this.speedMax = .25;

	this.projectileRadius = this.radius / 4;
	this.projectileSpeed = .3;

	// Helper variables.

	this.coordsTemp = new Coords();
	this.vertices = 
	[
		new Coords(), new Coords(), new Coords()
	];
}
{
	Actor.prototype.updateForTimerTick = function(world)
	{
		this.activity.perform(world, this);

		var speed = this.vel.magnitude();
		if (speed >= this.speedMax)
		{
			this.vel.normalize().multiplyScalar(this.speedMax);
		}

		this.pos.add(this.vel);
		this.pos.trimToRangeMinMax
		(
			world.actorPosMin,
			world.actorPosMax
		);

		this.vel.multiplyScalar(.98); // friction
	}

	// drawable

	Actor.prototype.drawToDisplay = function(display)
	{
		display.drawCircle
		(
			this.pos, this.radius, "Gray"
		);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();

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

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

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

	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.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.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

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

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

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

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

	Coords.prototype.trimToRangeMinMax = function(min, max)
	{
		if (this.x < min.x)
		{
			this.x = min.x;
		}
		else if (this.x > max.x)
		{
			this.x = max.x;
		}

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

		return this;
	}
}

function CollisionHelper()
{
	this.displacement = new Coords();
	this.edgeForward = new Coords();
	this.edgeRight = new Coords();
}
{
	CollisionHelper.Instance = new CollisionHelper();

	CollisionHelper.prototype.doCirclesCollide = function
	(
		circle0Center, circle0Radius, circle1Center, circle1Radius
	)
	{
		var distanceBetweenCenters = this.displacement.overwriteWith
		(
			circle1Center
		).subtract
		(
			circle0Center
		).magnitude();

		var sumOfRadii = circle0Radius + circle1Radius;

		var returnValue = (distanceBetweenCenters < sumOfRadii);

		return returnValue;
	}
}

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

	this.colorBack = "White";
	this.colorFore = "Gray";
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		this.graphics = canvas.getContext("2d");

		document.body.appendChild(canvas);
	}

	// drawing

	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = this.colorBack;
		this.graphics.fillRect
		(
			0, 0, this.size.x, this.size.y
		);

		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);
	}

	Display.prototype.drawCircle = function(center, radius, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;

		this.graphics.beginPath();
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);
		this.graphics.stroke();
	}

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

	Display.prototype.drawText = function(text, height, pos, color)
	{
		this.graphics.strokeStyle = this.colorBack;
		this.graphics.strokeText(text, pos.x, pos.y + height);

		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y + height);
	}
}

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

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

		this.world = world;

		this.inputHelper = new InputHelper();
		
		var millisecondsPerTimerTick = Math.floor(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);

		this.inputHelper.initialize();
	}

	// events

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

function InputHelper()
{
	this.keysPressed = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	InputHelper.prototype.removeKey = function(key)
	{
		if (this.keysPressed[key] != null)
		{
			this.keysPressed.splice(this.keysPressed.indexOf(key), 1);
			delete this.keysPressed[key];
		}
	}

	// events 

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var key = event.key;
		if (this.keysPressed[key] == null)
		{
			this.keysPressed.push(key);
			this.keysPressed[key] = key;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.removeKey(event.key);
	}

}

function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2;

	Polar.prototype.toCoords = function(coords)
	{
		var azimuthInRadians = this.azimuthInTurns * Polar.RadiansPerTurn; 
		coords.x = Math.cos(azimuthInRadians) * this.radius;
		coords.y = Math.sin(azimuthInRadians) * this.radius;
		return coords;
	}

	Polar.prototype.trimAzimuthToRangeMinMax = function(min, max)
	{
		if (this.azimuthInTurns < min)
		{
			this.azimuthInTurns = min;
		}
		else if (this.azimuthInTurns > max)
		{
			this.azimuthInTurns = max;
		}
		return this;
	}
}

function Obstacle(radius, pos)
{
	this.radius = radius;
	this.pos = pos;
}
{
	// drawable

	Obstacle.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.radius, "Gray");
	}
}

function Projectile(pos, vel)
{
	this.pos = pos;
	this.vel = vel;

	this.radius = 2;
	this.color = "Gray";
}
{
	Projectile.prototype.updateForTimerTick = function(world)
	{
		this.pos.add(this.vel);

		if (this.pos.x < 0 || this.pos.x > world.size.x)
		{
			this.vel.x *= -1;
		}

		if (this.pos.y < 0)
		{
			this.vel.y *= -1;
		} 
		else if (this.pos.y > world.size.y)
		{
			world.projectiles.length = 0;
		}

		this.updateForTimerTick_Collisions(world);
	}

	Projectile.prototype.updateForTimerTick_Collisions = function(world)
	{
		var collisionHelper = CollisionHelper.Instance;

		var obstacles = world.obstacles;

		for (var i = 0; i < obstacles.length; i++)
		{
			var obstacle = obstacles[i];
			var doProjectileAndObstacleCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radius,
				obstacle.pos, obstacle.radius
			);

			if (doProjectileAndObstacleCollide == true)
			{
				this.collideWithOther(obstacle);
				obstacles.remove(obstacle);
				i--;
				break;
			}
		}
	
		var actors = world.actors;

		for (var i = 0; i < actors.length; i++)
		{
			var actor = actors[i];
			var doProjectileAndActorCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radius,
				actor.pos, actor.radius
			);
			if (doProjectileAndActorCollide == true)
			{
				this.collideWithOther(actor);
			}
		}
	}

	// drawable

	Projectile.prototype.drawToDisplay = function(display)
	{
		display.drawLine
		(
			this.pos, 
			this.pos.clone().subtract(this.vel), 
			this.color
		);

		display.drawCircle
		(
			this.pos, this.radius, this.color
		);
	}

	// collidable

	Projectile.prototype.collideWithOther = function(other)
	{
		var posAfterCollision = new Coords();
		var velAfterCollision = new Coords();
		var displacementBetweenCenters = new Coords();
		var velocityRelative = new Coords();
  
		var sumOfBodyRadii = this.radius + other.radius; 
 
		displacementBetweenCenters.overwriteWith
		(
			other.pos
		).subtract
		(
			this.pos
		);
 
		var distanceBetweenCenters = displacementBetweenCenters.magnitude();
 
		var normalAtCollision = displacementBetweenCenters.divideScalar
		(
			distanceBetweenCenters
		);
 
		var velocityAlongNormal = normalAtCollision.multiplyScalar
		(
			this.vel.dotProduct
			(
				normalAtCollision
			)
		);

		this.vel.add
		(
			velocityAlongNormal.multiplyScalar(-2)
		)
	}
}

function World(size, actor, obstacles, projectiles)
{
	this.size = size;
	this.actors = [ actor ];
	this.obstacles = obstacles;
	this.projectiles = projectiles;

	this.actorPosMin = new Coords(actor.radius, actor.pos.y);
	this.actorPosMax = new Coords(this.size.x - actor.radius, actor.pos.y);
}
{
	World.random = function(size)
	{
		var obstacleGridSizeInCells = new Coords(8, 8);
		var spaceBetweenObstacles = new Coords(1, 1).multiplyScalar
		(
			size.x / (obstacleGridSizeInCells.x + 1)
		);

		var actorPos = new Coords
		(
			size.x / 2,
			size.y - spaceBetweenObstacles.y
		);

		var actor = new Actor
		(
			actorPos,
			Activity.Instances.UserInputAccept
		);

		var obstacleRadius = Math.floor(spaceBetweenObstacles.x / 2);

		var obstacles = [];
		for (var y = 1; y <= obstacleGridSizeInCells.y; y++)
		for (var x = 1; x <= obstacleGridSizeInCells.x; x++)
		{
			var pos = new Coords(x, y).multiply(spaceBetweenObstacles);

			var obstacle = new Obstacle
			(
				obstacleRadius, pos
			);

			obstacles.push(obstacle);
		}

		var projectile = new Projectile
		(
			actor.pos.clone().subtract
			(
				new Coords(0, actor.radius + actor.projectileRadius)
			),
			new Coords(1, -1).normalize().multiplyScalar
			(
				actor.projectileSpeed
			)
		);
		
		var projectiles = [ projectile ];

		var returnValue = new World
		(
			size,
			actor,
			obstacles,
			projectiles
		);

		return returnValue;
	}

	// instance methods

	World.prototype.updateForTimerTick = function()
	{		
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.updateForTimerTick(this);
		}

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.updateForTimerTick(this);
		}
	}

	// drawable

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

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

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

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

// run

main();

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

Posted in Uncategorized | Tagged , , | Leave a comment

An Asteroids Clone in JavaScript

Below is a simple clone of the classic video game Asteroids implemented in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2018/01/06 – This code has been posted to Github at the URL “https://github.com/thiscouldbebetter/AsteroidsGame“. An online version is also available at the URL “https://thiscouldbebetter.neocities.org/AsteroidsGame/_AsteroidsGame.html“.

Asteroids.png


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

// main

function main()
{
	var displaySize = new Coords(200, 200);

	var display = new Display(displaySize);

	var world = World.random(displaySize);

	Globals.Instance.initialize
	(
		10, // ticksPerSecond
		display,
		world
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Activity(perform)
{
	this.perform = perform;
}
{
	Activity.Instances = new Activity_Instances()
	
	function Activity_Instances()
	{
		this.DoNothing = new Activity(function perform() {});
		this.UserInputAccept = new Activity
		(
			function perform(world, actor)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var inputsActive = inputHelper.keysPressed;

				for (var i = 0; i < inputsActive.length; i++)
				{
					var inputActive = inputsActive[i];
					if (inputActive == "ArrowLeft")
					{
						actor.forward.subtract
						(
							actor.right.clone().multiplyScalar
							(
								actor.turnsPerTick
							)
						).normalize();
						actor.right.overwriteWith(actor.forward).right();
					}
					else if (inputActive == "ArrowRight")
					{
						actor.forward.add
						(
							actor.right.clone().multiplyScalar
							(
								actor.turnsPerTick
							)
						).normalize();
						actor.right.overwriteWith(actor.forward).right();
					}
					else if (inputActive == "ArrowUp")
					{
						actor.vel.add
						(
							actor.forward.clone().multiplyScalar
							(
								actor.accelPerTick
							)
						);
					}
					else if (inputActive == "Enter")
					{
						if (world.projectiles.length > 0)
						{
							return;
						}

						var projectilePos = actor.forward.clone().multiplyScalar
						(
							actor.lengthHalf
						).add
						(
							actor.pos
						);

						var projectileVel = actor.forward.clone().multiplyScalar
						(
							actor.projectileSpeed
						);

						var projectile = new Projectile
						(
							projectilePos, projectileVel
						);
						world.projectiles.push(projectile);

						inputHelper.removeKey(inputActive)
					}
				}
			}
		);
	}
}

function Actor(pos, activity)
{
	this.pos = pos;
	this.activity = activity;

	this.color = "Gray";
	this.widthHalf = 3;
	this.lengthHalf = 4;

	this.forward = new Coords(1, 0);
	this.right = this.forward.clone().right();

	this.vel = new Coords(0, 0);

	this.accelPerTick = .0025;
	this.turnsPerTick = .02;
	this.speedMax = .25;
	this.projectileSpeed = 1; 

	// Helper variables.

	this.coordsTemp = new Coords();
	this.vertices = 
	[
		new Coords(), new Coords(), new Coords()
	];
}
{
	Actor.prototype.updateForTimerTick = function(world)
	{
		this.activity.perform(world, this);

		var speed = this.vel.magnitude();
		if (speed >= this.speedMax)
		{
			this.vel.normalize().multiplyScalar(this.speedMax);
		}

		this.pos.add(this.vel);
		this.pos.wrapToRangeMax(world.size);

		var collisionHelper = CollisionHelper.Instance;

		var obstacles = world.obstacles;
		for (var i = 0; i < obstacles.length; i++)
		{
			var obstacle = obstacles[i];
			var doActorAndObstacleCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.widthHalf, // hack
				obstacle.pos, obstacle.radius
			);
			if (doActorAndObstacleCollide == true)
			{
				world.actors.remove(this);
			}
		}
	}

	// drawable

	Actor.prototype.drawToDisplay = function(display)
	{
		this.vertices[0].overwriteWith
		(
			this.forward
		).multiplyScalar
		(
			this.lengthHalf
		).add
		(
			this.pos
		);

		var back = this.coordsTemp.overwriteWith
		(
			this.forward	
		).multiplyScalar
		(
			0 - this.lengthHalf
		).add
		(
			this.pos
		);
		
		this.vertices[1].overwriteWith
		(
			this.right
		).multiplyScalar
		(
			this.widthHalf
		).add
		(
			back
		);

		this.vertices[2].overwriteWith
		(
			this.right
		).multiplyScalar
		(
			0 - this.widthHalf
		).add
		(
			back
		);

		display.drawPolygon(this.vertices, this.color);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();

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

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

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

	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.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.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

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

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

	Coords.prototype.right = function()
	{
		var temp = this.x;
		this.x = 0 - this.y;
		this.y = 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 CollisionHelper()
{
	this.displacement = new Coords();
	this.edgeForward = new Coords();
	this.edgeRight = new Coords();
}
{
	CollisionHelper.Instance = new CollisionHelper();

	CollisionHelper.prototype.doCirclesCollide = function
	(
		circle0Center, circle0Radius, circle1Center, circle1Radius
	)
	{
		var distanceBetweenCenters = this.displacement.overwriteWith
		(
			circle1Center
		).subtract
		(
			circle0Center
		).magnitude();

		var sumOfRadii = circle0Radius + circle1Radius;

		var returnValue = (distanceBetweenCenters < sumOfRadii);

		return returnValue;
	}
}

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

	this.colorBack = "White";
	this.colorFore = "Gray";
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		this.graphics = canvas.getContext("2d");

		document.body.appendChild(canvas);
	}

	// drawing

	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = this.colorBack;
		this.graphics.fillRect
		(
			0, 0, this.size.x, this.size.y
		);

		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);
	}

	Display.prototype.drawCircle = function(center, radius, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;

		this.graphics.beginPath();
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);
		this.graphics.stroke();
	}

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

	Display.prototype.drawPolygon = function(vertices, colorBorder)
	{
		this.graphics.strokeStyle = 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();
		this.graphics.stroke();
	}


	Display.prototype.drawRectangle = function(pos, size, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;
		this.graphics.strokeRect
		(
			pos.x, pos.y, size.x, size.y
		);
	}

	Display.prototype.drawText = function(text, height, pos, color)
	{
		this.graphics.strokeStyle = this.colorBack;
		this.graphics.strokeText(text, pos.x, pos.y + height);

		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y + height);
	}
}

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

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

		this.world = world;

		this.inputHelper = new InputHelper();
		
		var millisecondsPerTimerTick = Math.floor(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);

		this.inputHelper.initialize();
	}

	// events

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

function InputHelper()
{
	this.keysPressed = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	InputHelper.prototype.removeKey = function(key)
	{
		if (this.keysPressed[key] != null)
		{
			this.keysPressed.splice(this.keysPressed.indexOf(key), 1);
			delete this.keysPressed[key];
		}
	}

	// events 

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var key = event.key;
		if (this.keysPressed[key] == null)
		{
			this.keysPressed.push(key);
			this.keysPressed[key] = key;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.removeKey(event.key);
	}

}

function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2;

	Polar.prototype.toCoords = function(coords)
	{
		var azimuthInRadians = this.azimuthInTurns * Polar.RadiansPerTurn; 
		coords.x = Math.cos(azimuthInRadians) * this.radius;
		coords.y = Math.sin(azimuthInRadians) * this.radius;
		return coords;
	}

	Polar.prototype.trimAzimuthToRangeMinMax = function(min, max)
	{
		if (this.azimuthInTurns < min)
		{
			this.azimuthInTurns = min;
		}
		else if (this.azimuthInTurns > max)
		{
			this.azimuthInTurns = max;
		}
		return this;
	}
}

function Obstacle(radius, pos, vel)
{
	this.radius = radius;
	this.pos = pos;
	this.vel = vel;
}
{
	Obstacle.prototype.updateForTimerTick = function(world)
	{
		this.pos.add(this.vel).wrapToRangeMax(world.size);

		var collisionHelper = CollisionHelper.Instance;
		var obstacles = world.obstacles;
		for (var i = 0; i < obstacles.length; i++)
		{
			var other = obstacles[i];
			if (other != this)
			{
				var doThisAndOtherCollide = collisionHelper.doCirclesCollide
				(
					this.pos, this.radius,
					other.pos, other.radius
				);

				if (doThisAndOtherCollide == true)
				{
					this.collideWithOther(other);
				}
			}
		}
	}

	// drawable

	Obstacle.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.radius, "Gray");
	}

	// collidable

	Obstacle.prototype.collideWithOther = function(other)
	{
		var bodyPositionsAfterCollision = [new Coords(), new Coords()];
		var bodyVelsAfterCollision = [new Coords(), new Coords()];
		var displacement = new Coords();
		var velocityRelative = new Coords();
 
		var bodies = [ this, other ];
		var body0 = bodies[0];
		var body1 = bodies[1];
 
		var sumOfBodyRadii = 
			body0.radius + body1.radius; 
 
		velocityRelative.overwriteWith
		(
			body0.vel
		).subtract
		(
			body1.vel
		);
  
		displacement.overwriteWith
		(
			body0.pos
		).subtract
		(
			body1.pos
		);
 
		var distanceBetweenBodyCenters = displacement.magnitude();
		var overlap = sumOfBodyRadii - distanceBetweenBodyCenters;
		var overlapHalf = overlap / 2;
 
		var normalAtCollision = displacement.divideScalar
		(
			distanceBetweenBodyCenters
		);
 
		var velocityAlongNormal = normalAtCollision.multiplyScalar
		(
			velocityRelative.dotProduct
			(
				normalAtCollision
			)
		);
 
		velocityRelative.subtract
		(
			velocityAlongNormal
		).multiplyScalar
		(
			-1
		);
 
		for (var i = 0; i < bodies.length; i++)
		{
			var bodyThis = bodies[i];
			var bodyOther = bodies[1 - i];
		 
			var bodyPosAfterCollision = bodyPositionsAfterCollision[i];
			var bodyVelAfterCollision = bodyVelsAfterCollision[i];
 
			var multiplier = (i == 0 ? -1 : 1);
 
			bodyPosAfterCollision.overwriteWith
			(
				normalAtCollision
			).multiplyScalar
			(
				multiplier * overlapHalf
			).add
			(
				bodyThis.pos
			);
  
			bodyVelAfterCollision.overwriteWith
			(
				velocityRelative
			).multiplyScalar
			(
				multiplier
			).add
			(
				bodyOther.vel
			);
		}
 
		for (var i = 0; i < bodies.length; i++)
		{
			var bodyThis = bodies[i];
			var bodyPosAfterCollision = bodyPositionsAfterCollision[i];
			var bodyVelAfterCollision = bodyVelsAfterCollision[i];
 
			bodyThis.pos.overwriteWith
			(
				bodyPosAfterCollision
			);
			bodyThis.vel.overwriteWith
			(
				bodyVelAfterCollision
			);
		}
	}

}

function Projectile(pos, vel)
{
	this.pos = pos;
	this.vel = vel;

	this.radiusInFlight = 2;
	this.colorInFlight = "Gray";

	this.ticksSinceSpawned = 0;
	this.ticksToLive = 100;

	this.ticksSinceExplosion = null;
	this.ticksToExplode = 30;
	this.radiusExplodingMax = 20;
	this.colorExploding = "Gray";
}
{
	Projectile.prototype.drawToDisplay = function(display)
	{
		if (this.ticksSinceExplosion == null)
		{
			display.drawCircle
			(
				this.pos, this.radiusInFlight, this.colorInFlight
			);
			display.drawLine
			(
				this.pos, 
				this.pos.clone().subtract(this.vel), 
				this.colorInFlight
			);
		}
		else
		{
			var radiusCurrent = 
				this.radiusExplodingMax 
				* this.ticksSinceExplosion 
				/ this.ticksToExplode;
			display.drawCircle(this.pos, radiusCurrent, this.colorExploding);
		}
	}

	Projectile.prototype.updateForTimerTick = function(world)
	{
		if (this.ticksSinceSpawned >= this.ticksToLive)
		{
			world.projectiles.remove(this);
		}
		else if (this.ticksSinceExplosion == null)
		{
			this.pos.add(this.vel).wrapToRangeMax(world.size);
			
			this.updateForTimerTick_Obstacles(world);
		}
		else if (this.ticksSinceExplosion < this.ticksToExplode)
		{
			this.ticksSinceExplosion++;
		}
		else
		{
			// todo
		}

		this.ticksSinceSpawned++;
	}

	Projectile.prototype.updateForTimerTick_Obstacles = function(world)
	{
		var collisionHelper = CollisionHelper.Instance;

		var obstacles = world.obstacles;

		for (var i = 0; i < obstacles.length; i++)
		{
			var obstacle = obstacles[i];
			var doProjectileAndObstacleCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radiusInFlight,
				obstacle.pos, obstacle.radius
			);

			if (doProjectileAndObstacleCollide == true)
			{
				world.projectiles.remove(this);
				obstacles.remove(obstacle);
				i--;

				this.updateForTimerTick_Obstacles_Children
				(
					world, obstacle
				);

				break;
			}
		}
	}

	Projectile.prototype.updateForTimerTick_Obstacles_Children = function(world, obstacle)
	{
		var obstacleChildRadius = obstacle.radius / 2;
		if (obstacleChildRadius >= 2)
		{
			for (var c = 0; c < 2; c++)
			{
				var lateral = obstacle.vel.clone().right().normalize().multiplyScalar
				(
				 	(c == 0 ? -1 : 1)
				)

				var displacement = lateral.clone().multiplyScalar
				(
					obstacleChildRadius
				);
				var accel = lateral.clone().multiplyScalar(.1);

				var obstacleChildVel = obstacle.vel.clone().add(accel);

				var obstacleChild = new Obstacle
				(
					obstacleChildRadius,
					obstacle.pos.clone().add(displacement),
					obstacleChildVel
				);
				world.obstacles.push(obstacleChild);
			}
		}
	}
}

function World(size, actor, obstacles)
{
	this.size = size;
	this.actors = [ actor ];
	this.obstacles = obstacles;

	this.projectiles = [];
}
{
	World.random = function(size)
	{
		var actorPos = size.clone().multiplyScalar(.5);

		var actor = new Actor
		(
			actorPos,
			Activity.Instances.UserInputAccept
		);

		var numberOfObstacles = 2;
		var obstacleRadius = 16;

		var obstacles = [];
		var obstacleSpeedMax = .2;
		for (var i = 0; i < numberOfObstacles; i++)
		{
			var pos = new Coords().random().multiply(size);
			var vel = new Polar
			(
				Math.random(), // azimuthInTurns
				Math.random() * obstacleSpeedMax // radius
			).toCoords(new Coords());

			var obstacle = new Obstacle
			(
				obstacleRadius, pos, vel
			);

			obstacles.push(obstacle);
		}

		var returnValue = new World
		(
			size,
			actor,
			obstacles
		);

		return returnValue;
	}

	// instance methods

	World.prototype.updateForTimerTick = function()
	{		
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.updateForTimerTick(this);
		}

		for (var i = 0; i < this.obstacles.length; i++)
		{
			var obstacle = this.obstacles[i];
			obstacle.updateForTimerTick(this);
		}

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.updateForTimerTick(this);
		}
	}

	// drawable

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

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

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

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

// run

main();

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

Posted in Uncategorized | Tagged , , | Leave a comment

An Artillery Game in JavaScript

Below is a simple artillery game implemented in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2017/11/22 – I have updated this code to account for explosions being blocked by the landscape, though it may not be immediately clear that this is happening based on the visuals.

I also plan to make a live version of this game available at the URL “https://thiscouldbebetter.neocities.org/artillerygame.html“, and to post a Git repository of the code at https://github.com/thiscouldbebetter/ArtilleryGame“.

ArtilleryGame.png


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

function main()
{
	var displaySize = new Coords(200, 200);
 
	var display = new Display(displaySize);
 
	var world = World.random(new Coords(0, .05), displaySize);
 
	Globals.Instance.initialize
	(
		10, // ticksPerSecond
		display,
		world
	);
}
 
// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.clone = function()
	{
		var returnValues = [];

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

		return returnValues;
	}
}
 
// classes
 
function Activity(perform)
{
	this.perform = perform;
}
{
	Activity.Instances = new Activity_Instances()
	 
	function Activity_Instances()
	{
		this.DoNothing = new Activity(function perform() {});
		this.UserInputAccept = new Activity
		(
			function perform(world, actor)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var inputActive = inputHelper.keyPressed;
				var powerFactor = 1000;
 
				if (inputActive == "ArrowDown")
				{
					actor.powerCurrent -= actor.powerPerTick;
					actor.powerCurrent = 
						Math.round(actor.powerCurrent * powerFactor) / powerFactor;
					if (actor.powerCurrent < actor.powerMin)
					{
						actor.powerCurrent = actor.powerMin;
					}
				}   
				else if (inputActive == "ArrowLeft")
				{
					actor.firePolar.azimuthInTurns -= actor.turnsPerTick;
					actor.firePolar.trimAzimuthToRangeMinMax
					(
						actor.azimuthInTurnsMin, 
						actor.azimuthInTurnsMax
					);
				}
				else if (inputActive == "ArrowRight")
				{
					actor.firePolar.azimuthInTurns += actor.turnsPerTick;
					actor.firePolar.trimAzimuthToRangeMinMax
					(
						actor.azimuthInTurnsMin, 
						actor.azimuthInTurnsMax
					);
				}
				else if (inputActive == "ArrowUp")
				{
					actor.powerCurrent += actor.powerPerTick;
					actor.powerCurrent = 
						Math.round(actor.powerCurrent * powerFactor) / powerFactor;
					if (actor.powerCurrent > actor.powerMax)
					{
						actor.powerCurrent = actor.powerMax;
					}
				}
				else if (inputActive == "Enter")
				{
					var projectile = new Projectile
					(
						actor.color,
						actor.muzzlePos.clone(),
						// vel
						actor.firePolar.toCoords
						(
							new Coords()
						).normalize().multiplyScalar
						(
							actor.powerCurrent
						)
					);
 
					world.projectiles = [ projectile ];
 
					world.actorIndexCurrent = 1 - world.actorIndexCurrent;
				}   
				inputHelper.keyPressed = false;
			}
		);
	}
}
 
function Actor(color, pos, activity)
{
	this.color = color;
	this.pos = pos;
	this.activity = activity;
 
	this.wins = 0;
	this.ticksSinceKilled = null;
	this.ticksToDie = 30;
 
	this.collider = new Circle(this.pos, 8);
 
	this.powerMin = 1;
	this.powerMax = 4;
	this.powerPerTick = .1;
 
	this.azimuthInTurnsMin = .5;
	this.azimuthInTurnsMax = 1;
	this.turnsPerTick = 1.0 / Polar.DegreesPerTurn;
	this.firePolar = new Polar
	(
		(this.azimuthInTurnsMin + this.azimuthInTurnsMax) / 2, 
		this.collider.radius * 2
	);
	this.muzzlePos = this.pos.clone().add
	(
		this.firePolar.toCoords( new Coords() )
	);
 
	this.vel = new Coords();
 
	this.reset();
}
{
	Actor.prototype.reset = function()
	{
		this.firePolar.azimuthInTurns = 
			(this.azimuthInTurnsMin + this.azimuthInTurnsMax) / 2, 
		this.powerCurrent = (this.powerMin + this.powerMax) / 2;
 
		this.ticksSinceKilled = null;
		this.pos.y = 0;
		this.vel.clear();
	}
 
	Actor.prototype.updateForTimerTick = function(world)
	{
		if (this.ticksSinceKilled == null)
		{
			if (this == world.actorCurrent() && world.projectiles.length == 0)
			{
				this.activity.perform(world, this);
			}
 
			this.firePolar.toCoords(this.muzzlePos);
			this.muzzlePos.add(this.pos);
 
			var surfaceAltitude = world.landscape.altitudeAtX
			(
				this.pos.x
			);
			var isBelowGround = (this.pos.y >= surfaceAltitude);
			if (isBelowGround == false)
			{
				this.vel.add(world.gravityPerTick);
				this.pos.add(this.vel);
			}
			else
			{
				this.vel.clear();
				this.pos.y = surfaceAltitude;
			}
 
		}
		else if (this.ticksSinceKilled < this.ticksToDie)
		{
			 this.ticksSinceKilled++;
		}
		else
		{
			world.reset();
		}
	}
 
	// drawable
 
	Actor.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.collider.radius, this.color);
		display.drawLine
		(
			this.pos,
			this.muzzlePos
		);
		 
		if (this == Globals.Instance.world.actorCurrent())
		{
			var fireAzimuthInTurnsRecentered = Math.abs
			( 
				0.75 - this.firePolar.azimuthInTurns
			);
			var fireAzimuthInDegrees = Math.round
			(
				fireAzimuthInTurnsRecentered 
				* Polar.DegreesPerTurn
			);
			var text = "Angle:" + fireAzimuthInDegrees + " Power:" + this.powerCurrent;
			display.drawText
			(
				text,
				this.collider.radius,
				Coords.Instances.Zeroes,
				this.color
			);
		}
	}
}

function Circle(center, radius)
{
	this.center = center;
	this.radius = radius;
}

function CollisionHelper()
{
	this.displacement = new Coords();
}
{
	CollisionHelper.Instance = new CollisionHelper();
 
	CollisionHelper.prototype.doCirclesCollide = function(circle0, circle1)
	{
		var distanceBetweenCenters = this.displacement.overwriteWith
		(
			circle1.center
		).subtract
		(
			circle0.center
		).magnitude();
 
		var sumOfRadii = circle0.radius + circle1.radius;
 
		var returnValue = (distanceBetweenCenters < sumOfRadii);
 
		return returnValue;
	}

	CollisionHelper.prototype.doEdgesCollide = function(edge0, edge1)
	{
		var returnValue = null;
 
		if (this.edgeProjected == null)
		{
			this.edgeProjected = new Edge([new Coords(), new Coords()]);
		}
		var edgeProjected = this.edgeProjected;
		edgeProjected.overwriteWith(edge1).projectOnto(edge0);
		var edgeProjectedStart = edgeProjected.vertices[0];
		var edgeProjectedDirection = edgeProjected.direction;	   
 
		var distanceAlongEdgeProjectedToXAxis = 
			0 - edgeProjectedStart.y
			/ edgeProjectedDirection.y
 
		if 
		(
			distanceAlongEdgeProjectedToXAxis > 0 
			&& distanceAlongEdgeProjectedToXAxis < edgeProjected.length
		)
		{
			var distanceAlongEdge0ToIntersection =
				edgeProjectedStart.x 
				+ (edgeProjectedDirection.x * distanceAlongEdgeProjectedToXAxis);
 
			if 
			(
				distanceAlongEdge0ToIntersection > 0
				&& distanceAlongEdge0ToIntersection < edge0.length
			)
			{
				returnValue = true;
			}
		}
 
		return returnValue;
	}
}
 
function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();
 
	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}
 
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.addXY = function(x, y)
	{
		this.x += x;
		this.y += y;
		return this;
	}
 
	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;
	}
 
	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.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}
 
	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= 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;
		return this;
	}

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

	Coords.prototype.right = function()
	{
		var temp = this.x;
		this.x = 0 - this.y;
		this.y = temp;
		return this;
	}
	
	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}
}
 
function Display(size)
{
	this.size = size;
 
	this.colorBack = "White";
	this.colorFore = "Gray";
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		this.graphics = canvas.getContext("2d");
 
		document.body.appendChild(canvas);
	}
 
	// drawing
 
	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = this.colorBack;
		this.graphics.fillRect
		(
			0, 0, this.size.x, this.size.y
		);
 
		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);
	}
 
	Display.prototype.drawCircle = function(center, radius, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;
 
		this.graphics.beginPath();
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);
		this.graphics.stroke();
	}
 
	Display.prototype.drawLine = function(fromPos, toPos, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}
 
	Display.prototype.drawRectangle = function(pos, size, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;
		this.graphics.strokeRect
		(
			pos.x, pos.y, size.x, size.y
		);
	}
 
	Display.prototype.drawText = function(text, height, pos, color)
	{
		this.graphics.strokeStyle = this.colorBack;
		this.graphics.strokeText(text, pos.x, pos.y + height);
 
		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y + height);
	}
}

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

	this.displacement = new Coords();
	this.direction = new Coords();
	this.right = new Coords();
	this.recalculateDerivedValues();
}
{
	Edge.prototype.clone = function()
	{
		return new Edge(this.vertices.clone());
	}

	Edge.prototype.overwriteWith = function(other)
	{
		this.vertices[0].overwriteWith(other.vertices[0]);
		this.vertices[1].overwriteWith(other.vertices[1]);
		this.recalculateDerivedValues();
		return this;
	}

	Edge.prototype.projectOnto = function(other)
	{
		for (var i = 0; i < this.vertices.length; i++)
		{
			var vertexThis = this.vertices[i];
 
			vertexThis.subtract
			(
				other.vertices[0]
			).overwriteWithXY
			(
				vertexThis.dotProduct(other.direction),
				vertexThis.dotProduct(other.right)
			);
		}
 
		this.recalculateDerivedValues();
 
		return this;
	}

	Edge.prototype.recalculateDerivedValues = function()
	{
		this.displacement.overwriteWith
		(
			this.vertices[1]
		).subtract(this.vertices[0]);
		this.length = this.displacement.magnitude();
		this.direction.overwriteWith(this.displacement).divideScalar(this.length);
		this.right.overwriteWith(this.direction).right();
	}
}
 
function Globals()
{
	// Do nothing.
}
{
	Globals.Instance = new Globals();
 
	Globals.prototype.initialize = function(timerTicksPerSecond, display, world)
	{
		this.display = display;
		this.display.initialize();
 
		this.world = world;
 
		this.inputHelper = new InputHelper();
		 
		var millisecondsPerTimerTick = Math.floor(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);
 
		this.inputHelper.initialize();
	}
 
	// events
 
	Globals.prototype.handleEventTimerTick = function()
	{
		this.world.drawToDisplay(this.display);
		this.world.updateForTimerTick();
	}
}
 
function InputHelper()
{
	this.keyPressed = null;
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
	}
 
	// events 
 
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.keyPressed = event.key;
	}
}
 
function Landscape(size, horizonPoints)
{
	this.size = size;
	this.color = "Green";   

	this.edges = [];
	var horizonPointPrev = horizonPoints[0];
	for (var i = 1; i < horizonPoints.length; i++)
	{
		var horizonPoint = horizonPoints[i];
		var edge = new Edge([horizonPointPrev, horizonPoint]);
		this.edges.push(edge);
		horizonPointPrev = horizonPoint;
	}
}
{
	Landscape.random = function(size, numberOfPoints)
	{
		var points = [];
		for (var i = 0; i < numberOfPoints + 1; i++)
		{
			var point = new Coords(i * size.x / numberOfPoints, 0);
			points.push(point);
		}
 
		var returnValue = new Landscape(size, points).randomize();
 
		return returnValue;
	}
 
	// instance methods
 
	Landscape.prototype.altitudeAtX = function(xToCheck)
	{
		var returnValue;
 
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			var horizonPointPrev = edge.vertices[0];
			var horizonPoint = edge.vertices[1];

			if (horizonPoint.x > xToCheck)
			{
				var horizonChange = horizonPoint.clone().subtract
				(
					horizonPointPrev
				);
	 
				var t = 
					(xToCheck - horizonPointPrev.x)
					/ (horizonChange.x);
 
				var altitude = 
					horizonPointPrev.y 
					+ (t * horizonChange.y);
 
				returnValue = altitude;
				break;
			}
 
			horizonPointPrev = horizonPoint;
		}
 
		return returnValue;		 
	}

	Landscape.prototype.collidesWithEdge = function(edgeOther)
	{
		var returnValue = false;
		var collisionHelper = CollisionHelper.Instance;
		for (var i = 0; i < this.edges.length; i++)
		{
			var edgeThis = this.edges[i];
			var doEdgesCollide = collisionHelper.doEdgesCollide
			(
				edgeThis, edgeOther
			);
			if (doEdgesCollide == true)
			{
				returnValue = true;
				break;
			}
		}
		return returnValue
	}
 
	Landscape.prototype.randomize = function()
	{
		var altitudeMid = this.size.y / 2;
		var altitudeRange = this.size.y / 2;
		var altitudeRangeHalf = altitudeRange / 2;
		var altitudeMin = altitudeMid - altitudeRangeHalf;
		var altitudeMax = altitudeMin + altitudeRange;
 
		this.edges[0].vertices[0].y = 
			altitudeMin + Math.random() * altitudeRange;
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			var point = edge.vertices[1];
			point.y = altitudeMin + Math.random() * altitudeRange;
			edge.recalculateDerivedValues();
		}
 
		return this;		
	}
 
	// drawable
	 
	Landscape.prototype.drawToDisplay = function(display)
	{
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			display.drawLine(edge.vertices[0], edge.vertices[1], this.color);
		}
	}
}
 
function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2;
	Polar.DegreesPerTurn = 360;
 
	Polar.prototype.toCoords = function(coords)
	{
		var azimuthInRadians = this.azimuthInTurns * Polar.RadiansPerTurn; 
		coords.x = Math.cos(azimuthInRadians) * this.radius;
		coords.y = Math.sin(azimuthInRadians) * this.radius;
		return coords;
	}
 
	Polar.prototype.trimAzimuthToRangeMinMax = function(min, max)
	{
		if (this.azimuthInTurns < min)
		{
			this.azimuthInTurns = min;
		}
		else if (this.azimuthInTurns > max)
		{
			this.azimuthInTurns = max;
		}
		return this;
	}
}
 
function Projectile(color, pos, vel)
{
	this.color = color;
	this.pos = pos;
	this.vel = vel;
 
	this.collider = new Circle(this.pos, 2);
 
	this.ticksSinceExplosion = null;
	this.ticksToExplode = 30;
	this.radiusExplodingMax = 20;
}
{
	Projectile.prototype.radiusCurrent = function()
	{
		var radiusCurrent = 
			this.radiusExplodingMax 
			* this.ticksSinceExplosion 
			/ this.ticksToExplode;

		return radiusCurrent;
	}
 
	Projectile.prototype.updateForTimerTick = function(world)
	{
		if (this.ticksSinceExplosion == null)
		{
			this.vel.add(world.gravityPerTick);
			this.pos.add(this.vel);
			if (this.pos.y > world.size.y)
			{
				world.projectiles.length = 0;
			}
			else
			{
				var surfaceAltitude = world.landscape.altitudeAtX(this.pos.x);
				var isBeneathHorizon = (this.pos.y >= surfaceAltitude);
				if (isBeneathHorizon == true)
				{
					this.ticksSinceExplosion = 0;
					this.pos.y = surfaceAltitude;
				}
			}
		}
		else if (this.ticksSinceExplosion < this.ticksToExplode)
		{
			this.ticksSinceExplosion++;
		}
		else
		{  
			var collisionHelper = CollisionHelper.Instance;
			var actors = world.actors;
			for (var i = 0; i < actors.length; i++)
			{
				var actor = actors[i];

				this.collider.radius = this.radiusCurrent();
				var isActorWithinExplosionRadius = collisionHelper.doCirclesCollide
				(
					this.collider, actor.collider
				);

				if (isActorWithinExplosionRadius == true)
				{
					var edgeFromExplosionToActor = new Edge
					([
						this.pos.clone().addXY(0, -1), // hack 
						actor.pos.clone().addXY(0, -1)
					]);
					var isExplosionBlockedByGround = world.landscape.collidesWithEdge
					(
						edgeFromExplosionToActor
					);

					if (isExplosionBlockedByGround == false)
					{
						var actorOther = actors[1 - i];
						actorOther.ticksSinceKilled = 0;
						actorOther.wins++;
					}
				}
			}
			world.projectiles.length = 0;
		}
	}

	// drawable

	Projectile.prototype.drawToDisplay = function(display)
	{
		if (this.ticksSinceExplosion == null)
		{
			display.drawCircle
			(
				this.pos, this.collider.radius, this.color
			);
			display.drawLine
			(
				this.pos, 
				this.pos.clone().subtract(this.vel), 
				this.color
			);
		}
		else
		{
			display.drawCircle(this.pos, this.radiusCurrent(), this.color);
		}
	}
}
 
function World(gravityPerTick, size, landscape, actors)
{
	this.gravityPerTick = gravityPerTick;
	this.size = size;
	this.landscape = landscape;
	this.actors = actors;
 
	this.actorIndexCurrent = 0;
	this.projectiles = [];
}
{
	World.random = function(gravityPerTick, size)
	{
		var landscape = Landscape.random(size, 10);
 
		var actors = 
		[
			new Actor
			(
				"Blue", 
				new Coords(size.x / 6, 0),
				Activity.Instances.UserInputAccept
			), 
			new Actor
			(
				"Red", 
				new Coords(5 * size.x / 6, 0),
				Activity.Instances.UserInputAccept
			), 
		];
 
		var returnValue = new World
		(
			gravityPerTick,
			size,
			landscape,
			actors
		);
 
		return returnValue;
	}
 
	// instance methods
 
	World.prototype.actorCurrent = function()
	{
		return this.actors[this.actorIndexCurrent];
	}
 
	World.prototype.actorCurrentAdvance = function()
	{
		this.actorIndexCurrent = this.actors.length - 1 - this.actorIndexCurrent;
	}
 
	World.prototype.reset = function()
	{
		this.landscape.randomize();
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.reset();
		}   
	}
 
	World.prototype.updateForTimerTick = function()
	{
		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.updateForTimerTick(this);
		}
 
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.updateForTimerTick(this);
		}
	}
 
	// drawable
 
	World.prototype.drawToDisplay = function(display)
	{
		display.clear();
		this.landscape.drawToDisplay(display);
 
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.drawToDisplay(display);
			display.drawText("" + actor.wins, actor.radius, actor.pos, actor.color);
		}
 
		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.drawToDisplay(display);
		}
	}
}
 
// run
 
main();
 
</script>
</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment