A Graphical Inventory for a Video Game in JavaScript

The JavaScript code below implements a inventory session for a video game. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Use the arrow keys and the Enter key to select items and perform actions on them.

This program builds on an earlier post that used DOM elements to achieve more or less the same functionality. This version instead defines its own control classes, which are drawn to an HTML canvas.

There are some fixes and improvements yet to make. Among other things, there’s no images or descriptions for the selected items; all the status messages simply use JavaScript’s alert() function rather than being displayed to the canvas; the controls are rebuilt from scratch each time a key is pressed, when they should probably use some kind of databinding to handle changes instead; and the list of available actions should probably update itself based on the currently selected item rather than simply validating whether the selected action is valid after the Enter key is pressed. Also, some general code cleanup is probably in order.

InventorySession-Graphical



<html>
<body>

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

<script type="text/javascript">

// main

function main()
{
	var itemActions = ItemAction.Instances._All;

	var itemCategories = 
	[
		new ItemCategory("Gloves"),
		new ItemCategory("Handheld"),
		new ItemCategory("Hat"),
		new ItemCategory("Pants"),
		new ItemCategory("Potion"),
		new ItemCategory("Ring"),
		new ItemCategory("Scroll"),
		new ItemCategory("Shirt"),
		new ItemCategory("Shoes"),
		new ItemCategory("Treasure"),
	];

	var itemDefns = 
	[	
		new ItemDefn
		(
			"Boots of Stomping", 
			1, 
			[ "Shoes" ], 	 
			function(itemHolder, item)
			{
				alert("You step on some walnuts, and feast on their meat.");
			}
		),
		new ItemDefn("Coins", 				1, [ "Treasure" ], 	null ),
		new ItemDefn("Gem", 				1, [ "Treasure" ], 	null ),
		new ItemDefn
		(
			"Gloves of Dust Detection", 	
			1, 
			[ "Gloves" ],
			function(itemHolder, item) 
			{ 
				alert("You run a finger along the wall.  Yup, there's dust here.");
			}
		),
		new ItemDefn("Helmet of Increased Height", 	1, [ "Hat" ], 		null ),
		new ItemDefn("Jerkin of Jerkininess", 		1, [ "Shirt" ], 	null ),
		new ItemDefn("Pectoral Breastplate", 		1, [ "Shirt" ], 	null ),
		new ItemDefn
		(
			"Potion of Mind Erasing", 
			1, 
			[ "Potion" ], 	
			function(itemHolder, item)
			{
				itemHolder.itemSubtractOne(item);
				alert("You wake up several hours later.  It's amazing you weren't robbed.");
			}
		),
		new ItemDefn
		(
			"Potion of Soothe Guts", 
			1, 
			[ "Potion" ],
			function (itemHolder, item)
			{
				itemHolder.itemSubtractOne(item);
				alert("Ah, the refreshing taste of bismuth.  As usual, you vomit anyway.");
			}
		),
		new ItemDefn("Ring of Roundness", 		1, [ "Ring" ], 		null ),
		new ItemDefn("Ring of Shinyness", 		1, [ "Ring" ], 		null ),
		new ItemDefn
		(
			"Scroll of Teleport", 
			1, 
			[ "Scroll" ],
			function (itemHolder, item) 
			{ 
				itemHolder.itemSubtractOne(item);
				alert("Yeah, this place looks completely different from the other one."); 
			}
		),
		new ItemDefn("Shield of Concealment", 		1, [ "Handheld" ], 	null ),
		new ItemDefn
		(
			"Sword of Keenness", 
			1, 
			[ "Handheld" ],
			function(itemHolder, item)
			{
				alert("You test the blade.  Contrary to expectations, it's quite dull.");
			}
		),
		new ItemDefn("Trousers of Modesty", 		1, [ "Pants" ], 	null ),
	];

	var userEquipmentDefns =
	[
		new UserEquipmentDefn
		(
			"Biped",
			// slotDefns
			[
				new UserEquipmentSlotDefn("Head", [ "Hat" ] ),
				new UserEquipmentSlotDefn("Torso", [ "Shirt" ] ),
				new UserEquipmentSlotDefn("Gloves", [ "Gloves" ] ),
				new UserEquipmentSlotDefn("Left Finger", [ "Ring" ] ),
				new UserEquipmentSlotDefn("Right Finger", [ "Ring" ] ),
				new UserEquipmentSlotDefn("Left Hand", [ "Handheld" ] ),
				new UserEquipmentSlotDefn("Right Hand", [ "Handheld" ] ),
				new UserEquipmentSlotDefn("Legs", [ "Pants" ] ),
				new UserEquipmentSlotDefn("Feet", [ "Shoes" ] ),
			]
		)
	];

	var user = new User
	(
		"User0",
		new ItemHolder
		(
			"Inventory",
			[
				new Item("Boots of Stomping", 1),
				new Item("Gloves of Dust Detection", 1),
				new Item("Jerkin of Jerkininess", 1),
				new Item("Pectoral Breastplate", 1),
				new Item("Ring of Shinyness", 3),
				new Item("Sword of Keenness", 1),
				new Item("Scroll of Teleport", 5),
				new Item("Trousers of Modesty", 1),
			]
		),
		new UserEquipment
		(
			"Biped",
			[
				new UserEquipmentSlotAssignment
				(
					"Torso", "Pectoral Breastplate"
				),
			]
		)
	);

	var itemSession = new ItemSession
	(
		"ItemSession0",
		itemActions,
		itemDefns,
		userEquipmentDefns,
		user,
		// itemsOnGround
		[
			new Item("Coins", 100),
			new Item("Gem", 7),
			new Item("Shield of Concealment", 1),
			new Item("Potion of Soothe Guts", 1),
		]
	);

	var universe = new Universe(itemSession);

	var display = new Display
	(
		new Coords(800, 300), "LightGray", "White"
	);

	Globals.Instance.initialize(display, universe);

}

// extensions

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

// classes

function ControlButton(name, controllable, pos, size, text, click)
{
	this.name = name;
	this.controllable = controllable;
	this.pos = pos;
	this.size = size;
	this.text = text;
	this.click = click;
}
{
	ControlButton.prototype.drawToDisplay = function(display)
	{
		display.drawRectangle(this.posAbsolute, this.size);
		display.drawText(this.text, this.posAbsolute);
	}

	ControlButton.prototype.posAbsoluteUpdate = function(parent)
	{
		this.posAbsolute = this.pos.clone();
		if (parent != null)
		{
			this.posAbsolute.add(parent.posAbsolute);
		}
		return this;
	}
}

function ControlContainer(name, controllable, pos, size, children)
{
	this.name = name;
	this.controllable = controllable;
	this.pos = pos;
	this.size = size;
	this.children = children;

	this.isHighlighted = false;
}
{
	ControlContainer.prototype.drawToDisplay = function(display, posOfParent)
	{
		display.drawRectangle(this.posAbsolute, this.size);

		if (this.isHighlighted == true)
		{
			var margin = new Coords(1, 1);
			display.drawRectangle
			(
				this.posAbsolute.clone().add(margin),
				this.size.clone().subtract(margin).subtract(margin)
			);
		}

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

	ControlContainer.prototype.posAbsoluteUpdate = function(parent)
	{
		this.posAbsolute = this.pos.clone();
		if (parent != null)
		{
			this.posAbsolute.add(parent.posAbsolute);
		}

		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.posAbsoluteUpdate(this);
		}

		return this;
	}
}

function ControlLabel(name, controllable, pos, text)
{
	this.name = name;
	this.controllable = controllable;
	this.pos = pos;
	this.text = text;
}
{
	ControlLabel.prototype.drawToDisplay = function(display)
	{
		display.drawText(this.text, this.posAbsolute);
	}

	ControlLabel.prototype.posAbsoluteUpdate = function(parent)
	{
		this.posAbsolute = this.pos.clone();
		if (parent != null)
		{
			this.posAbsolute.add(parent.posAbsolute);
		}
		return this;
	}
}

function ControlList(name, controllable, pos, size, ySizeOfEntries, entries)
{
	this.name = name;
	this.controllable = controllable;
	this.pos = pos;
	this.size = size;
	this.entrySize = new Coords(this.size.x, ySizeOfEntries);
	this.entries = entries;

	this.numberOfEntriesVisible = Math.floor
	(
		this.size.y / this.entrySize.y
	);

	this.indexOfFirstVisibleEntry = 0;
}
{
	ControlList.prototype.drawToDisplay = function(display)
	{
		display.drawRectangle(this.posAbsolute, this.size);

		var entrySize = this.entrySize;
			
		for (var i = 0; i < this.numberOfEntriesVisible; i++)
		{
			var entryIndex = i + this.indexOfFirstVisibleEntry;
			if (entryIndex < this.entries.length)
			{
				var entry = this.entries[entryIndex];
				entry.drawToDisplay(display);

				if (entryIndex == this.indexOfChildWithFocus)
				{
					display.drawRectangleHollow
					(
						entry.posAbsolute,
						entrySize
					);
				}
			}
		}
	}

	ControlList.prototype.posAbsoluteUpdate = function(parent)
	{
		this.posAbsolute = this.pos.clone();
		if (parent != null)
		{
			this.posAbsolute.add(parent.posAbsolute);
		}

		for (var i = 0; i < this.numberOfEntriesVisible; i++)
		{
			var entryIndex = i + this.indexOfFirstVisibleEntry;
			if (entryIndex < this.entries.length)
			{
				var entry = this.entries[entryIndex];
				entry.pos.overwriteWith
				(
					new Coords
					(
						0, 
						i * this.entrySize.y
					)	
				);
				entry.posAbsoluteUpdate(this);
			}
		}

		return this;
	}
}

function Controllable()
{
	// static class
}
{
	Controllable.manyToControls = function(controllables, size)
	{
		var returnValues = [];

		for (var i = 0; i < controllables.length; i++)
		{
			var controllable = controllables[i];
			var controllableAsControl = controllable.toControl
			(
				new Coords(0, 0), size
			);
			returnValues.push(controllableAsControl);
		}

		return returnValues;
	}
}

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

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

	// methods

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

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

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

	Coords.prototype.inRangeMax = function(max)
	{
		var returnValue = 
		(
			this.x >= 0 && this.x <= max.x
			&& this.y >= 0 && this.y <= max.y
		);

		return returnValue;
	}

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

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

function Display(sizeInPixels, colorFore, colorBack)
{
	this.sizeInPixels = sizeInPixels;
	this.colorFore = colorFore;
	this.colorBack = colorBack;

	this.fontHeightInPixels = 10; // hack
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle(Coords.Instances.Zeroes, this.sizeInPixels);
	}

	Display.prototype.colorsForeAndBack = function(colorFore, colorBack)
	{
		this.colorFore = colorFore;
		this.colorBack = colorBack;
	}

	Display.prototype.drawControl = function(control)
	{
		control.drawToDisplay(this);
	}

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

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

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

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

	Display.prototype.drawUniverse = function(universe)
	{
		this.drawItemSession(universe.itemSession);
	}

	Display.prototype.drawItemSession = function(itemSession)
	{
		var itemSessionAsControl = itemSession.toControl
		(
			new Coords(0, 0), 
			this.sizeInPixels
		);

		itemSessionAsControl.posAbsoluteUpdate();

		this.drawControl(itemSessionAsControl);
	}

	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.fontHeightInPixels + "px sans-serif";

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

function Globals()
{}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(display, universe)
	{
		this.display = display;
		this.inputHelper = new InputHelper();
		this.universe = universe;

		this.display.initialize();
		this.universe.initialize();

		this.display.drawUniverse(this.universe);

		this.inputHelper.initialize();
	}
}

function Image(filePath)
{
	this.filePath = filePath;
}

function InputHelper()
{
	this.keyPressed = null;
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeydown.bind(this);
		document.body.onkeyup = this.handleEventKeyup.bind(this);
	}

	// event handlers

	InputHelper.prototype.handleEventKeydown = function(event)
	{
		event.preventDefault();
		this.keyPressed = event.key;
		Globals.Instance.universe.update();
	}

	InputHelper.prototype.handleEventKeyup = function(event)
	{
		this.keyPressed = null;
	}
}

function Item(defnName, quantity)
{
	this.id = Item.IDNext++;
	this.defnName = defnName;
	this.quantity = quantity;
}
{
	// static variables

	Item.IDNext = 0;

	// instance methods

	Item.prototype.defn = function()
	{
		return Globals.Instance.universe.itemSession.itemDefns[this.defnName];
	}

	Item.prototype.use = function(itemHolder)
	{
		var use = this.defn().use;
		if (use == null)
		{
			alert("Nothing happens.");
		}
		else
		{
			use(itemHolder, this);
		}
	}

	// controls

	Item.prototype.toControl = function(pos, size)
	{
		this._control = null; // hack - Always invalidate.

		if (this._control == null)
		{
			var labelItem = new ControlLabel
			(
				"labelItem" + this.id,
				this,
				pos,
				this.toString()
			);

			this._control = labelItem;
		}

		return this._control;
	}

	// string 

	Item.prototype.toString = function()
	{
		return this.defnName + " (" + this.quantity + ")";
	}
}

function ItemAction(name, perform)
{
	this.name = name;
	this.perform = perform;
}
{
	ItemAction.Instances = new ItemAction_Instances();

	function ItemAction_Instances()
	{
		this.Drop = new ItemAction
		(
			"Drop", 
			function(itemSession, itemHolder, item)
			{
				var user = itemSession.user;
				var itemHolderForGround = itemSession.itemHolderForGround;

				if (user.equipment.itemEquipped(item) == true)
				{
					alert("Cannot drop an equipped item!");
				}
				else if (itemHolder == itemHolderForGround)
				{
					alert("Already been dropped!");
				}
				else
				{
					user.itemHolder.itemRemove(item);
					itemHolderForGround.itemAdd(item);
				}
			}
		);

		this.Equip = new ItemAction
		(
			"Equip",
			function(itemSession, itemHolder, item)
			{ 
				if (itemHolder == itemSession.itemHolderForGround)
				{
					alert("Pick it up first!");
				}
				else
				{
					var equipment = itemSession.user.equipment;
					if (equipment.itemEquipped(item) == true)
					{
						alert("Already equipped!");
					}
					else
					{
						equipment.itemEquip(item);
					}
				}
			}
		);

		this.Merge = new ItemAction
		(
			"Merge", 
			function(itemSession, itemHolder, item)
			{ 
				var items = itemHolder.items;
				for (var i = 0; i < items.length; i++)
				{
					var itemOther = items[i];
					if (itemOther.defnName == item.defnName)
					{
						if (itemOther != item)
						{
							itemOther.quantity += item.quantity;
							itemHolder.itemRemove(item);
							break;
						}
					}
				}
			}
		);

		this.Split = new ItemAction
		(
			"Split", 
			function(itemSession, itemHolder, item)
			{ 
				if (item.quantity > 1)
				{
					var quantityToSplit = Math.floor
					(
						item.quantity / 2
					);
					item.quantity -= quantityToSplit;
					var itemNew = new Item
					(
						item.defnName, 
						quantityToSplit
					);
					itemHolder.items.push(itemNew);
				}
			}
		);

		this.Take = new ItemAction
		(
			"Take", 
			function(itemSession, itemHolder, item)
			{ 
				var itemHolderForUser = itemSession.user.itemHolder;
				if (itemHolder == itemHolderForUser)
				{
					alert("You've already got it!");
				}
				else
				{
					itemSession.itemHolderForGround.itemRemove(item);
					itemHolderForUser.itemAdd(item);
				}
			}
		);

		this.Unequip = new ItemAction
		(
			"Unequip", 
			function(itemSession, itemHolder, item)
			{ 
				if (itemHolder == itemSession.itemHolderForGround)
				{
					alert("Already not equipped!");
				}
				else
				{
					var equipment = itemSession.user.equipment;
					if (equipment.itemEquipped(item) == false)
					{
						alert("Already not equipped!");
					}
					else
					{
						equipment.itemUnequip(item);
					}
				}				
			}
		);

		this.Use = new ItemAction
		(
			"Use", 
			function(itemSession, itemHolder, item)
			{ 
				item.use(itemHolder);
			}
		);

		this._All = 
		[
			this.Drop,
			this.Equip,
			this.Merge,
			this.Split,
			this.Take,
			this.Unequip,
			this.Use,
		];
	}

	// methods

	// controllable

	ItemAction.prototype.toControl = function(pos, size)
	{
		var returnValue = new ControlLabel
		(
			"labelItemAction" + this.name,
			this,
			pos,
			this.name
		);

		return returnValue;
	}
}

function ItemCategory(name)
{
	this.name = name;
}

function ItemDefn(name, weight, categoryNames, use)
{
	this.name = name;
	this.weight = weight;
	this.categoryNames = categoryNames;
	this.use = use;
}

function ItemHolder(name, items)
{
	this.name = name;
	this.items = items;

	this.indexOfItemSelected = null;
}
{
	// instance methods

	ItemHolder.prototype.itemAdd = function(itemToAdd)
	{
		var itemExisting = null;

		for (var i = 0; i < this.items.length; i++)
		{
			var item = this.items[i];
			if (item.defnName == itemToAdd.defnName)
			{
				itemExisting = item;
				break;
			}
		}

		if (itemExisting == null)
		{
			this.items.push(itemToAdd);
		}
		else
		{
			itemExisting.quantity += itemToAdd.quantity;
		}
	}

	ItemHolder.prototype.itemRemove = function(item)
	{
		var itemIndex = this.items.indexOf(item);
		this.items.splice
		(
			itemIndex, 1
		);
	}

	ItemHolder.prototype.item = function(itemDefnNameToGet)
	{
		var returnValue = null;

		for (var i = 0; i < this.items.length; i++)
		{	
			var item = this.items[i];
			var itemDefnName = item.defnName;
			if (itemDefnName == itemDefnNameToGet)
			{
				returnValue = item;
				break;
			}
		}

		return returnValue; 
	}

	ItemHolder.prototype.itemSelectNext = function(offset)
	{
		if (this.indexOfItemSelected == null)
		{
			this.indexOfItemSelected = 0;
		}
		else 
		{
			this.indexOfItemSelected += offset;

			if (this.indexOfItemSelected >= this.items.length)
			{
				this.indexOfItemSelected = 0;
			}		
			else if (this.indexOfItemSelected < 0)
			{
				this.indexOfItemSelected = this.items.length - 1;
			}
		}
	}

	ItemHolder.prototype.itemSelected = function()
	{
		return (this.indexOfItemSelected == null ? null : this.items[this.indexOfItemSelected]);
	}

	ItemHolder.prototype.itemSubtractOne = function(item)
	{
		item.quantity -= 1;
		if (item.quantity <= 0)
		{
			this.itemRemove(item);
		}
	}

	ItemHolder.prototype.itemUse = function(item)
	{
		item.use(this);
	}

	// controls

	ItemHolder.prototype.toControl = function(pos, size)
	{
		this._control = null; // hack - Always invalidate.

		if (this._control == null)
		{
			var sizeOfChildContainer = new Coords
			(
				(size.x - 20),
				size.y - 30
			);

			var itemsAsListEntries = Controllable.manyToControls
			(
				this.items,
				new Coords(0, 0) // size
			);

			var listItems = new ControlList
			(
				"listItems",
				this,
				new Coords(10, 20), // pos
				sizeOfChildContainer,
				10, // ySizeOfEntries
				itemsAsListEntries
			);

			listItems.indexOfChildWithFocus = this.indexOfItemSelected;
			
			var containerItemHolder = new ControlContainer
			(
				"containerItemHolder",
				this,
				pos,
				size,
				// children
				[
					new ControlLabel
					(
						"labelItemHolder",
						this,
						new Coords(10, 10), // pos
						this.name + ":"
					),

					listItems,
				]
			);

			this._control = containerItemHolder;
		}

		return this._control;
	}
}

function ItemSelection(itemActions)
{
	this.itemActions = itemActions;

	this.itemHolder = null;
	this.indexOfActionSelected = null;
}
{
	ItemSelection.prototype.actionSelectNext = function(offset)
	{
		if (this.indexOfActionSelected == null)
		{
			this.indexOfActionSelected = 0;
		}
		else
		{
			this.indexOfActionSelected += offset;
			if (this.indexOfActionSelected < 0)
			{
				this.indexOfActionSelected = this.itemActions.length - 1;
			}
			else if (this.indexOfActionSelected >= this.itemActions.length)
			{
				this.indexOfActionSelected = 0;
			}
		}
	}

	ItemSelection.prototype.actionSelected = function()
	{
		return this.itemActions[this.indexOfActionSelected];
	}	

	ItemSelection.prototype.itemSelected = function()
	{
		var returnValue = null;

		if (this.itemHolder != null)
		{
			returnValue = this.itemHolder.itemSelected();
		}

		return returnValue;
	}

	// controllable

	ItemSelection.prototype.toControl = function(pos, size)
	{
		var sizeOfColumn = size.clone().subtract
		(
			new Coords(30, 0)
		).divide
		(
			new Coords(2, 1)
		);

		var sizeOfImage = new Coords(sizeOfColumn.x, sizeOfColumn.x);
		var sizeOfDescription = new Coords(size.x - 20, sizeOfColumn.x / 2);
		var sizeOfActions = new Coords(size.x - 20, 100); // hack

		var listActions = new ControlList
		(
			"listActions",
			this.itemActions,
			new Coords(10, 20), // pos
			new Coords(sizeOfActions.x - 20, sizeOfActions.y - 30), // size
			10, // entrySizeY
			Controllable.manyToControls(this.itemActions)
		);

		if (this.indexOfActionSelected != null)
		{
			listActions.indexOfChildWithFocus = this.indexOfActionSelected;
		}
			
		var itemSelected = this.itemSelected();

		var containerItemSelected = new ControlContainer
		(
			"containerItemSelected",
			this,
			pos,
			size,
			// children
			[
				new ControlLabel
				(
					"labelSelected",
					this,
					new Coords(10, 10),
					"Selected:"
				),

				new ControlLabel
				(
					"labelItemSelectedName",
					this,
					new Coords(10, 20),
					(itemSelected == null ? "[none]" : itemSelected.defnName)
				),

				new ControlContainer
				(
					"containerItemSelectedImage",
					this,
					new Coords(20 + sizeOfColumn.x, 20),
					sizeOfImage,
					[
						new ControlLabel
						(
							"labelImage",
							this,
							new Coords(10, 10),
							"[image]"
						),
					]
				),

				new ControlContainer
				(
					"containerDescription",
					this,
					new Coords(10, 30 + sizeOfImage.x),
					sizeOfDescription,
					[
						new ControlLabel
						(
							"labelDescription", 
							this,
							new Coords(10, 10), 
							"[description]"
						)
					]
				),

				new ControlContainer
				(
					"containerActions",
					this,
					new Coords(10, 40 + sizeOfImage.y + sizeOfDescription.y),
					sizeOfActions,
					[
						new ControlLabel
						(
							"labelActions", 
							this,
							new Coords(10, 10), 
							"Actions:"
						),
					
						listActions
					]
				),
			]
		);

		return containerItemSelected;
	}		
}

function ItemSession(name, itemActions, itemDefns, equipmentDefns, user, itemsOnGround)
{
	this.name = name;

	this.itemActions = itemActions;
	this.itemActions.addLookups("name");

	this.itemDefns = itemDefns;
	this.itemDefns.addLookups("name");

	this.equipmentDefns = equipmentDefns;
	this.equipmentDefns.addLookups("name");

	this.user = user;
	this.itemHolderForGround = new ItemHolder
	(
		"Ground",
		itemsOnGround
	);

	this.itemSelection = new ItemSelection(this.itemActions);

	this.fieldWithFocus = this.user;
}
{
	ItemSession.prototype.update = function()
	{
		var keyPressed = Globals.Instance.inputHelper.keyPressed;

		if (this.fieldWithFocus == this.user)
		{
			if (keyPressed == "ArrowDown")
			{
				this.user.itemHolder.itemSelectNext(1);	
			}
			else if (keyPressed == "ArrowLeft")
			{
				this.fieldWithFocus = this.itemHolderForGround;
			}
			else if (keyPressed == "ArrowUp")
			{
				this.user.itemHolder.itemSelectNext(-1);
			}
			else if (keyPressed == "ArrowRight")
			{
				this.fieldWithFocus = this.itemSelection;
			}
	
		}
		else if (this.fieldWithFocus == this.itemSelection)
		{
			if (keyPressed == "ArrowDown")
			{
				this.itemSelection.actionSelectNext(1);	
			}
			else if (keyPressed == "ArrowLeft")
			{
				this.fieldWithFocus = this.user;
			}
			else if (keyPressed == "ArrowUp")
			{
				this.itemSelection.actionSelectNext(-1);
			}
			else if (keyPressed == "ArrowRight")
			{
				this.fieldWithFocus = this.itemHolderForGround;
			}
			else if (keyPressed == "Enter")
			{
				var actionSelected = this.itemSelection.actionSelected();

				if (actionSelected != null)
				{
					var itemSelected = this.itemSelection.itemSelected();
					if (itemSelected != null)
					{
						actionSelected.perform(this, this.itemSelection.itemHolder, itemSelected);
					}
				}
			}
		}
		else if (this.fieldWithFocus == this.itemHolderForGround)
		{
			if (keyPressed == "ArrowDown")
			{
				this.itemHolderForGround.itemSelectNext(1);
			}
			else if (keyPressed == "ArrowLeft")
			{
				this.fieldWithFocus = this.itemSelection;
			}
			else if (keyPressed == "ArrowUp")
			{
				this.itemHolderForGround.itemSelectNext(-1);
			}
			else if (keyPressed == "ArrowRight")
			{
				this.fieldWithFocus = this.user;
			}
		}

		if (this.fieldWithFocus == this.user)
		{
			this.itemSelection.itemHolder = this.user.itemHolder;
		}
		else if (this.fieldWithFocus == this.itemHolderForGround)
		{
			this.itemSelection.itemHolder = this.itemHolderForGround;
		}

		Globals.Instance.display.keyPressed = null;

		Globals.Instance.display.drawItemSession(this);
	}

	ItemSession.prototype.initialize = function()
	{
		this.user.initialize();
	}

	// controls

	ItemSession.prototype.toControl = function(pos, size)
	{
		this._control = null; // hack - Always invalidate.

		if (this._control == null)
		{
			var sizeOfUserContainer = size.clone().subtract
			(
				new Coords(40, 30)	
			).divide
			(
				new Coords(2, 1)
			);

			var sizeOfContainersGroundAndSelected = size.clone().subtract
			(
				new Coords(sizeOfUserContainer.x + 40, 30)
			).divide
			(
				new Coords(2, 1)
			);

			var containerUser = this.user.toControl
			(
				new Coords(10, 20), 
				sizeOfUserContainer
			);

			var containerSelection = this.itemSelection.toControl
			(
				new Coords
				(
					sizeOfUserContainer.x + 20, 20
				), // pos
				sizeOfContainersGroundAndSelected
			);

			var containerGround = this.itemHolderForGround.toControl
			(
				new Coords
				(
					sizeOfUserContainer.x + sizeOfContainersGroundAndSelected.x + 30, 
					20
				), // pos
				sizeOfContainersGroundAndSelected	
			);

			if (this.fieldWithFocus == this.user)
			{
				containerUser.isHighlighted = true;
			}
			else if (this.fieldWithFocus == this.itemHolderForGround)
			{
				containerGround.isHighlighted = true;
			}
			else if (this.fieldWithFocus == this.itemSelection)
			{
				containerSelection.isHighlighted = true;	
			}

			var containerItemSession = new ControlContainer
			(
				"containerItemSession",
				this,
				pos,
				size,
				[
					new ControlLabel
					(
						"labelItemSession",
						this,
						new Coords(10, 10),
						this.name + ":"	
					),

					containerUser,

					containerSelection,

					containerGround,
				]
			);

			this._control = containerItemSession;
		}

		return this._control;
	}
}

function Universe(itemSession)
{
	this.itemSession = itemSession;
}
{
	Universe.prototype.initialize = function()
	{
		this.itemSession.initialize();
	}

	Universe.prototype.update = function()
	{
		this.itemSession.update();
	}
}

function User(name, itemHolder, equipment)
{
	this.name = name;
	this.itemHolder = itemHolder;
	this.equipment = equipment;
}
{
	User.prototype.initialize = function()
	{
		this.equipment.initialize(this);
	}

	// controls

	User.prototype.toControl = function(pos, size)
	{
		this._control = null; // hack - Always invalidate.

		if (this._control == null)
		{
			var sizeOfChildContainer = size.clone().subtract
			(
				new Coords(30, 30)
			).divide
			(
				new Coords(2, 1)
			);
	
			var containerUser = new ControlContainer
			(
				"containerUser",
				this,
				pos,
				size,
				// children
				[
					new ControlLabel
					(
						"labelUserName",
						this,
						new Coords(10, 10), // pos
						this.name + ":"
					),

					this.equipment.toControl
					(
						new Coords(10, 20), // pos
						sizeOfChildContainer
					),

					this.itemHolder.toControl
					(
						new Coords(sizeOfChildContainer.x + 20, 20), // pos
						sizeOfChildContainer
					),

				]
			);

			this._control = containerUser;
		}

		return this._control;
	}
}

function UserEquipment(defnName, slotAssignments)
{
	this.defnName = defnName;
	this.slotAssignments = slotAssignments;
	this.slots = null;
}
{

	// instance methods

	UserEquipment.prototype.defn = function()
	{
		return Globals.Instance.universe.itemSession.equipmentDefns[this.defnName];
	}

	UserEquipment.prototype.initialize = function(user)
	{
		var slotDefns = this.defn().slotDefns;

		this.slots = [];

		for (var i = 0; i < slotDefns.length; i++)
		{
			var slotDefn = slotDefns[i];
			var slotDefnName = slotDefn.name;
			var slot = new UserEquipmentSlot(slotDefnName, null);
			this.slots.push(slot);
		}

		this.slots.addLookups("defnName");

		for (var i = 0; i < this.slotAssignments.length; i++)
		{
			var slotAssignment = this.slotAssignments[i];
			var slotDefnName = slotAssignment.slotDefnName;
			var itemDefnName = slotAssignment.itemDefnName;

			var slot = this.slots[slotDefnName];
			var itemInSlot = user.itemHolder.item(itemDefnName);
			slot.item = itemInSlot;
		}

		this.slotAssignments = null;
	}

	UserEquipment.prototype.itemEquip = function(itemToEquip)
	{
		var userEquipmentDefn = this.defn();
		var itemDefn = itemToEquip.defn();
		var itemDefnCategoryNames = itemDefn.categoryNames;

		var slotToEquipItemIn = null;

		for (var i = 0; i < this.slots.length; i++)
		{
			var slot = this.slots[i];
			if (slot.item == itemToEquip)
			{
				slotToEquipItemIn = slot;
				break;
			}
			else if (slot.item == null)
			{
				var slotDefn = slot.defn(userEquipmentDefn);
				var itemCategoryNamesEquippable =
					slotDefn.itemCategoryNamesEquippable;
				for (var j = 0; j < itemCategoryNamesEquippable.length; j++)
				{
					var itemCategoryNameEquippable =
						itemCategoryNamesEquippable[j];
					var index = itemDefnCategoryNames.indexOf
					(
						itemCategoryNameEquippable
					);
	
					if (index >= 0)
					{
						slotToEquipItemIn = slot;
						break;
					}
				}
			}
		}

		if (slotToEquipItemIn == null)
		{
			alert("No matching free slots!");
		}
		else
		{
			slotToEquipItemIn.item = itemToEquip;
		}
	}


	UserEquipment.prototype.itemEquipped = function(itemToCheck)
	{
		var returnValue = false;

		for (var i = 0; i < this.slots.length; i++)
		{
			var slot = this.slots[i];
			var slotItem = slot.item;
			if (slotItem == itemToCheck)
			{
				returnValue = true;
				break;
			}
		}

		return returnValue;
	}



	UserEquipment.prototype.itemUnequip = function(itemToUnequip)
	{
		for (var i = 0; i < this.slots.length; i++)
		{
			var slot = this.slots[i];
			var slotItem = slot.item;
			if (slotItem == itemToUnequip)
			{
				slot.item = null;
				break;
			}
		}
	}

	// controls

	UserEquipment.prototype.toControl = function(pos, size)
	{
		this._control = null; // hack - Always invalidate.

		if (this._control == null)
		{
			var slotsAsListEntries = Controllable.manyToControls
			(
				this.slots,
				null // size
			); 

			var listSlots = new ControlList
			(
				"listSlots",
				this,
				new Coords(10, 20),
				size.clone().subtract
				(
					new Coords(20, 30)
				),
				10, // entrySizeY
				slotsAsListEntries
			);

			var containerUserEquipment = new ControlContainer
			(
				"containerUserEquipment",
				this,
				pos,
				size,
				[
					new ControlLabel
					(
						"labelEquipment",
						this,
						new Coords(10, 10),
						"Equipped:"
					),

					listSlots
				]
			);

			this._control = containerUserEquipment;
		}

		return this._control;
	}

}

function UserEquipmentDefn(name, slotDefns)
{
	this.name = name;
	this.slotDefns = slotDefns;
	this.slotDefns.addLookups("name");
}

function UserEquipmentSlot(defnName, item)
{
	this.defnName = defnName;
	this.item = item;
}
{
	UserEquipmentSlot.prototype.defn = function(userEquipmentDefn)
	{
		return userEquipmentDefn.slotDefns[this.defnName];
	}
}
{
	UserEquipmentSlot.prototype.toControl = function(pos, size)
	{
		var thisAsText = 
			this.defnName + ": " 
			+ (this.item == null ? "[none]" : this.item.defnName);

		var returnValue = new ControlLabel
		(
			"label" + this.slotDefnName,
			this,
			pos,
			thisAsText
		);

		return returnValue;
	}
}

function UserEquipmentSlotAssignment(slotDefnName, itemDefnName)
{
	this.slotDefnName = slotDefnName;
	this.itemDefnName = itemDefnName;
}

function UserEquipmentSlotDefn(name, itemCategoryNamesEquippable)
{
	this.name = name;
	this.itemCategoryNamesEquippable = itemCategoryNamesEquippable;
}


// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

TrueType Fonts in JavaScript with Font-Face

The JavaScript code below, when run, presents an interface that allows the user to specify a font file in TTF format and some text to render in that font. Then, when the button is clicked, the specified text is rendered using the specified font.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. You’ll also need to locate a .ttf file. On Windows machines, a good place to look is in the “C:\Windows\fonts” folder, which is where I found the “impact.ttf” file that I used in my testing.

FontFromFile


<html>
<body>

<!-- ui -->

<div>
	<label>Font File:</label>
	<input id="inputFontFile" type="file"></input>
</div>
<div>
	<label>Text to Draw:</label>
	<input id="inputTextToDraw" value="The quick fox jumped over the lazy dogs."></input>
</div>
<div><button id="buttonDrawTextWithFont" onclick="buttonDrawTextWithFont_Clicked();">Draw Text with Font</button></div>
<div style="border:1px solid">
	<canvas id="canvasOutput" width="300" height="40"></canvas>
<div>

<script type="text/javascript">

// ui event handlers

function buttonDrawTextWithFont_Clicked()
{
	var inputFontFile = document.getElementById("inputFontFile");

	var fontFile = inputFontFile.files[0];

	if (fontFile == null)
	{
		alert("No font file specified!");
	}
	else
	{
		var fontFileReader = new FileReader();
		fontFileReader.onload = 
			buttonDrawTextWithFont_Clicked_FontFileLoaded;
		fontFileReader.readAsDataURL(fontFile);
	}	
}

function buttonDrawTextWithFont_Clicked_FontFileLoaded(fileLoadedEvent)
{
	var fontFileReader = fileLoadedEvent.target;
	var fontFileAsDataURL = fontFileReader.result;

	var inputFontFile = document.getElementById("inputFontFile");
	var fontName = inputFontFile.files[0].name.split(".")[0];

	var fontAsStyleElement = document.createElement("style");
	fontAsStyleElement.innerHTML = 
		"@font-face { "
		+ "font-family: '" + fontName + "';"
		+ "src: url('" + fontFileAsDataURL + "');"; 
		+ "}"

	document.head.appendChild(fontAsStyleElement);	

	var inputTextToDraw = document.getElementById("inputTextToDraw");
	var textToDraw = inputTextToDraw.value;

	var canvasOutput = document.getElementById("canvasOutput");
	var graphics = canvasOutput.getContext("2d");

	graphics.font = "16px " + fontName;
	graphics.fillStyle = "Black";
	graphics.fillText(textToDraw, 0, canvasOutput.height / 2);
}

</script>

</body>
</html>

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

A Simple HTML Parser in JavaScript

The JavaScript code below, when run, prompts the user to enter some HTML text. When the “Parse” button is clicked, that text will be parsed into an instance of a class named “HTMLDocument”, which contains within it objects corresponding to the elements and attributes of the HTML document. This object will then be converted back to text and displayed in order to verify that the HTML was parsed correctly.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Click the “Load Demo Text” button to populate the text field with a simple HTML document for testing.

Obviously there are functions built into JavaScript that can do this same thing, much better and more efficiently. At least one such method has actually been discussed in a previous post. But this program was written as a starting point for creating parsers for other formats, or in other programming languages.


<html>
<body>

	<!-- ui -->
	<div>
		<div><label>Layout:</label></div>
		<div><textarea id="inputStringToParse" cols="80" rows="20"></textarea></div>
	</div>
	<div>
		<button id="buttonLoadDemoText" onclick="buttonLoadDemoText_Clicked();">Load Demo Text</button>
		<button id="buttonParse" onclick="buttonParse_Clicked();">Parse</button>
	</div>
	<div>
		<label>Output:</label>
		<div id="divOutput"></div>
	</div>
	
<script type="text/javascript">

// ui event handlers

function buttonLoadDemoText_Clicked()
{
	// Any full tags in the string 
	// will be interpreted as real tags in the page,
	// so break tags up into substrings and concatenate.

	var demoText =
		"<" + "html><" + "body>"
		+ "<" + "script type='text/javascript'>alert('hello');<" + "/script>"
		+ "<" + "/body><" + "/html>";
	var inputStringToParse = document.getElementById("inputStringToParse");
	inputStringToParse.value = demoText;
}
	
function buttonParse_Clicked()
{
	var inputStringToParse = document.getElementById("inputStringToParse");
	var stringToParse = inputStringToParse.value;
	var htmlDocument = HTMLParser.stringToHTMLDocument(stringToParse);
	var htmlDocumentAsString = htmlDocument.toString();
	var divOutput = document.getElementById("divOutput");
	divOutput.innerHTML = htmlDocumentAsString;
}

// extensions

function StringExtensions()
{
	// extension class
}
{
	String.prototype.splitAndIgnoreEmptyStrings = function(delimiter)
	{
		return this.split(delimiter).filter
		(
			function(s) { return s != "";} 
		);
	}
}

// classes

function HTMLAttribute(name, value)
{
	this.name = name;
	this.value = value;
}
{
	HTMLAttribute.prototype.toString = function()
	{
		var returnValue = this.name + "=" + this.value;
		return returnValue;
	}
}

function HTMLDocument(elementRoot)
{
	this.elementRoot = elementRoot;
}
{
	HTMLDocument.prototype.toString = function()
	{
		var returnValue = this.elementRoot.toString();
		return returnValue;
	}
}

function HTMLElement(parent)
{
	this.parent = parent;
	this.tagName = null;
	this.attributes = [];
	this.content = null;
	this.children = [];

	if (this.parent != null)
	{
		this.parent.children.push(this);
	}
}
{
	HTMLElement.prototype.toString = function()
	{
		var returnValue = 
			"<" + this.tagName + " ";

		for (var a = 0; a < this.attributes.length; a++)
		{
			var attribute = this.attributes[a];
			var attributeAsString = attribute.toString();
			returnValue += attributeAsString;
		}

		if (this.children.length == 0 && this.content == null)
		{
				returnValue += "/>";
		}
		else
		{
			returnValue += ">";

			if (this.content == null)
			{
				for (var c = 0; c < this.children.length; c++)
				{
					var child = this.children[c];
					returnValue += child.toString();
				}
			}
			else
			{
				returnValue += this.content;
			}

			returnValue += "</" + this.tagName + ">";

		}

		returnValue = returnValue.split("<").join("&lt;");
		returnValue = returnValue.split(">").join("&gt;");

		return returnValue;
	}
}

function HTMLParser()
{
	// static class
}
{
	HTMLParser.stringToHTMLDocument = function(stringToParse)
	{
		var tagsAndContentsAsStrings = stringToParse.splitAndIgnoreEmptyStrings("<");

		var elementCurrent = null;
		var elementRoot;

		for (var t = 0; t < tagsAndContentsAsStrings.length; t++)
		{
			var tagAndContentAsString = tagsAndContentsAsStrings[t];
			
			if (tagAndContentAsString.startsWith("/") == true)
			{
				// close tag
				elementCurrent = elementCurrent.parent;
			}
			else
			{
				elementCurrent = new HTMLElement(elementCurrent);

				if (elementCurrent.parent == null)
				{
					elementRoot = elementCurrent;	
				}

				var tagAndContent = tagAndContentAsString.splitAndIgnoreEmptyStrings(">");

				var openTagAsString = tagAndContent[0];

				if (openTagAsString.endsWith("/") == true)
				{
					// self-closing tag
					elementCurrent = elementParent;
				}
				else if (tagAndContent.length > 1)
				{
					elementCurrent.content = tagAndContent[1];	
				}

				var tagNameAndAttributesAsStrings = openTagAsString.splitAndIgnoreEmptyStrings(" ");

				elementCurrent.tagName = tagNameAndAttributesAsStrings[0];

				for (var a = 1; a < tagNameAndAttributesAsStrings.length; a++)
				{
					var attributeAsString = tagNameAndAttributesAsStrings[a];
					var attributeNameAndValue = attributeAsString.split("=");
					var attributeName = attributeNameAndValue[0];
					var attributeValue = attributeNameAndValue[1];
					var attribute = new HTMLAttribute
					(
						attributeName, attributeValue
					);
					elementCurrent.attributes.push(attribute);
				}
			}			
		}

		var returnValue = new HTMLDocument(elementRoot);

		return returnValue;
	}
}

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

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

Getting Input from Gamepads with HTML5 and JavaScript

The JavaScript program below, when run, will display the current state of all gamepads connected to the user’s system. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This code makes use of the semi-standardized Gamepad API for web browsers, which is still surprisingly tricky. There’s supposed to be a way to capture events when the user connects and/or disconnects a gamepad, but Google Chrome doesn’t fully support it, so I’m not checking for that. So the code basically just polls all the gamepads 10 times a second and updates the display accordingly. After first plugging in the controller, or after leaving it unattended for a few minutes, it may be necessary to press certain buttons on the controller to “wake it up” and then restart the program.

GamepadStatus


<html>
<body>

<div id="divOutput"></div>

<script type="text/javascript">

// main

function main()
{
	var inputHelper = new InputHelper();
	new TimerHelper(inputHelper).initialize();
}

// classes

function Gamepad(index)
{
	this.index = index;
	this.axisDisplacements = [];
	this.buttonsPressed = [];
}
{
	Gamepad.prototype.statusAsString = function(inputHelper)
	{
		var returnValue = "Gamepad " + this.index + ": ";

		for (var i = 0; i < this.axisDisplacements.length; i++)
		{
			var axisDisplacement = this.axisDisplacements[i];
			var axisDisplacementAsString = 
				"A" + i + "="
				+ axisDisplacement + " ";
			returnValue += axisDisplacementAsString;			
		}

		for (var i = 0; i < this.buttonsPressed.length; i++)
		{
			var isButtonPressed = this.buttonsPressed[i];
			var buttonAsString = 
				"B" + i + "=" 
				+ (isButtonPressed == true ? 1 : 0) + " ";
			returnValue += buttonAsString;
		}

		return returnValue;
	}

	Gamepad.prototype.updateFromSystemGamepad = function(systemGamepad)
	{
		var systemAxisDisplacements = systemGamepad.axes;
		for (var i = 0; i < systemAxisDisplacements.length; i++)
		{
			var systemAxisDisplacement = systemAxisDisplacements[i];
			this.axisDisplacements[i] = systemAxisDisplacement;
		}

		var systemButtons = systemGamepad.buttons;
		for (var i = 0; i < systemButtons.length; i++)
		{
			var isButtonPressed = systemButtons[i].pressed;
			this.buttonsPressed[i] = isButtonPressed;
		}
	}
}

function DisplayHelper()
{
	// static class
}
{
	DisplayHelper.showMessage = function(message)
	{
		var divOutput = document.getElementById("divOutput");
		divOutput.innerHTML = message;
	}
}

function InputHelper()
{
	this.gamepadsConnected = [];
}
{
	InputHelper.prototype.checkForGamepads = function()
	{
		var systemGamepads = this.systemGamepads();
		for (var i = 0; i < systemGamepads.length; i++)
		{
			var systemGamepad = systemGamepads[i];
			if (systemGamepad != null)
			{
				var gamepad = new Gamepad(i);
				this.gamepadsConnected.push(gamepad);
			}
		}
	}

	InputHelper.prototype.displayStatus = function()
	{
		DisplayHelper.showMessage(this.statusAsString());
	}

	InputHelper.prototype.initialize = function()
	{
		this.checkForGamepads();

		this.displayStatus();
	}

	InputHelper.prototype.statusAsString = function()
	{
		var returnValue = 
			"There are currently " 
			+ this.gamepadsConnected.length 
			+ " gamepad(s) connected:";

		for (var i = 0; i < this.gamepadsConnected.length; i++)
		{
			var gamepad = this.gamepadsConnected[i];
			var gamepadStatus = gamepad.statusAsString();
			returnValue += "<br />" + gamepadStatus;
		}
	
		return returnValue;
	}

	InputHelper.prototype.systemGamepads = function()
	{
		return navigator.getGamepads();
	}

	InputHelper.prototype.update = function()
	{
		var systemGamepads = this.systemGamepads();

		for (var i = 0; i < this.gamepadsConnected.length; i++)
		{
			var gamepad = this.gamepadsConnected[i];
			var systemGamepad = systemGamepads[gamepad.index];
			gamepad.updateFromSystemGamepad(systemGamepad);
		}
		
	}
}

function TimerHelper(inputHelper)
{
	this.inputHelper = inputHelper;
}
{
	TimerHelper.prototype.initialize = function()
	{
		this.inputHelper.initialize();

		this.timer = window.setInterval
		(
			this.handleEventTimerTick.bind(this),
			100 // millisecondsPerTick
		);
	}

	TimerHelper.prototype.handleEventTimerTick = function()
	{
		this.inputHelper.update();
		this.inputHelper.displayStatus();
	}
}

// main

main();

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

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

Capturing an Image from a Camera with HTML5 and JavaScript

The JavaScript code below, when run, will display a real-time feed of the user’s default camera, and allow the user to click a button to capture a still image from that camera. To see it in action, copy it into an .html file, host that file within a web server, and open the corresponding URL in a web browser that runs JavaScript.

Note that the code, as written, will likely only run on web browsers based on the WebKit code library, such as Google Chrome. This is because the code makes use of the WebKit-specific “webkitGetUserMedia” function, as a generic implementation of “getUserMedia” that works in the same way in all browsers has not yet been decided on. It should also be noted that it might be a fairly trivial task to modify the code to work in other browsers.


<html>
<body>

<script type="text/javascript">

// main

function main()
{
	var sizeInPixels = new Coords(160, 120);
	var imageCapturer = new ImageCapturer(sizeInPixels);
	imageCapturer.initialize();

}

// classes

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

function ImageCapturer(sizeInPixels)
{
	this.sizeInPixels = sizeInPixels;
}
{
	ImageCapturer.prototype.captureAndDisplayImage = function()
	{
		var imageCaptured = this.captureImage();
		this.displayImage(imageCaptured);
	}

	ImageCapturer.prototype.captureImage = function()
	{
		return this.videoInput;
	}

	ImageCapturer.prototype.displayImage = function(imageToDraw)
	{
		this.graphics.drawImage
		(
			imageToDraw, 
			0, 0, 
			this.sizeInPixels.x, this.sizeInPixels.y
		);
	}

	ImageCapturer.prototype.initialize = function()
	{
		var divImageCapturer = document.createElement("div");

		var divInput = document.createElement("div");
		var videoInput = document.createElement("video");
		videoInput.width = this.sizeInPixels.x;
		videoInput.height = this.sizeInPixels.y;
		videoInput.autoplay = true;
		divInput.appendChild(videoInput);
		divImageCapturer.appendChild(divInput);
		this.videoInput = videoInput;

		var divControls = document.createElement("div");
		var buttonCaptureImage = document.createElement("button");
		buttonCaptureImage.onclick = this.captureAndDisplayImage.bind(this);
		buttonCaptureImage.innerHTML = "Capture Image";
		divControls.appendChild(buttonCaptureImage);
		divImageCapturer.appendChild(divControls);

		var divOutput = document.createElement("div");
		var canvasOutput = document.createElement("canvas");
		canvasOutput.width = this.sizeInPixels.x;
		canvasOutput.height = this.sizeInPixels.y;
		divOutput.appendChild(canvasOutput);
		divImageCapturer.appendChild(divOutput);

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

		navigator.webkitGetUserMedia // for Google Chrome
		( 
			{ video: true },
			// success
			function(stream)
			{
				var streamAsObjectURL = window.URL.createObjectURL(stream);
				videoInput.src = streamAsObjectURL;
				videoInput.play();
			},
			// error
			function()
			{
				throw "An error has occurred.";
			}
		);	

		document.body.appendChild(divImageCapturer);
	}

}

// run

main();

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

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

A Pixel Matrix Transceiver in JavaScript

The JavaScript code below implements a rudimentary data transmitter that converts a user-specified string to a series of “frames”, each of which is an image made up of a grid of black and white cells. It then displays those images to the screen in sequence. At the same time, a data receiver reads each of these images and converts them back to the corresponding string, and displays the string equivalent of the current image.

The idea is to ultimately expand this program until it can be used as a basis for optical transmission of data through free space. In other words, a primitive network data link. One might point a telescopic camera at the grid being displayed on the monitor of a computer running the transmitter, feed those images to a computer running the transceiver, and decode them back into the original data.

Obviously it’s not there yet.

PixelMatrixTranceiver


<html>

<body>

<!-- ui -->

<div id="divProtocol" style="border:1px solid;">

	<label>Protocol:</label>
	<div>
		<label>Frames per Second:</label>
		<input id="inputFramesPerSecond" type="number" value="1"></input>
	</div>

	<div>
		<label>Frame Size in Cells:</label>
		<input id="inputFrameSizeInCellsX" type="number" value="10"></input>
		<label>x</label>	
		<input id="inputFrameSizeInCellsY" type="number" value="10"></input>
	</div>
</div>

<div id="divTransmitter" style="border:1px solid;">

	<label>Transmitter</label>
	<div>
		<label>Frame Size in Pixels:</label>
		<input id="inputFrameSizeInPixelsX" type="number" value="100"></input>
		<label>x</label>
		<input id="inputFrameSizeInPixelsY" type="number" value="100"></input>
	</div>
	<div>
		<label>Message:</label>
		<input id="inputMessage" value="This is a test!"></input>
	</div>
</div>

<div id="divReceiver" style="border:1px solid;">

	<label>Receiver:</label>
	<div>
		<label>Frame Corner Positions:</label>
		<div>
			<label>NW:</label>
			<input id="inputCornerNWPosX" value="0"></input>
			<label>x</label>
			<input id="inputCornerNWPosY" value="0"></input>
		</div>	


		<div>
			<label>NE:</label>
			<input id="inputCornerNEPosX" value="100"></input>
			<label>x</label>
			<input id="inputCornerNEPosY" value="0"></input>
		</div>

		<div>
			<label>SE:</label>
			<input id="inputCornerSEPosX" value="100"></input>
			<label>x</label>
			<input id="inputCornerSEPosY" value="100"></input>
		</div>

		<div>
			<label>SW:</label>
			<input id="inputCornerSWPosX" value="0"></input>
			<label>x</label>
			<input id="inputCornerSWPosY" value="100"></input>
		</div>
	</div>
</div>

<div>
	<button id="buttonTransmissionStart" onclick="buttonTransmissionStart_Clicked();">Start Transmission</button>
</div>

<div id="divOutput">
</div>

<script>

// events

function buttonTransmissionStart_Clicked()
{
	var inputFramesPerSecond = document.getElementById("inputFramesPerSecond");
	var inputFrameSizeInCellsX = document.getElementById("inputFrameSizeInCellsX");
	var inputFrameSizeInCellsY = document.getElementById("inputFrameSizeInCellsY");
	var inputFrameSizeInPixelsX = document.getElementById("inputFrameSizeInPixelsX");
	var inputFrameSizeInPixelsY = document.getElementById("inputFrameSizeInPixelsY");
	var inputMessage = document.getElementById("inputMessage");
	var inputCornerNWPosX = document.getElementById("inputCornerNWPosX");
	var inputCornerNWPosY = document.getElementById("inputCornerNWPosY");
	var inputCornerNEPosX = document.getElementById("inputCornerNEPosX");
	var inputCornerNEPosY = document.getElementById("inputCornerNEPosY");
	var inputCornerSEPosX = document.getElementById("inputCornerSEPosX");
	var inputCornerSEPosY = document.getElementById("inputCornerSEPosY");
	var inputCornerSWPosX = document.getElementById("inputCornerSWPosX");
	var inputCornerSWPosY = document.getElementById("inputCornerSWPosY");

	var framesPerSecond = parseInt(inputFramesPerSecond.value);
	var frameSizeInCellsX = parseInt(inputFrameSizeInCellsX.value);
	var frameSizeInCellsY = parseInt(inputFrameSizeInCellsY.value);
	var frameSizeInPixelsX = parseInt(inputFrameSizeInPixelsX.value);
	var frameSizeInPixelsY = parseInt(inputFrameSizeInPixelsY.value);
	var message = inputMessage.value;
	var cornerNWPosX = parseInt(inputCornerNWPosX.value);
	var cornerNWPosY = parseInt(inputCornerNWPosY.value);
	var cornerNEPosX = parseInt(inputCornerNEPosX.value);
	var cornerNEPosY = parseInt(inputCornerNEPosY.value);
	var cornerSEPosX = parseInt(inputCornerSEPosX.value);
	var cornerSEPosY = parseInt(inputCornerSEPosY.value);
	var cornerSWPosX = parseInt(inputCornerSWPosX.value);
	var cornerSWPosY = parseInt(inputCornerSWPosY.value);

	var frameSizeInCells = new Coords(frameSizeInCellsX, frameSizeInCellsY);
	var frameSizeInPixels = new Coords(frameSizeInPixelsX, frameSizeInPixelsY);

	var cellCornerPosNW = new Coords(cornerNWPosX, cornerNWPosY);
	var cellCornerPosNE = new Coords(cornerNEPosX, cornerNEPosY);
	var cellCornerPosSE = new Coords(cornerSEPosX, cornerSEPosY);
	var cellCornerPosSW = new Coords(cornerSWPosX, cornerSWPosY);
	
	var display = new Display(20, frameSizeInPixels);
	var transmitter = new Transmitter(frameSizeInCells);
	var receiver = new Receiver
	(
		frameSizeInCells,
		[
			cellCornerPosNW,
			cellCornerPosNE,
			cellCornerPosSE,
			cellCornerPosSW,
		]
	);

	Globals.Instance.initialize
	(
		framesPerSecond,
		display,
		transmitter,
		receiver,
		message
	);
}

// classes 

function Converter()
{
	// static class
}
{
	// conversion

	Converter.bitsToString = function(bitsToConvert, bitsPerChar)
	{
		var returnValue = "";

		var bitOffset = 0;

		while (bitOffset < bitsToConvert.length)
		{
			var charCodeSoFar = 0;

			for (var i = 0; i < bitsPerChar; i++)
			{
				var bit = bitsToConvert[bitOffset];
				var bitValueInPlace = bit << i;
				charCodeSoFar += bitValueInPlace;
				bitOffset++;
			}

			returnValue += String.fromCharCode(charCodeSoFar);
		}

		return returnValue;
	}

	Converter.charToBits = function(charToConvert, bitsPerChar)
	{
		var returnValues = [];
		var charAsInteger = ("" + charToConvert).charCodeAt(0);
		for (var i = 0; i < bitsPerChar; i++)
		{
			var bit = (charAsInteger >> i) & 1;
			returnValues.push(bit);
		}
		
		return returnValues;
	}

	Converter.stringToBits = function(stringToConvert, bitsPerChar)
	{
		var returnValues = [];

		for (var i = 0; i < stringToConvert.length; i++)
		{
			var charToConvert = stringToConvert[i];
			var charAsBits = Converter.charToBits(charToConvert, bitsPerChar);
			returnValues = returnValues.concat(charAsBits);	
		}

		return returnValues;
	}

}

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

	Coords.Instances = new Coords_Instances();

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

	// methods

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

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

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

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

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

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

function Display(fontHeightInPixels, sizeInPixels)
{
	this.fontHeightInPixels = fontHeightInPixels;
	this.sizeInPixels = sizeInPixels;
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			Coords.Instances.Zeroes, 
			this.sizeInPixels, 
			"White",
			"Gray"
		);		
	}

	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		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.drawTextAtPos = function(text, pos, color)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillText
		(
			text, pos.x, pos.y
		);
	}

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

		this.graphics = canvas.getContext("2d");
		this.graphics.font = "" + this.fontHeightInPixels + "px sans-serif";

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

		divOutput.appendChild(canvas);

	}

	Display.prototype.intensityOfPixelAtPos = function(pixelPos)
	{
		var pixelAsComponentsRGBA = this.graphics.getImageData
		(
			pixelPos.x, pixelPos.y, 1, 1	
		).data;

		var componentMax = 255;
		var pixelIntensity = 
			(
				pixelAsComponentsRGBA[0]
				+ pixelAsComponentsRGBA[1]
				+ pixelAsComponentsRGBA[2]
			) 
			/ 
			(componentMax * 3);

		return pixelIntensity;
	}

}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();
	
	Globals.prototype.initialize = function
	(
		framesPerSecond, display, transmitter, receiver, message
	)
	{
		this.display = display;
		this.display.initialize();

		this.transmitter = transmitter;

		this.receiver = receiver;

		var millisecondsPerSecond = 1000;
		var millisecondsPerTimerTick = millisecondsPerSecond / framesPerSecond;
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this),
			millisecondsPerTimerTick
		);

		this.transmitter.sendMessage(message)
	}

	// events

	Globals.prototype.handleEventTimerTick = function()
	{
		this.transmitter.frameAdvanceAndDrawToDisplay
		(
			this.display
		);

		var messageSegment = this.receiver.readFrameFromDisplayAsString
		(
			this.display
		);
	
		this.receiver.drawToDisplay(this.display);
	}
}

function Receiver(frameSizeInCells, cornerCellPositionsClockwiseFromNW)
{
	this.frameSizeInCells = frameSizeInCells;
	this.cornerCellPositionsClockwiseFromNW = 
		cornerCellPositionsClockwiseFromNW;

	this.bitsPerChar = 8; // hack
}
{
	Receiver.prototype.cellCenter = function(cellPosInCells)
	{
		var cellCenterInCells = cellPosInCells.clone().add
		(
			Coords.Instances.Halves
		);

		var cornerNW = this.cornerCellPosNW();
		var cornerNE = this.cornerCellPosNE();
			var cornerSE = this.cornerCellPosSE();
		var cornerSW = this.cornerCellPosSW();

		var cellCenterInFrames = cellCenterInCells.clone().divide
		(
			this.frameSizeInCells
		); 

		var cellLeft = cornerNW.clone().multiplyScalar
		(
			1 - cellCenterInFrames.y
		).add
		(
			cornerSW.clone().multiplyScalar
			(
				cellCenterInFrames.y
			)
		);
		
		var cellRight = cornerNE.clone().multiplyScalar
		(
			1 - cellCenterInFrames.y
		).add
		(
			cornerSE.clone().multiplyScalar
			(
				cellCenterInFrames.y
			)
		);

		var cellCenter = cellLeft.clone().multiplyScalar
		(
			1 - cellCenterInFrames.x
		).add
		(
			cellRight.clone().multiplyScalar
			(
				cellCenterInFrames.x
			)
		);


		return cellCenter;
	}

	Receiver.prototype.cornerCellPosNE = function()
	{
		return this.cornerCellPositionsClockwiseFromNW[1];
	}

	Receiver.prototype.cornerCellPosNW = function()
	{
		return this.cornerCellPositionsClockwiseFromNW[0];
	}

	Receiver.prototype.cornerCellPosSE = function()
	{
		return this.cornerCellPositionsClockwiseFromNW[2];
	}

	Receiver.prototype.cornerCellPosSW = function()
	{
		return this.cornerCellPositionsClockwiseFromNW[3];
	}

	Receiver.prototype.drawToDisplay = function(display)
	{
		var cellPosInCells = new Coords();

		for (var y = 0; y < this.frameSizeInCells.y; y++)
		{
			cellPosInCells.y = y;

			for (var x = 0; x < this.frameSizeInCells.x; x++)
			{
				cellPosInCells.x = x;

				var cellCenterInPixels = this.cellCenter
				(
					cellPosInCells
				);	

				display.drawRectangle
				(
					cellCenterInPixels,
					Coords.Instances.Ones,
					"Cyan"
				);			
			}
		}

		display.drawTextAtPos
		(
			this.messageSegmentForFrameCurrent,
			new Coords
			(
				0, 
				display.sizeInPixels.y
			),
			"Cyan"
		);
	}

	Receiver.prototype.readFrameFromDisplayAsString = function(display)
	{
		var messageSegmentAsBits = [];

		var cellPosInCells = new Coords();

		for (var y = 0; y < this.frameSizeInCells.y; y++)
		{
			cellPosInCells.y = y;

			for (var x = 0; x < this.frameSizeInCells.x; x++)
			{
				cellPosInCells.x = x;

				var cellCenterInPixels = this.cellCenter
				(
					cellPosInCells
				);
		
				var pixelIntensity = display.intensityOfPixelAtPos
				(
					cellCenterInPixels
				);

				var bitForCell;

				if (pixelIntensity < .5)
				{
					bitForCell = 0;
				}
				else // (pixelIntensity >= .5)
				{
					bitForCell = 1;
				}

				messageSegmentAsBits.push(bitForCell);
			}
		}

		var messageSegmentAsString = Converter.bitsToString
		(
			messageSegmentAsBits,
			this.bitsPerChar
		);

		this.messageSegmentForFrameCurrent = messageSegmentAsString;
	}
}

function Transmitter(frameSizeInCells)
{
	this.frameSizeInCells = frameSizeInCells;
	
	this.messageToSendAsString = null;
	this.charOffsetWithinMessage = 0;

	var cellsPerFrame = 
		this.frameSizeInCells.x * this.frameSizeInCells.y;
	var bitsPerCell = 1;
	var bitsPerFrame = cellsPerFrame * bitsPerCell;

	this.bitsPerChar = 8;

	this.frameSizeInChars = Math.floor(bitsPerFrame / this.bitsPerChar);
}
{
	Transmitter.prototype.frameAdvanceAndDrawToDisplay = function(display)
	{
		if (this.messageToSendAsString != null)
		{
			var messageSegmentToSend = this.messageToSendAsString.substr
			(
				this.charOffsetWithinMessage,
				this.frameSizeInChars
			);

			var messageSegmentToSendAsBits = Converter.stringToBits
			(
				messageSegmentToSend,
				this.bitsPerChar
			);

			var messageSegmentToSendAsFrame = new DataFrame
			(
				this.frameSizeInCells,
				messageSegmentToSendAsBits
			);			

			var charOffsetNext =
				this.charOffsetWithinMessage + this.frameSizeInChars;

			if (charOffsetNext < this.messageToSendAsString.length)
			{
				this.charOffsetWithinMessage = charOffsetNext;
			}

			messageSegmentToSendAsFrame.drawToDisplay(display);
		}
	}

	Transmitter.prototype.sendMessage = function(messageToSendAsString)
	{
		this.messageToSendAsString = messageToSendAsString;
		this.charOffsetWithinMessage = 0;
	}
}

function DataFrame(sizeInCells, bits)
{
	this.sizeInCells = sizeInCells;
	this.bits = bits;
}
{
	DataFrame.prototype.drawToDisplay = function(display)
	{
		display.clear();

		var cellPos = new Coords();
		var cellPosInPixels = new Coords();
		var cellSizeInPixels = display.sizeInPixels.clone().divide
		(
			this.sizeInCells
		);

		for (var y = 0; y < this.sizeInCells.y; y++)
		{
			cellPos.y = y;
			for (var x = 0; x <  this.sizeInCells.x; x++)
			{
				cellPos.x = x;

				cellPosInPixels.overwriteWith
				(
					cellPos
				).multiply
				(
					cellSizeInPixels
				);
				
				var bitIndex = y * this.sizeInCells.x + x;
				var bitValue = this.bits[bitIndex];
				var bitColor = (bitValue == 1 ? "White" : "Black");

				display.drawRectangle
				(
					cellPosInPixels,
					cellSizeInPixels,
					bitColor
				);	
			}
		}
	}
}

// tests

function TestFixture()
{
	// do nothing
}
{
	TestFixture.prototype.testBitToString = function()
	{
		var bitsPerChar = 8;
		var messageAsStringToEncode = "This is a test!";
		var messageAsBits = Converter.stringToBits
		(
			messageAsStringToEncode, bitsPerChar
		);
		var messageAsStringDecoded = Converter.bitsToString(messageAsBits, bitsPerChar);

		if (messageAsStringToEncode != messageAsStringDecoded)
		{
			throw "testBitToString() failed!";
		}
	}
}

var tests = new TestFixture();
tests.testBitToString();

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

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

An Aerial Combat Game in JavaScript

The JavaScript code below implements a simple 3D aerial combat (“dogfight”) game. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

Use the W, A, S, D keys to change direction, and the and the up and down arrow keys to accelerate and decelerate. The object is to catch the green ship and avoid the red ship.

DogfightGame


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

// main

function main()
{
	var meshForMovers = new Mesh
	(
		// vertices
		[
			new Coords(-5, 5, 0),
			new Coords(5, 5, 0),
			new Coords(2, 0, 0),
			new Coords(-2, 0, 0),
			new Coords(0, 0, 20),
		],
		// faces
		[
			new MeshFace([ 0, 1, 4 ])	,
			new MeshFace([ 1, 2, 4 ]),
			new MeshFace([ 2, 3, 4 ]),
			new MeshFace([ 3, 0, 4 ]),
			new MeshFace([ 0, 1, 2, 3 ]),
		]
	);

	var venueSizeInPixels = new Coords(1, 1, 1).multiplyScalar(1000000);

	var moverForPlayer = new Mover
	(
		"Player",
		1, // integrity
		1.2, // speedMax
		0.1, // accelPerTick
		0.05, // turnPerTick
		new Location
		(
			new Coords(-100, 0, 0),
			Orientation.fromForwardAndDown
			(
				new Coords(1, 0, 0),
				new Coords(0, 0, 1)
			)
		),
		new Activity_UserInputAccept(),
		"Gray", // color
		meshForMovers
	);

	Globals.Instance.initialize
	(
		20, // framesPerSecond
		new Display(new Coords(300, 300, 1)),
		new Venue
		(
			"Venue0",
			venueSizeInPixels,
			// movers
			[
				moverForPlayer,

				new Mover
				(
					"Prey",
					1, // integrity
					1, // speedMax
					0.1, // accelPerTick
					0.05, // turnPerTick
					new Location
					(
						new Coords(100, 0, 0),
						Orientation.fromForwardAndDown
						(
							new Coords(1, 0, 0),
							new Coords(0, 0, 1)
						)
					),
					new Activity_MoveRandomly(),
					"Green", // color
					meshForMovers
				),

				new Mover
				(
					"Predator",
					1, // integrity
					1, // speedMax
					0.1, // accelPerTick
					0.05, // turnPerTick
					new Location
					(
						new Coords(0, 0, 0),
						Orientation.fromForwardAndDown
						(
							new Coords(1, 0, 0),
							new Coords(0, 0, 1)
						)
					),
					new Activity_MoveTowardTargetPos
					(
						moverForPlayer.loc.pos
					),
					"Red", // color
					meshForMovers
				),
			]
		)
	);
}

// classes

function Action()
{
	// abstract class
}
{
	// instances 

	Action.Instances = new Action_Instances();

	function Action_Instances()
	{
		this.Accelerate = new Action_Accelerate(1);
		this.Decelerate = new Action_Accelerate(-1);
		this.PitchDown = new Action_TurnAxisTowardOtherInDirection(0, 2, 1);
		this.PitchUp = new Action_TurnAxisTowardOtherInDirection(0, 2, -1);
		this.RollLeft = new Action_TurnAxisTowardOtherInDirection(2, 1, 1);
		this.RollRight = new Action_TurnAxisTowardOtherInDirection(2, 1, -1);
		this.YawLeft = new Action_TurnAxisTowardOtherInDirection(0, 1, -1);
		this.YawRight = new Action_TurnAxisTowardOtherInDirection(0, 1, 1);

	}
}

function Action_Accelerate(sign)
{
	this.sign = sign;
}
{
	Action_Accelerate.prototype.performForActor = function(actor)
	{
		var actorLoc = actor.loc;

		actorLoc.accel.overwriteWith
		(
			actorLoc.orientation.forward
		).multiplyScalar
		(
			this.sign * actor.accelPerTick
		);
	}
}

function Action_TurnAxisTowardOtherInDirection(indexOfAxisToBeTurned, indexOfAxisToTurnToward, sign)
{
	this.indexOfAxisToBeTurned = indexOfAxisToBeTurned;
	this.indexOfAxisToTurnToward = indexOfAxisToTurnToward;
	this.sign = sign;

	this.temp = new Coords();
}
{
	Action_TurnAxisTowardOtherInDirection.prototype.performForActor = function(actor)
	{
		var actorLoc = actor.loc;
		var actorOrientation = actorLoc.orientation;
		var axes = actorOrientation.axes;
		
		var axisToBeTurned = axes[this.indexOfAxisToBeTurned];
		var axisToTurnToward = axes[this.indexOfAxisToTurnToward];

		axisToBeTurned.add
		(
			this.temp.overwriteWith
			(
				axisToTurnToward
			).multiplyScalar
			(
				this.sign * actor.turnPerTick
			)
		).normalize();

		actorOrientation.orthogonalizeAxes().normalizeAxes();
	}
}

function Activity_DoNothing()
{
	// do nothing
}
{
	Activity_DoNothing.prototype.performForActor = function(actor)
	{
		// do nothing
	}
}

function Activity_MoveRandomly()
{
	this.ticksToHoldCourse = 0;

	this.targetPos = new Coords();
	this.directionToTarget = new Coords();
	this.ticksToHoldCourseMax = 200;
}
{
	Activity_MoveRandomly.prototype.performForActor = function(actor)
	{
		var actorLoc = actor.loc;
		var actorOrientation = actorLoc.orientation;

		this.ticksToHoldCourse--;

		if (this.ticksToHoldCourse <= 0)
		{
			var venue = Globals.Instance.venue;
		
			this.targetPos.randomize().multiply
			(
				venue.sizeInPixels
			).subtract
			(
				venue.sizeInPixelsHalf
			);

			this.ticksToHoldCourse = Math.floor
			(
				Math.random() 
				* this.ticksToHoldCourseMax
			);
		}

		// hack - Instant turning.
		actorOrientation.forward.overwriteWith
		(
			this.targetPos
		).subtract
		(
			actorLoc.pos
		).normalize();

		actorOrientation.orthogonalizeAxes();

		Action.Instances.Accelerate.performForActor(actor);
	}
}

function Activity_MoveTowardTargetPos(targetPos)
{
	this.targetPos = targetPos;
}
{
	Activity_MoveTowardTargetPos.prototype.performForActor = function(actor)
	{
		var actorLoc = actor.loc;
		var actorOrientation = actorLoc.orientation;
		
		// hack - Instant turning.
		actorOrientation.forward.overwriteWith
		(
			this.targetPos
		).subtract
		(
			actorLoc.pos
		).normalize();

		actorOrientation.orthogonalizeAxes();

		Action.Instances.Accelerate.performForActor(actor);
	}
}

function Activity_UserInputAccept()
{
	// do nothing
}
{
	Activity_UserInputAccept.prototype.performForActor = function(actor)
	{
		var keyCodesPressed = Globals.Instance.inputHelper.keyCodesPressed;

		var actions = Action.Instances;

		for (var i = 0; i < keyCodesPressed.length; i++)
		{
			var keyCodePressed = keyCodesPressed[i];
			var actionToPerform = null;

			if (keyCodePressed == "_38") // up arrow
			{
				actionToPerform = actions.Accelerate;
			}
			if (keyCodePressed == "_40") // down arrow
			{
				actionToPerform = actions.Decelerate;
			}
			else if (keyCodePressed == "_65") // a
			{
				actionToPerform = actions.YawLeft;
			}
			else if (keyCodePressed == "_68") // d
			{
				actionToPerform = actions.YawRight;
			}
			else if (keyCodePressed == "_69") // e
			{
				actionToPerform = actions.RollRight;
			}
			else if (keyCodePressed == "_81") // q
			{
				actionToPerform = actions.RollLeft;
			}
			else if (keyCodePressed == "_83") // s
			{
				actionToPerform = actions.PitchUp;
			}
			else if (keyCodePressed == "_87") // w
			{
				actionToPerform = actions.PitchDown;
			}

			if (actionToPerform != null)
			{
				actionToPerform.performForActor(actor);
			}
		}
	}
}

function Camera(focalLength, loc)
{
	this.focalLength = focalLength;
	this.loc = loc;
}

function CompassPoint(name, direction)
{
	this.name = name;
	this.direction = direction;
	this.loc = new Location(new Coords());
}
{
	CompassPoint.prototype.updateForVenueTimerTick = function(venue)
	{
		this.loc.pos.overwriteWith
		(
			this.direction
		).multiplyScalar
		(
			100
		).add
		(
			venue.moverForPlayer.loc.pos
		);

	}
}

function Constants()
{
	// static class
}
{
	Constants.RadiansPerCycle = Math.PI * 2.0;
}

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

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

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

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

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

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

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

	Coords.prototype.isInRangeMax = function(max)
	{
		returnValue = 
		(
			this.x >= 0 && this.x <= max.x
			&& this.y >= 0 && this.y <= max.y
			&& this.z >= 0 && this.z <= max.z
		);

		return returnValue;
	}

	Coords.prototype.isInRangeMaxXY = function(max)
	{
		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 + this.z * this.z);
	}

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

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

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

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

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

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

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

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

	Coords.prototype.trimToMagnitudeMax = function(magnitudeMax)
	{
		var magnitude = this.magnitude();
		if (magnitude > magnitudeMax)
		{
			this.divideScalar
			(
				magnitude
			).multiplyScalar
			(
				magnitudeMax
			);
		}
		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;
		}

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

		if (this.z < min.z)
		{
			this.z = min.z;
		}
		else if (this.z > max.z)
		{
			this.z = max.z;
		}

		return this;
	}
}

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

	this.sizeInPixelsHalf = sizeInPixels.clone().divideScalar(2);

	// temporary variables
	this.drawPos = new Coords();
	this.transformOrient = new Transform_Orient();
}
{
	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = "Black";
		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
		);
	}

	Display.prototype.drawCompassPointForCamera = function(compassPoint, camera)
	{
		this.graphics.strokeStyle = "Gray";
		this.graphics.fillStyle = "Gray";

		var drawPos = this.drawPos;
		this.transformWorldPosToViewPos
		(
			drawPos.overwriteWith(compassPoint.loc.pos),
			camera
		);

		if (drawPos.isInRangeMaxXY(this.sizeInPixels) == true)
		{
			this.drawTextAtLocationForCamera
			(
				compassPoint.name, 
				compassPoint.loc, 
				camera
			);
		}
	}

	Display.prototype.drawMeshAtLocationForCamera = function(mesh, loc, camera)
	{
		var drawPos = this.drawPos;
		var vertices = mesh.vertices;
		var faces = mesh.faces;

		this.transformOrient.orientation = loc.orientation;
		var meshPos = loc.pos;

		for (var f = 0; f < faces.length; f++)
		{
			var face = faces[f];

			this.graphics.beginPath();

			for (var vi = 0; vi < face.vertexIndices.length; vi++)
			{
				var vertexIndex = face.vertexIndices[vi];
				var vertex = vertices[vertexIndex];

				drawPos.overwriteWith
				(
					vertex
				);

				this.transformOrient.applyToCoords
				(
					drawPos
				);

				drawPos.add
				(
					meshPos
				);

				this.transformWorldPosToViewPos
				(
					drawPos,
					camera
				);

				if (drawPos.z < 0)
				{
					break;
				}
				else if (vi == 0)
				{	
					this.graphics.moveTo(drawPos.x, drawPos.y);
				}
				else
				{
					this.graphics.lineTo(drawPos.x, drawPos.y);
				}
			}

			this.graphics.closePath();
			this.graphics.stroke();
		}
	}

	Display.prototype.drawMoverForCamera = function(mover, camera)
	{
		this.graphics.strokeStyle = mover.color;

		var drawPos = this.drawPos;
		this.transformWorldPosToViewPos
		(
			drawPos.overwriteWith(mover.loc.pos),
			camera
		);

		var moverDistanceFromPlayer = Math.round(mover.displacementFromPlayer.magnitude());
		var moverLabel = mover.name + "\n" + moverDistanceFromPlayer;
		
		if (drawPos.z > 0 && drawPos.isInRangeMaxXY(this.sizeInPixels) == true)
		{
			this.drawTextAtPos(moverLabel, drawPos);
			this.drawMeshAtLocationForCamera(mover.mesh, mover.loc, camera);
		}
		else
		{
			drawPos.z = 0;
			drawPos.subtract
			(
				this.sizeInPixelsHalf
			).trimToMagnitudeMax
			(
				this.sizeInPixelsHalf.x
			).add
			(
				this.sizeInPixelsHalf
			);

			this.drawTextAtPos(moverLabel, drawPos);

			this.graphics.beginPath();
			this.graphics.arc
			(
				drawPos.x, drawPos.y, 
				5, // radius
				0, Constants.RadiansPerCycle
			);
			this.graphics.stroke();
		}


	}

	Display.prototype.drawTextAtLocationForCamera = function(textToDraw, loc, camera)
	{
		var drawPos = this.drawPos;

		drawPos.overwriteWith
		(
			loc.pos
		);

		this.transformWorldPosToViewPos
		(
			drawPos,
			camera
		);

		if (drawPos.z > 0)
		{
			this.drawTextAtPos(textToDraw, drawPos);
		}
	}

	Display.prototype.drawTextAtPos = function(textToDraw, drawPos)
	{
		var textToDrawAsLines = textToDraw.split("\n");
		for (var i = 0; i < textToDrawAsLines.length; i++)
		{
			var lineToDraw = textToDrawAsLines[i];
			this.graphics.fillText(lineToDraw, drawPos.x, drawPos.y);	
			this.graphics.strokeText(lineToDraw, drawPos.x, drawPos.y);
			drawPos.y += 10; // hack
		}
	}


	Display.prototype.drawVenue = function(venue)
	{
		this.clear();

		var camera = venue.camera;
		var movers = venue.movers;
		var compassPoints = venue.compassPoints;

		for (var i = 1; i < movers.length; i++)
		{
			var mover = movers[i];
			this.drawMoverForCamera(mover, camera);
		}

		for (var i = 0; i < compassPoints.length; i++)
		{
			var compassPoint = compassPoints[i];
			this.drawCompassPointForCamera(compassPoint, camera);
		}

		this.drawTextAtPos("Time:" + Globals.Instance.secondsSoFar() + "s", new Coords(10, 10));
		this.drawTextAtPos("Kills:" + venue.killsSoFar, new Coords(10, 20));
		this.drawTextAtPos("Deaths:" + venue.deathsSoFar, new Coords(10, 30));
	}

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

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

		document.body.appendChild(canvas);
	}

	Display.prototype.transformWorldPosToViewPos = function(drawPos, camera)
	{
		var cameraLoc = camera.loc;
		var cameraOrientation = cameraLoc.orientation;

		drawPos.subtract
		(
			cameraLoc.pos
		).overwriteWithXYZ
		(
			drawPos.dotProduct(cameraOrientation.right),
			drawPos.dotProduct(cameraOrientation.down),
			drawPos.dotProduct(cameraOrientation.forward)
		)

		var distanceAlongCameraForward = drawPos.z;

		drawPos.multiplyScalar
		(
			camera.focalLength
		).divideScalar
		(
			distanceAlongCameraForward
		).add
		(
			this.sizeInPixelsHalf
		);

		drawPos.z = distanceAlongCameraForward;
	}
}

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

	Globals.Instance = new Globals();

	// methods

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

		this.inputHelper = new InputHelper();

		this.display.initialize();

		this.timerTicksSoFar = 0;
		this.timerTicksPerSecond = timerTicksPerSecond;
		var millisecondsPerTimerTick = Math.round(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);

		this.inputHelper.initialize();
	}

	Globals.prototype.secondsSoFar = function()
	{
		return Math.floor(this.timerTicksSoFar / this.timerTicksPerSecond);
	}

	// events

	Globals.prototype.handleEventTimerTick = function()
	{
		this.venue.updateForTimerTick();
		this.timerTicksSoFar++;
	}
}

function InputHelper()
{
	// do nothing
}
{
	InputHelper.prototype.initialize = function()
	{
		this.keyCodesPressed = [];

		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	// events

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var keyCode = "_" + event.keyCode;
		if (this.keyCodesPressed[keyCode] == null)
		{
			this.keyCodesPressed.push(keyCode);
			this.keyCodesPressed[keyCode] = keyCode;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		var keyCode = "_" + event.keyCode;
		delete this.keyCodesPressed[keyCode];
		this.keyCodesPressed.splice
		(
			this.keyCodesPressed.indexOf(keyCode), 1
		);
	}

}

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

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

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

function Mesh(vertices, faces)
{
	this.vertices = vertices;
	this.faces = faces;

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

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

	this.plane = new Plane(new Coords(), 0);
}
{
	MeshFace.prototype.recalculateForMesh = function(mesh)
	{
		var vertex0 = mesh.vertices[this.vertexIndices[0]];
		var vertex1 = mesh.vertices[this.vertexIndices[1]];
		var vertex2 = mesh.vertices[this.vertexIndices[2]];

		var edge0 = vertex1.clone().subtract(vertex0);
		var edge1 = vertex2.clone().subtract(vertex1);

		this.plane.normal.overwriteWith(edge0).crossProduct(edge1);
		this.plane.distanceFromOrigin = this.plane.normal.dotProduct(vertex0);
	}
}

function Mover
(
	name, integrity, speedMax, accelPerTick, turnPerTick, loc, activity, color, mesh
)
{
	this.name = name;
	this.integrity = integrity;
	this.speedMax = speedMax;
	this.accelPerTick = accelPerTick;
	this.turnPerTick = turnPerTick;
	this.loc = loc;
	this.activity = activity;
	this.color = color;
	this.mesh = mesh;

	this.displacementFromPlayer = new Coords();
}
{
	Mover.prototype.updateForVenueTimerTick = function(venue)
	{
		this.activity.performForActor(this);

		var loc = this.loc;
		loc.vel.add(loc.accel);
		loc.accel.clear();
		loc.vel.trimToMagnitudeMax(this.speedMax);
		loc.pos.add(loc.vel);
		loc.pos.trimToRangeMinMax
		(
			venue.sizeInPixelsHalfNegative, 
			venue.sizeInPixelsHalf
		);

		var moverForPlayer = venue.moverForPlayer;

		if (this != moverForPlayer)
		{
			this.displacementFromPlayer.overwriteWith
			(
				loc.pos
			).subtract
			(
				moverForPlayer.loc.pos
			);

			var distanceFromPlayer = this.displacementFromPlayer.magnitude();
			var distanceMinForKill = 10;
			if (distanceFromPlayer <= distanceMinForKill)
			{
				if (this.name == "Prey")
				{
					venue.killsSoFar++;
				}
				else if (this.name == "Predator")
				{
					venue.deathsSoFar++;
				}

				loc.pos.randomize().multiply
				(
					venue.sizeInPixels
				).subtract
				(
					venue.sizeInPixelsHalf
				).divideScalar
				(
					1000
				).add
				(
					moverForPlayer.loc.pos
				);
			}
		}
	}
}

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

	this.axes = 
	[
		this.forward,
		this.right,
		this.down
	];
}
{
	// static methods

	Orientation.fromForwardAndDown = function(forward, down)
	{
		return new Orientation
		(
			forward, 
			new Coords(0, 0, 0), 
			down
		).orthogonalizeAxes();
	}

	// instance methods

	Orientation.prototype.normalizeAxes = function()
	{
		this.forward.normalize();
		this.right.normalize();
		this.down.normalize();
		return this;
	}

	Orientation.prototype.orthogonalizeAxes = function()
	{
		this.right.overwriteWith
		(
			this.down
		).crossProduct
		(
			this.forward
		);

		this.down.overwriteWith
		(
			this.forward
		).crossProduct
		(
			this.right
		);

		this.normalizeAxes();

		return this;
	}

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

	Orientation.prototype.toString = function()
	{
		var returnValue = 
			this.forward.toString() 
			+ "x" + this.right.toString()
			+ "x" + this.down.toString();

		return returnValue;
	}
}

function Transform_Orient(orientation)
{
	this.orientation = orientation;

	this.orientationTemp = new Orientation(new Coords(), new Coords(), new Coords());
	this.result = new Coords();
}
{
	Transform_Orient.prototype.applyToCoords = function(coordsToTransform)
	{
		this.orientationTemp.overwriteWith(this.orientation);
		this.result.clear().add
		(
			this.orientationTemp.forward.multiplyScalar(coordsToTransform.z)
		).add
		(
			this.orientationTemp.right.multiplyScalar(coordsToTransform.x)
		).add
		(
			this.orientationTemp.down.multiplyScalar(coordsToTransform.y)
		);
		coordsToTransform.overwriteWith(this.result);
	}
}

function Venue(name, sizeInPixels, movers)
{
	this.name = name;
	this.sizeInPixels = sizeInPixels;
	this.movers = movers;

	this.sizeInPixelsHalf = this.sizeInPixels.clone().divideScalar(2);
	this.sizeInPixelsHalfNegative = this.sizeInPixelsHalf.clone().multiplyScalar(-1);

	this.moverForPlayer = this.movers[0];

	this.camera = new Camera
	(
		300, // focalLength
		this.moverForPlayer.loc
	);

	var directionNamesAndOffsets = 
	[
		[ "West", new Coords(-1, 0, 0) ],
		[ "East", new Coords(1, 0, 0) ],
		[ "North", new Coords(0, -1, 0) ],
		[ "South", new Coords(0, 1, 0) ],
		[ "Up", new Coords(0, 0, -1) ],
		[ "Down", new Coords(0, 0, 1) ],
	];

	this.compassPoints = [];

	for (var i = 0; i < directionNamesAndOffsets.length; i++)
	{
		var directionNameAndOffset = directionNamesAndOffsets[i];
		var directionName = directionNameAndOffset[0];
		var directionOffset = directionNameAndOffset[1];
		var compassPoint = new CompassPoint(directionName, directionOffset); 
		this.compassPoints.push(compassPoint);
	}

	this.killsSoFar = 0;
	this.deathsSoFar = 0;
}
{
	Venue.prototype.updateForTimerTick = function()
	{
		for (var i = 0; i < this.movers.length; i++)
		{
			var mover = this.movers[i];
			mover.updateForVenueTimerTick(this);
		}		

		for (var i = 0; i < this.compassPoints.length; i++)
		{
			var compassPoint = this.compassPoints[i];
			compassPoint.updateForVenueTimerTick(this);
		}

		Globals.Instance.display.drawVenue(this);
	}
}

// run

main();

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

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