moebius-web

web based ansi art editor

moebius-web

public/js/core.js


function createPalette(RGB6Bit) {
	"use strict";
	var RGBAColours = RGB6Bit.map((RGB6Bit) => {
		return new Uint8Array(
			[
				RGB6Bit[0] << 2 | RGB6Bit[0] >> 4,
				RGB6Bit[1] << 2 | RGB6Bit[1] >> 4,
				RGB6Bit[2] << 2 | RGB6Bit[2] >> 4,
				255
			]
		);
	});
	var foreground = 7;
	var background = 0;

	function getRGBAColour(index) {
		return RGBAColours[index];
	}

	function getForegroundColour() {
		return foreground;
	}

	function getBackgroundColour() {
		return background;
	}

	function setForegroundColour(newForeground) {
		foreground = newForeground;
		document.dispatchEvent(new CustomEvent("onForegroundChange", { "detail": foreground }));
	}

	function setBackgroundColour(newBackground) {
		background = newBackground;
		document.dispatchEvent(new CustomEvent("onBackgroundChange", { "detail": background }));
	}

	return {
		"getRGBAColour": getRGBAColour,
		"getForegroundColour": getForegroundColour,
		"getBackgroundColour": getBackgroundColour,
		"setForegroundColour": setForegroundColour,
		"setBackgroundColour": setBackgroundColour
	};
}

function createDefaultPalette() {
	"use strict";
	return createPalette([
		[0, 0, 0],
		[0, 0, 42],
		[0, 42, 0],
		[0, 42, 42],
		[42, 0, 0],
		[42, 0, 42],
		[42, 21, 0],
		[42, 42, 42],
		[21, 21, 21],
		[21, 21, 63],
		[21, 63, 21],
		[21, 63, 63],
		[63, 21, 21],
		[63, 21, 63],
		[63, 63, 21],
		[63, 63, 63]
	]);
}

function createPalettePreview(canvas) {
	"use strict";
	var imageData;

	function updatePreview() {
		var colour;
		var foreground = palette.getRGBAColour(palette.getForegroundColour());
		var background = palette.getRGBAColour(palette.getBackgroundColour());
		for (var y = 0, i = 0; y < canvas.height; y++) {
			for (var x = 0; x < canvas.width; x++, i += 4) {
				if (y >= 10 && y < canvas.height - 10 && x > 10 && x < canvas.width - 10) {
					colour = foreground;
				} else {
					colour = background;
				}
				imageData.data.set(colour, i);
			}
		}
		canvas.getContext("2d").putImageData(imageData, 0, 0);
	}

	imageData = canvas.getContext("2d").createImageData(canvas.width, canvas.height);
	updatePreview();
	document.addEventListener("onForegroundChange", updatePreview);
	document.addEventListener("onBackgroundChange", updatePreview);

	return {
		"setForegroundColour": updatePreview,
		"setBackgroundColour": updatePreview
	};
}

function createPalettePicker(canvas) {
	"use strict";
	var imageData = [];
	var mousedowntime;
	var presstime;

	function updateColor(index) {
		var colour = palette.getRGBAColour(index);
		for (var y = 0, i = 0; y < imageData[index].height; y++) {
			for (var x = 0; x < imageData[index].width; x++, i += 4) {
				imageData[index].data.set(colour, i);
			}
		}
		canvas.getContext("2d").putImageData(imageData[index], (index > 7) ? (canvas.width / 2) : 0, (index % 8) * imageData[index].height);
	}

	function updatePalette() {
		for (var i = 0; i < 16; i++) {
			updateColor(i);
		}
	}

	function pressStart(evt) {
		mousedowntime = new Date().getTime();
	}

	function touchEnd(evt) {
		var rect = canvas.getBoundingClientRect();
		var x = Math.floor((evt.touches[0].pageX - rect.left) / (canvas.width / 2));
		var y = Math.floor((evt.touches[0].pageY - rect.top) / (canvas.height / 8));
		var colourIndex = y + ((x === 0) ? 0 : 8);
		presstime = new Date().getTime() - mousedowntime;
		if (presstime < 200) {
			palette.setForegroundColour(colourIndex);
		} else {
			palette.setBackgroundColour(colourIndex);
		}
	}

	function mouseEnd(evt) {
		var rect = canvas.getBoundingClientRect();
		var x = Math.floor((evt.clientX - rect.left) / (canvas.width / 2));
		var y = Math.floor((evt.clientY - rect.top) / (canvas.height / 8));
		var colourIndex = y + ((x === 0) ? 0 : 8);
		if (evt.altKey === false && evt.ctrlKey === false) {
			presstime = new Date().getTime() - mousedowntime;
			if (presstime < 200) {
				palette.setForegroundColour(colourIndex);
			} else {
				palette.setBackgroundColour(colourIndex);
			}
		} else {
			palette.setBackgroundColour(colourIndex);
		}
	}

	for (var i = 0; i < 16; i++) {
		imageData[i] = canvas.getContext("2d").createImageData(canvas.width / 2, canvas.height / 8);
	}

	function keydown(evt) {
		var keyCode = (evt.keyCode || evt.which);
		if (keyCode >= 48 && keyCode <= 55) {
			var num = keyCode - 48;
			if (evt.ctrlKey === true) {
				evt.preventDefault();
				if (palette.getForegroundColour() === num) {
					palette.setForegroundColour(num + 8);
				} else {
					palette.setForegroundColour(num);
				}
			} else if (evt.altKey) {
				evt.preventDefault();
				if (palette.getBackgroundColour() === num) {
					palette.setBackgroundColour(num + 8);
				} else {
					palette.setBackgroundColour(num);
				}
			}
		} else if (keyCode >= 37 && keyCode <= 40 && evt.ctrlKey === true) {
			evt.preventDefault();
			switch (keyCode) {
				case 37:
					var colour = palette.getBackgroundColour();
					colour = (colour === 0) ? 15 : (colour - 1);
					palette.setBackgroundColour(colour);
					break;
				case 38:
					var colour = palette.getForegroundColour();
					colour = (colour === 0) ? 15 : (colour - 1);
					palette.setForegroundColour(colour);
					break;
				case 39:
					var colour = palette.getBackgroundColour();
					colour = (colour === 15) ? 0 : (colour + 1);
					palette.setBackgroundColour(colour);
					break;
				case 40:
					var colour = palette.getForegroundColour();
					colour = (colour === 15) ? 0 : (colour + 1);
					palette.setForegroundColour(colour);
					break;
				default:
					break;
			}
		}
	}

	updatePalette();
	canvas.addEventListener("touchstart", pressStart);
	canvas.addEventListener("touchend", touchEnd);
	canvas.addEventListener("touchcancel", touchEnd);
	canvas.addEventListener("mouseup", mouseEnd);
	canvas.addEventListener("contextmenu", (evt) => {
		evt.preventDefault();
	});
	document.addEventListener("keydown", keydown);
}

function loadImageAndGetImageData(url, callback) {
	"use strict";
	var imgElement = new Image();
	imgElement.addEventListener("load", () => {
		var canvas = createCanvas(imgElement.width, imgElement.height);
		var ctx = canvas.getContext("2d");
		ctx.drawImage(imgElement, 0, 0);
		var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
		callback(imageData);
	});
	imgElement.addEventListener("error", () => {
		callback(undefined);
	});
	imgElement.src = url;
}

function loadFontFromImage(fontName, letterSpacing, palette, callback) {
	"use strict";
	var fontData = {};
	var fontGlyphs;
	var alphaGlyphs;
	var letterSpacingImageData;

	function parseFontData(imageData) {
		var fontWidth = imageData.width / 16;
		var fontHeight = imageData.height / 16;
		if ((fontWidth === 8) && (imageData.height % 16 === 0) && (fontHeight >= 1 && fontHeight <= 32)) {
			var data = new Uint8Array(fontWidth * fontHeight * 256 / 8);
			var k = 0;
			for (var value = 0; value < 256; value += 1) {
				var x = (value % 16) * fontWidth;
				var y = Math.floor(value / 16) * fontHeight;
				var pos = (y * imageData.width + x) * 4;
				var i = 0;
				while (i < fontWidth * fontHeight) {
					data[k] = data[k] << 1;
					if (imageData.data[pos] > 127) {
						data[k] += 1;
					}
					if ((i += 1) % fontWidth === 0) {
						pos += (imageData.width - 8) * 4;
					}
					if (i % 8 === 0) {
						k += 1;
					}
					pos += 4;
				}
			}
			return {
				"width": fontWidth,
				"height": fontHeight,
				"data": data
			};
		}
		return undefined;
	}

	function generateNewFontGlyphs() {
		var canvas = createCanvas(fontData.width, fontData.height);
		var ctx = canvas.getContext("2d");
		var bits = new Uint8Array(fontData.width * fontData.height * 256);
		for (var i = 0, k = 0; i < fontData.width * fontData.height * 256 / 8; i += 1) {
			for (var j = 7; j >= 0; j -= 1, k += 1) {
				bits[k] = (fontData.data[i] >> j) & 1;
			}
		}
		fontGlyphs = new Array(16);
		for (var foreground = 0; foreground < 16; foreground++) {
			fontGlyphs[foreground] = new Array(16);
			for (var background = 0; background < 16; background++) {
				fontGlyphs[foreground][background] = new Array(256);
				for (var charCode = 0; charCode < 256; charCode++) {
					fontGlyphs[foreground][background][charCode] = ctx.createImageData(fontData.width, fontData.height);
					for (var i = 0, j = charCode * fontData.width * fontData.height; i < fontData.width * fontData.height; i += 1, j += 1) {
						var colour = palette.getRGBAColour((bits[j] === 1) ? foreground : background);
						fontGlyphs[foreground][background][charCode].data.set(colour, i * 4);
					}
				}
			}
		}
		alphaGlyphs = new Array(16);
		for (var foreground = 0; foreground < 16; foreground++) {
			alphaGlyphs[foreground] = new Array(256);
			for (var charCode = 0; charCode < 256; charCode++) {
				if (charCode === 220 || charCode === 223) {
					var imageData = ctx.createImageData(fontData.width, fontData.height);
					for (var i = 0, j = charCode * fontData.width * fontData.height; i < fontData.width * fontData.height; i += 1, j += 1) {
						if (bits[j] === 1) {
							imageData.data.set(palette.getRGBAColour(foreground), i * 4);
						}
					}
					var alphaCanvas = createCanvas(imageData.width, imageData.height);
					alphaCanvas.getContext("2d").putImageData(imageData, 0, 0);
					alphaGlyphs[foreground][charCode] = alphaCanvas;
				}
			}
		}
		letterSpacingImageData = new Array(16);
		for (var i = 0; i < 16; i++) {
			var canvas = createCanvas(1, fontData.height);
			var ctx = canvas.getContext("2d");
			var imageData = ctx.getImageData(0, 0, 1, fontData.height);
			var colour = palette.getRGBAColour(i);
			for (var j = 0; j < fontData.height; j++) {
				imageData.data.set(colour, j * 4);
			}
			letterSpacingImageData[i] = imageData;
		}
	}

	function getWidth() {
		if (letterSpacing === true) {
			return fontData.width + 1;
		}
		return fontData.width;
	}

	function getHeight() {
		return fontData.height;
	}

	function setLetterSpacing(newLetterSpacing) {
		if (newLetterSpacing !== letterSpacing) {
			generateNewFontGlyphs();
			letterSpacing = newLetterSpacing;
			document.dispatchEvent(new CustomEvent("onLetterSpacingChange", { "detail": letterSpacing }));
		}
	}

	function getLetterSpacing() {
		return letterSpacing;
	}

	loadImageAndGetImageData("fonts/" + fontName + ".png", (imageData) => {
		if (imageData === undefined) {
			callback(false);
		} else {
			var newFontData = parseFontData(imageData);
			if (newFontData === undefined) {
				callback(false);
			} else {
				fontData = newFontData;
				generateNewFontGlyphs();
				callback(true);
			}
		}
	});

	function draw(charCode, foreground, background, ctx, x, y) {
		if (letterSpacing === true) {
			ctx.putImageData(fontGlyphs[foreground][background][charCode], x * (fontData.width + 1), y * fontData.height);
			if (charCode >= 192 && charCode <= 223) {
				ctx.putImageData(fontGlyphs[foreground][background][charCode], x * (fontData.width + 1) + 1, y * fontData.height, fontData.width - 1, 0, 1, fontData.height);
			} else {
				ctx.putImageData(letterSpacingImageData[background], x * (fontData.width + 1) + 8, y * fontData.height);
			}
		} else {
			ctx.putImageData(fontGlyphs[foreground][background][charCode], x * fontData.width, y * fontData.height);
		}
	}

	function drawWithAlpha(charCode, foreground, ctx, x, y) {
		if (letterSpacing === true) {
			ctx.drawImage(alphaGlyphs[foreground][charCode], x * (fontData.width + 1), y * fontData.height);
			if (charCode >= 192 && charCode <= 223) {
				ctx.drawImage(alphaGlyphs[foreground][charCode], fontData.width - 1, 0, 1, fontData.height, x * (fontData.width + 1) + fontData.width, y * fontData.height, 1, fontData.height);
			}
		} else {
			ctx.drawImage(alphaGlyphs[foreground][charCode], x * fontData.width, y * fontData.height);
		}
	}

	return {
		"getWidth": getWidth,
		"getHeight": getHeight,
		"setLetterSpacing": setLetterSpacing,
		"getLetterSpacing": getLetterSpacing,
		"draw": draw,
		"drawWithAlpha": drawWithAlpha
	};
}

function createTextArtCanvas(canvasContainer, callback) {
	"use strict";
	var columns = 80,
		rows = 25,
		iceColours = false,
		imageData = new Uint16Array(columns * rows),
		canvases,
		ctxs,
		offBlinkCanvases,
		onBlinkCanvases,
		offBlinkCtxs,
		onBlinkCtxs,
		blinkTimer,
		blinkOn = false,
		mouseButton = false,
		currentUndo = [],
		undoBuffer = [],
		redoBuffer = [],
		drawHistory = [];

	function updateBeforeBlinkFlip(x, y) {
		var dataIndex = y * columns + x;
		var contextIndex = Math.floor(y / 25);
		var contextY = y % 25;
		var charCode = imageData[dataIndex] >> 8;
		var background = (imageData[dataIndex] >> 4) & 15;
		var foreground = imageData[dataIndex] & 15;
		var shifted = background >= 8;
		if (shifted === true) {
			background -= 8;
		}
		if (blinkOn === true && shifted) {
			font.draw(charCode, background, background, ctxs[contextIndex], x, contextY);
		} else {
			font.draw(charCode, foreground, background, ctxs[contextIndex], x, contextY);
		}
	}


	function redrawGlyph(index, x, y) {
		var contextIndex = Math.floor(y / 25);
		var contextY = y % 25;
		var charCode = imageData[index] >> 8;
		var background = (imageData[index] >> 4) & 15;
		var foreground = imageData[index] & 15;
		if (iceColours === true) {
			font.draw(charCode, foreground, background, ctxs[contextIndex], x, contextY);
		} else {
			if (background >= 8) {
				background -= 8;
				font.draw(charCode, foreground, background, offBlinkCtxs[contextIndex], x, contextY);
				font.draw(charCode, background, background, onBlinkCtxs[contextIndex], x, contextY);
			} else {
				font.draw(charCode, foreground, background, offBlinkCtxs[contextIndex], x, contextY);
				font.draw(charCode, foreground, background, onBlinkCtxs[contextIndex], x, contextY);
			}
		}
	}

	function redrawEntireImage() {
		for (var y = 0, i = 0; y < rows; y++) {
			for (var x = 0; x < columns; x++, i++) {
				redrawGlyph(i, x, y);
			}
		}
	}

	function blink() {
		if (blinkOn === false) {
			blinkOn = true;
			for (var i = 0; i < ctxs.length; i++) {
				ctxs[i].drawImage(onBlinkCanvases[i], 0, 0);
			}
		} else {
			blinkOn = false;
			for (var i = 0; i < ctxs.length; i++) {
				ctxs[i].drawImage(offBlinkCanvases[i], 0, 0);
			}
		}
	}

	function createCanvases() {
		if (canvases !== undefined) {
			canvases.forEach((canvas) => {
				canvasContainer.removeChild(canvas);
			});
		}
		canvases = [];
		offBlinkCanvases = [];
		offBlinkCtxs = [];
		onBlinkCanvases = [];
		onBlinkCtxs = [];
		ctxs = [];
		var fontWidth = font.getWidth();
		var fontHeight = font.getHeight();
		var canvasWidth = fontWidth * columns;
		var canvasHeight = fontHeight * 25;
		for (var i = 0; i < Math.floor(rows / 25); i++) {
			var canvas = createCanvas(canvasWidth, canvasHeight);
			canvases.push(canvas);
			ctxs.push(canvas.getContext("2d"));
			var onBlinkCanvas = createCanvas(canvasWidth, canvasHeight);
			onBlinkCanvases.push(onBlinkCanvas);
			onBlinkCtxs.push(onBlinkCanvas.getContext("2d"));
			var offBlinkCanvas = createCanvas(canvasWidth, canvasHeight);
			offBlinkCanvases.push(offBlinkCanvas);
			offBlinkCtxs.push(offBlinkCanvas.getContext("2d"));
		}
		var canvasHeight = fontHeight * (rows % 25);
		if (rows % 25 !== 0) {
			var canvas = createCanvas(canvasWidth, canvasHeight);
			canvases.push(canvas);
			ctxs.push(canvas.getContext("2d"));
			var onBlinkCanvas = createCanvas(canvasWidth, canvasHeight);
			onBlinkCanvases.push(onBlinkCanvas);
			onBlinkCtxs.push(onBlinkCanvas.getContext("2d"));
			var offBlinkCanvas = createCanvas(canvasWidth, canvasHeight);
			offBlinkCanvases.push(offBlinkCanvas);
			offBlinkCtxs.push(offBlinkCanvas.getContext("2d"));
		}
		canvasContainer.style.width = canvasWidth + "px";
		for (var i = 0; i < canvases.length; i++) {
			canvasContainer.appendChild(canvases[i]);
		}
		if (blinkTimer !== undefined) {
			clearInterval(blinkTimer);
			blinkOn = false;
		}
		redrawEntireImage();
		if (iceColours === false) {
			blinkTimer = setInterval(blink, 250);
		}
	}

	function updateTimer() {
		if (blinkTimer !== undefined) {
			clearInterval(blinkTimer);
		}
		if (iceColours === false) {
			blinkOn = false;
			blinkTimer = setInterval(blink, 500);
		}
	}

	function setFont(fontName, callback) {
		font = loadFontFromImage(fontName, font.getLetterSpacing(), palette, (success) => {
			createCanvases();
			redrawEntireImage();
			document.dispatchEvent(new CustomEvent("onFontChange", { "detail": fontName }));
			callback();
		});
	}

	function resize(newColumnValue, newRowValue) {
		if ((newColumnValue !== columns || newRowValue !== rows) && (newColumnValue > 0 && newRowValue > 0)) {
			clearUndos();
			var maxColumn = (columns > newColumnValue) ? newColumnValue : columns;
			var maxRow = (rows > newRowValue) ? newRowValue : rows;
			var newImageData = new Uint16Array(newColumnValue * newRowValue);
			for (var y = 0; y < maxRow; y++) {
				for (var x = 0; x < maxColumn; x++) {
					newImageData[y * newColumnValue + x] = imageData[y * columns + x];
				}
			}
			imageData = newImageData;
			columns = newColumnValue;
			rows = newRowValue;
			createCanvases();
			document.dispatchEvent(new CustomEvent("onTextCanvasSizeChange", { "detail": { "columns": columns, "rows": rows } }));
		}
	}

	function getIceColours() {
		return iceColours;
	}

	function setIceColours(newIceColours) {
		if (iceColours !== newIceColours) {
			iceColours = newIceColours;
			updateTimer();
			redrawEntireImage();
		}
	}

	function onLetterSpacingChange(letterSpacing) {
		createCanvases();
	}

	function getImage() {
		var completeCanvas = createCanvas(font.getWidth() * columns, font.getHeight() * rows);
		var y = 0;
		var ctx = completeCanvas.getContext("2d");
		((iceColours === true) ? canvases : offBlinkCanvases).forEach((canvas) => {
			ctx.drawImage(canvas, 0, y);
			y += canvas.height;
		});
		return completeCanvas;
	}

	function getImageData() {
		return imageData;
	}

	function setImageData(newColumnValue, newRowValue, newImageData, newIceColours) {
		clearUndos();
		columns = newColumnValue;
		rows = newRowValue;
		imageData = newImageData;
		if (iceColours !== newIceColours) {
			iceColours = newIceColours;
			updateTimer();
		}
		createCanvases();
		redrawEntireImage();
		document.dispatchEvent(new CustomEvent("onOpenedFile"));
	}

	function getColumns() {
		return columns;
	}

	function getRows() {
		return rows;
	}

	function clearUndos() {
		currentUndo = [];
		undoBuffer = [];
		redoBuffer = [];
	}

	function clear() {
		title.reset();
		clearUndos();
		imageData = new Uint16Array(columns * rows);
		redrawEntireImage();
	}

	palette = createDefaultPalette();
	font = loadFontFromImage("CP437 8x16", false, palette, (success) => {
		createCanvases();
		updateTimer();
		callback();
	});

	function draw(index, charCode, foreground, background, x, y) {
		currentUndo.push([index, imageData[index], x, y]);
		imageData[index] = (charCode << 8) + (background << 4) + foreground;
		drawHistory.push((index << 16) + imageData[index]);
	}

	function getBlock(x, y) {
		var index = y * columns + x;
		var charCode = imageData[index] >> 8;
		var foregroundColour = imageData[index] & 15;
		var backgroundColour = (imageData[index] >> 4) & 15;
		return {
			"x": x,
			"y": y,
			"charCode": charCode,
			"foregroundColour": foregroundColour,
			"backgroundColour": backgroundColour
		};
	}

	function getHalfBlock(x, y) {
		var textY = Math.floor(y / 2);
		var index = textY * columns + x;
		var foreground = imageData[index] & 15;
		var background = (imageData[index] >> 4) & 15;
		var upperBlockColour = 0;
		var lowerBlockColour = 0;
		var isBlocky = false;
		var isVerticalBlocky = false;
		var leftBlockColour;
		var rightBlockColour;
		switch (imageData[index] >> 8) {
			case 0:
			case 32:
			case 255:
				upperBlockColour = background;
				lowerBlockColour = background;
				isBlocky = true;
				break;
			case 220:
				upperBlockColour = background;
				lowerBlockColour = foreground;
				isBlocky = true;
				break;
			case 221:
				isVerticalBlocky = true;
				leftBlockColour = foreground;
				rightBlockColour = background;
				break;
			case 222:
				isVerticalBlocky = true;
				leftBlockColour = background;
				rightBlockColour = foreground;
				break;
			case 223:
				upperBlockColour = foreground;
				lowerBlockColour = background;
				isBlocky = true;
				break;
			case 219:
				upperBlockColour = foreground;
				lowerBlockColour = foreground;
				isBlocky = true;
				break;
			default:
				if (foreground === background) {
					isBlocky = true;
					upperBlockColour = foreground;
					lowerBlockColour = foreground;
				} else {
					isBlocky = false;
				}
		}
		return {
			"x": x,
			"y": y,
			"textY": textY,
			"isBlocky": isBlocky,
			"upperBlockColour": upperBlockColour,
			"lowerBlockColour": lowerBlockColour,
			"halfBlockY": y % 2,
			"isVerticalBlocky": isVerticalBlocky,
			"leftBlockColour": leftBlockColour,
			"rightBlockColour": rightBlockColour
		};
	}

	function drawHalfBlock(index, foreground, x, y, textY) {
		var halfBlockY = y % 2;
		var charCode = imageData[index] >> 8;
		var currentForeground = imageData[index] & 15;
		var currentBackground = (imageData[index] >> 4) & 15;
		if (charCode === 219) {
			if (currentForeground !== foreground) {
				if (halfBlockY === 0) {
					draw(index, 223, foreground, currentForeground, x, textY);
				} else {
					draw(index, 220, foreground, currentForeground, x, textY);
				}
			}
		} else if (charCode !== 220 && charCode !== 223) {
			if (halfBlockY === 0) {
				draw(index, 223, foreground, currentBackground, x, textY);
			} else {
				draw(index, 220, foreground, currentBackground, x, textY);
			}
		} else {
			if (halfBlockY === 0) {
				if (charCode === 223) {
					if (currentBackground === foreground) {
						draw(index, 219, foreground, 0, x, textY);
					} else {
						draw(index, 223, foreground, currentBackground, x, textY);
					}
				} else if (currentForeground === foreground) {
					draw(index, 219, foreground, 0, x, textY);
				} else {
					draw(index, 223, foreground, currentForeground, x, textY);
				}
			} else {
				if (charCode === 220) {
					if (currentBackground === foreground) {
						draw(index, 219, foreground, 0, x, textY);
					} else {
						draw(index, 220, foreground, currentBackground, x, textY);
					}
				} else if (currentForeground === foreground) {
					draw(index, 219, foreground, 0, x, textY);
				} else {
					draw(index, 220, foreground, currentForeground, x, textY);
				}
			}
		}
	}

	document.addEventListener("onLetterSpacingChange", onLetterSpacingChange);

	function getXYCoords(clientX, clientY, callback) {
		var rect = canvasContainer.getBoundingClientRect();
		var x = Math.floor((clientX - rect.left) / font.getWidth());
		var y = Math.floor((clientY - rect.top) / font.getHeight());
		var halfBlockY = Math.floor((clientY - rect.top) / font.getHeight() * 2);
		callback(x, y, halfBlockY);
	}

	canvasContainer.addEventListener("touchstart", (evt) => {
		if (evt.touches.length == 2 && evt.changedTouches.length == 2) {
			evt.preventDefault();
			undo();
		} else if (evt.touches.length > 2 && evt.changedTouches.length > 2) {
			evt.preventDefault();
			redo();
		} else {
			mouseButton = true;
			getXYCoords(evt.touches[0].pageX, evt.touches[0].pageY, (x, y, halfBlockY) => {
				if (evt.altKey === true) {
					sampleTool.sample(x, halfBlockY);
				} else {
					document.dispatchEvent(new CustomEvent("onTextCanvasDown", { "detail": { "x": x, "y": y, "halfBlockY": halfBlockY, "leftMouseButton": (evt.button === 0 && evt.ctrlKey !== true), "rightMouseButton": (evt.button === 2 || evt.ctrlKey === true) } }));
				}
			});
		}
	});

	canvasContainer.addEventListener("mousedown", (evt) => {
		if (evt.touches.length == 2 && evt.changedTouches.length == 2) {
			evt.preventDefault();
			undo();
		} else if (evt.touches.length == 3 && evt.changedTouches.length == 3) {
			evt.preventDefault();
			redo();
		} else {
			mouseButton = true;
			getXYCoords(evt.clientX, evt.clientY, (x, y, halfBlockY) => {
				if (evt.altKey === true) {
					sampleTool.sample(x, halfBlockY);
				} else {
					document.dispatchEvent(new CustomEvent("onTextCanvasDown", { "detail": { "x": x, "y": y, "halfBlockY": halfBlockY, "leftMouseButton": (evt.button === 0 && evt.ctrlKey !== true), "rightMouseButton": (evt.button === 2 || evt.ctrlKey === true) } }));
				}
			});
		}
	});

	canvasContainer.addEventListener("contextmenu", (evt) => {
		evt.preventDefault();
	});

	canvasContainer.addEventListener("touchmove", (evt) => {
		evt.preventDefault();
		getXYCoords(evt.touches[0].pageX, evt.touches[0].pageY, (x, y, halfBlockY) => {
			document.dispatchEvent(new CustomEvent("onTextCanvasDrag", { "detail": { "x": x, "y": y, "halfBlockY": halfBlockY, "leftMouseButton": (evt.button === 0 && evt.ctrlKey !== true), "rightMouseButton": (evt.button === 2 || evt.ctrlKey === true) } }));
		});
	});

	canvasContainer.addEventListener("mousemove", (evt) => {
		evt.preventDefault();
		if (mouseButton === true) {
			getXYCoords(evt.clientX, evt.clientY, (x, y, halfBlockY) => {
				document.dispatchEvent(new CustomEvent("onTextCanvasDrag", { "detail": { "x": x, "y": y, "halfBlockY": halfBlockY, "leftMouseButton": (evt.button === 0 && evt.ctrlKey !== true), "rightMouseButton": (evt.button === 2 || evt.ctrlKey === true) } }));
			});
		}
	});

	canvasContainer.addEventListener("touchend", (evt) => {
		evt.preventDefault();
		mouseButton = false;
		document.dispatchEvent(new CustomEvent("onTextCanvasUp", {}));
	});

	canvasContainer.addEventListener("mouseup", (evt) => {
		evt.preventDefault();
		if (mouseButton === true) {
			mouseButton = false;
			document.dispatchEvent(new CustomEvent("onTextCanvasUp", {}));
		}
	});

	canvasContainer.addEventListener("touchenter", (evt) => {
		evt.preventDefault();
		document.dispatchEvent(new CustomEvent("onTextCanvasUp", {}));
	});

	canvasContainer.addEventListener("mouseenter", (evt) => {
		evt.preventDefault();
		if (mouseButton === true && (evt.which === 0 || evt.buttons === 0)) {
			mouseButton = false;
			document.dispatchEvent(new CustomEvent("onTextCanvasUp", {}));
		}
	});

	function sendDrawHistory() {
		worker.draw(drawHistory);
		drawHistory = [];
	}

	function undo() {
		if (currentUndo.length > 0) {
			undoBuffer.push(currentUndo);
			currentUndo = [];
		}
		if (undoBuffer.length > 0) {
			var currentRedo = [];
			var undoChunk = undoBuffer.pop();
			for (var i = undoChunk.length - 1; i >= 0; i--) {
				var undo = undoChunk.pop();
				if (undo[0] < imageData.length) {
					currentRedo.push([undo[0], imageData[undo[0]], undo[2], undo[3]]);
					imageData[undo[0]] = undo[1];
					drawHistory.push((undo[0] << 16) + undo[1]);
					if (iceColours === false) {
						updateBeforeBlinkFlip(undo[2], undo[3]);
					}
					redrawGlyph(undo[0], undo[2], undo[3]);
				}
			}
			redoBuffer.push(currentRedo);
			sendDrawHistory();
		}
	}

	function redo() {
		if (redoBuffer.length > 0) {
			var redoChunk = redoBuffer.pop();
			for (var i = redoChunk.length - 1; i >= 0; i--) {
				var redo = redoChunk.pop();
				if (redo[0] < imageData.length) {
					currentUndo.push([redo[0], imageData[redo[0]], redo[2], redo[3]]);
					imageData[redo[0]] = redo[1];
					drawHistory.push((redo[0] << 16) + redo[1]);
					if (iceColours === false) {
						updateBeforeBlinkFlip(redo[2], redo[3]);
					}
					redrawGlyph(redo[0], redo[2], redo[3]);
				}
			}
			undoBuffer.push(currentUndo);
			currentUndo = [];
			sendDrawHistory();
		}
	}

	function startUndo() {
		if (currentUndo.length > 0) {
			undoBuffer.push(currentUndo);
			currentUndo = [];
		}
		redoBuffer = [];
	}

	function optimiseBlocks(blocks) {
		blocks.forEach((block) => {
			var index = block[0];
			var attribute = imageData[index];
			var background = (attribute >> 4) & 15;
			if (background >= 8) {
				switch (attribute >> 8) {
					case 0:
					case 32:
					case 255:
						draw(index, 219, background, 0, block[1], block[2]);
						break;
					case 219:
						draw(index, 219, (attribute & 15), 0, block[1], block[2]);
						break;
					case 221:
						var foreground = (attribute & 15);
						if (foreground < 8) {
							draw(index, 222, background, foreground, block[1], block[2]);
						}
						break;
					case 222:
						var foreground = (attribute & 15);
						if (foreground < 8) {
							draw(index, 221, background, foreground, block[1], block[2]);
						}
						break;
					case 223:
						var foreground = (attribute & 15);
						if (foreground < 8) {
							draw(index, 220, background, foreground, block[1], block[2]);
						}
						break;
					case 220:
						var foreground = (attribute & 15);
						if (foreground < 8) {
							draw(index, 223, background, foreground, block[1], block[2]);
						}
						break;
					default:
						break;
				}
			}
		});
	}

	function drawBlocks(blocks) {
		blocks.forEach((block) => {
			if (iceColours === false) {
				updateBeforeBlinkFlip(block[1], block[2]);
			}
			redrawGlyph(block[0], block[1], block[2]);
		});
	}

	function undoWithoutSending() {
		for (var i = currentUndo.length - 1; i >= 0; i--) {
			var undo = currentUndo.pop();
			imageData[undo[0]] = undo[1];
		}
		drawHistory = [];
	}

	function drawEntryPoint(callback, optimise) {
		var blocks = [];
		callback(function(charCode, foreground, background, x, y) {
			var index = y * columns + x;
			blocks.push([index, x, y]);
			draw(index, charCode, foreground, background, x, y);
		});
		if (optimise) {
			optimiseBlocks(blocks);
		}
		drawBlocks(blocks);
		sendDrawHistory();
	}

	function drawHalfBlockEntryPoint(callback) {
		var blocks = [];
		callback(function(foreground, x, y) {
			var textY = Math.floor(y / 2);
			var index = textY * columns + x;
			blocks.push([index, x, textY]);
			drawHalfBlock(index, foreground, x, y, textY);
		});
		optimiseBlocks(blocks);
		drawBlocks(blocks);
		sendDrawHistory();
	}

	function deleteArea(x, y, width, height, background) {
		var maxWidth = x + width;
		var maxHeight = y + height;
		drawEntryPoint(function(draw) {
			for (var dy = y; dy < maxHeight; dy++) {
				for (var dx = x; dx < maxWidth; dx++) {
					draw(0, 0, background, dx, dy);
				}
			}
		});
	}

	function getArea(x, y, width, height) {
		var data = new Uint16Array(width * height);
		for (var dy = 0, j = 0; dy < height; dy++) {
			for (var dx = 0; dx < width; dx++, j++) {
				var i = (y + dy) * columns + (x + dx);
				data[j] = imageData[i];
			}
		}
		return {
			"data": data,
			"width": width,
			"height": height
		};
	}

	function setArea(area, x, y) {
		var maxWidth = Math.min(area.width, columns - x);
		var maxHeight = Math.min(area.height, rows - y);
		drawEntryPoint(function(draw) {
			for (var py = 0; py < maxHeight; py++) {
				for (var px = 0; px < maxWidth; px++) {
					var attrib = area.data[py * area.width + px];
					draw(attrib >> 8, attrib & 15, (attrib >> 4) & 15, x + px, y + py);
				}
			}
		});
	}

	function quickDraw(blocks) {
		blocks.forEach((block) => {
			if (imageData[block[0]] !== block[1]) {
				imageData[block[0]] = block[1];
				if (iceColours === false) {
					updateBeforeBlinkFlip(block[2], block[3]);
				}
				redrawGlyph(block[0], block[2], block[3]);
			}
		});
	}

	return {
		"resize": resize,
		"redrawEntireImage": redrawEntireImage,
		"setFont": setFont,
		"getIceColours": getIceColours,
		"setIceColours": setIceColours,
		"getImage": getImage,
		"getImageData": getImageData,
		"setImageData": setImageData,
		"getColumns": getColumns,
		"getRows": getRows,
		"clear": clear,
		"draw": drawEntryPoint,
		"getBlock": getBlock,
		"getHalfBlock": getHalfBlock,
		"drawHalfBlock": drawHalfBlockEntryPoint,
		"startUndo": startUndo,
		"undo": undo,
		"redo": redo,
		"deleteArea": deleteArea,
		"getArea": getArea,
		"setArea": setArea,
		"quickDraw": quickDraw
	};
}

Download

raw zip tar