A Simple Paint Program in JavaScript Using HTML5

The JavaScript code shown below allows the user to paint a simple picture onto an HTML5 canvas and then save it to their local computer.  To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.  Or, to see an online version, visit http://thiscouldbebetter.neocities.org/paint.html.

UPDATE 2014/01/14 – I have updated the code to include a layer tool, and also to bring it somewhat up to date with how I do things nowadays.  As of the present writing, neither the neocities.org version nor the screenshot below have been updated to reflect the latest changes.

UPDATE 2017/03/10 – I have further updated the code in an attempt to better encapsulate some of the DOM-related implementation details. It’s debatable how well I succeeded in doing so, though. Also, I added the ability to move layers up or down.

UPDATE 2017/03/17 – I have further updated the code to implement simple select, copy, cut, and paste functionality, as well as the ability to move layers around.

UPDATE 2017/03/20 – The code has been modified to add enhanced save and load functionality. The image can now be saved as either a flat PNG or a multi-layered TAR file, and it can be loaded from either source. Currently, however, the multi-layer save does not preserve “shifting” of layers, and loading simply loads the layers from the saved file on top of the existing layers in the workspace, using whatever the current canvas size happens to be. The code that performs the actual file processing could probably stand to be cleaned up as well.

PaintProgram


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

function main()
{
	var view = new View
	(
		new Coords(320, 240), // size
		"Paint", // toolNameInitial
		[
			new ToolFile(),
			new ToolViewSize(),
			new ToolBrushSize(),
			new ToolColorPalette(),
			new ToolLayers(),
			new ToolPaint(),
			new ToolFill(),
			new ToolSelect(),
		]
	);

	document.body.appendChild
	(
		view.controlUpdate().domElementUpdate()
	);
}

// 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;
	}
}

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

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

		return returnValue;
	}


	String.prototype.padRight = function(lengthToPadTo, charToPadWith)
	{
		var returnValue = this;

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

		return returnValue;
	}
}

// classes

function Base64Encoder()
{
	// static class
}
{
	// constants

	Base64Encoder.Base64DigitsAsString = 
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ" 
		+ "abcdefghijklmnopqrstuvwxyz"
		+ "0123456789"
		+ "+/";

	// static methods

	Base64Encoder.base64StringToBytes = function(base64StringToConvert)
	{
		// Convert each four sets of six bits (sextets, or Base 64 digits)
		// into three sets of eight bits (octets, or bytes)

		var returnBytes = [];

		var bytesPerSet = 3;
		var base64DigitsPerSet = 4;
		var base64DigitsAll = Base64Encoder.Base64DigitsAsString;

		var indexOfEqualsSign = base64StringToConvert.indexOf("=");

		if (indexOfEqualsSign >= 0)
		{
			base64StringToConvert = base64StringToConvert.substring
			(
				0, 
				indexOfEqualsSign
			);
		}

		var numberOfBase64DigitsToConvert = base64StringToConvert.length;

		var numberOfFullSets = Math.floor
		(
			numberOfBase64DigitsToConvert 
			/ base64DigitsPerSet
		);

		var numberOfBase64DigitsInFullSets = 
			numberOfFullSets * base64DigitsPerSet;

		var numberOfBase64DigitsLeftAtEnd = 
			numberOfBase64DigitsToConvert - numberOfBase64DigitsInFullSets;

		for (var s = 0; s < numberOfFullSets; s++)
		{
			var d = s * base64DigitsPerSet;

			var valueToEncode = 
				(base64DigitsAll.indexOf(base64StringToConvert[d]) << 18)
				| (base64DigitsAll.indexOf(base64StringToConvert[d + 1]) << 12)
				| (base64DigitsAll.indexOf(base64StringToConvert[d + 2]) << 6)
				| (base64DigitsAll.indexOf(base64StringToConvert[d + 3]));

			returnBytes.push((valueToEncode >> 16) & 0xFF);
			returnBytes.push((valueToEncode >> 8) & 0xFF);
			returnBytes.push((valueToEncode) & 0xFF);
		}	

		var d = numberOfFullSets * base64DigitsPerSet;

		if (numberOfBase64DigitsLeftAtEnd > 0)
		{
			var valueToEncode = 0;

			for (var i = 0; i < numberOfBase64DigitsLeftAtEnd; i++)
			{
				var digit = base64StringToConvert[d + i];
				var digitValue = base64DigitsAll.indexOf(digit);
				var bitsToShift = (18 - 6 * i);
				var digitValueShifted = digitValue << bitsToShift;

				valueToEncode = 
					valueToEncode
					| digitValueShifted;
			}


			for (var b = 0; b < numberOfBase64DigitsLeftAtEnd; b++)
			{
				var byteValue = (valueToEncode >> (16 - 8 * b)) & 0xFF;
				if (byteValue > 0)
				{
					returnBytes.push(byteValue);
				}
			}
		}

		return returnBytes;
	}

	Base64Encoder.bytesToBase64String = function(bytesToEncode)
	{
		// Encode each three sets of eight bits (octets, or bytes)
		// as four sets of six bits (sextets, or Base 64 digits)

		var returnString = "";

		var bytesPerSet = 3;
		var base64DigitsPerSet = 4;
		var base64DigitsAsString = Base64Encoder.Base64DigitsAsString;

		var numberOfBytesToEncode = bytesToEncode.length;
		var numberOfFullSets = Math.floor(numberOfBytesToEncode / bytesPerSet);
		var numberOfBytesInFullSets = numberOfFullSets * bytesPerSet;
		var numberOfBytesLeftAtEnd = numberOfBytesToEncode - numberOfBytesInFullSets;

		for (var s = 0; s < numberOfFullSets; s++)
		{
			var b = s * bytesPerSet;

			var valueToEncode = 
				(bytesToEncode[b] << 16)
				| (bytesToEncode[b + 1] << 8)
				| (bytesToEncode[b + 2]);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += base64DigitsAsString[((valueToEncode & 0x000FC0) >>> 6)];
			returnString += base64DigitsAsString[((valueToEncode & 0x00003F))];
		}	

		var b = numberOfFullSets * bytesPerSet;

		if (numberOfBytesLeftAtEnd == 1)
		{
			var valueToEncode = (bytesToEncode[b] << 16);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += "==";
		}		
		else if (numberOfBytesLeftAtEnd == 2)
		{
			var valueToEncode = 
				(bytesToEncode[b] << 16)
				| (bytesToEncode[b + 1] << 8);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += base64DigitsAsString[((valueToEncode & 0x000FC0) >>> 6)];
			returnString += "=";
		}

		return returnString;
	}
}

function ByteStream(bytes)
{
	this.bytes = bytes;  

	this.byteIndexCurrent = 0;
}
{
	// constants

	ByteStream.BitsPerByte = 8;
	ByteStream.BitsPerByteTimesTwo = ByteStream.BitsPerByte * 2;
	ByteStream.BitsPerByteTimesThree = ByteStream.BitsPerByte * 3;

	// instance methods

	ByteStream.prototype.hasMoreBytes = function()
	{
		return (this.byteIndexCurrent < this.bytes.length);
	}
	
	ByteStream.prototype.readBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.readByte();
		}

		return returnValue;
	}

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

		this.byteIndexCurrent++;

		return returnValue;
	}

	ByteStream.prototype.readString = function(lengthOfString)
	{
		var returnValue = "";

		for (var i = 0; i < lengthOfString; i++)
		{
			var byte = this.readByte();

			if (byte != 0)
			{
				var byteAsChar = String.fromCharCode(byte);
				returnValue += byteAsChar;
			}
		}

		return returnValue;
	}

	ByteStream.prototype.writeBytes = function(bytesToWrite)
	{
		for (var b = 0; b < bytesToWrite.length; b++)
		{
			this.bytes.push(bytesToWrite[b]);
		}

		this.byteIndexCurrent = this.bytes.length;
	}

	ByteStream.prototype.writeByte = function(byteToWrite)
	{
		this.bytes.push(byteToWrite);

		this.byteIndexCurrent++;
	}

	ByteStream.prototype.writeString = function(stringToWrite, lengthPadded)
	{	
		for (var i = 0; i < stringToWrite.length; i++)
		{
			var charAsByte = stringToWrite.charCodeAt(i);
			this.writeByte(charAsByte);
		}
		
		var numberOfPaddingChars = lengthPadded - stringToWrite.length;
		for (var i = 0; i < numberOfPaddingChars; i++)
		{
			this.writeByte(0);
		}
	}
}

function Color(name, systemColor)
{
	this.name = name;
	this.systemColor = systemColor;
}
{
	Color.Instances = new Color_Instances();

	function Color_Instances()
	{
		this.Black 	= new Color("Black", "Black");
		this.Blue 	= new Color("Blue", "Blue");
		this.Cyan	= new Color("Cyan", "Cyan");
		this.Gray	= new Color("Gray", "Gray");
		this.GrayDark	= new Color("GrayDark", "DarkGray");
		this.GrayLight 	= new Color("GrayLight", "LightGray");
		this.Green	= new Color("Green", "Green");
		this.Orange	= new Color("Orange", "Orange");
		this.Red 	= new Color("Red", "Red");
		this.Violet	= new Color("Violet", "Violet");
		this.White 	= new Color("White", "White");
		this.Yellow	= new Color("Yellow", "Yellow");

		this._All =
		[
			this.Black,
			this.GrayDark,
			this.Gray,
			this.GrayLight,
			this.White,

			this.Red,
			this.Orange,
			this.Yellow,
			this.Green,
			this.Cyan,
			this.Blue,
			this.Violet
		];

		this._All.addLookups("name");
	}
}

function Control()
{
	// static class
}
{
	Control.controllablesToControls = function(controllables)
	{
		var returnValues = [];

		for (var i = 0; i < controllables.length; i++)
		{
			var controllable = controllables[i];
			var control = controllable.controlUpdate();
			returnValues.push(control);
		}

		return returnValues;
	}
}

function ControlButton(text, click)
{
	this.text = text;
	this.click = click;
}
{
	// dom

	ControlButton.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var returnValue = document.createElement("button");
			returnValue.innerHTML = this.text;
			returnValue.onclick = this.handleEventClick.bind(this);
			this.domElement = returnValue;
		}

		return this.domElement;
	}

	// event handlers

	ControlButton.prototype.handleEventClick = function(event)
	{
		this.click();
	}
}

function ControlCanvas
(
	name, 
	size, 
	layers, 
	mousedown, 
	mousemove, 
	mouseout, 
	mouseover, 
	mouseup
)
{
	this.name = name;
	this.size = size;
	this.layers = layers;

	this.mousedown = mousedown;
	this.mousemove = mousemove;
	this.mouseout = mouseout;
	this.mouseover = mouseover;
	this.mouseup = mouseup;
}
{
	// dom
	
	ControlCanvas.prototype.domElementUpdate = function()
	{
		if (this.display == null)
		{
			this.display = new Display(this.size);
			this.display.initialize();
			this.domElement = this.display.canvas;

			// hack
			this.domElement.onmousedown = this.handleEventMouseDown.bind(this);
			this.domElement.onmousemove = this.handleEventMouseMove.bind(this);
			this.domElement.onmouseout = this.handleEventMouseOut.bind(this);
			this.domElement.onmouseover = this.handleEventMouseOver.bind(this);
			this.domElement.onmouseup = this.handleEventMouseUp.bind(this);
		}

		this.display.clear();

		for (var i = 0; i < this.layers.length; i++)
		{
			var layer = this.layers[i];
			if (layer.isVisible == true)
			{
				this.display.drawOther
				(
					layer.display, 
					layer.offset
				);
			}
		}

		return this.domElement;
	}

	// events

	ControlCanvas.prototype.handleEventMouseDown = function(event)
	{
		if (this.mousedown != null)
		{
			this.mousedown(event);
		}
	}

	ControlCanvas.prototype.handleEventMouseMove = function(event)
	{
		if (this.mousemove != null)
		{
			this.mousemove(event);
		}
	}

	ControlCanvas.prototype.handleEventMouseOut = function(event)
	{
		if (this.mouseout != null)
		{
			this.mouseout(event);
		}
	}

	ControlCanvas.prototype.handleEventMouseOver = function(event)
	{
		if (this.mouseover != null)
		{
			this.mouseover(event);
		}
	}

	ControlCanvas.prototype.handleEventMouseUp = function(event)
	{
		if (this.mouseup != null)
		{
			this.mouseup(event);
		}
	}

}

function ControlContainer(name, children)
{
	this.name = name;
	this.children = children.addLookups("name");
}
{
	// dom

	ControlContainer.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var returnValue = document.createElement("div");
			returnValue.id = this.name;

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

				returnValue.appendChild(childAsDOMElement);
			}

			this.domElement = returnValue;
		}

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

function ControlFileUploader(name, change)
{
	this.name = name;
	this.change = change;
}
{
	ControlFileUploader.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var returnValue = document.createElement("input");
			returnValue.type = "file";
			returnValue.id = this.name;
			returnValue.onchange = this.handleEventChanged.bind(this);

			this.domElement = returnValue;
		}

		return this.domElement;
	}

	ControlFileUploader.prototype.handleEventChanged = function(event)
	{
		var fileToLoad = event.target.files[0];

		if (fileToLoad == null)
		{
			if (this.change != null)
			{
				this.change(null);
			}
		}
		else
		{
			var fileReader = new FileReader();
			fileReader.file = fileToLoad;
			fileReader.onload = this.handleEventChanged_FileLoaded.bind(this);
			fileReader.readAsBinaryString(fileToLoad);
		}
	}

	ControlFileUploader.prototype.handleEventChanged_FileLoaded = function(event)
	{
		var fileReader = event.target;
		var file = fileReader.file;
		var fileName = file.name;
		var fileType = file.type;
		var fileContentAsBinaryString = fileReader.result;

		var fileContentAsBytes = FileHelper.binaryStringToBytes
		(	
			fileContentAsBinaryString
		);

		if (this.change != null)
		{
			this.change(fileName, fileType, fileContentAsBytes);
		}

	}
}

function ControlLabel(text)
{
	this.text = text;
}
{
	// dom

	ControlLabel.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var returnValue = document.createElement("label");
			returnValue.innerHTML = this.text;

			this.domElement = returnValue;
		}

		return this.domElement;
	}
}


function ControlNumberBox(name, value, change)
{
	this.name = name;
	this.value = value;
	this.change = change;
}
{
	// dom

	ControlNumberBox.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var returnValue = document.createElement("input");
			returnValue.type = "number"
			returnValue.id = this.name;
			returnValue.style.width = "64px"; // hack
			returnValue.onchange = this.handleEventChanged.bind(this);
			returnValue.value = this.value;

			this.domElement = returnValue;
		}

		return this.domElement;
	}

	// events

	ControlNumberBox.prototype.handleEventChanged = function(event)
	{
		var valueAsInt = parseInt(event.target.value);
		this.value = valueAsInt;

		if (this.change != null)
		{
			this.change(event.target.value);
		}
	}
}

function ControlSelectBox(name, options, optionValueFieldName, change)
{
	this.name = name;
	this.options = options;
	this.optionValueFieldName = optionValueFieldName;
	this.change = change;

	this.selectedIndex = null;
}
{
	// dom

	ControlSelectBox.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var returnValue = document.createElement("select");
			returnValue.id = this.name;
			returnValue.onchange = this.handleEventValueChanged.bind(this);

			this.domElement = returnValue;

			this.domElementUpdate_Options();
		}

		if (this.selectedIndex != null)
		{
			this.domElement.selectedIndex = this.selectedIndex;
		}

		return this.domElement;
	}

	ControlSelectBox.prototype.domElementUpdate_Options = function()
	{
		this.domElement.innerHTML = "";

		for (var i = 0; i < this.options.length; i++)
		{
			var option = this.options[i];
			var optionValue = option[this.optionValueFieldName];
			var optionText = optionValue; // todo
			var optionAsDOMElement = document.createElement("option");
			optionAsDOMElement.innerHTML = optionText;
			this.domElement.appendChild(optionAsDOMElement);
		}
	}


	// event handlers

	ControlSelectBox.prototype.handleEventValueChanged = function(event)
	{
		var valueToSet = event.target.value;

		if (this.change != null)
		{
			this.change(valueToSet);
		}
	}
	
}

function ControlTextBox(name, value, change)
{
	this.name = name;
	this.value = value;
	this.change = change;
}
{
	// dom

	ControlTextBox.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var returnValue = document.createElement("input");
			returnValue.id = this.name;
			returnValue.value = this.value;
			returnValue.onchange = this.handleEventValueChanged.bind(this);

			this.domElement = returnValue;
		}

		return this.domElement;
	}

	ControlTextBox.prototype.handleEventValueChanged = function(event)
	{
		this.value = event.target.value;

		if (this.change != null)
		{
			this.change(event.target.value);
		}
	}
}

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

	Coords.Instances = new Coords_Instances();

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

	// methods

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

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

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

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

	Coords.prototype.isInRangeMinMax = function(min, max)
	{
		var returnValue = 
		(
			this.x >= min.x
			&& this.x <= max.x
			&& this.y >= min.y
			&& this.y >= max.y
		);

		return returnValue;
	}

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

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

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

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

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

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

function Display(size)
{
	this.size = size;
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		canvas.style.cursor = "crosshair";
		canvas.style.backgroundImage = 
			"url('" + View.imageURLForAlphaZeroBuild() + "')";

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

	Display.prototype.sizeSet = function(sizeToSet)
	{
		this.size = sizeToSet;

		var canvasNew = document.createElement("canvas");
		canvasNew.width = this.size.x;
		canvasNew.height = this.size.y;

		var graphicsNew = canvasNew.getContext("2d");
		graphicsNew.drawImage(this.canvas, 0, 0);

		this.canvas.width = canvasNew.width;
		this.canvas.height = canvasNew.height;

		this.graphics.drawImage(canvasNew, 0, 0);
	}

	// drawing

	Display.prototype.clear = function()
	{
		this.clearRectangle(Coords.Instances.Zeroes, this.size);
	}

	Display.prototype.clearRectangle = function(pos, size)
	{
		this.graphics.clearRect
		(
			pos.x, pos.y, size.x, size.y
		);
	}

	Display.prototype.drawOther = function(other, pos, sourcePos, sourceSize)
	{
		if (sourcePos == null)
		{
			this.graphics.drawImage(other.canvas, pos.x, pos.y);
		}
		else
		{
			this.graphics.drawImage
			(
				other.canvas,
				sourcePos.x, sourcePos.y,
				sourceSize.x, sourceSize.y,
				pos.x, pos.y,
				sourceSize.x, sourceSize.y // target size
			);
		}
	}

	Display.prototype.drawLine = function(startPos, endPos, color, width)
	{
		var graphics = this.graphics;

		graphics.strokeStyle = color.systemColor;
		graphics.lineWidth = width;
		graphics.lineCap = "round";
		graphics.beginPath();
		graphics.moveTo(startPos.x, startPos.y);
		graphics.lineTo(endPos.x, endPos.y);
		graphics.stroke();
	}

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

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder.systemColor;
			this.graphics.strokeRect(pos.x, pos.y, size.x, size.y);
		}
	}
}

function FileHelper()
{
	// static class
}
{
	FileHelper.binaryStringToBytes = function(binaryString)
	{
		var returnValues = [];

		for (var i = 0; i < binaryString.length; i++)
		{
			var byte = binaryString.charCodeAt(i);
			returnValues.push(byte);
		}

		return returnValues;
	}

	FileHelper.destroyClickedElement = function(event)
	{
		document.body.removeChild(event.target);
	}

	FileHelper.loadFileAsBinaryString = function(fileToLoad, contextForCallback, callback)
	{	
		var fileReader = new FileReader();
		fileReader.onloadend = function(fileLoadedEvent)
		{
			var returnValue = null;

			if (fileLoadedEvent.target.readyState == FileReader.DONE)
			{
				returnValue = fileLoadedEvent.target.result;
			}

			callback.call
			(
				contextForCallback, 
				fileToLoad,
				returnValue
			);
		}

		fileReader.readAsBinaryString(fileToLoad);
	}

	FileHelper.saveBytesAsFile = 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 downloadLink = document.createElement("a");
		downloadLink.download = fileNameToSaveAs;
		downloadLink.innerHTML = "Download File";
		downloadLink.href = window.URL.createObjectURL(bytesToWriteAsBlob);
		downloadLink.onclick = FileHelper.destroyClickedElement;
		downloadLink.style.display = "none";
		document.body.appendChild(downloadLink);	
		downloadLink.click();
	}
}

function Layer(name, size, offset)
{
	this.name = name;
	this.size = size;
	this.offset = offset;
	this.isVisible = true;

	this.display = new Display(size);
	this.display.initialize();
}

function Selection(pos, size)
{
	this.pos = pos;
	this.size = size;
	this._max = new Coords();

	this.color = Color.Instances.Cyan; // todo
}
{
	Selection.prototype.drawToDisplay = function(display)
	{
		display.drawRectangle(this.pos, this.size, null, this.color);
	}

	Selection.prototype.max = function()
	{
		return this._max.overwriteWith(this.pos).add(this.size);
	}	
}

function TarFile(fileName, entries)
{
	this.fileName = fileName;
	this.entries = entries;
}
{
	// constants

	TarFile.ChunkSize = 512;

	// static methods

	TarFile.fromBytes = function(fileName, bytes)
	{
		var reader = new ByteStream(bytes);

		var entries = [];

		var chunkSize = TarFile.ChunkSize;

		var numberOfConsecutiveZeroChunks = 0;

		while (reader.hasMoreBytes() == true)
		{
			var chunkAsBytes = reader.readBytes(chunkSize);

			var areAllBytesInChunkZeroes = true;

			for (var b = 0; b < chunkAsBytes.length; b++)
			{
				if (chunkAsBytes[b] != 0)
				{
					areAllBytesInChunkZeroes = false;
					break;
				}
			}

			if (areAllBytesInChunkZeroes == true)
			{
				numberOfConsecutiveZeroChunks++;

				if (numberOfConsecutiveZeroChunks == 2)
				{
					break;
				}
			}
			else
			{
				numberOfConsecutiveZeroChunks = 0;

				var entry = TarFileEntry.fromBytes(chunkAsBytes, reader);

				entries.push(entry);
			}
		}

		var returnValue = new TarFile
		(
			fileName,
			entries
		);

		return returnValue;
	}
	
	TarFile.new = function(fileName)
	{
		return new TarFile
		(
			fileName,
			[] // entries
		);
	}

	// instance methods
	
	TarFile.prototype.downloadAs = function(fileNameToSaveAs)
	{	
		FileHelper.saveBytesAsFile
		(
			this.toBytes(),
			fileNameToSaveAs
		)
	}	
	
	TarFile.prototype.entriesForDirectories = function()
	{
		var returnValues = [];
		
		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			if (entry.header.typeFlag.name == "Directory")
			{
				returnValues.push(entry);
			}
		}
		
		return returnValues;
	}
	
	TarFile.prototype.toBytes = function()
	{
		var fileAsBytes = [];		

		// hack - For easier debugging.
		var entriesAsByteArrays = [];
		
		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			var entryAsBytes = entry.toBytes();
			entriesAsByteArrays.push(entryAsBytes);
		}		
		
		for (var i = 0; i < entriesAsByteArrays.length; i++)
		{
			var entryAsBytes = entriesAsByteArrays[i];
			fileAsBytes = fileAsBytes.concat(entryAsBytes);
		}
		
		var chunkSize = TarFile.ChunkSize;
		
		var numberOfZeroChunksToWrite = 2;
		
		for (var i = 0; i < numberOfZeroChunksToWrite; i++)
		{
			for (var b = 0; b < chunkSize; b++)
			{
				fileAsBytes.push(0);
			}
		}

		return fileAsBytes;
	}
	
	// strings

	TarFile.prototype.toString = function()
	{
		var newline = "\n";

		var returnValue = "[TarFile]" + newline;

		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			var entryAsString = entry.toString();
			returnValue += entryAsString;
		}

		returnValue += "[/TarFile]" + newline;

		return returnValue;
	}
}

function TarFileEntry(header, dataAsBytes)
{
	this.header = header;
	this.dataAsBytes = dataAsBytes;
}
{
	// methods
	
	// static methods
	
	TarFileEntry.directoryNew = function(directoryName)
	{
		var header = new TarFileEntryHeader.directoryNew(directoryName);
		
		var entry = new TarFileEntry(header, []);
		
		return entry;
	}
	
	TarFileEntry.fileNew = function(fileName, fileContentsAsBytes)
	{
		var header = new TarFileEntryHeader.fileNew(fileName, fileContentsAsBytes);
		
		var entry = new TarFileEntry(header, fileContentsAsBytes);
		
		return entry;
	}
	
	TarFileEntry.fromBytes = function(chunkAsBytes, reader)
	{
		var chunkSize = TarFile.ChunkSize;
	
		var header = TarFileEntryHeader.fromBytes
		(
			chunkAsBytes
		);
	
		var sizeOfDataEntryInBytesUnpadded = header.fileSizeInBytes;	

		var numberOfChunksOccupiedByDataEntry = Math.ceil
		(
			sizeOfDataEntryInBytesUnpadded / chunkSize
		)
	
		var sizeOfDataEntryInBytesPadded = 
			numberOfChunksOccupiedByDataEntry
			* chunkSize;
	
		var dataAsBytes = reader.readBytes
		(
			sizeOfDataEntryInBytesPadded
		).slice
		(
			0, sizeOfDataEntryInBytesUnpadded
		);
	
		var entry = new TarFileEntry(header, dataAsBytes);
		
		return entry;
	}
	
	TarFileEntry.manyFromByteArrays = function
	(
		fileNamePrefix, fileNameSuffix, entriesAsByteArrays
	)
	{
		var returnValues = [];
		
		for (var i = 0; i < entriesAsByteArrays.length; i++)
		{
			var entryAsBytes = entriesAsByteArrays[i];
			var entry = TarFileEntry.fileNew
			(		
				fileNamePrefix + i + fileNameSuffix,
				entryAsBytes
			);
			
			returnValues.push(entry);
		}
		
		return returnValues;
	}
	
	// instance methods

	TarFileEntry.prototype.download = function(event)
	{
		FileHelper.saveBytesAsFile
		(
			this.dataAsBytes,
			this.header.fileName
		);
	}
	
	TarFileEntry.prototype.remove = function(event)
	{
		alert("Not yet implemented!"); // todo
	}
	
	TarFileEntry.prototype.toBytes = function()
	{
		var entryAsBytes = [];
	
		var chunkSize = TarFile.ChunkSize;
	
		var headerAsBytes = this.header.toBytes();
		entryAsBytes = entryAsBytes.concat(headerAsBytes);
		
		entryAsBytes = entryAsBytes.concat(this.dataAsBytes);

		var sizeOfDataEntryInBytesUnpadded = this.header.fileSizeInBytes;	

		var numberOfChunksOccupiedByDataEntry = Math.ceil
		(
			sizeOfDataEntryInBytesUnpadded / chunkSize
		)
	
		var sizeOfDataEntryInBytesPadded = 
			numberOfChunksOccupiedByDataEntry
			* chunkSize;
			
		var numberOfBytesOfPadding = 
			sizeOfDataEntryInBytesPadded - sizeOfDataEntryInBytesUnpadded;
	
		for (var i = 0; i < numberOfBytesOfPadding; i++)
		{
			entryAsBytes.push(0);
		}
		
		return entryAsBytes;
	}	
		
	// strings
	
	TarFileEntry.prototype.toString = function()
	{
		var newline = "\n";

		headerAsString = this.header.toString();

		var dataAsHexadecimalString = ByteHelper.bytesToStringHexadecimal
		(
			this.dataAsBytes
		);

		var returnValue = 
			"[TarFileEntry]" + newline
			+ headerAsString
			+ "[Data]"
			+ dataAsHexadecimalString
			+ "[/Data]" + newline
			+ "[/TarFileEntry]"
			+ newline;

		return returnValue
	}
	
}

function TarFileEntryHeader
(
	fileName,
	fileMode,
	userIDOfOwner,
	userIDOfGroup,
	fileSizeInBytes,
	timeModifiedInUnixFormat,
	checksum,
	typeFlag,
	nameOfLinkedFile,
	uStarIndicator,
	uStarVersion,
	userNameOfOwner,
	groupNameOfOwner,
	deviceNumberMajor,
	deviceNumberMinor,
	filenamePrefix
)
{
	this.fileName = fileName;
	this.fileMode = fileMode;
	this.userIDOfOwner = userIDOfOwner;
	this.userIDOfGroup = userIDOfGroup;
	this.fileSizeInBytes = fileSizeInBytes;
	this.timeModifiedInUnixFormat = timeModifiedInUnixFormat;
	this.checksum = checksum;
	this.typeFlag = typeFlag;
	this.nameOfLinkedFile = nameOfLinkedFile;
	this.uStarIndicator = uStarIndicator;
	this.uStarVersion = uStarVersion;
	this.userNameOfOwner = userNameOfOwner;
	this.groupNameOfOwner = groupNameOfOwner;
	this.deviceNumberMajor = deviceNumberMajor;
	this.deviceNumberMinor = deviceNumberMinor;
	this.filenamePrefix = filenamePrefix;
}
{
	TarFileEntryHeader.SizeInBytes = 500;

	// static methods
	
	TarFileEntryHeader.default = function()
	{
		var returnValue = new TarFileEntryHeader
		(
			"".padRight(100, "\0"), // fileName
			"100777 \0", // fileMode
			"0 \0".padLeft(8, " "), // userIDOfOwner
			"0 \0".padLeft(8, " "), // userIDOfGroup
			0, // fileSizeInBytes
			[49, 50, 55, 50, 49, 49, 48, 55, 53, 55, 52, 32], // hack - timeModifiedInUnixFormat
			0, // checksum
			TarFileTypeFlag.Instances.Normal,		
			"".padRight(100, "\0"), // nameOfLinkedFile,
			"".padRight(6, "\0"), // uStarIndicator,
			"".padRight(2, "\0"), // uStarVersion,
			"".padRight(32, "\0"), // userNameOfOwner,
			"".padRight(32, "\0"), // groupNameOfOwner,
			"".padRight(8, "\0"), // deviceNumberMajor,
			"".padRight(8, "\0"), // deviceNumberMinor,
			"".padRight(155, "\0") // filenamePrefix	
		);		
		
		return returnValue;
	}
	
	TarFileEntryHeader.directoryNew = function(directoryName)
	{
		var header = TarFileEntryHeader.default();
		header.fileName = directoryName;
		header.typeFlag = TarFileTypeFlag.Instances.Directory;
		header.fileSizeInBytes = 0;
		header.checksumCalculate();
		
		return header;
	}
	
	TarFileEntryHeader.fileNew = function(fileName, fileContentsAsBytes)
	{
		var header = TarFileEntryHeader.default();
		header.fileName = fileName;
		header.typeFlag = TarFileTypeFlag.Instances.Normal;
		header.fileSizeInBytes = fileContentsAsBytes.length;
		header.checksumCalculate();
		
		return header;
	}

	TarFileEntryHeader.fromBytes = function(bytes)
	{
		var reader = new ByteStream(bytes);

		var fileName = reader.readString(100).trim();
		var fileMode = reader.readString(8);
		var userIDOfOwner = reader.readString(8);
		var userIDOfGroup = reader.readString(8);
		var fileSizeInBytesAsStringOctal = reader.readString(12);
		var timeModifiedInUnixFormat = reader.readBytes(12);
		var checksumAsStringOctal = reader.readString(8);
		var typeFlagValue = reader.readString(1);
		var nameOfLinkedFile = reader.readString(100);
		var uStarIndicator = reader.readString(6);
		var uStarVersion = reader.readString(2);
		var userNameOfOwner = reader.readString(32);
		var groupNameOfOwner = reader.readString(32);
		var deviceNumberMajor = reader.readString(8);
		var deviceNumberMinor = reader.readString(8);
		var filenamePrefix = reader.readString(155);
		var reserved = reader.readBytes(12);

		var fileSizeInBytes = parseInt
		(
			fileSizeInBytesAsStringOctal.trim(), 8
		);
		
		var checksum = parseInt
		(
			checksumAsStringOctal, 8
		);		
		
		var typeFlags = TarFileTypeFlag.Instances._All;
		var typeFlagID = "_" + typeFlagValue;
		var typeFlag = typeFlags[typeFlagID];

		var returnValue = new TarFileEntryHeader
		(
			fileName,
			fileMode,
			userIDOfOwner,
			userIDOfGroup,
			fileSizeInBytes,
			timeModifiedInUnixFormat,
			checksum,
			typeFlag,
			nameOfLinkedFile,
			uStarIndicator,
			uStarVersion,
			userNameOfOwner,
			groupNameOfOwner,
			deviceNumberMajor,
			deviceNumberMinor,
			filenamePrefix
		);

		return returnValue;
	}

	// instance methods
	
	TarFileEntryHeader.prototype.checksumCalculate = function()
	{	
		var thisAsBytes = this.toBytes();
	
		// The checksum is the sum of all bytes in the header,
		// except we obviously can't include the checksum itself.
		// So it's assumed that all 8 of checksum's bytes are spaces (0x20=32).
		// So we need to set this manually.
						
		var offsetOfChecksumInBytes = 148;
		var numberOfBytesInChecksum = 8;
		var presumedValueOfEachChecksumByte = " ".charCodeAt(0);
		for (var i = 0; i < numberOfBytesInChecksum; i++)
		{
			var offsetOfByte = offsetOfChecksumInBytes + i;
			thisAsBytes[offsetOfByte] = presumedValueOfEachChecksumByte;
		}
		
		var checksumSoFar = 0;

		for (var i = 0; i < thisAsBytes.length; i++)
		{
			var byteToAdd = thisAsBytes[i];
			checksumSoFar += byteToAdd;
		}		

		this.checksum = checksumSoFar;
		
		return this.checksum;
	}
	
	TarFileEntryHeader.prototype.toBytes = function()
	{
		var headerAsBytes = [];
		var writer = new ByteStream(headerAsBytes);
		
		var fileSizeInBytesAsStringOctal = (this.fileSizeInBytes.toString(8) + " ").padLeft(12, " ")
		var checksumAsStringOctal = (this.checksum.toString(8) + " \0").padLeft(8, " ");

		writer.writeString(this.fileName, 100);
		writer.writeString(this.fileMode, 8);
		writer.writeString(this.userIDOfOwner, 8);
		writer.writeString(this.userIDOfGroup, 8);
		writer.writeString(fileSizeInBytesAsStringOctal, 12);
		writer.writeBytes(this.timeModifiedInUnixFormat);
		writer.writeString(checksumAsStringOctal, 8);
		writer.writeString(this.typeFlag.value, 1);		
		writer.writeString(this.nameOfLinkedFile, 100);
		writer.writeString(this.uStarIndicator, 6);
		writer.writeString(this.uStarVersion, 2);
		writer.writeString(this.userNameOfOwner, 32);
		writer.writeString(this.groupNameOfOwner, 32);
		writer.writeString(this.deviceNumberMajor, 8);
		writer.writeString(this.deviceNumberMinor, 8);
		writer.writeString(this.filenamePrefix, 155);
		writer.writeString("".padRight(12, "\0")); // reserved

		return headerAsBytes;
	}		
		
	// strings

	TarFileEntryHeader.prototype.toString = function()
	{		
		var newline = "\n";
	
		var returnValue = 
			"[TarFileEntryHeader "
			+ "fileName='" + this.fileName + "' "
			+ "typeFlag='" + (this.typeFlag == null ? "err" : this.typeFlag.name) + "' "
			+ "fileSizeInBytes='" + this.fileSizeInBytes + "' "
			+ "]"
			+ newline;

		return returnValue;
	}
}	

function TarFileTypeFlag(value, name)
{
	this.value = value;
	this.id = "_" + this.value;
	this.name = name;
}
{
	TarFileTypeFlag.Instances = new TarFileTypeFlag_Instances();

	function TarFileTypeFlag_Instances()
	{
		this.Normal 		= new TarFileTypeFlag("0", "Normal");
		this.HardLink 		= new TarFileTypeFlag("1", "Hard Link");
		this.SymbolicLink 	= new TarFileTypeFlag("2", "Symbolic Link");
		this.CharacterSpecial 	= new TarFileTypeFlag("3", "Character Special");
		this.BlockSpecial 	= new TarFileTypeFlag("4", "Block Special");
		this.Directory		= new TarFileTypeFlag("5", "Directory");
		this.FIFO		= new TarFileTypeFlag("6", "FIFO");
		this.ContiguousFile 	= new TarFileTypeFlag("7", "Contiguous File");

		// Additional types not implemented:
		// 'g' - global extended header with meta data (POSIX.1-2001)
		// 'x' - extended header with meta data for the next file in the archive (POSIX.1-2001)
		// 'A'–'Z' - Vendor specific extensions (POSIX.1-1988)
		// [other values] - reserved for future standardization

		this._All = 
		[
			this.Normal,
			this.HardLink,
			this.SymbolicLink,
			this.CharacterSpecial,
			this.BlockSpecial,
			this.Directory,
			this.FIFO,
			this.ContiguousFile,
		];

		for (var i = 0; i < this._All.length; i++)
		{
			var item = this._All[i];
			this._All[item.id] = item;
		}
	}
}	

function ToolBrushSize()
{
	this.name = "BrushSize";
	this.brushSizeSelected = 1;
}
{
	ToolBrushSize.prototype.brushSizeSet = function(valueToSet)
	{
		this.brushSizeSelected = valueToSet;
	}

	ToolBrushSize.prototype.controlUpdate = function()
	{		
		if (this.control == null)
		{
			var returnValue = new ControlContainer
			(
				"containerToolBrushSize",
				[
					new ControlLabel("Brush Size:"),
					new ControlNumberBox
					(
						null, // id
						this.brushSizeSelected, // value
						this.brushSizeSet.bind(this)
					),
				]
			);

			this.control = returnValue;
		}

		return this.control;	
	}
}

function ToolColorPalette()
{
	this.name = "ColorPalette";
	this.colorSelected = Color.Instances.Black;
}
{
	// event handlers

	ToolColorPalette.prototype.colorSetByName = function(colorName)
	{
		this.colorSelected = Color.Instances._All[colorName];
	}

	// controllable

	ToolColorPalette.prototype.controlUpdate = function()
	{
		if (this.control == null)
		{
			var colors = Color.Instances._All;

			var returnValue = new ControlContainer
			(
				"containerToolColorPalette",
				[
					new ControlLabel("Color:"),
					new ControlSelectBox
					(
						"selectColor",
						colors,
						"name",
						this.colorSetByName.bind(this)
					),
				]
			);

			this.control = returnValue;
		}

		return this.control;
	}
}

function ToolFile()
{
	this.name = "FileSave";
	this.fileNameToSaveAs = "Untitled";
}
{
	// event handlers

	ToolFile.prototype.processFileNameToSaveAsChange = function(event)
	{
		this.fileNameToSaveAs = event.target.value;
	}

	ToolFile.prototype.processSaveAsPNG = function(event)
	{
		var canvas = this.parentView.control.children["viewCanvas"].display.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 fileNameToSaveAs = this.fileNameToSaveAs;
		if (fileNameToSaveAs.toLowerCase().endsWith(".png") == false)
		{
			fileNameToSaveAs += ".png";
		}

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

	ToolFile.prototype.processSaveAsTAR = function(event)
	{
		var fileNameToSaveAs = this.fileNameToSaveAs;
		if (fileNameToSaveAs.toLowerCase().endsWith(".tar") == false)
		{
			fileNameToSaveAs += ".tar";
		}

		var layersAsTarFile = TarFile.new(fileNameToSaveAs);

		var layers = this.parentView.layers;

		for (var i = 0; i < layers.length; i++)
		{
			var layer = layers[i];

			var canvas = layer.display.canvas;

			var layerAsDataURL = canvas.toDataURL("image/png");

			var layerAsBase64String = layerAsDataURL.split(',')[1];

			var layerAsBytes = Base64Encoder.base64StringToBytes
			(
				layerAsBase64String
			);

			var layerAsTarFileEntry = TarFileEntry.fileNew
			(
				layer.name + ".png",
				layerAsBytes
			);
			layersAsTarFile.entries.push(layerAsTarFileEntry);
		}

		layersAsTarFile.downloadAs(fileNameToSaveAs);
	}

	// controllable

	ToolFile.prototype.controlUpdate = function()
	{
		if (this.control == null)
		{
			var containerSave = new ControlContainer
			(
				"containerSave",
				[
					new ControlLabel("Save As:"),

					new ControlTextBox
					(
						"textFileName", // id
						"Untitled",
						this.processFileNameToSaveAsChange.bind(this)
					),
					new ControlButton
					(
						"As PNG",
						this.processSaveAsPNG.bind(this)
					),

					new ControlButton
					(
						"As TAR",
						this.processSaveAsTAR.bind(this)
					),
				]
			);

			var containerLoad = new ControlContainer
			(
				"containerLoad",
				[
					new ControlLabel("Load:"),
					new ControlFileUploader
					(
						"fileToLoad", 
						this.fileToUploadChanged.bind(this)
					),
				]
			);

			var returnValue = new ControlContainer
			(
				"containerToolFile",
				[
					containerSave,
					containerLoad,
				]
			);

			this.control = returnValue;
		}

		return this.control;
	}

	ToolFile.prototype.fileToUploadChanged = function(fileName, fileType, fileContentAsBytes)
	{
		if (fileContentAsBytes != null)
		{
			if (fileType == "image/png")
			{
				this.layerAddFromPNGAsBytes(fileContentAsBytes);
			}
			else if (fileType == "application/x-tar")
			{
				var tarFile = TarFile.fromBytes(fileName, fileContentAsBytes);
				var tarFileEntries = tarFile.entries;
				for (var i = 0; i < tarFileEntries.length; i++)
				{
					var entry = tarFileEntries[i];
					var entryDataAsBytes = entry.dataAsBytes;
	
					this.layerAddFromPNGAsBytes(entryDataAsBytes);
				}
			}
			else
			{
				alert("Unrecognized file type!");
			}
		}

	}

	ToolFile.prototype.layerAddFromPNGAsBytes = function(fileContentAsBytes)
	{
		var fileContentAsBase64 = 
			Base64Encoder.bytesToBase64String(fileContentAsBytes);
							
		var dataURL = "data:image/png;base64," + fileContentAsBase64;

		var imageLoadedAsImgElement = document.createElement("img");
		imageLoadedAsImgElement.src = dataURL;
		var imageSize = new Coords
		(
			imageLoadedAsImgElement.width,
			imageLoadedAsImgElement.height
		);
		var imageLoadedAsDisplay = new Display(imageSize);
		imageLoadedAsDisplay.initialize();
		var graphics = imageLoadedAsDisplay.canvas.getContext("2d");
		graphics.drawImage(imageLoadedAsImgElement, 0, 0);

		var view = this.parentView;
		var toolLayers = view.tools["Layers"];
		toolLayers.layerAdd();
		var layerNew = view.layerSelected();
		layerNew.display.drawOther
		(
			imageLoadedAsDisplay, new Coords(0, 0)
		);
		view.controlUpdate();
	}
}

function ToolFill()
{
	this.name = "Fill";
}
{
	// event handlers

	ToolFill.prototype.processSelection = function()
	{
		var tools = this.parentView.tools;

		var layerSelected = this.parentView.layerSelected();
		var display = layerSelected.display;
		var color = tools["ColorPalette"].colorSelected;
		var size = this.parentView.size;

		display.drawRectangle(Coords.Instances.Zeroes, size, color);

		this.parentView.controlUpdate();

	}

	// controllable

	ToolFill.prototype.controlUpdate = function()
	{
		if (this.control == null)
		{
			var returnValue = new ControlContainer
			(
				"containerFill",
				[
					new ControlButton
					(
						this.name,
						this.processSelection.bind(this)
 					)
				]
			);

			this.control = returnValue;	
		}

		return this.control;
	}
}


function ToolLayers()
{
	this.name = "Layers";
	this.layerIndexSelected = 0;
	this.moveStepDistance = 10;
}
{
	// event handlers

	ToolLayers.prototype.layerAdd = function()
	{
		var layersAll = this.parentView.layers;
		var layerNewIndex = layersAll.length;
		var layerNew = new Layer
		(
			"Layer" + layerNewIndex, 
			this.parentView.size.clone(),
			new Coords(0, 0) // offset
		);
		layerNew.parentView = this.parentView;
		layersAll.push(layerNew);
		layersAll[layerNew.name] = layerNew;

		this.layerIndexSelected = layerNewIndex;

		this.controlUpdate();
	}

	ToolLayers.prototype.layerClone = function()
	{
		var layerToClone = this.parentView.layerSelected();	
		var layersAll = this.parentView.layers;
		var layerClonedName = layerToClone + "_Clone";
		var layerNew = new Layer
		(
			layerClonedName,
			layerToClone.size.clone()
		);
		layerCloned.parentView = this.parentView;
		var layerClonedIndex = layersAll.indexOf(layerToClone) + 1;
		layersAll.splice(layerClonedIndex, 0, layerCloned);
		layersAll[layerCloned.name] = layerCloned;

		this.layerIndexSelected = layerClonedIndex;

		this.controlUpdate();
	}

	ToolLayers.prototype.layerSelectedHideOrShow = function()
	{
		var layerSelected = this.parentView.layerSelected();
		layerSelected.isVisible = (layerSelected.isVisible == false);
		this.controlUpdate();
		this.parentView.controlUpdate();
	}

	ToolLayers.prototype.layerSelectedLower = function(offset)
	{
		this.layerSelectedRaiseOrLower(-1);
	}

	ToolLayers.prototype.layerSelectedMergeDown = function()
	{
		var layerSelected = this.parentView.layerSelected();
		var layersAll = this.parentView.layers;
		var layerSelectedIndex = layersAll.indexOf(layerSelected);
		if (layerSelectedIndex > 0)
		{
			var layerUnderneathIndex = layerSelectedIndex - 1;
			var layerUnderneath = layersAll[layerUnderneathIndex];
			layerUnderneath.display.drawOther
			(
				layerSelected.display,
				layerSelected.offset.clone().subtract
				(
					layerUnderneath.offset
				)
			);
			this.layerSelectedRemove();
		}
	}

	ToolLayers.prototype.layerSelectedMove = function(direction)
	{
		var offset = direction.multiplyScalar(this.moveStepDistance);
		var layerSelected = this.parentView.layerSelected();
		layerSelected.offset.add(offset);
		this.parentView.controlUpdate();
	}

	ToolLayers.prototype.layerSelectedMoveDown = function()
	{
		this.layerSelectedMove(new Coords(0, 1));
	}

	ToolLayers.prototype.layerSelectedMoveLeft = function()
	{
		this.layerSelectedMove(new Coords(-1, 0));
	}

	ToolLayers.prototype.layerSelectedMoveRight = function()
	{
		this.layerSelectedMove(new Coords(1, 0));
	}

	ToolLayers.prototype.layerSelectedMoveUp = function()
	{
		this.layerSelectedMove(new Coords(0, -1));
	}

	ToolLayers.prototype.layerSelectedRaise = function(offset)
	{
		this.layerSelectedRaiseOrLower(1);
	}

	ToolLayers.prototype.layerSelectedRaiseOrLower = function(offset)
	{
		var layerSelected = this.parentView.layerSelected();

		var layers = this.parentView.layers;
		var layerSelectedIndex = layers.indexOf(layerSelected);
		var layerSelectedIndexNext = layerSelectedIndex + offset;
		if (layerSelectedIndexNext >= 0 && layerSelectedIndexNext < layers.length)
		{
			layers.splice(layerSelectedIndex, 1);
			layers.splice(layerSelectedIndexNext, 0, layerSelected);
			this.layerIndexSelected = layerSelectedIndexNext;
			this.controlUpdate();
			this.parentView.controlUpdate();
		}
	}

	ToolLayers.prototype.layerSelectedRemove = function()
	{
		var layerToRemove = this.parentView.layerSelected();

		var layersAll = this.parentView.layers;
		layersAll.splice(this.layerIndexSelected, 1);
		delete layersAll[layerToRemove.name];

		this.layerIndexSelected = 0;

		this.controlUpdate();
		this.parentView.controlUpdate();
	}

	ToolLayers.prototype.layerSelectedRename = function()
	{
		var layerSelected = this.parentView.layerSelected();
		var containerRename = this.control.children["containerRename"];
		var textName = containerRename.children["textName"];
		var nameToSet = textName.value;
		if (nameToSet == "")
		{
			alert("Invalid name!");
		}
		else
		{
			var layersAll = this.parentView.layers;
			delete layersAll[layerSelected.name];
			layerSelected.name = nameToSet;
			layersAll[nameToSet] = layerSelected;

			this.controlUpdate()
			this.parentView.controlUpdate();
		}
	}

	ToolLayers.prototype.layerSetByIndex = function(valueToSet)
	{
		this.layerIndexSelected = valueToSet;
		this.parentView.controlUpdate();
	}

	ToolLayers.prototype.moveStepDistanceChanged = function(valueToSet)
	{
		this.moveStepDistance = parseInt(valueToSet);
	}

	// controllable

	ToolLayers.prototype.controlUpdate = function()
	{
		if (this.control == null)
		{
			this.selectLayer = new ControlSelectBox
			(
				"selectLayer",
				this.parentView.layers, // options
				"name",
				this.layerSetByIndex.bind(this)
			);

			var containerRename = new ControlContainer
			(
				"containerRename",
				[
					new ControlButton
					(
						"Rename:",
						this.layerSelectedRename.bind(this)
					),
					new ControlTextBox
					(
						"textName", ""
					),
				]
			);

			var containerAddRemoveHide = new ControlContainer
			(
				"containerAddRemoveHide",
				[
					new ControlButton
					(
						"Add",
						this.layerAdd.bind(this)
					),

					new ControlButton
					(
						"Clone",
						this.layerClone.bind(this)
					),

					new ControlButton
					(
						"Remove",
						this.layerSelectedRemove.bind(this)
					),

					new ControlButton
					(
						"Hide/Show",
						this.layerSelectedHideOrShow.bind(this)
					),

				]
			);

			var containerMove = new ControlContainer
			(
				"containerMove",
				[
					new ControlLabel("Move:"),

					new ControlNumberBox
					(
						"numberMoveStepDistance",
						this.moveStepDistance,
						this.moveStepDistanceChanged.bind(this)
					),

					new ControlButton
					(
						"<",
						this.layerSelectedMoveLeft.bind(this)
		 			),
					new ControlButton
					(
						">",
						this.layerSelectedMoveRight.bind(this)
		 			),
					new ControlButton
					(
						"^",
						this.layerSelectedMoveUp.bind(this)
		 			),
					new ControlButton
					(
						"v",
						this.layerSelectedMoveDown.bind(this)
		 			),
				]
			);

			var containerRaiseOrLower = new ControlContainer
			(
				"containerRaiseOrLower",
				[
					new ControlButton
					(
						"Raise",
						this.layerSelectedRaise.bind(this)
					),

					new ControlButton
					(
						"Lower",
						this.layerSelectedLower.bind(this)
					),
					new ControlButton
					(
						"Merge Down",
						this.layerSelectedMergeDown.bind(this)
		 			),
				]
			);

			var returnValue = new ControlContainer
			(
				"contatinerToolLayers",
				[
					new ControlLabel("Layer:"),

					this.selectLayer,

					containerRename,

					containerAddRemoveHide,
	
					containerRaiseOrLower,

					containerMove,

				]
			);

			returnValue.domElementUpdate();

			this.control = returnValue;
		}

		var selectLayer = this.selectLayer;

		selectLayer.options = this.parentView.layers;
		selectLayer.domElementUpdate_Options();
		selectLayer.selectedIndex = this.layerIndexSelected;
		selectLayer.domElementUpdate();

		return this.control;
	}
}

function ToolPaint()
{
	this.name = "Paint";
}
{
	// event handlers

	ToolPaint.prototype.processMouseDown = function()
	{
		// do nothing
	}

	ToolPaint.prototype.processMouseMove = function()
	{
		var tools = this.parentView.tools;

		var layerSelected = this.parentView.layerSelected();

		layerSelected.display.drawLine
		(
			this.parentView.mousePosPrev, 
			this.parentView.mousePos,
			tools["ColorPalette"].colorSelected, 
			tools["BrushSize"].brushSizeSelected
		);

		this.parentView.controlUpdate();
	}

	ToolPaint.prototype.processSelection = function()
	{
		this.parentView.toolSelected = this;
	}

	// controllable

	ToolPaint.prototype.controlUpdate = function()
	{
		if (this.control == null)
		{
			var returnValue = new ControlContainer
			(
				"containerPaint",
				[
					new ControlButton
					(
						this.name,
						this.processSelection.bind(this)
 					)
				]
			);

			this.control = returnValue;	
		}

		return this.control;
	}
}

function ToolSelect()
{
	this.name = "Select";
}
{
	// event handlers

	ToolSelect.prototype.copy = function()
	{
		this.copyOrCut(false);	
	}

	ToolSelect.prototype.cut = function()
	{
		this.copyOrCut(true);	
	}

	ToolSelect.prototype.copyOrCut = function(cutRatherThanCopy)
	{
		var view = this.parentView;
		var layerSelected = view.layerSelected();
		var selection = view.selection;
		var layerForClipboard = new Layer
		(
			"layerClipboard",
			selection.size.clone(),
			new Coords(0, 0) // offset
		);

		layerForClipboard.display.drawOther
		(
			layerSelected.display, 
			new Coords(0, 0), // target pos
			selection.pos,
			selection.size
		);

		if (cutRatherThanCopy == true)
		{
			layerSelected.display.clearRectangle
			(
				selection.pos.clone().subtract
				(
					layerSelected.offset
				),
				selection.size
			);
		}

		selection.pos = null;

		view.layerForClipboard = layerForClipboard;

		view.controlUpdate();
	}

	ToolSelect.prototype.paste = function()
	{
		var view = this.parentView;
		var layerForClipboard = view.layerForClipboard;
		if (layerForClipboard != null)
		{
			var layersAll = view.layers;
			layersAll.push(layerForClipboard);
			view.layerForClipboard = null;

			var toolLayers = view.tools["Layers"];
			toolLayers.layerIndexSelected = layersAll.length - 1;
			toolLayers.controlUpdate();

			view.controlUpdate();
		}
	}

	ToolSelect.prototype.processMouseDown = function()
	{
		var layerSelected = this.parentView.layerSelected();

		var selection = this.parentView.selection;
		var mousePos = this.parentView.mousePos;

		if (selection.pos == null)
		{
			selection.pos = mousePos.clone();
			selection.size = new Coords(0, 0);
			selection.isComplete = false;
			selection.isBeingMoved = false;
		}
		else if (mousePos.isInRangeMinMax(selection.pos, selection.max()) == true)
		{
			selection.isBeingMoved = true;
		}
		else
		{
			selection.pos = null;	
		}
	
		this.parentView.controlUpdate();
	}

	ToolSelect.prototype.processMouseMove = function()
	{
		var selection = this.parentView.selection;
	
		if (selection.pos == null)
		{
			// do nothing
		}
		else if (selection.isComplete == false)
		{
			selection.size.overwriteWith
			(
				this.parentView.mousePos
			).subtract
			(
				selection.pos
			);
		}
		else if (selection.isBeingMoved == true)
		{
			var mousePos = this.parentView.mousePos;
			var mousePosPrev = this.parentView.mousePosPrev;
			var mouseMove = mousePos.clone().subtract(mousePosPrev);
			selection.pos.add(mouseMove);
		}

		this.parentView.controlUpdate();
	}

	ToolSelect.prototype.processMouseUp = function()
	{
		var selection = this.parentView.selection;
		selection.isComplete = true;

		this.parentView.controlUpdate();
	}

	ToolSelect.prototype.processSelection = function()
	{
		this.parentView.toolSelected = this;
	}

	// controllable

	ToolSelect.prototype.controlUpdate = function()
	{
		if (this.control == null)
		{
			var returnValue = new ControlContainer
			(
				"containerSelect",
				[
					new ControlButton
					(
						this.name,
						this.processSelection.bind(this)
		 			),
					new ControlButton
					(
						"Cut",
						this.cut.bind(this)
		 			),
					new ControlButton
					(
						"Copy",
						this.copy.bind(this)
		 			),
					new ControlButton
					(
						"Paste",
						this.paste.bind(this)
		 			),
				]
			);
	
			this.control = returnValue;	
		}

		return this.control;
	}
}

function ToolViewSize()
{
	this.name = "ViewSize";
}
{
	// event handlers

	ToolViewSize.prototype.viewSizeXChanged = function(valueToSet)
	{
		this.parentView.size.x = valueToSet;
		this.viewSizeSet();
	}

	ToolViewSize.prototype.viewSizeYChanged = function(valueToSet)
	{
		this.parentView.size.y = valueToSet;
		this.viewSizeSet();
	}

	ToolViewSize.prototype.viewSizeSet = function()
	{
		var size = this.parentView.size;

		var layers = this.parentView.layers;

		var displayMain = this.parentView.control.children["viewCanvas"].display;
		displayMain.sizeSet(size);

		for (var i = 0; i < layers.length; i++)
		{
			var layer = layers[i];
			layer.display.sizeSet(size);
			displayMain.drawOther(layer.display, Coords.Instances.Zeroes);
		}
	}

	// control

	ToolViewSize.prototype.controlUpdate = function()
	{
		if (this.control == null)
		{
			var size = this.parentView.size;

			var returnValue = new ControlContainer
			(
				"controlToolViewSize",
				[
					new ControlLabel
					(
						"Canvas Size:"
					),

					new ControlNumberBox
					(
						"numberViewSizeX",
						size.x,
						this.viewSizeXChanged.bind(this)
					),

					new ControlNumberBox
					(
						"numberViewSizeY",
						size.y,
						this.viewSizeYChanged.bind(this)
					),
				]
			);

			this.control = returnValue;
		}

		return this.control;
	}
}

function View(size, toolNameToSelectInitial, tools)
{
	this.size = size;
	this.tools = tools.addLookups("name");

	for (var i = 0; i < this.tools.length; i++)
	{
		var tool = this.tools[i];
		tool.parentView = this;
	}

	this.toolSelected = this.tools[toolNameToSelectInitial];

	this.layers = 
	[
		new Layer("Layer0", size, new Coords(0, 0)),
	];

	this.layers.addLookups("name");

	for (var i = 0; i < this.layers.length; i++)
	{
		var layer = this.layers[i];
		layer.parentView = this;
	}

	this.isMouseDown = false;
	this.mousePos = new Coords(0, 0);
	this.mousePosPrev = new Coords(0, 0);

	this.selection = new Selection();
}
{
	// static methods

	View.imageURLForAlphaZeroBuild = function()
	{
		var imageSizeInPixels = new Coords(32, 32);
		var imageSizeInPixelsHalf = imageSizeInPixels.clone().divideScalar(2);

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

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

		graphicsForCanvas.fillStyle = Color.Instances.Gray.systemColor;
		graphicsForCanvas.fillRect
		(
			0, 0, imageSizeInPixels.x, imageSizeInPixels.y
		);
		graphicsForCanvas.fillStyle = Color.Instances.GrayDark.systemColor;
		graphicsForCanvas.fillRect
		(
			0, 0, imageSizeInPixelsHalf.x, imageSizeInPixelsHalf.y
		);
		graphicsForCanvas.fillRect
		(
			imageSizeInPixelsHalf.x, 
			imageSizeInPixelsHalf.y, 
			imageSizeInPixelsHalf.x, 
			imageSizeInPixelsHalf.y
		);

		var imageFromCanvasURL = canvas.toDataURL("image/png");

		return imageFromCanvasURL;
	}

	// instance methods

	View.prototype.layerSelected = function()
	{
		var layerIndexSelected = this.tools["Layers"].layerIndexSelected;
		var returnValue = this.layers[layerIndexSelected]; 
		return returnValue;
	}

	// event handlers

	View.prototype.processMouseDown = function(event)
	{		
		event.preventDefault(); // otherwise the cursor changes

		var boundingClientRect = 
			event.target.getBoundingClientRect();
		this.mousePos.overwriteWithDimensions
		(
			event.clientX - boundingClientRect.left,
			event.clientY - boundingClientRect.top
		);
		this.mousePosPrev.overwriteWith(this.mousePos);
		this.isMouseDown = true;
		this.toolSelected.processMouseDown();
	}

	View.prototype.processMouseOver = function(event)
	{
		// do nothing
	}

	View.prototype.processMouseOut = function(event)
	{
		this.isMouseDown = false;		
	}

	View.prototype.processMouseMove = function(event)
	{
		if (this.isMouseDown == true)
		{
			this.mousePosPrev.overwriteWith(this.mousePos);
			var boundingClientRect = 
				event.target.getBoundingClientRect();
			this.mousePos.overwriteWithDimensions
			(
				event.clientX - boundingClientRect.left,
				event.clientY - boundingClientRect.top
			);
			this.toolSelected.processMouseMove();
		}
	}	

	View.prototype.processMouseUp = function(event)
	{
		this.isMouseDown = false;
	}

	// controllable

	View.prototype.controlUpdate = function()
	{ 
		if (this.control == null)
		{
			this.controlCanvas = new ControlCanvas
			(
				"viewCanvas",
				this.size,
				this.layers,
				this.processMouseDown.bind(this),
				this.processMouseMove.bind(this),
				this.processMouseOut.bind(this),
				this.processMouseOver.bind(this),
				this.processMouseUp.bind(this)
			);
	
			var containerTools = new ControlContainer
			(			
				"containerTools",
				Control.controllablesToControls(this.tools)
			);

			var containerView = new ControlContainer
			(
				"containerView",
				[
					this.controlCanvas,
					containerTools,
				]
			);

			this.control = containerView;
		}

		this.control.domElementUpdate();

		this.drawToDisplay(this.controlCanvas.display);

		return this.control;
	}

	// drawable

	View.prototype.drawToDisplay = function(display)
	{
		if (this.selection.pos != null)
		{
			display.drawRectangle
			(
				this.selection.pos,
				this.selection.size,
				null, // colorFill
				Color.Instances.Cyan // colorBorder
			);
		}
	}
}

// run

main();

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

Advertisements
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

2 Responses to A Simple Paint Program in JavaScript Using HTML5

  1. mujuw says:

    Can I use this code in a web application?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s