├── _CNAME ├── index.html ├── style.css ├── bundle.min.js └── bundle.js /_CNAME: -------------------------------------------------------------------------------- 1 | shakydraw.com -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ShakyDraw 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 |
22 |
23 |

ShakyDrawβ

24 | Beautiful hand-drawn block diagrams from plain text 25 | 26 | 27 | Download 28 | About 29 | 30 |
31 |
32 | 57 |
58 |
59 | 60 |
61 |
62 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Gloria+Hallelujah'); 2 | @import url('https://fonts.googleapis.com/css?family=Source+Code+Pro'); 3 | @import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400'); 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | outline: 0; 9 | box-sizing: border-box; 10 | } 11 | 12 | html, body { 13 | height: 100%; 14 | color: #313131; 15 | font-family: 'Roboto', sans-serif; 16 | } 17 | 18 | sup { 19 | font-size: 12px; 20 | padding-left: 3px; 21 | } 22 | 23 | .container { 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | .panel { 29 | width: 50%; 30 | float: left; 31 | height: calc(100% - 80px); 32 | margin-top: 10px; 33 | } 34 | 35 | textarea { 36 | width: 100%; 37 | height: 100%; 38 | font-family: 'Source Code Pro', monospace; 39 | font-size: 16px; 40 | border-width: 0; 41 | border-right: solid #eee 1px; 42 | box-sizing: border-box; 43 | padding: 15px; 44 | resize: none; 45 | outline: none; 46 | } 47 | 48 | canvas { 49 | width: auto; 50 | margin-top: 15px; 51 | margin-left: 15px; 52 | } 53 | 54 | .header { 55 | position: relative; 56 | z-index: 1; 57 | padding-left: 20px; 58 | padding-right: 20px; 59 | box-shadow: 0 0 8px rgba(0,0,0,0.2); 60 | padding: 15px; 61 | } 62 | 63 | .header > h1 { 64 | display: inline-block; 65 | margin-right: 10px; 66 | font-size: 24px; 67 | font-weight: 300; 68 | } 69 | 70 | .header h1 span { 71 | color: #818181; 72 | } 73 | 74 | .header > span { 75 | font-size: 13px; 76 | line-height: 29px; 77 | } 78 | 79 | .right-options { 80 | float: right; 81 | font-size: 13px; 82 | line-height: 24px; 83 | } 84 | 85 | .right-options > a { 86 | float: left; 87 | margin: 0 15px; 88 | text-decoration: none; 89 | color: #555; 90 | padding: 2px; 91 | } 92 | 93 | .right-options > a.download{ 94 | padding: 2px; 95 | text-decoration: none; 96 | color: #fff; 97 | background: #50a6f6; 98 | border-radius: 3px; 99 | display: block; 100 | width: 120px; 101 | text-align: center; 102 | border-bottom: none; 103 | } 104 | 105 | .modal-wrapper { 106 | position: fixed; 107 | top: 0; 108 | right: 0; 109 | left: 0; 110 | bottom: 0; 111 | background-color: rgba(0,0,0,.8); 112 | z-index: 2; 113 | display: none; 114 | } 115 | 116 | .modal { 117 | z-index: 10; 118 | position: absolute; 119 | top: 50%; 120 | left: 50%; 121 | background-color: #fff; 122 | width: 500px; 123 | height: 280px; 124 | border: 1px solid #ddd; 125 | padding: 20px 20px; 126 | transform: translateX(-50%) translateY(-50%); 127 | } 128 | 129 | .modal h1 { 130 | font-size: 24px; 131 | font-weight: 400; 132 | text-align: center; 133 | padding-top: 20px; 134 | } 135 | 136 | .modal p { 137 | font-size: 13px; 138 | margin: 20px 20px; 139 | text-align: center; 140 | } 141 | 142 | .modal a { 143 | text-decoration: none; 144 | background-color: #ccc; 145 | color: #333; 146 | } 147 | 148 | .modal a:visted { 149 | color: #333; 150 | } 151 | 152 | .modal a:hover { 153 | background-color: #ddd; 154 | } 155 | 156 | #close { 157 | font-size: 13px; 158 | text-transform: uppercase;; 159 | color: #fff; 160 | position: absolute; 161 | top: -30px; 162 | right: 0; 163 | } 164 | -------------------------------------------------------------------------------- /bundle.min.js: -------------------------------------------------------------------------------- 1 | var slice=[].slice,FONT='20pt \'Gloria Hallelujah\'',ShakyCanvas=function(a){this.ctx=a.getContext('2d'),this.ctx.lineWidth=3,this.ctx.font=FONT,this.ctx.textBaseline='middle'};ShakyCanvas.prototype.moveTo=function(a,b){this.x0=a,this.y0=b},ShakyCanvas.prototype.lineTo=function(a,b){return this.shakyLine(this.x0,this.y0,a,b),this.moveTo(a,b)},ShakyCanvas.prototype.shakyLine=function(a,b,e,f){var g,h,o,p,q,r,s,t,u,v,w,z;return h=e-a,o=f-b,r=Math.sqrt(h*h+o*o),g=Math.sqrt(r)/1.5,p=Math.random(),q=Math.random(),s=Math.random()*g,t=Math.random()*g,u=a+h*p+o/r*s,w=b+o*p-h/r*s,v=a+h*q-o/r*t,z=b+o*q+h/r*t,this.ctx.moveTo(a,b),this.ctx.bezierCurveTo(u,w,v,z,e,f)},ShakyCanvas.prototype.bulb=function(a,b){var e,f,g,h;for(e=function(){return 2*Math.random()-1},h=[],f=g=0;2>=g;f=++g)this.beginPath(),this.ctx.arc(a+e(),b+e(),5,0,2*Math.PI,!0),this.ctx.closePath(),h.push(this.ctx.fill());return h},ShakyCanvas.prototype.arrowhead=function(a,b,e,f){var g,h,o,p,q,r,s,t,u,v,w;return p=a-e,q=b-f,g=0===q?0>p?-Math.PI:0:Math.atan(q/p),h=g+0.5,o=g-0.5,r=20,t=e+r*Math.cos(h),v=f+r*Math.sin(h),this.beginPath(),this.moveTo(t,v),this.lineTo(e,f),this.stroke(),s=20,u=e+s*Math.cos(o),w=f+s*Math.sin(o),this.beginPath(),this.moveTo(u,w),this.lineTo(e,f),this.stroke()},ShakyCanvas.prototype.beginPath=function(){return this.ctx.beginPath()},ShakyCanvas.prototype.stroke=function(){return this.ctx.stroke()},ShakyCanvas.prototype.setStrokeStyle=function(a){return this.ctx.strokeStyle=a},ShakyCanvas.prototype.setFillStyle=function(a){return this.ctx.fillStyle=a},ShakyCanvas.prototype.fillText=function(){var a,b;return a=1<=arguments.length?slice.call(arguments,0):[],(b=this.ctx).fillText.apply(b,a)};var CELL_SIZE=15,X=function(a){return a*CELL_SIZE+CELL_SIZE/2},Y=function(a){return a*CELL_SIZE+CELL_SIZE/2},Point=function(a,b){this.x=a,this.y=b},Line=function(a,b,e,f,g,h,o){this.x0=a,this.y0=b,this.start=e,this.x1=f,this.y1=g,this.end=h,this.color=o};Line.prototype.draw=function(a){return a.setStrokeStyle(this.color),a.setFillStyle(this.color),a.beginPath(),a.moveTo(X(this.x0),Y(this.y0)),a.lineTo(X(this.x1),Y(this.y1)),a.stroke(),this._ending(a,this.start,X(this.x1),Y(this.y1),X(this.x0),Y(this.y0)),this._ending(a,this.end,X(this.x0),Y(this.y0),X(this.x1),Y(this.y1))},Line.prototype._ending=function(a,b,e,f,g,h){return'circle'===b?a.bulb(g,h):'arrow'===b?a.arrowhead(e,f,g,h):void 0};var Text=function(a,b,e,f){this.x0=a,this.y0=b,this.text=e,this.color=f};Text.prototype.draw=function(a){return a.setFillStyle(this.color),a.fillText(this.text,X(this.x0),Y(this.y0))};var parseASCIIArt=function(a){var b,e,f,g,h,o,p,q,r,s,t,u,v,w,z,A,B,C,D,E,F,G,H;for(B=a.split('\n'),s=B.length,F=Math.max.apply(Math,function(){var I,J,L;for(L=[],I=0,J=B.length;ID;G=C<=D?++w:--w)e[H][G]=' ';for(u=function(I,J){var L;return L=b(J,I),'|'===L||'-'===L||'+'===L||'~'===L||'!'===L},E=function(I,J){switch(b(J,I)){case'~':case'!':return'#666';}},t=function(I,J){var L;return L=b(J,I),'*'===L||'<'===L||'>'===L||'^'===L||'v'===L},r=function(){var I,J,L,M;for(H=I=0,L=s;0<=L?IL;H=0<=L?++I:--I)for(G=J=0,M=F;0<=M?JM;G=0<=M?++J:--J)if('|'===e[H][G]||'-'===e[H][G])return new Point(G,H)},f={'-':new Point(1,0),'|':new Point(0,1)},h=function(I,J,L,M){switch(b(J,I)){case'|':case'-':case'*':case'>':case'<':case'^':case'v':case'~':case'!':return e[J][I]=' ';case'+':switch(L=1-L,M=1-M,e[J][I]=' ',b(J-M,I-L)){case'|':case'!':case'+':return void(e[J][I]='|');case'-':case'~':return void(e[J][I]='-');}switch(b(J+M,I+L)){case'|':case'!':case'+':return e[J][I]='|';case'-':case'~':return e[J][I]='-';}}},g=function(I){var J,L,M,N;if(J=I.x0===I.x1?0:1,L=I.y0===I.y1?0:1,0!==J||0!==L){for(G=I.x0+J,H=I.y0+L,M=I.x1-J,N=I.y1-L;G<=M&&H<=N;)h(G,H,J,L),G+=J,H+=L;return h(I.x0,I.y0,J,L),h(I.x1,I.y1,J,L)}return h(I.x0,I.y0,J,L)},q=[],o=function(){var I,J,L,M,N,O,P,Q,R;if(I=r(),null==I)return!1;for(L=f[e[I.y][I.x]],O=I.x,Q=I.y,J=null;u(O-L.x,Q-L.y);)O-=L.x,Q-=L.y,null==J&&(J=E(O,Q));for(N=null,t(O-L.x,Q-L.y)&&(O-=L.x,Q-=L.y,N='*'===e[Q][O]?'circle':'arrow'),P=I.x,R=I.y;u(P+L.x,R+L.y);)P+=L.x,R+=L.y,null==J&&(J=E(P,R));return M=null,t(P+L.x,R+L.y)&&(P+=L.x,R+=L.y,M='*'===e[R][P]?'circle':'arrow'),A=new Line(O,Q,N,P,R,M,null==J?'black':J),q.push(A),g(A),'arrow'===N&&(A.x0-=L.x,A.y0-=L.y),'arrow'===M&&(A.x1+=L.x,A.y1+=L.y),!0},p=function(){var I,J,L,M,N,O,P,Q;for(O=[],H=L=0,N=s;0<=N?LN;H=0<=N?++L:--L)G=0,O.push(function(){var R;for(R=[];G ref1; x = ref <= ref1 ? ++k : --k) { 179 | data[y][x] = ' '; 180 | } 181 | } 182 | isPartOfLine = function(x, y) { 183 | var c; 184 | c = at(y, x); 185 | return c === '|' || c === '-' || c === '+' || c === '~' || c === '!'; 186 | }; 187 | toColor = function(x, y) { 188 | switch (at(y, x)) { 189 | case '~': 190 | case '!': 191 | return '#666'; 192 | } 193 | }; 194 | isLineEnding = function(x, y) { 195 | var c; 196 | c = at(y, x); 197 | return c === '*' || c === '<' || c === '>' || c === '^' || c === 'v'; 198 | }; 199 | findLineChar = function() { 200 | var m, n, ref2, ref3; 201 | for (y = m = 0, ref2 = height; 0 <= ref2 ? m < ref2 : m > ref2; y = 0 <= ref2 ? ++m : --m) { 202 | for (x = n = 0, ref3 = width; 0 <= ref3 ? n < ref3 : n > ref3; x = 0 <= ref3 ? ++n : --n) { 203 | if (data[y][x] === '|' || data[y][x] === '-') { 204 | return new Point(x, y); 205 | } 206 | } 207 | } 208 | }; 209 | dir = { 210 | '-': new Point(1, 0), 211 | '|': new Point(0, 1) 212 | }; 213 | eraseChar = function(x, y, dx, dy) { 214 | switch (at(y, x)) { 215 | case '|': 216 | case '-': 217 | case '*': 218 | case '>': 219 | case '<': 220 | case '^': 221 | case 'v': 222 | case '~': 223 | case '!': 224 | return data[y][x] = ' '; 225 | case '+': 226 | dx = 1 - dx; 227 | dy = 1 - dy; 228 | data[y][x] = ' '; 229 | switch (at(y - dy, x - dx)) { 230 | case '|': 231 | case '!': 232 | case '+': 233 | data[y][x] = '|'; 234 | return; 235 | case '-': 236 | case '~': 237 | data[y][x] = '-'; 238 | return; 239 | } 240 | switch (at(y + dy, x + dx)) { 241 | case '|': 242 | case '!': 243 | case '+': 244 | return data[y][x] = '|'; 245 | case '-': 246 | case '~': 247 | return data[y][x] = '-'; 248 | } 249 | } 250 | }; 251 | erase = function(line) { 252 | var dx, dy, x_, y_; 253 | dx = line.x0 !== line.x1 ? 1 : 0; 254 | dy = line.y0 !== line.y1 ? 1 : 0; 255 | if (dx !== 0 || dy !== 0) { 256 | x = line.x0 + dx; 257 | y = line.y0 + dy; 258 | x_ = line.x1 - dx; 259 | y_ = line.y1 - dy; 260 | while (x <= x_ && y <= y_) { 261 | eraseChar(x, y, dx, dy); 262 | x += dx; 263 | y += dy; 264 | } 265 | eraseChar(line.x0, line.y0, dx, dy); 266 | return eraseChar(line.x1, line.y1, dx, dy); 267 | } else { 268 | return eraseChar(line.x0, line.y0, dx, dy); 269 | } 270 | }; 271 | figures = []; 272 | extractLine = function() { 273 | var ch, color, d, end, start, x0, x1, y0, y1; 274 | ch = findLineChar(); 275 | if (ch == null) { 276 | return false; 277 | } 278 | d = dir[data[ch.y][ch.x]]; 279 | x0 = ch.x; 280 | y0 = ch.y; 281 | color = null; 282 | while (isPartOfLine(x0 - d.x, y0 - d.y)) { 283 | x0 -= d.x; 284 | y0 -= d.y; 285 | if (color == null) { 286 | color = toColor(x0, y0); 287 | } 288 | } 289 | start = null; 290 | if (isLineEnding(x0 - d.x, y0 - d.y)) { 291 | x0 -= d.x; 292 | y0 -= d.y; 293 | start = data[y0][x0] === '*' ? 'circle' : 'arrow'; 294 | } 295 | x1 = ch.x; 296 | y1 = ch.y; 297 | while (isPartOfLine(x1 + d.x, y1 + d.y)) { 298 | x1 += d.x; 299 | y1 += d.y; 300 | if (color == null) { 301 | color = toColor(x1, y1); 302 | } 303 | } 304 | end = null; 305 | if (isLineEnding(x1 + d.x, y1 + d.y)) { 306 | x1 += d.x; 307 | y1 += d.y; 308 | end = data[y1][x1] === '*' ? 'circle' : 'arrow'; 309 | } 310 | line = new Line(x0, y0, start, x1, y1, end, color != null ? color : 'black'); 311 | figures.push(line); 312 | erase(line); 313 | if (start === 'arrow') { 314 | line.x0 -= d.x; 315 | line.y0 -= d.y; 316 | } 317 | if (end === 'arrow') { 318 | line.x1 += d.x; 319 | line.y1 += d.y; 320 | } 321 | return true; 322 | }; 323 | extractText = function() { 324 | var color, end, m, prev, ref2, results, start, text; 325 | results = []; 326 | for (y = m = 0, ref2 = height; 0 <= ref2 ? m < ref2 : m > ref2; y = 0 <= ref2 ? ++m : --m) { 327 | x = 0; 328 | results.push((function() { 329 | var results1; 330 | results1 = []; 331 | while (x < width) { 332 | if (data[y][x] === ' ') { 333 | results1.push(x++); 334 | } else { 335 | start = end = x; 336 | while (end < width && data[y][end] !== ' ') { 337 | end++; 338 | } 339 | text = data[y].slice(start, end).join(''); 340 | prev = figures[figures.length - 1]; 341 | if ((prev != null ? prev.constructor.name : void 0) === 'Text' && prev.x0 + prev.text.length + 1 === start) { 342 | prev.text = prev.text + " " + text; 343 | } else { 344 | color = 'black'; 345 | if (text[0] === '\\' && text[text.length - 1] === '\\') { 346 | text = text.substring(1, text.length - 1); 347 | color = '#666'; 348 | } 349 | figures.push(new Text(x, y, text, color)); 350 | } 351 | results1.push(x = end); 352 | } 353 | } 354 | return results1; 355 | })()); 356 | } 357 | return results; 358 | }; 359 | while (extractLine()) {} 360 | extractText(); 361 | return figures; 362 | }; 363 | 364 | var doc = document; 365 | 366 | var $ = function(id) { 367 | return doc.getElementById(id); 368 | }; 369 | 370 | var drawDiagram = function() { 371 | var canvas, ctx, figure, figures, height, j, k, len, len1, results, width; 372 | localStorage.setItem('leftPane', textarea.value); 373 | figures = parseASCIIArt($('textarea').value); 374 | width = 0; 375 | height = 0; 376 | for (j = 0, len = figures.length; j < len; j++) { 377 | figure = figures[j]; 378 | if (figure.constructor.name === 'Line') { 379 | width = Math.max(width, X(figure.x1 + 1)); 380 | height = Math.max(height, Y(figure.y1 + 1)); 381 | } 382 | } 383 | canvas = $('canvas'); 384 | canvas.width = width; 385 | canvas.height = height; 386 | ctx = new ShakyCanvas(canvas); 387 | results = []; 388 | for (k = 0, len1 = figures.length; k < len1; k++) { 389 | figure = figures[k]; 390 | results.push(figure.draw(ctx)); 391 | } 392 | return results; 393 | }; 394 | 395 | var textarea = $('textarea'); 396 | 397 | textarea.addEventListener('change', function() { 398 | ga('send', { 399 | hitType: 'event', 400 | eventCategory: 'general', 401 | eventAction: 'click', 402 | eventLabel: 'redraw' 403 | }); 404 | drawDiagram(); 405 | }); 406 | textarea.addEventListener('keyup', function() { 407 | ga('send', { 408 | hitType: 'event', 409 | eventCategory: 'general', 410 | eventAction: 'click', 411 | eventLabel: 'redraw' 412 | }); 413 | drawDiagram(); 414 | }); 415 | doc.addEventListener('DOMContentLoaded', drawDiagram, false); 416 | var oldText = localStorage.getItem('leftPane'); 417 | if (oldText) 418 | textarea.value = oldText; 419 | 420 | if (doc.fonts) 421 | doc.fonts.ready.then(drawDiagram); 422 | else 423 | setTimeout(drawDiagram, 2000); // Things you need to do for Edge and IE :( 424 | 425 | $('download').addEventListener('click', function() { 426 | ga('send', { 427 | hitType: 'event', 428 | eventCategory: 'general', 429 | eventAction: 'click', 430 | eventLabel: 'download' 431 | }); 432 | var dataURL = $('canvas').toDataURL('image/png'); 433 | $('download').href = dataURL; 434 | }, false); 435 | $('close').addEventListener('click', function() { 436 | $('overlay').style['display'] = 'none'; 437 | }, false); 438 | $('about').addEventListener('click', function() { 439 | ga('send', { 440 | hitType: 'event', 441 | eventCategory: 'general', 442 | eventAction: 'click', 443 | eventLabel: 'about' 444 | }); 445 | $('overlay').style['display'] = 'block'; 446 | }, false); 447 | --------------------------------------------------------------------------------