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.


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

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

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

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

			var pixelColorRGB = [0, 0, 0];

			for (var b = 0; b < bytesPerPixel; b++)
			{
				var byteIndex = 
					pixelIndex * bytesPerPixel
					+ b;

				if (byteIndex >= numberOfBytes)
				{
					x = imageSizeInPixels.x;
					break;
				}

				var byte = bytes[byteIndex];
				pixelColorRGB[b] = byte;
			}

			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;

	for (var y = 0; 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
			);

			var alpha = pixelColorRGBA[3];
			if (alpha == 0)
			{
				y = imageSizeInPixels.y;
				break;
			}

			for (var b = 0; b < bytesPerPixel; b++)
			{
				var byteIndex = 
					pixelIndex * bytesPerPixel 
					+ b;

				var byte = pixelColorRGBA[b];

				bytes[byteIndex] = byte;
			}
		}
	}

	var numberOfBytes = bytes.length;

	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

Installing and Running Magento

The instructions below can be followed to install and run a sample e-commerce site using Magento. Magento is an open-source e-commerce platform implemented in PHP and originally developed by Varien, Inc.

1. Download and install XAMPP, a software package which incorporates the Apache web server, the MariaDB database, and the PHP server-side web language, all of which will be necessary to install Magento. As of this writing, the latest version of XAMPP is available for download from the URL “https://www.apachefriends.org/download.html”.

2. Install XAMPP. When prompted, specify and make a note of the password for the “root” user of the MariaDB database.

3. Initiate a MySQL client session somehow, perhaps by opening a command prompt and entering the command “mysql -u root –password”. Enter the password specified in the previous step when prompted.

4. At the MariaDB session prompt, enter the command “create database magento;”.

5. Download Magento. As of this writing, the latest version is available by visiting the URL “https://github.com/OpenMage/magento-mirror”, clicking on the “Clone or Download” button, and clicking the “Download ZIP” button.

6. Extract Magento from the downloaded .zip file using any convenient ZIP utility, perhaps 7-Zip.

7. Rename the extracted Magento directory to “magento” and copy it into Apache’s “htdocs” directory (perhaps at “C:\xampp\htdocs”).

8. Enable PHP’s “soap” extension by locating the file “php.ini” (perhaps at “C:\xampp\php\php.ini”) and editing it to remove the initial semicolon from the line “;extension=php_soap.dll”. This will uncomment the line, which should enable the extension.

9. Still within php,ini, locate the max_execution_time variable and increase its value significantly, perhaps to 120 (seconds). Save the file. This change may prevent a premature timeout error during the installation process.

10. Start or restart the Apache server, perhaps by starting the XAMPP control panel and clicking the “Start” button on the line for Apache. This will apply the changes from the previous steps to the PHP and web server setup.

11. Open a web browser and navigate to the Magento Installation Wizard at the URL “http://127.0.0.1/magento”. (Do not use “localhost”, as the Magento installer does not seem to handle this alias correctly.)

12. Follow the prompts to install Magento, and make a note of the encryption key generated by Magento in the final step. (The installation process is by no means robust, and following any errors it may be necessary to discard and replace te magento directory under htdocs, drop and re-create the “magento” database in MySQL, restart Apache, and re-start the installation. If errors do occur, Magento will likely suppress any details from appearing in the browser, so it may be helpful to look in the “var/reports” directory under the “magento” directory to see the actual error text.)

13. On the final page of the Magento Installation Wizard, click the button labeled “Go to Backend”. On the page that appears in a new browser tab, log in using the admin account created in the previous step. Dismiss any popup notifications, and verify that the Magento Admin Panel user interface appears as expected.

14 .Back on the final page of the Magento Installation Wizard, click the button labeled “Go to Frontend”. Verify that the default Magento frontend user interface appears.

Posted in Uncategorized | Tagged , | Leave a comment

Getting Started with Knockout.JS

The instructions below can be followed to create a simple task list application in Knockout.js. Knockout.js is an open-source JavaScript web framework that implements the “MVVM” (“Model-View-ViewModel”) software pattern. It was developed by Steve Sanderson while employed at Microsoft.

These instructions are a simplified version of a tutorial hosted the URL “http://learn.knockoutjs.com”.

KnockoutJS-TaskList.png

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

2. Download the current version of Knockout.js, place it in the newly created KnockoutJS directory, and rename the file to “knockout.js”. As of this writing, the latest version is available for download at the URL “http://knockoutjs.com/downloads/index.html”.

3. Still in the KnockoutJS directory, create a new text file named “TaskList-View.html”, containing the following text.


<!-- This code is minimally adapted from a sample at the URL "http://learn.knockoutjs.com". -->

<html>
<body>

<script type="text/javascript" src="knockout.js"></script>

<h3>Tasks</h3>

<form data-bind="submit: addTask">
    Add task: <input data-bind="value: newTaskText" placeholder="What needs to be done?" />
    <button type="submit">Add</button>
</form>

<ul data-bind="foreach: tasks, visible: tasks().length > 0">
    <li>
        <input type="checkbox" data-bind="checked: isDone" />
        <input data-bind="value: title, disable: isDone" />
        <a href="#" data-bind="click: $parent.removeTask">Delete</a>
    </li> 
</ul>

You have <b data-bind="text: incompleteTasks().length"> </b> incomplete task(s)
<span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>

<script type="text/javascript" src="TaskList-ViewModel.js"></script>

</body>
</html>

4. Still in the KnockoutJS directory, create a new text file named “TaskList-ViewModel.js”, containing the following text.


// This code is minimally adapted from a sample at the URL 
// http://learn.knockoutjs.com 

function Task(data) {
	this.title = ko.observable(data.title);
	this.isDone = ko.observable(data.isDone);
}

function TaskListViewModel() {
	// Data
	var self = this;
	self.tasks = ko.observableArray([]);
	self.newTaskText = ko.observable();
	self.incompleteTasks = ko.computed(function() {
		return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() });
	});

	// Operations
	self.addTask = function() {
		self.tasks.push(new Task({ title: this.newTaskText() }));
		self.newTaskText("");
	};
	self.removeTask = function(task) { self.tasks.remove(task) };
}

ko.applyBindings(new TaskListViewModel());

4. Open the file “TaskList.html” in a web browser that runs JavaScript. Verify that a task list interface appears, and that it works as expected.

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

Getting Started with ReactJS

The instructions below can be followed to create a simple user interface in React.js. React.js is an open-source user interface library for JavaScript originated by Facebook.

These instructions are a simplified version of the tutorial found on React.js’s official website, currently available at the URL
“https://facebook.github.io/react/tutorial/tutorial.html”
.

TicTacToeViaReactJS.png

1. If you have not already done so, install Node.js. As of this writing, the latest version is available for download at the URL “https://nodejs.org/en/download/”.

2. In any convenient location, create a new directory named “ReactJS”.

3. Open a command prompt, navigate to the ReactJS directory created in the previous step, and run the following command to install the “create-react-app” utility:

	npm install -g create-react-app

4. Still in the command prompt, run the following commands and wait for them to complete. A new React.js application called “my-app” will be created within the ReactJS directory.

	create-react-app my-app
	cd my-app
	npm start

5. Navigate to the “src” directory within the newly created “my-app” directory.

6. Delete the existing contents of the src directory.

7. Still within the src directory, create a new text file named “index.css”, containing the following text.


/* 
 * This code is minimally adapted from a sample found at the URL
 * https://facebook.github.io/react/tutorial/tutorial.html 
 */

body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

8. Still within the src directory, create a new text file named “index.js”, containing the following text.


// This code is minimally adapted from a sample found at the URL
// https://facebook.github.io/react/tutorial/tutorial.html

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ? "Move #" + move : "Game start";
      return (
        <li key={move}>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(<Game />, document.getElementById("root"));

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

10. Back in the command prompt, navigate to the “my-app” directory and run the command “npm start” to start the application.

11. If it is not done automatically, open a web browser and navigate to the URL “http://localhost:3000”. Verify that a tic-tac-toe grid is displayed.

12. Click the “Game Start” link, then click on the various cells of the tic-tac-toe to play the game.

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

A Flood-Fill Implementation in JavaScript

Below is a simple implementation of a floodfill algorithm in JavaScript. When run, the program will draw a green background on a canvas, render the word “FLOOD” over it in orange, and then flood-fill the background with cyan, leaving the interiors (and outside edges, due to a “tolerance” setting of 0) of the “O” and “D” characters with the original background. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The floodFill() function extends the existing CanvasRenderingContext2D class.
The function takes as its arguments the x and y position of the first pixel to fill, followed by an argument named “colorDifferenceTolerance”. The algorithm uses the A* (“A-star”) algorithm to identify contiguous pixels to be filled with the specified fill color. If the color tolerance is set to 0, as in the sample below, only contiguous pixels that exactly match the color of the first pixel will be overwritten with the fill color.

Floodfill.png


<html>
<body>


<script type="text/javascript">

// main
function main()
{
    var canvasSize = new Coords(100, 100);

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

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

    graphics.fillStyle = "DarkGreen";
    graphics.fillRect(0, 0, canvasSize.x, canvasSize.y);

    var fontHeight = canvasSize.y * .25;
    graphics.font = fontHeight + "px sans-serif";
    graphics.fillStyle = "Orange";
    graphics.fillText
    (
        "FLOOD", 
        fontHeight * .25, 
        (canvasSize.y - fontHeight) / 2 + fontHeight 
    );

    graphics.fillStyle = "Cyan";
    graphics.floodFill(0, 0, 0);
}

// extensions

function CanvasRenderingContext2DExtensions()
{
    // extension class
}
{
    CanvasRenderingContext2D.prototype.floodFill = function(x, y, colorDifferenceTolerance)
    {    
        var canvas = this.canvas;
        var imageSize = new Coords(canvas.width, canvas.height);
        var imageSizeMinusOnes = imageSize.clone().subtract(new Coords(1, 1));

        var colorToFillOverRGBA = this.getImageData(x, y, 1, 1).data;

        var pixelPos = new Coords(x, y);
        var pixelIndexStart = pixelPos.y * imageSize.x + pixelPos.x;
        var pixelIndicesToTest = [ pixelIndexStart ];
        var pixelIndicesAlreadyTested = [];

        var neighborOffsets = 
        [
            new Coords(-1, 0),
            new Coords(1, 0),
            new Coords(0, -1),
            new Coords(0, 1)
        ];

        while (pixelIndicesToTest.length > 0)
        {
            var pixelIndex = pixelIndicesToTest[0];
            pixelIndicesToTest.splice(0, 1);
            pixelIndicesAlreadyTested[pixelIndex] = pixelIndex;

            pixelPos.x = pixelIndex % imageSize.x;
            pixelPos.y = Math.floor(pixelIndex / imageSize.x);

            var pixelRGBA = this.getImageData(pixelPos.x, pixelPos.y, 1, 1).data;
            var pixelDifference = Math.abs
            (
                pixelRGBA[0] - colorToFillOverRGBA[0]
                + pixelRGBA[1] - colorToFillOverRGBA[1]
                + pixelRGBA[2] - colorToFillOverRGBA[2]
            );

            if (pixelDifference <= colorDifferenceTolerance)
            {
                this.fillRect(pixelPos.x, pixelPos.y, 1, 1);

                var neighborPos = new Coords();

                for (var n = 0; n < neighborOffsets.length; n++)
                {
                    var neighborOffset = neighborOffsets[n];

                    neighborPos.overwriteWith
                    (
                        pixelPos
                    ).add
                    (
                        neighborOffset
                    );

                    if (neighborPos.isInRange(imageSize) == true)
                    {
                        var neighborIndex = 
                            neighborPos.y * imageSize.x + neighborPos.x;
                        var isPixelIndexAlreadyUnderConsideration = 
                        (
                            pixelIndicesToTest.indexOf(neighborIndex) >= 0 
                            || pixelIndicesAlreadyTested[neighborIndex] != null
                        )  
                        if (isPixelIndexAlreadyUnderConsideration == false)
                        {
                            pixelIndicesToTest.push(neighborIndex);
                        }
                    }
                }
            }                
        }
    }
}

// classes

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.isInRange = 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;
    }
}

// run

main();

</script>

</body>
</html>

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