A Simulation of Pest Eradication in JavaScript

The JavaScript code below, when run, simulates a ecological pest-eradication scenario. An exterminator chooses a target at random, travels over a network of links and nodes to get as close as possible to the target, moves off the network towards the target itself, and removes it. The exterminator then chooses another target and random and continues.

The simulation was originally intended to test the feasibility of removing invasive or otherwise unwanted species such as poison ivy or fire ants. It is anticipated that the simulation will need a significant number of enhancements to effectively model such scenarios, including but not limited to: non-random selection of the next target, initial concealment of targets that must somehow be detected, propagation of targets over time, permanent “reservoirs” from which targets cannot be removed, and accounting for the time and resource costs of travel and eradication.

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

// main

function main()
{
	var map = Map.random
	(
		new Coords(300, 300), // size
		new Coords(10, 10), // margin
		200 // numberOfNodes
	);

	var world = World.random
	(
		map, 
		1, // numberOfAgents
		100 // numberOfTargets
	);

	var display = new Display(map.size);

	Globals.Instance.initialize(display, world);
}

// extensions

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

// classes

function Agent(id, pos)
{
	this.id = id;
	this.pos = pos;

	this.target = null;
	this.route = new Route([]);

	this.speed = 1;
}
{
	Agent.prototype.act = function(world)
	{
		var map = world.map;
		var mapNodes = map.nodes;
		var routeNodeIDs = this.route.nodeIDs;
		if (this.target == null)
		{
			var nodeStart = map.nodeNearestToPos(this.pos);
			var targetIndex = Math.floor
			(
				Math.random() * world.targets.length
			);
			this.target = world.targets[targetIndex];
			var nodeToTarget = map.nodeNearestToPos(this.target.pos);
			this.route.fromNodeIDToNodeID
			(
				map, nodeStart.id, nodeToTarget.id
			);
		}
		else
		{
			var targetPos;
			if (routeNodeIDs.length == 0)
			{
				targetPos = this.target.pos;
			}
			else
			{
				var idOfNodeNext = routeNodeIDs[0];
				var nodeNext = mapNodes[idOfNodeNext];
				targetPos = nodeNext.pos;
			}

			var displacementToTarget = targetPos.clone().subtract
			(
				this.pos
			);
			var distanceToTarget = displacementToTarget.magnitude();
			if (distanceToTarget < this.speed)
			{
				this.pos.overwriteWith(targetPos);
				if (routeNodeIDs.length == 0)
				{
					var targetIndex = world.targets.indexOf(this.target);
					world.targets.splice(targetIndex, 1);
					delete world.targets[this.target.id];
					this.target = null;
				}
				else
				{
					routeNodeIDs.splice(0, 1);
				}
			}
			else
			{
				var movement = displacementToTarget.divideScalar
				(
					distanceToTarget
				).multiplyScalar
				(
					this.speed	
				);
	
				this.pos.add(movement);
			}
		}
	}
}

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.clone = function()
	{
		return new Coords(this.x, this.y);
	}

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

	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.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}
}

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

	Display.prototype.colorBack = function(value)
	{
		this.graphics.fillStyle = value;
	}

	Display.prototype.colorFore = function(value)
	{
		this.graphics.strokeStyle = value;
	}

	Display.prototype.drawCircle = function(center, radius)
	{
		this.graphics.beginPath();
		this.graphics.arc
		(
			center.x, center.y,
			radius,
			0, Math.PI * 2 // start and stop angles
		);
		this.graphics.stroke();
	}

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

	Display.prototype.drawRectangle = function(center, size)
	{
		this.graphics.strokeRect
		(
			center.x - size.x / 2, center.y - size.y / 2, 
			size.x, size.y
		);
	}

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

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

		this.colorFore("Gray");
		this.colorBack("White");

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

		return this;
	}
}

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

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

		var timerTicksPerSecond = 10;
		var millisecondsPerTimerTick = Math.floor
		(
			1000 / timerTicksPerSecond
		);
		this.timer = setInterval
		(
			this.updateForTimerTick.bind(this), 
			millisecondsPerTimerTick
		);
	}

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

		this.world.updateForTimerTick();
	}
}

function IDHelper()
{
	this._idNext = 0;
}
{
	IDHelper.idNext = function()
	{
		var returnValue = "_" + this._idNext;
		this._idNext++;
		return returnValue;
	}
}

function Map(size, nodes, links)
{
	this.size = size;
	this.nodes = nodes.addLookups("id");
	this.links = links.addLookups("id");

	for (var i = 0; i < this.links.length; i++)
	{
		var link = this.links[i];
		var linkNodeIDs = link.nodeIDs;

		var nodeID0 = linkNodeIDs[0];
		var nodeID1 = linkNodeIDs[1];

		var node0 = this.nodes[nodeID0];
		var node1 = this.nodes[nodeID1];

		var linkID = link.id;
		node0.linkIDs.push(linkID);
		node1.linkIDs.push(linkID);

		node0.neighborIDs.push(nodeID1);
		node1.neighborIDs.push(nodeID0);
	}
}
{
	Map.random = function(size, margin, numberOfNodes)
	{
		var sizeMinusMargins = 
			size.clone().subtract(margin).subtract(margin);

		var nodes = [];
		for (var i = 0; i < numberOfNodes; i++)
		{
			var pos = new Coords().random().multiply
			(
				sizeMinusMargins
			).add
			(
				margin
			);
			var nodeID = "_" + i;
			var node = new MapNode(nodeID, pos);
			nodes.push(node);
		}

		var links = [];
		var nodesNotYetLinked = nodes.slice(1);
		var nodesLinked = [nodes[0]];
		var displacement = new Coords();
		while (nodesNotYetLinked.length > 0)
		{
			var distanceMinSoFar = Number.POSITIVE_INFINITY;
			var nodeNearestSoFar = null;

			for (var i = 0; i < nodesLinked.length; i++)
			{
				var nodeI = nodesLinked[i];
				var nodeIPos = nodeI.pos;

				for (var j = 0; j < nodesNotYetLinked.length; j++)
				{				
					var nodeJ = nodesNotYetLinked[j];

					if (nodeJ != nodeI)
					{
						var nodeJPos = nodeJ.pos;

						displacement.overwriteWith
						(
							nodeJPos
						).subtract
						(
							nodeIPos
						);

						var distance = displacement.magnitude();
						if (distance < distanceMinSoFar)
						{
							distanceMinSoFar = distance;
							nodesNearestSoFar = [nodeI, nodeJ];
						}
					}					
				}
			}

			var nodeToLinkFrom = nodesNearestSoFar[0];
			var nodeToLinkTo = nodesNearestSoFar[1];
			nodesNotYetLinked.splice
			(
				nodesNotYetLinked.indexOf(nodeToLinkTo), 1
			);
			nodesLinked.push(nodeToLinkTo);

			var idsOfNodesToLink = 
			[
				nodeToLinkFrom.id,
				nodeToLinkTo.id
			];
			var linkID = "_" + links.length;
			var link = new MapLink(linkID, idsOfNodesToLink);
			links.push(link);
		}

		var returnValue = new Map(size, nodes, links);
		return returnValue;
	}

	// instance methods

	Map.prototype.drawToDisplay = function(display)
	{
		display.colorFore("Gray");

		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			var nodes = link.nodes(this);
			var node0Pos = nodes[0].pos;
			var node1Pos = nodes[1].pos;
			display.drawLine(node0Pos, node1Pos);
		}

		var nodeSize = new Coords(2, 2);

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			var nodePos = node.pos;
			display.drawRectangle(nodePos, nodeSize);
		}
	}

	Map.prototype.nodeNearestToPos = function(posToCheck)
	{
		var node = this.nodes[0];
		var displacement = node.pos.clone().subtract
		(
			posToCheck
		);

		var nodeNearestSoFar = node;
		var distanceNearestSoFar = displacement.magnitude();

		for (var i = 1; i < this.nodes.length; i++)
		{
			node = this.nodes[i];
			var nodeDistance = displacement.overwriteWith
			(
				node.pos
			).subtract
			(
				posToCheck
			).magnitude();

			if (nodeDistance < distanceNearestSoFar)
			{
				distanceNearestSoFar = nodeDistance;
				nodeNearestSoFar = node;
			}
		}

		return nodeNearestSoFar;
	}
}

function MapLink(id, nodeIDs)
{
	this.id = id;
	this.nodeIDs = nodeIDs;
}
{
	MapLink.prototype.nodes = function(map)
	{
		var returnValues = [];
		for (var i = 0; i < this.nodeIDs.length; i++)
		{
			var nodeID = this.nodeIDs[i];
			var node = map.nodes[nodeID];
			returnValues.push(node);
		}
		return returnValues;
	}
}

function MapNode(id, pos)
{
	this.id = id;
	this.pos = pos;

	this.linkIDs = [];
	this.neighborIDs = [];
}

function Route(nodeIDs)
{
	this.nodeIDs = nodeIDs;
}
{
	Route.prototype.fromNodeIDToNodeID = function(map, fromNodeID, toNodeID)
	{
		var mapNodes = map.nodes;

		var fromNode = mapNodes[fromNodeID];
		var toNode = mapNodes[toNodeID];
		var displacementToGoal = toNode.pos.clone().subtract
		(
			fromNode
		);

		var nodeIDsToConsider = [];
		var nodeIDsConsidered = [];

		nodeIDsToConsider.push(fromNodeID);

		var nodeIDToDistanceLookup = [];
		var nodeIDToPredecessorLookup = [];

		while (nodeIDsToConsider.length > 0)
		{
			var nodeIDToConsider = nodeIDsToConsider[0];

			nodeIDsToConsider.splice(0, 1);
			nodeIDsConsidered.splice(0, 0, nodeIDToConsider);

			var nodeToConsider = mapNodes[nodeIDToConsider];
			var neighborIDs = nodeToConsider.neighborIDs;
			for (var n = 0; n < neighborIDs.length; n++)
			{
				var neighborID = neighborIDs[n];

				if (nodeIDsConsidered.indexOf(neighborID) == -1)
				{
					if (nodeIDsToConsider.indexOf(neighborID) == -1)
					{
						nodeIDToPredecessorLookup[neighborID] = nodeIDToConsider;

						var neighbor = mapNodes[neighborID];

						var neighborDistance = displacementToGoal.overwriteWith
						(
							toNode.pos
						).subtract
						(
							neighbor.pos 
						).magnitude();

						if (neighborDistance == 0)
						{
							this.nodeIDs.length = 0;
							var nodeIDCurrent = neighborID;
							while (nodeIDCurrent != null)
							{
								this.nodeIDs.splice(0, 0, nodeIDCurrent);
								nodeIDCurrent = nodeIDToPredecessorLookup[nodeIDCurrent];
							}

							nodeIDsToConsider.length = 0;
							break;
						}

						nodeIDToDistanceLookup[neighborID] = neighborDistance;
						nodeIDToPredecessorLookup[neighborID] = nodeIDToConsider;

						var i;
						for (i = 0; i < nodeIDsToConsider.length; i++)
						{
							var nodeSortedID = nodeIDsToConsider[i];
							var nodeSortedDistance = nodeIDToDistanceLookup[nodeSortedID];
							if (neighborDistance < nodeSortedDistance)
							{
								break;
							}
						}
						nodeIDsToConsider.splice(i, 0, neighborID);
					}
				}
			}
		}

		return this;
	}

	// drawable

	Route.prototype.drawToDisplayForMapAndAgent = function(display, map, agent)
	{
		var nodePosPrev = agent.pos;
		display.colorFore("Black");

		for (var i = 0; i < this.nodeIDs.length; i++)
		{
			var nodeID = this.nodeIDs[i];
			var node = map.nodes[nodeID];
			var nodePos = node.pos;
			display.drawLine(nodePosPrev, nodePos);

			nodePosPrev = nodePos;
		}
	}
}

function Target(id, pos)
{
	this.id = id;
	this.pos = pos;
}

function World(map, agents, targets)
{
	this.map = map;
	this.agents = agents.addLookups("id");
	this.targets = targets.addLookups("id");
}
{
	World.random = function(map, numberOfAgents, numberOfTargets)
	{
		var mapNodes = map.nodes;

		var agents = [];

		for (var i = 0; i < numberOfAgents; i++)
		{
			var nodeStart = mapNodes[i];
			var nodeEnd = mapNodes[mapNodes.length - 1 - i];
			var agent = new Agent
			(
				"_" + i, 
				nodeStart.pos.clone()
			);
			agents.push(agent);
		}

		var targets = [];

		for (var i = 0; i < numberOfTargets; i++)
		{
			var pos = new Coords().random().multiply(map.size);
			var targetID = IDHelper.idNext();
			var target = new Target(targetID, pos);
			targets.push(target);
		}

		var returnValue = new World
		(
			map, 
			agents,
			targets
		);

		return returnValue;
	}

	// instance methods

	World.prototype.drawToDisplay = function(display)
	{
		display.colorFore("Gray");
		var targetSize = new Coords(3, 3);

		for (var i = 0; i < this.targets.length; i++)
		{
			var target = this.targets[i];
			display.drawRectangle(target.pos, targetSize);
		}

		this.map.drawToDisplay(display);

		var agentRadius = 5;
		for (var i = 0; i < this.agents.length; i++)
		{
			var agent = this.agents[i];
			display.drawCircle(agent.pos, agentRadius);
			
			var route = agent.route;
			route.drawToDisplayForMapAndAgent(display, this.map, agent);
		}
	}

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

// run

main();

</script>
</body>
</html>
Advertisements
Posted in Uncategorized | Tagged , , , , , | Leave a comment

A Simple Data Transcoder in JavaScript

The JavaScript program below converts data encoded in UTF8, Base64, or hexadecimal format into one of the other encodings. To see the code in action, copy it into a .html file and open that file in a text encoder that runs JavaScript. Or, for an online version, visit “https://thiscouldbebetter.neocities.org/datatranscoder.html”.

DataTranscoder.png


<html>
<body>

<div id="divUI">

	<label><b>Data Transcoder</b></label><br />

	<label>Data to Encode:</label><br />
	<textarea id="textareaDataToEncode" cols="40" rows="10"></textarea><br />

	<label>From:</label>
	<select id="selectEncodingFrom">
		<option>Base64</option>
		<option>Hexadecimal</option>
		<option selected>UTF8</option>
	</select>
	<label>To:</label>
	<select id="selectEncodingTo">
		<option selected>Base64</option>
		<option>Hexadecimal</option>
		<option>UTF8</option>
	</select><br />

	<button onclick="buttonEncode_Clicked();">v Encode v</button>
	<button onclick="buttonDecode_Clicked();">^ Decode ^</button><br />

	<label>Data Encoded:</label><br />
	<textarea id="textareaDataEncoded" cols="40" rows="10"></textarea><br />

</div>

<script type="text/javascript">

// ui event handlers

function buttonDecode_Clicked()
{
	var selectEncodingFrom = document.getElementById
	(
		"selectEncodingFrom"
	);

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

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

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

	var encodingNameFrom = selectEncodingFrom.value;
	var encodingFrom = Encoding.Instances[encodingNameFrom];

	var encodingNameTo = selectEncodingTo.value;
	var encodingTo = Encoding.Instances[encodingNameTo];

	var dataToEncode = textareaDataEncoded.value;
	var dataAsBytes = encodingTo.decodeToBytes(dataToEncode);
	var dataEncoded = encodingFrom.encodeBytes(dataAsBytes);

	textareaDataToEncode.value = dataEncoded;
}

function buttonEncode_Clicked()
{
	var selectEncodingFrom = document.getElementById
	(
		"selectEncodingFrom"
	);

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

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

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

	var encodingNameFrom = selectEncodingFrom.value;
	var encodingFrom = Encoding.Instances[encodingNameFrom];

	var encodingNameTo = selectEncodingTo.value;
	var encodingTo = Encoding.Instances[encodingNameTo];

	var dataToEncode = textareaDataToEncode.value;
	var dataAsBytes = encodingFrom.decodeToBytes(dataToEncode);
	var dataEncoded = encodingTo.encodeBytes(dataAsBytes);

	textareaDataEncoded.value = dataEncoded;
}

// classes

function Encoding(name, decodeToBytes, encodeBytes)
{
	this.name = name;
	this.decodeToBytes = decodeToBytes;
	this.encodeBytes = encodeBytes;
}
{
	Encoding.Instances = new Encoding_Instances();

	function Encoding_Instances()
	{
		this.Base64 = new Encoding
		(
			"Base64",
			function decodeToBytes(dataToDecode)
			{
				var dataAsBinaryString = atob(dataToDecode);
				var dataAsBytes = [];
				for (var i = 0; i < dataAsBinaryString.length; i++)
				{
					var byte = dataAsBinaryString.charCodeAt(i);
					dataAsBytes.push(byte);
				}
				return dataAsBytes;
			},
			function encodeBytes(bytesToEncode)
			{
				var dataAsBinaryString = "";
				for (var i = 0; i < bytesToEncode.length; i++)
				{
					var byte = bytesToEncode[i];
					var byteAsChar = String.fromCharCode(byte);
					dataAsBinaryString += byteAsChar;
				}
				var returnValue = btoa(dataAsBinaryString);
				return returnValue;
			}
		);

		this.Hexadecimal = new Encoding
		(
			"Hexadecimal",
			function decodeToBytes(dataToDecode)
			{
				var returnValues = []
				for (var i = 0; i < dataToDecode.length; i += 2)
				{
					var byteAsHexadecimal = dataToDecode.substr(i, 2);
					var byte = parseInt(byteAsHexadecimal, 16);
					returnValues.push(byte);
				}
				return returnValues;
			},
			function encodeBytes(bytesToEncode)
			{
				var returnValue = "";

				for (var i = 0; i < bytesToEncode.length; i++)
				{
					var byte = bytesToEncode[i];
					var byteAsString = byte.toString(16);
					returnValue += byteAsString;
				}

				return returnValue;
			}
		);

		this.UTF8 = new Encoding
		(
			"UTF8",
			function decodeToBytes(dataToDecode)
			{
				var returnValues = []
				for (var i = 0; i < dataToDecode.length; i++)
				{
					var byte = dataToDecode.charCodeAt(i);
					returnValues.push(byte);
				}
				return returnValues;

			},
			function encodeBytes(bytesToEncode)
			{
				var returnValue = "";

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

</script>

</body>
</html>

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

Exploring the SVG Vector Image File Format in JavaScript

The JavaScript program below accepts a vector-based “path” in JSON format and converts that path to the SVG format. The output can then be viewed in a program that understands the SVG format, such as most modern web browsers.

The program currently only supports very simple paths with straight edges. It is meant primarily as a basis for further work.

UPDATE 2017/09/05 – I have added support for curved edges, dynamic canvas sizing, and drawing of “handles”. I also modified the code to use absolute vertex positions rather than relative ones.

SVGCreator.png


<html>
<body>
 
<div id="divUI">
	<label>Path as JSON:</label>
	<button onclick="buttonNew_Clicked();">New</button>
	<button onclick="buttonDemo_Clicked();">Demo</button><br />
	<textarea id="textareaPathAsJSON" cols="60" rows="30">
	</textarea><br />
	<button onclick="buttonDraw_Clicked();">Draw Path</button>
	<input id="checkboxDrawHandles" type="checkbox">Draw Handles</input><br />
	<div id="divDisplay"></div>
	<button onclick="buttonConvert_Clicked();">Convert JSON to SVG</button><br />
	<label>Path as SVG:</label><br />
	<textarea id="textareaPathAsSVG" cols="60" rows="10"></textarea><br />
	 
</div>
 
<script type="text/javascript">
 
// ui event handlers
 
function buttonConvert_Clicked()
{
	var textareaPathAsJSON = document.getElementById("textareaPathAsJSON");
	var pathAsStringJSON = textareaPathAsJSON.value;
	try
	{
		var path = Path.fromStringJSON(pathAsStringJSON);
		var pathAsStringSVG = path.toStringSVG();
		var textareaPathAsSVG = document.getElementById("textareaPathAsSVG");
		textareaPathAsSVG.value = pathAsStringSVG;
	}
	catch (ex)
	{
		alert("Invalid JSON!");
	}
}
 
function buttonDemo_Clicked()
{
	var path = Path.demo();
	var pathAsStringJSON = path.toStringJSON();
	var textareaPathAsJSON = document.getElementById("textareaPathAsJSON");
	textareaPathAsJSON.value = pathAsStringJSON;	
}

function buttonDraw_Clicked()
{
	var textareaPathAsJSON = document.getElementById("textareaPathAsJSON");
	var pathAsStringJSON = textareaPathAsJSON.value;
	var checkboxDrawHandles = document.getElementById("checkboxDrawHandles");
	var showHandles = checkboxDrawHandles.checked;

	try
	{
		var path = Path.fromStringJSON(pathAsStringJSON);
		var pathBounds = 
			Bounds.new().ofPoints(path.vertices.select("pos"));
		var displaySize = pathBounds.max.add(pathBounds.min);
		var display = new Display(displaySize);
		display.initialize();
		display.drawPath(path);
		if (showHandles == true)
		{
			display.drawPathHandles(path);
		}
	}
	catch (ex)
	{
		alert("Invalid JSON!");
	}
}

function buttonNew_Clicked()
{
	var path = new Path([], false, null, "#000000");
	var pathAsStringJSON = path.toStringJSON();
	var textareaPathAsJSON = document.getElementById("textareaPathAsJSON");
	textareaPathAsJSON.value = pathAsStringJSON;	
}
 
// classes

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

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

		return returnValues;
	}
}

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
	this._size = new Coords();
}
{
	Bounds.new = function()
	{
		return new Bounds(new Coords(), new Coords());
	}

	Bounds.prototype.ofPoints = function(points)
	{
		var point = points[0];
		this.min.overwriteWith(point);
		this.max.overwriteWith(point);
		for (var i = 0; i < points.length; i++)
		{
			point = points[i];
			if (point.x < this.min.x)
			{
				this.min.x = point.x;
			}
			if (point.x > this.max.y)
			{
				this.max.x = point.x;
			}
			if (point.y < this.min.y)
			{
				this.min.y = point.y;
			}
			if (point.y > this.max.y)
			{
				this.max.y = point.y;
			}
		}

		return this;
	}

	Bounds.prototype.size = function()
	{
		return this._size.overwriteWith(this.max).subtract(this.min);
	}
}
 
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.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	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(size)
{
	this.size = size;
}
{
	Display.prototype.drawPath = function(path, showHandles)
	{
		this.graphics.beginPath();

		var vertices = path.vertices;

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

		for (var v = 0; v < vertices.length; v++)
		{
			vertex = vertices[v];
			var vertexPos = vertex.pos;
			var vertexCurveControlPoints = vertex.curveControlPoints;

			var vNext = (v >= vertices.length - 1 ? 0 : v + 1); 
			var vertexNext = vertices[vNext];

			var vertexNextPos = vertexNext.pos;

			if (vertexCurveControlPoints == null)
			{				
				this.graphics.lineTo
				(
					vertexNextPos.x,
					vertexNextPos.y
				);
			}
			else
			{
				var curveControlPoint0 = vertexCurveControlPoints[0];
				var curveControlPoint1 = vertexCurveControlPoints[1];				

				this.graphics.bezierCurveTo
				(
					curveControlPoint0.x,
					curveControlPoint0.y,
					curveControlPoint1.x,
					curveControlPoint1.y,
					vertexNextPos.x,
					vertexNextPos.y
				);
			}
		}

		if (path.isClosed == true)
		{
			this.graphics.closePath();
		}

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

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

	Display.prototype.drawPathHandles = function(path)
	{
		var handleSizeHalf = 3;
		var handleSize = handleSizeHalf * 2;

		var vertices = path.vertices;

		for (var v = 0; v < vertices.length; v++)
		{
			var vertex = vertices[v];
			var vertexPos = vertex.pos;
			var vertexCurveControlPoints = vertex.curveControlPoints;

			var vNext = (v >= vertices.length - 1 ? 0 : v + 1); 
			var vertexNext = vertices[vNext];
			var vertexNextPos = vertexNext.pos;

			this.graphics.strokeStyle = "Gray";
			this.graphics.strokeRect
			(
				vertexPos.x - handleSizeHalf,
				vertexPos.y - handleSizeHalf,
				handleSize,
				handleSize
			);

			if (vertexCurveControlPoints != null)
			{
				var curveControlPoint0 = vertexCurveControlPoints[0];
				var curveControlPoint1 = vertexCurveControlPoints[1];
				
				this.graphics.beginPath();
				this.graphics.arc
				(
					curveControlPoint0.x, curveControlPoint0.y, 
					handleSizeHalf,
					0, 2 * Math.PI
				);
				this.graphics.stroke();

				this.graphics.beginPath();
				this.graphics.moveTo(vertexPos.x, vertexPos.y);
				this.graphics.lineTo(curveControlPoint0.x, curveControlPoint0.y);
				this.graphics.stroke();

				this.graphics.beginPath();
				this.graphics.arc
				(
					curveControlPoint1.x, curveControlPoint1.y, 
					handleSizeHalf,
					0, 2 * Math.PI
				);
				this.graphics.stroke();

				this.graphics.beginPath();
				this.graphics.moveTo(vertexNextPos.x, vertexNextPos.y);
				this.graphics.lineTo(curveControlPoint1.x, curveControlPoint1.y);
				this.graphics.stroke();
			}
		}
	}
 
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
 
		var divDisplay = document.getElementById("divDisplay");
		divDisplay.innerHTML = "";
		divDisplay.appendChild(canvas);
 
		this.graphics = canvas.getContext("2d");
	}
}
 
function Path(vertices, isClosed, colorFill, colorBorder)
{
	this.vertices = vertices;
	this.isClosed = isClosed;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;
}
{
	// static methods

	Path.demo = function()
	{
		var path = new Path
		(
			[ 
				new PathVertex( new Coords(50, 25) ),
				new PathVertex
				( 
					new Coords(75, 75), // pos
					// curveControlPoints
					[
						new Coords(60, 60),
						new Coords(40, 60),
					]
				),
				new PathVertex( new Coords(25, 75) ),
			],
			true, // isClosed
			"#00ffff", // colorFill
			"#000000" // colorBorder
		);

		return path;
	}

	// instance methods

	// serializable
 
	Path.fromStringJSON = function(pathAsStringJSON)
	{
		var path = JSON.parse(pathAsStringJSON);
		path.__proto__ = Path.prototype;
		var vertices = path.vertices;
		for (var i = 0; i < vertices.length; i++)
		{
			var vertex = vertices[i];
			vertex.pos.__proto__ = Coords.prototype;
			var curveControlPoints = vertex.curveControlPoints;
			if (curveControlPoints != null)
			{
				for (var ccp = 0; ccp < 2; ccp++)
				{
					var curveControlPoint = curveControlPoints[ccp];
					curveControlPoint.__proto__ = Coords.prototype;
				}
			}
			vertex.__proto__ = PathVertex.prototype;
		}
		return path;
	}
 
	Path.prototype.toStringJSON = function()
	{
		var returnValue = JSON.stringify(this, null, 4);
		return returnValue;
	}
 
	// svg
 
	Path.prototype.toStringSVG = function()
	{
		var pathAsSVG = "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n";
 
		pathAsSVG += "<svg xmlns='http://www.w3.org/2000/svg'>\n";
		pathAsSVG += "\t<g>\n";
		pathAsSVG += "\t\t<path \n";
		pathAsSVG += "\t\t\tstyle='"
		if (this.colorBorder != null)
		{
			pathAsSVG += "stroke:" + this.colorBorder + ";";
		}
		if (this.colorFill != null)
		{
			pathAsSVG += "fill:" + this.colorFill + ";";
		}
		pathAsSVG += "' \n";
		pathAsSVG += "\t\t\td='M "
		for (var v = 0; v < this.vertices.length; v++)
		{
			var vertex = this.vertices[v];
			var vertexPos = vertex.pos;
			var curveControlPoints = vertex.curveControlPoints;
			var vertexAsString = 
				vertexPos.x + "," + vertexPos.y + " ";

			if (curveControlPoints != null)
			{
				vertexAsString += "C ";

				for (var ccp = 0; ccp < 2; ccp++)
				{
					var curveControlPoint = curveControlPoints[ccp];
					vertexAsString += 
						curveControlPoint.x + "," 
						+ curveControlPoint.y + " ";
				}
			}

			pathAsSVG += vertexAsString;
 		}
		if (this.isClosed == true)
		{
			pathAsSVG += "Z";
		}
		pathAsSVG += "' \n";
		pathAsSVG += "\t\t/>\n";
		pathAsSVG += "\t</g>\n";
		pathAsSVG += "</svg>";
 
		return pathAsSVG;
	} 
}

function PathVertex(pos, curveControlPoints)
{
	this.pos = pos;
	this.curveControlPoints = curveControlPoints;
}
 
</script>
 
</body>
</html>

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

Drawing Ellipses (Ovals) to an HTML5 Canvas with JavaScript

The JavaScript code below, when run, prompts the user to input information about an ellipse (oval), and then draws that ellipse to the screen. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2017/08/21 (LATER) – I have modified the code significantly, to correct the inaccurate usage of “major axis”, when what it really meant was “semimajor” axis, and the same for the minor axis. In fact, I just replaced any references to “major” and “minor” with “width” and “height”, since before the user could easily specify a minor axis longer than the major one. The program also now accepts rotation in radians rather than in turns, since that is more standard. Finally, the same ellipse-drawing code is now called from both the original “fillEllipse” function and the new “strokeEllipse” function.

DrawOval.png


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

	<label><b>Ellipse Renderer</b></label><br />
 
	<label>Canvas Size:</label> 
	<input id="inputCanvasSizeX" type="number" value="100"></input>
	<label>x</label>
	<input id="inputCanvasSizeY" type="number" value="100"></input>
	<br />
 
	<label>Center Position:</label> 
	<input id="inputCenterX" type="number" value="50"></input>
	<label>x</label>
	<input id="inputCenterY" type="number" value="50"></input>
	<br />
 
	<label>Ellipse Size:</label>
	<input id="inputEllipseSizeX" type="number" value="60"></input>
	<label>x</label>
	<input id="inputEllipseSizeY" type="number" value="40"></input>
	<br />

	<label>Rotation in Radians:</label>
	<input id="inputRotationInRadians" type="number" value=".3"></input>
	<br />

	<button onclick="buttonDrawEllipse_Clicked();">Draw Ellipse</button>

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

</div>

<script type="text/javascript">

function buttonDrawEllipse_Clicked()
{ 
	var inputNames = 
	[
		"CanvasSizeX",
		"CanvasSizeY",
		"CenterX",
		"CenterY",
		"EllipseSizeX",
		"EllipseSizeY",
		"RotationInRadians"
	];
	
	var inputLookup = [];
	
	for (var i = 0; i < inputNames.length; i++)
	{
		var inputName = inputNames[i];
		var inputID = "input" + inputName;
		var input = document.getElementById(inputID);
		var inputValueAsFloat = parseFloat(input.value);
		inputLookup[inputName] = inputValueAsFloat;
	}

	var canvas = document.createElement("canvas");
	canvas.width = inputLookup["CanvasSizeX"];
	canvas.height = inputLookup["CanvasSizeY"];
	var divOutput = document.getElementById("divOutput");
	divOutput.innerHTML = "";
	divOutput.appendChild(canvas);
 
	var graphics = canvas.getContext("2d");	

	graphics.strokeRect(0, 0, canvas.width, canvas.height);

	var centerX = inputLookup["CenterX"];
	var centerY = inputLookup["CenterY"];
	var width = inputLookup["EllipseSizeX"];
	var height = inputLookup["EllipseSizeY"];
	var rotationInRadians = inputLookup["RotationInRadians"];

	graphics.fillStyle = "Cyan"; 
	graphics.fillEllipse
	(
		centerX,
		centerY,
		width,
		height,
		rotationInRadians
	);

	graphics.strokeStyle = "Black"; 
	graphics.strokeEllipse
	(
		centerX,
		centerY,
		width,
		height,
		rotationInRadians
	);
 
}
 
function CanvasRenderingContext2DExtensions()
{
	// extension class
}
{
	// helpers

	CanvasRenderingContext2D.prototype.pathEllipse = function
	(
		centerX,
		centerY,
		width,
		height,
		rotationInRadians
	)
	{
		this.save();
	
		this.translate(centerX, centerY);

		this.rotate(rotationInRadians); 

		var ratioOfHeightToWidth = height / width;
		this.scale(1, ratioOfHeightToWidth);
			
		this.beginPath();
		this.arc
		(
			0, 0, // center
			width / 2, // "radius"
			0, Math.PI * 2.0 // start, stop angle
		);
		this.fill();
	
		this.restore();
	}

	// extensions

	CanvasRenderingContext2D.prototype.fillEllipse = function
	(
		centerX, centerY, width, height, rotationInRadians
	)
	{
		this.pathEllipse
		(
			centerX, centerY, width, height, rotationInRadians
		);
		
		this.fill();		
	}
	
	CanvasRenderingContext2D.prototype.strokeEllipse = function
	(
		centerX, centerY, width, height, rotationInRadians
	)
	{
		this.pathEllipse
		(
			centerX, centerY, width, height, rotationInRadians
		);
		
		this.stroke();		
	}
 
}
 
</script>
 
</body>
</html>

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

A Binary To Image File Transcoder In JavaScript

The JavaScript program below, when run, allows the user to load an arbitrary file as bytes, convert it to a PNG image, save the image as a file, reload that image file, convert it back to bytes, and re-save the bytes. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2017/08/17 – I have updated the program to encode the number of bytes (up to 16MiB) in the upper-left pixel, leaving the rest of the first line blank.


<html>
<body>

<div id="divUI">

	<p><b>Binary File to Image Transcoder:</b></p>


	<div>
		<label>Filename:</label>
		<input id="inputFileName" value="Data"></input>
	</div>

	<div>
		<label>Bytes:</label><br />
		<input type="file" onchange="inputFileAsBytes_Changed(this);"></input><br />
		<textarea id="textareaBytesAsHexadecimal" cols="32" rows="16"></textarea><br />
		<button onclick="buttonBytesSaveToFile_Clicked();">Save Bytes</button><br />
	</div>

	<button onclick="buttonBytesToImage_Clicked();">Bytes to Image</button>
	<button onclick="buttonImageToBytes_Clicked();">Image to Bytes</button>

	<div>
		<label>Image:</label><br />
		<input type="file" onchange="inputFileAsImage_Changed(this);"></input><br />
		<div id="divImageDisplay"></div><br />
		<button onclick="buttonImageSaveToFile_Clicked();">Save Image</button><br />
	</div>

</div>

<script type="text/javascript">

// ui event handlers

function bytesFromTextarea()
{
	var textareaBytesAsHexadecimal = 
		document.getElementById("textareaBytesAsHexadecimal");
	var bytesAsStringHexadecimal = textareaBytesAsHexadecimal.value;

	var bytes = [];

	var numberOfNibbles = bytesAsStringHexadecimal.length

	for (var b = 0; b < numberOfNibbles; b += 2)
	{
		var byteAsStringHexadecimal = bytesAsStringHexadecimal.substr(b, 2);
		var byte = parseInt(byteAsStringHexadecimal, 16);
		bytes.push(byte);
	}

	return bytes;
}

function buttonBytesSaveToFile_Clicked()
{
	var inputFileName = document.getElementById("inputFileName");
	var fileName = inputFileName.value + ".bin";

	var bytes = bytesFromTextarea();

	new FileHelper().saveBytesToFileWithName(bytes, fileName);
}

function buttonBytesToImage_Clicked()
{
	var bytes = bytesFromTextarea();

	var numberOfBytes = bytes.length;
	
	var bytesPerPixel = 3;

	var numberOfPixels = Math.ceil(numberOfBytes / bytesPerPixel);
	
	var imageWidthInPixels 
		= Math.ceil(Math.sqrt(numberOfPixels));
	var imageHeightInPixels 
		= Math.ceil(numberOfPixels / imageWidthInPixels);
	var imageSizeInPixels 
		= new Coords(imageWidthInPixels, imageHeightInPixels + 1);

	var canvas = document.createElement("canvas");
	canvas.width = imageSizeInPixels.x;
	canvas.height = imageSizeInPixels.y;

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

	var numberOfBytes = bytes.length;

	// Max file size is 2^24 B = 16 MiB.
	var numberOfBytesMax = 2 << 23.

	if (numberOfBytes > numberOfBytesMax)
	{
		alert("File must not be larger than " + numberOfBytesMax + " bytes.");
		return;
	}

	// Store the file's length in bytes in the upper-left pixel.

	var numberOfBytesAsRGB = 
	[
		(numberOfBytes >> 16) & 0xff,
		(numberOfBytes >> 8) & 0xff,
		(numberOfBytes) & 0xff
	];
	var pixelColorAsString = 
		"rgb(" + numberOfBytesAsRGB.join(",") + ")";

	graphics.fillStyle = pixelColorAsString;
	graphics.fillRect(0, 0, 1, 1);


	// Store 3 bytes per pixel as RGB components.

	var byteIndex = 0;

	for (var y = 1; y < imageSizeInPixels.y; y++)
	{
		for (var x = 0; x < imageSizeInPixels.x; x++)
		{
			var pixelColorRGB = [0, 0, 0];

			for (var b = 0; b < bytesPerPixel; b++)
			{
				if (byteIndex < numberOfBytes)
				{
					var byte = bytes[byteIndex];
					pixelColorRGB[b] = byte;
				}

				byteIndex++;
			}

			var pixelColorAsString = 
				"rgb(" + pixelColorRGB.join(",") + ")";

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

	var imageAsDataURL = canvas.toDataURL("image/png");
	var imgElement = document.createElement("img");
	imgElement.onload = function(event2)
	{
		var divImageDisplay = document.getElementById
		(
			"divImageDisplay"
		);
		divImageDisplay.innerHTML = "";
		divImageDisplay.appendChild(imgElement);
	}
	imgElement.src = imageAsDataURL;
}

function buttonImageSaveToFile_Clicked()
{
	var divImageDisplay = document.getElementById
	(
		"divImageDisplay"
	);
	var imgElement = divImageDisplay.getElementsByTagName("img")[0];

	if (imgElement == null)
	{
		alert("No image specified!");
	}

	var canvas = document.createElement("canvas");
	canvas.width = imgElement.width;
	canvas.height = imgElement.height;
	var graphics = canvas.getContext("2d");

	graphics.drawImage(imgElement, 0, 0);

	var inputFileName = document.getElementById("inputFileName");
	var fileName = inputFileName.value;

	new FileHelper().saveCanvasToFileWithName
	(
		canvas, 
		fileName + ".png"
	);
}

function buttonImageToBytes_Clicked()
{
	var divImageDisplay = document.getElementById("divImageDisplay");
	var imgElement = divImageDisplay.getElementsByTagName("img")[0];

	if (imgElement == null)
	{
		alert("No image specified!");
		return;
	}

	var imageSizeInPixels = new Coords
	(
		imgElement.width,
		imgElement.height
	);

	var canvas = document.createElement("canvas");
	canvas.width = imageSizeInPixels.x;
	canvas.height = imageSizeInPixels.y;

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

	graphics.drawImage(imgElement, 0, 0);

	var bytesPerPixel = 3;

	var bytes = [];

	var pixelColorsRGBA = graphics.getImageData
	(
		0, 0, imageSizeInPixels.x, imageSizeInPixels.y
	).data;

	// Decode the file's length in bytes from the upper-left pixel.

	var numberOfBytesAsRGB = pixelColorsRGBA.slice(0, 3);

	var numberOfBytes = 
		(numberOfBytesAsRGB[0] << 16)
		+ (numberOfBytesAsRGB[1] << 8)
		+ (numberOfBytesAsRGB[2]);

	var byteIndex = 0;

	for (var y = 1; y < imageSizeInPixels.y; y++)
	{
		for (var x = 0; x < imageSizeInPixels.x; x++)
		{
			var pixelIndex = 
				y * imageSizeInPixels.x 
				+ x;

			var pixelColorRGBA = pixelColorsRGBA.slice
			(
				pixelIndex * 4, 
				pixelIndex * 4 + 4
			);

			for (var b = 0; b < bytesPerPixel; b++)
			{
				var byte = pixelColorRGBA[b];

				bytes[byteIndex] = byte;

				byteIndex++;
			}
		}
	}

	bytes.length = numberOfBytes;

	var bytesAsHexadecimal = "";

	for (var b = 0; b < numberOfBytes; b++)
	{
		var byte = bytes[b];
		var byteAsHexadecimal = byte.toString(16);
		while (byteAsHexadecimal.length < 2)
		{
			byteAsHexadecimal = "0" + byteAsHexadecimal;
		}
		bytesAsHexadecimal += byteAsHexadecimal;
	}

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

	textareaBytesAsHexadecimal.value = bytesAsHexadecimal;

}

function inputFileAsBytes_Changed(inputFileAsBytes)
{
	var file = inputFileAsBytes.files[0];

	var fileReader = new FileReader();
	fileReader.onload = function(event)
	{
		var fileAsBinaryString = event.target.result;

		var numberOfBytes = fileAsBinaryString.length;

		var bytesAsHexadecimal = "";

		for (var b = 0; b < numberOfBytes; b++)
		{
			var byte = fileAsBinaryString.charCodeAt(b);
			var byteAsHexadecimal = byte.toString(16);
			while (byteAsHexadecimal.length < 2)
			{
				byteAsHexadecimal = "0" + byteAsHexadecimal;
			}
			bytesAsHexadecimal += byteAsHexadecimal;
		}

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

		textareaBytesAsHexadecimal.value = 
			bytesAsHexadecimal;
		
	}
	fileReader.readAsBinaryString(file);
}

function inputFileAsImage_Changed(inputFileAsImage)
{
	var file = inputFileAsImage.files[0];

	var fileReader = new FileReader();
	fileReader.onload = function(event)
	{
		var imageAsDataURL = event.target.result;
		var imgElement = document.createElement("img");
		imgElement.onload = function(event2)
		{
			var divImageDisplay = document.getElementById
			(
				"divImageDisplay"
			);
			divImageDisplay.innerHTML = "";
			divImageDisplay.appendChild(imgElement);
		}
		imgElement.src = imageAsDataURL;
	}
	fileReader.readAsDataURL(file);
}

// classes

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

function FileHelper()
{
	// do nothing
}
{
	FileHelper.prototype.saveBytesToFileWithName = function(bytesToWrite, fileNameToSaveAs)
	{
		var bytesToWriteAsArrayBuffer = new ArrayBuffer(bytesToWrite.length);
		var bytesToWriteAsUIntArray = new Uint8Array(bytesToWriteAsArrayBuffer);
		for (var i = 0; i < bytesToWrite.length; i++) 
		{
			bytesToWriteAsUIntArray[i] = bytesToWrite[i];
		}
  
		var bytesToWriteAsBlob = new Blob
		(
			[ bytesToWriteAsArrayBuffer ], 
			{ type:"application/type" }
		);
  
		var link = document.createElement("a");
		link.href = window.URL.createObjectURL(bytesToWriteAsBlob);
		link.download = fileNameToSaveAs;
		link.click();
	}

	FileHelper.prototype.saveCanvasToFileWithName = function(canvas, fileNameToSaveAs)
	{
 		var imageFromCanvasURL = canvas.toDataURL("image/png");
 
		var imageAsByteString = atob(imageFromCanvasURL.split(',')[1]);
		var imageAsArrayBuffer = new ArrayBuffer(imageAsByteString.length);
		var imageAsArrayUnsigned = new Uint8Array(imageAsArrayBuffer);
		for (var i = 0; i < imageAsByteString.length; i++) 
		{
			imageAsArrayUnsigned[i] = imageAsByteString.charCodeAt(i);
		}
 
		var imageAsBlob = new Blob([imageAsArrayBuffer], {type:"image/png"});
 
		if (fileNameToSaveAs.toLowerCase().endsWith(".png") == false)
		{
			fileNameToSaveAs += ".png";
		}
 
		var link = document.createElement("a");
		link.href = window.URL.createObjectURL(imageAsBlob);
		link.download = fileNameToSaveAs;
		link.click();
	}

}

</script>

</body>
</html>

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

A Rudimentary JavaScript Minifier

The code below implements a rudimentary JavaScript minifier. A “minifier” takes a code listing and removes unnecessary space in it, primarily by renaming functions and variables using shorter names. Minifying JavaScript allows it to be transported and compiled more efficiently.

This implementation is fairly primitive. It’s unlikely to work for code that references more built-in functions and objects, or that declares more that about 52 variables or functions. Like many of the programs on this site, it is intended mostly as a basis for further work.

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


<html>
<body>

<div id="divUI">

	<label><b>JavaScript Minifier</b></label>
	
	<p>Enter JavaScript code in the textbox and click the button to minify it.</p>

	<label>Keywords to Ignore:</label><br />
	<textarea id="textareaKeywordsToIgnore" cols="80" rows="8">
alert
function
var
return
	</textarea><br />

	<label>Code to Minify:</label><br />
	<textarea id="textareaCodeToMinify" cols="80" rows="16">
// This is a test!

function add(addend0, addend1)
{
	var result = addend0 + addend1;
	return result;
}

var addend0 = 1; // test comment
var addend1 = 2;

var sum = add(addend0, addend1);

alert("The sum of " + addend0 + " and " + addend1 + " is " + sum);

	</textarea><br />

	<button onclick="buttonMinify_Clicked();">Minify</button><br />

	<label>Code after Minification:</label><br />
	<textarea id="textareaCodeMinified" cols="80" rows="8"></textarea><br />

</div>

<script type="text/javascript">

// ui event handlers

function buttonMinify_Clicked()
{
	var textareaKeywordsToIgnore = 
		document.getElementById("textareaKeywordsToIgnore");

	var keywordsToIgnoreAsString = 
		textareaKeywordsToIgnore.value;
	var keywordsToIgnore = 
		keywordsToIgnoreAsString.split("\n"); 

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

	var codeToMinify = textareaCodeToMinify.value;

	var minifier = new Minifier(keywordsToIgnore);

	var codeMinified = minifier.minifyCode(codeToMinify);

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

	textareaCodeMinified.value = codeMinified;
}

// classes

function Minifier(keywords)
{
	this.keywords = keywords;

	var identifiersMinifiedAsString = 
		"a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;w;x;y;z;"
		+ "A;B;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;Y;Z";

	this.identifiersMinified = 
		identifiersMinifiedAsString.split(";");
}
{
	Minifier.prototype.minifyCode = function(codeToMinify)
	{
		var codeAsLines = codeToMinify.split("\n");
		var codeAsLinesMinusComments = [];

		for (var i = 0; i < codeAsLines.length; i++)
		{
			var codeLine = codeAsLines[i];
			var indexOfDoubleSlash = codeLine.indexOf("//");

			var codeLineMinusComments;

			if (indexOfDoubleSlash == -1)
			{
				codeLineMinusComments = codeLine;
			}
			else
			{
				codeLineMinusComments = 
					codeLine.substr(0, indexOfDoubleSlash);
			}

			codeAsLinesMinusComments.push
			(
				codeLineMinusComments
			);

		}

		var codeMinusComments = codeAsLinesMinusComments.join("\n");

		var tokenizer = new Tokenizer();

		var codeAsTokens = 
			tokenizer.tokenizeString(codeMinusComments);

		var identifierLookup = [];

		for (var k = 0; k < this.keywords.length; k++)
		{
			var keyword = this.keywords[k];
			identifierLookup[keyword] = keyword + " ";	
		}

		var codeMinifiedAsTokens = [];

		for (var t = 0; t < codeAsTokens.length; t++)
		{
			var tokenToMinify = codeAsTokens[t];

			var tokenMinified;
		
			if (tokenToMinify.isIdentifier() == false)
			{
				tokenMinified = tokenToMinify;
			}
			else
			{
				tokenMinified = identifierLookup[tokenToMinify];

				if (tokenMinified == null)
				{
					var numberOfIdentifiersSoFar = 
						identifierLookup.length;

					tokenMinified = 
						this.identifiersMinified[numberOfIdentifiersSoFar];
	
					identifierLookup[tokenToMinify] = tokenMinified;
	 				identifierLookup.push(tokenToMinify);
				}
			}

			codeMinifiedAsTokens.push(tokenMinified);
		}

		var codeMinified = codeMinifiedAsTokens.join(""); // todo

		return codeMinified;
	}
}

function StringHelper()
{
	// do nothing
}
{
	String.prototype.isLetter = function()
	{
		var charCode = this.charCodeAt(0);
		var returnValue = 
		(
			(
				charCode >= "A".charCodeAt(0) 
				&& charCode <= "Z".charCodeAt(0) 
			)
			||
			(
				charCode >= "a".charCodeAt(0) 
				&& charCode <= "z".charCodeAt(0) 
			)

		);

		return returnValue;
	}

	String.prototype.isIdentifier = function()
	{
		var returnValue = 
		( 
			this.length >= 1
			&& isNaN( parseFloat(this) ) 
			&& this[0].isLetter()
		);

		return returnValue;
	}

	String.prototype.isNumber = function()
	{
		var returnValue = 
		(
			isNaN(parseFloat(this)) == false
			|| this == "."
		)

		return returnValue;
	}

	String.prototype.isWhitespace = function()
	{
		var returnValue = true;

		for (var i = 0; i < this.length; i++)
		{
			var char = this[i];
			if 
			(
				char != " "
				&& char != "\t"
				&& char != "\r"
				&& char != "\n"
			)
			{
				returnValue = false;
				break;
			}
		}

		return returnValue;
	}
}

function Tokenizer()
{
	// do nothing
}
{
	Tokenizer.prototype.tokenizeString = function(stringToTokenize)
	{
		var tokensSoFar = [];

		var tokenInProgress = "";

		var isWithinQuotes;

		for (var i = 0; i < stringToTokenize.length; i++)
		{
			var char = stringToTokenize[i];
			
			if (isWithinQuotes == true)
			{
				tokenInProgress += char;

				if (char == "\"")
				{
					isWithinQuotes = false;
					tokensSoFar.push(tokenInProgress);
					tokenInProgress = "";
				}
				else if (char == "\\")
				{
					i++;
					var charNext = stringToTokenize[i];
					tokenInProgress += charNext;
				}
			}
			else if (char.isWhitespace())
			{
				if (tokenInProgress.length > 1)
				{
					tokensSoFar.push(tokenInProgress);
					tokenInProgress = "";
				}
			}
			else if (char.isLetter() || char.isNumber())
			{
				tokenInProgress += char;	
			}
			else if (char == "\"")
			{
				isWithinQuotes = true;
				tokenInProgress += char;
			}
			else
			{
				tokensSoFar.push(tokenInProgress);
				tokensSoFar.push(char);
				tokenInProgress = "";
			}
		}		

		return tokensSoFar;
	}
}

</script>

</body>
</html>

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

Hello World with the Bootstrap Web UI Framework

The instructions below can be followed to build and run a minimal web page using Bootstrap. Bootstrap is a free and open source web UI development framework originally created by developers working at Twitter.

(To be honest, there’s not much to this post at the moment. When I tried to do a more complicated example than Hello World, the layout stopped working as expected. Nonetheless, I’ll post what I have for future reference.)

1. In any convenient location, create a new directory named “BootstrapTest”.

2. Download the package for Bootstrap into the newly created BootstrapTest directory. As of this writing, the latest version is available for download as a .zip file at the URL “https://getbootstrap.com/”.

3. Extract the contents of the downloaded .zip file using any convenient .zip extraction utility, and rename the resulting directory to “bootstrap”.

4. In the BootstrapTest directory, create a new text file named “BootstrapTest.html”, containing the following text. This code is minimally adapted from a sample found on the official Bootstrap site at the URL “https://getbootstrap.com/”.


<!-- This code is minimally adapted from a sample -->
<!-- found on the official Bootstrap site -->
<!-- at the URL https://getbootstrap.com. -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>
    <h1>Hello, world!</h1>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="bootstrap/js/bootstrap.min.js"></script>
  </body>
</html>


5. Open the BootstrapTest.html file created in the previous step in a web browser that runs JavaScript. Verify that the message “Hello, World” appears.

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