├── .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 | 
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 |
105 |
106 |
107 |
108 | Close
109 | OK
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 |
105 |
106 |
107 |
108 | Close
109 | OK
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 |
--------------------------------------------------------------------------------