├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── docs ├── app.js ├── index.html ├── unicode-chars.js └── util.js ├── makefile └── src ├── app.js ├── index.html └── js ├── app.js ├── controller ├── controller.js └── tools.js ├── entities ├── box.js └── pixel-context.js ├── model ├── constants.js ├── coord-pixel.js ├── coord.js ├── grid.js └── pixel.js ├── util ├── unicode-chars.js └── util.js └── view ├── canvas-zoom.js ├── canvas.js ├── decorators.js └── drawable-canvas.js /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | /bak/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ASCII Editor 2 | ============ 3 | 4 | ![travis badge](https://travis-ci.org/andresoviedo/ascii-editor.svg?branch=master) 5 | 6 | 7 | _ ____ ____ ___ ___ _____ _ _ _ 8 | / \ / ___| / ___|_ _|_ _| | ____|__| (_) |_ ___ _ __ 9 | / _ \ \___ \| | | | | | | _| / _` | | __/ _ \| '__| 10 | / ___ \ ___) | |___ | | | | | |__| (_| | | || (_) | | 11 | /_/ \_\____/ \____|___|___| |_____\__,_|_|\__\___/|_| 12 | 13 | 14 | 15 | The basic idea of this project is to have an editor to design schemas, tables, drawings, etc. for making technical documentation that can be 16 | used into the ubiquitous README files with import and export functionality. 17 | 18 | There is already some tools in internet like the awesome asciiflow, but I need something more professional and the asciiflow tool is not open source; 19 | so I have decided to cook my own stuff and share it with the world :) 20 | 21 | 22 | Try it! 23 | ======= 24 | 25 | * http://www.andresoviedo.org/ascii-editor 26 | 27 | 28 | Example 29 | ======= 30 | 31 | This is an example of what you can draw with the app 32 | 33 | 34 | 35 | +------------------------+ 36 | | | 37 | | | connector 38 | | BOX STYLE 1 |──────────┐ ┌──────────────────────┐ 39 | | | │ │ │ 40 | | | │ │ │ 41 | +------------------------+ └─────────┤ BOX STYLE 2 │ 42 | │ │ 43 | │ │ 44 | └──────────────────────┘ 45 | 46 | 47 | News (01/08/2018) 48 | ================= 49 | 50 | - Refactored project structure 51 | - Removed gradle combineJs plugin 52 | 53 | 54 | Next release 55 | ============ 56 | 57 | - Working on connectors... 58 | - Working on moving source code to TypeScript 59 | 60 | 61 | Design 62 | ====== 63 | 64 | - HTML canvas technology 65 | - JavaScript ECMAScript6 technology (Classes + Inheritance) 66 | - Object oriented design. Classes, Inheritance, 67 | - Patters design: Layers Pattern, Decorator Pattern 68 | - JQuery framework (just for manipulating DOM) 69 | 70 | 71 | Features implemented 72 | ==================== 73 | 74 | - Canvas controller (mouse click, mouse wheel, mouse drag, key down, key press, key up) 75 | - Canvas grid (100 x 200) 76 | - Scrollable canvas (mouse wheel) 77 | - Zoomable canvas (shift + mouse wheel) 78 | - Movable canvas (shift + mouse drag) 79 | - Resizable canvas (on windows resize) 80 | - Canvas cursor & pointer (arrow keys can control the cursor) 81 | - Write chars 82 | - Draw with different line styles 83 | - HTML Storage support to resume work 84 | - Tools: 85 | - Add / Edit text 86 | - Draw / Resize boxes (move lines also) 87 | - Select Area / Clear / Move it 88 | - Select Box / Move it 89 | - Clear canvas 90 | - Export to ASCII (so you can copy / paste) 91 | - Draw lines 92 | 93 | 94 | Still to be implement 95 | ===================== 96 | 97 | - improve connectors (reposition connector when its crossing the box) 98 | - draw lines 99 | - import ASCII 100 | - cut / copy / paste 101 | - trim to export (remove unnecessary lines, columns) 102 | - export indented (so it can be copied then to README.md files - should start with 4 spaces) 103 | - make size of canvas configurable / resizable 104 | - undo / redo 105 | 106 | 107 | Nice to have 108 | ============ 109 | 110 | - text behaviour: resize shape if writing text inside 111 | - save / restore configuration (zoom for example) 112 | - implement tables (add columns, add rows) 113 | - select shape / move shape (not just boxes, but tables) 114 | - implement chars library (choose char from unicode list) 115 | - integrate ASCII library 116 | - improve / beautify UI 117 | - fix state machine for all tools (like I did in SelectTool) 118 | - handle keyboard functions: printing F2 / handle find F3 / handle full screen F11 119 | - many more... 120 | 121 | 122 | Alternatives 123 | ============ 124 | 125 | * ascii-flow: http://asciiflow.com/ 126 | * textik: https://textik.com/ 127 | * sixteencolors: http://draw.sixteencolors.net 128 | * ascii-tables: https://ozh.github.io/ascii-tables/ 129 | 130 | 131 | Build 132 | ===== 133 | 134 | gradle combineJs 135 | 136 | 137 | Final Notes 138 | =========== 139 | 140 | You are free to use this program while you keep this file and the authoring comments in the code. Any comments, suggestions or contributions are welcome. 141 | 142 | 143 | Contact 144 | ======= 145 | 146 | http://www.andresoviedo.org 147 | 148 | 149 | ChangeLog 150 | ========= 151 | 152 | * 2018/08/01 153 | * (f) Removed gradle combineJs plugin 154 | * (f) Project structure refactored 155 | * 2017/03/24 156 | * (n) Refactored to file per Class 157 | * (n) Project moved to gradle 158 | * 2017/03/19 159 | * (f) Fixed backspace for removing chars 160 | * (n) Added support for handling carriage line return key when entering text 161 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | defaultTasks 'combineJs' 2 | 3 | task combineJs() { 4 | def main = ["src/js/app.js"] 5 | def controller = ["src/js/controller/controller.js", "src/js/controller/tools.js"] 6 | def entities = ["src/js/entities/box.js", "src/js/entities/pixel-context.js"] 7 | def model = ["src/js/model/constants.js", "src/js/model/coord-pixel.js", "src/js/model/coord.js", "src/js/model/grid.js", "src/js/model/pixel.js"] 8 | def util = ["src/js/util/unicode-chars.js", "src/js/util/util.js"] 9 | def view = ["src/js/view/canvas-zoom.js", "src/js/view/canvas.js", "src/js/view/decorators.js", "src/js/view/drawable-canvas.js"] 10 | def source = main + controller + entities + model + util + view 11 | // show the resolved files when gradle is run with -d 12 | // source.each{ logger.debug ("$it") } 13 | def output = file("docs/app.js") 14 | output.write('') // truncate output if needed 15 | source.each { f -> output.append(file(f).getText('UTF-8'),"UTF-8") } 16 | } 17 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ASCII Editor 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 84 | 85 | 86 | 87 |

ASCII Editor

88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 | https://github.com/andresoviedo/ascii-editor 104 |
105 | 106 |
107 | 108 | 109 | 110 |
111 | 112 |
113 | 114 |
115 | 116 |
117 | 118 |
119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/unicode-chars.js: -------------------------------------------------------------------------------- 1 | function UnicodeChars(){ 2 | this.LATIN_PLUS_SIGN = "+"; 3 | this.LATIN_HYPHEN_MINUS = "-"; 4 | this.LATIN_VERTICAL_LINE = "|"; 5 | this.BOX_HORIZONTAL="─"; 6 | this.BOX_VERTICAL="│"; 7 | this.BOX_CROSS="┼"; 8 | this.BOX_CORNER_TOP_LEFT="┌"; 9 | this.BOX_CORNER_TOP_RIGHT="┐"; 10 | this.BOX_CORNER_BOTTOM_LEFT="└"; 11 | this.BOX_CORNER_BOTTOM_RIGHT="┘"; 12 | this.BOX_HORIZONTAL_LIGHT_UP="┴"; 13 | this.BOX_HORIZONTAL_LIGHT_DOWN="┬"; 14 | this.BOX_VERTICAL_LIGHT_RIGHT="├"; 15 | this.BOX_VERTICAL_LIGHT_LEFT="┤"; 16 | 17 | this.init(); 18 | } 19 | 20 | UnicodeChars.prototype.init = function(){ 21 | var allChars = []; 22 | var boxChars = []; 23 | for (field in this){ 24 | if (this.hasOwnProperty(field)) { 25 | char = this[field]; 26 | allChars.push(char); 27 | if (field.startsWith("BOX")){ 28 | boxChars.push(char); 29 | } 30 | } 31 | } 32 | this.CHARS = allChars; 33 | this.BOX_CHARS = boxChars; 34 | } 35 | 36 | UnicodeChars.prototype.isChar = function(char){ 37 | return this.CHARS.indexOf(char) != -1; 38 | } 39 | 40 | UnicodeChars.prototype.isBoxChar = function(char){ 41 | return this.BOX_CHARS.indexOf(char) != -1; 42 | } 43 | 44 | var UC = UnicodeChars = new UnicodeChars(); 45 | 46 | -------------------------------------------------------------------------------- /docs/util.js: -------------------------------------------------------------------------------- 1 | //--------------------------------------------- UTIL FUNCTIONS ------------------------------------------------------// 2 | 3 | var KeyEvent = KeyEvent; 4 | if (typeof KeyEvent == "undefined") { 5 | KeyEvent = { 6 | DOM_VK_CANCEL: 3, 7 | DOM_VK_HELP: 6, 8 | DOM_VK_BACK_SPACE: 8, 9 | DOM_VK_TAB: 9, 10 | DOM_VK_CLEAR: 12, 11 | DOM_VK_RETURN: 13, 12 | DOM_VK_ENTER: 14, 13 | DOM_VK_SHIFT: 16, 14 | DOM_VK_CONTROL: 17, 15 | DOM_VK_ALT: 18, 16 | DOM_VK_PAUSE: 19, 17 | DOM_VK_CAPS_LOCK: 20, 18 | DOM_VK_ESCAPE: 27, 19 | DOM_VK_SPACE: 32, 20 | DOM_VK_PAGE_UP: 33, 21 | DOM_VK_PAGE_DOWN: 34, 22 | DOM_VK_END: 35, 23 | DOM_VK_HOME: 36, 24 | DOM_VK_LEFT: 37, 25 | DOM_VK_UP: 38, 26 | DOM_VK_RIGHT: 39, 27 | DOM_VK_DOWN: 40, 28 | DOM_VK_PRINTSCREEN: 44, 29 | DOM_VK_INSERT: 45, 30 | DOM_VK_DELETE: 46, 31 | DOM_VK_0: 48, 32 | DOM_VK_1: 49, 33 | DOM_VK_2: 50, 34 | DOM_VK_3: 51, 35 | DOM_VK_4: 52, 36 | DOM_VK_5: 53, 37 | DOM_VK_6: 54, 38 | DOM_VK_7: 55, 39 | DOM_VK_8: 56, 40 | DOM_VK_9: 57, 41 | DOM_VK_SEMICOLON: 59, 42 | DOM_VK_EQUALS: 61, 43 | DOM_VK_A: 65, 44 | DOM_VK_B: 66, 45 | DOM_VK_C: 67, 46 | DOM_VK_D: 68, 47 | DOM_VK_E: 69, 48 | DOM_VK_F: 70, 49 | DOM_VK_G: 71, 50 | DOM_VK_H: 72, 51 | DOM_VK_I: 73, 52 | DOM_VK_J: 74, 53 | DOM_VK_K: 75, 54 | DOM_VK_L: 76, 55 | DOM_VK_M: 77, 56 | DOM_VK_N: 78, 57 | DOM_VK_O: 79, 58 | DOM_VK_P: 80, 59 | DOM_VK_Q: 81, 60 | DOM_VK_R: 82, 61 | DOM_VK_S: 83, 62 | DOM_VK_T: 84, 63 | DOM_VK_U: 85, 64 | DOM_VK_V: 86, 65 | DOM_VK_W: 87, 66 | DOM_VK_X: 88, 67 | DOM_VK_Y: 89, 68 | DOM_VK_Z: 90, 69 | DOM_VK_CONTEXT_MENU: 93, 70 | DOM_VK_NUMPAD0: 96, 71 | DOM_VK_NUMPAD1: 97, 72 | DOM_VK_NUMPAD2: 98, 73 | DOM_VK_NUMPAD3: 99, 74 | DOM_VK_NUMPAD4: 100, 75 | DOM_VK_NUMPAD5: 101, 76 | DOM_VK_NUMPAD6: 102, 77 | DOM_VK_NUMPAD7: 103, 78 | DOM_VK_NUMPAD8: 104, 79 | DOM_VK_NUMPAD9: 105, 80 | DOM_VK_MULTIPLY: 106, 81 | DOM_VK_ADD: 107, 82 | DOM_VK_SEPARATOR: 108, 83 | DOM_VK_SUBTRACT: 109, 84 | DOM_VK_DECIMAL: 110, 85 | DOM_VK_DIVIDE: 111, 86 | DOM_VK_F1: 112, 87 | DOM_VK_F2: 113, 88 | DOM_VK_F3: 114, 89 | DOM_VK_F4: 115, 90 | DOM_VK_F5: 116, 91 | DOM_VK_F6: 117, 92 | DOM_VK_F7: 118, 93 | DOM_VK_F8: 119, 94 | DOM_VK_F9: 120, 95 | DOM_VK_F10: 121, 96 | DOM_VK_F11: 122, 97 | DOM_VK_F12: 123, 98 | DOM_VK_F13: 124, 99 | DOM_VK_F14: 125, 100 | DOM_VK_F15: 126, 101 | DOM_VK_F16: 127, 102 | DOM_VK_F17: 128, 103 | DOM_VK_F18: 129, 104 | DOM_VK_F19: 130, 105 | DOM_VK_F20: 131, 106 | DOM_VK_F21: 132, 107 | DOM_VK_F22: 133, 108 | DOM_VK_F23: 134, 109 | DOM_VK_F24: 135, 110 | DOM_VK_NUM_LOCK: 144, 111 | DOM_VK_SCROLL_LOCK: 145, 112 | DOM_VK_COMMA: 188, 113 | DOM_VK_PERIOD: 190, 114 | DOM_VK_SLASH: 191, 115 | DOM_VK_BACK_QUOTE: 192, 116 | DOM_VK_OPEN_BRACKET: 219, 117 | DOM_VK_BACK_SLASH: 220, 118 | DOM_VK_CLOSE_BRACKET: 221, 119 | DOM_VK_QUOTE: 222, 120 | DOM_VK_META: 224 121 | }; 122 | } 123 | 124 | function debug(data) { 125 | if (typeof data === 'string'){ 126 | console.log('\''+data+'\''); 127 | return; 128 | } 129 | if (typeof data == 'number'){ 130 | console.log(data); 131 | return; 132 | } 133 | var ok = false; 134 | for (var key in data) { 135 | ok = true; 136 | if (data.hasOwnProperty(key)) { 137 | console.log(key+"="+data[key]); 138 | } 139 | } 140 | if (!ok){ 141 | console.log(data); 142 | } 143 | } 144 | 145 | function getTextWidth(ctx, font){ 146 | ctx.font = font; 147 | var textMetrics = ctx.measureText("+"); 148 | var width = textMetrics.width; 149 | return width; 150 | } 151 | 152 | function getTextHeight(ctx, font, left, top, width, height) { 153 | 154 | // Draw the text in the specified area 155 | ctx.save(); 156 | ctx.font = font; 157 | ctx.fillText('█',width/2,height/2); 158 | ctx.restore(); 159 | 160 | // Get the pixel data from the canvas 161 | var data = ctx.getImageData(left, top, width, height).data, 162 | first = false, 163 | last = false, 164 | r = height, 165 | c = 0; 166 | 167 | // Find the last line with a non-white pixel 168 | while(!last && r) { 169 | r--; 170 | for(c = 0; c < width; c++) { 171 | if(data[r * width * 4 + c * 4 + 3]) { 172 | last = r; 173 | break; 174 | } 175 | } 176 | } 177 | 178 | var cellDescend = 0; 179 | if (last){ 180 | cellDescend = last - height/2; 181 | } 182 | 183 | // Find the first line with a non-white pixel 184 | while(r) { 185 | r--; 186 | for(c = 0; c < width; c++) { 187 | if(data[r * width * 4 + c * 4 + 3]) { 188 | first = r; 189 | break; 190 | } 191 | } 192 | 193 | // If we've got it then return the height 194 | if(first != r) break; 195 | } 196 | 197 | // debug 198 | if (debug=="true"){ 199 | ctx.save(); 200 | ctx.font = font; 201 | ctx.fillStyle = "#000000"; 202 | ctx.fillRect(0,0,width,height); 203 | ctx.fillStyle = "#ffffff"; 204 | ctx.fillText('█',width/2,height/2); 205 | ctx.strokeStyle = "#00ff00"; 206 | ctx.beginPath(); 207 | ctx.moveTo(0,first); 208 | ctx.lineTo(width, first); 209 | ctx.stroke(); 210 | ctx.beginPath(); 211 | ctx.moveTo(0,height/2); 212 | ctx.lineTo(width, height/2); 213 | ctx.stroke(); 214 | ctx.beginPath(); 215 | ctx.moveTo(0,last); 216 | ctx.lineTo(width, last); 217 | ctx.stroke(); 218 | ctx.restore(); 219 | } 220 | 221 | 222 | if (first != r){ 223 | return [last - first,cellDescend]; 224 | } 225 | 226 | // We screwed something up... What do you expect from free code? 227 | return [0,0]; 228 | } 229 | 230 | function drawBorder(canvasContext, width, height){ 231 | canvasContext.lineWidth = 5; 232 | canvasContext.strokeStyle = "#00FF00"; 233 | canvasContext.beginPath(); 234 | canvasContext.moveTo(0,0); 235 | canvasContext.lineTo(width,0); 236 | canvasContext.stroke(); 237 | canvasContext.beginPath(); 238 | canvasContext.moveTo(0,0); 239 | canvasContext.lineTo(0,height); 240 | canvasContext.stroke(); 241 | canvasContext.beginPath(); 242 | canvasContext.moveTo(0,height); 243 | canvasContext.lineTo(width,height); 244 | canvasContext.stroke(); 245 | canvasContext.beginPath(); 246 | canvasContext.moveTo(width,0); 247 | canvasContext.lineTo(width,height); 248 | canvasContext.stroke(); 249 | } 250 | 251 | function paint(ctx,font,cellWidth){ 252 | ctx.font = font; 253 | ctx.fillText('┌──┼──┐ █ ██', 0,cellHeight-cellDescend); 254 | ctx.fillText('├──┼──┤ █ ██', 0,cellHeight*2-cellDescend); 255 | ctx.fillText('└──┼──┘ █ ██', 0,cellHeight*3-cellDescend); 256 | ctx.fillText('+--+--+ █', 0,cellHeight*5-cellDescend); 257 | ctx.fillText('+--+--+ █ ██', 0,cellHeight*6-cellDescend); 258 | ctx.fillText('+--+--+ █ ██', 0,cellHeight*7-cellDescend); 259 | } 260 | 261 | function delegateProxy(target,delegateName){ 262 | // proxy handler 263 | var proxyHandler = { 264 | get(target, propKey, receiver) { 265 | const realTarget = target[propKey]? target : target[delegateName]? target[delegateName] : undefined; 266 | const prop = realTarget? realTarget[propKey] : undefined; 267 | if (typeof(prop) != "function"){ 268 | return prop; 269 | } 270 | return function (...args) { 271 | let result = prop.apply(realTarget, args); 272 | // console.log(propKey + JSON.stringify(args) + ' -> ' + JSON.stringify(result)); 273 | return result; 274 | }; 275 | } 276 | }; 277 | return new Proxy(target,proxyHandler); 278 | } -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test: 2 | echo executing tests... 3 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // ascii-editor.js 2 | (function () { 3 | 4 | // trigger module loading by adding script element 5 | function loadModule(mod) { 6 | var element = document.createElement('script'); 7 | element.setAttribute('type','text/javascript'); 8 | element.setAttribute('src',mod); 9 | document.getElementsByTagName('head')[0].appendChild(element); 10 | } 11 | 12 | // trigger module loading: : load order not guaranteed! 13 | loadModule("js/app.js"); 14 | loadModule("js/util/util.js"); 15 | loadModule("js/util/unicode-chars.js"); 16 | 17 | loadModule("js/model/constants.js"); 18 | loadModule("js/model/grid.js"); 19 | loadModule("js/model/coord.js"); 20 | loadModule("js/model/pixel.js"); 21 | loadModule("js/model/coord-pixel.js"); 22 | loadModule("js/view/canvas.js"); 23 | loadModule("js/view/canvas-zoom.js"); 24 | loadModule("js/view/drawable-canvas.js"); 25 | loadModule("js/view/decorators.js"); 26 | loadModule("js/entities/box.js"); 27 | loadModule("js/entities/pixel-context.js"); 28 | loadModule("js/controller/tools.js"); 29 | loadModule("js/controller/controller.js"); 30 | 31 | })() 32 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ASCII Editor 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 84 | 85 | 86 | 87 |

ASCII Editor

88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 | https://github.com/andresoviedo/ascii-editor 104 |
105 | 106 |
107 | 108 | 109 | 110 |
111 | 112 |
113 | 114 |
115 | 116 |
117 | 118 |
119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ┌─┐┌─┐┌─┐┌─┐┌─┐ ┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐ 3 | * │A││S││C││I││I│-->│E││d││i││t││o││r│ 4 | * └─┘└─┘└─┘└─┘└─┘ └─┘└─┘└─┘└─┘└─┘└─┘ 5 | * 6 | * This is the main code for drawing the ASCII code (pixels from now on) into the HTML canvas. 7 | * Basically there is a Canvas object holding a Grid object holding of matrix of Pixels. 8 | * 9 | * Features implemented so far: 10 | * - a grid is drawn into the canvas 11 | * - canvas can be zoomed 12 | * - boxes can be drawn 13 | * - tools has an associated mouse cursor 14 | * - pixels integration 15 | * - canvas can be cleared 16 | * 17 | * TODO: fix text tool so its not drawn outside the bounds of the canvas 18 | */ 19 | 20 | /* 21 | * Initialize canvas and use the Decorator Pattern to add more features (single responsability chain). 22 | * In order to implement Decorator Pattern, I use jquery to extend objects ($.extend()). 23 | * Since the wrapper mechanism is emulated (based on copying object properties), I have to make use of this.$ variable to reference the real 'this'. 24 | */ 25 | function init(){ 26 | // init here because it depends on modules 27 | init_constants(); 28 | // initialize grid 29 | var grid = new Grid(); 30 | // initialize canvas 31 | var canvas = delegateProxy(new ASCIICanvas(document.getElementById("ascii-canvas"),grid),"grid"); 32 | canvas.init(); 33 | // add canvas movability 34 | canvas = new MovableCanvas(canvas, "#canvas-container"); 35 | // add canvas zoom feature 36 | canvas = delegateProxy(new ZoomableCanvas(canvas), "canvas"); 37 | canvas.init(); 38 | // add ascii drawing capabilities 39 | canvas = delegateProxy(new DrawableCanvas(canvas), "canvas"); 40 | // add ascii drawing capabilities with style 41 | canvas = delegateProxy(new StylableCanvas(canvas), "canvas"); 42 | // add cursor decorator 43 | canvas = delegateProxy(new PointerDecorator(canvas, "pointer-button"), "canvas"); 44 | // add char writing capabilities 45 | canvas = delegateProxy(new WritableCanvas(canvas), "canvas"); 46 | // instantiate canvas controller (mouse control, keyboard control, tools, etc) 47 | var controller = new CanvasController(canvas); 48 | // add clear canvas capabilities 49 | controller.addTool(new ClearCanvasTool("clear-button",canvas)); 50 | // add set/edit text capabilities 51 | controller.addTool(new EditTextTool("text-button",canvas)); 52 | // add export to ascii capabilities 53 | controller.addTool(new ExportASCIITool("export-button", canvas, "#canvas-container", "#dialog-widget")); 54 | // add draw box capabilities 55 | controller.addTool(new BoxDrawerTool("box-button", canvas)); 56 | // add line drawing capabilities 57 | controller.addTool(new LineTool("line-button", canvas)); 58 | // add selection capabilities 59 | controller.addTool(new SelectTool("select-button", canvas)); 60 | // set default tool 61 | controller.setActiveTool("select-button"); 62 | // start animation loop 63 | animate(canvas); 64 | } 65 | 66 | function animate(canvas){ 67 | if (canvas.hasChanged()){ 68 | canvas.redraw(); 69 | canvas.setChanged(false); 70 | } 71 | window.requestAnimationFrame(function() { 72 | animate(canvas); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/js/controller/controller.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------- MOUSE CONTROLLER ------------------------------------------------// 2 | 3 | function CanvasController(canvas) { 4 | this.class = 'CanvasController'; 5 | this.canvas = canvas; 6 | this.tools = {}; 7 | this.shiftKeyEnabled = false; 8 | this.dummyTool = new CanvasTool(); 9 | this.canvasHTML = canvas.getCanvasHTML(); 10 | this.init(); 11 | this.lastPointerCoord = null; 12 | // visual only: adapt canvas container 13 | $("#canvas-container").width(this.canvas.getWidth()); 14 | // select first cell, so user can start writing right from start 15 | this.canvas.setSelectedCell(new Coord(0,0)); 16 | } 17 | 18 | CanvasController.prototype = { 19 | init : function() { 20 | $("#tools > button.tool").click(function(eventObject) { 21 | // active tool 22 | this.setActiveTool(eventObject.target.id); 23 | }.bind(this)); 24 | // bind mouse action for handling the drawing 25 | $(this.canvas.getCanvasHTML()).mousedown(function(eventObject) { 26 | // propagate event 27 | this.canvas.mouseDown(eventObject); 28 | this.lastPointerCoord = this.getGridCoord(eventObject); 29 | this.canvas.cellDown(this.lastPointerCoord); 30 | // invoke active tool 31 | try{ 32 | this.getActiveTool().mouseDown(eventObject); 33 | this.getActiveTool().cellDown(this.lastPointerCoord); 34 | } catch(e){ 35 | console.error(e.stack); 36 | } 37 | }.bind(this)); 38 | $(this.canvas.getCanvasHTML()).mouseup(function() { 39 | // propagate event 40 | this.canvas.mouseUp(); 41 | this.canvas.cellUp(this.lastPointerCoord); 42 | try{ 43 | this.getActiveTool().mouseUp(); 44 | this.getActiveTool().cellUp(this.lastPointerCoord); 45 | } catch(e){ 46 | console.error(e.stack); 47 | } 48 | }.bind(this)); 49 | $(this.canvas.getCanvasHTML()).mouseenter(function() { 50 | this.canvas.getCanvasHTML().style.cursor = this.canvas.cursor(); 51 | this.canvas.mouseEnter(); 52 | try{ 53 | this.getActiveTool().mouseEnter(); 54 | this.canvas.getCanvasHTML().style.cursor = this.getActiveTool().cursor(); 55 | } catch(e){ 56 | console.error(e.stack); 57 | } 58 | }.bind(this)); 59 | $(this.canvas.getCanvasHTML()).mousemove(function(eventObject) { 60 | // propagate event 61 | this.canvas.mouseMove(eventObject); 62 | this.lastPointerCoord = this.getGridCoord(eventObject); 63 | this.canvas.cellMove(this.lastPointerCoord); 64 | try{ 65 | this.getActiveTool().mouseMove(eventObject); 66 | this.getActiveTool().cellMove(this.lastPointerCoord); 67 | } catch(e){ 68 | console.error(e.stack); 69 | } 70 | }.bind(this)); 71 | $(this.canvas.getCanvasHTML()).mouseleave(function() { 72 | this.canvas.mouseLeave(); 73 | try{ 74 | this.getActiveTool().mouseLeave(); 75 | } catch(e){ 76 | console.error(e.stack); 77 | } 78 | }.bind(this)); 79 | $(window).keydown(function(eventObject) { 80 | if (eventObject.keyCode == KeyEvent.DOM_VK_SHIFT) { 81 | this.shiftKeyEnabled = true; 82 | } 83 | this.canvas.keyDown(eventObject); 84 | try{ 85 | this.getActiveTool().keyDown(eventObject); 86 | } catch(e){ 87 | console.error(e.stack); 88 | } 89 | }.bind(this)); 90 | $(document).keypress(function(eventObject) { 91 | this.canvas.keyPress(eventObject); 92 | try{ 93 | this.getActiveTool().keyPress(eventObject); 94 | } catch(e){ 95 | console.error(e.stack); 96 | } 97 | }.bind(this)); 98 | $(window).keyup(function(eventObject) { 99 | if (eventObject.keyCode == KeyEvent.DOM_VK_SHIFT) { 100 | this.shiftKeyEnabled = false; 101 | } 102 | this.canvas.keyUp(eventObject); 103 | try{ 104 | this.getActiveTool().keyUp(eventObject); 105 | } catch(e){ 106 | console.error(e.stack); 107 | } 108 | }.bind(this)); 109 | } 110 | ,addTool : function(tool){ 111 | this.tools[tool.getId()] = tool; 112 | } 113 | ,getActiveTool : function(){ 114 | if (this.shiftKeyEnabled) return this.dummyTool; 115 | for (var tool in this.tools){ 116 | if (this.tools[tool].isEnabled()){ 117 | return this.tools[tool]; 118 | } 119 | } 120 | return null; 121 | } 122 | ,setActiveTool : function(elementId){ 123 | try { 124 | // toggle active button (visual feature only) 125 | $("#tools > button.tool").removeClass("active"); 126 | $("#" + elementId).toggleClass("active"); 127 | // enable tool 128 | for (var tool in this.tools){ 129 | this.tools[tool].setEnabled(this.tools[tool].getId() == elementId); 130 | } 131 | this.getActiveTool().click(); 132 | }catch(e){ 133 | console.error(e.stack); 134 | } 135 | } 136 | , getGridCoord: function(mouseEvent){ 137 | // get HTML canvas relative coordinates 138 | canvasHTMLCoord = this.getCanvasHTMLCoord(mouseEvent); 139 | // get canvas coord 140 | return this.canvas.getGridCoord(canvasHTMLCoord); 141 | } 142 | , getCanvasHTMLCoord : function(mouseEvent){ 143 | var x; 144 | var y; 145 | if (mouseEvent.pageX || mouseEvent.pageY) { 146 | x = mouseEvent.pageX; 147 | y = mouseEvent.pageY; 148 | } else { 149 | x = mouseEvent.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 150 | y = mouseEvent.clientY + document.body.scrollTop + document.documentElement.scrollTop; 151 | } 152 | x -= this.canvasHTML.offsetLeft; 153 | y -= this.canvasHTML.offsetTop; 154 | // are we inside a scrollable div? 155 | // TODO: should this be handled by MovableTool? (it know there is a container) 156 | var parent = $(this.canvasHTML).parent(); 157 | if (parent){ 158 | x += parent.scrollLeft(); 159 | y += parent.scrollTop(); 160 | } 161 | return new Coord(x,y); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/js/controller/tools.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------- CANVAS TOOL ------------------------------------------------------ // 2 | 3 | /** 4 | * Abstract tool 5 | */ 6 | class CanvasTool { 7 | constructor(toolId){ 8 | this.toolId = toolId; 9 | this.enabled = false; 10 | } 11 | getId(){ return this.toolId; } 12 | isEnabled(){ return this.enabled; } 13 | setEnabled(enabled){ this.enabled = enabled; } 14 | click() {} 15 | mouseDown(eventObject) { } 16 | mouseMove(eventObject) { } 17 | mouseUp() { } 18 | mouseEnter() { } 19 | mouseLeave() { } 20 | cellDown(coord) { } 21 | cellMove(coord) { } 22 | cellUp(coord) { } 23 | keyDown(eventObject){ } 24 | keyPress(eventObject){ } 25 | keyUp(eventObject){ } 26 | cursor(){} 27 | } 28 | 29 | // ---------------------------------------------- MOVE FEATURE ----------------------------------------------------- // 30 | 31 | class SelectTool extends CanvasTool { 32 | constructor(toolId, canvas){ 33 | super(toolId); 34 | this.canvas = canvas; 35 | this.changed = false; 36 | // mouse action 37 | this.startCoord = null; 38 | this.currentCoord = null; 39 | this.startTime = null; 40 | this.currentTime = null; 41 | this.action = null; 42 | // area selection 43 | this.controlKeyEnabled = false; 44 | this.selectionArea = null; 45 | this.finalBox = null; 46 | this.finalMove = null; 47 | // shape selection 48 | this.endPointsInfo = null; 49 | this.selectedBox = null; 50 | } 51 | cursor(){ 52 | return "pointer"; 53 | } 54 | hasChanged(){ 55 | return this.canvas.hasChanged() || this.changed; 56 | } 57 | setChanged(changed){ 58 | this.canvas.setChanged(changed) 59 | this.changed = changed; 60 | } 61 | keyDown(eventObject){ 62 | // check if canvas has the focus 63 | if (!this.canvas.isFocused()) return; 64 | // capture control key event 65 | if (eventObject.keyCode == KeyEvent.DOM_VK_CONTROL){ 66 | this.controlKeyEnabled = true; 67 | } 68 | // check if user is deleting the selection 69 | if (eventObject.keyCode == KeyEvent.DOM_VK_DELETE) { 70 | if (this.finalBox != null){ 71 | console.log("Deleting selection '"+this.finalBox+"'..."); 72 | this.canvas.clear(this.finalBox.min, this.finalBox.max); 73 | this.canvas.commit(); 74 | this.finalBox = null; 75 | return; 76 | } 77 | } 78 | } 79 | keyUp(eventObject){ 80 | // capture control key event 81 | if (eventObject.keyCode == KeyEvent.DOM_VK_CONTROL){ 82 | this.controlKeyEnabled = false; 83 | } 84 | } 85 | cellDown(coord){ 86 | this.startCoord = coord; 87 | this.startTime = this.currentTime = new Date().getTime(); 88 | this.mouseStatus = "down"; 89 | this.process(); 90 | } 91 | cellMove(coord){ 92 | this.currentCoord = coord; 93 | this.currentTime = new Date().getTime(); 94 | this.mouseStatus = this.mouseStatus == "down" || this.mouseStatus == "dragging"? "dragging" : "hover"; 95 | this.process(); 96 | } 97 | cellUp(coord){ 98 | this.currentCoord = coord; 99 | this.currentTime = new Date().getTime(); 100 | this.mouseStatus = "up"; 101 | this.process(); 102 | } 103 | mouseLeave(){ 104 | this.mouseStatus = "leave"; 105 | this.process(); 106 | } 107 | process(){ 108 | var coord = this.currentCoord; 109 | this.action = this.getNextAction(); 110 | switch(this.action){ 111 | case "select-area": 112 | case "select-area-in-progress": this.selectArea(coord); break; 113 | case "select-area-ready": this.selectArea(coord); break; 114 | case "select-area-moving": this.selectArea(coord); break; 115 | case "select-area-finalized": this.selectArea(coord); this.action = null; break; 116 | case "select-shape": 117 | case "select-shape-moving": this.selectShape(coord); break; 118 | case "select-shape-finalized": this.selectShape(coord); this.action = null; break; 119 | case "all": this.selectArea(coord); this.selectShape(coord); break; 120 | case "rollback": this.canvas.rollback(); this.action = null; 121 | } 122 | } 123 | getNextAction(){ 124 | if (this.mouseStatus == "hover") return this.action; 125 | if (this.mouseStatus == "down" && this.action == "select-area-ready") return "select-area-moving"; 126 | if (this.mouseStatus == "down") return "all"; 127 | if (this.mouseStatus == "dragging" && this.action == "select-area-moving") return "select-area-moving"; 128 | if (this.mouseStatus == "dragging" && this.action == "select-area-in-progress") return "select-area-in-progress"; 129 | if (this.mouseStatus == "dragging" && this.action == "select-shape-moving") return "select-shape-moving"; 130 | if (this.mouseStatus == "dragging" && this.controlKeyEnabled) return "select-area-in-progress"; 131 | if (this.mouseStatus == "dragging" && this.selectedBox != null) return "select-shape-moving"; 132 | if (this.mouseStatus == "dragging") return "select-area-in-progress"; 133 | if (this.mouseStatus == "up" && this.action == "select-shape-moving") return "select-shape-finalized"; 134 | if (this.mouseStatus == "up" && this.action == "select-area-in-progress") return "select-area-ready"; 135 | if (this.mouseStatus == "up" && this.action == "select-area-moving") return "select-area-finalized"; 136 | if (this.mouseStatus == "leave") return "rollback"; 137 | return undefined; 138 | } 139 | selectArea (coord){ 140 | // user finalized the selection 141 | if (this.mouseStatus == "up"){ 142 | // only do it once (user may click several times on the final selection) 143 | if (this.finalBox == null){ 144 | this.finalBox = this.selectionArea; 145 | this.selectionArea = null; 146 | } else if (this.finalMove != null){ 147 | // user thas completed moving the selection 148 | this.canvas.commit(); 149 | this.finalMove = null; 150 | this.finalBox = null; 151 | this.selectionArea = null; 152 | } 153 | return; 154 | } 155 | // user is selecting, either to start a new selection or to move selection 156 | if (this.mouseStatus == "down"){ 157 | // check if user is starting new selection 158 | if (this.finalBox == null || !this.finalBox.contains(coord)){ 159 | this.finalBox = null; 160 | this.selectionArea = null; 161 | this.canvas.rollback(); 162 | } 163 | // user is going to move the selection 164 | return; 165 | } 166 | 167 | if (this.mouseStatus == "dragging") { 168 | // user is moving selection 169 | if (this.finalBox != null && this.finalBox.contains(coord) || this.finalMove != null && this.finalMove.contains(coord)){ 170 | // move implementation 171 | this.canvas.rollback(); 172 | // calculate movement difference 173 | var diffCoord = coord.substract(this.startCoord); 174 | // move selected area 175 | this.canvas.moveArea(this.finalBox, diffCoord); 176 | // update final box 177 | this.finalMove = this.finalBox.add(diffCoord); 178 | return; 179 | } 180 | // user is selecting... 181 | // check we are selecting at least 1 pixel 182 | if (this.startCoord == null || this.startCoord.equals(coord)){ 183 | if (this.selectionArea != null){ 184 | this.selectionArea = null; 185 | this.canvas.rollback(); 186 | } 187 | return; 188 | } 189 | // calculate box so we know from where to where we should draw the line 190 | this.canvas.rollback(); 191 | this.selectionArea = new Box(this.startCoord, coord); 192 | 193 | // stack non-empty pixel within the selected square 194 | for (var minX = this.selectionArea.minX; minX <= this.selectionArea.maxX; minX++) { 195 | for (var minY = this.selectionArea.minY; minY <= this.selectionArea.maxY; minY++) { 196 | var pixelCoord = new Coord(minX, minY); 197 | var pixelValue = this.canvas.getPixel(pixelCoord).getValue(); 198 | this.canvas.stackPixel(pixelCoord, pixelValue != null? pixelValue : " "); 199 | } 200 | } 201 | } 202 | this.changed = true; 203 | } 204 | selectShape (coord){ 205 | if (this.mouseStatus == "down") { 206 | this.endPointsInfo = this.canvas.detectEndPoints(this.startCoord); 207 | this.selectedBox = this.detectBox(coord); 208 | this.detectConnectedBoxes(this.selectedBox); 209 | return; 210 | } 211 | if (this.action != "select-shape" && this.action != "select-shape-moving" && this.action != "select-shape-finalized") return; 212 | if (this.mouseStatus == "leave") { 213 | this.endPointsInfo = null; 214 | this.selectedBox = null; 215 | this.canvas.rollback(); 216 | return; 217 | } 218 | if (this.mouseStatus == "up") { 219 | this.canvas.commit(); 220 | return 221 | }; 222 | // box not detected 223 | if (this.selectedBox == null) return; 224 | // move box 225 | this.canvas.rollback(); 226 | this.drawConnections(this.selectedBox,zeroCoord,""); 227 | this.canvas.moveArea(this.selectedBox.box,coord.substract(this.startCoord)); 228 | this.drawConnections(this.selectedBox,coord.substract(this.startCoord),"-"); 229 | this.changed = true; 230 | } 231 | detectBox(coord){ 232 | var boxPoints = this.canvas.detectBox(coord); 233 | if (boxPoints == null) return null; 234 | var box = this.canvas.getBox(boxPoints); 235 | var endPoints = this.canvas.getEndPoints(boxPoints); 236 | // for (var point in boxPoints){ this.canvas.stackPixel(boxPoints[point],'-'); } // debug 237 | // this.canvas.stackPixel(box.min,'+'); this.canvas.stackPixel(box.max,'+'); // debug 238 | // for (var point in endPoints){ this.canvas.stackPixel(endPoints[point],'+'); } // debug 239 | return new BoxInfo(boxPoints, box, endPoints); 240 | } 241 | detectConnectedBoxes(boxInfo){ 242 | if (boxInfo == null || boxInfo.connectors.length == 0) return; // no T or + connections 243 | var connectors = boxInfo.connectors; 244 | var connections = boxInfo.connections; 245 | for (var i in connectors){ 246 | var start = connectors[i]; 247 | for (var j in contextCoords){ 248 | var dir = contextCoords[j]; 249 | var next = start.add(dir); 250 | if (boxInfo.box.contains(next)) continue; 251 | var linePoints = this.canvas.getLinePoints(start, dir) 252 | /*for (var k in linePoints){ 253 | this.canvas.stackPixel(linePoints[k],'?'); 254 | }*/ 255 | if (linePoints == null) continue; // error somewhere! 256 | connections.push(new Connection(linePoints)); 257 | } 258 | } 259 | } 260 | detectBox_with_endPoints(coord){ 261 | // detect corner endpoints & connection endpoints 262 | var cornerEndPoints = []; 263 | var connectionEndPoints = []; 264 | if (this.endPointsInfo.length >= 2){ 265 | for (var endPointIdx in endPointsInfo){ 266 | var endPointInfo = endPointsInfo[endPointIdx]; 267 | if (endPointInfo.isCorner()){ 268 | cornerEndPoints.push(endPointInfo); 269 | } else if (endPointInfo.context.length >= 3){ 270 | connectionEndPoints.push(endPointInfo); 271 | } 272 | if (endPointInfo.childEndpoints.length > 0){ 273 | for (var endPointIdx2 in endPointInfo.childEndpoints){ 274 | var endPointInfo2 = endPointInfo.childEndpoints[endPointIdx2]; 275 | if (endPointInfo2.context.length >= 3){ 276 | connectionEndPoints.push(endPointInfo2); 277 | } 278 | } 279 | } 280 | } 281 | } 282 | // we need at least 2 corners to start detecting the box 283 | if (cornerEndPoints.length < 2) return; 284 | var epCorner1 = cornerEndPoints[0], epCorner2 = cornerEndPoints[1]; 285 | // get child corners 286 | var childCorners1 = epCorner1.getCorners(), childCorners2 = epCorner2.getCorners(); 287 | if (!childCorners1 || !childCorners2 || childCorners1.length == 0 || childCorners2.length == 0) return; 288 | // we need both childs to be in the same axis 289 | if (!childCorners1[0].position.hasSameAxis(childCorners2[0].position)) return; 290 | // test whether the clicked coord its in a corner 291 | var startCoordIsCorner = epCorner1.isHorizontal != epCorner2.isHorizontal; 292 | // test if both childs are the opposite corner 293 | if (startCoordIsCorner && childCorners1[0].position.equals(childCorners2[0].position)){ 294 | console.log("Detected box type 1"); 295 | return new BoxInfo(new Box(null, this.startCoord,childCorners1[0].position),connectionEndPoints); 296 | } 297 | // test whether the user click on a side of the box 298 | if (!startCoordIsCorner && this.canvas.isDrawCharArea(new Box(childCorners1[0].position,childCorners2[0].position))){ 299 | console.log("Detected box type 2"); 300 | return new BoxInfo(new Box(null, epCorner1.position, childCorners2[0].position),connectionEndPoints); 301 | } 302 | return null; 303 | } 304 | detectConnectedBoxes_with_endpoints(boxInfo){ 305 | if (boxInfo == null) return null; 306 | var connectorEndPoints = boxInfo.connectors; 307 | var possibleBoxEndpoints = []; 308 | for (var endPointIdx in connectorEndPoints){ 309 | var endPoint = connectorEndPoints[endPointIdx]; 310 | // TODO: handle tables 311 | if (endPoint.context.length == 3){ 312 | var tDirection = this.getTDirection(endPoint.context); 313 | var possibleBoxConnection = this.canvas.getLinePoints(endPoint.position, tDirection); 314 | if (possibleBoxConnection != null){ 315 | boxInfo.connections.push(new Connection(possibleBoxConnection)); 316 | } 317 | } 318 | } 319 | } 320 | getTDirection(pixelContext){ 321 | if (pixelContext.length != 3) throw new Error("This is not a T connected pixel"); 322 | if (!pixelContext.left) return rightCoord; 323 | if (!pixelContext.right) return leftCoord; 324 | if (!pixelContext.top) return bottomCoord; 325 | if (!pixelContext.bottom) return topCoord; 326 | } 327 | drawConnections(boxInfo,coordDiff,value){ 328 | if (boxInfo == null || boxInfo.connections.length == 0) return; 329 | for (var i in boxInfo.connections){ 330 | var connection = boxInfo.connections[i]; 331 | var horizontalLength = connection.getDirection().add(rightCoord).getLength(); 332 | var horizontalLength2 = connection.getEndDirection().add(rightCoord).getLength(); 333 | var dir = horizontalLength == 0 || Math.abs(horizontalLength) >= 2; 334 | var dir2 = horizontalLength2 == 0 || Math.abs(horizontalLength2) >= 2; 335 | var lineType = null; 336 | if (dir && dir2) lineType = "horizontal-horizontal"; 337 | if (!dir && !dir2) lineType = "vertical-vertical"; 338 | if (dir && !dir2) lineType = "horizontal-vertical"; 339 | if (!dir && dir2) lineType = "vertical-horizontal"; 340 | this.canvas.drawLine(connection.points[0].add(coordDiff), connection.points[connection.points.length-1], lineType, value); 341 | } 342 | } 343 | } 344 | 345 | // ------------------------------------------------- TOOLS DECORATORS ---------------------------------------------- // 346 | 347 | class ClearCanvasTool extends CanvasTool { 348 | constructor(toolId, canvas){ 349 | super(toolId); 350 | this.canvas = canvas; 351 | } 352 | click(){ 353 | this.canvas.clear(); 354 | this.canvas.commit(); 355 | } 356 | } 357 | 358 | class EditTextTool extends CanvasTool { 359 | constructor(toolId,canvas){ 360 | super(toolId); 361 | this.canvas = canvas; 362 | this.mouseCoord = null; 363 | this.startCoord = null; 364 | this.currentText = null; 365 | this.init(); 366 | } 367 | init(){ 368 | $("#text-input").keyup(function(event) { 369 | if (event.keyCode == KeyEvent.DOM_VK_ESCAPE){ 370 | this.close(); 371 | return; 372 | } 373 | this.refresh(); 374 | }.bind(this)); 375 | $("#text-input").keypress(function(eventObject) { 376 | this.refresh(); 377 | }.bind(this)); 378 | $("#text-input").change(function() { 379 | this.refresh(); 380 | }.bind(this)); 381 | $("#text-input").blur(function() { 382 | // TODO: close on blur, but count that ok button is also trigerring blur 383 | // this.close(); 384 | }.bind(this)); 385 | $("#text-input-close").click(function() { 386 | this.close(); 387 | }.bind(this)); 388 | $("#text-input-OK").click(function() { 389 | this.refresh(); 390 | this.canvas.getGrid().commit(); 391 | this.close(); 392 | }.bind(this)); 393 | } 394 | mouseDown(eventObject) { 395 | this.mouseCoord = eventObject; 396 | } 397 | cellDown(startCoord) { 398 | // guess where the text exactly starts 399 | this.startCoord = this.canvas.getTextStart(startCoord); 400 | // show widget 50 pixels up 401 | $("#text-widget").css({"left":this.mouseCoord.clientX,"top":Math.max(0,this.mouseCoord.clientY-50)}); 402 | // get current text 403 | this.currentText = this.canvas.getText(this.startCoord); 404 | // initialize widget 405 | $("#text-input").val(this.currentText != null? this.currentText : ""); 406 | // show widget & set focus 407 | $("#text-widget").show(400, function() { 408 | $("#text-input").focus(); 409 | }); 410 | } 411 | refresh() { 412 | var newValue = $("#text-input").val(); 413 | this.canvas.rollback(); 414 | if (this.currentText != null){ 415 | this.canvas.getGrid().import(this.currentText.replace(/./g," "),this.startCoord); 416 | } 417 | try{ 418 | this.canvas.getGrid().import(newValue,this.startCoord); 419 | }catch(e){ 420 | console.log(e.stack); 421 | } 422 | this.canvas.setChanged(true); 423 | } 424 | close() { 425 | $("#text-input").val(""); 426 | $("#text-widget").hide(); 427 | this.canvas.getGrid().rollback(); 428 | } 429 | cursor() { 430 | return "text"; 431 | } 432 | } 433 | 434 | class EndPointInfo { 435 | constructor(position,context,isHorizontal,startWithArrow, endWithArrow, endWithArrow2){ 436 | this.class = "EndPointInfo"; 437 | this.position = position; 438 | this.context = context; 439 | this.isHorizontal = isHorizontal; 440 | this.startWithArrow = startWithArrow; 441 | this.endWithArrow = endWithArrow; 442 | this.endWithArrow2 = endWithArrow2; 443 | this.childEndpoints = null; 444 | } 445 | isCorner(){ 446 | return this.context.length == 2 && this.context.bottom != this.context.top && this.context.left != this.context.rigth; 447 | } 448 | getCorners(){ 449 | if (this.childEndpoints == null) return null; 450 | var cornerEndPoints = []; 451 | for (var endPointIdx in this.childEndpoints){ 452 | if (this.childEndpoints[endPointIdx].isCorner()){ 453 | cornerEndPoints.push(this.childEndpoints[endPointIdx]); 454 | } 455 | } 456 | return cornerEndPoints; 457 | } 458 | toString(){ 459 | return "EndPointInfo: position '"+this.position+"', context '"+this.context+"', isHorizontal '"+this.isHorizontal 460 | +"', startWithArrow '"+this.startWithArrow+"', endWithArrow '"+this.endWithArrow+"', endWithArrow2 '"+this.endWithArrow2 461 | +"', childEndpoints '"+this.childEndpoints+"'"; 462 | } 463 | } 464 | 465 | class BoxInfo { 466 | constructor(points, box,connectors){ 467 | this.points = points; 468 | this.box = box; 469 | this.connectors = connectors; 470 | this.connections = []; 471 | } 472 | } 473 | 474 | class Connection { 475 | constructor(points){ 476 | this.points = points; 477 | } 478 | getDirection(){ 479 | return this.points[1].substract(this.points[0]); 480 | } 481 | getEndDirection(){ 482 | return this.points[this.points.length-1].substract(this.points[this.points.length-2]); 483 | } 484 | } 485 | 486 | /** 487 | * This is the function to draw boxes. Basically it needs 2 coordinates: startCoord and endCoord. 488 | */ 489 | class BoxDrawerTool extends CanvasTool { 490 | constructor(toolId,canvas) { 491 | super(toolId); 492 | this.canvas = canvas; 493 | this.startCoord = null; 494 | this.endCoord = null; 495 | this.mouseStatus = null; 496 | this.mode = null; 497 | this.endPointsInfo = null; 498 | } 499 | cellDown(coord) { 500 | this.mouseStatus = "down"; 501 | this.startCoord = coord; 502 | this.endPointsInfo = this.canvas.detectEndPoints(coord); 503 | } 504 | cellMove(coord) { 505 | // reset previous resizing data 506 | this.canvas.rollback(); 507 | 508 | if (this.startCoord == null) { 509 | if (this.mouseStatus == null && this.mode == null){ 510 | if (this.canvas.isDrawChar(this.canvas.getPixel(this.canvas.getPointerCell()))){ 511 | this.endPointsInfo = this.canvas.detectEndPoints(coord); 512 | if (this.endPointsInfo != null){ 513 | if (this.endPointsInfo.length == 2 && this.endPointsInfo[0].context.length == 1 && this.endPointsInfo[1].context.length == 1 514 | && this.endPointsInfo[0].position.hasSameAxis(this.endPointsInfo[1].position)){ 515 | var ep1 = this.endPointsInfo[0], ep2 = this.endPointsInfo[1]; 516 | console.log("Highlighting line from '"+ep1.position+"' to '"+ep2.position+"'..."); 517 | this.canvas.drawLine(ep1.position, ep2.position, ep1.isHorizontal, "+", true); 518 | } 519 | } 520 | } 521 | } 522 | return; 523 | }; 524 | 525 | this.endCoord = coord; 526 | 527 | // update mouse status 528 | if (this.mouseStatus == "down"){ 529 | this.mouseStatus = "moving"; 530 | } 531 | else if (this.mouseStatus == "up"){ 532 | this.mouseStatus = "hover"; 533 | } 534 | 535 | // guess action 536 | if (this.mouseStatus == "moving"){ 537 | if (this.mode == null){ 538 | if (this.canvas.isDrawChar(this.canvas.getPixel(this.canvas.getSelectedCell()))){ 539 | this.mode = "resizing"; 540 | } else { 541 | this.mode = "boxing"; 542 | } 543 | } 544 | } else { 545 | this.mode = null; 546 | } 547 | 548 | // check whether the user is drawing a box or resizing it 549 | if (this.mode == "resizing" && this.endPointsInfo != null){ 550 | // debug 551 | // console.log("Resizing..."+ this.endPointsInfo.length); 552 | 553 | // what we are doing? 554 | var action = null; 555 | // detect whether we are moving a line or doing something else 556 | if (this.endPointsInfo.length == 2 && this.endPointsInfo[0].context.length == 1 && this.endPointsInfo[1].context.length == 1 557 | && this.endPointsInfo[0].position.hasSameAxis(this.endPointsInfo[1].position)){ 558 | action = "moving-line"; 559 | } else if (this.endPointsInfo.length == 2 && this.endPointsInfo[0].context.length == 2 && this.endPointsInfo[1].context.length == 2 560 | && this.endPointsInfo[0].childEndpoints.length > 0 && this.endPointsInfo[1].childEndpoints.length > 0 561 | && this.endPointsInfo[0].childEndpoints[0].position.hasSameAxis(this.endPointsInfo[1].childEndpoints[0].position)){ 562 | if (this.endPointsInfo[0].childEndpoints[0].position.equals(this.endPointsInfo[1].childEndpoints[0].position)){ 563 | action = "resizing-box"; 564 | } else{ 565 | action = "resizing-side"; 566 | } 567 | } 568 | 569 | if (action == "moving-line"){ 570 | console.log("Moving line from '"+this.startCoord+"' to '"+coord.substract(this.startCoord)+"'..."); 571 | var ep1 = this.endPointsInfo[0], ep2 = this.endPointsInfo[1]; 572 | this.canvas.drawLine(ep1.position, ep2.position, ep1.isHorizontal, "", true); 573 | this.canvas.drawLine(ep1.position.add(coord.substract(this.startCoord)), ep2.position.add(coord.substract(this.startCoord)), ep1.isHorizontal, "-", false); 574 | // this.canvas.moveArea(new Box(ep1.position,ep2.position), coord.substract(this.startCoord)); 575 | } else if (action == "resizing-side"){ 576 | // delete the lines we are resizing ("" so its no drawn as uncommited change) 577 | this.canvas.drawLine(this.startCoord, this.endPointsInfo[0].childEndpoints[0].position, this.endPointsInfo[0].isHorizontal, "", true); 578 | this.canvas.drawLine(this.startCoord, this.endPointsInfo[1].childEndpoints[0].position, this.endPointsInfo[1].isHorizontal, "", true); 579 | // draw lines at new position, displacing only over 1 coordinate if moving a side 580 | var sideCoord = this.endPointsInfo[0].isHorizontal? new Coord(this.startCoord.x, coord.y) : new Coord(coord.x, this.startCoord.y); 581 | this.canvas.drawLine(sideCoord, this.endPointsInfo[0].childEndpoints[0].position, this.endPointsInfo[0].isHorizontal, "+", false); 582 | this.canvas.drawLine(sideCoord, this.endPointsInfo[1].childEndpoints[0].position, this.endPointsInfo[1].isHorizontal, "+", false); 583 | } else if (action == "resizing-box"){ 584 | // delete the lines we are resizing ("" so its no drawn as uncommited change) 585 | this.canvas.drawLine(this.startCoord, this.endPointsInfo[0].childEndpoints[0].position, this.endPointsInfo[0].isHorizontal, "", true); 586 | this.canvas.drawLine(this.startCoord, this.endPointsInfo[1].childEndpoints[0].position, this.endPointsInfo[1].isHorizontal, "", true); 587 | // draw lines at new position 588 | this.canvas.drawLine(coord, this.endPointsInfo[0].childEndpoints[0].position, this.endPointsInfo[0].isHorizontal, "+", false); 589 | this.canvas.drawLine(coord, this.endPointsInfo[1].childEndpoints[0].position, this.endPointsInfo[1].isHorizontal, "+", false); 590 | 591 | // draw arrows in case 592 | /*for (endPointIdx in endPointsInfo) { 593 | if (this.endPointsInfo[endPointIdx].startWithArrow) this.canvas.stackPixel(coord, "^"); 594 | if (this.endPointsInfo[endPointIdx].endWithArrow) this.canvas.stackPixel(this.endPointsInfo[endPointIdx].position, "^"); 595 | this.endPointsInfo[endPointIdx].endWithArrow2 && this.canvas.stackPixel(new Coord(this.endPointsInfo[endPointIdx].isHorizontal ? 596 | this.endPointsInfo[endPointIdx].position.x : coord.x, this.endPointsInfo[endPointIdx].isHorizontal ? coord.y : this.endPointsInfo[endPointIdx].position.y), "^"); 597 | }*/ 598 | } 599 | this.canvas.setChanged(true); 600 | } else if (this.mode == "boxing"){ 601 | // reset stack so we start drawing box every time the user moves the mouse 602 | this.canvas.getGrid().rollback(); 603 | // draw horizontal line first, then vertical line 604 | this.canvas.drawLine(this.startCoord, coord, true, '+', false); 605 | // draw vertical line first, then horizontal line 606 | this.canvas.drawLine(this.startCoord, coord, false, '+', false); 607 | // update canvas 608 | this.canvas.setChanged(true) 609 | } 610 | } 611 | cellUp(coord) { 612 | // When the user releases the mouse, we know the second coordinate so we draw the box 613 | this.startCoord = null; 614 | 615 | if (this.mode == "boxing"){ 616 | // user has the mouse-up (normal situation) 617 | } else if (this.mode == "resizing"){ 618 | // user has finished resizing 619 | } else{ 620 | // if user is leaving the canvas, reset stack 621 | this.canvas.getGrid().rollback(); 622 | } 623 | // perform changes 624 | this.canvas.getGrid().commit(); 625 | 626 | // update status 627 | this.mouseStatus = null; 628 | this.mode = null; 629 | 630 | // update canvas 631 | this.canvas.setChanged(true); 632 | } 633 | mouseLeave() { 634 | // If the mouse leaves the canvas, we dont want to draw nothing 635 | this.canvas.getGrid().rollback(); 636 | this.mouseStatus = "out"; 637 | } 638 | cursor() { 639 | return "crosshair"; 640 | } 641 | } 642 | 643 | class LineTool extends CanvasTool { 644 | constructor(toolId, canvas){ 645 | super(toolId); 646 | this.canvas = canvas; 647 | this.startCoord = null; 648 | this.mouseStatus = null; 649 | } 650 | cellDown(coord) { 651 | this.mouseStatus = "down"; 652 | this.startCoord = coord; 653 | } 654 | cellMove(coord) { 655 | if (this.mouseStatus == "down"){ 656 | this.canvas.rollback(); 657 | this.canvas.drawLine(this.startCoord, coord, "best", "-"); 658 | } 659 | } 660 | cellUp(){ 661 | // perform changes 662 | this.canvas.getGrid().commit(); 663 | // update status 664 | this.mouseStatus = "up"; 665 | // update canvas 666 | this.canvas.setChanged(true); 667 | } 668 | } 669 | 670 | /** 671 | * This tool allows exporting the grid text so user can copy/paste from there 672 | */ 673 | class ExportASCIITool extends CanvasTool { 674 | constructor(toolId, canvas, canvasWidgetSelectorId, widgetSelectorId){ 675 | super(toolId); 676 | this.canvas = canvas; 677 | this.toolId = toolId; 678 | this.canvasWidget = $(canvasWidgetSelectorId); 679 | this.exportWidget = $(widgetSelectorId); 680 | this.mode = 0; 681 | this.init(); 682 | } 683 | init(){ 684 | $(this.widget).hide(); 685 | $("#dialog-textarea").keyup(function(event) { 686 | if (event.keyCode == KeyEvent.DOM_VK_ESCAPE){ 687 | if (this.mode == 1){ 688 | this.close(); 689 | return; 690 | } 691 | return; 692 | } 693 | }.bind(this)); 694 | /*$("#dialog-widget-close").click(function() { 695 | this.close(); 696 | }.bind(this));*/ 697 | } 698 | click(){ 699 | if (this.mode == 1){ 700 | this.close(); 701 | return; 702 | } 703 | $("#dialog-textarea").val(this.canvas.getGrid().export()); 704 | $(this.canvasWidget).hide(); 705 | this.mode = 1; 706 | $(this.exportWidget).show(); 707 | /*$("#dialog-textarea").focus(function(){ 708 | var $this = $(this); 709 | $this.select(); 710 | });*/ 711 | } 712 | close() { 713 | $(this.exportWidget).hide(); 714 | $(this.canvasWidget).show(); 715 | $("#dialog-textarea").val(""); 716 | this.mode = 0; 717 | } 718 | } 719 | -------------------------------------------------------------------------------- /src/js/entities/box.js: -------------------------------------------------------------------------------- 1 | //--------------------------------------------- DRAW CLASSES ------------------------------------------------------- // 2 | 3 | /** 4 | * Calculates the mins and max given 2 coordinates 5 | */ 6 | function Box(coordA, coordB) { 7 | this.class = 'Box'; 8 | this.minX = Math.min(coordA.x, coordB.x); 9 | this.minY = Math.min(coordA.y, coordB.y); 10 | this.maxX = Math.max(coordA.x, coordB.x); 11 | this.maxY = Math.max(coordA.y, coordB.y); 12 | this.min = new Coord(this.minX, this.minY); 13 | this.max = new Coord(this.maxX, this.maxY); 14 | this.midX = Math.floor((this.maxX + this.minX) / 2) 15 | this.midY = Math.floor((this.maxY + this.minY) / 2) 16 | this.mid = new Coord(this.midX, this.midY); 17 | } 18 | 19 | Box.prototype = { 20 | contains : function(coord){ 21 | return coord && coord.x >= this.minX && coord.x <= this.maxX && coord.y >= this.minY && coord.y <= this.maxY; 22 | } 23 | , add : function(coord){ 24 | return new Box(this.min.add(coord),this.max.add(coord)); 25 | } 26 | , squareSize : function(){ 27 | // +1 because boxes include bounds 28 | return (this.maxX-this.minX+1)*(this.maxY-this.minY+1); 29 | } 30 | , toString : function(){ 31 | return "Box: ('"+this.min+"'->'"+this.max+"', mid:"+this.mid+")"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/js/entities/pixel-context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Encapsulates data for the surrounding pixels 3 | */ 4 | class PixelContext { 5 | constructor(left, right, top, bottom){ 6 | this.class = 'PixelContext'; 7 | this.left = left; 8 | this.right = right; 9 | this.bottom = bottom; 10 | this.top = top; 11 | this.length = this.left + this.right + this.bottom + this.top; 12 | } 13 | getLength() { 14 | return this.length; 15 | } 16 | toString() { 17 | return "PixelContext["+this.left+","+this.right+","+this.bottom+","+this.top+"]"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/js/model/constants.js: -------------------------------------------------------------------------------- 1 | 2 | function init_constants(){ 3 | 4 | // Default font for drawing the ASCII pixels 5 | window.defaultFont = "10px Courier New"; 6 | // Default canvas zoom. Canvas can be zoomed from 1x (not zoomed) to 4x (zoomed) 7 | window.defaultZoom = 1; 8 | 9 | 10 | // Number of rows for the grid 11 | window.defaultNumberOfRows = 100; 12 | // Number of cols for the grid 13 | window.defaultNumberOfCols = 200; 14 | 15 | 16 | // list of characters we use for drawing boxes 17 | window.boxChars1 = UC.BOX_CHARS; 18 | window.boxChars1.push(UC.LATIN_PLUS_SIGN); 19 | window.boxChars1.push(UC.LATIN_HYPHEN_MINUS); 20 | window.boxChars1.push(UC.LATIN_VERTICAL_LINE); 21 | 22 | // list of characters we use for drawing arrows 23 | window.arrowChars1 = [">", "<", "^", "v"]; 24 | 25 | // Draw styles 26 | window.drawStyles = {}; 27 | window.drawStyles["0"] = {"horizontal":"-", "vertical":"|", "corner":"+", "cross":"+"}; 28 | window.drawStyles["1"] = {"horizontal":UC.BOX_HORIZONTAL, "vertical":UC.BOX_VERTICAL, "corner":UC.BOX_CROSS, "cross":UC.BOX_CROSS, 29 | "corner-top-left":UC.BOX_CORNER_TOP_LEFT, "corner-top-right":UC.BOX_CORNER_TOP_RIGHT, "corner-bottom-left":UC.BOX_CORNER_BOTTOM_LEFT, "corner-bottom-right":UC.BOX_CORNER_BOTTOM_RIGHT, 30 | "horizontal-light-up":UC.BOX_HORIZONTAL_LIGHT_UP, "horizontal-light-down":UC.BOX_HORIZONTAL_LIGHT_DOWN, 31 | "vertical-light-right":UC.BOX_VERTICAL_LIGHT_RIGHT, "vertical-light-left":UC.BOX_VERTICAL_LIGHT_LEFT 32 | }; 33 | 34 | // ascii|latin-1-supplement|lat-extended-a|latin-extended-b|arrows|box-drawing| 35 | window.printableCharsRegex = /[ -~]|[¡-ÿ]|[Ā-ſ]|[ƀ-ɏ]|[←-⇿]|[─-╿]/iu; 36 | 37 | 38 | 39 | // Basic constants 40 | window.zeroCoord = new Coord(0,0), leftCoord = new Coord(-1, 0), rightCoord = new Coord(1, 0), topCoord = new Coord(0, -1), bottomCoord = new Coord(0, 1); 41 | window.contextCoords = [leftCoord, rightCoord, topCoord, bottomCoord]; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/js/model/coord-pixel.js: -------------------------------------------------------------------------------- 1 | //---------------------------------------------- PIXEL POSITION CLASS -----------------------------------------------// 2 | 3 | function PixelPosition(coord, pixel) { 4 | this.coord = coord; 5 | this.pixel = pixel; 6 | } 7 | -------------------------------------------------------------------------------- /src/js/model/coord.js: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------- COORD CLASS ---------------------------------------------------// 2 | 3 | /** 4 | * A simple pair of coordinates x,y for to use to locate any pixel 5 | */ 6 | function Coord(x, y) { 7 | this.class = 'Coord'; 8 | this.x = x; 9 | this.y = y; 10 | } 11 | 12 | Coord.prototype = { 13 | toString : function() { 14 | return "Coord["+this.x+","+this.y+"]"; 15 | } 16 | , add : function(other) { 17 | return new Coord(this.x + other.x, this.y + other.y); 18 | } 19 | , equals : function(other){ 20 | return this.x == other.x && this.y == other.y; 21 | } 22 | , substract : function(other){ 23 | return new Coord(this.x - other.x, this.y - other.y); 24 | } 25 | , getLength : function() { 26 | return Math.sqrt(this.x * this.x + this.y * this.y); 27 | } 28 | , clone : function() { 29 | return new Coord(this.x, this.y); 30 | } 31 | , hasSameAxis : function(other) { 32 | return this.x == other.x || this.y == other.y; 33 | } 34 | , isOppositeDir: function(other){ 35 | return this.add(other).getLength() == 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/js/model/grid.js: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------- GRID CLASS -----------------------------------------------------// 2 | 3 | /** 4 | * This class holds the pixels matrix and a stack for faster access to pixels being modified 5 | * The matrix is an array of cols x rows (x, y) 6 | */ 7 | function Grid() { 8 | this.class = 'Grid'; 9 | this.cols = defaultNumberOfCols; 10 | this.rows = defaultNumberOfRows; 11 | this.matrix = Array(this.cols); 12 | this.pixelsStack = []; 13 | this.changed = true; 14 | this.init(); 15 | } 16 | 17 | /** 18 | * Initialize grid and restore previous user data if available 19 | */ 20 | Grid.prototype = { 21 | init : function(){ 22 | for (var a = 0;a < this.matrix.length;a++) { 23 | this.matrix[a] = Array(this.rows); 24 | for (var b = 0;b < this.matrix[a].length;b++) { 25 | this.matrix[a][b] = new Pixel; 26 | } 27 | } 28 | if(typeof(Storage) !== "undefined") { 29 | previousData = localStorage.getItem("data"); 30 | if (previousData != null){ 31 | this.import(previousData, new Coord(0,0), true, true); 32 | this.commit(); 33 | } 34 | } 35 | } 36 | , isOutOfBounds : function(coord){ 37 | if (coord.x < 0 || coord.x >= this.cols){ 38 | return true; 39 | } 40 | if (coord.y < 0 || coord.y >= this.rows){ 41 | return true; 42 | } 43 | return false; 44 | } 45 | /** 46 | * Return the pixel located at the specified coord 47 | */ 48 | , getPixel : function(coord) { 49 | if (this.isOutOfBounds(coord)){ 50 | return undefined; 51 | } 52 | return this.matrix[coord.x][coord.y]; 53 | } 54 | /** 55 | * Clears/reset the whole matrix of pixels 56 | */ 57 | , clear : function(start,final) { 58 | console.log("Clearing grid from '"+start+"' to '"+final+"'..."); 59 | startRow = start? start.x : 0; 60 | finalRow = final? final.x : this.matrix.length -1; 61 | startCol = start? start.y : 0; 62 | finalCol = final? final.y : this.matrix[0].length -1 63 | for (var row = startRow; row <= finalRow; row++) { 64 | for (var col = startCol; col <= finalCol; col++) { 65 | if (this.isOutOfBounds(new Coord(row,col))) continue; 66 | this.matrix[row][col].clear(); 67 | } 68 | } 69 | this.changed = true; 70 | } 71 | , stackPixel : function(coord, value) { 72 | if (this.isOutOfBounds(coord)) return; 73 | if (value != null && value != "" && !printableCharsRegex.test(value)) throw new Error("Char non recognized ["+value.charCodeAt(0)+"]"); 74 | var pixel = this.getPixel(coord); 75 | this.pixelsStack.push(new PixelPosition(coord, pixel)); 76 | pixel.tempValue = value; 77 | this.changed = true; 78 | } 79 | , stackArea : function(area, value) { 80 | for (minX = area.minX; minX <= area.maxX; minX++) { 81 | for (minY = area.minY; minY <= area.maxY; minY++) { 82 | // get pixel we are moving 83 | pixelCoord = new Coord(minX, minY); 84 | pixelValue = this.getPixel(pixelCoord).getValue(); 85 | this.stackPixel(pixelCoord, " "); 86 | } 87 | } 88 | } 89 | , savePixel : function(coord, value) { 90 | if (this.getPixel(coord).getValue() != value){ 91 | this.stackPixel(coord, value); 92 | } 93 | } 94 | /** 95 | * Clears the stack so we have no temporary pixels to be drawn 96 | */ 97 | , rollback : function() { 98 | // console.log("rollback"); 99 | for (var b in this.pixelsStack) { 100 | this.pixelsStack[b].pixel.tempValue = null; 101 | } 102 | this.pixelsStack.length = 0; 103 | this.changed = true; 104 | } 105 | /** 106 | * Imports the specified text into the specified coordinates. The text can be multiline. 107 | * All the whitespace characters will be replaced for nulls and it means we want to delete the pixel 108 | */ 109 | , import : function(text, coord, ommitBlanks, ommitUnrecognized) { 110 | lines = text.split("\n"); 111 | for (e = 0;e < lines.length;e++) { 112 | for (var g = lines[e], l = 0;l < g.length;l++) { 113 | var h = g.charAt(l); 114 | if (ommitBlanks && (h == "" || h == " ")) continue; 115 | try{ 116 | this.stackPixel(new Coord(l,e).add(coord), h); 117 | }catch(e){ 118 | if (ommitUnrecognized) continue; 119 | throw e; 120 | } 121 | } 122 | } 123 | } 124 | , moveArea : function(area, diff) { 125 | // stack the area we are moving 126 | this.stackArea(area); 127 | // move the area to new position 128 | for (minX = area.minX; minX <= area.maxX; minX++) { 129 | for (minY = area.minY; minY <= area.maxY; minY++) { 130 | // get pixel we are moving 131 | pixelCoord = new Coord(minX, minY); 132 | // get current pixel value 133 | pixelValue = this.getPixel(pixelCoord).value; 134 | // get pixel we are overwriting 135 | pixelCoord2 = pixelCoord.add(diff); 136 | // check if pixel is inside canvas 137 | if (this.isOutOfBounds(pixelCoord2)) continue; 138 | // get pixel value we are overwriting 139 | pixelValue2 = this.getPixel(pixelCoord2).getValue(); 140 | // stack the pixel we are overwriting 141 | this.stackPixel(pixelCoord2, pixelValue != null? pixelValue : pixelValue2 != null && pixelValue2 != ""? pixelValue2 : " "); 142 | } 143 | } 144 | } 145 | , export : function(){ 146 | var data = ""; 147 | for (row=0; row ' + JSON.stringify(result)); 295 | return result; 296 | }; 297 | } 298 | }; 299 | return new Proxy(target,proxyHandler); 300 | } -------------------------------------------------------------------------------- /src/js/view/canvas-zoom.js: -------------------------------------------------------------------------------- 1 | // -------------------------------------------- ZOOMABLE CANVAS ---------------------------------------------------- // 2 | 3 | function ZoomableCanvas(canvas){ 4 | this.class = "ZoomableCanvas"; 5 | this.canvas = canvas; 6 | this.shiftKeyEnabled = false; 7 | } 8 | 9 | ZoomableCanvas.prototype = { 10 | init : function(){ 11 | $(this.canvas.getCanvasHTML()).bind("mousewheel", this.onMouseWheel.bind(this)); 12 | }, 13 | keyDown : function(eventObject){ 14 | this.canvas.keyDown(eventObject); 15 | if (eventObject.keyCode == KeyEvent.DOM_VK_SHIFT) { 16 | this.shiftKeyEnabled = true; 17 | } 18 | } 19 | , keyUp: function(eventObject){ 20 | this.canvas.keyUp(eventObject); 21 | if (eventObject.keyCode == KeyEvent.DOM_VK_SHIFT) { 22 | this.shiftKeyEnabled = false; 23 | } 24 | } 25 | , onMouseWheel: function(eventObject) { 26 | if (!this.shiftKeyEnabled) return; 27 | var newZoom = this.canvas.getZoom() * (eventObject.originalEvent.wheelDelta > 0 ? 1.1 : 0.9); 28 | newZoom = Math.max(Math.min(newZoom, 4), 1); 29 | this.canvas.setZoom(newZoom); 30 | this.canvas.resize(); 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/js/view/canvas.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------- CANVAS CLASS ----------------------------------------------------// 2 | 3 | function ASCIICanvas(htmlCanvas, grid) { 4 | this.class = 'ASCIICanvas'; 5 | this.canvasHTML = htmlCanvas; 6 | this.canvasContext = this.canvasHTML.getContext("2d"); 7 | this.grid = grid; 8 | this.font = defaultFont; 9 | this.cellWidth = null; 10 | this.cellHeight = null; 11 | this.cellDescend = null; 12 | this.zoom = defaultZoom; 13 | this.changed = true; 14 | this.linesMode = false; 15 | } 16 | 17 | ASCIICanvas.prototype = { 18 | init : function(){ 19 | $(window).resize(function() { 20 | this.resize(); 21 | }.bind(this)); 22 | this.resize(); 23 | } 24 | ,getWidth: function() { return this.getCanvasHTML().width } 25 | , getGrid : function() { return this.grid } 26 | , getCellWidth : function() { return this.cellWidth } 27 | , getCellHeight : function() { return this.cellHeight } 28 | , getZoom : function() { return this.zoom } 29 | , setZoom: function(newZoom) { this.zoom = newZoom } 30 | , getCanvasHTML : function() { return this.canvasHTML } 31 | , getCanvasContext : function() { return this.canvasContext } 32 | , isFocused : function(){ 33 | return document.body == document.activeElement; 34 | } 35 | , getGridCoord: function(mouseCoord){ 36 | // remove zoom 37 | unzoomedCoord = new Coord(mouseCoord.x/this.zoom,mouseCoord.y/this.zoom); 38 | // get pixel located at the specified coordinate 39 | return new Coord(Math.floor(unzoomedCoord.x / this.cellWidth), Math.floor(unzoomedCoord.y / this.cellHeight)); 40 | } 41 | , setChanged : function(changed){ 42 | this.changed = changed; 43 | } 44 | , drawRect : function(coord,width,height,style){ 45 | this.canvasContext.fillStyle = style; 46 | this.canvasContext.fillRect(coord.x*this.cellWidth,coord.y*this.cellHeight,width,height); 47 | } 48 | , drawText : function(text,coord,style){ 49 | this.getCanvasContext().fillStyle = style; 50 | var canvasCoord = this.getTextLocation(coord); 51 | this.getCanvasContext().fillText(text,canvasCoord.x,canvasCoord.y); 52 | } 53 | , mouseDown : function(eventObject) { } 54 | , mouseMove : function(eventObject) { } 55 | , mouseUp : function() { } 56 | , mouseEnter : function() { } 57 | , cellDown : function(coord) { 58 | this.startCoord = coord; 59 | } 60 | , cellMove : function(coord) { } 61 | , cellUp : function(coord) { } 62 | , mouseLeave : function() { } 63 | , keyUp : function(eventObject){ } 64 | , keyDown : function(eventObject){ 65 | if (this.isFocused() && eventObject.keyCode == KeyEvent.DOM_VK_BACK_SPACE) { 66 | eventObject.preventDefault(); 67 | } 68 | } 69 | , keyPress : function(eventObject){ 70 | if (eventObject.keyCode == KeyEvent.DOM_VK_BACK_SPACE) { 71 | eventObject.preventDefault(); 72 | } 73 | } 74 | , cursor : function() { return "crosshair"; } 75 | , hasChanged : function(){ 76 | return this.changed; 77 | } 78 | , getTextLocation : function(coord){ 79 | return new Coord(coord.x*this.cellWidth, coord.y*this.cellHeight+this.cellHeight-this.cellDescend); 80 | } 81 | , recalculateCellDimensions : function(){ 82 | if (this.cellWidth == null){ 83 | this.cellWidth = getTextWidth(this.canvasContext, defaultFont); 84 | console.log("Cell width '"+this.cellWidth+"'"); 85 | } 86 | if (this.cellHeight == null){ 87 | heightMetrics = getTextHeight(this.canvasContext,this.font, 0, 0, 100, 100); 88 | this.canvasContext.clearRect(0, 0, 100, 100); 89 | this.cellHeight = heightMetrics[0]; 90 | this.cellDescend = heightMetrics[1]; 91 | if (this.cellHeight == 0) { 92 | this.cellHeight = this.cellWidth*1.5; 93 | } 94 | console.log("Cell height '"+this.cellHeight+"', cell descend '"+this.cellDescend+"'"); 95 | } 96 | } 97 | , resize : function(){ 98 | this.recalculateCellDimensions(this.canvasContext); 99 | this.canvasHTML.width = this.grid.cols * Math.round(this.cellWidth) * this.zoom; 100 | this.canvasHTML.height = this.grid.rows * Math.round(this.cellHeight) * this.zoom; 101 | $("#canvas-container").width(this.getCanvasHTML().width); 102 | this.changed = true; 103 | // console.log("New canvas size ("+this.grid.cols+","+this.grid.rows+","+this.grid.zoom+") '"+this.canvasHTML.width+"/"+this.canvasHTML.height+"'"); 104 | } 105 | , redraw : function() { 106 | 107 | // console.log("Redrawing canvas... zoom '"+this.zoom+"'"); 108 | 109 | this.canvasContext.setTransform(1, 0, 0, 1, 0, 0); 110 | this.canvasContext.scale(this.zoom, this.zoom); 111 | 112 | // clear everything so we dont have pixels drawn over pixels 113 | this.canvasContext.clearRect(0, 0, this.canvasHTML.width, this.canvasHTML.height); 114 | 115 | // Draw border 116 | // drawBorder(this.canvasContext,this.canvasHTML.width, this.canvasHTML.height); 117 | 118 | // debug 119 | // console.log("Drawing grid with font '"+this.font+"' & size '"+this.cellWidth+"/"+this.cellHeight+"'..."); 120 | 121 | // set line width & color for the grid 122 | this.canvasContext.lineWidth = "1"; 123 | this.canvasContext.strokeStyle = "#DDDDDD"; 124 | 125 | // draw rows 126 | for (i=0; i"; 263 | } 264 | if (pixelContext.bottom) { 265 | return "v"; 266 | } 267 | if (pixelContext.top) { 268 | return "^"; 269 | } 270 | if (pixelContext.right) { 271 | return "<"; 272 | } 273 | } 274 | else if (pixelContext.getLength() == 3) { 275 | /* 276 | * This handles this case: X 277 | * < X 278 | * X 279 | */ 280 | if (!pixelContext.left) { 281 | return "<"; 282 | } 283 | /* 284 | * This handles this case: X 285 | * X ^ X 286 | * 287 | */ 288 | else if (!pixelContext.bottom) { 289 | return "^"; 290 | } 291 | /* 292 | * This handles this case: 293 | * X v X 294 | * X 295 | */ 296 | else if (!pixelContext.top) { 297 | return "v"; 298 | } 299 | /* 300 | * This handles this case: X 301 | * X > 302 | * X 303 | */ 304 | if (!pixelContext.right) { 305 | return ">"; 306 | } 307 | } 308 | } 309 | 310 | /* 311 | * This handles this case: X 312 | * X - X 313 | * X 314 | */ 315 | if (4 == pixelContext.getLength()) { 316 | return "-"; 317 | } 318 | 319 | return pixelValue; 320 | } 321 | } 322 | 323 | // -------------------------------------------------- DECORATORS --------------------------------------------------- // 324 | 325 | /** 326 | * This tool handles the cursor position & movement (arrow keys) and pointer hovering. 327 | * This tool also supports the writing and the edition of the text (Backspace & Delete are supported) 328 | */ 329 | function PointerDecorator(canvas, toolId){ 330 | this.class = "PointerDecorator"; 331 | this.canvas = canvas; 332 | this.toolId = toolId; 333 | this.selectedCell = null; 334 | this.pointerCell = null; 335 | this.drawSelectedCell = false; 336 | this.changed = false; 337 | } 338 | 339 | PointerDecorator.prototype = { 340 | 341 | getPointerCell : function() { return this.pointerCell } 342 | , setPointerCell : function(coord) { this.pointerCell = coord } 343 | , getDrawSelectedCell : function() { return this.drawSelectedCell } 344 | , setDrawSelectedCell : function(draw) { this.drawSelectedCell = draw } 345 | , getSelectedCell : function() { return this.selectedCell } 346 | , setSelectedCell : function(coord){ 347 | if (this.canvas.getGrid().getPixel(coord) != undefined){ 348 | this.selectedCell = coord; 349 | } 350 | } 351 | , hasChanged : function(){ 352 | this.refresh(); 353 | return this.canvas.hasChanged() || this.changed; 354 | } 355 | , setChanged : function(changed){ 356 | this.canvas.setChanged(changed) 357 | this.changed = changed; 358 | } 359 | , redraw : function(){ 360 | this.canvas.redraw(); 361 | // draw selected cell 362 | if (this.getSelectedCell() != null && this.getDrawSelectedCell()){ 363 | this.canvas.drawText("▏",this.getSelectedCell(),"#009900"); 364 | } 365 | // draw pointer 366 | if (this.getPointerCell() != null){ 367 | this.canvas.drawRect(this.getPointerCell(),this.canvas.getCellWidth(), this.canvas.getCellHeight(), "#009900"); 368 | } 369 | } 370 | /** 371 | * implementation of intermitent cursor 372 | */ 373 | , refresh : function(){ 374 | var shouldDraw = this.getSelectedCell() != null && new Date().getTime() % 2000 < 1000; 375 | var changed = shouldDraw != this.getDrawSelectedCell(); 376 | this.setDrawSelectedCell(shouldDraw); 377 | this.changed = this.changed || changed; 378 | } 379 | , cellDown : function(coord){ 380 | this.canvas.cellDown(coord); 381 | this.mouseStatus = "down"; 382 | this.setSelectedCell(coord); 383 | this.changed = true; 384 | } 385 | , cellMove : function(coord){ 386 | this.canvas.cellMove(coord); 387 | this.mouseStatus = this.mouseStatus == "up" || this.mouseStatus == "hover"? "hover" : "moving"; 388 | this.setPointerCell(coord); 389 | this.changed = true; 390 | } 391 | , cellUp : function(coord){ 392 | this.canvas.cellUp(coord); 393 | this.mouseStatus = "up"; 394 | } 395 | , mouseLeave : function(){ 396 | this.canvas.mouseLeave(); 397 | this.setPointerCell(null); 398 | this.changed = true; 399 | } 400 | /** 401 | * This to prevent moving the document with the arrow keys 402 | */ 403 | , keyDown : function(eventObject){ 404 | this.canvas.keyDown(eventObject); 405 | 406 | // check if canvas has the focus 407 | if (this.canvas.isFocused()){ return } 408 | 409 | // prevent from processing unwanted keys 410 | if (eventObject.keyCode == KeyEvent.DOM_VK_LEFT){ 411 | eventObject.preventDefault(); 412 | } 413 | else if (eventObject.keyCode == KeyEvent.DOM_VK_RIGHT){ 414 | eventObject.preventDefault(); 415 | } 416 | else if (eventObject.keyCode == KeyEvent.DOM_VK_UP){ 417 | eventObject.preventDefault(); 418 | } 419 | else if (eventObject.keyCode == KeyEvent.DOM_VK_DOWN){ 420 | eventObject.preventDefault(); 421 | } 422 | } 423 | 424 | , keyUp : function(eventObject){ 425 | this.canvas.keyUp(eventObject); 426 | // check if we have the focus 427 | if (!this.canvas.isFocused()){ return } 428 | // check if there is the pointer is inside the canvas 429 | if (this.getSelectedCell() == null){ return; } 430 | // move selected cell with the arrows & backspace key 431 | if (eventObject.keyCode == KeyEvent.DOM_VK_LEFT){ 432 | this.setSelectedCell(this.getSelectedCell().add(leftCoord)); 433 | } else if (eventObject.keyCode == KeyEvent.DOM_VK_RIGHT){ 434 | this.setSelectedCell(this.getSelectedCell().add(rightCoord)); 435 | } else if (eventObject.keyCode == KeyEvent.DOM_VK_UP){ 436 | this.setSelectedCell(this.getSelectedCell().add(topCoord)); 437 | } else if (eventObject.keyCode == KeyEvent.DOM_VK_DOWN){ 438 | this.setSelectedCell(this.getSelectedCell().add(bottomCoord)); 439 | } 440 | this.changed = true; 441 | } 442 | } 443 | 444 | // ---------------------------------------------- CANVAS CHAR DECORATOR -------------------------------------------- // 445 | 446 | /** 447 | * This tool handles the cursor position & movement (arrow keys) and pointer hovering. 448 | * This tool also supports the writing and the edition of the text (Backspace & Delete are supported) 449 | */ 450 | function WritableCanvas(canvas){ 451 | this.class = "WritableCanvas"; 452 | this.canvas = canvas; 453 | } 454 | 455 | WritableCanvas.prototype = { 456 | 457 | // some chars are not sent to keypress like period or space 458 | keyDown : function(eventObject){ 459 | this.canvas.keyDown(eventObject); 460 | // dont write anything unless canvas has the focus 461 | if (!this.canvas.isFocused()) return; 462 | // prevent space key to scroll down page 463 | if (eventObject.keyCode == KeyEvent.DOM_VK_SPACE) { 464 | eventObject.preventDefault(); 465 | this.importChar(" "); 466 | this.canvas.setSelectedCell(this.canvas.getSelectedCell().add(rightCoord)); 467 | } /*else if (eventObject.keyCode == KeyEvent.DOM_VK_PERIOD){ 468 | this.importChar("."); 469 | this.canvas.setSelectedCell(this.canvas.getSelectedCell().add(rightCoord)); 470 | }*/ 471 | // delete previous character 472 | else if (eventObject.keyCode == KeyEvent.DOM_VK_BACK_SPACE) { 473 | if (this.canvas.getPixel(this.canvas.getSelectedCell().add(leftCoord)) != undefined){ 474 | this.canvas.setSelectedCell(this.canvas.getSelectedCell().add(leftCoord)); 475 | this.importChar(" "); 476 | } 477 | } 478 | // delete next character 479 | else if (eventObject.keyCode == KeyEvent.DOM_VK_DELETE){ 480 | // get current text 481 | currentText = this.canvas.getText(this.canvas.getSelectedCell()); 482 | if (currentText == null){ return; } 483 | // delete first character and replace last with space (we are moving text to left) 484 | currentText = currentText.substring(1)+" "; 485 | this.importChar(currentText); 486 | } 487 | // jump to next line 488 | else if (eventObject.keyCode == KeyEvent.DOM_VK_RETURN){ 489 | var startOfText = this.canvas.getTextColStart(this.canvas.getSelectedCell()); 490 | if (startOfText && startOfText.add(bottomCoord)){ 491 | this.canvas.setSelectedCell(startOfText.add(bottomCoord)); 492 | } 493 | } 494 | } 495 | , keyPress : function(eventObject){ 496 | // propagate event 497 | this.canvas.keyPress(eventObject); 498 | // dont write anything 499 | if (!this.canvas.isFocused()){ return } 500 | // write key 501 | if (this.canvas.getPixel(this.canvas.getSelectedCell().add(rightCoord)) != undefined){ 502 | try{ 503 | this.importChar(String.fromCharCode(eventObject.charCode)); 504 | this.canvas.setSelectedCell(this.canvas.getSelectedCell().add(rightCoord)); 505 | }catch(e){ 506 | console.log(e.message); 507 | } 508 | } 509 | } 510 | , importChar : function(char){ 511 | this.canvas.import(char,this.canvas.getSelectedCell()); 512 | this.canvas.commit(); 513 | this.canvas.setChanged(true); 514 | } 515 | } 516 | 517 | // -------------------------------------------- MOVABLE CANVAS DECORATOR ------------------------------------------- // 518 | 519 | class MovableCanvas extends CanvasDecorator { 520 | constructor(canvas, htmlContainerSelectorId){ 521 | super(canvas); 522 | this.class = "MovableCanvas"; 523 | this.htmlContainerSelectorId = htmlContainerSelectorId; 524 | this.lastMouseEvent = null; 525 | this.shiftKeyEnabled = false; 526 | } 527 | 528 | keyDown(eventObject){ 529 | super.keyDown(eventObject); 530 | if (eventObject.keyCode == KeyEvent.DOM_VK_SHIFT) { 531 | this.shiftKeyEnabled = true; 532 | } 533 | } 534 | keyUp(eventObject){ 535 | super.keyUp(eventObject); 536 | if (eventObject.keyCode == KeyEvent.DOM_VK_SHIFT) { 537 | this.shiftKeyEnabled = false; 538 | } 539 | } 540 | mouseDown(eventObject) { 541 | super.mouseDown(eventObject); 542 | this.lastMouseEvent = eventObject; 543 | } 544 | mouseMove(eventObject){ 545 | super.mouseMove(eventObject); 546 | if (!super.isFocused() || !this.shiftKeyEnabled) return; 547 | if (this.lastMouseEvent == null) return; 548 | $(this.htmlContainerSelectorId).scrollTop(Math.max(0,$(this.htmlContainerSelectorId).scrollTop() - (eventObject.clientY-this.lastMouseEvent.clientY))); 549 | $(this.htmlContainerSelectorId).scrollLeft(Math.max(0,$(this.htmlContainerSelectorId).scrollLeft() - (eventObject.clientX-this.lastMouseEvent.clientX))); 550 | this.lastMouseEvent = eventObject; 551 | } 552 | mouseUp(){ 553 | super.mouseUp(); 554 | this.lastMouseEvent = null; 555 | } 556 | 557 | 558 | } -------------------------------------------------------------------------------- /src/js/view/drawable-canvas.js: -------------------------------------------------------------------------------- 1 | function DrawableCanvas(canvas){ 2 | this.class = "DrawableCanvas"; 3 | this.canvas = canvas; 4 | this.grid = canvas.getGrid(); 5 | } 6 | 7 | /** 8 | * Add text & line drawing capabilities to canvas 9 | */ 10 | DrawableCanvas.prototype = { 11 | /** 12 | * Return true if the specified pixel has a drawing character 13 | */ 14 | isDrawChar : function(pixel) { 15 | if (pixel == null || pixel == undefined){ 16 | return pixel; 17 | } 18 | return UC.isChar(pixel.getValue()); 19 | } 20 | /** 21 | * Returns the context of the specified pixel. That is, the status of the surrounding pixels 22 | */ 23 | , getPixelContext : function(coord) { 24 | var left = this.isDrawChar(this.canvas.getPixel(coord.add(leftCoord))); 25 | var right = this.isDrawChar(this.canvas.getPixel(coord.add(rightCoord))); 26 | var top = this.isDrawChar(this.canvas.getPixel(coord.add(topCoord))); 27 | var bottom = this.isDrawChar(this.canvas.getPixel(coord.add(bottomCoord))); 28 | return new PixelContext(left, right, top, bottom); 29 | } 30 | , drawLine : function(startCoord, endCoord, mode, pixelValue, ommitIntersections) { 31 | // console.log("Drawing line from "+startCoord+" to "+endCoord+" with value '"+pixelValue+"'..."); 32 | if (mode == "best"){ 33 | return this.drawLineImpl3(startCoord, endCoord, mode, pixelValue, ommitIntersections); 34 | } 35 | if (mode == "horizontal-horizontal" || mode == "vertical-vertical" 36 | || mode == "vertical-horizontal" || mode == "vertical-horizontal"){ 37 | return this.drawLineImpl2(startCoord, endCoord, mode, pixelValue, ommitIntersections); 38 | } 39 | return this.drawLineImpl(startCoord, endCoord, mode, pixelValue, ommitIntersections); 40 | } 41 | /** 42 | * This functions draws a line of pixels from startCoord to endCoord. The line can be drawn 2 ways: either first horizontal line of first vertical line. 43 | * For drawing boxes, the line should be drawn both ways. 44 | */ 45 | , drawLineImpl : function(startCoord, endCoord, drawHorizontalFirst, pixelValue, ommitIntersections) { 46 | // calculate box so we know from where to where we should draw the line 47 | var box = new Box(startCoord, endCoord), minX = box.minX, minY = box.minY, maxX = box.maxX, maxY = box.maxY; 48 | 49 | // calculate where to draw the horizontal line 50 | var yPosHorizontalLine = drawHorizontalFirst ? startCoord.y : endCoord.y 51 | for (;minX <= maxX; minX++) { 52 | var newCoord = new Coord(minX, yPosHorizontalLine), pixelContext = this.getPixelContext(new Coord(minX, yPosHorizontalLine)); 53 | // stack pixels even if we are omiting intersections 54 | var finalValue = pixelValue; 55 | if (ommitIntersections && (pixelContext.top+pixelContext.bottom==2)) finalValue = null; 56 | this.grid.stackPixel(newCoord, finalValue); 57 | } 58 | // calculate where to draw the vertical line 59 | var xPosLine = drawHorizontalFirst ? endCoord.x : startCoord.x; 60 | for (;minY <= maxY; minY++) { 61 | var newCoord = new Coord(xPosLine, minY), pixelContext = this.getPixelContext(new Coord(xPosLine, minY)); 62 | // stack pixels even if we are omiting intersections 63 | var finalValue = pixelValue; 64 | if (ommitIntersections && (pixelContext.left+pixelContext.right==2)) finalValue = null; 65 | this.grid.stackPixel(newCoord, pixelValue); 66 | } 67 | } 68 | // draw stepped line 69 | , drawLineImpl2 : function(startCoord, endCoord, drawMode, pixelValue, ommitIntersections) { 70 | if (drawMode == "horizontal-horizontal" || drawMode == "vertical-vertical"){ 71 | var box = new Box(startCoord, endCoord), minX = box.minX, minY = box.minY, maxX = box.maxX, maxY = box.maxY; 72 | var midCoord1 = null; 73 | var midCoord2 = null; 74 | if (drawMode == "horizontal-horizontal") { midCoord1 = new Coord(box.midX, startCoord.y); midCoord2 = new Coord(box.midX, endCoord.y); } 75 | if (drawMode == "vertical-vertical") { midCoord1 = new Coord(startCoord.x, box.midY); midCoord2 = new Coord(endCoord.x, box.midY); } 76 | this.drawLineImpl(startCoord, midCoord1, drawMode == "horizontal-horizontal", pixelValue, ommitIntersections); 77 | this.drawLineImpl(midCoord1, midCoord2, drawMode != "horizontal-horizontal", pixelValue, ommitIntersections); 78 | this.drawLineImpl(midCoord2, endCoord, drawMode == "horizontal-horizontal", pixelValue, ommitIntersections); 79 | } 80 | else if (drawMode == "horizontal-vertical" || drawMode == "vertical-horizontal"){ 81 | this.drawLineImpl(startCoord, endCoord, drawMode == "horizontal-vertical", pixelValue, ommitIntersections); 82 | } 83 | } 84 | , drawLineImpl3 : function(startCoord, endCoord, drawMode, pixelValue, ommitIntersections) { 85 | this.drawLineImpl2(startCoord, endCoord, "horizontal-horizontal", pixelValue, ommitIntersections); 86 | } 87 | , getTextStart : function(startCoord) { 88 | // guess where the text starts (leftmost col and upmost row) 89 | var startingColumn = startCoord.x; 90 | for (col=startingColumn; col>=0; col--){ 91 | pixel = this.grid.getPixel(new Coord(col,startCoord.y)); 92 | if (this.isDrawChar(pixel)){ 93 | break; 94 | } 95 | previousPixelValue = pixel.getValue(); 96 | if (previousPixelValue == null){ 97 | if (col == 0){ 98 | break; 99 | } else{ 100 | pixel2 = this.grid.getPixel(new Coord(col-1,startCoord.y)); 101 | previousPixelValue2 = pixel2.getValue(); 102 | if (previousPixelValue2 == null || this.isDrawChar(pixel2)) break; 103 | } 104 | } 105 | startingColumn = col; 106 | } 107 | var startingRow = startCoord.y; 108 | for (row=startingRow; row>=0; row--){ 109 | pixel = this.grid.getPixel(new Coord(startingColumn,row)); 110 | previousPixelValue = pixel.getValue(); 111 | if (previousPixelValue == null || this.isDrawChar(pixel)) break; 112 | startingRow = row; 113 | } 114 | return new Coord(startingColumn, startingRow); 115 | }, getTextColStart : function(startCoord) { 116 | // guess where the text starts 117 | var chars_found = 0; 118 | var startingColumn = startCoord.x; 119 | for (col=startingColumn; col>=0; col--){ 120 | pixel = this.grid.getPixel(new Coord(col,startCoord.y)); 121 | if (this.isDrawChar(pixel)){ 122 | break; 123 | } 124 | previousPixelValue = pixel.getValue(); 125 | if (previousPixelValue == null){ 126 | if (col == 0){ 127 | break; 128 | } else{ 129 | pixel2 = this.grid.getPixel(new Coord(col-1,startCoord.y)); 130 | previousPixelValue2 = pixel2.getValue(); 131 | if (previousPixelValue2 == null || this.isDrawChar(pixel2)) break; 132 | } 133 | } 134 | else{ 135 | chars_found++; 136 | } 137 | startingColumn = col; 138 | } 139 | if (chars_found==0) return null; 140 | return new Coord(startingColumn, startCoord.y); 141 | } 142 | 143 | /* 144 | * TODO: implement trim function so we export just the necessary text 145 | */ 146 | /*function trimText(text){ 147 | lines = text.split("\n"); 148 | ret = ""; 149 | for (e = 0;e < lines.length;e++) { 150 | ret += "\n"; 151 | for (var g = lines[e], l = 0;l < g.length;l++) { 152 | var h = g.charAt(l); 153 | ret += h; 154 | } 155 | } 156 | }*/ 157 | , getText : function(startCoord){ 158 | pixel = this.grid.getPixel(startCoord); 159 | if (pixel == undefined) return undefined; 160 | 161 | pixelValue = pixel.getValue(); 162 | if (pixelValue == undefined || pixelValue == null) return null; 163 | if (this.isDrawChar(pixel)) return null; 164 | 165 | var text = ""; 166 | for (row=startCoord.y; row this.grid.cols-2){ 183 | break; 184 | } 185 | 186 | pixel2 = this.grid.getPixel(new Coord(col+1,row)); 187 | nextPixelValue2 = pixel2.getValue(); 188 | if (this.isDrawChar(pixel2)){ 189 | break; 190 | } 191 | 192 | if (nextPixelValue2 != null){ 193 | text += " "; 194 | continue; 195 | } 196 | } 197 | } 198 | if (text.startsWith("\n")) text = text.substring(1); 199 | return text; 200 | } 201 | , getFinalCoords : function(coord, direction) { 202 | var ret = []; 203 | for (i=0, currentCord = coord;;i++) { 204 | var nextCoord = currentCord.add(direction); 205 | if (!this.isDrawChar(this.canvas.getPixel(nextCoord))) { 206 | if(!currentCord.equals(coord)) ret.push(currentCord); 207 | return ret; 208 | } 209 | currentCord = nextCoord; 210 | if(this.getPixelContext(currentCord).getLength() >= 3){ 211 | ret.push(currentCord); 212 | } 213 | } 214 | } 215 | // Detect the final endpoint of a line. The line should not be necessarily straight 216 | , getFinalEndPoint : function(coord, direction) { 217 | for (var i=0, currentCoord = coord, currentDirection = direction; i< 1000;i++) { 218 | var currentPixelContext = this.getPixelContext(currentCoord); 219 | var nextCoord = currentCoord.add(currentDirection); 220 | var isNextPixelDrawChar = this.isDrawChar(this.canvas.getPixel(nextCoord)); 221 | if(currentPixelContext.getLength() == 2 && isNextPixelDrawChar){ currentCoord = nextCoord; continue; } 222 | if(currentPixelContext.getLength() >= 3 && !currentCoord.equals(coord)) return currentCoord; 223 | if(currentPixelContext.left && currentDirection.add(leftCoord).getLength() != 0) { currentCoord = currentCoord.add(leftCoord); currentDirection = leftCoord; continue; } 224 | if(currentPixelContext.right && currentDirection.add(rightCoord).getLength() != 0) { currentCoord = currentCoord.add(rightCoord); currentDirection = rightCoord; continue; } 225 | if(currentPixelContext.top && currentDirection.add(topCoord).getLength() != 0) { currentCoord = currentCoord.add(topCoord); currentDirection = topCoord; continue; } 226 | if(currentPixelContext.bottom && currentDirection.add(bottomCoord).getLength() != 0) { currentCoord = currentCoord.add(bottomCoord); currentDirection = bottomCoord; continue; } 227 | return currentCoord.equals(coord)? null : currentCoord; 228 | } 229 | } 230 | , getLinePoints : function(coord, direction) { 231 | var ret = []; 232 | for (var i=0, currentCoord = coord, currentDirection = direction; i< 1000;i++) { 233 | var currentPixelContext = this.getPixelContext(currentCoord); 234 | var nextCoord = currentCoord.add(currentDirection); 235 | var isNextPixelDrawChar = this.isDrawChar(this.canvas.getPixel(nextCoord)); 236 | if(currentPixelContext.length >= 3 && !currentCoord.equals(coord)) return ret; 237 | if(currentPixelContext.length >= 2 && isNextPixelDrawChar){ ret.push(currentCoord); currentCoord = currentCoord.add(currentDirection); continue; } 238 | if(currentPixelContext.left && !currentDirection.isOppositeDir(leftCoord)) { currentDirection = leftCoord; continue; } 239 | if(currentPixelContext.right && !currentDirection.isOppositeDir(rightCoord)) { currentDirection = rightCoord; continue; } 240 | if(currentPixelContext.top && !currentDirection.isOppositeDir(topCoord)) { currentDirection = topCoord; continue; } 241 | if(currentPixelContext.bottom && !currentDirection.isOppositeDir(bottomCoord)) { currentDirection = bottomCoord; continue; } 242 | return ret.length == 1? null : ret.push(currentCoord), ret; 243 | } 244 | } 245 | , detectEndPoints : function(coord){ 246 | endPointsInfo = []; 247 | for (direction in contextCoords) { 248 | endPoints = this.getFinalCoords(coord, contextCoords[direction]); 249 | for (endPointIndex in endPoints) { 250 | endPoint = endPoints[endPointIndex]; 251 | isHorizontal = contextCoords[direction].x != 0; 252 | startWithArrow = arrowChars1.indexOf(this.canvas.getPixel(coord).getValue()) != -1; 253 | endWithArrow = arrowChars1.indexOf(this.canvas.getPixel(endPoint).getValue()) != -1; 254 | endPointInfo = new EndPointInfo(endPoint, this.getPixelContext(endPoint), isHorizontal, startWithArrow, endWithArrow); 255 | endPointsInfo.push(endPointInfo); 256 | if (length == 1) { 257 | //console.log("Found simple endpoint: "+endPointInfo); 258 | } else { 259 | // console.log("Found complex endpoint: "+endPointInfo); 260 | endPointInfo.childEndpoints = []; 261 | for (var direction2 in contextCoords) { 262 | // dont go backwards 263 | if (contextCoords[direction].add(contextCoords[direction2]).getLength() == 0) continue; 264 | // dont go in the same direction 265 | // if (contextCoords[direction].add(contextCoords[direction2]).getLength() == 2) continue; 266 | var endPoints2 = this.getFinalCoords(endPoint, contextCoords[direction2]); 267 | for (var endPointIndex2 in endPoints2){ 268 | ep2 = new EndPointInfo(endPoints2[endPointIndex2], this.getPixelContext(endPoints2[endPointIndex2]), isHorizontal, 269 | startWithArrow, -1 != arrowChars1.indexOf(this.canvas.getPixel(endPoints2[endPointIndex2]).getValue()), endWithArrow); 270 | endPointInfo.childEndpoints.push(ep2); 271 | // console.log("Found child endpoint: "+ep2); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | return endPointsInfo; 278 | } 279 | , detectBox(coord){ 280 | if (!this.isDrawChar(this.canvas.getPixel(coord))) return null; // did you click on the right place? 281 | var loopCoords = {left:1, right:1, top:1, bottom:1}; 282 | var firstDir = null; 283 | var pixelContext = this.getPixelContext(coord); 284 | if (pixelContext.right) firstDir = rightCoord; 285 | else if (pixelContext.bottom) firstDir = bottomCoord; 286 | else if (pixelContext.top) firstDir = topCoord; 287 | else if (pixelContext.left) firstDir = leftCoord; 288 | var points = []; 289 | for (var i=0, currentCoord = coord, currentDirection = firstDir; i< 1000;i++) { 290 | pixelContext = this.getPixelContext(currentCoord); 291 | if (pixelContext.getLength() < 2) return null; // dead end 292 | var nextCoord = currentCoord.add(currentDirection); // let's try next char 293 | if (nextCoord.equals(coord)) return points; // loop complete :) 294 | var isNextPixelDrawChar = this.isDrawChar(this.canvas.getPixel(nextCoord)); 295 | if (pixelContext.getLength() >= 2 && isNextPixelDrawChar) { points.push(currentCoord); currentCoord = nextCoord; continue; } 296 | if (pixelContext.bottom && loopCoords["bottom"] && !currentDirection.isOppositeDir(bottomCoord)) { loopCoords["bottom"]=0; currentDirection = bottomCoord; continue; } 297 | if (pixelContext.left && loopCoords["left"] && !currentDirection.isOppositeDir(leftCoord)) { loopCoords["left"]=0; currentDirection = leftCoord; continue; } 298 | if (pixelContext.top && loopCoords["top"] && !currentDirection.isOppositeDir(topCoord)) { loopCoords["top"]=0; currentDirection = topCoord; continue; } 299 | if (pixelContext.right && loopCoords["right"] && !currentDirection.isOppositeDir(rightCoord)) { loopCoords["right"]=0; currentDirection = rightCoord; continue; } 300 | return null; // d'oh! loop incomplete 301 | } 302 | } 303 | , getBox(points){ 304 | var minX=Number.MAX_VALUE,maxX=Number.MIN_VALUE, minY=Number.MAX_VALUE, maxY=Number.MIN_VALUE; 305 | for (i in points){ p = points[i]; minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } 306 | return new Box(new Coord(minX,minY),new Coord(maxX,maxY)); 307 | } 308 | , getEndPoints(points){ 309 | var ret = []; 310 | for (i in points){ p = points[i]; if (this.getPixelContext(p).getLength() >= 3) ret.push(p); } 311 | return ret; 312 | } 313 | , isDrawCharArea: function(area){ 314 | if (this.canvas.isOutOfBounds(area.min) || this.canvas.isOutOfBounds(area.max)) throw "OutOfBoundException"; 315 | for (col = area.minX;col<=area.maxX;col++){ 316 | for (row = area.minY;row<=area.maxY;row++){ 317 | if (!this.isDrawChar(this.canvas.getPixel(new Coord(col,row)))) return false; 318 | } 319 | } 320 | return area.squareSize() > 0; 321 | } 322 | } 323 | --------------------------------------------------------------------------------