Drawing a Rotated Image to an HTML5 Canvas

The JavaScript code below, when run, prompts the user to specify an image file to be uploaded, and then displays that image, scaled to 100 x 100 pixels and rotated 45 degrees clockwise in the center of an HTML5 canvas.

ImageRotatedOnHTML5Canvas.png


<html>
<body>

<!-- ui -->

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

<!-- ui ends -->

<script type="text/javascript">

// ui event handlers

function inputFileImageToDraw_Changed(inputFileImageToDraw)
{
	var fileToLoad = inputFileImageToDraw.files[0];
	if (fileToLoad != null)
	{
		if (fileToLoad.type.match("image.*") != null)
		{
			var fileReader = new FileReader();
			fileReader.onload =
				inputFileImageToDraw_Changed_FileLoaded;
			fileReader.readAsDataURL(fileToLoad);
		}
	}
}

function inputFileImageToDraw_Changed_FileLoaded(fileLoadedEvent)
{
	var imageToDraw = document.createElement("img");
	imageToDraw.src = fileLoadedEvent.target.result;

	var display = new Display
	(
		new Coords(200, 200), "Gray", "Black"
	);
	display.initialize();

	display.clear();

	display.drawImageAtPosWithSizeAndRotation
	(
		imageToDraw,
		new Coords(100, 100), // pos
		new Coords(100, 100), // size
		.125 // rotationInCycles
	);

}
// classes

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

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

	Display.RadiansPerCycle = Math.PI * 2;

	// methods

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

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

	Display.prototype.drawImageAtPosWithSizeAndRotation = function
	(
		imageToDraw, pos, size, rotationInCycles
	)
	{
		this.graphics.save();
		this.graphics.translate(pos.x, pos.y);
		var rotationInRadians =
			rotationInCycles * Display.RadiansPerCycle;
		this.graphics.rotate(rotationInRadians);
		this.graphics.drawImage
		(
			imageToDraw,
			0 - size.x / 2, 0 - size.y / 2, // pos
			size.x, size.y // size
		);
		this.graphics.restore();
	}

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

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

</script>

</body>
</html>

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

A Simple Flashcard System in JavaScript

The JavaScript program below, when run, prompts for a lesson file, and, when one is provided, quizzes the user with the questions from it. When the user answers all questions correctly three times in a row, the lesson is counted as complete.

The lesson file should be a text file. Each line in the file represents a question and its correct response, delimited by a semicolon.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/flashcards.html.

UPDATE 2017/01/13 – The program has been modified to allow the user to change the number of times each question must be answered correctly, as well as to reverse the question and answer. Also, some very basic input validation has been added.

flashcards


<html>
<body>

	<!-- user interface -->
	<div style="border:1px solid">
		<label>Lesson File:</label>
		<input id="inputFileLesson" type="file"></input>
		
		<div>
			<label>Reverse Question and Answer:</label>
			<input id="checkboxReverse" type="checkbox"></input>
		</div>

		<div>
			<label>Times Each Question Must Be Answered Correctly:</label>		
			<input id="inputTimesCorrectPerQuestion" type="number" value="3"></input>
		</div>

		<button id="buttonStart" onclick="buttonStart_Clicked();">Start Lesson</button>
	</div>

	<div style="border:1px solid">
		<div>
			<div><label>Question:</label></div>
			<textarea id="textareaPresentation" readonly="readonly"></textarea>
		</div>

		<div>
			<div><label>Answer:</label></div>
			<input id="inputResponse"></input>
		</div>

		<p id="pStatusMessage">Upload a valid lesson file and click the Start button to begin.</p>
	</div>
	
<script type="text/javascript">

// ui event handlers

function buttonStart_Clicked()
{
	var inputFileLesson = document.getElementById("inputFileLesson");
	var file = inputFileLesson.files[0];
	
	if (file == null)
	{
		alert("A valid lesson file must be specified by clicking the Lesson File button.");
	}
	else
	{
		var fileReader = new FileReader();
		fileReader.onload = inputFileLesson_Changed_FileLoaded;
		fileReader.readAsText(file);
	}
}

function inputFileLesson_Changed_FileLoaded(event)
{
	var fileContents = event.target.result;

	var questionsAsStrings = fileContents.split("\n");

	var questions = [];

	for (var i = 0; i < questionsAsStrings.length; i++)
	{
		var questionAsString = questionsAsStrings[i];

		var presentationAndResponse = questionAsString.split(";");

		var presentation = presentationAndResponse[0];
		var response = presentationAndResponse[1].trim();

		var question = new Question(presentation, response);

		questions.push(question);
	}
	
	var checkboxReverse = document.getElementById("checkboxReverse");
	var arePresentationAndResponseReversed = checkboxReverse.checked;
	
	var inputTimesCorrectPerQuestion = document.getElementById
	(
		"inputTimesCorrectPerQuestion"
	);
	var timesCorrectPerQuestion = parseInt(inputTimesCorrectPerQuestion.value);
	if (isNaN(timesCorrectPerQuestion) == true || timesCorrectPerQuestion <= 0)
	{
		alert("A positive number must be entered in the Times Each Question Must Be Answered Correctly box.");
	}
	else
	{
		var lessonDefn = new LessonDefn
		(
			questions, 
			arePresentationAndResponseReversed,
			timesCorrectPerQuestion
		);

		Globals.Instance.initialize(lessonDefn);
	}
}

// classes

function DisplayHelper()
{
	// do nothing
}
{
	DisplayHelper.prototype.displayLessonRun = function(lessonRun)
	{
		var textareaPresentation = document.getElementById
		(
			"textareaPresentation"
		);

		var questionCurrent = lessonRun.questionCurrent();
		var presentation = 
		(
			lessonRun.defn.arePresentationAndResponseReversed == true 
			? questionCurrent.responseCorrect 
			: questionCurrent.presentation
		);
		textareaPresentation.value = presentation;

		var inputResponse = document.getElementById("inputResponse");
		inputResponse.value = "";
		inputResponse.focus();

		var pStatusMessage = document.getElementById("pStatusMessage");
		pStatusMessage.innerHTML = lessonRun.statusMessage;
	}
}

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

	Globals.Instance = new Globals();

	// methods

	Globals.prototype.initialize = function(lessonDefn)
	{
		this.lessonDefn = lessonDefn;

		this.lessonRun = new LessonRun(this.lessonDefn);

		this.displayHelper = new DisplayHelper();
		this.inputHelper = new InputHelper();

		this.lessonRun.initialize();

		this.inputHelper.initialize();

		this.displayHelper.displayLessonRun(this.lessonRun);
	}
}


function InputHelper()
{
	// do nothing
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeydown.bind(this);
	}

	// event handlers

	InputHelper.prototype.handleEventKeydown = function(event)
	{
		if (event.key == "Enter")
		{
			var inputResponse = document.getElementById("inputResponse");
			var responseActual = inputResponse.value.trim();
			
			var lessonRun = Globals.Instance.lessonRun;
			var questionCurrent = lessonRun.questionCurrent();
			var responseExpected = 
			(
				lessonRun.defn.arePresentationAndResponseReversed == true 
				? questionCurrent.presentation 
				: questionCurrent.responseCorrect
			);
	
			var responseRecordCurrent = lessonRun.responseRecordCurrent();

			if (responseActual == responseExpected)
			{
				responseRecordCurrent.timesCorrect++;
				lessonRun.statusMessage = 
					"Correct!  The previous question has been answered correctly "
					+ responseRecordCurrent.timesCorrect
					+ " times in a row.";
			}
			else
			{
				responseRecordCurrent.timesCorrect = 0;
				lessonRun.statusMessage = 
					"Incorrect!  The correct answer was "
					+ responseExpected
					+ ".  You answered "
					+ responseActual
					+ ".";	
			}

			if (lessonRun.isComplete() == true)
			{
				lessonRun.statusMessage = 
					"Lesson complete!  Each question was answered correctly " 
					+ lessonRun.defn.timesCorrectPerQuestion
					+ " times in a row.";
				document.body.onkeydown = null;
			}
			else
			{
				lessonRun.questionAdvance();
			}

			Globals.Instance.displayHelper.displayLessonRun
			(
				lessonRun
			);
		}
		
	}
}

function LessonDefn(questions, arePresentationAndResponseReversed, timesCorrectPerQuestion)
{
	this.questions = questions;
	this.arePresentationAndResponseReversed = arePresentationAndResponseReversed;
	this.timesCorrectPerQuestion = timesCorrectPerQuestion;
}

function LessonRun(defn)
{
	this.defn = defn;
	this.statusMessage = 
		"Each question must be correctly answered " 
		+ this.defn.timesCorrectPerQuestion
		+ " times in a row.";
}
{
	LessonRun.prototype.initialize = function()
	{
		this.questionIndexCurrent = 0;
		this.responseRecords = [];

		var questions = this.defn.questions;

		for (var i = 0; i < questions.length; i++)
		{
			var question = questions[i];
			var responseRecord = new ResponseRecord();
			this.responseRecords.push(responseRecord);
		}
	}

	LessonRun.prototype.isComplete = function()
	{
		var returnValue = true;

		var timesRequired = this.defn.timesCorrectPerQuestion;

		for (var i = 0; i < this.responseRecords.length; i++)
		{
			var responseRecord = this.responseRecords[i];
			if (responseRecord.timesCorrect < timesRequired)
			{
				returnValue = false;
				break;
			}
		}	

		return returnValue;
	}

	LessonRun.prototype.questionAdvance = function()
	{
		var isFirstTime = true;
		var timesRequired = this.defn.timesCorrectPerQuestion;

		while 
		(
			isFirstTime == true 
			|| this.responseRecordCurrent().timesCorrect >= timesRequired
		)
		{
			isFirstTime = false;

			this.questionIndexCurrent++;

			if (this.questionIndexCurrent >= this.defn.questions.length)
			{
				this.questionIndexCurrent = 0;
			}
		}
	}

	LessonRun.prototype.questionCurrent = function()
	{
		return this.defn.questions[this.questionIndexCurrent];
	}

	LessonRun.prototype.responseRecordCurrent = function()
	{
		return this.responseRecords[this.questionIndexCurrent];
	}

}

function Question(presentation, responseCorrect)
{
	this.presentation = presentation;
	this.responseCorrect = responseCorrect;
}

function ResponseRecord()
{
	this.timesCorrect = 0;
}

</script>

</body>
</html>

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

Accessing a Google API using OAuth2 in JavaScript

The instructions below describe how to create a JavaScript web application that accesses one of Google’s APIs using OAuth2.

1. If you have not already done so, install and activate a web server. If using Windows, the XAMPP package is recommended. Make a note of the location of the root document folder for the web server, perhaps at “C:\xampp\htdocs”.

2. Open a web browser.

3. If you have not already done so, create a Google account and then log in to that account.

4. In the web browser, navigate to the URL “https://console.developers.google.com“.

5. Click the “Create a project” button. In the dialog that appears, enter the value “OAuth2Test” in the Project name box, then click the “Create” button. Wait for the new project to be created.

6. A new dialog box titled “Credentials” will appear. (If it doesn’t, click on the “Credentials” item in the left-hand menu pane.) Click the “Create credentials” button, then select the entry titled “API key” from the list that appears.

7. A new dialog titled “API key created” will appear. Record the value in the “Your API key” box for later use, then click the “Close” button.

8. Click the “Create credentials” button again, and this time select the entry titled “OAuth client ID” from the list that appears. A warning message will appear stating that, “To create an OAuth client ID, you must first set a product name on the consent screen”.

9. Click the “Configure consent screen” button. On the “OAuth consent screen” tab that appears, enter the value “OAuth2Test” in the “Product name shown to users” box, then click the “Save” button.

10. Back on the “Create client ID” screen, click the “Web application” radio button. Enter the value “Oauth2Test” in the Name box that appears, and the value “http://localhost” in both the “Authorized JavaScript origins” and “Authorized redirect URIs” boxes beneath.

11. Click the “Create” button. Record the client ID and client secret that are displayed in the dialog that appears.

12. Enable the Google People API for the newly created project by navigating to the URL “https://console.developers.google.com/apis/api/people.googleapis.com/overview” and clicking the “Enable” button.

13. In the web server’s document folder (as determined in a previous step), create a new text file named “OAuth2.html”, containing the text from the code listing below. Substitute the API key and client ID obtained in the previous step at the end of the program, where indicated. This code is adapted from a sample found at the URL “https://developers.google.com/api-client-library/javascript/samples/samples“, under the heading “Authorizing and Making Authorized Requests”.


<html>
	
<body>

<button id="authorize-button" onclick="buttonAuthorize_Clicked()">Authorize</button>
<button id="signout-button" onclick="buttonSignout_Clicked()">Sign Out</button>

<script src="https://apis.google.com/js/api.js"></script>

<script type="text/javascript">

// ui event handlers

function buttonAuthorize_Clicked(event) 
{
	googleAPIClient.signIn();
}

function buttonSignout_Clicked(event) 
{
	googleAPIClient.signOut();
}

function GoogleAPIClient(googleAPI, apiKey, clientID)
{
	this.googleAPI = googleAPI;
	this.apiKey = apiKey;
	this.clientID = clientID;

	this.initialize();
}
{
	// public methods

	GoogleAPIClient.prototype.getUserGivenName = function() 
	{
		this.googleAPI.client.people.people.get
		(
			{
				resourceName: "people/me"
			}
		).then
		(
 			this.getUserGivenName_Success,
			this.getUserGivenName_Error
		);
	}

	GoogleAPIClient.prototype.getUserGivenName_Success = function(response) 
	{
		var givenName = 
			response.result.names[0].givenName;

		console.log
		(
			"Hello, " + givenName + "!"
		);
	}

	GoogleAPIClient.prototype.getUserGivenName_Error = function(reason) 
	{
		console.log
		(
			"Error: " + reason.result.error.message
		);
	}

	GoogleAPIClient.prototype.signIn = function()
	{
		this.googleAPI.auth2.getAuthInstance().signIn();
	}

	GoogleAPIClient.prototype.signOut = function()
	{
		this.googleAPI.auth2.getAuthInstance().signOut();
	}

	// initialization

	GoogleAPIClient.prototype.initialize = function() 
	{
		// Load the API client and auth2 library

		this.googleAPI.load
		(
			"client:auth2", 
			this.initialize_2.bind(this)
		);
	}

	GoogleAPIClient.prototype.initialize_2 = function() 
	{
		var initializerObject = 			
		{
			apiKey: this.apiKey,
			discoveryDocs: 
			[
				"https://people.googleapis.com"
					+ "/$discovery/rest?version=v1"
			],
			clientId: this.clientID,
			scope: "profile"
		}

		this.googleAPI.client.init
		(
			initializerObject
		).then
		(
			this.initialize_3.bind(this)
		);
	}

	GoogleAPIClient.prototype.initialize_3 = function() 
	{
		var authInstance = this.googleAPI.auth2.getAuthInstance();

		// Listen for sign-in state changes.
		authInstance.isSignedIn.listen
		(
			this.updateSigninStatus.bind(this)
		);

		// Handle the initial sign-in state.
		this.updateSigninStatus
		(
			authInstance.isSignedIn.get()
		);
	}

	// helper methods

	GoogleAPIClient.prototype.updateSigninStatus = function(isSignedIn) 
	{
		if (isSignedIn == true) 
		{
			this.getUserGivenName();
		}
	}

} // end class GoogleAPIClient

// run 

var googleAPIClient = new GoogleAPIClient
(
	gapi,
	"INSERT_API_KEY_HERE",
	"CLIENT_ID_HERE.apps.googleusercontent.com"
);

</script>

</body>

</html>

14. In the web browser, open the debugging pane (perhaps by pressing the F12 key) and make sure the Console sub-pane is visible.

15. In the web browser, navigate to the URL “http://localhost/OAuth2.html“. A page with two buttons, named “Authorize” and “Sign Out” will appear.

16. In another tab of the web browser, sign out of your Google account.

17. Click the “Authorize” button. On the dialog that appears, enter the username and password of a valid Google account (such as the one you just singed out of). If a dialog appears prompting for permissions, click the “Allow” button.

18. Verify that a customized greeting message is displayed in the debugging console pane.

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

A Network Routing Simulation in JavaScript with the Bellman-Ford Algorithm

The JavaScript code below, when run, simulates routing of packets within a simple computer network using the Bellman-Ford routing algorithm. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The simulation will be updated once per second, as packets are routed towards their destination nodes. New packets can be created by entering the appropriate text in the box and clicking the “Do” button. The nodes will share routing tables among themselves every 5 seconds.

networkroutingsimulator



<html>
<body>

<script type="text/javascript">

// main

function main()
{
	var display = new Display
	(
		new Coords(400, 300), // sizeInPixels
		10, // fontHeightInPixels
		"White", // colorBackground
		"Gray" // colorForeground
	);

	var network = new Network
	(
		new Coords(20, 20), // nodeSizeInPixels
		5, // timerTicksPerRouteShare
		// nodes
		[
			new Node("Node0", new Coords(30, 30)),
			new Node("Node1", new Coords(90, 90)),
			new Node("Node2", new Coords(150, 150)),
			new Node("Node3", new Coords(90, 210)),

		],
		// links
		[
			new Link( ["Node0", "Node1"], 8),
			new Link( ["Node1", "Node2"], 8),
			new Link( ["Node2", "Node3"], 8),
		], 
		// packets
		[
			// todo
		]
	);

	Globals.Instance.initialize
	(
		display,
		1, // timerTicksPerSecond
		network
	);
}

// extensions

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

		return this;
	}

	Array.prototype.remove = function(element)
	{
		var indexOfElement = this.indexOf(element);
		if (indexOfElement >= 0)
		{
			this.splice(indexOfElement, 1);
		}
		return this;
	}
}

// classes

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

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

}

function Display(sizeInPixels, fontHeightInPixels, colorBackground, colorBorder)
{
	this.sizeInPixels = sizeInPixels;
	this.fontHeightInPixels = fontHeightInPixels;
	this.colorBackground = colorBackground;
	this.colorBorder = colorBorder;
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			Coords.Instances.Zeroes, 
			this.sizeInPixels, 
			this.colorBackground,
			this.colorBorder
		);
	}

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

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

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

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

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

		document.body.appendChild(canvas);
	}

}

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

	Globals.Instance = new Globals();
	
	// methods

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

		this.network = network;
		this.network.initialize();

		var millisecondsPerTimerTick = 
			1000 / timerTicksPerSecond;

		this.timerTicksSoFar = 0;

		this.handleEventTimerTick();

		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this),
			millisecondsPerTimerTick
		);
	}

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

		this.timerTicksSoFar++;
	}
}

function Link(namesOfNodesLinked, costToTraverse)
{
	this.namesOfNodesLinked = namesOfNodesLinked;
	this.costToTraverse = costToTraverse;
}
{
	Link.prototype.node0 = function()
	{
		return Globals.Instance.network.nodes[this.namesOfNodesLinked[0]];
	}

	Link.prototype.node1 = function()
	{
		return Globals.Instance.network.nodes[this.namesOfNodesLinked[1]];
	}

	Link.prototype.nodes = function()
	{
		return [ this.node0(), this.node1() ];
	}

	// drawable

	Link.prototype.drawToDisplay = function(display)
	{
		var node0Center = this.node0().center(); 
		var node1Center = this.node1().center();

		display.drawLine
		(
			node0Center, node1Center, "Gray"
		);

		var midpoint = node0Center.add
		(
			node1Center
		).divideScalar(2);

		display.drawText
		(
			"" + this.costToTraverse, 
			midpoint,
			"Gray"
		);
	}	
}

function Network(nodeSizeInPixels, timerTicksPerRouteShare, nodes, links, packets)
{
	this.nodeSizeInPixels = nodeSizeInPixels;
	this.timerTicksPerRouteShare = timerTicksPerRouteShare;
	this.nodes = nodes;
	this.links = links;
	this.packets = packets;

	this.nodes.addLookups("name");
	this.packetsToRemove = [];
}
{
	Network.prototype.initialize = function()
	{
		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			var nodesLinked = link.nodes();
			for (var n = 0; n < nodesLinked.length; n++)
			{
				var nodeSource = nodesLinked[n];
				var nodeTarget = nodesLinked[1 - n]; 

				var linkToNeighbor = new Link
				(
					[
						nodeSource.name, 
						nodeTarget.name,
					],
					link.costToTraverse
				);

				nodeSource.linksToNeighbors.push(linkToNeighbor);				
			}
		}

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.initialize();
		}

		this.domElementUpdate();
	}

	Network.prototype.routesShareAmongNodes = function()
	{
		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.routesShareWithNeighbors();
		}
	}

	Network.prototype.updateForTimerTick = function()
	{
		var timerTicksSoFar = Globals.Instance.timerTicksSoFar;
		if 
		(
			timerTicksSoFar != 0 
			&& timerTicksSoFar % this.timerTicksPerRouteShare == 0
		)
		{
			this.routesShareAmongNodes();	
		}

		this.packetsToRemove.length = 0;

		for (var i = 0; i < this.packets.length; i++)
		{
			var packet = this.packets[i];
			if (packet.isDelivered() == true)
			{
				this.packetsToRemove.push(packet);
				packet.nodeCurrent().packetsDelivered.push(packet);
			}
			else
			{
				packet.updateForTimerTick();
			}
		}

		for (var i = 0; i < this.packetsToRemove.length; i++)
		{
			var packet = this.packetsToRemove[i];
			this.packets.remove(packet);
		}		
	}

	// dom

	Network.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divNetwork = document.createElement("div");

			var divControls = document.createElement("div");

			var labelCommand = document.createElement("label");
			labelCommand.innerHTML = "Command:";
			divControls.appendChild(labelCommand);

			var inputCommandText = document.createElement("input");
			inputCommandText.id = "inputCommandText";
			inputCommandText.value = "packet Node0 Node3 data"
			divControls.appendChild(inputCommandText);
			
			var buttonCommandPerform = document.createElement("button");
			buttonCommandPerform.innerHTML = "Do";
			buttonCommandPerform.onclick = this.buttonCommandPerform_Clicked.bind(this);
			divControls.appendChild(buttonCommandPerform);

			var buttonCommandHelp = document.createElement("button");
			buttonCommandHelp.innerHTML = "Help";
			buttonCommandHelp.onclick = this.buttonCommandHelp_Clicked.bind(this);
			divControls.appendChild(buttonCommandHelp);

			divNetwork.appendChild(divControls);

			document.body.appendChild(divNetwork);

			this.domElement = divNetwork;
		}
	}

	Network.prototype.buttonCommandHelp_Clicked = function()
	{
		var message = "Valid command format: 'packet [from] [to] [data]'";
		alert(message);
	}

	Network.prototype.buttonCommandPerform_Clicked = function()
	{
		var inputCommandText = document.getElementById("inputCommandText");
		var commandText = inputCommandText.value;
		var commandArguments = commandText.split(" ");
		var operationName = commandArguments[0];
		
		if (operationName == "packet")
		{
			if (commandArguments.length != 4)
			{
				alert("Wrong number of arguments!");
			}

			var nodeNameSource = commandArguments[1]; 
			var nodeTargetName = commandArguments[2];
			var payload = commandArguments[3];

			var nodeSource = this.nodes[nodeNameSource];
			var nodeTarget = this.nodes[nodeTargetName];

			if (nodeSource == null)
			{
				alert("Invalid source node name: " + nodeNameSource);
			}
			else if (nodeSource == null)
			{
				alert("Invalid target node name: " + nodeNameSource);
			}
			else
			{
				var packet = new Packet
				(
					nodeNameSource, nodeTargetName, payload
				);
				this.packets.push(packet);

			}
		}
		else
		{
			alert("Unrecognized command!");
		}
	}

	// drawable

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

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

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

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

		display.drawText
		(
			"Time:" + Globals.Instance.timerTicksSoFar, 
			new Coords(10, 10), 
			"Gray"
		);

		display.drawText
		(
			"Routes shared every " 
				+ this.timerTicksPerRouteShare 
				+ " ticks.", 
			new Coords(10, 20), 
			"Gray"
		);
	}
}

function Node(name, pos)
{
	this.name = name;
	this.pos = pos;
	
	this.linksToNeighbors = [];
	this.routes = [];
	this.packetsDelivered = [];
}
{
	Node.prototype.center = function()
	{
		return this.size().clone().divideScalar(2).add(this.pos);
	}

	Node.prototype.initialize = function()
	{
		for (var i = 0; i < this.linksToNeighbors.length; i++)
		{
			var link = this.linksToNeighbors[i];

			var neighborName = link.namesOfNodesLinked[1];

			this.linksToNeighbors[neighborName] = link;

			var route = new Route
			(
				neighborName, // nodeTargetName, 
				link.costToTraverse, // totalCostToTarget, 
				neighborName // nodeNextName
			);
			this.routes.push(route);
		}

		this.routes.addLookups("nodeTargetName");
	}

	Node.prototype.routesShareWithNeighbors = function()
	{
		for (var n = 0; n < this.linksToNeighbors.length; n++)
		{
			var link = this.linksToNeighbors[n];
			var nodeNeighbor = link.node1();
			nodeNeighbor.routesUpdateFromNeighbor
			(
				this.name, 
				link.costToTraverse, 
				this.routes
			);
		}
	}

	Node.prototype.routesUpdateFromNeighbor = function
	(
		neighborName, costToNeighbor, routesFromNeighbor
	)
	{
		for (var r = 0; r < routesFromNeighbor.length; r++)
		{
			var routeFromNeighbor = routesFromNeighbor[r];

			var totalCostToTargetThroughNeighbor = 
				costToNeighbor + routeFromNeighbor.totalCostToTarget;

			var nodeTargetName = routeFromNeighbor.nodeTargetName;

			if (nodeTargetName == this.name)
			{
				// do nothing
			}
			else 
			{
				var routeExisting = this.routes[nodeTargetName];
				if (routeExisting == null)
				{
					var routeNew = new Route
					(
						nodeTargetName, // target
						totalCostToTargetThroughNeighbor, 
						neighborName // nodeNextName
					);
	
					this.routes.push(routeNew);
					this.routes[nodeTargetName] = routeNew;
				}
				else if (routeExisting.totalCostToTarget > totalCostToTargetThroughNeighbor)
				{
					routeExisting.totalCostToTarget = totalCostToTargetThroughNeighbor;
					routeExisting.nodeNextName = neighborName;
				}
			}
		}
	}

	Node.prototype.size = function()
	{
		return Globals.Instance.network.nodeSizeInPixels;
	}

	// drawable

	Node.prototype.drawToDisplay = function(display)
	{
		var network = Globals.Instance.network;

		display.drawRectangle
		(
			this.pos, network.nodeSizeInPixels, "White", "Gray"
		);
		display.drawText(this.name, this.pos, "Gray"); 


		var textPos = this.center();
		textPos.y += display.fontHeightInPixels;

		for (var i = 0; i < this.routes.length; i++)
		{
			var route = this.routes[i];
			display.drawText(route.toString(), textPos, "Blue");
			textPos.y += display.fontHeightInPixels;
		}

		for (var i = 0; i < this.packetsDelivered.length; i++)
		{
			var packet = this.packetsDelivered[i];
			display.drawText(packet.toString(), textPos, "DarkGreen");
			textPos.y += display.fontHeightInPixels;
		}

	}
}

function Packet(nodeSourceName, nodeTargetName, payload)
{
	this.nodeSourceName = nodeSourceName;
	this.nodeCurrentName = this.nodeSourceName;
	this.nodeTargetName = nodeTargetName;
	this.payload = payload;

	this.nodeNextName = null;
	this.ticksTowardNodeNext = 0;
}
{
	Packet.prototype.fractionTowardNodeNext = function()
	{
		var returnValue = 
			this.ticksTowardNodeNext 
			/ this.linkCurrent().costToTraverse;

		return returnValue;		
	}

	Packet.prototype.isDelivered = function()
	{
		return (this.nodeCurrentName == this.nodeTargetName);
	}

	Packet.prototype.linkCurrent = function()
	{
		return this.nodeCurrent().linksToNeighbors[this.nodeNextName];
	}

	Packet.prototype.nodeCurrent = function()
	{
		return Globals.Instance.network.nodes[this.nodeCurrentName];
	}

	Packet.prototype.nodeNext = function()
	{
		return Globals.Instance.network.nodes[this.nodeNextName];
	}

	Packet.prototype.updateForTimerTick = function()
	{
		if (this.nodeNextName == null)
		{
			var nodeCurrent = this.nodeCurrent();
			var route = nodeCurrent.routes[this.nodeTargetName];
			if (route == null)
			{
				// Drop the packet?
			}
			else
			{
				this.nodeNextName = route.nodeNextName;
			}
		}
		else
		{
			var linkCurrent = this.linkCurrent();
			if (linkCurrent != null)
			{
				this.ticksTowardNodeNext++;

				if (this.ticksTowardNodeNext < linkCurrent.costToTraverse)
				{
					// todo
				}
				else
				{
					this.nodeCurrentName = this.nodeNextName;	
					this.nodeNextName = null;
					this.ticksTowardNodeNext = 0;
				}
			}

		}
	}

	// drawable

	Packet.prototype.drawToDisplay = function(display, network)
	{
		var pos = this.nodeCurrent().center().clone();

		if (this.nodeNextName != null)
		{
			var fractionTowardNodeNext = 
				this.fractionTowardNodeNext();

			pos.multiplyScalar
			(
				1 - fractionTowardNodeNext
			).add
			(
				this.nodeNext().center().clone().multiplyScalar
				(
					fractionTowardNodeNext
				)
			);
		}


		display.drawText(this.toString(), pos, "Red"); 
	}

	// string

	Packet.prototype.toString = function()
	{
		var returnValue = 
			"[packet"
			+ " from:" + this.nodeSourceName 
			+ " to:" + this.nodeTargetName
			+ " data:" + this.payload 
			+ "]";

		return returnValue;
	}

}

function Route(nodeTargetName, totalCostToTarget, nodeNextName)
{
	this.nodeTargetName = nodeTargetName;
	this.totalCostToTarget = totalCostToTarget;
	this.nodeNextName = nodeNextName;
}
{
	// string

	Route.prototype.toString = function()
	{
		var returnValue = 
			"[route"
			+ " to:" + this.nodeTargetName
			+ " cost:" + this.totalCostToTarget
			+ "]";

		return returnValue;
	}
}

// run 

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , , | 1 Comment

An Entropy Generator in JavaScript

The JavaScript code below, when run, attempts to gather “entropy” (randomness) from the time between the user’s keypresses, and displays the gathered entropy as bits, hexadecimal digits, and Base64 digits. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

I’m not sure how well it works. With rapid typing, long runs of 1’s or 0’s tend to appear, which doesn’t seem very random. I’m not sure if the problem is just the nature of typing, the limitations of the way JavaScript runs, or just my own misunderstanding of how entropy works.

entropy


<html>
<body>

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

<script type="text/javascript">

// main
	
function main()
{
	var session = new Session();
	Globals.Instance.initialize(session);	
}

// classes

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

	Globals.prototype.initialize = function(session)
	{
		this.session = session;
		this.session.initialize();
	}
}

function Session()
{
	this.entropyBitsGatheredAsString = "";
	this.timeOfPreviousKeyEvent = new Date();
}
{
	// methods

	Session.prototype.entropyGatherFromEventKey = function(event)
	{
		var now = new Date();

		var millisecondsSincePreviousKeyEvent = 
			now.getTime() 
			- this.timeOfPreviousKeyEvent.getTime();

		var entropyBitGathered = 
			millisecondsSincePreviousKeyEvent % 2;

		this.entropyBitsGatheredAsString += "" + entropyBitGathered; 

		this.domElementUpdate();

		this.timeOfPreviousKeyEvent = now;
	}

	Session.prototype.entropyGatheringReset = function()
	{
		this.entropyBitsGatheredAsString = "";	

		this.domElementUpdate();
	}

	Session.prototype.initialize = function()
	{
		this.domElementUpdate();
		document.body.onkeyup = this.entropyGatherFromEventKey.bind(this);
	}

	// dom

	Session.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divSession = document.createElement("div");

			var divBits = document.createElement("div");

			var labelBitsGatheredSoFar = document.createElement("label");
			labelBitsGatheredSoFar.innerHTML = "Bits Gathered So Far:";
			divBits.appendChild(labelBitsGatheredSoFar);
			
			var inputBitsGatheredSoFar = document.createElement("textarea");
			inputBitsGatheredSoFar.cols = 40;
			inputBitsGatheredSoFar.rows = 10;
			inputBitsGatheredSoFar.disabled = true;
			this.inputBitsGatheredSoFar = inputBitsGatheredSoFar;
			divBits.appendChild(inputBitsGatheredSoFar);
		
			divSession.appendChild(divBits);

			var divHexadecimal = document.createElement("div");

			var labelAsHexadecimal = document.createElement("label");
			labelAsHexadecimal.innerHTML = "As Hexadecimal:";
			divHexadecimal.appendChild(labelAsHexadecimal);

			var inputAsHexadecimal = document.createElement("textarea");
			inputAsHexadecimal.cols = 40;
			inputAsHexadecimal.rows = 10;
			inputAsHexadecimal.readOnly = true;
			this.inputAsHexadecimal = inputAsHexadecimal;
			divHexadecimal.appendChild(inputAsHexadecimal);

			divSession.appendChild(divHexadecimal);

			var divBase64 = document.createElement("div");

			var labelAsBase64 = document.createElement("label");
			labelAsBase64.innerHTML = "As Base64:";
			divBase64.appendChild(labelAsBase64);

			var inputAsBase64 = document.createElement("textarea");
			inputAsBase64.cols = 40;
			inputAsBase64.rows = 10;
			inputAsBase64.readOnly = true;
			this.inputAsBase64 = inputAsBase64;
			divBase64.appendChild(inputAsBase64);

			divSession.appendChild(divBase64);

			var divControls = document.createElement("div");

			var buttonReset = document.createElement("button");
			buttonReset.innerHTML = "Reset";
			buttonReset.onclick = this.entropyGatheringReset.bind(this);
			divControls.appendChild(buttonReset);

			divSession.appendChild(divControls);

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

			this.domElement = divSession;

		}

		this.inputBitsGatheredSoFar.value = this.entropyBitsGatheredAsString;

		var numberOfBits = this.entropyBitsGatheredAsString.length;

		var bitsPerNibble = 4;
		var numberOfNibbles = Math.floor(numberOfBits / bitsPerNibble);
		var entropyGatheredAsHexadecimal = "";

		for (var i = 0; i < numberOfNibbles; i++)
		{
			var nibbleAsBitString = this.entropyBitsGatheredAsString.substr
			(
				i * bitsPerNibble, bitsPerNibble
			);

			var nibbleValue = parseInt(nibbleAsBitString, 2);

			var nibbleValueAsHexadecimal = nibbleValue.toString(16);

			entropyGatheredAsHexadecimal += nibbleValueAsHexadecimal;
		}

		this.inputAsHexadecimal.value = entropyGatheredAsHexadecimal;

		var base64DigitsAll = 
			"0123456789"
			+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
			+ "abcdefghijklmnopqrstuvwxyz"
			+ "+/";

		var bitsPerSextet = 6;
		var numberOfSextets = Math.floor(numberOfBits / bitsPerSextet);
		var entropyGatheredAsBase64 = "";

		for (var i = 0; i < numberOfSextets; i++)
		{
			var sextetAsBitString = this.entropyBitsGatheredAsString.substr
			(
				i * bitsPerSextet, bitsPerSextet
			);

			var sextetValue = parseInt(sextetAsBitString, 2);

			var sextetValueAsBase64 = base64DigitsAll[sextetValue];

			entropyGatheredAsBase64 += sextetValueAsBase64;
		}

		this.inputAsBase64.value = entropyGatheredAsBase64;

		return this.domElement;
	}

}

// run

main();

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

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

A Simple Hex Editor in JavaScript

The JavaScript code below implements a simple hex editor in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/hexeditor.html.

hexeditor


<html>
<body>

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

<script type="text/javascript">

// main

function main()
{
	var session = new Session([]);

	Globals.Instance.initialize(session);
}

// extensions

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

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

		return thisPadded;
	}
}

// classes

function Converter()
{
	// static class
}
{
	Converter.PrintableCharacters = 
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		+ "abcdefghijklmnopqrstuvwxyz"
		+ "0123456789";
		// todo - Symbols.

	Converter.bytesToStringASCII = function(bytes)
	{
		var returnValue = "";

		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsCharASCII = String.fromCharCode
			(
				byte
			);

			if (Converter.PrintableCharacters.indexOf(byteAsCharASCII) == -1)
			{
				byteAsCharASCII = ".";	
			}

			returnValue += byteAsCharASCII;
		}

		return returnValue;
	}

	Converter.bytesToStringHexadecimal = function(bytes)
	{
		var returnValue = "";

		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsStringHexadecimal = 
				byte.toString(16).padLeft(2, '0');

			returnValue += byteAsStringHexadecimal;
		}

		return returnValue;
	}

	Converter.stringHexadecimalToBytes = function(stringHexadecimal)
	{
		var returnValues = [];

		var nibblesForByteCurrent = [];

		for (i = 0; i < stringHexadecimal.length; i++)
		{
			var charForNibble = stringHexadecimal[i];
			var nibbleAsInt = parseInt(charForNibble, 16);
			if (isNaN(nibbleAsInt) == false)
			{
				nibblesForByteCurrent.push(nibbleAsInt);
				if (nibblesForByteCurrent.length == 2)
				{
					var byte = 
						(nibblesForByteCurrent[0] << 4) 
						+ nibblesForByteCurrent[1];
					returnValues.push(byte);
					nibblesForByteCurrent.length = 0;
				}
			}			
		}

		return returnValues;
	}	
}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();
	
	Globals.prototype.initialize = function(session)
	{
		this.session = session;
		this.session.domElementUpdate();
	}
}

function Session(bytes)
{
	this.bytes = bytes;
	this.finalNibble = "";
}
{
	// dom

	Session.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divSession = document.createElement("div");

			var textareaHexadecimal = document.createElement("textarea");
			textareaHexadecimal.cols = 40;
			textareaHexadecimal.rows = 10;
			textareaHexadecimal.onkeyup = this.textareaHexadecimal_KeyUp.bind(this);
			textareaHexadecimal.oninput = this.textareaHexadecimal_Changed.bind(this);
			this.textareaHexadecimal = textareaHexadecimal;
			divSession.appendChild(textareaHexadecimal);

			var textareaASCII = document.createElement("textarea");
			textareaASCII.cols = 20;
			textareaASCII.rows = 10;
			textareaASCII.disabled = true;
			this.textareaASCII = textareaASCII;
			divSession.appendChild(textareaASCII);

			var divFileOperations = document.createElement("div");
		
			var buttonSave = document.createElement("button");
			buttonSave.innerHTML = "Save";
			buttonSave.onclick = this.buttonSave_Clicked.bind(this);
			divFileOperations.appendChild(buttonSave);

			var inputFileToLoad = document.createElement("input");
			inputFileToLoad.type = "file";
			inputFileToLoad.onchange = this.inputFileToLoad_Changed.bind(this);
			divFileOperations.appendChild(inputFileToLoad);

			divSession.appendChild(divFileOperations);

			var divCursor = document.createElement("div");
			
			var labelCursorPosition = document.createElement("label");
			labelCursorPosition.innerHTML = "Cursor Position:";
			divCursor.appendChild(labelCursorPosition);

			var inputCursorPosition = document.createElement("input");
			inputCursorPosition.disabled = true;
			this.inputCursorPosition = inputCursorPosition;
			divCursor.appendChild(inputCursorPosition);

			divSession.appendChild(divCursor);

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

			this.domElement = divSession;
		}

		var bytesAsStringHexadecimal = Converter.bytesToStringHexadecimal
		(
			this.bytes
		);
		this.textareaHexadecimal.value = 
			bytesAsStringHexadecimal + this.finalNibble;

		var bytesAsStringASCII = Converter.bytesToStringASCII
		(
			this.bytes
		);
		this.textareaASCII.value = bytesAsStringASCII;

		var cursorPos = this.textareaHexadecimal.selectionStart;
		var cursorPosAsString = 
			"0d" + cursorPos 
			+ "; 0x" + cursorPos.toString(16)
			+ "; 0b" + cursorPos.toString(2);

		this.inputCursorPosition.value = cursorPosAsString;

		return this.domElement;
	}

	// events

	Session.prototype.buttonSave_Clicked = function()
	{
		var dataAsArrayBuffer = new ArrayBuffer(this.bytes.length);
		var dataAsArrayUnsigned = new Uint8Array(dataAsArrayBuffer);
		for (var i = 0; i < this.bytes.length; i++) 
		{
			dataAsArrayUnsigned[i] = this.bytes[i];
		}
		var dataAsBlob = new Blob([dataAsArrayBuffer], {type:'bytes'});


		var link = document.createElement("a");
		link.href = window.URL.createObjectURL(dataAsBlob);
		link.download = "Data.bin";
		link.click();
	}

	Session.prototype.inputFileToLoad_Changed = function(event)
	{
		var inputFileToLoad = event.target;
		var fileToLoad = inputFileToLoad.files[0];
		if (fileToLoad != null)
		{
	  		var fileReader = new FileReader();
			fileReader.onload = this.inputFileToLoad_Changed_Loaded.bind(this); 
	   		fileReader.readAsBinaryString(fileToLoad);
		}
	}

	Session.prototype.inputFileToLoad_Changed_Loaded = function(fileLoadedEvent) 
	{
		var dataAsBinaryString = fileLoadedEvent.target.result;

		this.bytes = [];

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

		this.domElementUpdate();
	}

	Session.prototype.textareaHexadecimal_Changed = function(event)
	{
		var bytesAsStringHexadecimal = event.target.value;
		this.bytes = Converter.stringHexadecimalToBytes
		(
			bytesAsStringHexadecimal
		);

		if (bytesAsStringHexadecimal.length % 2 == 0)
		{
			this.finalNibble = "";
		}
		else
		{
			this.finalNibble = bytesAsStringHexadecimal.substr
			(
				bytesAsStringHexadecimal.length - 1,
				1
			);

			var finalNibbleAsInt = parseInt(this.finalNibble, 16);
			if (isNaN(finalNibbleAsInt) == true)
			{
				this.finalNibble = "";
			}
		}

		this.domElementUpdate();
	}

	Session.prototype.textareaHexadecimal_KeyUp = function(event)
	{
		if (event.key.indexOf("Arrow") == 0)
		{
			this.domElementUpdate();
		}
	}

}

// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

An Pixel Art Editor in JavaScript

The JavaScript below implements a simple image editor for editing very tiny images pixel-by-pixel. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit thiscouldbebetter.neocities.org/pixelarteditor.html.

pixelarteditor


<html>
<body>

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

<script type="text/javascript">

// main

function main()
{
	var session = new Session
	(
		new Coords(16, 16), // imageSizeInPixelsActual
		16, // magnificationFactor
		// colors
		[
			"Black",
			"White",
			"Red",
			"Orange",
			"Yellow",
			"Green",
			"Blue",
			"Violet",
			"Brown",
			"Gray",	
		]
	);

	session.initialize();
}

// classes

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

}

function Display(sizeInPixels)
{
	this.sizeInPixels = sizeInPixels;
}
{
	Display.prototype.drawImage = function(image)
	{
		this.graphics.drawImage
		(
			image,
			0, 0
		);
	}

	Display.prototype.drawImageStretched = function(image)
	{
		this.graphics.drawImage
		(
			image,
			0, 0,
			this.sizeInPixels.x, this.sizeInPixels.y
		);
	}

	Display.prototype.drawPixel = function(color, pos)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillRect
		(
			pos.x, pos.y, 1, 1
		);
	}

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

	Display.prototype.fillWithColor = function(color)
	{
		this.drawRectangle(color, new Coords(0, 0), this.sizeInPixels);
	}

	Display.prototype.initialize = function(domElementParent)
	{
		this.canvas = document.createElement("canvas");
		this.canvas.style = "border:1px solid;" 
		this.canvas.width = this.sizeInPixels.x;
		this.canvas.height = this.sizeInPixels.y;

		domElementParent.appendChild(this.canvas);
	
		this.graphics = this.canvas.getContext("2d");
		this.graphics.imageSmoothingEnabled = false;

		this.fillWithColor("White");
	}
}

function Session(imageSizeInPixelsActual, magnificationFactor, colors)
{
	this.imageSizeInPixelsActual = imageSizeInPixelsActual;
	this.magnificationFactor = magnificationFactor;
	this.colors = colors;
}
{
	// methods

	Session.prototype.initialize = function()
	{
		this.imageSizeInPixelsMagnified = this.imageSizeInPixelsActual.clone().multiplyScalar
		(
			this.magnificationFactor
		);

		this.cellSizeInPixels = new Coords(1, 1).multiplyScalar
		(
			this.magnificationFactor
		);

		var divMain = document.getElementById("divMain");
		divMain.innerHTML = "";

		var divSize = document.createElement("div");

		var labelWidth = document.createElement("label");
		labelWidth.innerHTML = "Width:";
		divSize.appendChild(labelWidth);

		var inputWidth = document.createElement("input");
		inputWidth.type = "number";
		inputWidth.value = this.imageSizeInPixelsActual.x;
		inputWidth.onchange = this.inputWidth_Changed.bind(this);
		divSize.appendChild(inputWidth);	

		var labelHeight = document.createElement("label");
		labelHeight.innerHTML = "Height:";
		divSize.appendChild(labelHeight);

		var inputHeight = document.createElement("input");
		inputHeight.type = "number";
		inputHeight.value = this.imageSizeInPixelsActual.y;
		inputHeight.onchange = this.inputHeight_Changed.bind(this);
		divSize.appendChild(inputHeight);

		divMain.appendChild(divSize);

		var divImages = document.createElement("div");

		this.displayMagnified = new Display(this.imageSizeInPixelsMagnified);
		this.displayMagnified.initialize(divImages);
		this.displayMagnified.canvas.onmousemove = 
			this.canvasMagnified_MouseMoved.bind(this);
		
		this.displayActualSize = new Display(this.imageSizeInPixelsActual);
		this.displayActualSize.initialize(divImages);

		divMain.appendChild(divImages);

		var divFileOperations = document.createElement("div");

		var inputFileToLoad = document.createElement("input");
		inputFileToLoad.type = "file";
		inputFileToLoad.onchange = this.inputFileToLoad_Changed.bind(this);
		divFileOperations.appendChild(inputFileToLoad);

		var buttonSave = document.createElement("button");
		buttonSave.innerHTML = "Save";
		buttonSave.onclick = this.buttonSave_Clicked.bind(this);
		divFileOperations.appendChild(buttonSave);

		divMain.appendChild(divFileOperations);

		var buttonClear = document.createElement("button");
		buttonClear.innerHTML = "Clear";
		buttonClear.onclick = this.buttonClear_Clicked.bind(this);
		divMain.appendChild(buttonClear);

		this.colorSelected = this.colors[0];

		var divColors = document.createElement("div");

		for (var i = 0; i < this.colors.length; i++)
		{
			var color = this.colors[i];
			var buttonColor = document.createElement("button");
			buttonColor.innerHTML = color;
			buttonColor.style.color = color;
			buttonColor.onclick = this.buttonColor_Clicked.bind(this);
			divColors.appendChild(buttonColor);
		}

		divMain.appendChild(divColors);
	}

	// ui events

	Session.prototype.buttonClear_Clicked = function()
	{
		this.displayMagnified.fillWithColor(this.colorSelected);
		this.displayActualSize.fillWithColor(this.colorSelected);
	}

	Session.prototype.buttonColor_Clicked = function(event)
	{
		var buttonColor = event.target;
		this.colorSelected = buttonColor.innerHTML;
	}

	Session.prototype.buttonSave_Clicked = function()
	{
		var canvas = this.displayActualSize.canvas;

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

		var link = document.createElement("a");
		link.href = window.URL.createObjectURL(imageAsBlob);
		link.download = "Image.png";
		link.click();
	}

	Session.prototype.canvasMagnified_MouseMoved = function(event)
	{
		if (event.buttons == 0)
		{
			return;
		}

		var canvas = event.target;
		var canvasBounds = canvas.getBoundingClientRect();

		var clickPosInPixels = new Coords
		(
			event.clientX - canvasBounds.left, 
			event.clientY - canvasBounds.top
		);

		var clickPosInCells = clickPosInPixels.clone().divide
		(
			this.cellSizeInPixels
		).floor();

		var cellPosInPixels = clickPosInCells.clone().multiply
		(
			this.cellSizeInPixels
		);

		var color = this.colorSelected;

		this.displayMagnified.drawRectangle
		(
			color,
			cellPosInPixels,
			this.cellSizeInPixels
		);

		this.displayActualSize.drawPixel
		(
			color,
			clickPosInCells
		);
	}

	Session.prototype.inputFileToLoad_Changed = function(event)
	{
		var inputFileToLoad = event.target;
		var fileToLoad = inputFileToLoad.files[0];
		if (fileToLoad != null)
		{
			if (fileToLoad.type.match("image.*") != null)
			{
		  		var fileReader = new FileReader();
				fileReader.onload = this.inputFileToLoad_Changed_Loaded.bind(this); 
		   		fileReader.readAsDataURL(fileToLoad);
			}
		}
	}

	Session.prototype.inputFileToLoad_Changed_Loaded = function(fileLoadedEvent) 
	{
		var imageLoaded = document.createElement("img");
		imageLoaded.src = fileLoadedEvent.target.result;

		this.imageSizeInPixelsActual.x = imageLoaded.width;
		this.imageSizeInPixelsActual.y = imageLoaded.height;

		this.initialize();

		this.displayActualSize.drawImage(imageLoaded);
		this.displayMagnified.drawImageStretched(imageLoaded);
	}

	Session.prototype.inputHeight_Changed = function(event)
	{
		var inputHeight = event.target;
		this.imageSizeInPixelsActual.y = inputHeight.value;
		this.initialize();
	}

	Session.prototype.inputWidth_Changed = function(event)
	{
		var inputWidth = event.target;
		this.imageSizeInPixelsActual.x = inputWidth.value;
		this.initialize();
	}


}

// run

main();

</script>

</body>
</html>

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