moebius-web

web based ansi art editor

moebius-web

public/scripts/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 = [];

		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 mouseDown(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.ctrlKey === false && evt.which != 3) {
						palette.setForegroundColour(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("mousedown", mouseDown);
		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("mousedown", (evt) => {
				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("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("mouseup", (evt) => {
				evt.preventDefault();
				if (mouseButton === true) {
						mouseButton = false;
						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