of the image.
+ function uncompress(file, width, height) {
+ var uncompressed, p, repeatAttr, repeatChar, count;
+ // Initialize the data used to store the image, each text character has two bytes, one for the character code, and the other for the attribute.
+ uncompressed = new Uint8Array(width * height * 2);
+ i = 0;
+ while (i < uncompressed.length) {
+ p = file.get(); // , the current code under inspection.
+ count = p & 63; // , the times data is repeated
+ switch (p >> 6) { // Look at which RLE scheme to use
+ case 1: // Handle repeated character code.
+ for (repeatChar = file.get(), j = 0; j <= count; ++j) {
+ uncompressed[i++] = repeatChar;
+ uncompressed[i++] = file.get();
+ }
+ break;
+ case 2: // Handle repeated attributes.
+ for (repeatAttr = file.get(), j = 0; j <= count; ++j) {
+ uncompressed[i++] = file.get();
+ uncompressed[i++] = repeatAttr;
+ }
+ break;
+ case 3: // Handle repeated character code and attributes.
+ for (repeatChar = file.get(), repeatAttr = file.get(), j = 0; j <= count; ++j) {
+ uncompressed[i++] = repeatChar;
+ uncompressed[i++] = repeatAttr;
+ }
+ break;
+ default: // Handle no RLE.
+ for (j = 0; j <= count; ++j) {
+ uncompressed[i++] = file.get();
+ uncompressed[i++] = file.get();
+ }
+ }
+ }
+ return uncompressed; // Return the final, data.
+ }
+
+ // Convert the bytes to a File() object, and reader the settings in the header, by calling XBinHeader().
+ file = new File(bytes);
+ header = new XBinHeader(file);
+
+ // If palette information is included, read it immediately after the header, if not, use the default palette used for BIN files.
+ if (header.palette) {
+ file.read(48);
+ }
+ // If font information is included, read it, if not, use the default 80x25 font.
+ if (header.font) {
+ file.read(header.fontHeight * 256);
+ }
+ // Fetch the image data, and uncompress if necessary.
+ imageData = header.compressed ? uncompress(file, header.width, header.height) : file.read(header.width * header.height * 2);
+
+ output = new Uint8Array(imageData.length / 2 * 3);
+
+ for (i = 0, j = 0; i < imageData.length; i += 2, j += 3) {
+ output[j] = imageData[i];
+ output[j + 1] = imageData[i + 1] & 15;
+ output[j + 2] = imageData[i + 1] >> 4;
+ }
+
+ return {
+ "width": header.width,
+ "height": header.height,
+ "data": output
+ };
+ }
+
+ function loadFile(file, callback, palette, codepage, noblink) {
+ var extension, reader;
+ extension = file.name.split(".").pop().toLowerCase();
+ reader = new FileReader();
+ reader.onload = function(data) {
+ switch (extension) {
+ case "png":
+ case "gif":
+ case "jpg":
+ case "jpeg":
+ loadImg(data.target.result, callback, palette, codepage, noblink);
+ break;
+ case "xb":
+ callback(loadXbin(new Uint8Array(data.target.result)));
+ break;
+ default:
+ callback(loadAnsi(new Uint8Array(data.target.result)));
+ }
+ };
+ switch (extension) {
+ case "png":
+ case "gif":
+ case "jpg":
+ case "jpeg":
+ reader.readAsDataURL(file);
+ break;
+ default:
+ reader.readAsArrayBuffer(file);
+ }
+ }
+
+ return {
+ "loadFile": loadFile
+ };
+}());
diff --git a/js/network.js b/js/network.js
new file mode 100644
index 0000000..3e64bc3
--- /dev/null
+++ b/js/network.js
@@ -0,0 +1,303 @@
+function createWorkerHandler(inputHandle) {
+ "use strict";
+ var worker = new Worker("js/worker.js");
+ var handle = localStorage.getItem("handle");
+ if (handle === null) {
+ handle = "Anonymous";
+ localStorage.setItem("handle", handle);
+ }
+ inputHandle.value = handle;
+ var connected = false;
+ worker.postMessage({ "cmd": "handle", "handle": handle });
+ showOverlay($("websocket-overlay"));
+
+ function onConnected() {
+ var excludedElements = document.getElementsByClassName("excluded-for-websocket");
+ for (var i = 0; i < excludedElements.length; i++) {
+ excludedElements[i].style.display = "none";
+ }
+ var includedElement = document.getElementsByClassName("included-for-websocket");
+ for (var i = 0; i < includedElement.length; i++) {
+ includedElement[i].style.display = "block";
+ }
+ title.setName(window.location.hostname);
+ worker.postMessage({ "cmd": "join", "handle": handle });
+ connected = true;
+ }
+
+ function onDisconnected() {
+ if (connected === true) {
+ alert("You were disconnected from the server, try refreshing the page to try again.");
+ } else {
+ hideOverlay($("websocket-overlay"));
+ }
+ connected = false;
+ }
+
+ function onImageData(columns, rows, data, iceColours, letterSpacing) {
+ textArtCanvas.setImageData(columns, rows, data, iceColours, letterSpacing);
+ hideOverlay($("websocket-overlay"));
+ }
+
+ function onChat(handle, text, showNotification) {
+ chat.addConversation(handle, text, showNotification);
+ }
+
+ function onJoin(handle, sessionID, showNotification) {
+ chat.join(handle, sessionID, showNotification);
+ }
+
+ function onPart(sessionID) {
+ chat.part(sessionID);
+ }
+
+ function onNick(handle, sessionID, showNotification) {
+ chat.nick(handle, sessionID, showNotification);
+ }
+
+ function onDraw(blocks) {
+ textArtCanvas.quickDraw(blocks);
+ }
+
+ function onMessage(msg) {
+ var data = msg.data;
+ switch (data.cmd) {
+ case "connected":
+ onConnected();
+ break;
+ case "disconnected":
+ onDisconnected();
+ break;
+ case "imageData":
+ onImageData(data.columns, data.rows, new Uint16Array(data.data), data.iceColours, data.letterSpacing);
+ break;
+ case "chat":
+ onChat(data.handle, data.text, data.showNotification);
+ break;
+ case "join":
+ onJoin(data.handle, data.sessionID, data.showNotification);
+ break;
+ case "part":
+ onPart(data.sessionID);
+ break;
+ case "nick":
+ onNick(data.handle, data.sessionID, data.showNotification);
+ break;
+ case "draw":
+ onDraw(data.blocks);
+ break;
+ }
+ }
+
+ function draw(blocks) {
+ worker.postMessage({ "cmd": "draw", "blocks": blocks });
+ }
+
+ function setHandle(newHandle) {
+ if (handle !== newHandle) {
+ handle = newHandle;
+ localStorage.setItem("handle", handle);
+ worker.postMessage({ "cmd": "nick", "handle": handle });
+ }
+ }
+
+ function sendChat(text) {
+ worker.postMessage({ "cmd": "chat", "text": text });
+ }
+
+ function isConnected() {
+ return connected;
+ }
+
+ worker.addEventListener("message", onMessage);
+ worker.postMessage({ "cmd": "connect", "url": "wss://" + window.location.hostname + ":1337" + window.location.pathname });
+
+ return {
+ "draw": draw,
+ "setHandle": setHandle,
+ "sendChat": sendChat,
+ "isConnected": isConnected
+ };
+}
+
+function createChatController(divChatButton, divChatWindow, divMessageWindow, divUserList, inputHandle, inputMessage, inputNotificationCheckbox, onFocusCallback, onBlurCallback) {
+ "use strict";
+ var enabled = false;
+ var userList = {};
+ var notifications = localStorage.getItem("notifications");
+ if (notifications === null) {
+ notifications = false;
+ localStorage.setItem("notifications", notifications);
+ } else {
+ notifications = JSON.parse(notifications);
+ }
+ inputNotificationCheckbox.checked = notifications;
+
+ function scrollToBottom() {
+ var rect = divMessageWindow.getBoundingClientRect();
+ divMessageWindow.scrollTop = divMessageWindow.scrollHeight - rect.height;
+ }
+
+ function newNotification(text) {
+ var notification = new Notification(title.getName() + " - ANSiEdit", {
+ "body": text,
+ "icon": "../images/face.png"
+ });
+ setTimeout(() => {
+ notification.close();
+ }, 7000);
+ }
+
+ function addConversation(handle, text, showNotification) {
+ var div = document.createElement("DIV");
+ var spanHandle = document.createElement("SPAN");
+ var spanSeperator = document.createElement("SPAN");
+ var spanText = document.createElement("SPAN");
+ spanHandle.textContent = handle;
+ spanHandle.classList.add("handle");
+ spanSeperator.textContent = " ";
+ spanText.textContent = text;
+ div.appendChild(spanHandle);
+ div.appendChild(spanSeperator);
+ div.appendChild(spanText);
+ var rect = divMessageWindow.getBoundingClientRect();
+ var doScroll = (rect.height > divMessageWindow.scrollHeight) || (divMessageWindow.scrollTop === divMessageWindow.scrollHeight - rect.height);
+ divMessageWindow.appendChild(div);
+ if (doScroll) {
+ scrollToBottom();
+ }
+ if (showNotification === true && enabled === false && divChatButton.classList.contains("notification") === false) {
+ divChatButton.classList.add("notification");
+ }
+ }
+
+ function onFocus() {
+ onFocusCallback();
+ }
+
+ function onBlur() {
+ onBlurCallback();
+ }
+
+ function blurHandle(evt) {
+ if (inputHandle.value === "") {
+ inputHandle.value = "Anonymous";
+ }
+ worker.setHandle(inputHandle.value);
+ }
+
+ function keypressHandle(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 13) {
+ inputMessage.focus();
+ }
+ }
+
+ function keypressMessage(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 13) {
+ if (inputMessage.value !== "") {
+ var text = inputMessage.value;
+ inputMessage.value = "";
+ worker.sendChat(text);
+ }
+ }
+ }
+
+ inputHandle.addEventListener("focus", onFocus);
+ inputHandle.addEventListener("blur", onBlur);
+ inputMessage.addEventListener("focus", onFocus);
+ inputMessage.addEventListener("blur", onBlur);
+ inputHandle.addEventListener("blur", blurHandle);
+ inputHandle.addEventListener("keypress", keypressHandle);
+ inputMessage.addEventListener("keypress", keypressMessage);
+
+ function toggle() {
+ if (enabled === true) {
+ divChatWindow.style.display = "none";
+ enabled = false;
+ onBlurCallback();
+ divChatButton.classList.remove("active");
+ } else {
+ divChatWindow.style.display = "block";
+ enabled = true;
+ scrollToBottom();
+ onFocusCallback();
+ inputMessage.focus();
+ divChatButton.classList.remove("notification");
+ divChatButton.classList.add("active");
+ }
+ }
+
+ function isEnabled() {
+ return enabled;
+ }
+
+ function join(handle, sessionID, showNotification) {
+ if (userList[sessionID] === undefined) {
+ if (notifications === true && showNotification === true) {
+ newNotification(handle + " has joined");
+ }
+ userList[sessionID] = { "handle": handle, "div": document.createElement("DIV") };
+ userList[sessionID].div.classList.add("user-name");
+ userList[sessionID].div.textContent = handle;
+ divUserList.appendChild(userList[sessionID].div);
+ }
+ }
+
+ function nick(handle, sessionID, showNotification) {
+ if (userList[sessionID] !== undefined) {
+ if (showNotification === true && notifications === true) {
+ newNotification(userList[sessionID].handle + " has changed their name to " + handle);
+ }
+ userList[sessionID].handle = handle;
+ userList[sessionID].div.textContent = handle;
+ }
+ }
+
+ function part(sessionID) {
+ if (userList[sessionID] !== undefined) {
+ if (notifications === true) {
+ newNotification(userList[sessionID].handle + " has left");
+ }
+ divUserList.removeChild(userList[sessionID].div);
+ delete userList[sessionID];
+ }
+ }
+
+ function globalToggleKeydown(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 27) {
+ toggle();
+ }
+ }
+
+ function notificationCheckboxClicked(evt) {
+ if (inputNotificationCheckbox.checked) {
+ if (Notification.permission !== "granted") {
+ Notification.requestPermission((permission) => {
+ notifications = true;
+ localStorage.setItem("notifications", notifications);
+ });
+ } else {
+ notifications = true;
+ localStorage.setItem("notifications", notifications);
+ }
+ } else {
+ notifications = false;
+ localStorage.setItem("notifications", notifications);
+ }
+ }
+
+ document.addEventListener("keydown", globalToggleKeydown);
+ inputNotificationCheckbox.addEventListener("click", notificationCheckboxClicked);
+
+ return {
+ "addConversation": addConversation,
+ "toggle": toggle,
+ "isEnabled": isEnabled,
+ "join": join,
+ "nick": nick,
+ "part": part
+ };
+}
diff --git a/js/savers.js b/js/savers.js
new file mode 100644
index 0000000..6194c8b
--- /dev/null
+++ b/js/savers.js
@@ -0,0 +1,95 @@
+var Savers = (function() {
+ "use strict";
+
+ // function toANSFormat(input) {
+ // var highest, inputIndex, end, charCode, fg, bg, bold, blink, currentFg, currentBg, currentBold, currentBlink, attribs, attribIndex, output;
+ //
+ // function ansiColor(binColor) {
+ // switch (binColor) {
+ // case 1:
+ // return 4;
+ // case 3:
+ // return 6;
+ // case 4:
+ // return 1;
+ // case 6:
+ // return 3;
+ // default:
+ // return binColor;
+ // }
+ // }
+ //
+ // highest = getHighestRow(input);
+ // output = [27, 91, 48, 109];
+ // for (inputIndex = 0, end = highest * 80 * 3, currentFg = 7, currentBg = 0, currentBold = false, currentBlink = false; inputIndex < end; inputIndex += 3) {
+ // attribs = [];
+ // charCode = input[inputIndex];
+ // fg = input[inputIndex + 1];
+ // bg = input[inputIndex + 2];
+ // if (fg > 7) {
+ // bold = true;
+ // fg = fg - 8;
+ // } else {
+ // bold = false;
+ // }
+ // if (bg > 7) {
+ // blink = true;
+ // bg = bg - 8;
+ // } else {
+ // blink = false;
+ // }
+ // if ((currentBold && !bold) || (currentBlink && !blink)) {
+ // attribs.push([48]);
+ // currentFg = 7;
+ // currentBg = 0;
+ // currentBold = false;
+ // currentBlink = false;
+ // }
+ // if (bold && !currentBold) {
+ // attribs.push([49]);
+ // currentBold = true;
+ // }
+ // if (blink && !currentBlink) {
+ // attribs.push([53]);
+ // currentBlink = true;
+ // }
+ // if (fg !== currentFg) {
+ // attribs.push([51, 48 + ansiColor(fg)]);
+ // currentFg = fg;
+ // }
+ // if (bg !== currentBg) {
+ // attribs.push([52, 48 + ansiColor(bg)]);
+ // currentBg = bg;
+ // }
+ // if (attribs.length) {
+ // output.push(27, 91);
+ // for (attribIndex = 0; attribIndex < attribs.length; ++attribIndex) {
+ // output = output.concat(attribs[attribIndex]);
+ // if (attribIndex !== attribs.length - 1) {
+ // output.push(59);
+ // } else {
+ // output.push(109);
+ // }
+ // }
+ // }
+ // output.push(charCode);
+ // }
+ // return new Uint8Array(output);
+ // }
+
+ function imageDataToDataURL(imageData, noblink) {
+ var bytes, i, j, flags;
+ bytes = new Uint8Array((imageData.width * imageData.height * 2) + 11);
+ flags = noblink ? 8 : 0;
+ bytes.set(new Uint8Array([88, 66, 73, 78, 26, (imageData.width & 0xff), (imageData.width >> 8), (imageData.height & 0xff), (imageData.height >> 8), 16, flags]), 0);
+ for (i = 0, j = 11; i < imageData.data.length; i += 3, j += 2) {
+ bytes[j] = imageData.data[i];
+ bytes[j + 1] = imageData.data[i + 1] + (imageData.data[i + 2] << 4);
+ }
+ return "data:image/x-bin;base64," + btoa(String.fromCharCode.apply(null, bytes));
+ }
+
+ return {
+ "imageDataToDataURL": imageDataToDataURL
+ };
+}());
diff --git a/js/ui.js b/js/ui.js
new file mode 100644
index 0000000..85e8f96
--- /dev/null
+++ b/js/ui.js
@@ -0,0 +1,462 @@
+function createSettingToggle(divButton, getter, setter) {
+ "use strict";
+ var currentSetting;
+
+ function update() {
+ currentSetting = getter();
+ if (currentSetting === true) {
+ divButton.classList.add("enabled");
+ } else {
+ divButton.classList.remove("enabled");
+ }
+ }
+
+ function changeSetting(evt) {
+ evt.preventDefault();
+ currentSetting = !currentSetting;
+ setter(currentSetting);
+ update();
+ }
+
+ divButton.addEventListener("click", changeSetting);
+ update();
+
+ return {
+ "update": update
+ };
+}
+
+var Toolbar = (function() {
+ "use strict";
+ var currentButton;
+ var currentOnBlur;
+
+ function add(divButton, onFocus, onBlur) {
+ function enable() {
+ if (currentButton !== divButton) {
+ if (currentButton !== undefined) {
+ currentButton.classList.remove("toolbar-displayed");
+ }
+ if (currentOnBlur !== undefined) {
+ currentOnBlur();
+ }
+ divButton.classList.add("toolbar-displayed");
+ currentButton = divButton;
+ currentOnBlur = onBlur;
+ if (onFocus !== undefined) {
+ onFocus();
+ }
+ }
+ }
+ divButton.addEventListener("click", (evt) => {
+ evt.preventDefault();
+ enable();
+ });
+ return {
+ "enable": enable
+ };
+ }
+
+ return {
+ "add": add
+ };
+}());
+
+function onReturn(divElement, divTarget) {
+ "use strict";
+ divElement.addEventListener("keypress", (evt) => {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.altKey === false && evt.ctrlKey === false && evt.metaKey === false && keyCode === 13) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ divTarget.click();
+ }
+ });
+}
+
+function onClick(divElement, func) {
+ "use strict";
+ divElement.addEventListener("click", (evt) => {
+ evt.preventDefault();
+ func(divElement);
+ });
+}
+
+function onFileChange(divElement, func) {
+ "use strict";
+ divElement.addEventListener("change", (evt) => {
+ if (evt.target.files.length > 0) {
+ func(evt.target.files[0]);
+ }
+ });
+}
+
+function onSelectChange(divElement, func) {
+ "use strict";
+ divElement.addEventListener("change", (evt) => {
+ func(divElement.value);
+ });
+}
+
+function createPositionInfo(divElement) {
+ "use strict";
+ function update(x, y) {
+ divElement.textContent = (x + 1) + ", " + (y + 1);
+ }
+
+ return {
+ "update": update
+ };
+}
+
+function showOverlay(divElement) {
+ "use strict";
+ divElement.classList.add("enabled");
+}
+
+function hideOverlay(divElement) {
+ "use strict";
+ divElement.classList.remove("enabled");
+}
+
+function undoAndRedo(evt) {
+ "use strict";
+ var keyCode = (evt.keyCode || evt.which);
+ if ((evt.ctrlKey === true || (evt.metaKey === true && evt.shiftKey === false)) && keyCode === 90) {
+ evt.preventDefault();
+ textArtCanvas.undo();
+ } else if ((evt.ctrlKey === true && evt.keyCode === 89) || (evt.metaKey === true && evt.shiftKey === true && keyCode === 90)) {
+ evt.preventDefault();
+ textArtCanvas.redo();
+ }
+}
+
+function createTitleHandler(inputElement, onFocusCallback, onBlurCallback) {
+ "use strict";
+ function updateTitle() {
+ document.title = inputElement.value + " - moebius";
+ }
+
+ function onFocus() {
+ onFocusCallback();
+ }
+
+ function onBlur() {
+ onBlurCallback();
+ updateTitle();
+ }
+
+ function keyPress(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 13) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ if (inputElement.value === "") {
+ inputElement.value = "untitled";
+ }
+ inputElement.blur();
+ }
+ }
+
+ function setName(newName) {
+ inputElement.value = newName;
+ updateTitle();
+ }
+
+ function getName() {
+ return inputElement.value;
+ }
+
+ function reset() {
+ setName("untitled");
+ }
+
+ inputElement.addEventListener("focus", onFocus);
+ inputElement.addEventListener("blur", onBlur);
+ inputElement.addEventListener("keypress", keyPress);
+ reset();
+
+ return {
+ "getName": getName,
+ "setName": setName,
+ "reset": reset
+ };
+}
+
+function createPaintShortcuts(keyPair) {
+ "use strict";
+ var ignored = false;
+
+ function keyDown(evt) {
+ if (ignored === false) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.ctrlKey === false && evt.altKey === false && evt.shiftKey === false && evt.metaKey === false) {
+ if (keyCode >= 48 && keyCode <= 55) {
+ var colour = keyCode - 48;
+ var currentColour = palette.getForegroundColour();
+ if (currentColour === colour) {
+ palette.setForegroundColour(colour + 8);
+ } else {
+ palette.setForegroundColour(colour);
+ }
+ } else {
+ var charCode = String.fromCharCode(keyCode);
+ if (keyPair[charCode] !== undefined) {
+ if (worker.isConnected() === false || keyPair[charCode].classList.contains("excluded-for-websocket") === false) {
+ evt.preventDefault();
+ keyPair[charCode].click();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function keyDownWithCtrl(evt) {
+ if (ignored === false) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.ctrlKey === true && evt.altKey === false && evt.shiftKey === false && evt.metaKey === false) {
+ var charCode = String.fromCharCode(keyCode);
+ if (keyPair[charCode] !== undefined) {
+ if (worker.isConnected() === false || keyPair[charCode].classList.contains("excluded-for-websocket") === false) {
+ evt.preventDefault();
+ keyPair[charCode].click();
+ }
+ }
+ }
+ }
+ }
+
+ document.addEventListener("keydown", keyDownWithCtrl);
+
+ function enable() {
+ document.addEventListener("keydown", keyDown);
+ }
+
+ function disable() {
+ document.removeEventListener("keydown", keyDown);
+ }
+
+ function ignore() {
+ ignored = true;
+ }
+
+ function unignore() {
+ ignored = false;
+ }
+
+ enable();
+
+ return {
+ "enable": enable,
+ "disable": disable,
+ "ignore": ignore,
+ "unignore": unignore
+ };
+}
+
+function createToggleButton(stateOneName, stateTwoName, stateOneClick, stateTwoClick) {
+ "use strict";
+ var divContainer = document.createElement("DIV");
+ divContainer.classList.add("toggle-button-container");
+ var stateOne = document.createElement("DIV");
+ stateOne.classList.add("toggle-button");
+ stateOne.classList.add("left");
+ stateOne.textContent = stateOneName;
+ var stateTwo = document.createElement("DIV");
+ stateTwo.classList.add("toggle-button");
+ stateTwo.classList.add("right");
+ stateTwo.textContent = stateTwoName;
+ divContainer.appendChild(stateOne);
+ divContainer.appendChild(stateTwo);
+
+ function getElement() {
+ return divContainer;
+ }
+
+ function setStateOne() {
+ stateOne.classList.add("enabled");
+ stateTwo.classList.remove("enabled");
+ }
+
+ function setStateTwo() {
+ stateTwo.classList.add("enabled");
+ stateOne.classList.remove("enabled");
+ }
+
+ stateOne.addEventListener("click", (evt) => {
+ setStateOne();
+ stateOneClick();
+ });
+
+ stateTwo.addEventListener("click", (evt) => {
+ setStateTwo();
+ stateTwoClick();
+ });
+
+ return {
+ "getElement": getElement,
+ "setStateOne": setStateOne,
+ "setStateTwo": setStateTwo
+ };
+}
+
+function createGrid(divElement) {
+ "use strict";
+ var canvases = [];
+ var enabled = false;
+
+ function createCanvases() {
+ var fontWidth = font.getWidth();
+ var fontHeight = font.getHeight();
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var canvasWidth = fontWidth * columns;
+ var canvasHeight = fontHeight * 25;
+ canvases = [];
+ for (var i = 0; i < Math.floor(rows / 25); i++) {
+ var canvas = createCanvas(canvasWidth, canvasHeight);
+ canvases.push(canvas);
+ }
+ if (rows % 25 !== 0) {
+ var canvas = createCanvas(canvasWidth, fontHeight * (rows % 25));
+ canvases.push(canvas);
+ }
+ }
+
+ function renderGrid(canvas) {
+ var columns = textArtCanvas.getColumns();
+ var rows = Math.min(textArtCanvas.getRows(), 25);
+ var fontWidth = canvas.width / columns;
+ var fontHeight = font.getHeight();
+ var ctx = canvas.getContext("2d");
+ var imageData = ctx.createImageData(canvas.width, canvas.height);
+ var byteWidth = canvas.width * 4;
+ var darkGray = new Uint8Array([63, 63, 63, 255]);
+ for (var y = 0; y < rows; y += 1) {
+ for (var x = 0, i = y * fontHeight * byteWidth; x < canvas.width; x += 1, i += 4) {
+ imageData.data.set(darkGray, i);
+ }
+ }
+ for (var x = 0; x < columns; x += 1) {
+ for (var y = 0, i = x * fontWidth * 4; y < canvas.height; y += 1, i += byteWidth) {
+ imageData.data.set(darkGray, i);
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ }
+
+ function createGrid() {
+ createCanvases();
+ renderGrid(canvases[0]);
+ divElement.appendChild(canvases[0]);
+ for (var i = 1; i < canvases.length; i++) {
+ canvases[i].getContext("2d").drawImage(canvases[0], 0, 0);
+ divElement.appendChild(canvases[i]);
+ }
+ }
+
+ function resize() {
+ canvases.forEach((canvas) => {
+ divElement.removeChild(canvas);
+ });
+ createGrid();
+ }
+
+ createGrid();
+
+ document.addEventListener("onTextCanvasSizeChange", resize);
+ document.addEventListener("onLetterSpacingChange", resize);
+ document.addEventListener("onFontChange", resize);
+ document.addEventListener("onOpenedFile", resize);
+
+ function isShown() {
+ return enabled;
+ }
+
+ function show(turnOn) {
+ if (enabled === true && turnOn === false) {
+ divElement.classList.remove("enabled");
+ enabled = false;
+ } else if (enabled === false && turnOn === true) {
+ divElement.classList.add("enabled");
+ enabled = true;
+ }
+ }
+
+ return {
+ "isShown": isShown,
+ "show": show
+ };
+}
+
+function createToolPreview(divElement) {
+ "use strict";
+ var canvases = [];
+ var ctxs = [];
+
+ function createCanvases() {
+ var fontWidth = font.getWidth();
+ var fontHeight = font.getHeight();
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var canvasWidth = fontWidth * columns;
+ var canvasHeight = fontHeight * 25;
+ canvases = new Array();
+ ctxs = new Array();
+ for (var i = 0; i < Math.floor(rows / 25); i++) {
+ var canvas = createCanvas(canvasWidth, canvasHeight);
+ canvases.push(canvas);
+ ctxs.push(canvas.getContext("2d"));
+ }
+ if (rows % 25 !== 0) {
+ var canvas = createCanvas(canvasWidth, fontHeight * (rows % 25));
+ canvases.push(canvas);
+ ctxs.push(canvas.getContext("2d"));
+ }
+ canvases.forEach((canvas) => {
+ divElement.appendChild(canvas);
+ });
+ }
+
+ function resize() {
+ canvases.forEach((canvas) => {
+ divElement.removeChild(canvas);
+ });
+ createCanvases();
+ }
+
+ function drawHalfBlock(foreground, x, y) {
+ var halfBlockY = y % 2;
+ var textY = Math.floor(y / 2);
+ var ctxIndex = Math.floor(textY / 25);
+ if (ctxIndex >= 0 && ctxIndex < ctxs.length) {
+ font.drawWithAlpha((halfBlockY === 0) ? 223 : 220, foreground, ctxs[ctxIndex], x, textY % 25);
+ }
+ }
+
+ function clear() {
+ for (var i = 0; i < ctxs.length; i++) {
+ ctxs[i].clearRect(0, 0, canvases[i].width, canvases[i].height);
+ }
+ }
+
+ createCanvases();
+ divElement.classList.add("enabled");
+
+ document.addEventListener("onTextCanvasSizeChange", resize);
+ document.addEventListener("onLetterSpacingChange", resize);
+ document.addEventListener("onFontChange", resize);
+ document.addEventListener("onOpenedFile", resize);
+
+ return {
+ "clear": clear,
+ "drawHalfBlock": drawHalfBlock,
+ };
+}
+
+function menuHover() {
+ $("file-menu").classList.remove("hover");
+ $("edit-menu").classList.remove("hover");
+ $("view-menu").classList.remove("hover");
+}
diff --git a/js/worker.js b/js/worker.js
new file mode 100644
index 0000000..055df58
--- /dev/null
+++ b/js/worker.js
@@ -0,0 +1,135 @@
+var socket;
+var sessionID;
+var joint;
+var connected = false;
+
+function send(cmd, msg) {
+ socket.send(JSON.stringify([cmd, msg]));
+}
+
+function onOpen() {
+ postMessage({ "cmd": "connected" });
+}
+
+function onClose(evt) {
+ postMessage({ "cmd": "disconnected" });
+}
+
+function onChat(handle, text, showNotification) {
+ postMessage({ "cmd": "chat", "handle": handle, "text": text, "showNotification": showNotification });
+}
+
+function onStart(msg, newSessionID) {
+ joint = msg;
+ sessionID = newSessionID;
+ msg.chat.forEach((msg) => {
+ onChat(msg[0], msg[1], false);
+ });
+}
+
+function onJoin(handle, joinSessionID, showNotification) {
+ if (joinSessionID === sessionID) {
+ showNotification = false;
+ }
+ postMessage({ "cmd": "join", "sessionID": joinSessionID, "handle": handle, "showNotification": showNotification });
+}
+
+function onNick(handle, nickSessionID) {
+ postMessage({ "cmd": "nick", "sessionID": nickSessionID, "handle": handle, "showNotification": (nickSessionID !== sessionID) });
+}
+
+function onPart(sessionID) {
+ postMessage({ "cmd": "part", "sessionID": sessionID });
+}
+
+function onDraw(blocks) {
+ var outputBlocks = new Array();
+ var index;
+ blocks.forEach((block) => {
+ index = block >> 16;
+ outputBlocks.push([index, block & 0xffff, index % joint.columns, Math.floor(index / joint.columns)]);
+ });
+ postMessage({ "cmd": "draw", "blocks": outputBlocks });
+}
+
+function onMessage(evt) {
+ var data = evt.data;
+ if (typeof (data) === "object") {
+ var fr = new FileReader();
+ fr.addEventListener("load", (evt) => {
+ postMessage({ "cmd": "imageData", "data": evt.target.result, "columns": joint.columns, "rows": joint.rows, "iceColours": joint.iceColours, "letterSpacing": joint.letterSpacing });
+ connected = true;
+ });
+ fr.readAsArrayBuffer(data);
+ } else {
+ data = JSON.parse(data);
+ switch (data[0]) {
+ case "start":
+ sessionID = data[2];
+ var userList = data[3];
+ Object.keys(userList).forEach((userSessionID) => {
+ onJoin(userList[userSessionID], userSessionID, false);
+ });
+ onStart(data[1], data[2]);
+ break;
+ case "join":
+ onJoin(data[1], data[2], true);
+ break;
+ case "nick":
+ onNick(data[1], data[2]);
+ break;
+ case "draw":
+ onDraw(data[1]);
+ break;
+ case "part":
+ onPart(data[1]);
+ break;
+ case "chat":
+ onChat(data[1], data[2], true);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+function removeDuplicates(blocks) {
+ var indexes = [];
+ var index;
+ blocks = blocks.reverse();
+ blocks = blocks.filter((block) => {
+ index = block >> 16;
+ if (indexes.lastIndexOf(index) === -1) {
+ indexes.push(index);
+ return true;
+ }
+ return false;
+ });
+ return blocks.reverse();
+}
+
+self.onmessage = function(msg) {
+ var data = msg.data;
+ switch (data.cmd) {
+ case "connect":
+ socket = new WebSocket(data.url);
+ socket.addEventListener("open", onOpen);
+ socket.addEventListener("message", onMessage);
+ socket.addEventListener("close", onClose);
+ break;
+ case "join":
+ send("join", data.handle);
+ break;
+ case "nick":
+ send("nick", data.handle);
+ break;
+ case "chat":
+ send("chat", data.text);
+ break;
+ case "draw":
+ send("draw", removeDuplicates(data.blocks));
+ break;
+ default:
+ break;
+ }
+};
diff --git a/public/css/style.css b/public/css/style.css
new file mode 100644
index 0000000..6937e13
--- /dev/null
+++ b/public/css/style.css
@@ -0,0 +1,574 @@
+* {
+ -webkit-touch-callout: none !important;
+ user-select: none !important;
+ -ms-user-select: none !important;
+ -webkit-user-select: none !important;
+}
+
+html,
+body,
+body>div {
+ width: 100%;
+ height: 100%;
+ margin: 0px;
+}
+
+body {
+ position: fixed;
+ overflow: hidden;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+}
+
+#body-container {
+ min-width: 960px;
+ min-height: 480px;
+ overflow: auto;
+}
+
+#top-toolbar,
+#left-toolbar,
+#bottom-toolbar,
+#position-info {
+ background-color: rgb(63, 63, 63);
+}
+
+#top-toolbar {
+ float: left;
+ width: 100%;
+ height: 20px;
+ display: block;
+}
+
+#artwork-title {
+ width: 300px;
+ margin: 0px;
+ margin-left: calc(50% - 151px - 120px);
+ border-left: 1px solid rgb(31, 31, 31);
+ border-right: 1px solid rgb(31, 31, 31);
+ border-top: none;
+ border-bottom: none;
+}
+
+#artwork-title {
+ text-align: center;
+ padding: 0px;
+ background: none;
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 12px;
+ line-height: 20px;
+ color: rgb(200, 200, 200);
+ background-color: rgb(95, 95, 95);
+ outline: none;
+}
+
+#chat-button.notification {
+ background-color: rgb(96, 127, 127);
+}
+
+#chat-button {
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 10px;
+ color: white;
+ line-height: 19px;
+ float: right;
+ padding: 0px 8px;
+ cursor: pointer;
+}
+
+#chat-icon-close-text {
+ display: none;
+}
+
+#chat-button.active #chat-icon-text {
+ display: none;
+}
+
+#chat-button.active #chat-icon-close-text {
+ display: block;
+}
+
+#chat-button img {
+ vertical-align: middle;
+}
+
+#chat-window {
+ width: 640px;
+ height: 400px;
+ position: absolute;
+ right: 8px;
+ top: 20px;
+ border-bottom-left-radius: 3px;
+ border-bottom-right-radius: 3px;
+ z-index: 3;
+ box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.7);
+ background: linear-gradient(to bottom, #eeeeee 0px, white 64px, white 369px, #eeeeee 370px, white 100%);
+ display: none;
+}
+
+#user-list {
+ width: 140px;
+ height: 350px;
+ border-left: 1px solid rgb(63, 63, 63);
+ overflow: auto;
+}
+
+#user-list div {
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 12px;
+ text-align: center;
+ margin: 4px;
+ padding: 4px;
+ border-radius: 6px;
+ color: white;
+ background: linear-gradient(to bottom, #7c7 0%, #383 1px, #383 calc(100% - 1px), #040 100%);
+}
+
+#message-window {
+ float: left;
+ width: 499px;
+ height: 370px;
+ overflow: auto;
+}
+
+#message-window span {
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 12px;
+ margin: 4px;
+ font-size: 12px;
+ line-height: 20px;
+}
+
+#handle-input,
+#message-input {
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 12px;
+ border: 0px;
+ margin: 0px;
+ outline-style: none;
+ height: 21px;
+ padding: 4px 9px;
+ border-top: 1px solid rgb(63, 63, 63);
+ line-height: 20px;
+}
+
+#handle-input {
+ text-align: center;
+ background: none;
+ background-color: rgb(95, 95, 95);
+ color: white;
+ outline: none;
+ width: 82px;
+ height: 21px;
+}
+
+#message-input {
+ background: none;
+ width: 521px;
+ float: right;
+ border-left: 1px solid rgb(63, 63, 63);
+}
+
+#message-window div:first-child {
+ margin-top: 16px;
+}
+
+#message-window div:last-child {
+ margin-bottom: 16px;
+ border-bottom: none;
+}
+
+#message-window div {
+ margin: 1px 16px 1px 24px;
+ text-indent: -8px;
+ padding-bottom: 2px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+#message-window span {
+ font: normal normal normal 12px "Lucida Grande", sans-serif;
+ color: black;
+}
+
+#message-window span.handle {
+ color: red;
+}
+
+
+#artwork-title:focus {
+ background-color: rgb(127, 127, 127);
+ color: white;
+}
+
+#left-toolbar {
+ float: left;
+ width: 40px;
+ height: calc(100% - 20px);
+}
+
+#left-toolbar>canvas {
+ vertical-align: bottom;
+}
+
+#viewport {
+ margin-left: 40px;
+ width: calc(100% - 40px);
+ height: calc(100% - 20px - 20px);
+ background: rgb(48, 48, 48);
+ text-align: center;
+ overflow: auto;
+ user-select: none;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ -o-user-select: none;
+}
+
+#bottom-toolbar {
+ text-align: center;
+ width: calc(100% - 180px);
+ height: 20px;
+ float: left;
+ padding-left: 60px;
+}
+
+#position-info {
+ float: left;
+ width: 80px;
+ height: 20px;
+ text-align: center;
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 12px;
+ color: rgb(200, 200, 200);
+ line-height: 20px;
+}
+
+#canvas-container {
+ margin: 16px auto;
+ width: 640px;
+ box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.7);
+ position: relative;
+ cursor: crosshair;
+}
+
+#canvas-container canvas {
+ display: block;
+ vertical-align: bottom;
+}
+
+.canvas-overlay {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ display: none;
+}
+
+.canvas-overlay.enabled {
+ display: block;
+}
+
+canvas {
+ image-rendering: optimizeSpeed;
+ image-rendering: crisp-edges;
+ image-rendering: pixelated;
+}
+
+#bottom-toolbar>div>div.button {
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 12px;
+ color: rgb(200, 200, 200);
+ background-color: rgb(96, 96, 96);
+ cursor: pointer;
+ text-align: center;
+ box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.7);
+}
+
+#left-toolbar>div {
+ text-align: center;
+ height: 24px;
+ padding: 3px 0px;
+}
+
+#left-toolbar div.seperator {
+ height: 1px;
+ padding: 0px;
+ margin: 4px auto;
+ width: 32px;
+ background-color: rgb(96, 96, 96);
+}
+
+.toolbar-displayed {
+ background-color: rgb(96, 127, 127);
+}
+
+#bottom-toolbar {
+ overflow: hidden;
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 12px;
+ color: rgb(200, 200, 200);
+}
+
+canvas.fkey {
+ margin: 0px 8px;
+ vertical-align: middle;
+}
+
+div.menu-title {
+ font-family: "Lucida Grande", sans-serif;
+ width: 32px;
+ height: 20px;
+ display: inline-block;
+ position: relative;
+ vertical-align: top;
+ font-size: 12px;
+ color: rgb(200, 200, 200);
+ background-color: rgb(64, 64, 64);
+ line-height: 20px;
+ padding: 0px 0px 0px 8px;
+ overflow: hidden;
+}
+
+div.menu-list {
+ cursor: default;
+ position: absolute;
+ width: 160px;
+ margin-left: -8px;
+ background-color: rgb(64, 64, 64);
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-right: 1px solid rgb(127, 127, 127);
+ border-bottom: 1px solid rgb(127, 127, 127);
+ border-left: 1px solid rgb(127, 127, 127);
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.7);
+ z-index: 2;
+}
+
+div.hover,
+div.menu-title:hover {
+ background-color: rgb(96, 96, 96);
+ cursor: pointer;
+ overflow: visible;
+}
+
+div.menu-item {
+ padding: 2px 4px;
+ margin: 4px 2px;
+}
+
+div.menu-list div.seperator {
+ height: 1px;
+ background-color: rgb(127, 127, 127);
+ margin: 2px;
+}
+
+div.menu-item:hover {
+ cursor: pointer;
+ color: white;
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+div.menu-item.disabled {
+ cursor: default;
+ background-color: inherit;
+ color: inherit;
+}
+
+div.menu-item.enabled {
+ background-image: url("../images/toolbar/done.png");
+ background-position: 130px 0px;
+ background-repeat: no-repeat;
+ background-size: 20px 20px;
+}
+
+div.overlay {
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ display: none;
+ z-index: 4;
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0px, rgba(0, 0, 0, 0) 20px, rgba(0, 0, 0, 0.8) 21px, rgba(0, 0, 0, 0.2) 100%);
+}
+
+div.overlay.enabled {
+ display: block;
+}
+
+div.dialog {
+ font-family: "Lucida Grande", sans-serif;
+ color: rgb(200, 200, 200);
+ font-size: 12px;
+ position: fixed;
+ left: calc(50% - 180px);
+ top: 20px;
+ width: 360px;
+ background-color: rgb(64, 64, 64);
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-right: 1px solid rgb(127, 127, 127);
+ border-bottom: 1px solid rgb(127, 127, 127);
+ border-left: 1px solid rgb(127, 127, 127);
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.7);
+ text-align: center;
+ padding-top: 16px;
+}
+
+div.dialog p {
+ margin: 30px 0px;
+}
+
+div.dialog label {
+ display: inline-block;
+ text-align: right;
+ width: 230px;
+}
+
+div.dialog select {
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 16px;
+ display: block;
+ margin: 8px auto;
+}
+
+div.dialog input {
+ background: none;
+ outline: none;
+ color: rgb(200, 200, 200);
+ border: 1px solid rgb(127, 127, 127);
+ padding: 4px;
+ margin: 4px;
+}
+
+div.button {
+ font-family: "Lucida Grande", sans-serif;
+ color: white;
+ font-size: 12px;
+ display: inline-block;
+ width: 100px;
+ height: 24px;
+ margin: 12px 4px;
+ background: linear-gradient(to bottom, #eee 0%, #888 1px, #999 calc(100% - 1px), #777 100%);
+ color: rgba(255, 255, 255, 0.8);
+ border-radius: 4px;
+ text-align: center;
+ line-height: 24px;
+ text-shadow: rgba(0, 0, 0, 0.7) 0px 1px 1px;
+ box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.4);
+ cursor: pointer;
+}
+
+div.button:active {
+ color: rgba(255, 255, 255, 0.6);
+ background: linear-gradient(to bottom, #ccc 0%, #666 1px, #777 calc(100% - 1px), #555 100%);
+ box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.4);
+}
+
+div.floating-panel {
+ position: fixed;
+ background-color: rgb(64, 64, 64);
+ display: none;
+ padding-top: 20px;
+ padding-bottom: 4px;
+ padding-left: 4px;
+ padding-right: 4px;
+ border: 1px solid rgb(127, 127, 127);
+ border-radius: 4px;
+ cursor: move;
+ box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.3);
+}
+
+div.floating-panel div {
+ position: relative;
+ display: block;
+}
+
+div.floating-panel.enabled {
+ display: block;
+}
+
+div.floating-panel canvas {
+ cursor: crosshair;
+ vertical-align: bottom;
+}
+
+div.toggle-button-container {
+ width: 120px;
+ margin: 8px auto;
+ font-family: "Lucida Grande", sans-serif;
+ color: rgb(200, 200, 200);
+ font-size: 10px;
+ line-height: 20px;
+ text-align: center;
+}
+
+div.toggle-button {
+ width: 59px;
+ height: 20px;
+ margin: 4px auto;
+ background-color: rgb(63, 63, 63);
+ cursor: pointer;
+}
+
+div.toggle-button.enabled {
+ color: white;
+ background-color: rgb(127, 127, 127);
+}
+
+div.toggle-button.left {
+ float: left;
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+ border-top: 1px solid black;
+ border-left: 1px solid black;
+ border-bottom: 1px solid black;
+}
+
+div.toggle-button.right {
+ float: right;
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+ border-top: 1px solid black;
+ border-right: 1px solid black;
+ border-bottom: 1px solid black;
+}
+
+@keyframes blink {
+ 50% {
+ border: 1px solid rgb(192, 192, 192);
+ }
+}
+
+canvas.cursor {
+ position: absolute;
+ border: 1px solid white;
+ border-radius: 1px;
+ animation: blink 1.5s steps(1) infinite;
+ z-index: 1;
+}
+
+canvas.selection-cursor {
+ position: absolute;
+ border: 1px solid;
+ border-image: url("../images/selection_border.gif") 1 repeat;
+ border-image-width: 1px;
+ z-index: 1;
+}
+
+.included-for-websocket {
+ display: none;
+}
+
+#notification-setting {
+ width: 140px;
+ height: 20px;
+ font-family: "Lucida Grande", sans-serif;
+ font-size: 10px;
+ color: white;
+ line-height: 20px;
+ float: right;
+ border-left: 1px solid rgb(63, 63, 63);
+ background-color: rgb(95, 95, 95);
+}
diff --git a/public/fonts/CP437 8x14.png b/public/fonts/CP437 8x14.png
new file mode 100755
index 0000000..b4e19ba
Binary files /dev/null and b/public/fonts/CP437 8x14.png differ
diff --git a/public/fonts/CP437 8x16.png b/public/fonts/CP437 8x16.png
new file mode 100755
index 0000000..b96745d
Binary files /dev/null and b/public/fonts/CP437 8x16.png differ
diff --git a/public/fonts/CP437 8x19.png b/public/fonts/CP437 8x19.png
new file mode 100755
index 0000000..956143d
Binary files /dev/null and b/public/fonts/CP437 8x19.png differ
diff --git a/public/fonts/CP437 8x8.png b/public/fonts/CP437 8x8.png
new file mode 100755
index 0000000..6404c69
Binary files /dev/null and b/public/fonts/CP437 8x8.png differ
diff --git a/public/fonts/CP737 8x14.png b/public/fonts/CP737 8x14.png
new file mode 100755
index 0000000..9ac004e
Binary files /dev/null and b/public/fonts/CP737 8x14.png differ
diff --git a/public/fonts/CP737 8x16.png b/public/fonts/CP737 8x16.png
new file mode 100755
index 0000000..5dd8ac9
Binary files /dev/null and b/public/fonts/CP737 8x16.png differ
diff --git a/public/fonts/CP737 8x8.png b/public/fonts/CP737 8x8.png
new file mode 100755
index 0000000..445842e
Binary files /dev/null and b/public/fonts/CP737 8x8.png differ
diff --git a/public/fonts/CP775 8x14.png b/public/fonts/CP775 8x14.png
new file mode 100755
index 0000000..b999b39
Binary files /dev/null and b/public/fonts/CP775 8x14.png differ
diff --git a/public/fonts/CP775 8x16.png b/public/fonts/CP775 8x16.png
new file mode 100755
index 0000000..e7c1d74
Binary files /dev/null and b/public/fonts/CP775 8x16.png differ
diff --git a/public/fonts/CP775 8x8.png b/public/fonts/CP775 8x8.png
new file mode 100755
index 0000000..2b008f0
Binary files /dev/null and b/public/fonts/CP775 8x8.png differ
diff --git a/public/fonts/CP850 8x14.png b/public/fonts/CP850 8x14.png
new file mode 100755
index 0000000..f294521
Binary files /dev/null and b/public/fonts/CP850 8x14.png differ
diff --git a/public/fonts/CP850 8x16.png b/public/fonts/CP850 8x16.png
new file mode 100755
index 0000000..d7bfae9
Binary files /dev/null and b/public/fonts/CP850 8x16.png differ
diff --git a/public/fonts/CP850 8x19.png b/public/fonts/CP850 8x19.png
new file mode 100755
index 0000000..18e8f20
Binary files /dev/null and b/public/fonts/CP850 8x19.png differ
diff --git a/public/fonts/CP850 8x8.png b/public/fonts/CP850 8x8.png
new file mode 100755
index 0000000..c77fc8d
Binary files /dev/null and b/public/fonts/CP850 8x8.png differ
diff --git a/public/fonts/CP851 8x14.png b/public/fonts/CP851 8x14.png
new file mode 100755
index 0000000..a50a395
Binary files /dev/null and b/public/fonts/CP851 8x14.png differ
diff --git a/public/fonts/CP851 8x16.png b/public/fonts/CP851 8x16.png
new file mode 100755
index 0000000..507c9f5
Binary files /dev/null and b/public/fonts/CP851 8x16.png differ
diff --git a/public/fonts/CP851 8x19.png b/public/fonts/CP851 8x19.png
new file mode 100755
index 0000000..a05c087
Binary files /dev/null and b/public/fonts/CP851 8x19.png differ
diff --git a/public/fonts/CP851 8x8.png b/public/fonts/CP851 8x8.png
new file mode 100755
index 0000000..4391264
Binary files /dev/null and b/public/fonts/CP851 8x8.png differ
diff --git a/public/fonts/CP852 8x14.png b/public/fonts/CP852 8x14.png
new file mode 100755
index 0000000..fead270
Binary files /dev/null and b/public/fonts/CP852 8x14.png differ
diff --git a/public/fonts/CP852 8x16.png b/public/fonts/CP852 8x16.png
new file mode 100755
index 0000000..000d331
Binary files /dev/null and b/public/fonts/CP852 8x16.png differ
diff --git a/public/fonts/CP852 8x19.png b/public/fonts/CP852 8x19.png
new file mode 100755
index 0000000..87dc918
Binary files /dev/null and b/public/fonts/CP852 8x19.png differ
diff --git a/public/fonts/CP852 8x8.png b/public/fonts/CP852 8x8.png
new file mode 100755
index 0000000..eedb484
Binary files /dev/null and b/public/fonts/CP852 8x8.png differ
diff --git a/public/fonts/CP853 8x14.png b/public/fonts/CP853 8x14.png
new file mode 100755
index 0000000..0642a9b
Binary files /dev/null and b/public/fonts/CP853 8x14.png differ
diff --git a/public/fonts/CP853 8x16.png b/public/fonts/CP853 8x16.png
new file mode 100755
index 0000000..5a97f26
Binary files /dev/null and b/public/fonts/CP853 8x16.png differ
diff --git a/public/fonts/CP853 8x19.png b/public/fonts/CP853 8x19.png
new file mode 100755
index 0000000..8d56d8e
Binary files /dev/null and b/public/fonts/CP853 8x19.png differ
diff --git a/public/fonts/CP853 8x8.png b/public/fonts/CP853 8x8.png
new file mode 100755
index 0000000..820229a
Binary files /dev/null and b/public/fonts/CP853 8x8.png differ
diff --git a/public/fonts/CP855 8x14.png b/public/fonts/CP855 8x14.png
new file mode 100755
index 0000000..fdeac17
Binary files /dev/null and b/public/fonts/CP855 8x14.png differ
diff --git a/public/fonts/CP855 8x16.png b/public/fonts/CP855 8x16.png
new file mode 100755
index 0000000..fbe4605
Binary files /dev/null and b/public/fonts/CP855 8x16.png differ
diff --git a/public/fonts/CP855 8x8.png b/public/fonts/CP855 8x8.png
new file mode 100755
index 0000000..3cf9acf
Binary files /dev/null and b/public/fonts/CP855 8x8.png differ
diff --git a/public/fonts/CP857 8x14.png b/public/fonts/CP857 8x14.png
new file mode 100755
index 0000000..0f49c1d
Binary files /dev/null and b/public/fonts/CP857 8x14.png differ
diff --git a/public/fonts/CP857 8x16.png b/public/fonts/CP857 8x16.png
new file mode 100755
index 0000000..4073e64
Binary files /dev/null and b/public/fonts/CP857 8x16.png differ
diff --git a/public/fonts/CP857 8x8.png b/public/fonts/CP857 8x8.png
new file mode 100755
index 0000000..df14cbe
Binary files /dev/null and b/public/fonts/CP857 8x8.png differ
diff --git a/public/fonts/CP860 8x14.png b/public/fonts/CP860 8x14.png
new file mode 100755
index 0000000..a10d156
Binary files /dev/null and b/public/fonts/CP860 8x14.png differ
diff --git a/public/fonts/CP860 8x16.png b/public/fonts/CP860 8x16.png
new file mode 100755
index 0000000..06e9dfc
Binary files /dev/null and b/public/fonts/CP860 8x16.png differ
diff --git a/public/fonts/CP860 8x19.png b/public/fonts/CP860 8x19.png
new file mode 100755
index 0000000..bdc74d5
Binary files /dev/null and b/public/fonts/CP860 8x19.png differ
diff --git a/public/fonts/CP860 8x8.png b/public/fonts/CP860 8x8.png
new file mode 100755
index 0000000..172ad65
Binary files /dev/null and b/public/fonts/CP860 8x8.png differ
diff --git a/public/fonts/CP861 8x14.png b/public/fonts/CP861 8x14.png
new file mode 100755
index 0000000..69bb4c5
Binary files /dev/null and b/public/fonts/CP861 8x14.png differ
diff --git a/public/fonts/CP861 8x16.png b/public/fonts/CP861 8x16.png
new file mode 100755
index 0000000..4f9230b
Binary files /dev/null and b/public/fonts/CP861 8x16.png differ
diff --git a/public/fonts/CP861 8x19.png b/public/fonts/CP861 8x19.png
new file mode 100755
index 0000000..357ca4b
Binary files /dev/null and b/public/fonts/CP861 8x19.png differ
diff --git a/public/fonts/CP861 8x8.png b/public/fonts/CP861 8x8.png
new file mode 100755
index 0000000..f11be61
Binary files /dev/null and b/public/fonts/CP861 8x8.png differ
diff --git a/public/fonts/CP862 8x14.png b/public/fonts/CP862 8x14.png
new file mode 100755
index 0000000..c82c3aa
Binary files /dev/null and b/public/fonts/CP862 8x14.png differ
diff --git a/public/fonts/CP862 8x16.png b/public/fonts/CP862 8x16.png
new file mode 100755
index 0000000..e472ed0
Binary files /dev/null and b/public/fonts/CP862 8x16.png differ
diff --git a/public/fonts/CP862 8x8.png b/public/fonts/CP862 8x8.png
new file mode 100755
index 0000000..4fe96e7
Binary files /dev/null and b/public/fonts/CP862 8x8.png differ
diff --git a/public/fonts/CP863 8x14.png b/public/fonts/CP863 8x14.png
new file mode 100755
index 0000000..eca5910
Binary files /dev/null and b/public/fonts/CP863 8x14.png differ
diff --git a/public/fonts/CP863 8x16.png b/public/fonts/CP863 8x16.png
new file mode 100755
index 0000000..86d1eb5
Binary files /dev/null and b/public/fonts/CP863 8x16.png differ
diff --git a/public/fonts/CP863 8x19.png b/public/fonts/CP863 8x19.png
new file mode 100755
index 0000000..ca35d31
Binary files /dev/null and b/public/fonts/CP863 8x19.png differ
diff --git a/public/fonts/CP863 8x8.png b/public/fonts/CP863 8x8.png
new file mode 100755
index 0000000..53abb17
Binary files /dev/null and b/public/fonts/CP863 8x8.png differ
diff --git a/public/fonts/CP864 8x14.png b/public/fonts/CP864 8x14.png
new file mode 100755
index 0000000..a0bdbe2
Binary files /dev/null and b/public/fonts/CP864 8x14.png differ
diff --git a/public/fonts/CP864 8x16.png b/public/fonts/CP864 8x16.png
new file mode 100755
index 0000000..e3169b9
Binary files /dev/null and b/public/fonts/CP864 8x16.png differ
diff --git a/public/fonts/CP864 8x8.png b/public/fonts/CP864 8x8.png
new file mode 100755
index 0000000..ac40d0a
Binary files /dev/null and b/public/fonts/CP864 8x8.png differ
diff --git a/public/fonts/CP865 8x14.png b/public/fonts/CP865 8x14.png
new file mode 100755
index 0000000..bb30482
Binary files /dev/null and b/public/fonts/CP865 8x14.png differ
diff --git a/public/fonts/CP865 8x16.png b/public/fonts/CP865 8x16.png
new file mode 100755
index 0000000..93a341f
Binary files /dev/null and b/public/fonts/CP865 8x16.png differ
diff --git a/public/fonts/CP865 8x19.png b/public/fonts/CP865 8x19.png
new file mode 100755
index 0000000..5af551d
Binary files /dev/null and b/public/fonts/CP865 8x19.png differ
diff --git a/public/fonts/CP865 8x8.png b/public/fonts/CP865 8x8.png
new file mode 100755
index 0000000..4bbf61a
Binary files /dev/null and b/public/fonts/CP865 8x8.png differ
diff --git a/public/fonts/CP866 8x14.png b/public/fonts/CP866 8x14.png
new file mode 100755
index 0000000..dd0991c
Binary files /dev/null and b/public/fonts/CP866 8x14.png differ
diff --git a/public/fonts/CP866 8x16.png b/public/fonts/CP866 8x16.png
new file mode 100755
index 0000000..95e6ca8
Binary files /dev/null and b/public/fonts/CP866 8x16.png differ
diff --git a/public/fonts/CP866 8x8.png b/public/fonts/CP866 8x8.png
new file mode 100755
index 0000000..1be3bf0
Binary files /dev/null and b/public/fonts/CP866 8x8.png differ
diff --git a/public/fonts/CP869 8x14.png b/public/fonts/CP869 8x14.png
new file mode 100755
index 0000000..aa04a51
Binary files /dev/null and b/public/fonts/CP869 8x14.png differ
diff --git a/public/fonts/CP869 8x16.png b/public/fonts/CP869 8x16.png
new file mode 100755
index 0000000..416d0cb
Binary files /dev/null and b/public/fonts/CP869 8x16.png differ
diff --git a/public/fonts/CP869 8x8.png b/public/fonts/CP869 8x8.png
new file mode 100755
index 0000000..d274796
Binary files /dev/null and b/public/fonts/CP869 8x8.png differ
diff --git a/public/fonts/MicroKnight 8x16.png b/public/fonts/MicroKnight 8x16.png
new file mode 100755
index 0000000..9667613
Binary files /dev/null and b/public/fonts/MicroKnight 8x16.png differ
diff --git a/public/fonts/MicroKnight+ 8x16.png b/public/fonts/MicroKnight+ 8x16.png
new file mode 100755
index 0000000..6c11289
Binary files /dev/null and b/public/fonts/MicroKnight+ 8x16.png differ
diff --git a/public/fonts/P0t-NOoDLE 8x16.png b/public/fonts/P0t-NOoDLE 8x16.png
new file mode 100755
index 0000000..cc54633
Binary files /dev/null and b/public/fonts/P0t-NOoDLE 8x16.png differ
diff --git a/public/fonts/Topaz 1200 8x16.png b/public/fonts/Topaz 1200 8x16.png
new file mode 100755
index 0000000..cec9312
Binary files /dev/null and b/public/fonts/Topaz 1200 8x16.png differ
diff --git a/public/fonts/Topaz 500 8x16.png b/public/fonts/Topaz 500 8x16.png
new file mode 100755
index 0000000..862f1c2
Binary files /dev/null and b/public/fonts/Topaz 500 8x16.png differ
diff --git a/public/fonts/Topaz+ 1200 8x16.png b/public/fonts/Topaz+ 1200 8x16.png
new file mode 100755
index 0000000..d2c9c1c
Binary files /dev/null and b/public/fonts/Topaz+ 1200 8x16.png differ
diff --git a/public/fonts/Topaz+ 500 8x16.png b/public/fonts/Topaz+ 500 8x16.png
new file mode 100755
index 0000000..686a198
Binary files /dev/null and b/public/fonts/Topaz+ 500 8x16.png differ
diff --git a/public/fonts/mO'sOul 8x16.png b/public/fonts/mO'sOul 8x16.png
new file mode 100755
index 0000000..c45da9e
Binary files /dev/null and b/public/fonts/mO'sOul 8x16.png differ
diff --git a/public/img/android-chrome-192x192.png b/public/img/android-chrome-192x192.png
new file mode 100644
index 0000000..1d0aa15
Binary files /dev/null and b/public/img/android-chrome-192x192.png differ
diff --git a/public/img/apple-touch-icon.png b/public/img/apple-touch-icon.png
new file mode 100644
index 0000000..2cbee4b
Binary files /dev/null and b/public/img/apple-touch-icon.png differ
diff --git a/public/img/browserconfig.xml b/public/img/browserconfig.xml
new file mode 100644
index 0000000..ba99813
--- /dev/null
+++ b/public/img/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #da532c
+
+
+
diff --git a/public/img/chat.png b/public/img/chat.png
new file mode 100644
index 0000000..7dcac60
Binary files /dev/null and b/public/img/chat.png differ
diff --git a/public/img/face.png b/public/img/face.png
new file mode 100644
index 0000000..c05f8e0
Binary files /dev/null and b/public/img/face.png differ
diff --git a/public/img/favicon-16x16.png b/public/img/favicon-16x16.png
new file mode 100644
index 0000000..eb29613
Binary files /dev/null and b/public/img/favicon-16x16.png differ
diff --git a/public/img/favicon-32x32.png b/public/img/favicon-32x32.png
new file mode 100644
index 0000000..71acd8c
Binary files /dev/null and b/public/img/favicon-32x32.png differ
diff --git a/public/img/favicon.ico b/public/img/favicon.ico
new file mode 100644
index 0000000..abb958b
Binary files /dev/null and b/public/img/favicon.ico differ
diff --git a/public/img/favicon.png b/public/img/favicon.png
new file mode 100644
index 0000000..1c7d371
Binary files /dev/null and b/public/img/favicon.png differ
diff --git a/public/img/favicon_192.png b/public/img/favicon_192.png
new file mode 100644
index 0000000..e7ac2cd
Binary files /dev/null and b/public/img/favicon_192.png differ
diff --git a/public/img/mstile-150x150.png b/public/img/mstile-150x150.png
new file mode 100644
index 0000000..7569519
Binary files /dev/null and b/public/img/mstile-150x150.png differ
diff --git a/public/img/safari-pinned-tab.svg b/public/img/safari-pinned-tab.svg
new file mode 100644
index 0000000..b1b19cb
--- /dev/null
+++ b/public/img/safari-pinned-tab.svg
@@ -0,0 +1,74 @@
+
+
+
diff --git a/public/img/selection_border.gif b/public/img/selection_border.gif
new file mode 100644
index 0000000..182413a
Binary files /dev/null and b/public/img/selection_border.gif differ
diff --git a/public/img/site.webmanifest b/public/img/site.webmanifest
new file mode 100644
index 0000000..8dc2840
--- /dev/null
+++ b/public/img/site.webmanifest
@@ -0,0 +1,14 @@
+{
+ "name": "moebius",
+ "short_name": "moebius",
+ "icons": [
+ {
+ "src": "android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/public/img/toolbar/character_brush.png b/public/img/toolbar/character_brush.png
new file mode 100644
index 0000000..5c4c2b7
Binary files /dev/null and b/public/img/toolbar/character_brush.png differ
diff --git a/public/img/toolbar/circle.png b/public/img/toolbar/circle.png
new file mode 100644
index 0000000..876861c
Binary files /dev/null and b/public/img/toolbar/circle.png differ
diff --git a/public/img/toolbar/done.png b/public/img/toolbar/done.png
new file mode 100644
index 0000000..b706ad3
Binary files /dev/null and b/public/img/toolbar/done.png differ
diff --git a/public/img/toolbar/fill.png b/public/img/toolbar/fill.png
new file mode 100644
index 0000000..7a14df4
Binary files /dev/null and b/public/img/toolbar/fill.png differ
diff --git a/public/img/toolbar/freestyle.png b/public/img/toolbar/freestyle.png
new file mode 100644
index 0000000..6a72394
Binary files /dev/null and b/public/img/toolbar/freestyle.png differ
diff --git a/public/img/toolbar/keyboard.png b/public/img/toolbar/keyboard.png
new file mode 100644
index 0000000..2b6b9b4
Binary files /dev/null and b/public/img/toolbar/keyboard.png differ
diff --git a/public/img/toolbar/line.png b/public/img/toolbar/line.png
new file mode 100644
index 0000000..c36bd4c
Binary files /dev/null and b/public/img/toolbar/line.png differ
diff --git a/public/img/toolbar/sample.png b/public/img/toolbar/sample.png
new file mode 100644
index 0000000..c6c995b
Binary files /dev/null and b/public/img/toolbar/sample.png differ
diff --git a/public/img/toolbar/selection.png b/public/img/toolbar/selection.png
new file mode 100644
index 0000000..7b1a133
Binary files /dev/null and b/public/img/toolbar/selection.png differ
diff --git a/public/img/toolbar/square.png b/public/img/toolbar/square.png
new file mode 100644
index 0000000..1f3acfa
Binary files /dev/null and b/public/img/toolbar/square.png differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..a342971
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ moebius
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Resize
+
Cancel
+
+
+
+
+
+
Cancel
+
+
+
+
+
+
+
+
Done
+
+
+
+
+
Please wait... Retrieving data from server.
+
+
+
+
+
+
+
diff --git a/public/js/core.js b/public/js/core.js
new file mode 100644
index 0000000..751b38c
--- /dev/null
+++ b/public/js/core.js
@@ -0,0 +1,1102 @@
+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
+ };
+}
diff --git a/public/js/document_onload.js b/public/js/document_onload.js
new file mode 100644
index 0000000..b0613b0
--- /dev/null
+++ b/public/js/document_onload.js
@@ -0,0 +1,217 @@
+var worker;
+var title;
+var palette;
+var font;
+var textArtCanvas;
+var cursor;
+var selectionCursor;
+var positionInfo;
+var toolPreview;
+var pasteTool;
+var chat;
+var sampleTool;
+
+function $(divName) {
+ "use strict";
+ return document.getElementById(divName);
+}
+
+function createCanvas(width, height) {
+ "use strict";
+ var canvas = document.createElement("CANVAS");
+ canvas.width = width;
+ canvas.height = height;
+ return canvas;
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ "use strict";
+ pasteTool = createPasteTool($("cut"), $("copy"), $("paste"), $("delete"));
+ positionInfo = createPositionInfo($("position-info"));
+ textArtCanvas = createTextArtCanvas($("canvas-container"), () => {
+ selectionCursor = createSelectionCursor($("canvas-container"));
+ cursor = createCursor($("canvas-container"));
+ document.addEventListener("keydown", undoAndRedo);
+ onClick($("new"), () => {
+ if (confirm("All changes will be lost. Are you sure?") === true) {
+ textArtCanvas.clear();
+ $("sauce-title").value = "";
+ $("sauce-group").value = "";
+ $("sauce-author").value = "";
+ }
+ });
+ onClick($("open"), () => {
+ showOverlay($("open-overlay"));
+ });
+ onClick($("save-ansi"), Save.ans);
+ onClick($("save-utf8"), Save.utf8);
+ onClick($("save-bin"), Save.bin);
+ onClick($("save-xbin"), Save.xb);
+ onClick($("save-png"), Save.png);
+ onClick($("cut"), pasteTool.cut);
+ onClick($("copy"), pasteTool.copy);
+ onClick($("paste"), pasteTool.paste);
+ onClick($("delete"), pasteTool.deleteSelection);
+ onClick($("file-menu"), menuHover);
+ onClick($("edit-menu"), menuHover);
+ onClick($("view-menu"), menuHover);
+ var palettePreview = createPalettePreview($("palette-preview"));
+ var palettePicker = createPalettePicker($("palette-picker"));
+ var iceColoursToggle = createSettingToggle($("ice-colors-toggle"), textArtCanvas.getIceColours, textArtCanvas.setIceColours);
+ var letterSpacingToggle = createSettingToggle($("letter-spacing-toggle"), () => {
+ return font.getLetterSpacing();
+ }, (newLetterSpacing) => {
+ font.setLetterSpacing(newLetterSpacing);
+ });
+ onFileChange($("open-file"), (file) => {
+ Load.file(file, (columns, rows, imageData, iceColours, letterSpacing) => {
+ var indexOfPeriod = file.name.lastIndexOf(".");
+ if (indexOfPeriod !== -1) {
+ title.setName(file.name.substr(0, indexOfPeriod));
+ } else {
+ title.setName(file.name);
+ }
+ textArtCanvas.setImageData(columns, rows, imageData, iceColours, letterSpacing);
+ iceColoursToggle.update();
+ letterSpacingToggle.update();
+ hideOverlay($("open-overlay"));
+ $("open-file").value = "";
+ });
+ });
+ onClick($("open-cancel"), () => {
+ hideOverlay($("open-overlay"));
+ });
+ onClick($("edit-sauce"), () => {
+ showOverlay($("sauce-overlay"));
+ keyboard.ignore();
+ paintShortcuts.ignore();
+ $("sauce-title").focus();
+ freestyle.ignore();
+ characterBrush.ignore();
+ });
+ onClick($("sauce-done"), () => {
+ hideOverlay($("sauce-overlay"));
+ keyboard.unignore();
+ paintShortcuts.unignore();
+ freestyle.unignore();
+ characterBrush.unignore();
+ });
+ onReturn($("sauce-title"), $("sauce-done"));
+ onReturn($("sauce-group"), $("sauce-done"));
+ onReturn($("sauce-author"), $("sauce-done"));
+ var paintShortcuts = createPaintShortcuts({
+ "D": $("default-colour"),
+ "Q": $("swap-colours"),
+ "K": $("keyboard"),
+ "F": $("freestyle"),
+ "B": $("character-brush"),
+ "N": $("fill"),
+ "G": $("grid-toggle")
+ });
+ var keyboard = createKeyboardController();
+ Toolbar.add($("keyboard"), () => {
+ paintShortcuts.disable();
+ keyboard.enable();
+ }, () => {
+ paintShortcuts.enable();
+ keyboard.disable();
+ }).enable();
+ title = createTitleHandler($("artwork-title"), () => {
+ keyboard.ignore();
+ paintShortcuts.ignore();
+ freestyle.ignore();
+ characterBrush.ignore();
+ }, () => {
+ keyboard.unignore();
+ paintShortcuts.unignore();
+ freestyle.unignore();
+ characterBrush.unignore();
+ });
+ onClick($("undo"), textArtCanvas.undo);
+ onClick($("redo"), textArtCanvas.redo);
+ onClick($("resize"), () => {
+ showOverlay($("resize-overlay"));
+ $("columns-input").value = textArtCanvas.getColumns();
+ $("rows-input").value = textArtCanvas.getRows();
+ keyboard.ignore();
+ paintShortcuts.ignore();
+ freestyle.ignore();
+ characterBrush.ignore();
+ $("columns-input").focus();
+ });
+ onClick($("resize-apply"), () => {
+ var columnsValue = parseInt($("columns-input").value, 10);
+ var rowsValue = parseInt($("rows-input").value, 10);
+ if (!isNaN(columnsValue) && !isNaN(rowsValue)) {
+ textArtCanvas.resize(columnsValue, rowsValue);
+ hideOverlay($("resize-overlay"));
+ }
+ keyboard.unignore();
+ paintShortcuts.unignore();
+ freestyle.unignore();
+ characterBrush.unignore();
+ });
+ onReturn($("columns-input"), $("resize-apply"));
+ onReturn($("rows-input"), $("resize-apply"));
+ onClick($("resize-cancel"), () => {
+ hideOverlay($("resize-overlay"));
+ keyboard.unignore();
+ paintShortcuts.unignore();
+ freestyle.unignore();
+ characterBrush.unignore();
+ });
+ onClick($("default-colour"), () => {
+ palette.setForegroundColour(7);
+ palette.setBackgroundColour(0);
+ });
+ onClick($("swap-colours"), () => {
+ var tempForeground = palette.getForegroundColour();
+ palette.setForegroundColour(palette.getBackgroundColour());
+ palette.setBackgroundColour(tempForeground);
+ });
+ onClick($("fonts"), () => {
+ showOverlay($("fonts-overlay"));
+ });
+ onSelectChange($("font-select"), () => {
+ textArtCanvas.setFont($("font-select").value, () => {
+ hideOverlay($("fonts-overlay"));
+ });
+ });
+ onClick($("fonts-cancel"), () => {
+ hideOverlay($("fonts-overlay"));
+ });
+ var grid = createGrid($("grid"));
+ var gridToggle = createSettingToggle($("grid-toggle"), grid.isShown, grid.show);
+ var freestyle = createFreehandController(createShadingPanel());
+ Toolbar.add($("freestyle"), freestyle.enable, freestyle.disable);
+ var characterBrush = createFreehandController(createCharacterBrushPanel());
+ Toolbar.add($("character-brush"), characterBrush.enable, characterBrush.disable);
+ var fill = createFillController();
+ Toolbar.add($("fill"), fill.enable, fill.disable);
+ var line = createLineController();
+ Toolbar.add($("line"), line.enable, line.disable);
+ var square = createSquareController();
+ Toolbar.add($("square"), square.enable, square.disable);
+ var circle = createCircleController();
+ Toolbar.add($("circle"), circle.enable, circle.disable);
+ toolPreview = createToolPreview($("tool-preview"));
+ var selection = createSelectionTool($("canvas-container"));
+ Toolbar.add($("selection"), selection.enable, selection.disable);
+ chat = createChatController($("chat-button"), $("chat-window"), $("message-window"), $("user-list"), $("handle-input"), $("message-input"), $("notification-checkbox"), () => {
+ keyboard.ignore();
+ paintShortcuts.ignore();
+ freestyle.ignore();
+ characterBrush.ignore();
+ }, () => {
+ keyboard.unignore();
+ paintShortcuts.unignore();
+ freestyle.unignore();
+ characterBrush.unignore();
+ });
+ var chatToggle = createSettingToggle($("chat-toggle"), chat.isEnabled, chat.toggle);
+ onClick($("chat-button"), chat.toggle);
+ sampleTool = createSampleTool($("sample"), freestyle, $("freestyle"), characterBrush, $("character-brush"));
+ Toolbar.add($("sample"), sampleTool.enable, sampleTool.disable);
+ worker = createWorkerHandler($("handle-input"));
+ });
+});
diff --git a/public/js/elementhelper.js b/public/js/elementhelper.js
new file mode 100644
index 0000000..575b2be
--- /dev/null
+++ b/public/js/elementhelper.js
@@ -0,0 +1,23 @@
+var ElementHelper = (function() {
+ "use strict";
+
+ function create(elementName, args) {
+ var element;
+ args = args || {};
+ element = document.createElement(elementName);
+ Object.getOwnPropertyNames(args).forEach(function(name) {
+ if (typeof args[name] === "object") {
+ Object.getOwnPropertyNames(args[name]).forEach(function(subName) {
+ element[name][subName] = args[name][subName];
+ });
+ } else {
+ element[name] = args[name];
+ }
+ });
+ return element;
+ }
+
+ return {
+ "create": create
+ };
+}());
diff --git a/public/js/file.js b/public/js/file.js
new file mode 100644
index 0000000..1fb4215
--- /dev/null
+++ b/public/js/file.js
@@ -0,0 +1,1040 @@
+var Load = (function() {
+ "use strict";
+
+ function File(bytes) {
+ var pos, SAUCE_ID, COMNT_ID, commentCount;
+
+ SAUCE_ID = new Uint8Array([0x53, 0x41, 0x55, 0x43, 0x45]);
+ COMNT_ID = new Uint8Array([0x43, 0x4F, 0x4D, 0x4E, 0x54]);
+
+ this.get = function() {
+ if (pos >= bytes.length) {
+ throw "Unexpected end of file reached.";
+ }
+ pos += 1;
+ return bytes[pos - 1];
+ };
+
+ this.get16 = function() {
+ var v;
+ v = this.get();
+ return v + (this.get() << 8);
+ };
+
+ this.get32 = function() {
+ var v;
+ v = this.get();
+ v += this.get() << 8;
+ v += this.get() << 16;
+ return v + (this.get() << 24);
+ };
+
+ this.getC = function() {
+ return String.fromCharCode(this.get());
+ };
+
+ this.getS = function(num) {
+ var string;
+ string = "";
+ while (num > 0) {
+ string += this.getC();
+ num -= 1;
+ }
+ return string.replace(/\s+$/, "");
+ };
+
+ this.lookahead = function(match) {
+ var i;
+ for (i = 0; i < match.length; i += 1) {
+ if ((pos + i === bytes.length) || (bytes[pos + i] !== match[i])) {
+ break;
+ }
+ }
+ return i === match.length;
+ };
+
+ this.read = function(num) {
+ var t;
+ t = pos;
+
+ num = num || this.size - pos;
+ while ((pos += 1) < this.size) {
+ num -= 1;
+ if (num === 0) {
+ break;
+ }
+ }
+ return bytes.subarray(t, pos);
+ };
+
+ this.seek = function(newPos) {
+ pos = newPos;
+ };
+
+ this.peek = function(num) {
+ num = num || 0;
+ return bytes[pos + num];
+ };
+
+ this.getPos = function() {
+ return pos;
+ };
+
+ this.eof = function() {
+ return pos === this.size;
+ };
+
+ pos = bytes.length - 128;
+
+ if (this.lookahead(SAUCE_ID)) {
+ this.sauce = {};
+
+ this.getS(5);
+
+ this.sauce.version = this.getS(2); // String, maximum of 2 characters
+ this.sauce.title = this.getS(35); // String, maximum of 35 characters
+ this.sauce.author = this.getS(20); // String, maximum of 20 characters
+ this.sauce.group = this.getS(20); // String, maximum of 20 characters
+ this.sauce.date = this.getS(8); // String, maximum of 8 characters
+ this.sauce.fileSize = this.get32(); // unsigned 32-bit
+ this.sauce.dataType = this.get(); // unsigned 8-bit
+ this.sauce.fileType = this.get(); // unsigned 8-bit
+ this.sauce.tInfo1 = this.get16(); // unsigned 16-bit
+ this.sauce.tInfo2 = this.get16(); // unsigned 16-bit
+ this.sauce.tInfo3 = this.get16(); // unsigned 16-bit
+ this.sauce.tInfo4 = this.get16(); // unsigned 16-bit
+
+ this.sauce.comments = [];
+ commentCount = this.get(); // unsigned 8-bit
+ this.sauce.flags = this.get(); // unsigned 8-bit
+ if (commentCount > 0) {
+
+ pos = bytes.length - 128 - (commentCount * 64) - 5;
+
+ if (this.lookahead(COMNT_ID)) {
+
+ this.getS(5);
+
+ while (commentCount > 0) {
+ this.sauce.comments.push(this.getS(64));
+ commentCount -= 1;
+ }
+ }
+ }
+ }
+
+ pos = 0;
+
+ if (this.sauce) {
+
+ if (this.sauce.fileSize > 0 && this.sauce.fileSize < bytes.length) {
+
+ this.size = this.sauce.fileSize;
+ } else {
+
+ this.size = bytes.length - 128;
+ }
+ } else {
+
+ this.size = bytes.length;
+ }
+ }
+
+ function ScreenData(width) {
+ var imageData, maxY, pos;
+
+ function binColor(ansiColor) {
+ switch (ansiColor) {
+ case 4:
+ return 1;
+ case 6:
+ return 3;
+ case 1:
+ return 4;
+ case 3:
+ return 6;
+ case 12:
+ return 9;
+ case 14:
+ return 11;
+ case 9:
+ return 12;
+ case 11:
+ return 14;
+ default:
+ return ansiColor;
+ }
+ }
+
+ this.reset = function() {
+ imageData = new Uint8Array(width * 100 * 3);
+ maxY = 0;
+ pos = 0;
+ };
+
+ this.reset();
+
+ this.raw = function(bytes) {
+ var i, j;
+ maxY = Math.ceil(bytes.length / 2 / width);
+ imageData = new Uint8Array(width * maxY * 3);
+ for (i = 0, j = 0; j < bytes.length; i += 3, j += 2) {
+ imageData[i] = bytes[j];
+ imageData[i + 1] = bytes[j + 1] & 15;
+ imageData[i + 2] = bytes[j + 1] >> 4;
+ }
+ };
+
+ function extendImageData(y) {
+ var newImageData;
+ newImageData = new Uint8Array(width * (y + 100) * 3 + imageData.length);
+ newImageData.set(imageData, 0);
+ imageData = newImageData;
+ }
+
+ this.set = function(x, y, charCode, fg, bg) {
+ pos = (y * width + x) * 3;
+ if (pos >= imageData.length) {
+ extendImageData(y);
+ }
+ imageData[pos] = charCode;
+ imageData[pos + 1] = binColor(fg);
+ imageData[pos + 2] = binColor(bg);
+ if (y > maxY) {
+ maxY = y;
+ }
+ };
+
+ this.getData = function() {
+ return imageData.subarray(0, width * (maxY + 1) * 3);
+ };
+
+ this.getHeight = function() {
+ return maxY + 1;
+ };
+
+ this.rowLength = width * 3;
+
+ this.stripBlinking = function() {
+ var i;
+ for (i = 2; i < imageData.length; i += 3) {
+ if (imageData[i] >= 8) {
+ imageData[i] -= 8;
+ }
+ }
+ };
+ }
+
+ function loadAnsi(bytes) {
+ var file, escaped, escapeCode, j, code, values, columns, imageData, topOfScreen, x, y, savedX, savedY, foreground, background, bold, blink, inverse;
+
+ file = new File(bytes);
+
+ function resetAttributes() {
+ foreground = 7;
+ background = 0;
+ bold = false;
+ blink = false;
+ inverse = false;
+ }
+ resetAttributes();
+
+ function newLine() {
+ x = 1;
+ if (y === 26 - 1) {
+ topOfScreen += 1;
+ } else {
+ y += 1;
+ }
+ }
+
+ function setPos(newX, newY) {
+ x = Math.min(columns, Math.max(1, newX));
+ y = Math.min(26, Math.max(1, newY));
+ }
+
+ x = 1;
+ y = 1;
+ topOfScreen = 0;
+
+ escapeCode = "";
+ escaped = false;
+
+ columns = (file.sauce !== undefined) ? file.sauce.tInfo1 : 80;
+
+ imageData = new ScreenData(columns);
+
+ function getValues() {
+ return escapeCode.substr(1, escapeCode.length - 2).split(";").map(function(value) {
+ var parsedValue;
+ parsedValue = parseInt(value, 10);
+ return isNaN(parsedValue) ? 1 : parsedValue;
+ });
+ }
+
+ while (!file.eof()) {
+ code = file.get();
+ if (escaped) {
+ escapeCode += String.fromCharCode(code);
+ if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) {
+ escaped = false;
+ values = getValues();
+ if (escapeCode.charAt(0) === "[") {
+ switch (escapeCode.charAt(escapeCode.length - 1)) {
+ case "A": // Up cursor.
+ y = Math.max(1, y - values[0]);
+ break;
+ case "B": // Down cursor.
+ y = Math.min(26 - 1, y + values[0]);
+ break;
+ case "C": // Forward cursor.
+ if (x === columns) {
+ newLine();
+ }
+ x = Math.min(columns, x + values[0]);
+ break;
+ case "D": // Backward cursor.
+ x = Math.max(1, x - values[0]);
+ break;
+ case "H": // Set the cursor position by calling setPos(), first , then .
+ if (values.length === 1) {
+ setPos(1, values[0]);
+ } else {
+ setPos(values[1], values[0]);
+ }
+ break;
+ case "J": // Clear screen.
+ if (values[0] === 2) {
+ x = 1;
+ y = 1;
+ imageData.reset();
+ }
+ break;
+ case "K": // Clear until the end of line.
+ for (j = x - 1; j < columns; j += 1) {
+ imageData.set(j, y - 1 + topOfScreen, 0, 0);
+ }
+ break;
+ case "m": // Attributes, work through each code in turn.
+ for (j = 0; j < values.length; j += 1) {
+ if (values[j] >= 30 && values[j] <= 37) {
+ foreground = values[j] - 30;
+ } else if (values[j] >= 40 && values[j] <= 47) {
+ background = values[j] - 40;
+ } else {
+ switch (values[j]) {
+ case 0: // Reset attributes
+ resetAttributes();
+ break;
+ case 1: // Bold
+ bold = true;
+ break;
+ case 5: // Blink
+ blink = true;
+ break;
+ case 7: // Inverse
+ inverse = true;
+ break;
+ case 22: // Bold off
+ bold = false;
+ break;
+ case 25: // Blink off
+ blink = false;
+ break;
+ case 27: // Inverse off
+ inverse = false;
+ break;
+ }
+ }
+ }
+ break;
+ case "s": // Save the current and positions.
+ savedX = x;
+ savedY = y;
+ break;
+ case "u": // Restore the current and positions.
+ x = savedX;
+ y = savedY;
+ break;
+ }
+ }
+ escapeCode = "";
+ }
+ } else {
+ switch (code) {
+ case 10: // Lone linefeed (LF).
+ newLine();
+ break;
+ case 13: // Carriage Return, and Linefeed (CRLF)
+ if (file.peek() === 0x0A) {
+ file.read(1);
+ newLine();
+ }
+ break;
+ case 26: // Ignore eof characters until the actual end-of-file, or sauce record has been reached.
+ break;
+ default:
+ if (code === 27 && file.peek() === 0x5B) {
+ escaped = true;
+ } else {
+ if (!inverse) {
+ imageData.set(x - 1, y - 1 + topOfScreen, code, bold ? (foreground + 8) : foreground, blink ? (background + 8) : background);
+ } else {
+ imageData.set(x - 1, y - 1 + topOfScreen, code, bold ? (background + 8) : background, blink ? (foreground + 8) : foreground);
+ }
+ x += 1;
+ if (x === columns + 1) {
+ newLine();
+ }
+ }
+ }
+ }
+ }
+
+ return {
+ "width": columns,
+ "height": imageData.getHeight(),
+ "data": imageData.getData(),
+ "noblink": file.sauce ? ((file.sauce.flags & 1) === 1) : false,
+ "title": file.sauce ? file.sauce.title : "",
+ "author": file.sauce ? file.sauce.author : "",
+ "group": file.sauce ? file.sauce.group : ""
+ };
+ }
+
+ function convertData(data) {
+ var output = new Uint16Array(data.length / 3);
+ for (var i = 0, j = 0; i < data.length; i += 1, j += 3) {
+ output[i] = (data[j] << 8) + (data[j + 2] << 4) + data[j + 1];
+ }
+ return output;
+ }
+
+ function bytesToString(bytes, offset, size) {
+ var text = "", i;
+ for (i = 0; i < size; i++) {
+ text += String.fromCharCode(bytes[offset + i]);
+ }
+ return text;
+ }
+
+ function getSauce(bytes, defaultColumnValue) {
+ var sauce, fileSize, dataType, columns, rows, flags;
+
+ function removeTrailingWhitespace(text) {
+ return text.replace(/\s+$/, "");
+ }
+
+ if (bytes.length >= 128) {
+ sauce = bytes.slice(-128);
+ if (bytesToString(sauce, 0, 5) === "SAUCE" && bytesToString(sauce, 5, 2) === "00") {
+ fileSize = (sauce[93] << 24) + (sauce[92] << 16) + (sauce[91] << 8) + sauce[90];
+ dataType = sauce[94];
+ if (dataType === 5) {
+ columns = sauce[95] * 2;
+ rows = fileSize / columns / 2;
+ } else {
+ columns = (sauce[97] << 8) + sauce[96];
+ rows = (sauce[99] << 8) + sauce[98];
+ }
+ flags = sauce[105];
+ return {
+ "title": removeTrailingWhitespace(bytesToString(sauce, 7, 35)),
+ "author": removeTrailingWhitespace(bytesToString(sauce, 42, 20)),
+ "group": removeTrailingWhitespace(bytesToString(sauce, 62, 20)),
+ "fileSize": (sauce[93] << 24) + (sauce[92] << 16) + (sauce[91] << 8) + sauce[90],
+ "columns": columns,
+ "rows": rows,
+ "iceColours": (flags & 0x01) === 1,
+ "letterSpacing": (flags >> 1 & 0x02) === 2
+ };
+ }
+ }
+ return {
+ "title": "",
+ "author": "",
+ "group": "",
+ "fileSize": bytes.length,
+ "columns": defaultColumnValue,
+ "rows": undefined,
+ "iceColours": false,
+ "letterSpacing": false
+ };
+ }
+
+ function convertUInt8ToUint16(uint8Array, start, size) {
+ var i, j;
+ var uint16Array = new Uint16Array(size / 2);
+ for (i = 0, j = 0; i < size; i += 2, j += 1) {
+ uint16Array[j] = (uint8Array[start + i] << 8) + uint8Array[start + i + 1];
+ }
+ return uint16Array;
+ }
+
+ function loadBin(bytes) {
+ var sauce = getSauce(bytes, 160);
+ var data;
+ if (sauce.rows === undefined) {
+ sauce.rows = sauce.fileSize / 160 / 2;
+ }
+ data = convertUInt8ToUint16(bytes, 0, sauce.columns * sauce.rows * 2);
+ return {
+ "columns": sauce.columns,
+ "rows": sauce.rows,
+ "data": data,
+ "iceColours": sauce.iceColours,
+ "letterSpacing": sauce.letterSpacing,
+ "title": sauce.title,
+ "author": sauce.author,
+ "group": sauce.group
+ };
+ }
+
+ function uncompress(bytes, dataIndex, fileSize, column, rows) {
+ var data = new Uint16Array(column * rows);
+ var i, value, count, j, k, char, attribute;
+ for (i = dataIndex, j = 0; i < fileSize;) {
+ value = bytes[i++];
+ count = value & 0x3F;
+ switch (value >> 6) {
+ case 1:
+ char = bytes[i++];
+ for (k = 0; k <= count; k++) {
+ data[j++] = (char << 8) + bytes[i++];
+ }
+ break;
+ case 2:
+ attribute = bytes[i++];
+ for (k = 0; k <= count; k++) {
+ data[j++] = (bytes[i++] << 8) + attribute;
+ }
+ break;
+ case 3:
+ char = bytes[i++];
+ attribute = bytes[i++];
+ for (k = 0; k <= count; k++) {
+ data[j++] = (char << 8) + attribute;
+ }
+ break;
+ default:
+ for (k = 0; k <= count; k++) {
+ data[j++] = (bytes[i++] << 8) + bytes[i++];
+ }
+ break;
+ }
+ }
+ return data;
+ }
+
+ function loadXBin(bytes) {
+ var sauce = getSauce(bytes);
+ var columns, rows, fontHeight, flags, paletteFlag, fontFlag, compressFlag, iceColoursFlag, font512Flag, dataIndex, data;
+ if (bytesToString(bytes, 0, 4) === "XBIN" && bytes[4] === 0x1A) {
+ columns = (bytes[6] << 8) + bytes[5];
+ rows = (bytes[8] << 8) + bytes[7];
+ fontHeight = bytes[9];
+ flags = bytes[10];
+ paletteFlag = (flags & 0x01) === 1;
+ fontFlag = (flags >> 1 & 0x01) === 1;
+ compressFlag = (flags >> 2 & 0x01) === 1;
+ iceColoursFlag = (flags >> 3 & 0x01) === 1;
+ font512Flag = (flags >> 4 & 0x01) === 1;
+ dataIndex = 11;
+ if (paletteFlag === true) {
+ dataIndex += 48;
+ }
+ if (fontFlag === true) {
+ if (font512Flag === true) {
+ dataIndex += 512 * fontHeight;
+ } else {
+ dataIndex += 256 * fontHeight;
+ }
+ }
+ if (compressFlag === true) {
+ data = uncompress(bytes, dataIndex, sauce.fileSize, columns, rows);
+ } else {
+ data = convertUInt8ToUint16(bytes, dataIndex, columns * rows * 2);
+ }
+ }
+ return {
+ "columns": columns,
+ "rows": rows,
+ "data": data,
+ "iceColours": iceColoursFlag,
+ "letterSpacing": false,
+ "title": sauce.title,
+ "author": sauce.author,
+ "group": sauce.group
+ };
+ }
+
+ function file(file, callback) {
+ var reader = new FileReader();
+ reader.addEventListener("load", function(evt) {
+ var data = new Uint8Array(reader.result);
+ var imageData;
+ switch (file.name.split(".").pop().toLowerCase()) {
+ case "xb":
+ imageData = loadXBin(data);
+ callback(imageData.columns, imageData.rows, imageData.data, imageData.iceColours, imageData.letterSpacing);
+ break;
+ case "bin":
+ imageData = loadBin(data);
+ callback(imageData.columns, imageData.rows, imageData.data, imageData.iceColours, imageData.letterSpacing);
+ break;
+ default:
+ imageData = loadAnsi(data);
+ $("sauce-title").value = imageData.title;
+ $("sauce-group").value = imageData.group;
+ $("sauce-author").value = imageData.author;
+ callback(imageData.width, imageData.height, convertData(imageData.data), imageData.noblink, false);
+ break;
+ }
+ });
+ reader.readAsArrayBuffer(file);
+ }
+
+ return {
+ "file": file
+ };
+}());
+
+var Save = (function() {
+ "use strict";
+ function saveFile(bytes, sauce, filename) {
+ var outputBytes;
+ if (sauce !== undefined) {
+ outputBytes = new Uint8Array(bytes.length + sauce.length);
+ outputBytes.set(sauce, bytes.length);
+ } else {
+ outputBytes = new Uint8Array(bytes.length);
+ }
+ outputBytes.set(bytes, 0);
+ var downloadLink = document.createElement("A");
+ if ((navigator.userAgent.indexOf("Chrome") === -1) && (navigator.userAgent.indexOf("Safari") !== -1)) {
+ var base64String = "";
+ for (var i = 0; i < outputBytes.length; i += 1) {
+ base64String += String.fromCharCode(outputBytes[i]);
+ }
+ downloadLink.href = "data:application/octet-stream;base64," + btoa(base64String);
+ } else {
+ var blob = new Blob([outputBytes], { "type": "application/octet-stream" });
+ downloadLink.href = URL.createObjectURL(blob);
+ }
+ downloadLink.download = filename;
+ var clickEvent = document.createEvent("MouseEvent");
+ clickEvent.initEvent("click", true, true);
+ downloadLink.dispatchEvent(clickEvent);
+ window.URL.revokeObjectURL(downloadLink.href);
+ }
+
+ function createSauce(datatype, filetype, filesize, doFlagsAndTInfoS) {
+ function addText(text, maxlength, index) {
+ var i;
+ for (i = 0; i < maxlength; i += 1) {
+ sauce[i + index] = (i < text.length) ? text.charCodeAt(i) : 0x20;
+ }
+ }
+ var sauce = new Uint8Array(129);
+ sauce[0] = 0x1A;
+ sauce.set(new Uint8Array([0x53, 0x41, 0x55, 0x43, 0x45, 0x30, 0x30]), 1);
+ addText($("sauce-title").value, 35, 8);
+ addText($("sauce-author").value, 20, 43);
+ addText($("sauce-group").value, 20, 63);
+ var date = new Date();
+ addText(date.getFullYear().toString(10), 4, 83);
+ var month = date.getMonth() + 1;
+ addText((month < 10) ? ("0" + month.toString(10)) : month.toString(10), 2, 87);
+ var day = date.getDate();
+ addText((day < 10) ? ("0" + day.toString(10)) : day.toString(10), 2, 89);
+ sauce[91] = filesize & 0xFF;
+ sauce[92] = (filesize >> 8) & 0xFF;
+ sauce[93] = (filesize >> 16) & 0xFF;
+ sauce[94] = filesize >> 24;
+ sauce[95] = datatype;
+ sauce[96] = filetype;
+ var columns = textArtCanvas.getColumns();
+ sauce[97] = columns & 0xFF;
+ sauce[98] = columns >> 8;
+ var rows = textArtCanvas.getRows();
+ sauce[99] = rows & 0xFF;
+ sauce[100] = rows >> 8;
+ sauce[105] = 0;
+ if (doFlagsAndTInfoS) {
+ var flags = 0;
+ if (textArtCanvas.getIceColours() === true) {
+ flags += 1;
+ }
+ if (font.getLetterSpacing() === false) {
+ flags += (1 << 1);
+ } else {
+ flags += (1 << 2);
+ }
+ sauce[106] = flags;
+ var fontName = "IBM VGA";
+ addText(fontName, fontName.length, 107);
+ }
+ return sauce;
+ }
+
+ function getUnicode(charCode) {
+ switch (charCode) {
+ case 1: return 0x263A;
+ case 2: return 0x263B;
+ case 3: return 0x2665;
+ case 4: return 0x2666;
+ case 5: return 0x2663;
+ case 6: return 0x2660;
+ case 7: return 0x2022;
+ case 8: return 0x25D8;
+ case 9: return 0x25CB;
+ case 10: return 0x25D9;
+ case 11: return 0x2642;
+ case 12: return 0x2640;
+ case 13: return 0x266A;
+ case 14: return 0x266B;
+ case 15: return 0x263C;
+ case 16: return 0x25BA;
+ case 17: return 0x25C4;
+ case 18: return 0x2195;
+ case 19: return 0x203C;
+ case 20: return 0x00B6;
+ case 21: return 0x00A7;
+ case 22: return 0x25AC;
+ case 23: return 0x21A8;
+ case 24: return 0x2191;
+ case 25: return 0x2193;
+ case 26: return 0x2192;
+ case 27: return 0x2190;
+ case 28: return 0x221F;
+ case 29: return 0x2194;
+ case 30: return 0x25B2;
+ case 31: return 0x25BC;
+ case 127: return 0x2302;
+ case 128: return 0x00C7;
+ case 129: return 0x00FC;
+ case 130: return 0x00E9;
+ case 131: return 0x00E2;
+ case 132: return 0x00E4;
+ case 133: return 0x00E0;
+ case 134: return 0x00E5;
+ case 135: return 0x00E7;
+ case 136: return 0x00EA;
+ case 137: return 0x00EB;
+ case 138: return 0x00E8;
+ case 139: return 0x00EF;
+ case 140: return 0x00EE;
+ case 141: return 0x00EC;
+ case 142: return 0x00C4;
+ case 143: return 0x00C5;
+ case 144: return 0x00C9;
+ case 145: return 0x00E6;
+ case 146: return 0x00C6;
+ case 147: return 0x00F4;
+ case 148: return 0x00F6;
+ case 149: return 0x00F2;
+ case 150: return 0x00FB;
+ case 151: return 0x00F9;
+ case 152: return 0x00FF;
+ case 153: return 0x00D6;
+ case 154: return 0x00DC;
+ case 155: return 0x00A2;
+ case 156: return 0x00A3;
+ case 157: return 0x00A5;
+ case 158: return 0x20A7;
+ case 159: return 0x0192;
+ case 160: return 0x00E1;
+ case 161: return 0x00ED;
+ case 162: return 0x00F3;
+ case 163: return 0x00FA;
+ case 164: return 0x00F1;
+ case 165: return 0x00D1;
+ case 166: return 0x00AA;
+ case 167: return 0x00BA;
+ case 168: return 0x00BF;
+ case 169: return 0x2310;
+ case 170: return 0x00AC;
+ case 171: return 0x00BD;
+ case 172: return 0x00BC;
+ case 173: return 0x00A1;
+ case 174: return 0x00AB;
+ case 175: return 0x00BB;
+ case 176: return 0x2591;
+ case 177: return 0x2592;
+ case 178: return 0x2593;
+ case 179: return 0x2502;
+ case 180: return 0x2524;
+ case 181: return 0x2561;
+ case 182: return 0x2562;
+ case 183: return 0x2556;
+ case 184: return 0x2555;
+ case 185: return 0x2563;
+ case 186: return 0x2551;
+ case 187: return 0x2557;
+ case 188: return 0x255D;
+ case 189: return 0x255C;
+ case 190: return 0x255B;
+ case 191: return 0x2510;
+ case 192: return 0x2514;
+ case 193: return 0x2534;
+ case 194: return 0x252C;
+ case 195: return 0x251C;
+ case 196: return 0x2500;
+ case 197: return 0x253C;
+ case 198: return 0x255E;
+ case 199: return 0x255F;
+ case 200: return 0x255A;
+ case 201: return 0x2554;
+ case 202: return 0x2569;
+ case 203: return 0x2566;
+ case 204: return 0x2560;
+ case 205: return 0x2550;
+ case 206: return 0x256C;
+ case 207: return 0x2567;
+ case 208: return 0x2568;
+ case 209: return 0x2564;
+ case 210: return 0x2565;
+ case 211: return 0x2559;
+ case 212: return 0x2558;
+ case 213: return 0x2552;
+ case 214: return 0x2553;
+ case 215: return 0x256B;
+ case 216: return 0x256A;
+ case 217: return 0x2518;
+ case 218: return 0x250C;
+ case 219: return 0x2588;
+ case 220: return 0x2584;
+ case 221: return 0x258C;
+ case 222: return 0x2590;
+ case 223: return 0x2580;
+ case 224: return 0x03B1;
+ case 225: return 0x00DF;
+ case 226: return 0x0393;
+ case 227: return 0x03C0;
+ case 228: return 0x03A3;
+ case 229: return 0x03C3;
+ case 230: return 0x00B5;
+ case 231: return 0x03C4;
+ case 232: return 0x03A6;
+ case 233: return 0x0398;
+ case 234: return 0x03A9;
+ case 235: return 0x03B4;
+ case 236: return 0x221E;
+ case 237: return 0x03C6;
+ case 238: return 0x03B5;
+ case 239: return 0x2229;
+ case 240: return 0x2261;
+ case 241: return 0x00B1;
+ case 242: return 0x2265;
+ case 243: return 0x2264;
+ case 244: return 0x2320;
+ case 245: return 0x2321;
+ case 246: return 0x00F7;
+ case 247: return 0x2248;
+ case 248: return 0x00B0;
+ case 249: return 0x2219;
+ case 250: return 0x00B7;
+ case 251: return 0x221A;
+ case 252: return 0x207F;
+ case 253: return 0x00B2;
+ case 254: return 0x25A0;
+ case 0:
+ case 255:
+ return 0x00A0;
+ default:
+ return charCode;
+ }
+ }
+
+ function unicodeToArray(unicode) {
+ if (unicode < 0x80) {
+ return [unicode];
+ } else if (unicode < 0x800) {
+ return [(unicode >> 6) | 192, (unicode & 63) | 128];
+ }
+ return [(unicode >> 12) | 224, ((unicode >> 6) & 63) | 128, (unicode & 63) | 128];
+ }
+
+ function getUTF8(charCode) {
+ return unicodeToArray(getUnicode(charCode));
+ }
+
+ function encodeANSi(useUTF8) {
+ function ansiColor(binColor) {
+ switch (binColor) {
+ case 1:
+ return 4;
+ case 3:
+ return 6;
+ case 4:
+ return 1;
+ case 6:
+ return 3;
+ default:
+ return binColor;
+ }
+ }
+ var imageData = textArtCanvas.getImageData();
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var output = [27, 91, 48, 109];
+ var bold = false;
+ var blink = false;
+ var currentForeground = 7;
+ var currentBackground = 0;
+ var currentBold = false;
+ var currentBlink = false;
+ for (var inputIndex = 0; inputIndex < rows * columns; inputIndex++) {
+ var attribs = [];
+ var charCode = imageData[inputIndex] >> 8;
+ var foreground = imageData[inputIndex] & 15;
+ var background = imageData[inputIndex] >> 4 & 15;
+ switch (charCode) {
+ case 10:
+ charCode = 9;
+ break;
+ case 13:
+ charCode = 14;
+ break;
+ case 26:
+ charCode = 16;
+ break;
+ case 27:
+ charCode = 17;
+ break;
+ default:
+ break;
+ }
+ if (foreground > 7) {
+ bold = true;
+ foreground = foreground - 8;
+ } else {
+ bold = false;
+ }
+ if (background > 7) {
+ blink = true;
+ background = background - 8;
+ } else {
+ blink = false;
+ }
+ if ((currentBold && !bold) || (currentBlink && !blink)) {
+ attribs.push([48]);
+ currentForeground = 7;
+ currentBackground = 0;
+ currentBold = false;
+ currentBlink = false;
+ }
+ if (bold && !currentBold) {
+ attribs.push([49]);
+ currentBold = true;
+ }
+ if (blink && !currentBlink) {
+ attribs.push([53]);
+ currentBlink = true;
+ }
+ if (foreground !== currentForeground) {
+ attribs.push([51, 48 + ansiColor(foreground)]);
+ currentForeground = foreground;
+ }
+ if (background !== currentBackground) {
+ attribs.push([52, 48 + ansiColor(background)]);
+ currentBackground = background;
+ }
+ if (attribs.length) {
+ output.push(27, 91);
+ for (var attribIndex = 0; attribIndex < attribs.length; attribIndex += 1) {
+ output = output.concat(attribs[attribIndex]);
+ if (attribIndex !== attribs.length - 1) {
+ output.push(59);
+ } else {
+ output.push(109);
+ }
+ }
+ }
+ if (useUTF8 === true) {
+ getUTF8(charCode).forEach((utf8Code) => {
+ output.push(utf8Code);
+ });
+ if ((inputIndex + 1) % columns === 0) {
+ output.push(10);
+ }
+ } else {
+ output.push(charCode);
+ }
+ }
+ var sauce = createSauce(1, 1, output.length, true);
+ saveFile(new Uint8Array(output), sauce, (useUTF8 === true) ? title.getName() + ".utf8.ans" : title.getName() + ".ans");
+ }
+
+ function ans() {
+ encodeANSi(false);
+ }
+
+ function utf8() {
+ encodeANSi(true);
+ }
+
+ function convert16BitArrayTo8BitArray(Uint16s) {
+ var Uint8s = new Uint8Array(Uint16s.length * 2);
+ for (var i = 0, j = 0; i < Uint16s.length; i++, j += 2) {
+ Uint8s[j] = Uint16s[i] >> 8;
+ Uint8s[j + 1] = Uint16s[i] & 255;
+ }
+ return Uint8s;
+ }
+
+ function bin() {
+ var columns = textArtCanvas.getColumns();
+ if (columns % 2 === 0) {
+ var imageData = convert16BitArrayTo8BitArray(textArtCanvas.getImageData());
+ var sauce = createSauce(5, columns / 2, imageData.length, true);
+ saveFile(imageData, sauce, title.getName() + ".bin");
+ }
+ }
+
+ function xb() {
+ var imageData = convert16BitArrayTo8BitArray(textArtCanvas.getImageData());
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var iceColours = textArtCanvas.getIceColours();
+ var flags = 0;
+ if (iceColours === true) {
+ flags += 1 << 3;
+ }
+ var output = new Uint8Array(11 + imageData.length);
+ output.set(new Uint8Array([
+ 88, 66, 73, 78, 26,
+ columns & 255,
+ columns >> 8,
+ rows & 255,
+ rows >> 8,
+ font.getHeight(),
+ flags
+ ]), 0);
+ output.set(imageData, 11);
+ var sauce = createSauce(6, 0, imageData.length, false);
+ saveFile(output, sauce, title.getName() + ".xb");
+ }
+
+ function dataUrlToBytes(dataURL) {
+ var base64Index = dataURL.indexOf(";base64,") + 8;
+ var byteChars = atob(dataURL.substr(base64Index, dataURL.length - base64Index));
+ var bytes = new Uint8Array(byteChars.length);
+ for (var i = 0; i < bytes.length; i++) {
+ bytes[i] = byteChars.charCodeAt(i);
+ }
+ return bytes;
+ }
+
+ function png() {
+ saveFile(dataUrlToBytes(textArtCanvas.getImage().toDataURL()), undefined, title.getName() + ".png");
+ }
+
+ return {
+ "ans": ans,
+ "utf8": utf8,
+ "bin": bin,
+ "xb": xb,
+ "png": png
+ };
+}());
diff --git a/public/js/freehand_tools.js b/public/js/freehand_tools.js
new file mode 100644
index 0000000..27f0103
--- /dev/null
+++ b/public/js/freehand_tools.js
@@ -0,0 +1,1208 @@
+function createPanelCursor(divElement) {
+ "use strict";
+ var cursor = createCanvas(0, 0);
+ cursor.classList.add("cursor");
+ divElement.appendChild(cursor);
+
+ function show() {
+ cursor.style.display = "block";
+ }
+
+ function hide() {
+ cursor.style.display = "none";
+ }
+
+ function resize(width, height) {
+ cursor.style.width = width + "px";
+ cursor.style.height = height + "px";
+ }
+
+ function setPos(x, y) {
+ cursor.style.left = x - 1 + "px";
+ cursor.style.top = y - 1 + "px";
+ }
+
+ return {
+ "show": show,
+ "hide": hide,
+ "resize": resize,
+ "setPos": setPos
+ };
+}
+
+function createFloatingPanelPalette(width, height) {
+ "use strict";
+ var canvasContainer = document.createElement("DIV");
+ var cursor = createPanelCursor(canvasContainer);
+ var canvas = createCanvas(width, height);
+ canvasContainer.appendChild(canvas);
+ var ctx = canvas.getContext("2d");
+ var imageData = new Array(16);
+
+ function generateSwatch(colour) {
+ imageData[colour] = ctx.createImageData(width / 8, height / 2);
+ var rgba = palette.getRGBAColour(colour);
+ for (var y = 0, i = 0; y < imageData[colour].height; y++) {
+ for (var x = 0; x < imageData[colour].width; x++, i += 4) {
+ imageData[colour].data.set(rgba, i);
+ }
+ }
+ }
+
+ function generateSwatches() {
+ for (var colour = 0; colour < 16; colour++) {
+ generateSwatch(colour);
+ }
+ }
+
+ function redrawSwatch(colour) {
+ ctx.putImageData(imageData[colour], (colour % 8) * (width / 8), (colour > 7) ? 0 : (height / 2));
+ }
+
+ function redrawSwatches() {
+ for (var colour = 0; colour < 16; colour++) {
+ redrawSwatch(colour);
+ }
+ }
+
+ function mouseDown(evt) {
+ var rect = canvas.getBoundingClientRect();
+ var mouseX = evt.clientX - rect.left;
+ var mouseY = evt.clientY - rect.top;
+ var colour = Math.floor(mouseX / (width / 8)) + ((mouseY < (height / 2)) ? 8 : 0);
+ if (evt.ctrlKey === false && evt.altKey === false) {
+ palette.setForegroundColour(colour);
+ } else {
+ palette.setBackgroundColour(colour);
+ }
+ }
+
+ function updateColour(colour) {
+ generateSwatch(colour);
+ redrawSwatch(colour);
+ }
+
+ function updatePalette() {
+ for (var colour = 0; colour < 16; colour++) {
+ updateColour(colour);
+ }
+ }
+
+ function getElement() {
+ return canvasContainer;
+ }
+
+ function updateCursor(colour) {
+ cursor.resize(width / 8, height / 2);
+ cursor.setPos((colour % 8) * (width / 8), (colour > 7) ? 0 : (height / 2));
+ }
+
+ function onForegroundChange(evt) {
+ updateCursor(evt.detail);
+ }
+
+ function resize(newWidth, newHeight) {
+ width = newWidth;
+ height = newHeight;
+ canvas.width = width;
+ canvas.height = height;
+ generateSwatches();
+ redrawSwatches();
+ updateCursor(palette.getForegroundColour());
+ }
+
+ generateSwatches();
+ redrawSwatches();
+ updateCursor(palette.getForegroundColour());
+ canvas.addEventListener("mousedown", mouseDown);
+ canvas.addEventListener("contextmenu", (evt) => {
+ evt.preventDefault();
+ });
+ document.addEventListener("onForegroundChange", onForegroundChange);
+
+ return {
+ "updateColour": updateColour,
+ "updatePalette": updatePalette,
+ "getElement": getElement,
+ "showCursor": cursor.show,
+ "hideCursor": cursor.hide,
+ "resize": resize
+ };
+}
+
+function createFloatingPanel(x, y) {
+ "use strict";
+ var panel = document.createElement("DIV");
+ panel.classList.add("floating-panel");
+ $("body-container").appendChild(panel);
+ var enabled = false;
+ var prev;
+
+ function setPos(newX, newY) {
+ panel.style.left = newX + "px";
+ x = newX;
+ panel.style.top = newY + "px";
+ y = newY;
+ }
+
+ function mousedown(evt) {
+ prev = [evt.clientX, evt.clientY];
+ }
+
+ function touchMove(evt) {
+ if (evt.which === 1 && prev !== undefined) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ var rect = panel.getBoundingClientRect();
+ setPos(rect.left + (evt.touches[0].pageX - prev[0]), rect.top + (evt.touches[0].pageY - prev[1]));
+ prev = [evt.touches[0].pageX, evt.touches[0].pageY];
+ }
+ }
+
+ function mouseMove(evt) {
+ if (evt.which === 1 && prev !== undefined) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ var rect = panel.getBoundingClientRect();
+ setPos(rect.left + (evt.clientX - prev[0]), rect.top + (evt.clientY - prev[1]));
+ prev = [evt.clientX, evt.clientY];
+ }
+ }
+
+ function mouseUp() {
+ prev = undefined;
+ }
+
+ function enable() {
+ panel.classList.add("enabled");
+ enabled = true;
+ document.addEventListener("touchmove", touchMove);
+ document.addEventListener("mousemove", mouseMove);
+ document.addEventListener("mouseup", mouseUp);
+ }
+
+ function disable() {
+ panel.classList.remove("enabled");
+ enabled = false;
+ document.removeEventListener("touchmove", touchMove);
+ document.removeEventListener("mousemove", mouseMove);
+ document.removeEventListener("mouseup", mouseUp);
+ }
+
+ function append(element) {
+ panel.appendChild(element);
+ }
+
+ setPos(x, y);
+ panel.addEventListener("mousedown", mousedown);
+
+ return {
+ "setPos": setPos,
+ "enable": enable,
+ "disable": disable,
+ "append": append
+ };
+}
+
+function createFreehandController(panel) {
+ "use strict";
+ var prev = {};
+ var drawMode;
+
+ function line(x0, y0, x1, y1, callback) {
+ var dx = Math.abs(x1 - x0);
+ var sx = (x0 < x1) ? 1 : -1;
+ var dy = Math.abs(y1 - y0);
+ var sy = (y0 < y1) ? 1 : -1;
+ var err = ((dx > dy) ? dx : -dy) / 2;
+ var e2;
+
+ while (true) {
+ callback(x0, y0);
+ if (x0 === x1 && y0 === y1) {
+ break;
+ }
+ e2 = err;
+ if (e2 > -dx) {
+ err -= dy;
+ x0 += sx;
+ }
+ if (e2 < dy) {
+ err += dx;
+ y0 += sy;
+ }
+ }
+ }
+
+ function draw(coords) {
+ if (prev.x !== coords.x || prev.y !== coords.y || prev.halfBlockY !== coords.halfBlockY) {
+ if (drawMode.halfBlockMode === true) {
+ var colour = (coords.leftMouseButton === true) ? palette.getForegroundColour() : palette.getBackgroundColour();
+ if (Math.abs(prev.x - coords.x) > 1 || Math.abs(prev.halfBlockY - coords.halfBlockY) > 1) {
+ textArtCanvas.drawHalfBlock((callback) => {
+ line(prev.x, prev.halfBlockY, coords.x, coords.halfBlockY, (x, y) => {
+ callback(colour, x, y);
+ });
+ });
+ } else {
+ textArtCanvas.drawHalfBlock((callback) => {
+ callback(colour, coords.x, coords.halfBlockY);
+ });
+ }
+ } else {
+ if (Math.abs(prev.x - coords.x) > 1 || Math.abs(prev.y - coords.y) > 1) {
+ textArtCanvas.draw((callback) => {
+ line(prev.x, prev.y, coords.x, coords.y, (x, y) => {
+ callback(drawMode.charCode, drawMode.foreground, drawMode.background, x, y);
+ });
+ }, false);
+ } else {
+ textArtCanvas.draw((callback) => {
+ callback(drawMode.charCode, drawMode.foreground, drawMode.background, coords.x, coords.y);
+ }, false);
+ }
+ }
+ positionInfo.update(coords.x, coords.y);
+ prev = coords;
+ }
+ }
+
+ function canvasUp() {
+ prev = {};
+ }
+
+ function canvasDown(evt) {
+ drawMode = panel.getMode();
+ textArtCanvas.startUndo();
+ draw(evt.detail);
+ }
+
+ function canvasDrag(evt) {
+ draw(evt.detail);
+ }
+
+ function enable() {
+ document.addEventListener("onTextCanvasDown", canvasDown);
+ document.addEventListener("onTextCanvasUp", canvasUp);
+ document.addEventListener("onTextCanvasDrag", canvasDrag);
+ panel.enable();
+ }
+
+ function disable() {
+ document.removeEventListener("onTextCanvasDown", canvasDown);
+ document.removeEventListener("onTextCanvasUp", canvasUp);
+ document.removeEventListener("onTextCanvasDrag", canvasDrag);
+ panel.disable();
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable,
+ "select": panel.select,
+ "ignore": panel.ignore,
+ "unignore": panel.unignore
+ };
+}
+
+function createShadingPanel() {
+ "use strict";
+ var panelWidth = font.getWidth() * 20;
+ var panel = createFloatingPanel(50, 30);
+ var palettePanel = createFloatingPanelPalette(panelWidth, 40);
+ var canvasContainer = document.createElement("div");
+ var cursor = createPanelCursor(canvasContainer);
+ var canvases = new Array(16);
+ var halfBlockMode = true;
+ var x = 0;
+ var y = 0;
+ var ignored = false;
+
+ function updateCursor() {
+ var width = canvases[0].width / 5;
+ var height = canvases[0].height / 15;
+ cursor.resize(width, height);
+ cursor.setPos(x * width, y * height);
+ }
+
+ function mouseDownGenerator(colour) {
+ return function(evt) {
+ var rect = canvases[colour].getBoundingClientRect();
+ var mouseX = evt.clientX - rect.left;
+ var mouseY = evt.clientY - rect.top;
+ halfBlockMode = false;
+ x = Math.floor(mouseX / (canvases[colour].width / 5));
+ y = Math.floor(mouseY / (canvases[colour].height / 15));
+ palettePanel.hideCursor();
+ updateCursor();
+ cursor.show();
+ };
+ }
+
+ function generateCanvases() {
+ var fontHeight = font.getHeight();
+ for (var foreground = 0; foreground < 16; foreground++) {
+ var canvas = createCanvas(panelWidth, fontHeight * 15);
+ var ctx = canvas.getContext("2d");
+ var y = 0;
+ for (var background = 0; background < 8; background++) {
+ if (foreground !== background) {
+ for (var i = 0; i < 4; i++) {
+ font.draw(219, foreground, background, ctx, i, y);
+ }
+ for (var i = 4; i < 8; i++) {
+ font.draw(178, foreground, background, ctx, i, y);
+ }
+ for (var i = 8; i < 12; i++) {
+ font.draw(177, foreground, background, ctx, i, y);
+ }
+ for (var i = 12; i < 16; i++) {
+ font.draw(176, foreground, background, ctx, i, y);
+ }
+ for (var i = 16; i < 20; i++) {
+ font.draw(0, foreground, background, ctx, i, y);
+ }
+ y += 1;
+ }
+ }
+ for (var background = 8; background < 16; background++) {
+ if (foreground !== background) {
+ for (var i = 0; i < 4; i++) {
+ font.draw(219, foreground, background, ctx, i, y);
+ }
+ for (var i = 4; i < 8; i++) {
+ font.draw(178, foreground, background, ctx, i, y);
+ }
+ for (var i = 8; i < 12; i++) {
+ font.draw(177, foreground, background, ctx, i, y);
+ }
+ for (var i = 12; i < 16; i++) {
+ font.draw(176, foreground, background, ctx, i, y);
+ }
+ for (var i = 16; i < 20; i++) {
+ font.draw(0, foreground, background, ctx, i, y);
+ }
+ y += 1;
+ }
+ }
+ canvas.addEventListener("mousedown", mouseDownGenerator(foreground));
+ canvases[foreground] = canvas;
+ }
+ }
+
+ function keyDown(evt) {
+ if (ignored === false) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (halfBlockMode === false) {
+ switch (keyCode) {
+ case 37:
+ evt.preventDefault();
+ x = Math.max(x - 1, 0);
+ updateCursor();
+ break;
+ case 38:
+ evt.preventDefault();
+ y = Math.max(y - 1, 0);
+ updateCursor();
+ break;
+ case 39:
+ evt.preventDefault();
+ x = Math.min(x + 1, 4);
+ updateCursor();
+ break;
+ case 40:
+ evt.preventDefault();
+ y = Math.min(y + 1, 14);
+ updateCursor();
+ break;
+ default:
+ break;
+ }
+ } else if (keyCode >= 37 && keyCode <= 40) {
+ evt.preventDefault();
+ halfBlockMode = false;
+ palettePanel.hideCursor();
+ cursor.show();
+ }
+ }
+ }
+
+ function enable() {
+ document.addEventListener("keydown", keyDown);
+ panel.enable();
+ }
+
+ function disable() {
+ document.removeEventListener("keydown", keyDown);
+ panel.disable();
+ }
+
+ function ignore() {
+ ignored = true;
+ }
+
+ function unignore() {
+ ignored = false;
+ }
+
+ function getMode() {
+ var charCode = 0;
+ switch (x) {
+ case 0: charCode = 219; break;
+ case 1: charCode = 178; break;
+ case 2: charCode = 177; break;
+ case 3: charCode = 176; break;
+ case 4: charCode = 0; break;
+ default: break;
+ }
+ var foreground = palette.getForegroundColour();
+ var background = y;
+ if (y >= foreground) {
+ background += 1;
+ }
+ return {
+ "halfBlockMode": halfBlockMode,
+ "foreground": foreground,
+ "background": background,
+ "charCode": charCode
+ };
+ }
+
+ function foregroundChange(evt) {
+ canvasContainer.removeChild(canvasContainer.firstChild);
+ canvasContainer.insertBefore(canvases[evt.detail], canvasContainer.firstChild);
+ palettePanel.showCursor();
+ cursor.hide();
+ halfBlockMode = true;
+ }
+
+ function fontChange() {
+ panelWidth = font.getWidth() * 20;
+ palettePanel.resize(panelWidth, 40);
+ generateCanvases();
+ updateCursor();
+ canvasContainer.removeChild(canvasContainer.firstChild);
+ canvasContainer.insertBefore(canvases[palette.getForegroundColour()], canvasContainer.firstChild);
+ }
+
+ function select(charCode) {
+ halfBlockMode = false;
+ x = 3 - (charCode - 176);
+ y = palette.getBackgroundColour();
+ if (y > palette.getForegroundColour()) {
+ y -= 1;
+ }
+ palettePanel.hideCursor();
+ updateCursor();
+ cursor.show();
+ }
+
+ document.addEventListener("onForegroundChange", foregroundChange);
+ document.addEventListener("onLetterSpacingChange", fontChange);
+ document.addEventListener("onFontChange", fontChange);
+
+ palettePanel.showCursor();
+ panel.append(palettePanel.getElement());
+ generateCanvases();
+ updateCursor();
+ canvasContainer.insertBefore(canvases[palette.getForegroundColour()], canvasContainer.firstChild);
+ panel.append(canvasContainer);
+ cursor.hide();
+
+ return {
+ "enable": enable,
+ "disable": disable,
+ "getMode": getMode,
+ "select": select,
+ "ignore": ignore,
+ "unignore": unignore
+ };
+}
+
+function createCharacterBrushPanel() {
+ "use strict";
+ var panelWidth = font.getWidth() * 16;
+ var panel = createFloatingPanel(50, 30);
+ var palettePanel = createFloatingPanelPalette(panelWidth, 40);
+ var canvasContainer = document.createElement("div");
+ var cursor = createPanelCursor(canvasContainer);
+ var canvas = createCanvas(panelWidth, font.getHeight() * 16);
+ var ctx = canvas.getContext("2d");
+ var x = 0;
+ var y = 0;
+ var ignored = false;
+
+ function updateCursor() {
+ var width = canvas.width / 16;
+ var height = canvas.height / 16;
+ cursor.resize(width, height);
+ cursor.setPos(x * width, y * height);
+ }
+
+ function redrawCanvas() {
+ var foreground = palette.getForegroundColour();
+ var background = palette.getBackgroundColour();
+ for (var y = 0, charCode = 0; y < 16; y++) {
+ for (var x = 0; x < 16; x++, charCode++) {
+ font.draw(charCode, foreground, background, ctx, x, y);
+ }
+ }
+ }
+
+ function keyDown(evt) {
+ if (ignored === false) {
+ var keyCode = (evt.keyCode || evt.which);
+ switch (keyCode) {
+ case 37:
+ evt.preventDefault();
+ x = Math.max(x - 1, 0);
+ updateCursor();
+ break;
+ case 38:
+ evt.preventDefault();
+ y = Math.max(y - 1, 0);
+ updateCursor();
+ break;
+ case 39:
+ evt.preventDefault();
+ x = Math.min(x + 1, 15);
+ updateCursor();
+ break;
+ case 40:
+ evt.preventDefault();
+ y = Math.min(y + 1, 15);
+ updateCursor();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ function enable() {
+ document.addEventListener("keydown", keyDown);
+ panel.enable();
+ }
+
+ function disable() {
+ document.removeEventListener("keydown", keyDown);
+ panel.disable();
+ }
+
+ function getMode() {
+ var charCode = y * 16 + x;
+ return {
+ "halfBlockMode": false,
+ "foreground": palette.getForegroundColour(),
+ "background": palette.getBackgroundColour(),
+ "charCode": charCode
+ };
+ }
+
+ function resizeCanvas() {
+ panelWidth = font.getWidth() * 16;
+ palettePanel.resize(panelWidth, 40);
+ canvas.width = panelWidth;
+ canvas.height = font.getHeight() * 16;
+ redrawCanvas();
+ updateCursor();
+ }
+
+ function mouseUp(evt) {
+ var rect = canvas.getBoundingClientRect();
+ var mouseX = evt.clientX - rect.left;
+ var mouseY = evt.clientY - rect.top;
+ x = Math.floor(mouseX / (canvas.width / 16));
+ y = Math.floor(mouseY / (canvas.height / 16));
+ updateCursor();
+ }
+
+ function select(charCode) {
+ x = charCode % 16;
+ y = Math.floor(charCode / 16);
+ updateCursor();
+ }
+
+ function ignore() {
+ ignored = true;
+ }
+
+ function unignore() {
+ ignored = false;
+ }
+
+ document.addEventListener("onForegroundChange", redrawCanvas);
+ document.addEventListener("onBackgroundChange", redrawCanvas);
+ document.addEventListener("onLetterSpacingChange", resizeCanvas);
+ document.addEventListener("onFontChange", resizeCanvas);
+ canvas.addEventListener("mouseup", mouseUp);
+
+ panel.append(palettePanel.getElement());
+ updateCursor();
+ cursor.show();
+ canvasContainer.appendChild(canvas);
+ panel.append(canvasContainer);
+ redrawCanvas();
+
+ return {
+ "enable": enable,
+ "disable": disable,
+ "getMode": getMode,
+ "select": select,
+ "ignore": ignore,
+ "unignore": unignore
+ };
+}
+
+function createFillController() {
+ "use strict";
+
+ function fillPoint(evt) {
+ var block = textArtCanvas.getHalfBlock(evt.detail.x, evt.detail.halfBlockY);
+ if (block.isBlocky) {
+ var targetColour = (block.halfBlockY === 0) ? block.upperBlockColour : block.lowerBlockColour;
+ var fillColour = palette.getForegroundColour();
+ if (targetColour !== fillColour) {
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var coord = [evt.detail.x, evt.detail.halfBlockY];
+ var queue = [coord];
+ textArtCanvas.startUndo();
+ textArtCanvas.drawHalfBlock((callback) => {
+ while (queue.length !== 0) {
+ coord = queue.pop();
+ block = textArtCanvas.getHalfBlock(coord[0], coord[1]);
+ if (block.isBlocky && (((block.halfBlockY === 0) && (block.upperBlockColour === targetColour)) || ((block.halfBlockY === 1) && (block.lowerBlockColour === targetColour)))) {
+ callback(fillColour, coord[0], coord[1]);
+ if (coord[0] > 0) {
+ queue.push([coord[0] - 1, coord[1], 0]);
+ }
+ if (coord[0] < columns - 1) {
+ queue.push([coord[0] + 1, coord[1], 1]);
+ }
+ if (coord[1] > 0) {
+ queue.push([coord[0], coord[1] - 1, 2]);
+ }
+ if (coord[1] < rows * 2 - 1) {
+ queue.push([coord[0], coord[1] + 1, 3]);
+ }
+ } else if (block.isVerticalBlocky) {
+ if (coord[2] !== 0 && block.leftBlockColour === targetColour) {
+ textArtCanvas.draw(function(callback) {
+ callback(221, fillColour, block.rightBlockColour, coord[0], block.textY);
+ }, true);
+ if (coord[0] > 0) {
+ queue.push([coord[0] - 1, coord[1], 0]);
+ }
+ if (coord[1] > 2) {
+ if (block.halfBlockY === 1) {
+ queue.push([coord[0], coord[1] - 2, 2]);
+ } else {
+ queue.push([coord[0], coord[1] - 1, 2]);
+ }
+ }
+ if (coord[1] < rows * 2 - 2) {
+ if (block.halfBlockY === 1) {
+ queue.push([coord[0], coord[1] + 1, 3]);
+ } else {
+ queue.push([coord[0], coord[1] + 2, 3]);
+ }
+ }
+ }
+ if (coord[2] !== 1 && block.rightBlockColour === targetColour) {
+ textArtCanvas.draw(function(callback) {
+ callback(222, fillColour, block.leftBlockColour, coord[0], block.textY);
+ }, true);
+ if (coord[0] > 0) {
+ queue.push([coord[0] - 1, coord[1], 0]);
+ }
+ if (coord[1] > 2) {
+ if (block.halfBlockY === 1) {
+ queue.push([coord[0], coord[1] - 2, 2]);
+ } else {
+ queue.push([coord[0], coord[1] - 1, 2]);
+ }
+ }
+ if (coord[1] < rows * 2 - 2) {
+ if (block.halfBlockY === 1) {
+ queue.push([coord[0], coord[1] + 1, 3]);
+ } else {
+ queue.push([coord[0], coord[1] + 2, 3]);
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ function enable() {
+ document.addEventListener("onTextCanvasDown", fillPoint);
+ }
+
+ function disable() {
+ document.removeEventListener("onTextCanvasDown", fillPoint);
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable
+ };
+}
+
+function createLineController() {
+ "use strict";
+ var startXY;
+ var endXY;
+
+ function canvasDown(evt) {
+ startXY = evt.detail;
+ }
+
+ function line(x0, y0, x1, y1, callback) {
+ var dx = Math.abs(x1 - x0);
+ var sx = (x0 < x1) ? 1 : -1;
+ var dy = Math.abs(y1 - y0);
+ var sy = (y0 < y1) ? 1 : -1;
+ var err = ((dx > dy) ? dx : -dy) / 2;
+ var e2;
+
+ while (true) {
+ callback(x0, y0);
+ if (x0 === x1 && y0 === y1) {
+ break;
+ }
+ e2 = err;
+ if (e2 > -dx) {
+ err -= dy;
+ x0 += sx;
+ }
+ if (e2 < dy) {
+ err += dx;
+ y0 += sy;
+ }
+ }
+ }
+
+ function canvasUp() {
+ toolPreview.clear();
+ var foreground = palette.getForegroundColour();
+ textArtCanvas.startUndo();
+ textArtCanvas.drawHalfBlock((draw) => {
+ line(startXY.x, startXY.halfBlockY, endXY.x, endXY.halfBlockY, function(lineX, lineY) {
+ draw(foreground, lineX, lineY);
+ });
+ });
+ startXY = undefined;
+ endXY = undefined;
+ }
+
+ function canvasDrag(evt) {
+ if (startXY !== undefined) {
+ if (endXY === undefined || (evt.detail.x !== endXY.x || evt.detail.y !== endXY.y || evt.detail.halfBlockY !== endXY.halfBlockY)) {
+ if (endXY !== undefined) {
+ toolPreview.clear();
+ }
+ endXY = evt.detail;
+ var foreground = palette.getForegroundColour();
+ line(startXY.x, startXY.halfBlockY, endXY.x, endXY.halfBlockY, function(lineX, lineY) {
+ toolPreview.drawHalfBlock(foreground, lineX, lineY);
+ });
+ }
+ }
+ }
+
+ function enable() {
+ document.addEventListener("onTextCanvasDown", canvasDown);
+ document.addEventListener("onTextCanvasUp", canvasUp);
+ document.addEventListener("onTextCanvasDrag", canvasDrag);
+ }
+
+ function disable() {
+ document.removeEventListener("onTextCanvasDown", canvasDown);
+ document.removeEventListener("onTextCanvasUp", canvasUp);
+ document.removeEventListener("onTextCanvasDrag", canvasDrag);
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable
+ };
+}
+
+function createSquareController() {
+ "use strict";
+ var panel = createFloatingPanel(50, 30);
+ var palettePanel = createFloatingPanelPalette(160, 40);
+ var startXY;
+ var endXY;
+ var outlineMode = true;
+ var outlineToggle = createToggleButton("Outline", "Filled", () => {
+ outlineMode = true;
+ }, () => {
+ outlineMode = false;
+ });
+
+ function canvasDown(evt) {
+ startXY = evt.detail;
+ }
+
+ function processCoords() {
+ var x0, y0, x1, y1;
+ if (startXY.x < endXY.x) {
+ x0 = startXY.x;
+ x1 = endXY.x;
+ } else {
+ x0 = endXY.x;
+ x1 = startXY.x;
+ }
+ if (startXY.halfBlockY < endXY.halfBlockY) {
+ y0 = startXY.halfBlockY;
+ y1 = endXY.halfBlockY;
+ } else {
+ y0 = endXY.halfBlockY;
+ y1 = startXY.halfBlockY;
+ }
+ return { "x0": x0, "y0": y0, "x1": x1, "y1": y1 };
+ }
+
+ function canvasUp() {
+ toolPreview.clear();
+ var coords = processCoords();
+ var foreground = palette.getForegroundColour();
+ textArtCanvas.startUndo();
+ textArtCanvas.drawHalfBlock((draw) => {
+ if (outlineMode === true) {
+ for (var px = coords.x0; px <= coords.x1; px++) {
+ draw(foreground, px, coords.y0);
+ draw(foreground, px, coords.y1);
+ }
+ for (var py = coords.y0 + 1; py < coords.y1; py++) {
+ draw(foreground, coords.x0, py);
+ draw(foreground, coords.x1, py);
+ }
+ } else {
+ for (var py = coords.y0; py <= coords.y1; py++) {
+ for (var px = coords.x0; px <= coords.x1; px++) {
+ draw(foreground, px, py);
+ }
+ }
+ }
+ });
+ startXY = undefined;
+ endXY = undefined;
+ }
+
+ function canvasDrag(evt) {
+ if (startXY !== undefined) {
+ if (evt.detail.x !== startXY.x || evt.detail.y !== startXY.y || evt.detail.halfBlockY !== startXY.halfBlockY) {
+ if (endXY !== undefined) {
+ toolPreview.clear();
+ }
+ endXY = evt.detail;
+ var coords = processCoords();
+ var foreground = palette.getForegroundColour();
+ if (outlineMode === true) {
+ for (var px = coords.x0; px <= coords.x1; px++) {
+ toolPreview.drawHalfBlock(foreground, px, coords.y0);
+ toolPreview.drawHalfBlock(foreground, px, coords.y1);
+ }
+ for (var py = coords.y0 + 1; py < coords.y1; py++) {
+ toolPreview.drawHalfBlock(foreground, coords.x0, py);
+ toolPreview.drawHalfBlock(foreground, coords.x1, py);
+ }
+ } else {
+ for (var py = coords.y0; py <= coords.y1; py++) {
+ for (var px = coords.x0; px <= coords.x1; px++) {
+ toolPreview.drawHalfBlock(foreground, px, py);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function enable() {
+ panel.enable();
+ document.addEventListener("onTextCanvasDown", canvasDown);
+ document.addEventListener("onTextCanvasUp", canvasUp);
+ document.addEventListener("onTextCanvasDrag", canvasDrag);
+ }
+
+ function disable() {
+ panel.disable();
+ document.removeEventListener("onTextCanvasDown", canvasDown);
+ document.removeEventListener("onTextCanvasUp", canvasUp);
+ document.removeEventListener("onTextCanvasDrag", canvasDrag);
+ }
+
+ panel.append(palettePanel.getElement());
+ palettePanel.showCursor();
+ panel.append(outlineToggle.getElement());
+ if (outlineMode === true) {
+ outlineToggle.setStateOne();
+ } else {
+ outlineToggle.setStateTwo();
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable
+ };
+}
+
+function createCircleController() {
+ "use strict";
+ var panel = createFloatingPanel(50, 30);
+ var palettePanel = createFloatingPanelPalette(160, 40);
+ var startXY;
+ var endXY;
+ var outlineMode = true;
+ var outlineToggle = createToggleButton("Outline", "Filled", () => {
+ outlineMode = true;
+ }, () => {
+ outlineMode = false;
+ });
+
+ function canvasDown(evt) {
+ startXY = evt.detail;
+ }
+
+ function processCoords() {
+ var sx, sy, width, height;
+ sx = startXY.x;
+ sy = startXY.halfBlockY;
+ width = Math.abs(endXY.x - startXY.x);
+ height = Math.abs(endXY.halfBlockY - startXY.halfBlockY);
+ return {
+ "sx": sx,
+ "sy": sy,
+ "width": width,
+ "height": height
+ };
+ }
+
+ function ellipseOutline(sx, sy, width, height, callback) {
+ var a2 = width * width;
+ var b2 = height * height;
+ var fa2 = 4 * a2;
+ var fb2 = 4 * b2;
+ for (var px = 0, py = height, sigma = 2 * b2 + a2 * (1 - 2 * height); b2 * px <= a2 * py; px += 1) {
+ callback(sx + px, sy + py);
+ callback(sx - px, sy + py);
+ callback(sx + px, sy - py);
+ callback(sx - px, sy - py);
+ if (sigma >= 0) {
+ sigma += fa2 * (1 - py);
+ py -= 1;
+ }
+ sigma += b2 * ((4 * px) + 6);
+ }
+ for (var px = width, py = 0, sigma = 2 * a2 + b2 * (1 - 2 * width); a2 * py <= b2 * px; py += 1) {
+ callback(sx + px, sy + py);
+ callback(sx - px, sy + py);
+ callback(sx + px, sy - py);
+ callback(sx - px, sy - py);
+ if (sigma >= 0) {
+ sigma += fb2 * (1 - px);
+ px -= 1;
+ }
+ sigma += a2 * ((4 * py) + 6);
+ }
+ }
+
+ function ellipseFilled(sx, sy, width, height, callback) {
+ var a2 = width * width;
+ var b2 = height * height;
+ var fa2 = 4 * a2;
+ var fb2 = 4 * b2;
+ for (var px = 0, py = height, sigma = 2 * b2 + a2 * (1 - 2 * height); b2 * px <= a2 * py; px += 1) {
+ var amount = px * 2;
+ var start = sx - px;
+ var y0 = sy + py;
+ var y1 = sy - py;
+ for (var i = 0; i < amount; i++) {
+ callback(start + i, y0);
+ callback(start + i, y1);
+ }
+ if (sigma >= 0) {
+ sigma += fa2 * (1 - py);
+ py -= 1;
+ }
+ sigma += b2 * ((4 * px) + 6);
+ }
+ for (var px = width, py = 0, sigma = 2 * a2 + b2 * (1 - 2 * width); a2 * py <= b2 * px; py += 1) {
+ var amount = px * 2;
+ var start = sx - px;
+ var y0 = sy + py;
+ var y1 = sy - py;
+ for (var i = 0; i < amount; i++) {
+ callback(start + i, y0);
+ callback(start + i, y1);
+ }
+ if (sigma >= 0) {
+ sigma += fb2 * (1 - px);
+ px -= 1;
+ }
+ sigma += a2 * ((4 * py) + 6);
+ }
+ }
+
+ function canvasUp() {
+ toolPreview.clear();
+ var coords = processCoords();
+ var foreground = palette.getForegroundColour();
+ textArtCanvas.startUndo();
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var doubleRows = rows * 2;
+ textArtCanvas.drawHalfBlock((draw) => {
+ if (outlineMode === true) {
+ ellipseOutline(coords.sx, coords.sy, coords.width, coords.height, (px, py) => {
+ if (px >= 0 && px < columns && py >= 0 && py < doubleRows) {
+ draw(foreground, px, py);
+ }
+ });
+ } else {
+ ellipseFilled(coords.sx, coords.sy, coords.width, coords.height, (px, py) => {
+ if (px >= 0 && px < columns && py >= 0 && py < doubleRows) {
+ draw(foreground, px, py);
+ }
+ });
+ }
+ });
+ startXY = undefined;
+ endXY = undefined;
+ }
+
+ function canvasDrag(evt) {
+ if (startXY !== undefined) {
+ if (evt.detail.x !== startXY.x || evt.detail.y !== startXY.y || evt.detail.halfBlockY !== startXY.halfBlockY) {
+ if (endXY !== undefined) {
+ toolPreview.clear();
+ }
+ endXY = evt.detail;
+ var coords = processCoords();
+ var foreground = palette.getForegroundColour();
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var doubleRows = rows * 2;
+ if (outlineMode === true) {
+ ellipseOutline(coords.sx, coords.sy, coords.width, coords.height, (px, py) => {
+ if (px >= 0 && px < columns && py >= 0 && py < doubleRows) {
+ toolPreview.drawHalfBlock(foreground, px, py);
+ }
+ });
+ } else {
+ ellipseFilled(coords.sx, coords.sy, coords.width, coords.height, (px, py) => {
+ if (px >= 0 && px < columns && py >= 0 && py < doubleRows) {
+ toolPreview.drawHalfBlock(foreground, px, py);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ function enable() {
+ panel.enable();
+ document.addEventListener("onTextCanvasDown", canvasDown);
+ document.addEventListener("onTextCanvasUp", canvasUp);
+ document.addEventListener("onTextCanvasDrag", canvasDrag);
+ }
+
+ function disable() {
+ panel.disable();
+ document.removeEventListener("onTextCanvasDown", canvasDown);
+ document.removeEventListener("onTextCanvasUp", canvasUp);
+ document.removeEventListener("onTextCanvasDrag", canvasDrag);
+ }
+
+ panel.append(palettePanel.getElement());
+ palettePanel.showCursor();
+ panel.append(outlineToggle.getElement());
+ if (outlineMode === true) {
+ outlineToggle.setStateOne();
+ } else {
+ outlineToggle.setStateTwo();
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable
+ };
+}
+
+function createSampleTool(divElement, freestyle, divFreestyle, characterBrush, divCharacterBrush) {
+ "use strict";
+
+ function sample(x, halfBlockY) {
+ var block = textArtCanvas.getHalfBlock(x, halfBlockY);
+ if (block.isBlocky) {
+ if (block.halfBlockY === 0) {
+ palette.setForegroundColour(block.upperBlockColour);
+ } else {
+ palette.setForegroundColour(block.lowerBlockColour);
+ }
+ } else {
+ block = textArtCanvas.getBlock(block.x, Math.floor(block.y / 2));
+ palette.setForegroundColour(block.foregroundColour);
+ palette.setBackgroundColour(block.backgroundColour);
+ if (block.charCode >= 176 && block.charCode <= 178) {
+ freestyle.select(block.charCode);
+ divFreestyle.click();
+ } else {
+ characterBrush.select(block.charCode);
+ divCharacterBrush.click();
+ }
+ }
+ }
+
+ function canvasDown(evt) {
+ sample(evt.detail.x, evt.detail.halfBlockY);
+ }
+
+ function enable() {
+ document.addEventListener("onTextCanvasDown", canvasDown);
+ }
+
+ function disable() {
+ document.removeEventListener("onTextCanvasDown", canvasDown);
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable,
+ "sample": sample
+ };
+}
+
+function createSelectionTool(divElement) {
+ "use strict";
+ function canvasDown(evt) {
+ selectionCursor.setStart(evt.detail.x, evt.detail.y);
+ selectionCursor.setEnd(evt.detail.x, evt.detail.y);
+ }
+
+ function canvasDrag(evt) {
+ selectionCursor.setEnd(evt.detail.x, evt.detail.y);
+ }
+
+ function enable() {
+ document.addEventListener("onTextCanvasDown", canvasDown);
+ document.addEventListener("onTextCanvasDrag", canvasDrag);
+ }
+
+ function disable() {
+ selectionCursor.hide();
+ document.removeEventListener("onTextCanvasDown", canvasDown);
+ document.removeEventListener("onTextCanvasDrag", canvasDrag);
+ pasteTool.disable();
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable
+ };
+}
diff --git a/public/js/keyboard.js b/public/js/keyboard.js
new file mode 100644
index 0000000..383c10a
--- /dev/null
+++ b/public/js/keyboard.js
@@ -0,0 +1,720 @@
+function createFKeyShorcut(canvas, charCode) {
+ "use strict";
+ function update() {
+ canvas.style.width = font.getWidth() + "px";
+ canvas.style.height = font.getHeight() + "px";
+ font.draw(charCode, palette.getForegroundColour(), palette.getBackgroundColour(), canvas.getContext("2d"), 0, 0);
+ }
+ document.addEventListener("onForegroundChange", update);
+ document.addEventListener("onBackgroundChange", update);
+ document.addEventListener("onFontChange", update);
+
+ update();
+}
+
+function createFKeysShortcut() {
+ "use strict";
+ var shortcuts = [176, 177, 178, 219, 223, 220, 221, 222, 254, 249, 7, 0];
+
+ for (var i = 0; i < 12; i++) {
+ createFKeyShorcut($("fkey" + i), shortcuts[i]);
+ }
+
+ function keyDown(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.altKey === false && evt.ctrlKey === false && evt.metaKey === false && keyCode >= 112 && keyCode <= 124) {
+ evt.preventDefault();
+ textArtCanvas.startUndo();
+ textArtCanvas.draw((callback) => {
+ callback(shortcuts[keyCode - 112], palette.getForegroundColour(), palette.getBackgroundColour(), cursor.getX(), cursor.getY());
+ }, false);
+ cursor.right();
+ }
+ }
+
+ function enable() {
+ document.addEventListener("keydown", keyDown);
+
+ }
+
+ function disable() {
+ document.removeEventListener("keydown", keyDown);
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable
+ };
+}
+
+function createCursor(canvasContainer) {
+ "use strict";
+ var canvas = createCanvas(font.getWidth(), font.getHeight());
+ var x = 0;
+ var y = 0;
+ var dx = 0;
+ var dy = 0;
+ var visible = false;
+
+ function show() {
+ canvas.style.display = "block";
+ visible = true;
+ }
+
+ function hide() {
+ canvas.style.display = "none";
+ visible = false;
+ }
+
+ function startSelection() {
+ selectionCursor.setStart(x, y);
+ dx = x;
+ dy = y;
+ hide();
+ }
+
+ function endSelection() {
+ selectionCursor.hide();
+ show();
+ }
+
+ function move(newX, newY) {
+ if (selectionCursor.isVisible() === true) {
+ endSelection();
+ }
+ x = Math.min(Math.max(newX, 0), textArtCanvas.getColumns() - 1);
+ y = Math.min(Math.max(newY, 0), textArtCanvas.getRows() - 1);
+ var canvasWidth = font.getWidth();
+ canvas.style.left = (x * canvasWidth) - 1 + "px";
+ canvas.style.top = (y * font.getHeight()) - 1 + "px";
+ positionInfo.update(x, y);
+ pasteTool.setSelection(x, y, 1, 1);
+ }
+
+ function updateDimensions() {
+ canvas.width = font.getWidth() + 1;
+ canvas.height = font.getHeight() + 1;
+ move(x, y);
+ }
+
+ function getX() {
+ return x;
+ }
+
+ function getY() {
+ return y;
+ }
+
+ function left() {
+ move(x - 1, y);
+ }
+
+ function right() {
+ move(x + 1, y);
+ }
+
+ function up() {
+ move(x, y - 1);
+ }
+
+ function down() {
+ move(x, y + 1);
+ }
+
+ function newLine() {
+ move(0, y + 1);
+ }
+
+ function startOfCurrentRow() {
+ move(0, y);
+ }
+
+ function endOfCurrentRow() {
+ move(textArtCanvas.getColumns() - 1, y);
+ }
+
+ function shiftLeft() {
+ if (selectionCursor.isVisible() === false) {
+ startSelection();
+ }
+ dx = Math.max(dx - 1, 0);
+ selectionCursor.setEnd(dx, dy);
+ }
+
+ function shiftRight() {
+ if (selectionCursor.isVisible() === false) {
+ startSelection();
+ }
+ dx = Math.min(dx + 1, textArtCanvas.getColumns() - 1);
+ selectionCursor.setEnd(dx, dy);
+ }
+
+ function shiftUp() {
+ if (selectionCursor.isVisible() === false) {
+ startSelection();
+ }
+ dy = Math.max(dy - 1, 0);
+ selectionCursor.setEnd(dx, dy);
+ }
+
+ function shiftDown() {
+ if (selectionCursor.isVisible() === false) {
+ startSelection();
+ }
+ dy = Math.min(dy + 1, textArtCanvas.getRows() - 1);
+ selectionCursor.setEnd(dx, dy);
+ }
+
+ function keyDown(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.ctrlKey === false && evt.altKey === false) {
+ if (evt.shiftKey === false && evt.metaKey === false) {
+ switch (keyCode) {
+ case 13:
+ evt.preventDefault();
+ newLine();
+ break;
+ case 35:
+ evt.preventDefault();
+ endOfCurrentRow();
+ break;
+ case 36:
+ evt.preventDefault();
+ startOfCurrentRow();
+ break;
+ case 37:
+ evt.preventDefault();
+ left();
+ break;
+ case 38:
+ evt.preventDefault();
+ up();
+ break;
+ case 39:
+ evt.preventDefault();
+ right();
+ break;
+ case 40:
+ evt.preventDefault();
+ down();
+ break;
+ default:
+ break;
+ }
+ } else if (evt.metaKey === true && evt.shiftKey === false) {
+ switch (keyCode) {
+ case 37:
+ evt.preventDefault();
+ startOfCurrentRow();
+ break;
+ case 39:
+ evt.preventDefault();
+ endOfCurrentRow();
+ break;
+ default:
+ break;
+ }
+ } else if (evt.shiftKey === true && evt.metaKey === false) {
+ switch (keyCode) {
+ case 37:
+ evt.preventDefault();
+ shiftLeft();
+ break;
+ case 38:
+ evt.preventDefault();
+ shiftUp();
+ break;
+ case 39:
+ evt.preventDefault();
+ shiftRight();
+ break;
+ case 40:
+ evt.preventDefault();
+ shiftDown();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+
+ function enable() {
+ document.addEventListener("keydown", keyDown);
+ show();
+ pasteTool.setSelection(x, y, 1, 1);
+ }
+
+ function disable() {
+ document.removeEventListener("keydown", keyDown);
+ hide();
+ pasteTool.disable();
+ }
+
+ function isVisible() {
+ return visible;
+ }
+
+ canvas.classList.add("cursor");
+ hide();
+ canvasContainer.insertBefore(canvas, canvasContainer.firstChild);
+ document.addEventListener("onLetterSpacingChange", updateDimensions);
+ document.addEventListener("onTextCanvasSizeChange", updateDimensions);
+ document.addEventListener("onFontChange", updateDimensions);
+ document.addEventListener("onOpenedFile", updateDimensions);
+ move(x, y);
+
+ return {
+ "show": show,
+ "hide": hide,
+ "move": move,
+ "getX": getX,
+ "getY": getY,
+ "left": left,
+ "right": right,
+ "up": up,
+ "down": down,
+ "newLine": newLine,
+ "startOfCurrentRow": startOfCurrentRow,
+ "endOfCurrentRow": endOfCurrentRow,
+ "shiftLeft": shiftLeft,
+ "shiftRight": shiftRight,
+ "enable": enable,
+ "disable": disable,
+ "isVisible": isVisible
+ };
+}
+
+function createSelectionCursor(divElement) {
+ "use strict";
+ var cursor = createCanvas(0, 0);
+ var sx, sy, dx, dy, x, y, width, height;
+ var visible = false;
+
+ function processCoords() {
+ x = Math.min(sx, dx);
+ y = Math.min(sy, dy);
+ x = Math.max(x, 0);
+ y = Math.max(y, 0);
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ width = Math.abs(dx - sx) + 1;
+ height = Math.abs(dy - sy) + 1;
+ width = Math.min(width, columns - x);
+ height = Math.min(height, rows - y);
+ }
+
+ function show() {
+ cursor.style.display = "block";
+ }
+
+ function hide() {
+ cursor.style.display = "none";
+ visible = false;
+ pasteTool.disable();
+ }
+
+ function updateCursor() {
+ var fontWidth = font.getWidth();
+ var fontHeight = font.getHeight();
+ cursor.style.left = x * fontWidth - 1 + "px";
+ cursor.style.top = y * fontHeight - 1 + "px";
+ cursor.width = width * fontWidth + 1;
+ cursor.height = height * fontHeight + 1;
+ }
+
+ function setStart(startX, startY) {
+ sx = startX;
+ sy = startY;
+ processCoords();
+ x = startX;
+ y = startY;
+ width = 1;
+ height = 1;
+ updateCursor();
+ }
+
+ function setEnd(endX, endY) {
+ show();
+ dx = endX;
+ dy = endY;
+ processCoords();
+ updateCursor();
+ pasteTool.setSelection(x, y, width, height);
+ visible = true;
+ }
+
+ function isVisible() {
+ return visible;
+ }
+
+ cursor.classList.add("selection-cursor");
+ cursor.style.display = "none";
+ divElement.appendChild(cursor);
+
+ return {
+ "show": show,
+ "hide": hide,
+ "setStart": setStart,
+ "setEnd": setEnd,
+ "isVisible": isVisible
+ };
+}
+
+function createKeyboardController() {
+ "use strict";
+ var fkeys = createFKeysShortcut();
+ var enabled = false;
+ var ignored = false;
+
+ function draw(charCode) {
+ textArtCanvas.startUndo();
+ textArtCanvas.draw((callback) => {
+ callback(charCode, palette.getForegroundColour(), palette.getBackgroundColour(), cursor.getX(), cursor.getY());
+ }, false);
+ cursor.right();
+ }
+
+ function deleteText() {
+ textArtCanvas.startUndo();
+ textArtCanvas.draw((callback) => {
+ callback(0, 7, 0, cursor.getX() - 1, cursor.getY());
+ }, false);
+ cursor.left();
+ }
+
+ function keyDown(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (ignored === false) {
+ if (evt.altKey === false && evt.ctrlKey === false && evt.metaKey === false) {
+ if (keyCode === 9) {
+ evt.preventDefault();
+ draw(keyCode);
+ } else if (keyCode === 8) {
+ evt.preventDefault();
+ if (cursor.getX() > 0) {
+ deleteText();
+ }
+ }
+ }
+ }
+ }
+
+ function convertUnicode(keyCode) {
+ switch (keyCode) {
+ case 0x2302: return 127;
+ case 0x00C7: return 128;
+ case 0x00FC: return 129;
+ case 0x00E9: return 130;
+ case 0x00E2: return 131;
+ case 0x00E4: return 132;
+ case 0x00E0: return 133;
+ case 0x00E5: return 134;
+ case 0x00E7: return 135;
+ case 0x00EA: return 136;
+ case 0x00EB: return 137;
+ case 0x00E8: return 138;
+ case 0x00EF: return 139;
+ case 0x00EE: return 140;
+ case 0x00EC: return 141;
+ case 0x00C4: return 142;
+ case 0x00C5: return 143;
+ case 0x00C9: return 144;
+ case 0x00E6: return 145;
+ case 0x00C6: return 146;
+ case 0x00F4: return 147;
+ case 0x00F6: return 148;
+ case 0x00F2: return 149;
+ case 0x00FB: return 150;
+ case 0x00F9: return 151;
+ case 0x00FF: return 152;
+ case 0x00D6: return 153;
+ case 0x00DC: return 154;
+ case 0x00A2: return 155;
+ case 0x00A3: return 156;
+ case 0x00A5: return 157;
+ case 0x20A7: return 158;
+ case 0x0192: return 159;
+ case 0x00E1: return 160;
+ case 0x00ED: return 161;
+ case 0x00F3: return 162;
+ case 0x00FA: return 163;
+ case 0x00F1: return 164;
+ case 0x00D1: return 165;
+ case 0x00AA: return 166;
+ case 0x00BA: return 167;
+ case 0x00BF: return 168;
+ case 0x2310: return 169;
+ case 0x00AC: return 170;
+ case 0x00BD: return 171;
+ case 0x00BC: return 172;
+ case 0x00A1: return 173;
+ case 0x00AB: return 174;
+ case 0x00BB: return 175;
+ case 0x2591: return 176;
+ case 0x2592: return 177;
+ case 0x2593: return 178;
+ case 0x2502: return 179;
+ case 0x2524: return 180;
+ case 0x2561: return 181;
+ case 0x2562: return 182;
+ case 0x2556: return 183;
+ case 0x2555: return 184;
+ case 0x2563: return 185;
+ case 0x2551: return 186;
+ case 0x2557: return 187;
+ case 0x255D: return 188;
+ case 0x255C: return 189;
+ case 0x255B: return 190;
+ case 0x2510: return 191;
+ case 0x2514: return 192;
+ case 0x2534: return 193;
+ case 0x252C: return 194;
+ case 0x251C: return 195;
+ case 0x2500: return 196;
+ case 0x253C: return 197;
+ case 0x255E: return 198;
+ case 0x255F: return 199;
+ case 0x255A: return 200;
+ case 0x2554: return 201;
+ case 0x2569: return 202;
+ case 0x2566: return 203;
+ case 0x2560: return 204;
+ case 0x2550: return 205;
+ case 0x256C: return 206;
+ case 0x2567: return 207;
+ case 0x2568: return 208;
+ case 0x2564: return 209;
+ case 0x2565: return 210;
+ case 0x2559: return 211;
+ case 0x2558: return 212;
+ case 0x2552: return 213;
+ case 0x2553: return 214;
+ case 0x256B: return 215;
+ case 0x256A: return 216;
+ case 0x2518: return 217;
+ case 0x250C: return 218;
+ case 0x2588: return 219;
+ case 0x2584: return 220;
+ case 0x258C: return 221;
+ case 0x2590: return 222;
+ case 0x2580: return 223;
+ case 0x03B1: return 224;
+ case 0x00DF: return 225;
+ case 0x0393: return 226;
+ case 0x03C0: return 227;
+ case 0x03A3: return 228;
+ case 0x03C3: return 229;
+ case 0x00B5: return 230;
+ case 0x03C4: return 231;
+ case 0x03A6: return 232;
+ case 0x0398: return 233;
+ case 0x03A9: return 234;
+ case 0x03B4: return 235;
+ case 0x221E: return 236;
+ case 0x03C6: return 237;
+ case 0x03B5: return 238;
+ case 0x2229: return 239;
+ case 0x2261: return 240;
+ case 0x00B1: return 241;
+ case 0x2265: return 242;
+ case 0x2264: return 243;
+ case 0x2320: return 244;
+ case 0x2321: return 245;
+ case 0x00F7: return 246;
+ case 0x2248: return 247;
+ case 0x00B0: return 248;
+ case 0x2219: return 249;
+ case 0x00B7: return 250;
+ case 0x221A: return 251;
+ case 0x207F: return 252;
+ case 0x00B2: return 253;
+ case 0x25A0: return 254;
+ case 0x00A0: return 255;
+ default: return keyCode;
+ }
+ }
+
+ function keyPress(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (ignored === false) {
+ if (evt.altKey === false && evt.ctrlKey === false && evt.metaKey === false) {
+ if (keyCode >= 32) {
+ evt.preventDefault();
+ draw(convertUnicode(keyCode));
+ } else if (keyCode === 13) {
+ evt.preventDefault();
+ cursor.newLine();
+ } else if (keyCode === 8) {
+ evt.preventDefault();
+ if (cursor.getX() > 0) {
+ deleteText();
+ }
+ } else if (keyCode === 167) {
+ evt.preventDefault();
+ draw(21);
+ }
+ } else if (evt.ctrlKey === true) {
+ if (keyCode === 21) {
+ evt.preventDefault();
+ var block = textArtCanvas.getBlock(cursor.getX(), cursor.getY());
+ palette.setForegroundColour(block.foregroundColour);
+ palette.setBackgroundColour(block.backgroundColour);
+ }
+ }
+ }
+ }
+
+ function textCanvasDown(evt) {
+ cursor.move(evt.detail.x, evt.detail.y);
+ selectionCursor.setStart(evt.detail.x, evt.detail.y);
+ }
+
+ function textCanvasDrag(evt) {
+ cursor.hide();
+ selectionCursor.setEnd(evt.detail.x, evt.detail.y);
+ }
+
+ function enable() {
+ document.addEventListener("keydown", keyDown);
+ document.addEventListener("keypress", keyPress);
+ document.addEventListener("onTextCanvasDown", textCanvasDown);
+ document.addEventListener("onTextCanvasDrag", textCanvasDrag);
+ cursor.enable();
+ fkeys.enable();
+ positionInfo.update(cursor.getX(), cursor.getY());
+ enabled = true;
+ }
+
+ function disable() {
+ document.removeEventListener("keydown", keyDown);
+ document.removeEventListener("keypress", keyPress);
+ document.removeEventListener("onTextCanvasDown", textCanvasDown);
+ document.removeEventListener("onTextCanvasDrag", textCanvasDrag);
+ selectionCursor.hide();
+ cursor.disable();
+ fkeys.disable();
+ enabled = false;
+ }
+
+ function ignore() {
+ ignored = true;
+ if (enabled === true) {
+ cursor.disable();
+ fkeys.disable();
+ }
+ }
+
+ function unignore() {
+ ignored = false;
+ if (enabled === true) {
+ cursor.enable();
+ fkeys.enable();
+ }
+ }
+
+ return {
+ "enable": enable,
+ "disable": disable,
+ "ignore": ignore,
+ "unignore": unignore
+ };
+}
+
+function createPasteTool(cutItem, copyItem, pasteItem, deleteItem) {
+ "use strict";
+ var buffer;
+ var x = 0;
+ var y = 0;
+ var width = 0;
+ var height = 0;
+ var enabled = false;
+
+ function setSelection(newX, newY, newWidth, newHeight) {
+ x = newX;
+ y = newY;
+ width = newWidth;
+ height = newHeight;
+ if (buffer !== undefined) {
+ pasteItem.classList.remove("disabled");
+ }
+ cutItem.classList.remove("disabled");
+ copyItem.classList.remove("disabled");
+ deleteItem.classList.remove("disabled");
+ enabled = true;
+ }
+
+ function disable() {
+ pasteItem.classList.add("disabled");
+ cutItem.classList.add("disabled");
+ copyItem.classList.add("disabled");
+ deleteItem.classList.add("disabled");
+ enabled = false;
+ }
+
+ function copy() {
+ buffer = textArtCanvas.getArea(x, y, width, height);
+ pasteItem.classList.remove("disabled");
+ }
+
+ function deleteSelection() {
+ if (selectionCursor.isVisible() || cursor.isVisible()) {
+ textArtCanvas.startUndo();
+ textArtCanvas.deleteArea(x, y, width, height, palette.getBackgroundColour());
+ }
+ }
+
+ function cut() {
+ if (selectionCursor.isVisible() || cursor.isVisible()) {
+ copy();
+ deleteSelection();
+ }
+ }
+
+ function paste() {
+ if (buffer !== undefined && (selectionCursor.isVisible() || cursor.isVisible())) {
+ textArtCanvas.startUndo();
+ textArtCanvas.setArea(buffer, x, y);
+ }
+ }
+
+ function keyDown(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (enabled) {
+ if ((evt.ctrlKey === true || evt.metaKey === true) && evt.altKey === false && evt.shiftKey === false) {
+ switch (keyCode) {
+ case 88:
+ evt.preventDefault();
+ cut();
+ break;
+ case 67:
+ evt.preventDefault();
+ copy();
+ break;
+ case 86:
+ evt.preventDefault();
+ paste();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ if ((evt.ctrlKey === true || evt.metaKey === true) && keyCode === 8) {
+ evt.preventDefault();
+ deleteSelection();
+ }
+ }
+
+
+ document.addEventListener("keydown", keyDown);
+
+ return {
+ "setSelection": setSelection,
+ "cut": cut,
+ "copy": copy,
+ "paste": paste,
+ "deleteSelection": deleteSelection,
+ "disable": disable
+ };
+}
diff --git a/public/js/loaders.js b/public/js/loaders.js
new file mode 100644
index 0000000..94e5264
--- /dev/null
+++ b/public/js/loaders.js
@@ -0,0 +1,678 @@
+var Loaders = (function() {
+ "use strict";
+
+ var Colors;
+
+ Colors = (function() {
+ function rgb2xyz(rgb) {
+ var xyz;
+ xyz = rgb.map(function(value) {
+ value = value / 255;
+ return ((value > 0.04045) ? Math.pow((value + 0.055) / 1.055, 2.4) : value / 12.92) * 100;
+ });
+ return [xyz[0] * 0.4124 + xyz[1] * 0.3576 + xyz[2] * 0.1805, xyz[0] * 0.2126 + xyz[1] * 0.7152 + xyz[2] * 0.0722, xyz[0] * 0.0193 + xyz[1] * 0.1192 + xyz[2] * 0.9505];
+ }
+
+ function xyz2lab(xyz) {
+ var labX, labY, labZ;
+ function process(value) {
+ return (value > 0.008856) ? Math.pow(value, 1 / 3) : (7.787 * value) + (16 / 116);
+ }
+ labX = process(xyz[0] / 95.047);
+ labY = process(xyz[1] / 100);
+ labZ = process(xyz[2] / 108.883);
+ return [116 * labY - 16, 500 * (labX - labY), 200 * (labY - labZ)];
+ }
+
+ function rgb2lab(rgb) {
+ return xyz2lab(rgb2xyz(rgb));
+ }
+
+ function labDeltaE(lab1, lab2) {
+ return Math.sqrt(Math.pow(lab1[0] - lab2[0], 2) + Math.pow(lab1[1] - lab2[1], 2) + Math.pow(lab1[2] - lab2[2], 2));
+ }
+
+ function rgbDeltaE(rgb1, rgb2) {
+ return labDeltaE(rgb2lab(rgb1), rgb2lab(rgb2));
+ }
+
+ function labCompare(lab, palette) {
+ var i, match, value, lowest;
+ for (i = 0; i < palette.length; ++i) {
+ value = labDeltaE(lab, palette[i]);
+ if (i === 0 || value < lowest) {
+ match = i;
+ lowest = value;
+ }
+ }
+ return match;
+ }
+
+ return {
+ "rgb2xyz": rgb2xyz,
+ "xyz2lab": xyz2lab,
+ "rgb2lab": rgb2lab,
+ "labDeltaE": labDeltaE,
+ "rgbDeltaE": rgbDeltaE,
+ "labCompare": labCompare
+ };
+ }());
+
+ function srcToImageData(src, callback) {
+ var img;
+ img = new Image();
+ img.onload = function() {
+ var imgCanvas, imgCtx, imgImageData;
+ imgCanvas = ElementHelper.create("canvas", { "width": img.width, "height": img.height });
+ imgCtx = imgCanvas.getContext("2d");
+ imgCtx.drawImage(img, 0, 0);
+ imgImageData = imgCtx.getImageData(0, 0, imgCanvas.width, imgCanvas.height);
+ callback(imgImageData);
+ };
+ img.src = src;
+ }
+
+ function rgbaAt(imageData, x, y) {
+ var pos;
+ pos = (y * imageData.width + x) * 4;
+ if (pos >= imageData.length) {
+ return [0, 0, 0, 255];
+ }
+ return [imageData.data[pos], imageData.data[pos + 1], imageData.data[pos + 2], imageData.data[pos + 3]];
+ }
+
+ function loadImg(src, callback, palette, codepage, noblink) {
+ srcToImageData(src, function(imageData) {
+ var imgX, imgY, i, paletteLab, topRGBA, botRGBA, topPal, botPal, data;
+
+ for (paletteLab = [], i = 0; i < palette.COLORS.length; ++i) {
+ paletteLab[i] = Colors.rgb2lab([palette.COLORS[i][0], palette.COLORS[i][1], palette.COLORS[i][2]]);
+ }
+
+ data = new Uint8Array(Math.ceil(imageData.height / 2) * imageData.width * 3);
+
+ for (imgY = 0, i = 0; imgY < imageData.height; imgY += 2) {
+ for (imgX = 0; imgX < imageData.width; imgX += 1) {
+ topRGBA = rgbaAt(imageData, imgX, imgY);
+ botRGBA = rgbaAt(imageData, imgX, imgY + 1);
+ if (topRGBA[3] === 0 && botRGBA[3] === 0) {
+ data[i++] = codepage.NULL;
+ data[i++] = 0;
+ data[i++] = 0;
+ } else {
+ topPal = Colors.labCompare(Colors.rgb2lab(topRGBA), paletteLab);
+ botPal = Colors.labCompare(Colors.rgb2lab(botRGBA), paletteLab);
+ if (topPal === botPal) {
+ data[i++] = codepage.FULL_BLOCK;
+ data[i++] = topPal;
+ data[i++] = 0;
+ } else if (topPal < 8 && botPal >= 8) {
+ data[i++] = codepage.LOWER_HALF_BLOCK;
+ data[i++] = botPal;
+ data[i++] = topPal;
+ } else if ((topPal >= 8 && botPal < 8) || (topPal < 8 && botPal < 8)) {
+ data[i++] = codepage.UPPER_HALF_BLOCK;
+ data[i++] = topPal;
+ data[i++] = botPal;
+ } else if (topRGBA[3] === 0) {
+ data[i++] = codepage.LOWER_HALF_BLOCK;
+ data[i++] = botPal;
+ if (noblink) {
+ data[i++] = topPal;
+ } else {
+ data[i++] = topPal - 8;
+ }
+ } else {
+ data[i++] = codepage.UPPER_HALF_BLOCK;
+ data[i++] = topPal;
+ if (noblink) {
+ data[i++] = botPal;
+ } else {
+ data[i++] = botPal - 8;
+ }
+ }
+ }
+ }
+ }
+ callback({
+ "width": imageData.width,
+ "height": Math.ceil(imageData.height / 2),
+ "data": data,
+ "alpha": true
+ });
+ });
+ }
+
+ function File(bytes) {
+ var pos, SAUCE_ID, COMNT_ID, commentCount;
+
+ SAUCE_ID = new Uint8Array([0x53, 0x41, 0x55, 0x43, 0x45]);
+ COMNT_ID = new Uint8Array([0x43, 0x4F, 0x4D, 0x4E, 0x54]);
+
+ // Returns an 8-bit byte at the current byte position, . Also advances by a single byte. Throws an error if we advance beyond the length of the array.
+ this.get = function() {
+ if (pos >= bytes.length) {
+ throw "Unexpected end of file reached.";
+ }
+ return bytes[pos++];
+ };
+
+ // Same as get(), but returns a 16-bit byte. Also advances by two (8-bit) bytes.
+ this.get16 = function() {
+ var v;
+ v = this.get();
+ return v + (this.get() << 8);
+ };
+
+ // Same as get(), but returns a 32-bit byte. Also advances by four (8-bit) bytes.
+ this.get32 = function() {
+ var v;
+ v = this.get();
+ v += this.get() << 8;
+ v += this.get() << 16;
+ return v + (this.get() << 24);
+ };
+
+ // Exactly the same as get(), but returns a character symbol, instead of the value. e.g. 65 = "A".
+ this.getC = function() {
+ return String.fromCharCode(this.get());
+ };
+
+ // Returns a string of characters at the current file position, and strips the trailing whitespace characters. Advances by by calling getC().
+ this.getS = function(num) {
+ var string;
+ string = "";
+ while (num-- > 0) {
+ string += this.getC();
+ }
+ return string.replace(/\s+$/, '');
+ };
+
+ // Returns "true" if, at the current , a string of characters matches . Does not increment .
+ this.lookahead = function(match) {
+ var i;
+ for (i = 0; i < match.length; ++i) {
+ if ((pos + i === bytes.length) || (bytes[pos + i] !== match[i])) {
+ break;
+ }
+ }
+ return i === match.length;
+ };
+
+ // Returns an array of bytes found at the current . Also increments .
+ this.read = function(num) {
+ var t;
+ t = pos;
+ // If num is undefined, return all the bytes until the end of file.
+ num = num || this.size - pos;
+ while (++pos < this.size) {
+ if (--num === 0) {
+ break;
+ }
+ }
+ return bytes.subarray(t, pos);
+ };
+
+ // Sets a new value for . Equivalent to seeking a file to a new position.
+ this.seek = function(newPos) {
+ pos = newPos;
+ };
+
+ // Returns the value found at , without incrementing .
+ this.peek = function(num) {
+ num = num || 0;
+ return bytes[pos + num];
+ };
+
+ // Returns the the current position being read in the file, in amount of bytes. i.e. .
+ this.getPos = function() {
+ return pos;
+ };
+
+ // Returns true if the end of file has been reached. is set later by the SAUCE parsing section, as it is not always the same value as the length of . (In case there is a SAUCE record, and optional comments).
+ this.eof = function() {
+ return pos === this.size;
+ };
+
+ // Seek to the position we would expect to find a SAUCE record.
+ pos = bytes.length - 128;
+ // If we find "SAUCE".
+ if (this.lookahead(SAUCE_ID)) {
+ this.sauce = {};
+ // Read "SAUCE".
+ this.getS(5);
+ // Read and store the various SAUCE values.
+ this.sauce.version = this.getS(2); // String, maximum of 2 characters
+ this.sauce.title = this.getS(35); // String, maximum of 35 characters
+ this.sauce.author = this.getS(20); // String, maximum of 20 characters
+ this.sauce.group = this.getS(20); // String, maximum of 20 characters
+ this.sauce.date = this.getS(8); // String, maximum of 8 characters
+ this.sauce.fileSize = this.get32(); // unsigned 32-bit
+ this.sauce.dataType = this.get(); // unsigned 8-bit
+ this.sauce.fileType = this.get(); // unsigned 8-bit
+ this.sauce.tInfo1 = this.get16(); // unsigned 16-bit
+ this.sauce.tInfo2 = this.get16(); // unsigned 16-bit
+ this.sauce.tInfo3 = this.get16(); // unsigned 16-bit
+ this.sauce.tInfo4 = this.get16(); // unsigned 16-bit
+ // Initialize the comments array.
+ this.sauce.comments = [];
+ commentCount = this.get(); // unsigned 8-bit
+ this.sauce.flags = this.get(); // unsigned 8-bit
+ if (commentCount > 0) {
+ // If we have a value for the comments amount, seek to the position we'd expect to find them...
+ pos = bytes.length - 128 - (commentCount * 64) - 5;
+ // ... and check that we find a COMNT header.
+ if (this.lookahead(COMNT_ID)) {
+ // Read COMNT ...
+ this.getS(5);
+ // ... and push everything we find after that into our array, in 64-byte chunks, stripping the trailing whitespace in the getS() function.
+ while (commentCount-- > 0) {
+ this.sauce.comments.push(this.getS(64));
+ }
+ }
+ }
+ }
+ // Seek back to the start of the file, ready for reading.
+ pos = 0;
+
+ if (this.sauce) {
+ // If we have found a SAUCE record, and the fileSize field passes some basic sanity checks...
+ if (this.sauce.fileSize > 0 && this.sauce.fileSize < bytes.length) {
+ // Set to the value set in SAUCE.
+ this.size = this.sauce.fileSize;
+ } else {
+ // If it fails the sanity checks, just assume that SAUCE record can't be trusted, and set to the position where the SAUCE record begins.
+ this.size = bytes.length - 128;
+ }
+ } else {
+ // If there is no SAUCE record, assume that everything in relates to an image.
+ this.size = bytes.length;
+ }
+ }
+
+ function ScreenData(width) {
+ var imageData, maxY, pos;
+
+ function binColor(ansiColor) {
+ switch (ansiColor) {
+ case 4:
+ return 1;
+ case 6:
+ return 3;
+ case 1:
+ return 4;
+ case 3:
+ return 6;
+ case 12:
+ return 9;
+ case 14:
+ return 11;
+ case 9:
+ return 12;
+ case 11:
+ return 14;
+ default:
+ return ansiColor;
+ }
+ }
+
+ this.reset = function() {
+ imageData = new Uint8Array(width * 100 * 3);
+ maxY = 0;
+ pos = 0;
+ };
+
+ this.reset();
+
+ function extendImageData(y) {
+ var newImageData;
+ newImageData = new Uint8Array(width * (y + 100) * 3 + imageData.length);
+ newImageData.set(imageData, 0);
+ imageData = newImageData;
+ }
+
+ this.set = function(x, y, charCode, fg, bg) {
+ pos = (y * width + x) * 3;
+ if (pos >= imageData.length) {
+ extendImageData(y);
+ }
+ imageData[pos] = charCode;
+ imageData[pos + 1] = binColor(fg);
+ imageData[pos + 2] = binColor(bg);
+ if (y > maxY) {
+ maxY = y;
+ }
+ };
+
+ this.getData = function() {
+ return imageData.subarray(0, width * (maxY + 1) * 3);
+ };
+
+ this.getHeight = function() {
+ return maxY + 1;
+ };
+
+ this.rowLength = width * 3;
+ }
+
+ function loadAnsi(bytes, icecolors) {
+ var file, escaped, escapeCode, j, code, values, columns, imageData, topOfScreen, x, y, savedX, savedY, foreground, background, bold, blink, inverse;
+
+ file = new File(bytes);
+
+ function resetAttributes() {
+ foreground = 7;
+ background = 0;
+ bold = false;
+ blink = false;
+ inverse = false;
+ }
+ resetAttributes();
+
+ function newLine() {
+ x = 1;
+ if (y === 26 - 1) {
+ ++topOfScreen;
+ } else {
+ ++y;
+ }
+ }
+
+ function setPos(newX, newY) {
+ x = Math.min(columns, Math.max(1, newX));
+ y = Math.min(26, Math.max(1, newY));
+ }
+
+ x = 1;
+ y = 1;
+ topOfScreen = 0;
+
+ escapeCode = "";
+ escaped = false;
+
+ columns = 80;
+
+ imageData = new ScreenData(columns);
+
+ function getValues() {
+ return escapeCode.substr(1, escapeCode.length - 2).split(";").map(function(value) {
+ var parsedValue;
+ parsedValue = parseInt(value, 10);
+ return isNaN(parsedValue) ? 1 : parsedValue;
+ });
+ }
+
+ while (!file.eof()) {
+ code = file.get();
+ if (escaped) {
+ escapeCode += String.fromCharCode(code);
+ if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) {
+ escaped = false;
+ values = getValues();
+ if (escapeCode.charAt(0) === "[") {
+ switch (escapeCode.charAt(escapeCode.length - 1)) {
+ case "A": // Up cursor.
+ y = Math.max(1, y - values[0]);
+ break;
+ case "B": // Down cursor.
+ y = Math.min(26 - 1, y + values[0]);
+ break;
+ case "C": // Forward cursor.
+ if (x === columns) {
+ newLine();
+ }
+ x = Math.min(columns, x + values[0]);
+ break;
+ case "D": // Backward cursor.
+ x = Math.max(1, x - values[0]);
+ break;
+ case "H": // Set the cursor position by calling setPos(), first , then .
+ if (values.length === 1) {
+ setPos(1, values[0]);
+ } else {
+ setPos(values[1], values[0]);
+ }
+ break;
+ case "J": // Clear screen.
+ if (values[0] === 2) {
+ x = 1;
+ y = 1;
+ imageData.reset();
+ }
+ break;
+ case "K": // Clear until the end of line.
+ for (j = x - 1; j < columns; ++j) {
+ imageData.set(j, y - 1 + topOfScreen, 0, 0);
+ }
+ break;
+ case "m": // Attributes, work through each code in turn.
+ for (j = 0; j < values.length; ++j) {
+ if (values[j] >= 30 && values[j] <= 37) {
+ foreground = values[j] - 30;
+ } else if (values[j] >= 40 && values[j] <= 47) {
+ background = values[j] - 40;
+ } else {
+ switch (values[j]) {
+ case 0: // Reset attributes
+ resetAttributes();
+ break;
+ case 1: // Bold
+ bold = true;
+ break;
+ case 5: // Blink
+ blink = true;
+ break;
+ case 7: // Inverse
+ inverse = true;
+ break;
+ case 22: // Bold off
+ bold = false;
+ break;
+ case 25: // Blink off
+ blink = false;
+ break;
+ case 27: // Inverse off
+ inverse = false;
+ break;
+ }
+ }
+ }
+ break;
+ case "s": // Save the current and positions.
+ savedX = x;
+ savedY = y;
+ break;
+ case "u": // Restore the current and positions.
+ x = savedX;
+ y = savedY;
+ break;
+ }
+ }
+ escapeCode = "";
+ }
+ } else {
+ switch (code) {
+ case 10: // Lone linefeed (LF).
+ newLine();
+ break;
+ case 13: // Carriage Return, and Linefeed (CRLF)
+ if (file.peek() === 0x0A) {
+ file.read(1);
+ newLine();
+ }
+ break;
+ case 26: // Ignore eof characters until the actual end-of-file, or sauce record has been reached.
+ break;
+ default:
+ if (code === 27 && file.peek() === 0x5B) {
+ escaped = true;
+ } else {
+ if (!inverse) {
+ imageData.set(x - 1, y - 1 + topOfScreen, code, bold ? (foreground + 8) : foreground, (icecolors && blink) ? (background + 8) : background);
+ } else {
+ imageData.set(x - 1, y - 1 + topOfScreen, code, bold ? (background + 8) : background, (icecolors && blink) ? (foreground + 8) : foreground);
+ }
+ if (++x === columns + 1) {
+ newLine();
+ }
+ }
+ }
+ }
+ }
+
+ return {
+ "width": columns,
+ "height": imageData.getHeight(),
+ "data": imageData.getData()
+ };
+ }
+
+ // A function to parse a sequence of bytes representing an XBiN file format.
+ function loadXbin(bytes) {
+ var file, header, imageData, output, i, j;
+
+ // This function is called to parse the XBin header.
+ function XBinHeader(file) {
+ var flags;
+
+ // Look for the magic number, throw an error if not found.
+ if (file.getS(4) !== "XBIN") {
+ throw "File ID does not match.";
+ }
+ if (file.get() !== 26) {
+ throw "File ID does not match.";
+ }
+
+ // Get the dimensions of the image...
+ this.width = file.get16();
+ this.height = file.get16();
+
+ // ... and the height of the font, if included.
+ this.fontHeight = file.get();
+
+ // Sanity check for the font height, throw an error if failed.
+ if (this.fontHeight === 0 || this.fontHeight > 32) {
+ throw "Illegal value for the font height (" + this.fontHeight + ").";
+ }
+
+ // Retrieve the flags.
+ flags = file.get();
+
+ // Check to see if a palette and font is included.
+ this.palette = ((flags & 1) === 1);
+ this.font = ((flags & 2) === 2);
+
+ // Sanity check for conflicting information in font settings.
+ if (this.fontHeight !== 16 && !this.font) {
+ throw "A non-standard font size was defined, but no font information was included with the file.";
+ }
+
+ // Check to see if the image data is , if non-blink mode is set, , and if 512 characters are included with the font data. .
+ this.compressed = ((flags & 4) === 4);
+ this.nonBlink = ((flags & 8) === 8);
+ this.char512 = ((flags & 16) === 16);
+ }
+
+ // Routine to decompress data found in an XBin , which contains a Run-Length encoding scheme. Needs to know the current and of the image.
+ function uncompress(file, width, height) {
+ var uncompressed, p, repeatAttr, repeatChar, count;
+ // Initialize the data used to store the image, each text character has two bytes, one for the character code, and the other for the attribute.
+ uncompressed = new Uint8Array(width * height * 2);
+ i = 0;
+ while (i < uncompressed.length) {
+ p = file.get(); // , the current code under inspection.
+ count = p & 63; // , the times data is repeated
+ switch (p >> 6) { // Look at which RLE scheme to use
+ case 1: // Handle repeated character code.
+ for (repeatChar = file.get(), j = 0; j <= count; ++j) {
+ uncompressed[i++] = repeatChar;
+ uncompressed[i++] = file.get();
+ }
+ break;
+ case 2: // Handle repeated attributes.
+ for (repeatAttr = file.get(), j = 0; j <= count; ++j) {
+ uncompressed[i++] = file.get();
+ uncompressed[i++] = repeatAttr;
+ }
+ break;
+ case 3: // Handle repeated character code and attributes.
+ for (repeatChar = file.get(), repeatAttr = file.get(), j = 0; j <= count; ++j) {
+ uncompressed[i++] = repeatChar;
+ uncompressed[i++] = repeatAttr;
+ }
+ break;
+ default: // Handle no RLE.
+ for (j = 0; j <= count; ++j) {
+ uncompressed[i++] = file.get();
+ uncompressed[i++] = file.get();
+ }
+ }
+ }
+ return uncompressed; // Return the final, data.
+ }
+
+ // Convert the bytes to a File() object, and reader the settings in the header, by calling XBinHeader().
+ file = new File(bytes);
+ header = new XBinHeader(file);
+
+ // If palette information is included, read it immediately after the header, if not, use the default palette used for BIN files.
+ if (header.palette) {
+ file.read(48);
+ }
+ // If font information is included, read it, if not, use the default 80x25 font.
+ if (header.font) {
+ file.read(header.fontHeight * 256);
+ }
+ // Fetch the image data, and uncompress if necessary.
+ imageData = header.compressed ? uncompress(file, header.width, header.height) : file.read(header.width * header.height * 2);
+
+ output = new Uint8Array(imageData.length / 2 * 3);
+
+ for (i = 0, j = 0; i < imageData.length; i += 2, j += 3) {
+ output[j] = imageData[i];
+ output[j + 1] = imageData[i + 1] & 15;
+ output[j + 2] = imageData[i + 1] >> 4;
+ }
+
+ return {
+ "width": header.width,
+ "height": header.height,
+ "data": output
+ };
+ }
+
+ function loadFile(file, callback, palette, codepage, noblink) {
+ var extension, reader;
+ extension = file.name.split(".").pop().toLowerCase();
+ reader = new FileReader();
+ reader.onload = function(data) {
+ switch (extension) {
+ case "png":
+ case "gif":
+ case "jpg":
+ case "jpeg":
+ loadImg(data.target.result, callback, palette, codepage, noblink);
+ break;
+ case "xb":
+ callback(loadXbin(new Uint8Array(data.target.result)));
+ break;
+ default:
+ callback(loadAnsi(new Uint8Array(data.target.result)));
+ }
+ };
+ switch (extension) {
+ case "png":
+ case "gif":
+ case "jpg":
+ case "jpeg":
+ reader.readAsDataURL(file);
+ break;
+ default:
+ reader.readAsArrayBuffer(file);
+ }
+ }
+
+ return {
+ "loadFile": loadFile
+ };
+}());
diff --git a/public/js/network.js b/public/js/network.js
new file mode 100644
index 0000000..3e64bc3
--- /dev/null
+++ b/public/js/network.js
@@ -0,0 +1,303 @@
+function createWorkerHandler(inputHandle) {
+ "use strict";
+ var worker = new Worker("js/worker.js");
+ var handle = localStorage.getItem("handle");
+ if (handle === null) {
+ handle = "Anonymous";
+ localStorage.setItem("handle", handle);
+ }
+ inputHandle.value = handle;
+ var connected = false;
+ worker.postMessage({ "cmd": "handle", "handle": handle });
+ showOverlay($("websocket-overlay"));
+
+ function onConnected() {
+ var excludedElements = document.getElementsByClassName("excluded-for-websocket");
+ for (var i = 0; i < excludedElements.length; i++) {
+ excludedElements[i].style.display = "none";
+ }
+ var includedElement = document.getElementsByClassName("included-for-websocket");
+ for (var i = 0; i < includedElement.length; i++) {
+ includedElement[i].style.display = "block";
+ }
+ title.setName(window.location.hostname);
+ worker.postMessage({ "cmd": "join", "handle": handle });
+ connected = true;
+ }
+
+ function onDisconnected() {
+ if (connected === true) {
+ alert("You were disconnected from the server, try refreshing the page to try again.");
+ } else {
+ hideOverlay($("websocket-overlay"));
+ }
+ connected = false;
+ }
+
+ function onImageData(columns, rows, data, iceColours, letterSpacing) {
+ textArtCanvas.setImageData(columns, rows, data, iceColours, letterSpacing);
+ hideOverlay($("websocket-overlay"));
+ }
+
+ function onChat(handle, text, showNotification) {
+ chat.addConversation(handle, text, showNotification);
+ }
+
+ function onJoin(handle, sessionID, showNotification) {
+ chat.join(handle, sessionID, showNotification);
+ }
+
+ function onPart(sessionID) {
+ chat.part(sessionID);
+ }
+
+ function onNick(handle, sessionID, showNotification) {
+ chat.nick(handle, sessionID, showNotification);
+ }
+
+ function onDraw(blocks) {
+ textArtCanvas.quickDraw(blocks);
+ }
+
+ function onMessage(msg) {
+ var data = msg.data;
+ switch (data.cmd) {
+ case "connected":
+ onConnected();
+ break;
+ case "disconnected":
+ onDisconnected();
+ break;
+ case "imageData":
+ onImageData(data.columns, data.rows, new Uint16Array(data.data), data.iceColours, data.letterSpacing);
+ break;
+ case "chat":
+ onChat(data.handle, data.text, data.showNotification);
+ break;
+ case "join":
+ onJoin(data.handle, data.sessionID, data.showNotification);
+ break;
+ case "part":
+ onPart(data.sessionID);
+ break;
+ case "nick":
+ onNick(data.handle, data.sessionID, data.showNotification);
+ break;
+ case "draw":
+ onDraw(data.blocks);
+ break;
+ }
+ }
+
+ function draw(blocks) {
+ worker.postMessage({ "cmd": "draw", "blocks": blocks });
+ }
+
+ function setHandle(newHandle) {
+ if (handle !== newHandle) {
+ handle = newHandle;
+ localStorage.setItem("handle", handle);
+ worker.postMessage({ "cmd": "nick", "handle": handle });
+ }
+ }
+
+ function sendChat(text) {
+ worker.postMessage({ "cmd": "chat", "text": text });
+ }
+
+ function isConnected() {
+ return connected;
+ }
+
+ worker.addEventListener("message", onMessage);
+ worker.postMessage({ "cmd": "connect", "url": "wss://" + window.location.hostname + ":1337" + window.location.pathname });
+
+ return {
+ "draw": draw,
+ "setHandle": setHandle,
+ "sendChat": sendChat,
+ "isConnected": isConnected
+ };
+}
+
+function createChatController(divChatButton, divChatWindow, divMessageWindow, divUserList, inputHandle, inputMessage, inputNotificationCheckbox, onFocusCallback, onBlurCallback) {
+ "use strict";
+ var enabled = false;
+ var userList = {};
+ var notifications = localStorage.getItem("notifications");
+ if (notifications === null) {
+ notifications = false;
+ localStorage.setItem("notifications", notifications);
+ } else {
+ notifications = JSON.parse(notifications);
+ }
+ inputNotificationCheckbox.checked = notifications;
+
+ function scrollToBottom() {
+ var rect = divMessageWindow.getBoundingClientRect();
+ divMessageWindow.scrollTop = divMessageWindow.scrollHeight - rect.height;
+ }
+
+ function newNotification(text) {
+ var notification = new Notification(title.getName() + " - ANSiEdit", {
+ "body": text,
+ "icon": "../images/face.png"
+ });
+ setTimeout(() => {
+ notification.close();
+ }, 7000);
+ }
+
+ function addConversation(handle, text, showNotification) {
+ var div = document.createElement("DIV");
+ var spanHandle = document.createElement("SPAN");
+ var spanSeperator = document.createElement("SPAN");
+ var spanText = document.createElement("SPAN");
+ spanHandle.textContent = handle;
+ spanHandle.classList.add("handle");
+ spanSeperator.textContent = " ";
+ spanText.textContent = text;
+ div.appendChild(spanHandle);
+ div.appendChild(spanSeperator);
+ div.appendChild(spanText);
+ var rect = divMessageWindow.getBoundingClientRect();
+ var doScroll = (rect.height > divMessageWindow.scrollHeight) || (divMessageWindow.scrollTop === divMessageWindow.scrollHeight - rect.height);
+ divMessageWindow.appendChild(div);
+ if (doScroll) {
+ scrollToBottom();
+ }
+ if (showNotification === true && enabled === false && divChatButton.classList.contains("notification") === false) {
+ divChatButton.classList.add("notification");
+ }
+ }
+
+ function onFocus() {
+ onFocusCallback();
+ }
+
+ function onBlur() {
+ onBlurCallback();
+ }
+
+ function blurHandle(evt) {
+ if (inputHandle.value === "") {
+ inputHandle.value = "Anonymous";
+ }
+ worker.setHandle(inputHandle.value);
+ }
+
+ function keypressHandle(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 13) {
+ inputMessage.focus();
+ }
+ }
+
+ function keypressMessage(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 13) {
+ if (inputMessage.value !== "") {
+ var text = inputMessage.value;
+ inputMessage.value = "";
+ worker.sendChat(text);
+ }
+ }
+ }
+
+ inputHandle.addEventListener("focus", onFocus);
+ inputHandle.addEventListener("blur", onBlur);
+ inputMessage.addEventListener("focus", onFocus);
+ inputMessage.addEventListener("blur", onBlur);
+ inputHandle.addEventListener("blur", blurHandle);
+ inputHandle.addEventListener("keypress", keypressHandle);
+ inputMessage.addEventListener("keypress", keypressMessage);
+
+ function toggle() {
+ if (enabled === true) {
+ divChatWindow.style.display = "none";
+ enabled = false;
+ onBlurCallback();
+ divChatButton.classList.remove("active");
+ } else {
+ divChatWindow.style.display = "block";
+ enabled = true;
+ scrollToBottom();
+ onFocusCallback();
+ inputMessage.focus();
+ divChatButton.classList.remove("notification");
+ divChatButton.classList.add("active");
+ }
+ }
+
+ function isEnabled() {
+ return enabled;
+ }
+
+ function join(handle, sessionID, showNotification) {
+ if (userList[sessionID] === undefined) {
+ if (notifications === true && showNotification === true) {
+ newNotification(handle + " has joined");
+ }
+ userList[sessionID] = { "handle": handle, "div": document.createElement("DIV") };
+ userList[sessionID].div.classList.add("user-name");
+ userList[sessionID].div.textContent = handle;
+ divUserList.appendChild(userList[sessionID].div);
+ }
+ }
+
+ function nick(handle, sessionID, showNotification) {
+ if (userList[sessionID] !== undefined) {
+ if (showNotification === true && notifications === true) {
+ newNotification(userList[sessionID].handle + " has changed their name to " + handle);
+ }
+ userList[sessionID].handle = handle;
+ userList[sessionID].div.textContent = handle;
+ }
+ }
+
+ function part(sessionID) {
+ if (userList[sessionID] !== undefined) {
+ if (notifications === true) {
+ newNotification(userList[sessionID].handle + " has left");
+ }
+ divUserList.removeChild(userList[sessionID].div);
+ delete userList[sessionID];
+ }
+ }
+
+ function globalToggleKeydown(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 27) {
+ toggle();
+ }
+ }
+
+ function notificationCheckboxClicked(evt) {
+ if (inputNotificationCheckbox.checked) {
+ if (Notification.permission !== "granted") {
+ Notification.requestPermission((permission) => {
+ notifications = true;
+ localStorage.setItem("notifications", notifications);
+ });
+ } else {
+ notifications = true;
+ localStorage.setItem("notifications", notifications);
+ }
+ } else {
+ notifications = false;
+ localStorage.setItem("notifications", notifications);
+ }
+ }
+
+ document.addEventListener("keydown", globalToggleKeydown);
+ inputNotificationCheckbox.addEventListener("click", notificationCheckboxClicked);
+
+ return {
+ "addConversation": addConversation,
+ "toggle": toggle,
+ "isEnabled": isEnabled,
+ "join": join,
+ "nick": nick,
+ "part": part
+ };
+}
diff --git a/public/js/savers.js b/public/js/savers.js
new file mode 100644
index 0000000..6194c8b
--- /dev/null
+++ b/public/js/savers.js
@@ -0,0 +1,95 @@
+var Savers = (function() {
+ "use strict";
+
+ // function toANSFormat(input) {
+ // var highest, inputIndex, end, charCode, fg, bg, bold, blink, currentFg, currentBg, currentBold, currentBlink, attribs, attribIndex, output;
+ //
+ // function ansiColor(binColor) {
+ // switch (binColor) {
+ // case 1:
+ // return 4;
+ // case 3:
+ // return 6;
+ // case 4:
+ // return 1;
+ // case 6:
+ // return 3;
+ // default:
+ // return binColor;
+ // }
+ // }
+ //
+ // highest = getHighestRow(input);
+ // output = [27, 91, 48, 109];
+ // for (inputIndex = 0, end = highest * 80 * 3, currentFg = 7, currentBg = 0, currentBold = false, currentBlink = false; inputIndex < end; inputIndex += 3) {
+ // attribs = [];
+ // charCode = input[inputIndex];
+ // fg = input[inputIndex + 1];
+ // bg = input[inputIndex + 2];
+ // if (fg > 7) {
+ // bold = true;
+ // fg = fg - 8;
+ // } else {
+ // bold = false;
+ // }
+ // if (bg > 7) {
+ // blink = true;
+ // bg = bg - 8;
+ // } else {
+ // blink = false;
+ // }
+ // if ((currentBold && !bold) || (currentBlink && !blink)) {
+ // attribs.push([48]);
+ // currentFg = 7;
+ // currentBg = 0;
+ // currentBold = false;
+ // currentBlink = false;
+ // }
+ // if (bold && !currentBold) {
+ // attribs.push([49]);
+ // currentBold = true;
+ // }
+ // if (blink && !currentBlink) {
+ // attribs.push([53]);
+ // currentBlink = true;
+ // }
+ // if (fg !== currentFg) {
+ // attribs.push([51, 48 + ansiColor(fg)]);
+ // currentFg = fg;
+ // }
+ // if (bg !== currentBg) {
+ // attribs.push([52, 48 + ansiColor(bg)]);
+ // currentBg = bg;
+ // }
+ // if (attribs.length) {
+ // output.push(27, 91);
+ // for (attribIndex = 0; attribIndex < attribs.length; ++attribIndex) {
+ // output = output.concat(attribs[attribIndex]);
+ // if (attribIndex !== attribs.length - 1) {
+ // output.push(59);
+ // } else {
+ // output.push(109);
+ // }
+ // }
+ // }
+ // output.push(charCode);
+ // }
+ // return new Uint8Array(output);
+ // }
+
+ function imageDataToDataURL(imageData, noblink) {
+ var bytes, i, j, flags;
+ bytes = new Uint8Array((imageData.width * imageData.height * 2) + 11);
+ flags = noblink ? 8 : 0;
+ bytes.set(new Uint8Array([88, 66, 73, 78, 26, (imageData.width & 0xff), (imageData.width >> 8), (imageData.height & 0xff), (imageData.height >> 8), 16, flags]), 0);
+ for (i = 0, j = 11; i < imageData.data.length; i += 3, j += 2) {
+ bytes[j] = imageData.data[i];
+ bytes[j + 1] = imageData.data[i + 1] + (imageData.data[i + 2] << 4);
+ }
+ return "data:image/x-bin;base64," + btoa(String.fromCharCode.apply(null, bytes));
+ }
+
+ return {
+ "imageDataToDataURL": imageDataToDataURL
+ };
+}());
diff --git a/public/js/ui.js b/public/js/ui.js
new file mode 100644
index 0000000..85e8f96
--- /dev/null
+++ b/public/js/ui.js
@@ -0,0 +1,462 @@
+function createSettingToggle(divButton, getter, setter) {
+ "use strict";
+ var currentSetting;
+
+ function update() {
+ currentSetting = getter();
+ if (currentSetting === true) {
+ divButton.classList.add("enabled");
+ } else {
+ divButton.classList.remove("enabled");
+ }
+ }
+
+ function changeSetting(evt) {
+ evt.preventDefault();
+ currentSetting = !currentSetting;
+ setter(currentSetting);
+ update();
+ }
+
+ divButton.addEventListener("click", changeSetting);
+ update();
+
+ return {
+ "update": update
+ };
+}
+
+var Toolbar = (function() {
+ "use strict";
+ var currentButton;
+ var currentOnBlur;
+
+ function add(divButton, onFocus, onBlur) {
+ function enable() {
+ if (currentButton !== divButton) {
+ if (currentButton !== undefined) {
+ currentButton.classList.remove("toolbar-displayed");
+ }
+ if (currentOnBlur !== undefined) {
+ currentOnBlur();
+ }
+ divButton.classList.add("toolbar-displayed");
+ currentButton = divButton;
+ currentOnBlur = onBlur;
+ if (onFocus !== undefined) {
+ onFocus();
+ }
+ }
+ }
+ divButton.addEventListener("click", (evt) => {
+ evt.preventDefault();
+ enable();
+ });
+ return {
+ "enable": enable
+ };
+ }
+
+ return {
+ "add": add
+ };
+}());
+
+function onReturn(divElement, divTarget) {
+ "use strict";
+ divElement.addEventListener("keypress", (evt) => {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.altKey === false && evt.ctrlKey === false && evt.metaKey === false && keyCode === 13) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ divTarget.click();
+ }
+ });
+}
+
+function onClick(divElement, func) {
+ "use strict";
+ divElement.addEventListener("click", (evt) => {
+ evt.preventDefault();
+ func(divElement);
+ });
+}
+
+function onFileChange(divElement, func) {
+ "use strict";
+ divElement.addEventListener("change", (evt) => {
+ if (evt.target.files.length > 0) {
+ func(evt.target.files[0]);
+ }
+ });
+}
+
+function onSelectChange(divElement, func) {
+ "use strict";
+ divElement.addEventListener("change", (evt) => {
+ func(divElement.value);
+ });
+}
+
+function createPositionInfo(divElement) {
+ "use strict";
+ function update(x, y) {
+ divElement.textContent = (x + 1) + ", " + (y + 1);
+ }
+
+ return {
+ "update": update
+ };
+}
+
+function showOverlay(divElement) {
+ "use strict";
+ divElement.classList.add("enabled");
+}
+
+function hideOverlay(divElement) {
+ "use strict";
+ divElement.classList.remove("enabled");
+}
+
+function undoAndRedo(evt) {
+ "use strict";
+ var keyCode = (evt.keyCode || evt.which);
+ if ((evt.ctrlKey === true || (evt.metaKey === true && evt.shiftKey === false)) && keyCode === 90) {
+ evt.preventDefault();
+ textArtCanvas.undo();
+ } else if ((evt.ctrlKey === true && evt.keyCode === 89) || (evt.metaKey === true && evt.shiftKey === true && keyCode === 90)) {
+ evt.preventDefault();
+ textArtCanvas.redo();
+ }
+}
+
+function createTitleHandler(inputElement, onFocusCallback, onBlurCallback) {
+ "use strict";
+ function updateTitle() {
+ document.title = inputElement.value + " - moebius";
+ }
+
+ function onFocus() {
+ onFocusCallback();
+ }
+
+ function onBlur() {
+ onBlurCallback();
+ updateTitle();
+ }
+
+ function keyPress(evt) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (keyCode === 13) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ if (inputElement.value === "") {
+ inputElement.value = "untitled";
+ }
+ inputElement.blur();
+ }
+ }
+
+ function setName(newName) {
+ inputElement.value = newName;
+ updateTitle();
+ }
+
+ function getName() {
+ return inputElement.value;
+ }
+
+ function reset() {
+ setName("untitled");
+ }
+
+ inputElement.addEventListener("focus", onFocus);
+ inputElement.addEventListener("blur", onBlur);
+ inputElement.addEventListener("keypress", keyPress);
+ reset();
+
+ return {
+ "getName": getName,
+ "setName": setName,
+ "reset": reset
+ };
+}
+
+function createPaintShortcuts(keyPair) {
+ "use strict";
+ var ignored = false;
+
+ function keyDown(evt) {
+ if (ignored === false) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.ctrlKey === false && evt.altKey === false && evt.shiftKey === false && evt.metaKey === false) {
+ if (keyCode >= 48 && keyCode <= 55) {
+ var colour = keyCode - 48;
+ var currentColour = palette.getForegroundColour();
+ if (currentColour === colour) {
+ palette.setForegroundColour(colour + 8);
+ } else {
+ palette.setForegroundColour(colour);
+ }
+ } else {
+ var charCode = String.fromCharCode(keyCode);
+ if (keyPair[charCode] !== undefined) {
+ if (worker.isConnected() === false || keyPair[charCode].classList.contains("excluded-for-websocket") === false) {
+ evt.preventDefault();
+ keyPair[charCode].click();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function keyDownWithCtrl(evt) {
+ if (ignored === false) {
+ var keyCode = (evt.keyCode || evt.which);
+ if (evt.ctrlKey === true && evt.altKey === false && evt.shiftKey === false && evt.metaKey === false) {
+ var charCode = String.fromCharCode(keyCode);
+ if (keyPair[charCode] !== undefined) {
+ if (worker.isConnected() === false || keyPair[charCode].classList.contains("excluded-for-websocket") === false) {
+ evt.preventDefault();
+ keyPair[charCode].click();
+ }
+ }
+ }
+ }
+ }
+
+ document.addEventListener("keydown", keyDownWithCtrl);
+
+ function enable() {
+ document.addEventListener("keydown", keyDown);
+ }
+
+ function disable() {
+ document.removeEventListener("keydown", keyDown);
+ }
+
+ function ignore() {
+ ignored = true;
+ }
+
+ function unignore() {
+ ignored = false;
+ }
+
+ enable();
+
+ return {
+ "enable": enable,
+ "disable": disable,
+ "ignore": ignore,
+ "unignore": unignore
+ };
+}
+
+function createToggleButton(stateOneName, stateTwoName, stateOneClick, stateTwoClick) {
+ "use strict";
+ var divContainer = document.createElement("DIV");
+ divContainer.classList.add("toggle-button-container");
+ var stateOne = document.createElement("DIV");
+ stateOne.classList.add("toggle-button");
+ stateOne.classList.add("left");
+ stateOne.textContent = stateOneName;
+ var stateTwo = document.createElement("DIV");
+ stateTwo.classList.add("toggle-button");
+ stateTwo.classList.add("right");
+ stateTwo.textContent = stateTwoName;
+ divContainer.appendChild(stateOne);
+ divContainer.appendChild(stateTwo);
+
+ function getElement() {
+ return divContainer;
+ }
+
+ function setStateOne() {
+ stateOne.classList.add("enabled");
+ stateTwo.classList.remove("enabled");
+ }
+
+ function setStateTwo() {
+ stateTwo.classList.add("enabled");
+ stateOne.classList.remove("enabled");
+ }
+
+ stateOne.addEventListener("click", (evt) => {
+ setStateOne();
+ stateOneClick();
+ });
+
+ stateTwo.addEventListener("click", (evt) => {
+ setStateTwo();
+ stateTwoClick();
+ });
+
+ return {
+ "getElement": getElement,
+ "setStateOne": setStateOne,
+ "setStateTwo": setStateTwo
+ };
+}
+
+function createGrid(divElement) {
+ "use strict";
+ var canvases = [];
+ var enabled = false;
+
+ function createCanvases() {
+ var fontWidth = font.getWidth();
+ var fontHeight = font.getHeight();
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var canvasWidth = fontWidth * columns;
+ var canvasHeight = fontHeight * 25;
+ canvases = [];
+ for (var i = 0; i < Math.floor(rows / 25); i++) {
+ var canvas = createCanvas(canvasWidth, canvasHeight);
+ canvases.push(canvas);
+ }
+ if (rows % 25 !== 0) {
+ var canvas = createCanvas(canvasWidth, fontHeight * (rows % 25));
+ canvases.push(canvas);
+ }
+ }
+
+ function renderGrid(canvas) {
+ var columns = textArtCanvas.getColumns();
+ var rows = Math.min(textArtCanvas.getRows(), 25);
+ var fontWidth = canvas.width / columns;
+ var fontHeight = font.getHeight();
+ var ctx = canvas.getContext("2d");
+ var imageData = ctx.createImageData(canvas.width, canvas.height);
+ var byteWidth = canvas.width * 4;
+ var darkGray = new Uint8Array([63, 63, 63, 255]);
+ for (var y = 0; y < rows; y += 1) {
+ for (var x = 0, i = y * fontHeight * byteWidth; x < canvas.width; x += 1, i += 4) {
+ imageData.data.set(darkGray, i);
+ }
+ }
+ for (var x = 0; x < columns; x += 1) {
+ for (var y = 0, i = x * fontWidth * 4; y < canvas.height; y += 1, i += byteWidth) {
+ imageData.data.set(darkGray, i);
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ }
+
+ function createGrid() {
+ createCanvases();
+ renderGrid(canvases[0]);
+ divElement.appendChild(canvases[0]);
+ for (var i = 1; i < canvases.length; i++) {
+ canvases[i].getContext("2d").drawImage(canvases[0], 0, 0);
+ divElement.appendChild(canvases[i]);
+ }
+ }
+
+ function resize() {
+ canvases.forEach((canvas) => {
+ divElement.removeChild(canvas);
+ });
+ createGrid();
+ }
+
+ createGrid();
+
+ document.addEventListener("onTextCanvasSizeChange", resize);
+ document.addEventListener("onLetterSpacingChange", resize);
+ document.addEventListener("onFontChange", resize);
+ document.addEventListener("onOpenedFile", resize);
+
+ function isShown() {
+ return enabled;
+ }
+
+ function show(turnOn) {
+ if (enabled === true && turnOn === false) {
+ divElement.classList.remove("enabled");
+ enabled = false;
+ } else if (enabled === false && turnOn === true) {
+ divElement.classList.add("enabled");
+ enabled = true;
+ }
+ }
+
+ return {
+ "isShown": isShown,
+ "show": show
+ };
+}
+
+function createToolPreview(divElement) {
+ "use strict";
+ var canvases = [];
+ var ctxs = [];
+
+ function createCanvases() {
+ var fontWidth = font.getWidth();
+ var fontHeight = font.getHeight();
+ var columns = textArtCanvas.getColumns();
+ var rows = textArtCanvas.getRows();
+ var canvasWidth = fontWidth * columns;
+ var canvasHeight = fontHeight * 25;
+ canvases = new Array();
+ ctxs = new Array();
+ for (var i = 0; i < Math.floor(rows / 25); i++) {
+ var canvas = createCanvas(canvasWidth, canvasHeight);
+ canvases.push(canvas);
+ ctxs.push(canvas.getContext("2d"));
+ }
+ if (rows % 25 !== 0) {
+ var canvas = createCanvas(canvasWidth, fontHeight * (rows % 25));
+ canvases.push(canvas);
+ ctxs.push(canvas.getContext("2d"));
+ }
+ canvases.forEach((canvas) => {
+ divElement.appendChild(canvas);
+ });
+ }
+
+ function resize() {
+ canvases.forEach((canvas) => {
+ divElement.removeChild(canvas);
+ });
+ createCanvases();
+ }
+
+ function drawHalfBlock(foreground, x, y) {
+ var halfBlockY = y % 2;
+ var textY = Math.floor(y / 2);
+ var ctxIndex = Math.floor(textY / 25);
+ if (ctxIndex >= 0 && ctxIndex < ctxs.length) {
+ font.drawWithAlpha((halfBlockY === 0) ? 223 : 220, foreground, ctxs[ctxIndex], x, textY % 25);
+ }
+ }
+
+ function clear() {
+ for (var i = 0; i < ctxs.length; i++) {
+ ctxs[i].clearRect(0, 0, canvases[i].width, canvases[i].height);
+ }
+ }
+
+ createCanvases();
+ divElement.classList.add("enabled");
+
+ document.addEventListener("onTextCanvasSizeChange", resize);
+ document.addEventListener("onLetterSpacingChange", resize);
+ document.addEventListener("onFontChange", resize);
+ document.addEventListener("onOpenedFile", resize);
+
+ return {
+ "clear": clear,
+ "drawHalfBlock": drawHalfBlock,
+ };
+}
+
+function menuHover() {
+ $("file-menu").classList.remove("hover");
+ $("edit-menu").classList.remove("hover");
+ $("view-menu").classList.remove("hover");
+}
diff --git a/public/js/worker.js b/public/js/worker.js
new file mode 100644
index 0000000..055df58
--- /dev/null
+++ b/public/js/worker.js
@@ -0,0 +1,135 @@
+var socket;
+var sessionID;
+var joint;
+var connected = false;
+
+function send(cmd, msg) {
+ socket.send(JSON.stringify([cmd, msg]));
+}
+
+function onOpen() {
+ postMessage({ "cmd": "connected" });
+}
+
+function onClose(evt) {
+ postMessage({ "cmd": "disconnected" });
+}
+
+function onChat(handle, text, showNotification) {
+ postMessage({ "cmd": "chat", "handle": handle, "text": text, "showNotification": showNotification });
+}
+
+function onStart(msg, newSessionID) {
+ joint = msg;
+ sessionID = newSessionID;
+ msg.chat.forEach((msg) => {
+ onChat(msg[0], msg[1], false);
+ });
+}
+
+function onJoin(handle, joinSessionID, showNotification) {
+ if (joinSessionID === sessionID) {
+ showNotification = false;
+ }
+ postMessage({ "cmd": "join", "sessionID": joinSessionID, "handle": handle, "showNotification": showNotification });
+}
+
+function onNick(handle, nickSessionID) {
+ postMessage({ "cmd": "nick", "sessionID": nickSessionID, "handle": handle, "showNotification": (nickSessionID !== sessionID) });
+}
+
+function onPart(sessionID) {
+ postMessage({ "cmd": "part", "sessionID": sessionID });
+}
+
+function onDraw(blocks) {
+ var outputBlocks = new Array();
+ var index;
+ blocks.forEach((block) => {
+ index = block >> 16;
+ outputBlocks.push([index, block & 0xffff, index % joint.columns, Math.floor(index / joint.columns)]);
+ });
+ postMessage({ "cmd": "draw", "blocks": outputBlocks });
+}
+
+function onMessage(evt) {
+ var data = evt.data;
+ if (typeof (data) === "object") {
+ var fr = new FileReader();
+ fr.addEventListener("load", (evt) => {
+ postMessage({ "cmd": "imageData", "data": evt.target.result, "columns": joint.columns, "rows": joint.rows, "iceColours": joint.iceColours, "letterSpacing": joint.letterSpacing });
+ connected = true;
+ });
+ fr.readAsArrayBuffer(data);
+ } else {
+ data = JSON.parse(data);
+ switch (data[0]) {
+ case "start":
+ sessionID = data[2];
+ var userList = data[3];
+ Object.keys(userList).forEach((userSessionID) => {
+ onJoin(userList[userSessionID], userSessionID, false);
+ });
+ onStart(data[1], data[2]);
+ break;
+ case "join":
+ onJoin(data[1], data[2], true);
+ break;
+ case "nick":
+ onNick(data[1], data[2]);
+ break;
+ case "draw":
+ onDraw(data[1]);
+ break;
+ case "part":
+ onPart(data[1]);
+ break;
+ case "chat":
+ onChat(data[1], data[2], true);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+function removeDuplicates(blocks) {
+ var indexes = [];
+ var index;
+ blocks = blocks.reverse();
+ blocks = blocks.filter((block) => {
+ index = block >> 16;
+ if (indexes.lastIndexOf(index) === -1) {
+ indexes.push(index);
+ return true;
+ }
+ return false;
+ });
+ return blocks.reverse();
+}
+
+self.onmessage = function(msg) {
+ var data = msg.data;
+ switch (data.cmd) {
+ case "connect":
+ socket = new WebSocket(data.url);
+ socket.addEventListener("open", onOpen);
+ socket.addEventListener("message", onMessage);
+ socket.addEventListener("close", onClose);
+ break;
+ case "join":
+ send("join", data.handle);
+ break;
+ case "nick":
+ send("nick", data.handle);
+ break;
+ case "chat":
+ send("chat", data.text);
+ break;
+ case "draw":
+ send("draw", removeDuplicates(data.blocks));
+ break;
+ default:
+ break;
+ }
+};