diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..6937e13 --- /dev/null +++ b/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/img/android-chrome-192x192.png b/img/android-chrome-192x192.png new file mode 100644 index 0000000..1d0aa15 Binary files /dev/null and b/img/android-chrome-192x192.png differ diff --git a/img/apple-touch-icon.png b/img/apple-touch-icon.png new file mode 100644 index 0000000..2cbee4b Binary files /dev/null and b/img/apple-touch-icon.png differ diff --git a/img/browserconfig.xml b/img/browserconfig.xml new file mode 100644 index 0000000..ba99813 --- /dev/null +++ b/img/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/img/chat.png b/img/chat.png new file mode 100644 index 0000000..7dcac60 Binary files /dev/null and b/img/chat.png differ diff --git a/img/face.png b/img/face.png new file mode 100644 index 0000000..c05f8e0 Binary files /dev/null and b/img/face.png differ diff --git a/img/favicon-16x16.png b/img/favicon-16x16.png new file mode 100644 index 0000000..eb29613 Binary files /dev/null and b/img/favicon-16x16.png differ diff --git a/img/favicon-32x32.png b/img/favicon-32x32.png new file mode 100644 index 0000000..71acd8c Binary files /dev/null and b/img/favicon-32x32.png differ diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 0000000..abb958b Binary files /dev/null and b/img/favicon.ico differ diff --git a/img/favicon.png b/img/favicon.png new file mode 100644 index 0000000..1c7d371 Binary files /dev/null and b/img/favicon.png differ diff --git a/img/favicon_192.png b/img/favicon_192.png new file mode 100644 index 0000000..e7ac2cd Binary files /dev/null and b/img/favicon_192.png differ diff --git a/img/mstile-150x150.png b/img/mstile-150x150.png new file mode 100644 index 0000000..7569519 Binary files /dev/null and b/img/mstile-150x150.png differ diff --git a/img/safari-pinned-tab.svg b/img/safari-pinned-tab.svg new file mode 100644 index 0000000..b1b19cb --- /dev/null +++ b/img/safari-pinned-tab.svg @@ -0,0 +1,74 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/img/selection_border.gif b/img/selection_border.gif new file mode 100644 index 0000000..182413a Binary files /dev/null and b/img/selection_border.gif differ diff --git a/img/site.webmanifest b/img/site.webmanifest new file mode 100644 index 0000000..8dc2840 --- /dev/null +++ b/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/img/toolbar/character_brush.png b/img/toolbar/character_brush.png new file mode 100644 index 0000000..5c4c2b7 Binary files /dev/null and b/img/toolbar/character_brush.png differ diff --git a/img/toolbar/circle.png b/img/toolbar/circle.png new file mode 100644 index 0000000..876861c Binary files /dev/null and b/img/toolbar/circle.png differ diff --git a/img/toolbar/done.png b/img/toolbar/done.png new file mode 100644 index 0000000..b706ad3 Binary files /dev/null and b/img/toolbar/done.png differ diff --git a/img/toolbar/fill.png b/img/toolbar/fill.png new file mode 100644 index 0000000..7a14df4 Binary files /dev/null and b/img/toolbar/fill.png differ diff --git a/img/toolbar/freestyle.png b/img/toolbar/freestyle.png new file mode 100644 index 0000000..6a72394 Binary files /dev/null and b/img/toolbar/freestyle.png differ diff --git a/img/toolbar/keyboard.png b/img/toolbar/keyboard.png new file mode 100644 index 0000000..2b6b9b4 Binary files /dev/null and b/img/toolbar/keyboard.png differ diff --git a/img/toolbar/line.png b/img/toolbar/line.png new file mode 100644 index 0000000..c36bd4c Binary files /dev/null and b/img/toolbar/line.png differ diff --git a/img/toolbar/sample.png b/img/toolbar/sample.png new file mode 100644 index 0000000..c6c995b Binary files /dev/null and b/img/toolbar/sample.png differ diff --git a/img/toolbar/selection.png b/img/toolbar/selection.png new file mode 100644 index 0000000..7b1a133 Binary files /dev/null and b/img/toolbar/selection.png differ diff --git a/img/toolbar/square.png b/img/toolbar/square.png new file mode 100644 index 0000000..1f3acfa Binary files /dev/null and b/img/toolbar/square.png differ diff --git a/index.html b/index.html index 9833c72..a342971 100644 --- a/index.html +++ b/index.html @@ -3,26 +3,27 @@ - - - - - - - + + + + + + + - + moebius - - - - - - - + + + + + + + +
@@ -72,7 +73,7 @@
Chat(Escape) X Chat + id="chat-icon-close-text">(Escape) X Chat
@@ -88,21 +89,21 @@
-
Keyboard
-
Freestyle
-
Character Brush
-
Fill +
Fill
-
Line
-
Square
-
Circle
+
Line
+
Square
+
Circle
-
Selection
-
Sample +
Sample
diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..751b38c --- /dev/null +++ b/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/js/document_onload.js b/js/document_onload.js new file mode 100644 index 0000000..b0613b0 --- /dev/null +++ b/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/js/elementhelper.js b/js/elementhelper.js new file mode 100644 index 0000000..575b2be --- /dev/null +++ b/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/js/file.js b/js/file.js new file mode 100644 index 0000000..1fb4215 --- /dev/null +++ b/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/js/freehand_tools.js b/js/freehand_tools.js new file mode 100644 index 0000000..27f0103 --- /dev/null +++ b/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/js/keyboard.js b/js/keyboard.js new file mode 100644 index 0000000..383c10a --- /dev/null +++ b/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/js/loaders.js b/js/loaders.js new file mode 100644 index 0000000..94e5264 --- /dev/null +++ b/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/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 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + 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 + + + + + + + + + + +

+
+ + + + +
Chat(Escape) X Chat +
+
+
+
+
+
+
+ + +
+
+
+ + +
Keyboard
+
Freestyle
+
Character Brush
+
Fill +
+
Line
+
Square
+
Circle
+
+
Selection
+
Sample +
+
+
+
+
+
+
+
+
+
+ F1: + F2: + F3: + F4: + F5: + F6: + F7: + F8: + F9: + F10: + F11: + F12: +
+
+
+
+
+
+ +
Cancel
+
+
+
+
+
+
+
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; + } +};