After some free ASCII flowchart drawer started charging money, I decided to write my own. Salient features are that you can draw a box (mouse down, mouse move, mouse up), then ctrlB will draw a box. Copy/Paste, Undo, Redo, click anywhere and type are all working features. I pasted the main code here, and it relies on two other minor files to provide a cursor and box object, but this code should still be very reviewable.I did run the code through JSHint and made a judgement call on the few remaining ickies. This is my first project with canvas that's not just a prototype, so any insights there are welcome. Finally, this will become a Chrome extension, so I only care about this working on Chrome. Except when I don't of course (which)./* Unidraw, because we can*///Documentation://  http://en.wikipedia.org/wiki/Box-drawing_character//Competition://  http://www.asciidraw.com/#Draw //  http://asciiflow.com/(function IIFE(){use strict;var canvas,    context,    clipboard;var model = (function(){  //Privates  var cells = [],      tabSize = 4;  //Exposed  var cursor = new Cursor();  function write( x, y, s )  {    //Make sure that we have an array for y    //Always assume overwrite mode    var originalX = x;    cells[y] = cells[y] || [];    for( var i = 0; i < s.length ; i++)    {      var c = s[i];      if( c.charCodeAt(0) > 31 )      {        cells[y][x++] = s[i];      }      else if ( c == \n )      {        y++;        cells[y] = cells[y] || [];        x = originalX;      }      else if ( c == '\t' )       {        x += tabSize;      }    }    return new Cursor( x, y );  }  function setCell( cursor , c )  {    return write( cursor.x , cursor.y , c );  }  function getCell( cursor )  {    return cells[cursor.y] ? cells[cursor.y][cursor.x] ||   :  ;  }  function stringify()  {    var s = '', x, y;    for( y = 0 ; y < cells.length ; y++ )    {      if( cells[y] )        for( x = 0 ; x < cells[y].length ; x++ )          s = s + ( cells[y][x] ||   );      s = s + '\n';    }    return s?s: ;  }  function backspace()  { //Move everything one character to the left of the cursor    if( cells[ model.cursor.y ] )      cells[ model.cursor.y ].splice( model.cursor.x-1 , 1 );    model.cursor.recede();      }  function addVersion( key )  { //Called internally. add a version to a version array (found with `key`)    var json = localStorage[key];    var versions = json ? JSON.parse( json ) : [];    versions.push( stringify() );    localStorage[key] = JSON.stringify( versions );      }  function getVersion( key )  { //Called internally, get a version (and remove it) from a version array    var json = localStorage[key];    var versions = json ? JSON.parse( json ) : [];    var version = versions.pop();    localStorage[key] = JSON.stringify( versions );          return version;  }  function storeVersion()  { //Called from controller, removes all redo versions    addVersion( 'undo' , stringify()  );    localStorage.removeItem( 'redo' );  }   function restoreVersion()  { //Called from controller, adds a redo version    var version = getVersion( 'undo' );    if(version){      addVersion( 'redo' );      cells = [];      write( 0 , 0 , version );    }  }  function redo()  { //Called from controller, puts version back on to undo    var version = getVersion( 'redo' );    if(version){      addVersion( 'undo' );      cells = [];      write( 0 , 0 , version );    }      }  function isLineCharacter(  cursor, dx , dy , returnValue )  {    cursor = { x: cursor.x + dx , y: cursor.y + dy };    return ~'><'.indexOf( getCell( cursor ) ) ? returnValue : 0;            }    //Modulify  return {    write: write,    stringify: stringify,    setCell: setCell,    getCell: getCell,    cursor: cursor,    backspace: backspace,    storeVersion: storeVersion,    restoreVersion: restoreVersion,    redo: redo,    isLineCharacter: isLineCharacter  };}());var ui = (function(){  //Privates  var fontSize = 15,      breatheDuration = 5 * 1000, //5 seconds      lightGrey = 211,      black = 0,      greyRange = lightGrey - black,      p = 20, //Padding..      magicalMultiplier = 0.8, //Dont ask      w,  //Width      h,  //Height      fh, //fontHeight      fw, //fontWidth      vo, //Vertical offset for writing      ho, //Horizontal offset for writing      metrics,      box;  //Exposed      function breathe()  { //Set the `caret` in a grey shade that follows a breathing cycle    var rightNow = new Date(),        position = rightNow % breatheDuration,        radians = position / breatheDuration * Math.PI,        sine = Math.sin( radians ),        shade = Math.floor( lightGrey - greyRange/2 + sine * greyRange / 2 ),        cx = model.cursor.x,        cy = model.cursor.y;    context.strokeStyle = 'rgb(' + shade + ',' + shade + ',' + shade + ')';                 context.lineWidth = 0.5;    context.beginPath();    context.moveTo(cx*fw + p, cy*fh + p);    context.lineTo(cx*fw + p + fw, cy*fh + p);    context.lineTo(cx*fw + p + fw, cy*fh + p + fh);    context.lineTo(cx*fw + p , cy*fh + p + fh);    context.lineTo(cx*fw + p, cy*fh + p);    context.stroke();  }  function drawBox()  {    context.strokeStyle = 'black';                 context.lineWidth = 0.5;    context.beginPath();    context.moveTo(box.from.x   *fw + p, box.from.y   *fh + p);  //Top left    context.lineTo((box.to.x+1) *fw + p, box.from.y   *fh + p);  //Top Right    context.lineTo((box.to.x+1) *fw + p, (box.to.y+1) *fh + p);  //Bottom Right    context.lineTo(box.from.x   *fw + p, (box.to.y+1) *fh + p);  //Bottom Left    context.lineTo(box.from.x   *fw + p, box.from.y   *fh + p);  //Top Left    context.stroke();          }  function setBox( cell1 , cell2 )  {    box = new Box( cell1 , cell2 );  }  function clearBox()  {    box = undefined;  }    function getBox()  {    return box;  }  function adapt()  { //Adapt the UI to the current size of the body    //Clearly, the UI maintains it's own model    w = canvas.width  = document.body.clientWidth;    h = canvas.height = window.innerHeight;    document.documentElement.style.overflow = 'hidden';    context.font = fontSize + (~navigator.userAgent.indexOf('Mac') ? px Consolas : px Monospace); //EVIL Mac Fix    metrics = context.measureText('A');    fh = fontSize+1;    fw = metrics.width;    vo = p+fh*magicalMultiplier;    ho = p;        drawGrid();      }  function drawGrid()  {    context.clearRect(0, 0, canvas.width, canvas.height);    for (var x = 0; x < w; x += fw)     {      context.moveTo(x + p, 0 + p);      context.lineTo(x + p, h );    }    for (var y = 0; y < h; y += fh)     {      context.moveTo(0 + p, y + p);      context.lineTo(w , y + p);    }    context.lineWidth = 0.1;    context.strokeStyle = lightgrey;    context.stroke();        context.strokeStyle = black;    context.fillStyle = black;        var string = model.stringify();    if( string ){      var strings = string.split(\n);      for( var row = 0 ; row < strings.length ; row++ )        for( var col = 0 ; col < strings[row].length ; col++ )          context.fillText( strings[row][col] , ho  + fw * col  , vo + fh * row );      }    if( box )      drawBox( box.from , box.to );  }  function translate( cursor )  { //Translate screen coordinates to cell coordinates    var x = Math.floor((cursor.x - p ) / fw ),        y = Math.floor((cursor.y - p ) / fh );    //Cheat on boundaries    x = x < 0 ? 0 : x;    y = y < 0 ? 0 : y;     //Return a new cell cursor object    return new Cursor(x,y);  }  //Modulify  return {    breathe : breathe,    drawGrid : drawGrid,    adapt: adapt,    translate: translate,    setBox: setBox,    clearBox: clearBox,    getBox: getBox  };}());var controller = (function(){    var BACKSPACE = 8,      TAB = 9,      ARROW_LEFT = 37,      ARROW_UP = 38,      ARROW_RIGHT = 39,      ARROW_DOWN = 40,      DELETE = 46,      KEY_B = 66,      KEY_C = 67,      KEY_Y = 89,      KEY_Z = 90;    var startingCell,      currentCell;  function normalizeEvent(e)  { //Normalize which for key events, inspiration:SO    if ( e.which === null && (e.charCode !== null || e.keyCode !== null) ) {      e.which = e.charCode !== null ? e.charCode : e.keyCode;    }      }  function onContentLoaded()  { //Could have been called onInit    //Set the 3 globals    canvas  = document.getElementById(canvas);    context = canvas.getContext (2d);    clipboard = document.getElementById('clipboard');    //Occupy full body & draw the initial UI    ui.adapt();    //Set up listeners    window.addEventListener( resize, ui.adapt );    canvas.addEventListener( mouseover, onMouseOver );    canvas.addEventListener( mousemove, onMouseOver );    canvas.addEventListener( mousedown, onMouseDown );    canvas.addEventListener( mouseup, onMouseUp );    canvas.addEventListener( click, onClick );    document.addEventListener( keypress, onKeyPress );    document.addEventListener( keydown, onKeyDown );    document.addEventListener( paste, onPaste );    //Make the cursor breathe    setInterval( ui.breathe , 1000/12 ); // 12 frames per second  }   function onPaste(e)  { //Determine where to paste, paste, determine & set new cursor location, redraw everything    var cursor = model.cursor;    model.cursor = model.write(  cursor.x , cursor.y ,  e.clipboardData.getData('text/plain') );    ui.drawGrid();  }  function onMouseDown(e)  {    //Remember where we start    startingCell = ui.translate( e );    //Clear any old boxes    ui.clearBox();    //Force the UI in onMouseOver to draw the new cursor without a mouse up    currentCell = { x : -1 , y : -1 };    onMouseOver(e);  }  function onMouseUp()  {    ui.setBox( startingCell , currentCell );    currentCell = startingCell = undefined;  }  function onMouseOver(e)  { //Are we dragging?, which cell are we on, update if we are in a different cell, and draw    if(!startingCell)      return;    var cell = ui.translate( e );    if( cell.x != currentCell.x || cell.y != currentCell.y )    {      currentCell = cell;      model.cursor =  cell;      ui.setBox( startingCell , currentCell );      ui.drawGrid();    }  }  function onClick(e)  { //Move the cursor to where the user clicked    model.cursor = ui.translate( e );    ui.drawGrid();  }  function onKeyPress(e)  { //console.log( e , String.fromCharCode( e.charCode || 32 ) );    if( e.ctrlKey  )      return;    model.storeVersion();    model.setCell( model.cursor, String.fromCharCode( e.charCode || 32 ) );    ui.clearBox();    model.cursor.advance();    ui.drawGrid();  }  function onKeyDown(e)  { //console.log( e , String.fromCharCode( e.charCode || 32 ) );    normalizeEvent(e);    var box = ui.getBox();    if( e.which == BACKSPACE )    {      model.backspace();      e.preventDefault();    }     else if( e.which == TAB )    {      model.cursor =  model.setCell( model.cursor , '\t' );      e.preventDefault();    }           else if( e.which == ARROW_LEFT ){      model.cursor.recede();    }        else if( e.which == ARROW_RIGHT ){      model.cursor.advance();    }    else if( e.which == ARROW_UP ){      model.cursor.up();         }    else if( e.which == ARROW_DOWN ){      model.cursor.down();         }          else if( e.keyIdentifier == 'Home' && e.ctrlKey ){      model.cursor = new Cursor( 0, 0 );    }    else if( e.keyIdentifier == 'Home' )    { //Move to complete left unless already there, in that case go top left      model.cursor.x ? model.cursor.x = 0 : model.cursor.y = 0;    }    else if( e.which == KEY_C && e.ctrlKey )    { //Copy a box or a whole character      if( box )      {        var lines = [];        box.eachRow( function(y){ lines[ y - box.from.y ] = ''; } );        box.each( function(cursor){ lines[ cursor.y - box.from.y ] += model.getCell(cursor); } );        var line = lines.join(\n);        clipboard.value = line;      }       else      {        clipboard.value = model.getCell( ui.getCursor() ) ||  ;        }      clipboard.focus();      clipboard.select();    }       else if( e.which == KEY_B && e.ctrlKey )    {      /* Styles:                                             */      var onLeft   = 1; //Bitflag 1      var onRight  = 2; //Bitflag 2      var onTop    = 4; //Bitflag 3      var onBottom = 8; //Bitflag 4      var lineRules = {};      lineRules[onLeft+onRight] = '';      lineRules[onTop+onBottom] = '';      lineRules[onTop+onLeft] = '';      lineRules[onTop+onRight] = '';      lineRules[onBottom+onLeft] = '';      lineRules[onBottom+onRight] = '';              lineRules[onLeft+onRight+onTop+onBottom] = '';      lineRules[onLeft+onRight+onTop] = '';      lineRules[onLeft+onRight+onBottom] = '';      lineRules[onTop+onBottom+onLeft] = '';      lineRules[onTop+onBottom+onRight] = '';      if( box )      {        model.storeVersion();        //Show intent        box.eachRow( function(y){ model.write( box.from.x, y, '' ); model.write( box.to.x, y , '' ); } );        box.eachColumn( function(x){ model.write( x, box.from.y, '' ); model.write( x, box.to.y , '' ); } );        //Line up        box.each(  function lineUp( cursor )        {          if( !model.isLineCharacter( cursor , 0 , 0 , true ) )            return;          var neighbourBitFlag =              model.isLineCharacter( cursor , -1 , +0 , onLeft ) +             model.isLineCharacter( cursor , +1 , +0 , onRight ) +             model.isLineCharacter( cursor , +0 , +1 , onBottom ) +             model.isLineCharacter( cursor , +0 , -1 , onTop );            if( lineRules[neighbourBitFlag] )              model.setCell( cursor , lineRules[neighbourBitFlag] );          });        }       }            else if ( e.which == DELETE )      {        if( box ){          box.each( function(cursor){ model.setCell( cursor,   ); } );        }      }      else if ( e.which == KEY_Z && e.ctrlKey )      { //Undo        model.restoreVersion();      }            else if ( e.which == KEY_Y && e.ctrlKey )      { //Undo        model.redo();      }                  //Clear the selection box after a key press (Control does not count)      if( e.keyIdentifier != Control && box )      {        ui.clearBox();      }      //Draw the grid in all cases      ui.drawGrid();  }  return {    onContentLoaded: onContentLoaded,  };}());//Engage!document.addEventListener( DOMContentLoaded, controller.onContentLoaded, false );})();A plunker can be found here.
ASCII flow chart drawer
javascript;google chrome;ascii art
I like it! I've got some quibbles about the style, but that's personal opinion, and the code works, so I won't go into that. I haven't gone through the code line-by-line (there's a lot!), but I've tried looking at the overall structure.If I were to suggest something, it might be a more declarative way to handle events, and keyboard event in particular. It's a minor thing, but the sort of thing I find more readable/direct.You've got a lot of functions that are simply named for the event they handle, but then you have to repeat that name when attaching them to events. I'd consider defining them as properties on an object, and then loop through them, e.g.var canvasEvents = {  mouseover: function () { ... }  mousemove: function () { ... }  mousedown: function () { ... }  mouseup: function () { ... }  click: function () { ... }};for(var event in canvasEvents) {  canvas.addEventListener(event, canvasEvents[event]);}Again, it's a minor thing, but defining the event handlers in one place and have them automatically attached would keep things nicely contained, I think.And you could do a similar thing sort of thing for keyboard events, to avoid the large else if... else if... structure. For instance,// Not the complete list - just a sampling// Order still matters of course, so ctrl+Home gets matches before Homevar keyCommands = [  {    mask: { which: BACKSPACE },    preventDefault: true,    handler: model.backspace  },  {    mask: { which: ARROW_UP },    handler: model.cursor.up  },  {    mask: { keyIdentifier: 'Home', ctrlKey: true },    handler: function () { model.cursor = new Cursor( 0, 0 ) }  },  {    mask: { keyIdentifier: 'Home' },    handler: function () { model.cursor.x ? model.cursor.x = 0 : model.cursor.y = 0 }  },  {    mask: { which: KEY_C, ctrlKey: true },    handler: function () { /*... etc ...*/ }  },  ....];// ....function handleKeyCombo(event) {  normalizeEvent(event);  function matchMask(mask) {    for( var property in mask ) {      if(event[property] != mask[property]) { // maybe use a strict comparison; your call        return false;      }    }    return true;  }  for( var i = 0, l = keyCommands.length ; i < l ; i++ ) {    var command = keyCommands[i];    if( matchMask(command.mask) ) {      command.handler(event); // or use call/apply if necessary      if(command.preventDefault) {        event.preventDefault();      }      break;    }  }}(note that you'll have to figure out a way to pass the local var box to the handlers, now that they're defined in a different scope)Aside: It might be a good use-case for the Map object, if available - masks as keys, handlers as values?Alternatively, you could also make an addKeyboardShortcutListener function that works similar to addEventListener, but accepts the key-combo mask as well as a handler.Basically, you can go more or less in-depth with this, but it'd make it (hopefully) easier to set up keyboard-handling, and (with some modification) use different key combos depending on platform.For instance, on the Mac, cmd is used instead of ctrl, but checking metaKey instead of ctrlKey isn't always enough. Something like undo is cmd+shift+Z by convention, and cmd + arrows is used for most navigation (though Home/End works too, and sometimes emacs-style combos too). Not that I'm demanding Mac support, but in general it might be nice to make the key-combo mapping more flexible.You've got a nice separation of model, ui, controller and so forth, but perhaps a bit more encapsulation within each of those would be nice, such as abstracting/encapsulating the key-combo matching.Semi-related: A long time ago, I wrote something to handle keyboard shortcuts. Maybe you can use it for something. I'm linking it mostly because for some complex combinations, key events start to get weird. I don't think the code quite works right anymore, but the technique might still be viable.Oh, and one thing I noticed is that making a 1-column vertical selection and drawing it, gave me something like:which seems a little off. I expected it to only draw vertical pipe glyphs, or, if the top and bottom should be flat, use the 3-way pipe glyphs for the ends:        or        