Updated project to use es6 classes, arrow functions, latest closure-compiler, latest jquery (#77)

* Update jquer and closure compiler

* Move to es6 classes, arrow functions, for/of

* Move to es6 style modules

* Add .DS_Store to .gitignore

* Remove note to update compile.sh
This commit is contained in:
Brian Schlenker 2017-01-21 06:55:18 -08:00 committed by Lewis Hemens
parent 8a50b5ab58
commit 141b8eff92
19 changed files with 4809 additions and 2692 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ closure-library/*
.settings/*
*~
_site/*
.DS_Store

1
README
View File

@ -13,4 +13,3 @@ Goto: http://localhost:8000/index.html
When developing, use the Google JS linter, gjslint.
Adding new source files will require a change to compile.sh.

BIN
closure-compiler.jar Executable file → Normal file

Binary file not shown.

View File

@ -1,13 +1,5 @@
java -client -jar closure-compiler.jar \
--js js-lib/common.js \
--js js-lib/view.js \
--js js-lib/draw.js \
--js js-lib/draw-select.js \
--js js-lib/state.js \
--js js-lib/controller.js \
--js js-lib/drive-controller.js \
--js js-lib/input-controller.js \
--js js-lib/launch.js \
--warning_level=VERBOSE --formatting=PRETTY_PRINT --language_in=ECMASCRIPT5 --compilation_level=ADVANCED_OPTIMIZATIONS \
--externs=jquery-1.9-externs.js \
--js js-lib/*.js \
--warning_level=VERBOSE --formatting=PRETTY_PRINT --language_in=ECMASCRIPT6 --compilation_level=ADVANCED_OPTIMIZATIONS \
--externs=jquery-3.1-externs.js \
> js-compiled.js

View File

@ -90,7 +90,7 @@ button {
}
.info-description {
vertical-align: text-bottom;
vertical-align: text-bottom;
margin-left: 10px;
display: inline-block;
height: 30px;
@ -141,7 +141,7 @@ button {
text-align: center;
padding-left: 0px;
}
/* Move file tools to the bottom. */
#file-tools {
left: 0px;
@ -557,7 +557,7 @@ textarea {
<div class="dialog-button-bar">
<button class="close-dialog-button">Close</button>
<button id="import-submit-button">Import</button>
</div>
</div>
</div>
<!-- These dialogs are handled seperately. -->
@ -587,7 +587,7 @@ textarea {
<canvas id="ascii-canvas"></canvas>
<script src="jquery-1.9.1.min.js"></script>
<script src="jquery-3.1.1.min.js"></script>
<script src="js-compiled.js"></script>
<script src="https://apis.google.com/js/client.js?onload=window.gapiCallback"></script>

2045
jquery-3.1-externs.js Normal file

File diff suppressed because it is too large Load Diff

4
jquery-3.1.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -2,241 +2,140 @@
* Common classes and constants.
*/
// Define namespace for closure compiler but don't make it a requirement.
try {
goog.provide('ascii');
throw 1;
} catch (e) {
/** type {Object} */
window.ascii = window.ascii || {};
}
/** @const */ var MAX_GRID_WIDTH = 2000;
/** @const */ var MAX_GRID_HEIGHT = 600;
/** @const */ var SPECIAL_VALUE = '+';
/** @const */ var ALT_SPECIAL_VALUE = '^';
/** @const */ var SPECIAL_ARROW_LEFT = '<';
/** @const */ var SPECIAL_ARROW_UP = '^';
/** @const */ var SPECIAL_ARROW_RIGHT = '>';
/** @const */ var SPECIAL_ARROW_DOWN = 'v';
/** @const */ var SPECIAL_VALUES = ['+', '\u2012', '\u2013', '-', '|'];
/** @const */ var ALT_SPECIAL_VALUES = ['>', '<', '^', 'v'];
/** @const */ var ALL_SPECIAL_VALUES = SPECIAL_VALUES.concat(ALT_SPECIAL_VALUES);
/** @const */ var MAX_UNDO = 50;
/** @const */ var SPECIAL_LINE_H = '-';
/** @const */ var SPECIAL_LINE_V = '|';
/** @const */ var ERASE_CHAR = '\u2009';
/** @const */ var DRAG_LATENCY = 150; // Milliseconds.
/** @const */ var DRAG_ACCURACY = 6; // Pixels.
/** @const */ var CHAR_PIXELS_H = 9;
/** @const */ var CHAR_PIXELS_V = 17;
/** @const */ var RENDER_PADDING_CELLS = 3;
/** @const */ var KEY_RETURN = '<enter>';
/** @const */ var KEY_BACKSPACE = '<backspace>';
/** @const */ var KEY_COPY = '<copy>';
/** @const */ var KEY_PASTE = '<paste>';
/** @const */ var KEY_CUT = '<cut>';
/** @const */ var KEY_UP = '<up>';
/** @const */ var KEY_DOWN = '<down>';
/** @const */ var KEY_LEFT = '<left>';
/** @const */ var KEY_RIGHT = '<right>';
// http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
/** @const */ var TOUCH_ENABLED =
'ontouchstart' in window ||
'onmsgesturechange' in window;
/**
* Stores a 2D vector.
*
* @constructor
* @param {number} x
* @param {number} y
*/
ascii.Vector = function(x, y) {
/** type {Number} */ this.x = x;
/** type {Number} */ this.y = y;
};
/**
* @param {ascii.Vector} other
* @return {boolean}
*/
ascii.Vector.prototype.equals = function(other) {
return (other != null) && (this.x == other.x) && (this.y == other.y);
};
/**
* @param {ascii.Vector} other
* @return {ascii.Vector}
*/
ascii.Vector.prototype.subtract = function(other) {
return new ascii.Vector(this.x - other.x, this.y - other.y);
};
/**
* @param {ascii.Vector} other
* @return {ascii.Vector}
*/
ascii.Vector.prototype.add = function(other) {
return new ascii.Vector(this.x + other.x, this.y + other.y);
};
/**
* @return {ascii.Vector}
*/
ascii.Vector.prototype.clone = function() {
return new ascii.Vector(this.x, this.y);
};
/** @return {number} */
ascii.Vector.prototype.length = function() {
return Math.sqrt(this.x * this.x + this.y * this.y);
};
/**
* @param {number} scale
* @return {ascii.Vector}
*/
ascii.Vector.prototype.scale = function(scale) {
return new ascii.Vector(this.x * scale, this.y * scale);
};
import { ERASE_CHAR, ALL_SPECIAL_VALUES } from './constants';
import Vector from './vector';
/**
* Represents a box with normalized position vectors.
*
* @constructor
* @param {ascii.Vector} a
* @param {ascii.Vector} b
*/
ascii.Box = function(a, b) {
/** type {Number} */ this.startX = Math.min(a.x, b.x);
/** type {Number} */ this.startY = Math.min(a.y, b.y);
/** type {Number} */ this.endX = Math.max(a.x, b.x);
/** type {Number} */ this.endY = Math.max(a.y, b.y);
};
export class Box {
/**
* @param {Vector} a
* @param {Vector} b
*/
constructor(a, b) {
/** type {number} */ this.startX = Math.min(a.x, b.x);
/** type {number} */ this.startY = Math.min(a.y, b.y);
/** type {number} */ this.endX = Math.max(a.x, b.x);
/** type {number} */ this.endY = Math.max(a.y, b.y);
}
/** @return {ascii.Vector} */
ascii.Box.prototype.topLeft = function() {
return new ascii.Vector(this.startX, this.startY);
};
/** @return {Vector} */
topLeft() {
return new Vector(this.startX, this.startY);
}
/** @return {ascii.Vector} */
ascii.Box.prototype.bottomRight = function() {
return new ascii.Vector(this.endX, this.endY);
};
/** @return {Vector} */
bottomRight() {
return new Vector(this.endX, this.endY);
}
/** @return {boolean} */
ascii.Box.prototype.contains = function(position) {
return position.x >= this.startX && position.x <= this.endX && position.y >= this.startY && position.y <= this.endY;
};
/** @const */ var DIR_LEFT = new ascii.Vector(-1, 0);
/** @const */ var DIR_RIGHT = new ascii.Vector(1, 0);
/** @const */ var DIR_UP = new ascii.Vector(0, -1);
/** @const */ var DIR_DOWN = new ascii.Vector(0, 1);
/** @const */ var DIRECTIONS = [DIR_LEFT, DIR_RIGHT, DIR_UP, DIR_DOWN];
/** @return {boolean} */
contains(position) {
return position.x >= this.startX && position.x <= this.endX
&& position.y >= this.startY && position.y <= this.endY;
}
}
/**
* An individual cell within the diagram and it's current value.
*
* @constructor
*/
ascii.Cell = function() {
/** @type {?string} */ this.value = null;
/** @type {?string} */ this.scratchValue = null;
};
export class Cell {
/** @return {?string} */
ascii.Cell.prototype.getRawValue = function() {
return (this.scratchValue != null ? this.scratchValue : this.value);
};
constructor() {
/** @type {?string} */ this.value = null;
/** @type {?string} */ this.scratchValue = null;
}
/** @return {boolean} */
ascii.Cell.prototype.isSpecial = function() {
return ALL_SPECIAL_VALUES.indexOf(this.getRawValue()) != -1;
};
/** @return {?string} */
getRawValue() {
return (this.scratchValue != null ? this.scratchValue : this.value);
}
/** @return {boolean} */
ascii.Cell.prototype.isEmpty = function() {
return this.value == null && this.scratchValue == null;
};
/** @return {boolean} */
isSpecial() {
return ALL_SPECIAL_VALUES.indexOf(this.getRawValue()) != -1;
}
/** @return {boolean} */
ascii.Cell.prototype.hasScratch = function() {
return this.scratchValue != null;
};
/** @return {boolean} */
isEmpty() {
return this.value == null && this.scratchValue == null;
}
/** @return {boolean} */
ascii.Cell.prototype.isErase = function() {
return this.scratchValue == ERASE_CHAR;
};
/** @return {boolean} */
hasScratch() {
return this.scratchValue != null;
}
/** @return {boolean} */
isErase() {
return this.scratchValue == ERASE_CHAR;
}
}
/**
* The context for a cell, i.e. the status of the cells around it.
*
* @param {boolean} left
* @param {boolean} right
* @param {boolean} up
* @param {boolean} down
* @constructor
*/
ascii.CellContext = function(left, right, up, down) {
/** @type {boolean} */ this.left = left;
/** @type {boolean} */ this.right = right;
/** @type {boolean} */ this.up = up;
/** @type {boolean} */ this.down = down;
/** @type {boolean} */ this.leftup = false;
/** @type {boolean} */ this.rightup = false;
/** @type {boolean} */ this.leftdown = false;
/** @type {boolean} */ this.rightdown = false;
};
export class CellContext {
/**
* @param {boolean} left
* @param {boolean} right
* @param {boolean} up
* @param {boolean} down
*/
constructor(left, right, up, down) {
/** @type {boolean} */ this.left = left;
/** @type {boolean} */ this.right = right;
/** @type {boolean} */ this.up = up;
/** @type {boolean} */ this.down = down;
/** @type {boolean} */ this.leftup = false;
/** @type {boolean} */ this.rightup = false;
/** @type {boolean} */ this.leftdown = false;
/** @type {boolean} */ this.rightdown = false;
}
/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
ascii.CellContext.prototype.sum = function() {
return this.left + this.right + this.up + this.down;
};
/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
sum() {
return this.left + this.right + this.up + this.down;
}
/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
ascii.CellContext.prototype.extendedSum = function() {
return this.left + this.right + this.up + this.down + this.leftup + this.leftdown + this.rightup + this.rightdown;
};
/**
* Returns the total number of surrounding special cells.
* @return {number}
*/
extendedSum() {
return this.left + this.right + this.up + this.down
+ this.leftup + this.leftdown + this.rightup + this.rightdown;
}
}
/**
* A pair of a vector and a string value. Used in history management.
* @constructor
* @struct
* @param {ascii.Vector} position
* @param {string|null} value
*/
ascii.MappedValue = function(position, value) {
this.position = position;
this.value = value;
};
export class MappedValue {
/**
* @param {Vector} position
* @param {string|null} value
*/
constructor(position, value) {
this.position = position;
this.value = value;
}
}
/**
* A pair of a vector and a cell. Used in history management.
* @constructor
* @struct
* @param {ascii.Vector} position
* @param {ascii.Cell} cell
*/
ascii.MappedCell = function(position, cell) {
this.position = position;
this.cell = cell;
};
export class MappedCell {
/**
* @param {Vector} position
* @param {Cell} cell
*/
constructor(position, cell) {
this.position = position;
this.cell = cell;
}
}

51
js-lib/constants.js Normal file
View File

@ -0,0 +1,51 @@
import Vector from './vector';
export const MAX_GRID_WIDTH = 2000;
export const MAX_GRID_HEIGHT = 600;
export const SPECIAL_VALUE = '+';
export const ALT_SPECIAL_VALUE = '^';
export const SPECIAL_ARROW_LEFT = '<';
export const SPECIAL_ARROW_UP = '^';
export const SPECIAL_ARROW_RIGHT = '>';
export const SPECIAL_ARROW_DOWN = 'v';
export const SPECIAL_VALUES = ['+', '\u2012', '\u2013', '-', '|'];
export const ALT_SPECIAL_VALUES = ['>', '<', '^', 'v'];
export const ALL_SPECIAL_VALUES = SPECIAL_VALUES.concat(ALT_SPECIAL_VALUES);
export const MAX_UNDO = 50;
export const SPECIAL_LINE_H = '-';
export const SPECIAL_LINE_V = '|';
export const ERASE_CHAR = '\u2009';
export const DRAG_LATENCY = 150; // Milliseconds.
export const DRAG_ACCURACY = 6; // Pixels.
export const CHAR_PIXELS_H = 9;
export const CHAR_PIXELS_V = 17;
export const RENDER_PADDING_CELLS = 3;
export const KEY_RETURN = '<enter>';
export const KEY_BACKSPACE = '<backspace>';
export const KEY_COPY = '<copy>';
export const KEY_PASTE = '<paste>';
export const KEY_CUT = '<cut>';
export const KEY_UP = '<up>';
export const KEY_DOWN = '<down>';
export const KEY_LEFT = '<left>';
export const KEY_RIGHT = '<right>';
// http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
export const TOUCH_ENABLED =
'ontouchstart' in window ||
'onmsgesturechange' in window;
export const DIR_LEFT = new Vector(-1, 0);
export const DIR_RIGHT = new Vector( 1, 0);
export const DIR_UP = new Vector( 0, -1);
export const DIR_DOWN = new Vector( 0, 1);
export const DIRECTIONS = [DIR_LEFT, DIR_RIGHT, DIR_UP, DIR_DOWN];

View File

@ -1,8 +1,23 @@
import * as c from './constants';
import Vector from './vector';
import View from './view';
import State from './state';
import DrawSelect from './draw-select';
import {
DrawFunction,
DrawBox,
DrawLine,
DrawFreeform,
DrawErase,
DrawMove,
DrawText,
} from './draw';
/**
* Different modes of control.
* @const
*/
var Mode = {
const Mode = {
NONE: 0,
DRAG: 1,
DRAW: 2
@ -10,253 +25,256 @@ var Mode = {
/**
* Handles user input events and modifies state.
*
* @constructor
* @param {ascii.View} view
* @param {ascii.State} state
*/
ascii.Controller = function(view, state) {
/** @type {ascii.View} */ this.view = view;
/** @type {ascii.State} */ this.state = state;
export default class Controller {
/**
* @param {View} view
* @param {State} state
*/
constructor(view, state) {
/** @type {View} */ this.view = view;
/** @type {State} */ this.state = state;
/** @type {ascii.DrawFunction} */ this.drawFunction =
new ascii.DrawBox(state);
/** @type {DrawFunction} */ this.drawFunction = new DrawBox(state);
/** @type {number} */ this.mode = Mode.NONE;
/** @type {ascii.Vector} */ this.dragOrigin;
/** @type {ascii.Vector} */ this.dragOriginCell;
/** @type {number} */ this.mode = Mode.NONE;
/** @type {Vector} */ this.dragOrigin;
/** @type {Vector} */ this.dragOriginCell;
this.installBindings();
};
/** @type {Vector} */ this.lastMoveCell = null;
/**
* @param {ascii.Vector} position
*/
ascii.Controller.prototype.startDraw = function(position) {
this.mode = Mode.DRAW;
this.drawFunction.start(this.view.screenToCell(position));
};
this.installBindings();
}
/**
* @param {ascii.Vector} position
*/
ascii.Controller.prototype.startDrag = function(position) {
this.mode = Mode.DRAG;
this.dragOrigin = position;
this.dragOriginCell = this.view.offset;
};
/**
* @param {Vector} position
*/
startDraw(position) {
this.mode = Mode.DRAW;
this.drawFunction.start(this.view.screenToCell(position));
}
/**
* @param {ascii.Vector} position
*/
ascii.Controller.prototype.handleMove = function(position) {
var moveCell = this.view.screenToCell(position);
/**
* @param {Vector} position
*/
startDrag(position) {
this.mode = Mode.DRAG;
this.dragOrigin = position;
this.dragOriginCell = this.view.offset;
}
// First move event, make sure we don't blow up here.
if (this.lastMoveCell == null) {
/**
* @param {Vector} position
*/
handleMove(position) {
var moveCell = this.view.screenToCell(position);
// First move event, make sure we don't blow up here.
if (this.lastMoveCell == null) {
this.lastMoveCell = moveCell;
}
// Update the cursor pointer, depending on the draw function.
if (!moveCell.equals(this.lastMoveCell)) {
this.view.canvas.style.cursor = this.drawFunction.getCursor(moveCell);
}
// In drawing mode, so pass the mouse move on, but remove duplicates.
if (this.mode == Mode.DRAW && !moveCell.equals(this.lastMoveCell)) {
this.drawFunction.move(moveCell);
}
// Drag in progress, update the view origin.
if (this.mode == Mode.DRAG) {
this.view.setOffset(this.dragOriginCell.add(
this.dragOrigin
.subtract(position)
.scale(1 / this.view.zoom)));
}
this.lastMoveCell = moveCell;
}
// Update the cursor pointer, depending on the draw function.
if (!moveCell.equals(this.lastMoveCell)) {
this.view.canvas.style.cursor = this.drawFunction.getCursor(moveCell);
/**
* Ends the current operation.
*/
endAll() {
if (this.mode == Mode.DRAW) {
this.drawFunction.end();
}
// Cleanup state.
this.mode = Mode.NONE;
this.dragOrigin = null;
this.dragOriginCell = null;
this.lastMoveCell = null;
}
// In drawing mode, so pass the mouse move on, but remove duplicates.
if (this.mode == Mode.DRAW && !moveCell.equals(this.lastMoveCell)) {
this.drawFunction.move(moveCell);
/**
* Installs input bindings for common use cases devices.
*/
installBindings() {
$(window).resize(e => { this.view.resizeCanvas() });
$('#draw-tools > button.tool').click(e => {
$('#text-tool-widget').hide(0);
this.handleDrawButton(e.target.id);
});
$('#file-tools > button.tool').click(e => {
this.handleFileButton(e.target.id);
});
$('button.close-dialog-button').click(e => {
$('.dialog').removeClass('visible');
});
$('#import-submit-button').click(e => {
this.state.clear();
this.state.fromText(
/** @type {string} */
($('#import-area').val()),
this.view.screenToCell(new Vector(
this.view.canvas.width / 2,
this.view.canvas.height / 2)));
this.state.commitDraw();
$('#import-area').val('');
$('.dialog').removeClass('visible');
});
$('#use-lines-button').click(e => {
$('.dialog').removeClass('visible');
this.view.setUseLines(true);
});
$('#use-ascii-button').click(e => {
$('.dialog').removeClass('visible');
this.view.setUseLines(false);
});
$(window).keypress(e => {
this.handleKeyPress(e);
});
$(window).keydown(e => {
this.handleKeyDown(e);
});
// Bit of a hack, just triggers the text tool to get a new value.
$('#text-tool-input, #freeform-tool-input').keyup(() => {
this.drawFunction.handleKey('');
});
$('#text-tool-input, #freeform-tool-input').change(() => {
this.drawFunction.handleKey('');
});
$('#text-tool-close').click(() => {
$('#text-tool-widget').hide();
this.state.commitDraw();
});
}
// Drag in progress, update the view origin.
if (this.mode == Mode.DRAG) {
this.view.setOffset(this.dragOriginCell.add(
this.dragOrigin
.subtract(position)
.scale(1 / this.view.zoom)));
}
this.lastMoveCell = moveCell;
};
/**
* Ends the current operation.
*/
ascii.Controller.prototype.endAll = function() {
if (this.mode == Mode.DRAW) {
this.drawFunction.end();
}
// Cleanup state.
this.mode = Mode.NONE;
this.dragOrigin = null;
this.dragOriginCell = null;
this.lastMoveCell = null;
};
/**
* Installs input bindings for common use cases devices.
*/
ascii.Controller.prototype.installBindings = function() {
var controller = this;
$(window).resize(function(e) { controller.view.resizeCanvas() });
$('#draw-tools > button.tool').click(function(e) {
$('#text-tool-widget').hide(0);
this.handleDrawButton(e.target.id);
}.bind(this));
$('#file-tools > button.tool').click(function(e) {
this.handleFileButton(e.target.id);
}.bind(this));
$('button.close-dialog-button').click(function(e) {
/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
handleDrawButton(id) {
$('#draw-tools > button.tool').removeClass('active');
$('#' + id).toggleClass('active');
$('.dialog').removeClass('visible');
}.bind(this));
$('#import-submit-button').click(function(e) {
this.state.clear();
this.state.fromText($('#import-area').val(),
this.view.screenToCell(new ascii.Vector(
this.view.canvas.width / 2,
this.view.canvas.height / 2)));
// Install the right draw tool based on button pressed.
if (id == 'box-button') {
this.drawFunction = new DrawBox(this.state);
}
if (id == 'line-button') {
this.drawFunction = new DrawLine(this.state, false);
}
if (id == 'arrow-button') {
this.drawFunction = new DrawLine(this.state, true);
}
if (id == 'freeform-button') {
this.drawFunction = new DrawFreeform(this.state, "X");
}
if (id == 'erase-button') {
this.drawFunction = new DrawErase(this.state);
}
if (id == 'move-button') {
this.drawFunction = new DrawMove(this.state);
}
if (id == 'text-button') {
this.drawFunction = new DrawText(this.state, this.view);
}
if (id == 'select-button') {
this.drawFunction = new DrawSelect(this.state);
}
this.state.commitDraw();
$('#import-area').val('');
this.view.canvas.focus();
}
/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
handleFileButton(id) {
$('.dialog').removeClass('visible');
}.bind(this));
$('#' + id + '-dialog').toggleClass('visible');
$('#use-lines-button').click(function(e) {
$('.dialog').removeClass('visible');
this.view.setUseLines(true);
}.bind(this));
if (id == 'import-button') {
$('#import-area').val('');
$('#import-area').focus();
}
$('#use-ascii-button').click(function(e) {
$('.dialog').removeClass('visible');
this.view.setUseLines(false);
}.bind(this));
$(window).keypress(function(e) {
this.handleKeyPress(e);
}.bind(this));
$(window).keydown(function(e) {
this.handleKeyDown(e);
}.bind(this));
// Bit of a hack, just triggers the text tool to get a new value.
$('#text-tool-input, #freeform-tool-input').keyup(function(){
this.drawFunction.handleKey('');
}.bind(this));
$('#text-tool-input, #freeform-tool-input').change(function(){
this.drawFunction.handleKey('');
}.bind(this));
$('#text-tool-close').click(function(){
$('#text-tool-widget').hide();
this.state.commitDraw();
}.bind(this));
};
/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
ascii.Controller.prototype.handleDrawButton = function(id) {
$('#draw-tools > button.tool').removeClass('active');
$('#' + id).toggleClass('active');
$('.dialog').removeClass('visible');
// Install the right draw tool based on button pressed.
if (id == 'box-button') {
this.drawFunction = new ascii.DrawBox(this.state);
}
if (id == 'line-button') {
this.drawFunction = new ascii.DrawLine(this.state, false);
}
if (id == 'arrow-button') {
this.drawFunction = new ascii.DrawLine(this.state, true);
}
if (id == 'freeform-button') {
this.drawFunction = new ascii.DrawFreeform(this.state, "X");
}
if (id == 'erase-button') {
this.drawFunction = new ascii.DrawErase(this.state);
}
if (id == 'move-button') {
this.drawFunction = new ascii.DrawMove(this.state);
}
if (id == 'text-button') {
this.drawFunction = new ascii.DrawText(this.state, this.view);
}
if (id == 'select-button') {
this.drawFunction = new ascii.DrawSelect(this.state);
}
this.state.commitDraw();
this.view.canvas.focus();
};
/**
* Handles the buttons in the UI.
* @param {string} id The ID of the element clicked.
*/
ascii.Controller.prototype.handleFileButton = function(id) {
$('.dialog').removeClass('visible');
$('#' + id + '-dialog').toggleClass('visible');
if (id == 'import-button') {
$('#import-area').val('');
$('#import-area').focus();
if (id == 'export-button') {
$('#export-area').val(this.state.outputText());
$('#export-area').select();
}
if (id == 'clear-button') {
this.state.clear();
}
if (id == 'undo-button') {
this.state.undo();
}
if (id == 'redo-button') {
this.state.redo();
}
}
if (id == 'export-button') {
$('#export-area').val(this.state.outputText());
$('#export-area').select();
}
if (id == 'clear-button') {
this.state.clear();
}
if (id == 'undo-button') {
this.state.undo();
}
if (id == 'redo-button') {
this.state.redo();
}
};
/**
* Handles key presses.
* @param {Object} event
*/
ascii.Controller.prototype.handleKeyPress = function(event) {
if (!event.ctrlKey && !event.metaKey && event.keyCode != 13) {
this.drawFunction.handleKey(String.fromCharCode(event.keyCode));
}
};
/**
* Handles key down events.
* @param {Object} event
*/
ascii.Controller.prototype.handleKeyDown = function(event) {
// Override some special characters so that they can be handled in one place.
var specialKeyCode = null;
if (event.ctrlKey || event.metaKey) {
if (event.keyCode == 67) { specialKeyCode = KEY_COPY; }
if (event.keyCode == 86) { specialKeyCode = KEY_PASTE; }
if (event.keyCode == 90) { this.state.undo(); }
if (event.keyCode == 89) { this.state.redo(); }
if (event.keyCode == 88) { specialKeyCode = KEY_CUT; }
/**
* Handles key presses.
* @param {jQuery.Event} event
*/
handleKeyPress(event) {
if (!event.ctrlKey && !event.metaKey && event.keyCode != 13) {
this.drawFunction.handleKey(String.fromCharCode(event.keyCode));
}
}
if (event.keyCode == 8) { specialKeyCode = KEY_BACKSPACE; }
if (event.keyCode == 13) { specialKeyCode = KEY_RETURN; }
if (event.keyCode == 38) { specialKeyCode = KEY_UP; }
if (event.keyCode == 40) { specialKeyCode = KEY_DOWN; }
if (event.keyCode == 37) { specialKeyCode = KEY_LEFT; }
if (event.keyCode == 39) { specialKeyCode = KEY_RIGHT; }
/**
* Handles key down events.
* @param {jQuery.Event} event
*/
handleKeyDown(event) {
// Override some special characters so that they can be handled in one place.
var specialKeyCode = null;
if (specialKeyCode != null) {
//event.preventDefault();
//event.stopPropagation();
this.drawFunction.handleKey(specialKeyCode);
if (event.ctrlKey || event.metaKey) {
if (event.keyCode == 67) { specialKeyCode = c.KEY_COPY; }
if (event.keyCode == 86) { specialKeyCode = c.KEY_PASTE; }
if (event.keyCode == 90) { this.state.undo(); }
if (event.keyCode == 89) { this.state.redo(); }
if (event.keyCode == 88) { specialKeyCode = c.KEY_CUT; }
}
if (event.keyCode == 8) { specialKeyCode = c.KEY_BACKSPACE; }
if (event.keyCode == 13) { specialKeyCode = c.KEY_RETURN; }
if (event.keyCode == 38) { specialKeyCode = c.KEY_UP; }
if (event.keyCode == 40) { specialKeyCode = c.KEY_DOWN; }
if (event.keyCode == 37) { specialKeyCode = c.KEY_LEFT; }
if (event.keyCode == 39) { specialKeyCode = c.KEY_RIGHT; }
if (specialKeyCode != null) {
//event.preventDefault();
//event.stopPropagation();
this.drawFunction.handleKey(specialKeyCode);
}
}
};
}

View File

@ -1,138 +1,147 @@
import * as c from './constants';
import Vector from './vector';
import State from './state';
import { MappedValue, Box } from './common';
import { DrawFunction, DrawErase } from './draw';
/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawSelect = function(state) {
this.state = state;
/** @type {ascii.Vector} */
this.startPosition = null;
/** @type {ascii.Vector} */
this.endPosition = null;
/** @type {ascii.Vector} */
this.dragStart = null;
/** @type {ascii.Vector} */
this.dragEnd = null;
/** @type {boolean} */
this.finished = true;
/** @type {Array.<ascii.MappedValue>} */
this.selectedCells = null;
};
/** @inheritDoc */
ascii.DrawSelect.prototype.start = function(position) {
// Must be dragging.
if (this.startPosition != null &&
this.endPosition != null &&
this.getSelectedBox().contains(position)) {
this.dragStart = position;
this.copyArea();
this.dragMove(position);
} else {
this.startPosition = position;
this.endPosition = null;
this.finished = false;
this.move(position);
}
};
ascii.DrawSelect.prototype.getSelectedBox = function() {
return new ascii.Box(this.startPosition, this.endPosition);
};
ascii.DrawSelect.prototype.copyArea = function() {
var nonEmptyCells = this.state.scratchCells.filter(function(value) {
var rawValue = value.cell.getRawValue();
return value.cell.getRawValue() != null && value.cell.getRawValue() != ERASE_CHAR;
});
var topLeft = this.getSelectedBox().topLeft();
this.selectedCells = nonEmptyCells.map(function(value) {
return new ascii.MappedValue(value.position.subtract(topLeft), value.cell.getRawValue());
});
};
/** @inheritDoc */
ascii.DrawSelect.prototype.move = function(position) {
if (this.dragStart != null) {
this.dragMove(position);
return;
}
if (this.finished == true) {
return;
}
this.endPosition = position;
this.state.clearDraw();
var box = new ascii.Box(this.startPosition, position);
for (var i = box.startX; i <= box.endX; i++) {
for (var j = box.startY; j <= box.endY; j++) {
var current = new ascii.Vector(i, j);
// Effectively highlights the cell.
var currentValue = this.state.getCell(current).getRawValue();
this.state.drawValue(current,
currentValue == null ? ERASE_CHAR : currentValue);
}
}
};
ascii.DrawSelect.prototype.dragMove = function(position) {
this.dragEnd = position;
this.state.clearDraw();
var eraser = new ascii.DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
var startPos = this.dragEnd.subtract(this.dragStart).add(this.getSelectedBox().topLeft());
this.drawSelected(startPos);
};
ascii.DrawSelect.prototype.drawSelected = function(startPos) {
for (var i in this.selectedCells) {
this.state.drawValue(this.selectedCells[i].position.add(startPos), this.selectedCells[i].value);
}
};
/** @inheritDoc */
ascii.DrawSelect.prototype.end = function() {
if (this.dragStart != null) {
this.state.commitDraw();
export default class DrawSelect {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
/** @type {Vector} */
this.startPosition = null;
/** @type {Vector} */
this.endPosition = null;
}
this.dragStart = null;
this.dragEnd = null;
this.finished = true;
};
/** @type {Vector} */
this.dragStart = null;
/** @type {Vector} */
this.dragEnd = null;
/** @inheritDoc */
ascii.DrawSelect.prototype.getCursor = function(position) {
if (this.startPosition != null &&
this.endPosition != null &&
new ascii.Box(this.startPosition, this.endPosition).contains(position)) {
return 'pointer';
}
return 'default';
};
/** @type {boolean} */
this.finished = true;
/** @inheritDoc */
ascii.DrawSelect.prototype.handleKey = function(value) {
if (this.startPosition != null &&
this.endPosition != null) {
if (value == KEY_COPY || value == KEY_CUT) {
/** @type {!Array<MappedValue>} */
this.selectedCells = [];
}
/** @inheritDoc */
start(position) {
// Must be dragging.
if (this.startPosition != null &&
this.endPosition != null &&
this.getSelectedBox().contains(position)) {
this.dragStart = position;
this.copyArea();
this.dragMove(position);
} else {
this.startPosition = position;
this.endPosition = null;
this.finished = false;
this.move(position);
}
if (value == KEY_CUT) {
var eraser = new ascii.DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
}
getSelectedBox() {
return new Box(this.startPosition, this.endPosition);
}
copyArea() {
var nonEmptyCells = this.state.scratchCells.filter(function(value) {
var rawValue = value.cell.getRawValue();
return value.cell.getRawValue() != null && value.cell.getRawValue() != c.ERASE_CHAR;
});
var topLeft = this.getSelectedBox().topLeft();
this.selectedCells = nonEmptyCells.map(function(value) {
return new MappedValue(value.position.subtract(topLeft), value.cell.getRawValue());
});
}
/** @inheritDoc */
move(position) {
if (this.dragStart != null) {
this.dragMove(position);
return;
}
if (this.finished == true) {
return;
}
this.endPosition = position;
this.state.clearDraw();
var box = new Box(this.startPosition, position);
for (var i = box.startX; i <= box.endX; i++) {
for (var j = box.startY; j <= box.endY; j++) {
var current = new Vector(i, j);
// Effectively highlights the cell.
var currentValue = this.state.getCell(current).getRawValue();
this.state.drawValue(current,
currentValue == null ? c.ERASE_CHAR : currentValue);
}
}
}
dragMove(position) {
this.dragEnd = position;
this.state.clearDraw();
var eraser = new DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
var startPos = this.dragEnd.subtract(this.dragStart).add(this.getSelectedBox().topLeft());
this.drawSelected(startPos);
}
drawSelected(startPos) {
for (var { position, value } of this.selectedCells) {
this.state.drawValue(position.add(startPos), value);
}
}
/** @inheritDoc */
end() {
if (this.dragStart != null) {
this.state.commitDraw();
this.startPosition = null;
this.endPosition = null;
}
this.dragStart = null;
this.dragEnd = null;
this.finished = true;
}
/** @inheritDoc */
getCursor(position) {
if (this.startPosition != null &&
this.endPosition != null &&
new Box(this.startPosition, this.endPosition).contains(position)) {
return 'pointer';
}
return 'default';
}
/** @inheritDoc */
handleKey(value) {
if (this.startPosition != null &&
this.endPosition != null) {
if (value == c.KEY_COPY || value == c.KEY_CUT) {
this.copyArea();
}
if (value == c.KEY_CUT) {
var eraser = new DrawErase(this.state);
eraser.start(this.startPosition);
eraser.move(this.endPosition);
this.state.commitDraw();
}
}
if (value == c.KEY_PASTE) {
this.drawSelected(this.startPosition);
this.state.commitDraw();
}
}
if (value == KEY_PASTE) {
this.drawSelected(this.startPosition);
this.state.commitDraw();
}
};
}

View File

@ -1,3 +1,8 @@
import * as c from './constants';
import State from './state';
import Vector from './vector';
import { Box } from './common';
/**
* All drawing classes and functions.
*/
@ -5,16 +10,14 @@
/**
* Draws a line on the diagram state.
*
* @param {ascii.State} state
* @param {ascii.Vector} startPosition
* @param {ascii.Vector} endPosition
* @param {State} state
* @param {Vector} startPosition
* @param {Vector} endPosition
* @param {boolean} clockwise
* @param {string=} opt_value
* @param {string=} value
*/
function drawLine(state, startPosition, endPosition, clockwise, opt_value) {
var value = opt_value || SPECIAL_VALUE;
var box = new ascii.Box(startPosition, endPosition);
function drawLine(state, startPosition, endPosition, clockwise, value = c.SPECIAL_VALUE) {
var box = new Box(startPosition, endPosition);
var startX = box.startX;
var startY = box.startY;
var endX = box.endX;
@ -24,16 +27,16 @@ function drawLine(state, startPosition, endPosition, clockwise, opt_value) {
var midY = clockwise ? startPosition.y : endPosition.y;
while (startX++ < endX) {
var position = new ascii.Vector(startX, midY);
var context = state.getContext(new ascii.Vector(startX, midY));
var position = new Vector(startX, midY);
var context = state.getContext(new Vector(startX, midY));
// Don't erase any lines that we cross.
if (value != ' ' || context.up + context.down != 2) {
state.drawValueIncremental(position, value);
}
}
while (startY++ < endY) {
var position = new ascii.Vector(midX, startY);
var context = state.getContext(new ascii.Vector(midX, startY));
var position = new Vector(midX, startY);
var context = state.getContext(new Vector(midX, startY));
// Don't erase any lines that we cross.
if (value != ' ' || context.left + context.right != 2) {
state.drawValueIncremental(position, value);
@ -42,477 +45,492 @@ function drawLine(state, startPosition, endPosition, clockwise, opt_value) {
state.drawValue(startPosition, value);
state.drawValue(endPosition, value);
state.drawValueIncremental(new ascii.Vector(midX, midY), value);
state.drawValueIncremental(new Vector(midX, midY), value);
}
/**
* Common interface for different drawing functions, e.g. box, line, etc.
* @interface
*/
ascii.DrawFunction = function() {};
/** Start of drawing. @param {ascii.Vector} position */
ascii.DrawFunction.prototype.start = function(position) {};
/** Drawing move. @param {ascii.Vector} position */
ascii.DrawFunction.prototype.move = function(position) {};
/** End of drawing. */
ascii.DrawFunction.prototype.end = function() {};
/** Cursor for given cell.
* @param {ascii.Vector} position
* @return {string}
*/
ascii.DrawFunction.prototype.getCursor = function(position) {};
/** Handle the key with given value being pressed. @param {string} value */
ascii.DrawFunction.prototype.handleKey = function(value) {};
export class DrawFunction {
/** Start of drawing. @param {Vector} position */
start(position) {};
/** Drawing move. @param {Vector} position */
move(position) {};
/** End of drawing. */
end() {};
/** Cursor for given cell.
* @param {Vector} position
* @return {string}
*/
getCursor(position) {};
/** Handle the key with given value being pressed. @param {string} value */
handleKey(value) {};
}
/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawBox = function(state) {
this.state = state;
/** @type {ascii.Vector} */ this.startPosition = null;
};
export class DrawBox {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
/** @type {Vector} */ this.startPosition = null;
/** @type {Vector} */ this.endPosition = null;
}
/** @inheritDoc */
ascii.DrawBox.prototype.start = function(position) {
this.startPosition = position;
};
/** @inheritDoc */
start(position) {
this.startPosition = position;
}
/** @inheritDoc */
ascii.DrawBox.prototype.move = function(position) {
this.endPosition = position;
this.state.clearDraw();
drawLine(this.state, this.startPosition, position, true);
drawLine(this.state, this.startPosition, position, false);
};
/** @inheritDoc */
move(position) {
this.endPosition = position;
this.state.clearDraw();
drawLine(this.state, this.startPosition, position, true);
drawLine(this.state, this.startPosition, position, false);
}
/** @inheritDoc */
ascii.DrawBox.prototype.end = function() {
this.state.commitDraw();
};
/** @inheritDoc */
end() {
this.state.commitDraw();
}
/** @inheritDoc */
ascii.DrawBox.prototype.getCursor = function(position) {
return 'crosshair';
};
/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}
/** @inheritDoc */
ascii.DrawBox.prototype.handleKey = function(value) {};
/** @inheritDoc */
handleKey(value) {};
}
/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @param {boolean} isArrow
* @implements {DrawFunction}
*/
ascii.DrawLine = function(state, isArrow) {
this.state = state;
this.isArrow = isArrow;
/** @type {ascii.Vector} */ this.startPosition = null;
};
/** @inheritDoc */
ascii.DrawLine.prototype.start = function(position) {
this.startPosition = position;
};
/** @inheritDoc */
ascii.DrawLine.prototype.move = function(position) {
this.state.clearDraw();
// Try to infer line orientation.
// TODO: Split the line into two lines if we can't satisfy both ends.
var startContext = this.state.getContext(this.startPosition);
var endContext = this.state.getContext(position);
var clockwise = (startContext.up && startContext.down) ||
(endContext.left && endContext.right);
drawLine(this.state, this.startPosition, position, clockwise);
if (this.isArrow) {
this.state.drawValue(position, ALT_SPECIAL_VALUE);
export class DrawLine {
/**
* @param {State} state
* @param {boolean} isArrow
*/
constructor(state, isArrow) {
this.state = state;
this.isArrow = isArrow;
/** @type {Vector} */ this.startPosition = null;
}
};
/** @inheritDoc */
ascii.DrawLine.prototype.end = function() {
this.state.commitDraw();
};
/** @inheritDoc */
start(position) {
this.startPosition = position;
}
/** @inheritDoc */
ascii.DrawLine.prototype.getCursor = function(position) {
return 'crosshair';
};
/** @inheritDoc */
move(position) {
this.state.clearDraw();
/** @inheritDoc */
ascii.DrawLine.prototype.handleKey = function(value) {};
// Try to infer line orientation.
// TODO: Split the line into two lines if we can't satisfy both ends.
var startContext = this.state.getContext(this.startPosition);
var endContext = this.state.getContext(position);
var clockwise = (startContext.up && startContext.down) ||
(endContext.left && endContext.right);
drawLine(this.state, this.startPosition, position, clockwise);
if (this.isArrow) {
this.state.drawValue(position, c.ALT_SPECIAL_VALUE);
}
}
/** @inheritDoc */
end() {
this.state.commitDraw();
}
/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}
/** @inheritDoc */
handleKey(value) {};
}
/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @param {?string} value
* @implements {DrawFunction}
*/
ascii.DrawFreeform = function(state, value) {
this.state = state;
this.value = value;
if (TOUCH_ENABLED) {
$('#freeform-tool-input').val('');
$('#freeform-tool-input').hide(0, function() {$('#freeform-tool-input').show(0, function() {$('#freeform-tool-input').focus();});});
}
};
/** @inheritDoc */
ascii.DrawFreeform.prototype.start = function(position) {
this.state.drawValue(position, this.value);
};
/** @inheritDoc */
ascii.DrawFreeform.prototype.move = function(position) {
this.state.drawValue(position, this.value);
};
/** @inheritDoc */
ascii.DrawFreeform.prototype.end = function() {
this.state.commitDraw();
};
/** @inheritDoc */
ascii.DrawFreeform.prototype.getCursor = function(position) {
return 'crosshair';
};
/** @inheritDoc */
ascii.DrawFreeform.prototype.handleKey = function(value) {
if (TOUCH_ENABLED) {
this.value = $('#freeform-tool-input').val().substr(0, 1);
$('#freeform-tool-input').blur();
$('#freeform-tool-input').hide(0);
}
if (value.length == 1) {
// The value is not a special character, so lets use it.
export class DrawFreeform {
/**
* @param {State} state
* @param {?string} value
*/
constructor(state, value) {
this.state = state;
this.value = value;
if (c.TOUCH_ENABLED) {
$('#freeform-tool-input').val('');
$('#freeform-tool-input').hide(0, function() {$('#freeform-tool-input').show(0, function() {$('#freeform-tool-input').focus();});});
}
}
};
/** @inheritDoc */
start(position) {
this.state.drawValue(position, this.value);
}
/** @inheritDoc */
move(position) {
this.state.drawValue(position, this.value);
}
/** @inheritDoc */
end() {
this.state.commitDraw();
}
/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}
/** @inheritDoc */
handleKey(value) {
if (c.TOUCH_ENABLED) {
this.value = $('#freeform-tool-input').val().substr(0, 1);
$('#freeform-tool-input').blur();
$('#freeform-tool-input').hide(0);
}
if (value.length == 1) {
// The value is not a special character, so lets use it.
this.value = value;
}
}
}
/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
* @implements {DrawFunction}
*/
ascii.DrawText = function(state, view) {
this.state = state;
this.startPosition = null;
};
/** @inheritDoc */
ascii.DrawText.prototype.start = function(position) {
this.state.commitDraw();
$('#text-tool-input').val('');
this.startPosition = position;
// Not working yet, needs fixing so that it can remove the underlying text completely.
//this.loadExistingText(position);
// Effectively highlights the starting cell.
var currentValue = this.state.getCell(this.startPosition).getRawValue();
this.state.drawValue(this.startPosition,
currentValue == null ? ERASE_CHAR : currentValue);
};
/** @inheritDoc */
ascii.DrawText.prototype.move = function(position) {};
/** @inheritDoc */
ascii.DrawText.prototype.end = function() {
if (this.startPosition != null) {
this.endPosition = this.startPosition;
export class DrawText {
/**
* @param {State} state
*/
constructor(state, view) {
this.state = state;
this.startPosition = null;
// Valid end click/press, show the textbox and focus it.
$('#text-tool-widget').hide(0, function() {$('#text-tool-widget').show(0, function() {$('#text-tool-input').focus();});});
this.endPosition = null;
};
/** @inheritDoc */
start(position) {
this.state.commitDraw();
$('#text-tool-input').val('');
this.startPosition = position;
// Not working yet, needs fixing so that it can remove the underlying text completely.
//this.loadExistingText(position);
// Effectively highlights the starting cell.
var currentValue = this.state.getCell(this.startPosition).getRawValue();
this.state.drawValue(this.startPosition,
currentValue == null ? c.ERASE_CHAR : currentValue);
}
};
/** @inheritDoc */
ascii.DrawText.prototype.getCursor = function(position) {
return 'pointer';
};
/** @inheritDoc */
move(position) {}
/** @inheritDoc */
ascii.DrawText.prototype.handleKey = function(value) {
var text = $('#text-tool-input').val();
this.state.clearDraw();
var x = 0, y = 0;
for(var i = 0; i < text.length; i++) {
if (text[i] == '\n') {
y++;
x = 0;
continue;
}
this.state.drawValue(this.endPosition.add(new ascii.Vector(x, y)), text[i]);
x++;
}
};
/**
* Loads any existing text if it is present.
* TODO: This is horrible, and does not quite work, fix it.
*/
ascii.DrawText.prototype.loadExistingText = function(position) {
var currentPosition = new ascii.Vector(position.x, position.y);
var cell = this.state.getCell(position);
var spacesCount = 0;
// Go back to find the start of the line.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
if (cell.getRawValue() == null) {
spacesCount++;
} else if (!cell.isSpecial()) {
spacesCount = 0;
}
currentPosition.x--;
cell = this.state.getCell(currentPosition);
}
this.startPosition = currentPosition.add(new ascii.Vector(spacesCount + 1, 0));
var text = '';
spacesCount = 0;
currentPosition = this.startPosition.clone();
// Go forward to load the text.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
cell = this.state.getCell(currentPosition);
if (cell.getRawValue() == null) {
spacesCount++;
text += ' ';
} else if (!cell.isSpecial()) {
spacesCount = 0;
text += cell.getRawValue();
this.state.drawValue(currentPosition, cell.getRawValue());
}
currentPosition.x++;
}
$('#text-tool-input').val(text.substr(0, text.length - 1));
};
/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
*/
ascii.DrawErase = function(state) {
this.state = state;
this.startPosition = null;
this.endPosition = null;
};
/** @inheritDoc */
ascii.DrawErase.prototype.start = function(position) {
this.startPosition = position;
this.move(position);
};
/** @inheritDoc */
ascii.DrawErase.prototype.move = function(position) {
this.state.clearDraw();
this.endPosition = position;
var startX = Math.min(this.startPosition.x, this.endPosition.x);
var startY = Math.min(this.startPosition.y, this.endPosition.y);
var endX = Math.max(this.startPosition.x, this.endPosition.x);
var endY = Math.max(this.startPosition.y, this.endPosition.y);
for (var i = startX; i <= endX; i++) {
for (var j = startY; j <= endY; j++) {
this.state.drawValue(new ascii.Vector(i, j), ERASE_CHAR);
/** @inheritDoc */
end() {
if (this.startPosition != null) {
this.endPosition = this.startPosition;
this.startPosition = null;
// Valid end click/press, show the textbox and focus it.
$('#text-tool-widget').hide(0, function() {$('#text-tool-widget').show(0, function() {$('#text-tool-input').focus();});});
}
}
};
/** @inheritDoc */
ascii.DrawErase.prototype.end = function() {
this.state.commitDraw();
};
/** @inheritDoc */
ascii.DrawErase.prototype.getCursor = function(position) {
return 'crosshair';
};
/** @inheritDoc */
ascii.DrawErase.prototype.handleKey = function(value) {};
/**
* @constructor
* @implements {ascii.DrawFunction}
* @param {ascii.State} state
*/
ascii.DrawMove = function(state) {
this.state = state;
this.startPosition = null;
this.ends = null;
};
/** @inheritDoc */
ascii.DrawMove.prototype.start = function(position) {
this.startPosition =
TOUCH_ENABLED ? this.snapToNearest(position) : position;
this.ends = null;
// If this isn't a special cell then quit, or things get weird.
if (!this.state.getCell(this.startPosition).isSpecial()) {
return;
/** @inheritDoc */
getCursor(position) {
return 'pointer';
}
var context = this.state.getContext(this.startPosition);
var ends = [];
for (var i in DIRECTIONS) {
var midPoints = this.followLine(this.startPosition, DIRECTIONS[i]);
for (var k in midPoints) {
var midPoint = midPoints[k];
// Clockwise is a lie, it is true if we move vertically first.
var clockwise = (DIRECTIONS[i].x != 0);
var startIsAlt = ALT_SPECIAL_VALUES.indexOf(this.state.getCell(position).getRawValue()) != -1;
var midPointIsAlt = ALT_SPECIAL_VALUES.indexOf(this.state.getCell(midPoint).getRawValue()) != -1;
var midPointContext = this.state.getContext(midPoint);
// Special case, a straight line with no turns.
if (midPointContext.sum() == 1) {
ends.push({position: midPoint, clockwise: clockwise, startIsAlt: startIsAlt, endIsAlt: midPointIsAlt});
/** @inheritDoc */
handleKey(value) {
var text = $('#text-tool-input').val();
this.state.clearDraw();
var x = 0, y = 0;
for(var i = 0; i < text.length; i++) {
if (text[i] == '\n') {
y++;
x = 0;
continue;
}
// Continue following lines from the midpoint.
for (var j in DIRECTIONS) {
if (DIRECTIONS[i].add(DIRECTIONS[j]).length() == 0 ||
DIRECTIONS[i].add(DIRECTIONS[j]).length() == 2) {
// Don't go back on ourselves, or don't carry on in same direction.
continue;
}
var secondEnds = this.followLine(midPoint, DIRECTIONS[j]);
// Ignore any directions that didn't go anywhere.
if (secondEnds.length == 0) {
continue;
}
var secondEnd = secondEnds[0];
var endIsAlt = ALT_SPECIAL_VALUES.indexOf(this.state.getCell(secondEnd).getRawValue()) != -1;
// On the second line we don't care about multiple
// junctions, just the last.
ends.push({position: secondEnd,
clockwise: clockwise, startIsAlt: startIsAlt, midPointIsAlt: midPointIsAlt, endIsAlt: endIsAlt});
this.state.drawValue(this.endPosition.add(new Vector(x, y)), text[i]);
x++;
}
}
/**
* Loads any existing text if it is present.
* TODO: This is horrible, and does not quite work, fix it.
*/
loadExistingText(position) {
var currentPosition = new Vector(position.x, position.y);
var cell = this.state.getCell(position);
var spacesCount = 0;
// Go back to find the start of the line.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
if (cell.getRawValue() == null) {
spacesCount++;
} else if (!cell.isSpecial()) {
spacesCount = 0;
}
currentPosition.x--;
cell = this.state.getCell(currentPosition);
}
this.startPosition = currentPosition.add(new Vector(spacesCount + 1, 0));
var text = '';
spacesCount = 0;
currentPosition = this.startPosition.clone();
// Go forward to load the text.
while ((!cell.isSpecial() && cell.getRawValue() != null) || spacesCount < 1) {
cell = this.state.getCell(currentPosition);
if (cell.getRawValue() == null) {
spacesCount++;
text += ' ';
} else if (!cell.isSpecial()) {
spacesCount = 0;
text += cell.getRawValue();
this.state.drawValue(currentPosition, cell.getRawValue());
}
currentPosition.x++;
}
$('#text-tool-input').val(text.substr(0, text.length - 1));
}
}
/**
* @implements {DrawFunction}
*/
export class DrawErase {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
this.startPosition = null;
this.endPosition = null;
}
/** @inheritDoc */
start(position) {
this.startPosition = position;
this.move(position);
}
/** @inheritDoc */
move(position) {
this.state.clearDraw();
this.endPosition = position;
var startX = Math.min(this.startPosition.x, this.endPosition.x);
var startY = Math.min(this.startPosition.y, this.endPosition.y);
var endX = Math.max(this.startPosition.x, this.endPosition.x);
var endY = Math.max(this.startPosition.y, this.endPosition.y);
for (var i = startX; i <= endX; i++) {
for (var j = startY; j <= endY; j++) {
this.state.drawValue(new Vector(i, j), c.ERASE_CHAR);
}
}
}
this.ends = ends;
// Redraw the new lines after we have cleared the existing ones.
this.move(this.startPosition);
};
/** @inheritDoc */
ascii.DrawMove.prototype.move = function(position) {
this.state.clearDraw();
// Clear all the lines so we can draw them afresh.
for (var i in this.ends) {
drawLine(this.state, this.startPosition, this.ends[i].position,
this.ends[i].clockwise, ' ');
/** @inheritDoc */
end() {
this.state.commitDraw();
}
for (var i in this.ends) {
drawLine(this.state, position, this.ends[i].position,
this.ends[i].clockwise);
}
for (var i in this.ends) {
// If the ends or midpoint of the line was a alt character (arrow), need to preserve that.
if (this.ends[i].startIsAlt) {
this.state.drawValue(position, ALT_SPECIAL_VALUE);
}
if (this.ends[i].endIsAlt) {
this.state.drawValue(this.ends[i].position, ALT_SPECIAL_VALUE);
}
if (this.ends[i].midPointIsAlt) {
var midX = this.ends[i].clockwise ? this.ends[i].position.x : position.x;
var midY = this.ends[i].clockwise ? position.y : this.ends[i].position.y;
this.state.drawValue(new ascii.Vector(midX, midY), ALT_SPECIAL_VALUE);
}
}
};
/** @inheritDoc */
ascii.DrawMove.prototype.end = function() {
this.state.commitDraw();
};
/** @inheritDoc */
getCursor(position) {
return 'crosshair';
}
/** @inheritDoc */
handleKey(value) {}
}
/**
* Follows a line in a given direction from the startPosition.
* Returns a list of positions that were line 'junctions'. This is a bit of a
* loose definition, but basically means a point around which we resize things.
* @param {ascii.Vector} startPosition
* @param {ascii.Vector} direction
* @return {Array.<ascii.Vector>}
* @implements {DrawFunction}
*/
ascii.DrawMove.prototype.followLine = function(startPosition, direction) {
var endPosition = startPosition.clone();
var junctions = [];
while (true) {
var nextEnd = endPosition.add(direction);
if (!this.state.getCell(nextEnd).isSpecial()) {
// Junctions: Right angles and end T-Junctions.
if (!startPosition.equals(endPosition)) {
export class DrawMove {
/**
* @param {State} state
*/
constructor(state) {
this.state = state;
this.startPosition = null;
/** @type {!Array<{position, clockwise, startIsAlt, midPointIsAlt, endIsAlt}>} */
this.ends = [];
}
/** @inheritDoc */
start(position) {
this.startPosition =
c.TOUCH_ENABLED ? this.snapToNearest(position) : position;
this.ends = [];
// If this isn't a special cell then quit, or things get weird.
if (!this.state.getCell(this.startPosition).isSpecial()) {
return;
}
var context = this.state.getContext(this.startPosition);
var ends = [];
for (var i of c.DIRECTIONS) {
var midPoints = this.followLine(this.startPosition, i);
for (var midPoint of midPoints) {
// Clockwise is a lie, it is true if we move vertically first.
var clockwise = (i.x != 0);
var startIsAlt = c.ALT_SPECIAL_VALUES.indexOf(this.state.getCell(position).getRawValue()) != -1;
var midPointIsAlt = c.ALT_SPECIAL_VALUES.indexOf(this.state.getCell(midPoint).getRawValue()) != -1;
var midPointContext = this.state.getContext(midPoint);
// Special case, a straight line with no turns.
if (midPointContext.sum() == 1) {
ends.push({position: midPoint, clockwise, startIsAlt, endIsAlt: midPointIsAlt});
continue;
}
// Continue following lines from the midpoint.
for (var j of c.DIRECTIONS) {
if (i.add(j).length() == 0 || i.add(j).length() == 2) {
// Don't go back on ourselves, or don't carry on in same direction.
continue;
}
var secondEnds = this.followLine(midPoint, j);
// Ignore any directions that didn't go anywhere.
if (secondEnds.length == 0) {
continue;
}
var secondEnd = secondEnds[0];
var endIsAlt = c.ALT_SPECIAL_VALUES.indexOf(this.state.getCell(secondEnd).getRawValue()) != -1;
// On the second line we don't care about multiple
// junctions, just the last.
ends.push({position: secondEnd, clockwise, startIsAlt, midPointIsAlt, endIsAlt});
}
}
}
this.ends = ends;
// Redraw the new lines after we have cleared the existing ones.
this.move(this.startPosition);
}
/** @inheritDoc */
move(position) {
this.state.clearDraw();
// Clear all the lines so we can draw them afresh.
for (var end of this.ends) {
drawLine(this.state, this.startPosition, end.position, end.clockwise, ' ');
}
for (var i in this.ends) {
drawLine(this.state, position, end.position, end.clockwise);
}
for (var end of this.ends) {
// If the ends or midpoint of the line was a alt character (arrow), need to preserve that.
if (end.startIsAlt) {
this.state.drawValue(position, c.ALT_SPECIAL_VALUE);
}
if (end.endIsAlt) {
this.state.drawValue(end.position, c.ALT_SPECIAL_VALUE);
}
if (end.midPointIsAlt) {
var midX = end.clockwise ? end.position.x : position.x;
var midY = end.clockwise ? position.y : end.position.y;
this.state.drawValue(new Vector(midX, midY), c.ALT_SPECIAL_VALUE);
}
}
}
/** @inheritDoc */
end() {
this.state.commitDraw();
}
/**
* Follows a line in a given direction from the startPosition.
* Returns a list of positions that were line 'junctions'. This is a bit of a
* loose definition, but basically means a point around which we resize things.
* @param {Vector} startPosition
* @param {Vector} direction
* @return {!Array<Vector>}
*/
followLine(startPosition, direction) {
var endPosition = startPosition.clone();
var junctions = [];
while (true) {
var nextEnd = endPosition.add(direction);
if (!this.state.getCell(nextEnd).isSpecial()) {
// Junctions: Right angles and end T-Junctions.
if (!startPosition.equals(endPosition)) {
junctions.push(endPosition);
}
return junctions;
}
endPosition = nextEnd;
var context = this.state.getContext(endPosition);
// Junctions: Side T-Junctions.
if (context.sum() == 3) {
junctions.push(endPosition);
}
return junctions;
}
endPosition = nextEnd;
var context = this.state.getContext(endPosition);
// Junctions: Side T-Junctions.
if (context.sum() == 3) {
junctions.push(endPosition);
}
}
};
/**
* For a given position, finds the nearest cell that is of any interest to the
* move tool, e.g. a corner or a line. Will look up to 1 cell in each direction
* including diagonally.
* @param {ascii.Vector} position
* @return {ascii.Vector}
*/
ascii.DrawMove.prototype.snapToNearest = function(position) {
if (this.state.getCell(position).isSpecial()) {
return position;
/**
* For a given position, finds the nearest cell that is of any interest to the
* move tool, e.g. a corner or a line. Will look up to 1 cell in each direction
* including diagonally.
* @param {Vector} position
* @return {Vector}
*/
snapToNearest(position) {
if (this.state.getCell(position).isSpecial()) {
return position;
}
var allDirections = c.DIRECTIONS.concat([
c.DIR_LEFT.add(c.DIR_UP),
c.DIR_LEFT.add(c.DIR_DOWN),
c.DIR_RIGHT.add(c.DIR_UP),
c.DIR_RIGHT.add(c.DIR_DOWN)]);
var bestDirection = null;
var bestContextSum = 0;
for (var direction of allDirections) {
// Find the most connected cell, essentially.
var newPos = position.add(direction);
var contextSum = this.state.getContext(newPos).sum();
if (this.state.getCell(newPos).isSpecial() &&
contextSum > bestContextSum) {
bestDirection = direction;
bestContextSum = contextSum;
}
}
if (bestDirection == null) {
// Didn't find anything, so just return the current cell.
return position;
}
return position.add(bestDirection);
}
var allDirections = DIRECTIONS.concat([
DIR_LEFT.add(DIR_UP),
DIR_LEFT.add(DIR_DOWN),
DIR_RIGHT.add(DIR_UP),
DIR_RIGHT.add(DIR_DOWN)]);
var bestDirection = null;
var bestContextSum = 0;
for (var i in allDirections) {
// Find the most connected cell, essentially.
var newPos = position.add(allDirections[i]);
var contextSum = this.state.getContext(newPos).sum();
if (this.state.getCell(newPos).isSpecial() &&
contextSum > bestContextSum) {
bestDirection = allDirections[i];
bestContextSum = contextSum;
/** @inheritDoc */
getCursor(position) {
if (this.state.getCell(position).isSpecial()) {
return 'pointer';
} else {
return 'default';
}
}
if (bestDirection == null) {
// Didn't find anything, so just return the current cell.
return position;
}
return position.add(bestDirection);
};
/** @inheritDoc */
ascii.DrawMove.prototype.getCursor = function(position) {
if (this.state.getCell(position).isSpecial()) {
return 'pointer';
} else {
return 'default';
}
};
/** @inheritDoc */
ascii.DrawMove.prototype.handleKey = function(value) {};
/** @inheritDoc */
handleKey(value) {}
}

View File

@ -1,266 +1,263 @@
/** @const */
var CLIENT_ID = '125643747010-9s9n1ne2fnnuh5v967licfkt83r4vba5.apps.googleusercontent.com';
/** @const */
var SCOPES = 'https://www.googleapis.com/auth/drive';
/** @const */
var DEVELOPER_KEY = 'AIzaSyBbKO_v9p-G9StQjYmtUYLP6Px4MkGions';
import Vector from './vector';
import State from './state';
import View from './view';
/**
*
* @constructor
*/
ascii.DriveController = function(state, view) {
/** @type {boolean} */
this.driveEnabled = false;
/** @type {ascii.State} */
this.state = state;
/** @type {ascii.View} */
this.view = view;
// This is a file resource, as defined by the Drive API.
/** @type {Object} */
this.file = null;
/** @type {string} */
this.cachedContent = '';
const CLIENT_ID = '125643747010-9s9n1ne2fnnuh5v967licfkt83r4vba5.apps.googleusercontent.com';
const SCOPES = 'https://www.googleapis.com/auth/drive';
const DEVELOPER_KEY = 'AIzaSyBbKO_v9p-G9StQjYmtUYLP6Px4MkGions';
this.tryInitialAuth();
$('#drive-button').click(function() {
if (!this.driveEnabled) {
// Haven't been able to immediately auth yet, so try full auth.
this.checkAuth(false);
this.waitForFullAuth();
} else {
this.loadDialog();
}
}.bind(this));
$('#drive-filename').click(function() {
var currentTitle = '' + $('#drive-filename').text();
var title = prompt('Enter new filename:', currentTitle);
this.file['title'] = title;
this.save();
this.loadFileList();
}.bind(this));
this.loopSave();
$(window).bind('hashchange', function() {
this.loadFromHash();
}.bind(this));
$('#drive-new-file-button').click(function() {
export default class DriveController {
constructor(state, view) {
/** @type {boolean} */
this.driveEnabled = false;
/** @type {State} */
this.state = state;
/** @type {View} */
this.view = view;
// This is a file resource, as defined by the Drive API.
/** @type {Object} */
this.file = null;
this.state.clear();
window.location.hash = '';
this.save();
$('#drive-dialog').removeClass('visible');
}.bind(this));
};
/** @type {string} */
this.cachedText = '';
/**
* Check if the current user has authorized the application.
*/
ascii.DriveController.prototype.checkAuth = function(immediate) {
window['gapi']['auth']['authorize']({
'client_id': CLIENT_ID,
'scope': SCOPES,
'immediate': immediate},
function(result) {
if (result && !result.error && !this.driveEnabled) {
this.driveEnabled = true;
$('#drive-button').addClass('active');
// We are authorized, so let's se if we can load from the URL hash.
// This seems to fail if we do it too early.
window.setTimeout(function() { this.loadFromHash(); }.bind(this), 500);
}
}.bind(this));
};
this.tryInitialAuth();
ascii.DriveController.prototype.tryInitialAuth = function() {
if (window['gapi'] && window['gapi']['auth'] && window['gapi']['auth']['authorize']) {
this.checkAuth(true);
} else {
window.setTimeout(function() {
this.tryInitialAuth();
}.bind(this), 500);
}
};
ascii.DriveController.prototype.waitForFullAuth = function() {
window.setTimeout(function() {
if (!this.driveEnabled) {
this.checkAuth(true);
this.waitForFullAuth();
} else {
this.loadDialog();
}
}.bind(this), 1000);
};
/**
* Handles a file resource being returned from Drive.
*/
ascii.DriveController.prototype.handleFile = function(file) {
this.file = file;
$('#drive-filename').text(file['title']);
window.location.hash = file['id'];
};
/**
* Loads the drive dialog.
*/
ascii.DriveController.prototype.loadDialog = function() {
$('#drive-dialog').addClass('visible');
var text = this.state.outputText();
// Don't save diagram if empty, just get's annoying.
if (text.length > 5 && text != this.cachedText) {
this.save();
}
this.loadFileList();
};
ascii.DriveController.prototype.loadFileList = function() {
this.safeExecute(this.getListRequest(), function(result) {
$('#drive-file-list').children().remove();
var items = result['items'];
for (var i in items) {
var entry = document.createElement('li');
var title = document.createElement('a');
entry.appendChild(title);
title.href = '#' + items[i]['id'];
$(title).click(function() { $('#drive-dialog').removeClass('visible'); });
title.innerHTML = items[i]['title'];
$('#drive-file-list').append(entry);
}
}.bind(this));
}
ascii.DriveController.prototype.safeExecute = function(request, callback) {
// Could make the API call, don't blow up tho (mobiles n stuff).
try {
request['execute'](function(result) {
if (!result['error']) {
callback(result);
$('#drive-button').click(() => {
if (!this.driveEnabled) {
// Haven't been able to immediately auth yet, so try full auth.
this.checkAuth(false);
this.waitForFullAuth();
} else {
this.loadDialog();
}
});
} catch (e) {}
};
/**
* Repeatedly save the diagram if it is editable and loaded.
*/
ascii.DriveController.prototype.loopSave = function() {
var text = this.state.outputText();
if (text != this.cachedText && this.file && this.file['editable']) {
this.save();
}
window.setTimeout(function() {
$('#drive-filename').click(() => {
var currentTitle = '' + $('#drive-filename').text();
var title = prompt('Enter new filename:', currentTitle);
this.file['title'] = title;
this.save();
this.loadFileList();
});
this.loopSave();
}.bind(this), 5000);
}
/**
* Saves the current diagram to drive.
*/
ascii.DriveController.prototype.save = function() {
var text = this.state.outputText();
$('#drive-save-state').text('Saving...');
this.safeExecute(this.getSaveRequest(text), function(result) {
this.handleFile(result);
$('#drive-save-state').text('Saved');
this.cachedText = text;
}.bind(this));
};
$(window).on('hashchange', this.loadFromHash);
ascii.DriveController.prototype.loadFromHash = function() {
if (window.location.hash.length > 1) {
$('#drive-save-state').text('Loading...');
var fileId = window.location.hash.substr(1, window.location.hash.length - 1);
this.safeExecute(this.getLoadRequest(fileId), function(result) {
this.handleFile(result);
this.reloadFileContent();
}.bind(this));
$('#drive-new-file-button').click(() => {
this.file = null;
this.state.clear();
window.location.hash = '';
this.save();
$('#drive-dialog').removeClass('visible');
});
}
};
ascii.DriveController.prototype.reloadFileContent = function() {
this.downloadFile(this.file['downloadUrl'], function(content) {
$('#drive-save-state').text('Loaded');
this.state.clear();
this.state.fromText(content, this.view.screenToCell(new ascii.Vector(
this.view.canvas.width / 2,
this.view.canvas.height / 2)));
this.state.commitDraw();
this.cachedText = this.state.outputText();
}.bind(this));
};
/**
* Check if the current user has authorized the application.
*/
checkAuth(immediate) {
window['gapi']['auth']['authorize']({
'client_id': CLIENT_ID,
'scope': SCOPES,
'immediate': immediate},
result => {
if (result && !result.error && !this.driveEnabled) {
this.driveEnabled = true;
$('#drive-button').addClass('active');
// We are authorized, so let's se if we can load from the URL hash.
// This seems to fail if we do it too early.
window.setTimeout(this.loadFromHash, 500);
}
});
}
ascii.DriveController.prototype.getSaveRequest = function(text) {
var boundary = '-------314159265358979323846';
var delimiter = "\r\n--" + boundary + "\r\n";
var close_delim = "\r\n--" + boundary + "--";
tryInitialAuth() {
if (window['gapi'] && window['gapi']['auth'] && window['gapi']['auth']['authorize']) {
this.checkAuth(true);
} else {
window.setTimeout(() => {
this.tryInitialAuth();
}, 500);
}
}
var title = this.file == null ? 'Untitled ASCII Diagram' : this.file['title'];
waitForFullAuth() {
window.setTimeout(() => {
if (!this.driveEnabled) {
this.checkAuth(true);
this.waitForFullAuth();
} else {
this.loadDialog();
}
}, 1000);
}
var metadata = {
'title': title,
'mimeType': 'text/plain'
};
/**
* Handles a file resource being returned from Drive.
*/
handleFile(file) {
this.file = file;
$('#drive-filename').text(file['title']);
window.location.hash = file['id'];
}
var multipartRequestBody =
delimiter +
'Content-Type: application/json\r\n\r\n' +
JSON.stringify(metadata) +
delimiter +
'Content-Type: ' + 'text/plain' + '\r\n' +
'\r\n' +
text +
close_delim;
// Choose upload path and method depending on whether we have create a file already.
var fileId = this.file == null ? '' : '/' + this.file['id'];
var method = this.file == null ? 'POST' : 'PUT';
/**
* Loads the drive dialog.
*/
loadDialog() {
$('#drive-dialog').addClass('visible');
return window['gapi']['client']['request']({
'path': '/upload/drive/v2/files' + fileId,
'method': method,
'params': {'uploadType': 'multipart'},
'headers': {
'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
},
'body': multipartRequestBody});
};
var text = this.state.outputText();
// Don't save diagram if empty, just get's annoying.
if (text.length > 5 && text != this.cachedText) {
this.save();
}
this.loadFileList();
}
ascii.DriveController.prototype.getLoadRequest = function(fileId) {
return window['gapi']['client']['request']({
'path': '/drive/v2/files/' + fileId,
'method': 'GET'});
};
loadFileList() {
this.safeExecute(this.getListRequest(), result => {
$('#drive-file-list').children().remove();
var items = result['items'];
for (var i in items) {
var entry = document.createElement('li');
var title = document.createElement('a');
entry.appendChild(title);
title.href = '#' + items[i]['id'];
$(title).click(function() { $('#drive-dialog').removeClass('visible'); });
title.innerHTML = items[i]['title'];
$('#drive-file-list').append(entry);
}
});
}
ascii.DriveController.prototype.getListRequest = function() {
return window['gapi']['client']['request']({
'path': '/drive/v2/files',
'params' : { 'q': 'mimeType = \'text/plain\' and trashed = false' },
'method': 'GET'});
};
safeExecute(request, callback) {
// Could make the API call, don't blow up tho (mobiles n stuff).
try {
request['execute'](function(result) {
if (!result['error']) {
callback(result);
}
});
} catch (e) {}
}
/**
* Download a file's content.
*
* @param {string} url
*/
ascii.DriveController.prototype.downloadFile = function(url, callback) {
var accessToken = window['gapi']['auth']['getToken']()['access_token'];
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
xhr.onload = function() {
callback(xhr.responseText);
};
xhr.onerror = function() {
callback(null);
};
xhr.send();
/**
* Repeatedly save the diagram if it is editable and loaded.
*/
loopSave() {
var text = this.state.outputText();
if (text != this.cachedText && this.file && this.file['editable']) {
this.save();
}
window.setTimeout(() => {
this.loopSave();
}, 5000);
}
/**
* Saves the current diagram to drive.
*/
save() {
var text = this.state.outputText();
$('#drive-save-state').text('Saving...');
this.safeExecute(this.getSaveRequest(text), result => {
this.handleFile(result);
$('#drive-save-state').text('Saved');
this.cachedText = text;
});
}
loadFromHash() {
if (window.location.hash.length > 1) {
$('#drive-save-state').text('Loading...');
var fileId = window.location.hash.substr(1, window.location.hash.length - 1);
this.safeExecute(this.getLoadRequest(fileId), result => {
this.handleFile(result);
this.reloadFileContent();
});
}
}
reloadFileContent() {
this.downloadFile(this.file['downloadUrl'], content => {
$('#drive-save-state').text('Loaded');
this.state.clear();
this.state.fromText(content, this.view.screenToCell(new Vector(
this.view.canvas.width / 2,
this.view.canvas.height / 2)));
this.state.commitDraw();
this.cachedText = this.state.outputText();
});
}
getSaveRequest(text) {
var boundary = '-------314159265358979323846';
var delimiter = "\r\n--" + boundary + "\r\n";
var close_delim = "\r\n--" + boundary + "--";
var title = this.file == null ? 'Untitled ASCII Diagram' : this.file['title'];
var metadata = {
'title': title,
'mimeType': 'text/plain'
};
var multipartRequestBody =
delimiter +
'Content-Type: application/json\r\n\r\n' +
JSON.stringify(metadata) +
delimiter +
'Content-Type: ' + 'text/plain' + '\r\n' +
'\r\n' +
text +
close_delim;
// Choose upload path and method depending on whether we have create a file already.
var fileId = this.file == null ? '' : '/' + this.file['id'];
var method = this.file == null ? 'POST' : 'PUT';
return window['gapi']['client']['request']({
'path': '/upload/drive/v2/files' + fileId,
'method': method,
'params': {'uploadType': 'multipart'},
'headers': {
'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
},
'body': multipartRequestBody});
}
getLoadRequest(fileId) {
return window['gapi']['client']['request']({
'path': '/drive/v2/files/' + fileId,
'method': 'GET'});
}
getListRequest() {
return window['gapi']['client']['request']({
'path': '/drive/v2/files',
'params' : { 'q': 'mimeType = \'text/plain\' and trashed = false' },
'method': 'GET'});
}
/**
* Download a file's content.
*
* @param {string} url
*/
downloadFile(url, callback) {
var accessToken = window['gapi']['auth']['getToken']()['access_token'];
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
xhr.onload = function() {
callback(xhr.responseText);
};
xhr.onerror = function() {
callback(null);
};
xhr.send();
}
}

View File

@ -1,190 +1,196 @@
import * as c from './constants';
import Controller from './controller';
import Vector from './vector';
/**
* Handles desktop inputs, and passes them onto the main controller.
* @constructor
* @param {ascii.Controller} controller
*/
ascii.DesktopController = function(controller) {
/** @type {ascii.Controller} */ this.controller = controller;
export class DesktopController {
/**
* @param {Controller} controller
*/
constructor(controller) {
/** @type {Controller} */ this.controller = controller;
/** @type {boolean} */ this.isDragging = false;
/** @type {boolean} */ this.isDragging = false;
this.installBindings();
};
this.installBindings();
};
/**
* @param {number} delta
*/
handleZoom(delta) {
var newzoom = this.controller.view.zoom * (delta > 0 ? 1.1 : 0.9);
newzoom = Math.max(Math.min(newzoom, 5), 0.2);
this.controller.view.setZoom(newzoom);
}
/**
* @param {number} delta
*/
ascii.DesktopController.prototype.handleZoom = function(delta) {
var newzoom = this.controller.view.zoom * (delta > 0 ? 1.1 : 0.9);
newzoom = Math.max(Math.min(newzoom, 5), 0.2);
this.controller.view.setZoom(newzoom);
};
/**
* Installs input bindings associated with keyboard controls.
*/
installBindings() {
var canvas = this.controller.view.canvas;
$(canvas).on('mousewheel', e => {
this.handleZoom(e.originalEvent.wheelDelta);
});
/**
* Installs input bindings associated with keyboard controls.
*/
ascii.DesktopController.prototype.installBindings = function() {
var canvas = this.controller.view.canvas;
$(canvas).bind('mousewheel', function(e) {
this.handleZoom(e.originalEvent.wheelDelta);
}.bind(this));
$(canvas).mousedown(e => {
// Can drag by holding either the control or meta (Apple) key.
if (e.ctrlKey || e.metaKey) {
this.controller.startDrag(new Vector(e.clientX, e.clientY));
} else {
this.controller.startDraw(new Vector(e.clientX, e.clientY));
}
});
$(canvas).mousedown(function(e) {
// Can drag by holding either the control or meta (Apple) key.
if (e.ctrlKey || e.metaKey) {
this.controller.startDrag(new ascii.Vector(e.clientX, e.clientY));
} else {
this.controller.startDraw(new ascii.Vector(e.clientX, e.clientY));
}
}.bind(this));
// Pass these events through to the main controller.
$(canvas).mouseup(e => {
this.controller.endAll();
});
// Pass these events through to the main controller.
$(canvas).mouseup(function(e) {
this.controller.endAll();
}.bind(this));
$(canvas).mouseleave(function(e) {
this.controller.endAll();
}.bind(this));
$(canvas).mousemove(function(e) {
this.controller.handleMove(new ascii.Vector(e.clientX, e.clientY));
}.bind(this));
};
$(canvas).mouseleave(e => {
this.controller.endAll();
});
$(canvas).mousemove(e => {
this.controller.handleMove(new Vector(e.clientX, e.clientY));
});
}
}
/**
* Handles touch inputs, and passes them onto the main controller.
* @constructor
* @param {ascii.Controller} controller
*/
ascii.TouchController = function(controller) {
/** @type {ascii.Controller} */ this.controller = controller;
export class TouchController {
/**
* @param {Controller} controller
*/
constructor(controller) {
/** @type {Controller} */ this.controller = controller;
/** @type {ascii.Vector} */ this.pressVector;
/** @type {Vector} */ this.pressVector;
/** @type {number} */ this.originalZoom;
/** @type {number} */ this.zoomLength;
/** @type {number} */ this.originalZoom;
/** @type {number} */ this.zoomLength;
/** @type {number} */ this.pressTimestamp;
/** @type {boolean} */ this.dragStarted = false;
/** @type {boolean} */ this.zoomStarted = false;
/** @type {number} */ this.pressTimestamp;
/** @type {boolean} */ this.dragStarted = false;
/** @type {boolean} */ this.zoomStarted = false;
this.installBindings();
};
this.installBindings();
}
/**
* @param {ascii.Vector} position
*/
ascii.TouchController.prototype.handlePress = function(position) {
this.pressVector = position;
this.pressTimestamp = $.now();
this.dragStarted = false;
/**
* @param {Vector} position
*/
handlePress(position) {
this.pressVector = position;
this.pressTimestamp = $.now();
this.dragStarted = false;
// If a drag or zoom didn't start and if we didn't release already, then handle it as a draw.
window.setTimeout(function() {
if (!this.dragStarted && !this.zoomStarted && this.pressVector != null) {
this.controller.startDraw(position);
// If a drag or zoom didn't start and if we didn't release already, then handle it as a draw.
window.setTimeout(() => {
if (!this.dragStarted && !this.zoomStarted && this.pressVector != null) {
this.controller.startDraw(position);
}
}, c.DRAG_LATENCY);
}
/**
* The multi-touch version of handlePress.
* @param {Vector} positionOne
* @param {Vector} positionTwo
*/
handlePressMulti(positionOne, positionTwo) {
// A second finger as been placed, cancel whatever we were doing.
this.controller.endAll();
this.zoomStarted = true;
this.dragStarted = false;
this.zoomLength = positionOne.subtract(positionTwo).length();
this.originalZoom = this.controller.view.zoom;
}
/**
* @param {Vector} position
*/
handleMove(position) {
// Initiate a drag if we have moved enough, quickly enough.
if (!this.dragStarted &&
($.now() - this.pressTimestamp) < c.DRAG_LATENCY &&
position.subtract(this.pressVector).length() > c.DRAG_ACCURACY) {
this.dragStarted = true;
this.controller.startDrag(position);
}
}.bind(this), DRAG_LATENCY);
};
/**
* The multi-touch version of handlePress.
* @param {ascii.Vector} positionOne
* @param {ascii.Vector} positionTwo
*/
ascii.TouchController.prototype.handlePressMulti =
function(positionOne, positionTwo) {
// A second finger as been placed, cancel whatever we were doing.
this.controller.endAll();
this.zoomStarted = true;
this.dragStarted = false;
this.zoomLength = positionOne.subtract(positionTwo).length();
this.originalZoom = this.controller.view.zoom;
};
/**
* @param {ascii.Vector} position
*/
ascii.TouchController.prototype.handleMove = function(position) {
// Initiate a drag if we have moved enough, quickly enough.
if (!this.dragStarted &&
($.now() - this.pressTimestamp) < DRAG_LATENCY &&
position.subtract(this.pressVector).length() > DRAG_ACCURACY) {
this.dragStarted = true;
this.controller.startDrag(position);
// Pass on the event.
this.controller.handleMove(position);
}
// Pass on the event.
this.controller.handleMove(position);
};
/**
* The multi-touch version of handleMove, effectively only deals with zooming.
* @param {ascii.Vector} positionOne
* @param {ascii.Vector} positionTwo
*/
ascii.TouchController.prototype.handleMoveMulti =
function(positionOne, positionTwo) {
if (this.zoomStarted) {
var newZoom = this.originalZoom *
positionOne.subtract(positionTwo).length() / this.zoomLength;
newZoom = Math.max(Math.min(newZoom, 5), 0.5);
this.controller.view.setZoom(newZoom);
/**
* The multi-touch version of handleMove, effectively only deals with zooming.
* @param {Vector} positionOne
* @param {Vector} positionTwo
*/
handleMoveMulti(positionOne, positionTwo) {
if (this.zoomStarted) {
var newZoom = this.originalZoom *
positionOne.subtract(positionTwo).length() / this.zoomLength;
newZoom = Math.max(Math.min(newZoom, 5), 0.5);
this.controller.view.setZoom(newZoom);
}
}
};
/**
* Ends all current actions, cleans up any state.
*/
ascii.TouchController.prototype.reset = function() {
this.dragStarted = false;
this.zoomStarted = false;
this.pressVector = null;
};
/**
* Ends all current actions, cleans up any state.
*/
reset() {
this.dragStarted = false;
this.zoomStarted = false;
this.pressVector = null;
}
/**
* Installs input bindings associated with touch controls.
*/
ascii.TouchController.prototype.installBindings = function() {
var canvas = this.controller.view.canvas;
/**
* Installs input bindings associated with touch controls.
*/
installBindings() {
var canvas = this.controller.view.canvas;
$(canvas).bind('touchstart', function(e) {
e.preventDefault();
if (e.originalEvent.touches.length == 1) {
this.handlePress(new ascii.Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY));
} else if (e.originalEvent.touches.length > 1) {
this.handlePressMulti(new ascii.Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY),
new ascii.Vector(
e.originalEvent.touches[1].pageX,
e.originalEvent.touches[1].pageY));
}
}.bind(this));
$(canvas).on('touchstart', e => {
e.preventDefault();
if (e.originalEvent.touches.length == 1) {
this.handlePress(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY));
} else if (e.originalEvent.touches.length > 1) {
this.handlePressMulti(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY),
new Vector(
e.originalEvent.touches[1].pageX,
e.originalEvent.touches[1].pageY));
}
});
$(canvas).bind('touchmove', function(e) {
e.preventDefault();
if (e.originalEvent.touches.length == 1) {
this.handleMove(new ascii.Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY));
} else if (e.originalEvent.touches.length > 1) {
this.handleMoveMulti(new ascii.Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY),
new ascii.Vector(
e.originalEvent.touches[1].pageX,
e.originalEvent.touches[1].pageY));
}
}.bind(this));
$(canvas).on('touchmove', e => {
e.preventDefault();
if (e.originalEvent.touches.length == 1) {
this.handleMove(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY));
} else if (e.originalEvent.touches.length > 1) {
this.handleMoveMulti(new Vector(
e.originalEvent.touches[0].pageX,
e.originalEvent.touches[0].pageY),
new Vector(
e.originalEvent.touches[1].pageX,
e.originalEvent.touches[1].pageY));
}
});
// Pass through, no special handling.
$(canvas).bind('touchend', function(e) {
e.preventDefault();
this.reset();
this.controller.endAll();
}.bind(this));
};
// Pass through, no special handling.
$(canvas).on('touchend', e => {
e.preventDefault();
this.reset();
this.controller.endAll();
});
}
}

View File

@ -1,14 +1,18 @@
import State from './state';
import View from './view';
import Controller from './controller';
import { TouchController, DesktopController } from './input-controller';
import DriveController from './drive-controller';
/**
* Runs the application.
*/
ascii.launch = function() {
var state = new ascii.State();
var view = new ascii.View(state);
var controller = new ascii.Controller(view, state);
var touchController = new ascii.TouchController(controller);
var desktopController = new ascii.DesktopController(controller);
var driveController = new ascii.DriveController(state, view);
(function() {
var state = new State();
var view = new View(state);
var controller = new Controller(view, state);
var touchController = new TouchController(controller);
var desktopController = new DesktopController(controller);
var driveController = new DriveController(state, view);
view.animate();
};
ascii.launch();
})();

View File

@ -1,343 +1,344 @@
import Vector from './vector';
import { Cell, MappedValue, MappedCell, CellContext, Box } from './common';
import * as c from './constants';
/**
* Holds the entire state of the diagram as a 2D array of cells
* and provides methods to modify the current state.
*
* @constructor
*/
ascii.State = function() {
/** @type {Array.<Array.<ascii.Cell>>} */
this.cells = new Array(MAX_GRID_WIDTH);
/** @type {Array.<ascii.MappedCell>} */
this.scratchCells = new Array();
/** @type {boolean} */
this.dirty = true;
export default class State {
constructor() {
/** @type {!Array<Array<Cell>>} */
this.cells = new Array(c.MAX_GRID_WIDTH);
/** @type {!Array<MappedCell>} */
this.scratchCells = [];
/** @type {boolean} */
this.dirty = true;
/** @type {Array.<Array.<ascii.MappedValue>>} */
this.undoStates = new Array();
/** @type {Array.<Array.<ascii.MappedValue>>} */
this.redoStates = new Array();
/** @type {!Array<Array<MappedValue>>|!Iterable<Iterable<MappedValue>>} */
this.undoStates = [];
/** @type {!Array<Array<MappedValue>>|!Iterable<Iterable<MappedValue>>} */
this.redoStates = [];
for (var i = 0; i < this.cells.length; i++) {
this.cells[i] = new Array(MAX_GRID_HEIGHT);
for (var j = 0; j < this.cells[i].length; j++) {
this.cells[i][j] = new ascii.Cell();
}
}
};
/**
* This clears the entire state, but is undoable.
*/
ascii.State.prototype.clear = function() {
for (var i = 0; i < this.cells.length; i++) {
for (var j = 0; j < this.cells[i].length; j++) {
var position = new ascii.Vector(i, j);
if (this.cells[i][j].getRawValue() != null) {
this.drawValue(new ascii.Vector(i, j), ERASE_CHAR);
for (var i = 0; i < this.cells.length; i++) {
this.cells[i] = new Array(c.MAX_GRID_HEIGHT);
for (var j = 0; j < this.cells[i].length; j++) {
this.cells[i][j] = new Cell();
}
}
}
this.commitDraw();
};
/**
* Returns the cell at the given coordinates.
*
* @param {ascii.Vector} vector
* @return {ascii.Cell}
*/
ascii.State.prototype.getCell = function(vector) {
return this.cells[vector.x][vector.y];
};
/**
* Sets the cells scratch (uncommitted) value at the given position.
*
* @param {ascii.Vector} position
* @param {?string} value
*/
ascii.State.prototype.drawValue = function(position, value) {
var cell = this.getCell(position);
this.scratchCells.push(new ascii.MappedCell(position, cell));
cell.scratchValue = value;
this.dirty = true;
};
/**
* Sets the cells scratch (uncommitted) value at the given position
* iff the value is different to what it already is.
*
* @param {ascii.Vector} position
* @param {?string} value
*/
ascii.State.prototype.drawValueIncremental = function(position, value) {
if (this.getCell(position).getRawValue() != value) {
this.drawValue(position, value);
}
};
/**
* Clears the current drawing scratchpad.
*/
ascii.State.prototype.clearDraw = function() {
for (var i in this.scratchCells) {
this.scratchCells[i].cell.scratchValue = null;
}
this.scratchCells.length = 0;
};
/**
* Returns the draw value of a cell at the given position.
*
* @param {ascii.Vector} position
* @return {?string}
*/
ascii.State.prototype.getDrawValue = function(position) {
var cell = this.getCell(position);
var value = cell.scratchValue != null ? cell.scratchValue : cell.value;
var isSpecial = SPECIAL_VALUES.indexOf(value) != -1;
var isAltSpecial = ALT_SPECIAL_VALUES.indexOf(value) != -1;
if (!isSpecial && !isAltSpecial) {
return value;
}
// Because the underlying state only stores actual cell values and there is
// no underlying representation of shapes, we do a lot of crazy logic here
// to make diagrams display as expected.
var context = this.getContext(position);
if (isSpecial && context.left && context.right && !context.up && !context.down) {
return SPECIAL_LINE_H;
}
if (isSpecial && !context.left && !context.right && context.up && context.down) {
return SPECIAL_LINE_V;
}
if (context.sum() == 4) {
return SPECIAL_LINE_H;
}
if (isAltSpecial && context.sum() == 3) {
if (!context.left) {
return SPECIAL_ARROW_LEFT;
}
if (!context.up) {
return SPECIAL_ARROW_UP;
}
if (!context.down) {
return SPECIAL_ARROW_DOWN;
}
if (!context.right) {
return SPECIAL_ARROW_RIGHT;
}
}
if ((isSpecial || isAltSpecial) && context.sum() == 3) {
this.extendContext(position, context);
if (!context.right && context.leftup && context.leftdown) {
return SPECIAL_LINE_V;
}
if (!context.left && context.rightup && context.rightdown) {
return SPECIAL_LINE_V;
}
if (!context.down && context.leftup && context.rightup) {
return SPECIAL_LINE_H;
}
if (!context.up && context.rightdown && context.leftdown) {
return SPECIAL_LINE_H;
}
var leftupempty = this.getCell(position.add(DIR_LEFT).add(DIR_UP)).isEmpty();
var rightupempty = this.getCell(position.add(DIR_RIGHT).add(DIR_UP)).isEmpty();
if (context.up && context.left && context.right && (!leftupempty || !rightupempty)) {
return SPECIAL_LINE_H;
}
var leftdownempty = this.getCell(position.add(DIR_LEFT).add(DIR_DOWN)).isEmpty();
var rightdownempty = this.getCell(position.add(DIR_RIGHT).add(DIR_DOWN)).isEmpty();
if (context.down && context.left && context.right && (!leftdownempty || !rightdownempty)) {
return SPECIAL_LINE_H;
}
return SPECIAL_VALUE;
}
if (isAltSpecial && context.sum() == 1) {
if (context.left) {
return SPECIAL_ARROW_RIGHT;
}
if (context.up) {
return SPECIAL_ARROW_DOWN;
}
if (context.down) {
return SPECIAL_ARROW_UP;
}
if (context.right) {
return SPECIAL_ARROW_LEFT;
}
}
return value;
};
/**
* @param {ascii.Vector} position
* @return {ascii.CellContext}
*/
ascii.State.prototype.getContext = function(position) {
var left = this.getCell(position.add(DIR_LEFT)).isSpecial();
var right = this.getCell(position.add(DIR_RIGHT)).isSpecial();
var up = this.getCell(position.add(DIR_UP)).isSpecial();
var down = this.getCell(position.add(DIR_DOWN)).isSpecial();
return new ascii.CellContext(left, right, up, down);
};
/**
* @param {ascii.Vector} position
* @param {ascii.CellContext} context
*/
ascii.State.prototype.extendContext = function(position, context) {
context.leftup = this.getCell(position.add(DIR_LEFT).add(DIR_UP)).isSpecial();
context.rightup = this.getCell(position.add(DIR_RIGHT).add(DIR_UP)).isSpecial();
context.leftdown = this.getCell(position.add(DIR_LEFT).add(DIR_DOWN)).isSpecial();
context.rightdown = this.getCell(position.add(DIR_RIGHT).add(DIR_DOWN)).isSpecial();
};
/**
* Ends the current draw, commiting anything currently drawn the scratchpad.
* @param {boolean=} opt_undo
*/
ascii.State.prototype.commitDraw = function(opt_undo) {
var oldValues = [];
// Dedupe the scratch values, or this causes havoc for history management.
var positions = this.scratchCells.map(function(value) {
return value.position.x.toString() + value.position.y.toString();
});
var scratchCellsUnique =
this.scratchCells.filter(function(value, index, arr) {
return positions.indexOf(positions[index]) == index;
});
this.scratchCells.length = 0;
for (var i in scratchCellsUnique) {
var position = scratchCellsUnique[i].position;
var cell = scratchCellsUnique[i].cell;
// Push the effective old value unto the array.
oldValues.push(new ascii.MappedValue(position,
cell.value != null ? cell.value : ' '));
var newValue = cell.getRawValue();
if (newValue == ERASE_CHAR || newValue == ' ') {
newValue = null;
}
// Let's store the actual drawed value, so behaviour matches what the user sees.
if (cell.isSpecial()) {
newValue = this.getDrawValue(position);
}
cell.scratchValue = null;
cell.value = newValue;
}
var stateStack = opt_undo ? this.redoStates : this.undoStates;
if (oldValues.length > 0) {
// If we have too many states, clear one out.
if (stateStack.length > MAX_UNDO) {
stateStack.shift();
}
stateStack.push(oldValues);
}
this.dirty = true;
};
/**
* Undoes the last committed state.
*/
ascii.State.prototype.undo = function() {
if (this.undoStates.length == 0) { return; }
var lastState = this.undoStates.pop();
for (var i in lastState) {
var mappedValue = lastState[i];
this.drawValue(mappedValue.position, mappedValue.value);
}
this.commitDraw(true);
};
/**
* Redoes the last undone.
*/
ascii.State.prototype.redo = function() {
if (this.redoStates.length == 0) { return; }
var lastState = this.redoStates.pop();
for (var i in lastState) {
var mappedValue = lastState[i];
this.drawValue(mappedValue.position, mappedValue.value);
}
this.commitDraw();
};
/**
* Outputs the entire contents of the diagram as text.
* @param {ascii.Box=} opt_box
* @return {string}
*/
ascii.State.prototype.outputText = function(opt_box) {
// Find the first/last cells in the diagram so we don't output everything.
var start = new ascii.Vector(Number.MAX_VALUE, Number.MAX_VALUE);
var end = new ascii.Vector(-1, -1);
if (!opt_box) {
/**
* This clears the entire state, but is undoable.
*/
clear() {
for (var i = 0; i < this.cells.length; i++) {
for (var j = 0; j < this.cells[i].length; j++) {
var position = new Vector(i, j);
if (this.cells[i][j].getRawValue() != null) {
if (i < start.x) { start.x = i; }
if (j < start.y) { start.y = j; }
if (i > end.x) { end.x = i; }
if (j > end.y) { end.y = j; }
this.drawValue(new Vector(i, j), c.ERASE_CHAR);
}
}
}
if (end.x < 0) { return '' }
} else {
start = opt_box.topLeft();
end = opt_box.bottomRight();
this.commitDraw();
}
var output = '';
for (var j = start.y; j <= end.y; j++) {
var line = '';
for (var i = start.x; i <= end.x; i++) {
var val = this.getDrawValue(new ascii.Vector(i, j));
line += (val == null || val == ERASE_CHAR) ? ' ' : val;
}
// Trim end whitespace.
output += line.replace(/\s+$/, '') + '\n';
/**
* Returns the cell at the given coordinates.
*
* @param {Vector} vector
* @return {Cell}
*/
getCell(vector) {
return this.cells[vector.x][vector.y];
}
return output;
};
/**
* Loads the given text into the diagram starting at the given offset (centered).
* @param {string} value
* @param {ascii.Vector} offset
*/
ascii.State.prototype.fromText = function(value, offset) {
var lines = value.split('\n');
var middle = new ascii.Vector(0, Math.round(lines.length / 2));
for (var j = 0; j < lines.length; j++) {
middle.x = Math.max(middle.x, Math.round(lines[j].length / 2));
/**
* Sets the cells scratch (uncommitted) value at the given position.
*
* @param {Vector} position
* @param {?string} value
*/
drawValue(position, value) {
var cell = this.getCell(position);
this.scratchCells.push(new MappedCell(position, cell));
cell.scratchValue = value;
this.dirty = true;
}
for (var j = 0; j < lines.length; j++) {
var line = lines[j];
for (var i = 0; i < line.length; i++) {
var char = line.charAt(i);
// Convert special output back to special chars.
// TODO: This is a horrible hack, need to handle multiple special chars
// correctly and preserve them through line drawing etc.
if (SPECIAL_VALUES.indexOf(char) != -1) {
char = SPECIAL_VALUE;
}
if (ALT_SPECIAL_VALUES.indexOf(char) != -1) {
char = ALT_SPECIAL_VALUE;
}
this.drawValue(new ascii.Vector(i, j).add(offset).subtract(middle), char);
/**
* Sets the cells scratch (uncommitted) value at the given position
* iff the value is different to what it already is.
*
* @param {Vector} position
* @param {?string} value
*/
drawValueIncremental(position, value) {
if (this.getCell(position).getRawValue() != value) {
this.drawValue(position, value);
}
}
};
/**
* Clears the current drawing scratchpad.
*/
clearDraw() {
for (var { cell } of this.scratchCells) {
cell.scratchValue = null;
}
this.scratchCells.length = 0;
}
/**
* Returns the draw value of a cell at the given position.
*
* @param {Vector} position
* @return {?string}
*/
getDrawValue(position) {
var cell = this.getCell(position);
var value = cell.scratchValue != null ? cell.scratchValue : cell.value;
var isSpecial = c.SPECIAL_VALUES.indexOf(value) != -1;
var isAltSpecial = c.ALT_SPECIAL_VALUES.indexOf(value) != -1;
if (!isSpecial && !isAltSpecial) {
return value;
}
// Because the underlying state only stores actual cell values and there is
// no underlying representation of shapes, we do a lot of crazy logic here
// to make diagrams display as expected.
var context = this.getContext(position);
if (isSpecial && context.left && context.right && !context.up && !context.down) {
return c.SPECIAL_LINE_H;
}
if (isSpecial && !context.left && !context.right && context.up && context.down) {
return c.SPECIAL_LINE_V;
}
if (context.sum() == 4) {
return c.SPECIAL_LINE_H;
}
if (isAltSpecial && context.sum() == 3) {
if (!context.left) {
return c.SPECIAL_ARROW_LEFT;
}
if (!context.up) {
return c.SPECIAL_ARROW_UP;
}
if (!context.down) {
return c.SPECIAL_ARROW_DOWN;
}
if (!context.right) {
return c.SPECIAL_ARROW_RIGHT;
}
}
if ((isSpecial || isAltSpecial) && context.sum() == 3) {
this.extendContext(position, context);
if (!context.right && context.leftup && context.leftdown) {
return c.SPECIAL_LINE_V;
}
if (!context.left && context.rightup && context.rightdown) {
return c.SPECIAL_LINE_V;
}
if (!context.down && context.leftup && context.rightup) {
return c.SPECIAL_LINE_H;
}
if (!context.up && context.rightdown && context.leftdown) {
return c.SPECIAL_LINE_H;
}
var leftupempty = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_UP)).isEmpty();
var rightupempty = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_UP)).isEmpty();
if (context.up && context.left && context.right && (!leftupempty || !rightupempty)) {
return c.SPECIAL_LINE_H;
}
var leftdownempty = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_DOWN)).isEmpty();
var rightdownempty = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_DOWN)).isEmpty();
if (context.down && context.left && context.right && (!leftdownempty || !rightdownempty)) {
return c.SPECIAL_LINE_H;
}
return c.SPECIAL_VALUE;
}
if (isAltSpecial && context.sum() == 1) {
if (context.left) {
return c.SPECIAL_ARROW_RIGHT;
}
if (context.up) {
return c.SPECIAL_ARROW_DOWN;
}
if (context.down) {
return c.SPECIAL_ARROW_UP;
}
if (context.right) {
return c.SPECIAL_ARROW_LEFT;
}
}
return value;
}
/**
* @param {Vector} position
* @return {CellContext}
*/
getContext(position) {
var left = this.getCell(position.add(c.DIR_LEFT)).isSpecial();
var right = this.getCell(position.add(c.DIR_RIGHT)).isSpecial();
var up = this.getCell(position.add(c.DIR_UP)).isSpecial();
var down = this.getCell(position.add(c.DIR_DOWN)).isSpecial();
return new CellContext(left, right, up, down);
}
/**
* @param {Vector} position
* @param {CellContext} context
*/
extendContext(position, context) {
context.leftup = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_UP)).isSpecial();
context.rightup = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_UP)).isSpecial();
context.leftdown = this.getCell(position.add(c.DIR_LEFT).add(c.DIR_DOWN)).isSpecial();
context.rightdown = this.getCell(position.add(c.DIR_RIGHT).add(c.DIR_DOWN)).isSpecial();
}
/**
* Ends the current draw, commiting anything currently drawn the scratchpad.
* @param {boolean=} opt_undo
*/
commitDraw(opt_undo) {
var oldValues = [];
// Dedupe the scratch values, or this causes havoc for history management.
var positions = this.scratchCells.map(function(value) {
return value.position.x.toString() + value.position.y.toString();
});
var scratchCellsUnique =
this.scratchCells.filter(function(value, index, arr) {
return positions.indexOf(positions[index]) == index;
});
this.scratchCells.length = 0;
for (var { position, cell } of scratchCellsUnique) {
// Push the effective old value unto the array.
oldValues.push(new MappedValue(position,
cell.value != null ? cell.value : ' '));
var newValue = cell.getRawValue();
if (newValue == c.ERASE_CHAR || newValue == ' ') {
newValue = null;
}
// Let's store the actual drawed value, so behaviour matches what the user sees.
if (cell.isSpecial()) {
newValue = this.getDrawValue(position);
}
cell.scratchValue = null;
cell.value = newValue;
}
var stateStack = opt_undo ? this.redoStates : this.undoStates;
if (oldValues.length > 0) {
// If we have too many states, clear one out.
if (stateStack.length > c.MAX_UNDO) {
stateStack.shift();
}
stateStack.push(oldValues);
}
this.dirty = true;
}
/**
* Undoes the last committed state.
*/
undo() {
if (this.undoStates.length == 0) { return; }
var lastState = this.undoStates.pop();
for (var i in lastState) {
var mappedValue = lastState[i];
this.drawValue(mappedValue.position, mappedValue.value);
}
this.commitDraw(true);
}
/**
* Redoes the last undone.
*/
redo() {
if (this.redoStates.length == 0) { return; }
var lastState = this.redoStates.pop();
for (var mappedValue of lastState) {
this.drawValue(mappedValue.position, mappedValue.value);
}
this.commitDraw();
}
/**
* Outputs the entire contents of the diagram as text.
* @param {Box=} opt_box
* @return {string}
*/
outputText(opt_box) {
// Find the first/last cells in the diagram so we don't output everything.
var start = new Vector(Number.MAX_VALUE, Number.MAX_VALUE);
var end = new Vector(-1, -1);
if (!opt_box) {
for (var i = 0; i < this.cells.length; i++) {
for (var j = 0; j < this.cells[i].length; j++) {
if (this.cells[i][j].getRawValue() != null) {
if (i < start.x) { start.x = i; }
if (j < start.y) { start.y = j; }
if (i > end.x) { end.x = i; }
if (j > end.y) { end.y = j; }
}
}
}
if (end.x < 0) { return '' }
} else {
start = opt_box.topLeft();
end = opt_box.bottomRight();
}
var output = '';
for (var j = start.y; j <= end.y; j++) {
var line = '';
for (var i = start.x; i <= end.x; i++) {
var val = this.getDrawValue(new Vector(i, j));
line += (val == null || val == c.ERASE_CHAR) ? ' ' : val;
}
// Trim end whitespace.
output += line.replace(/\s+$/, '') + '\n';
}
return output;
}
/**
* Loads the given text into the diagram starting at the given offset (centered).
* @param {string} value
* @param {Vector} offset
*/
fromText(value, offset) {
var lines = value.split('\n');
var middle = new Vector(0, Math.round(lines.length / 2));
for (var j = 0; j < lines.length; j++) {
middle.x = Math.max(middle.x, Math.round(lines[j].length / 2));
}
for (var j = 0; j < lines.length; j++) {
var line = lines[j];
for (var i = 0; i < line.length; i++) {
var char = line.charAt(i);
// Convert special output back to special chars.
// TODO: This is a horrible hack, need to handle multiple special chars
// correctly and preserve them through line drawing etc.
if (c.SPECIAL_VALUES.indexOf(char) != -1) {
char = c.SPECIAL_VALUE;
}
if (c.ALT_SPECIAL_VALUES.indexOf(char) != -1) {
char = c.ALT_SPECIAL_VALUE;
}
this.drawValue(new Vector(i, j).add(offset).subtract(middle), char);
}
}
}
}

57
js-lib/vector.js Normal file
View File

@ -0,0 +1,57 @@
/**
* Stores a 2D vector.
*/
export default class Vector {
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
/** type {number} */ this.x = x;
/** type {number} */ this.y = y;
}
/**
* @param {Vector} other
* @return {boolean}
*/
equals(other) {
return (other != null) && (this.x == other.x) && (this.y == other.y);
}
/**
* @param {Vector} other
* @return {Vector}
*/
subtract(other) {
return new Vector(this.x - other.x, this.y - other.y);
}
/**
* @param {Vector} other
* @return {Vector}
*/
add(other) {
return new Vector(this.x + other.x, this.y + other.y);
}
/**
* @return {Vector}
*/
clone() {
return new Vector(this.x, this.y);
}
/** @return {number} */
length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
/**
* @param {number} scale
* @return {Vector}
*/
scale(scale) {
return new Vector(this.x * scale, this.y * scale);
}
}

View File

@ -1,267 +1,273 @@
import State from './state';
import Vector from './vector';
import * as c from './constants';
/**
* Handles view operations, state and management of the screen.
*
* @constructor
* @param {ascii.State} state
*/
ascii.View = function(state) {
/** @type {ascii.State} */ this.state = state;
export default class View {
/**
* @param {State} state
*/
constructor(state) {
/** @type {State} */ this.state = state;
/** @type {Element} */ this.canvas = document.getElementById('ascii-canvas');
/** @type {Object} */ this.context = this.canvas.getContext('2d');
/** @type {Element} */ this.canvas = document.getElementById('ascii-canvas');
/** @type {Object} */ this.context = this.canvas.getContext('2d');
/** @type {number} */ this.zoom = 1;
/** @type {ascii.Vector} */ this.offset = new ascii.Vector(
MAX_GRID_WIDTH * CHAR_PIXELS_H / 2,
MAX_GRID_HEIGHT * CHAR_PIXELS_V / 2);
/** @type {number} */ this.zoom = 1;
/** @type {Vector} */ this.offset = new Vector(
c.MAX_GRID_WIDTH * c.CHAR_PIXELS_H / 2,
c.MAX_GRID_HEIGHT * c.CHAR_PIXELS_V / 2);
/** @type {boolean} */ this.dirty = true;
// TODO: Should probably save this setting in a cookie or something.
/** @type {boolean} */ this.useLines = false;
/** @type {boolean} */ this.dirty = true;
// TODO: Should probably save this setting in a cookie or something.
/** @type {boolean} */ this.useLines = false;
this.resizeCanvas();
};
/**
* Resizes the canvas, should be called if the viewport size changes.
*/
ascii.View.prototype.resizeCanvas = function() {
this.canvas.width = document.documentElement.clientWidth;
this.canvas.height = document.documentElement.clientHeight;
this.dirty = true;
};
/**
* Starts the animation loop for the canvas. Should only be called once.
*/
ascii.View.prototype.animate = function() {
if (this.dirty || this.state.dirty) {
this.dirty = false;
this.state.dirty = false;
this.render();
this.resizeCanvas();
}
var view = this;
window.requestAnimationFrame(function() { view.animate(); });
};
/**
* Renders the given state to the canvas.
* TODO: Room for efficiency here still. Drawing should be incremental,
* however performance is currently very acceptable on test devices.
*/
ascii.View.prototype.render = function() {
var context = this.context;
context.setTransform(1, 0, 0, 1, 0, 0);
// Clear the visible area.
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
context.scale(this.zoom, this.zoom);
context.translate(
this.canvas.width / 2 / this.zoom,
this.canvas.height / 2 / this.zoom);
// Only render grid lines and cells that are visible.
var startOffset = this.screenToCell(new ascii.Vector(
0,
0))
.subtract(new ascii.Vector(
RENDER_PADDING_CELLS, RENDER_PADDING_CELLS));
var endOffset = this.screenToCell(new ascii.Vector(
this.canvas.width,
this.canvas.height))
.add(new ascii.Vector(
RENDER_PADDING_CELLS, RENDER_PADDING_CELLS));
startOffset.x = Math.max(0, Math.min(startOffset.x, MAX_GRID_WIDTH));
endOffset.x = Math.max(0, Math.min(endOffset.x, MAX_GRID_WIDTH));
startOffset.y = Math.max(0, Math.min(startOffset.y, MAX_GRID_HEIGHT));
endOffset.y = Math.max(0, Math.min(endOffset.y, MAX_GRID_HEIGHT));
// Render the grid.
context.lineWidth = '1';
context.strokeStyle = '#EEEEEE';
context.beginPath();
for (var i = startOffset.x; i < endOffset.x; i++) {
context.moveTo(
i * CHAR_PIXELS_H - this.offset.x,
0 - this.offset.y);
context.lineTo(
i * CHAR_PIXELS_H - this.offset.x,
this.state.cells.length * CHAR_PIXELS_V - this.offset.y);
/**
* Resizes the canvas, should be called if the viewport size changes.
*/
resizeCanvas() {
this.canvas.width = document.documentElement.clientWidth;
this.canvas.height = document.documentElement.clientHeight;
this.dirty = true;
}
for (var j = startOffset.y; j < endOffset.y; j++) {
context.moveTo(
0 - this.offset.x,
j * CHAR_PIXELS_V - this.offset.y);
context.lineTo(
this.state.cells.length * CHAR_PIXELS_H - this.offset.x,
j * CHAR_PIXELS_V - this.offset.y);
}
this.context.stroke();
this.renderText(context, startOffset, endOffset, !this.useLines);
if (this.useLines) {
this.renderCellsAsLines(context, startOffset, endOffset);
}
};
ascii.View.prototype.renderText = function(context, startOffset, endOffset, drawSpecials) {
// Render cells.
context.font = '15px Courier New';
for (var i = startOffset.x; i < endOffset.x; i++) {
/**
* Starts the animation loop for the canvas. Should only be called once.
*/
animate() {
if (this.dirty || this.state.dirty) {
this.dirty = false;
this.state.dirty = false;
this.render();
}
var view = this;
window.requestAnimationFrame(function() { view.animate(); });
}
/**
* Renders the given state to the canvas.
* TODO: Room for efficiency here still. Drawing should be incremental,
* however performance is currently very acceptable on test devices.
*/
render() {
var context = this.context;
context.setTransform(1, 0, 0, 1, 0, 0);
// Clear the visible area.
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
context.scale(this.zoom, this.zoom);
context.translate(
this.canvas.width / 2 / this.zoom,
this.canvas.height / 2 / this.zoom);
// Only render grid lines and cells that are visible.
var startOffset = this.screenToCell(new Vector(
0,
0))
.subtract(new Vector(
c.RENDER_PADDING_CELLS, c.RENDER_PADDING_CELLS));
var endOffset = this.screenToCell(new Vector(
this.canvas.width,
this.canvas.height))
.add(new Vector(
c.RENDER_PADDING_CELLS, c.RENDER_PADDING_CELLS));
startOffset.x = Math.max(0, Math.min(startOffset.x, c.MAX_GRID_WIDTH));
endOffset.x = Math.max(0, Math.min(endOffset.x, c.MAX_GRID_WIDTH));
startOffset.y = Math.max(0, Math.min(startOffset.y, c.MAX_GRID_HEIGHT));
endOffset.y = Math.max(0, Math.min(endOffset.y, c.MAX_GRID_HEIGHT));
// Render the grid.
context.lineWidth = '1';
context.strokeStyle = '#EEEEEE';
context.beginPath();
for (var i = startOffset.x; i < endOffset.x; i++) {
context.moveTo(
i * c.CHAR_PIXELS_H - this.offset.x,
0 - this.offset.y);
context.lineTo(
i * c.CHAR_PIXELS_H - this.offset.x,
this.state.cells.length * c.CHAR_PIXELS_V - this.offset.y);
}
for (var j = startOffset.y; j < endOffset.y; j++) {
var cell = this.state.getCell(new ascii.Vector(i, j));
// Highlight the cell if it is special (grey) or it is part
// of a visible edit (blue).
if (cell.isSpecial() ||
(cell.hasScratch() && cell.getRawValue() != ' ')) {
this.context.fillStyle = cell.hasScratch() ? '#DEF' : '#F5F5F5';
context.fillRect(
i * CHAR_PIXELS_H - this.offset.x,
(j - 1) * CHAR_PIXELS_V - this.offset.y,
CHAR_PIXELS_H, CHAR_PIXELS_V);
}
var cellValue = this.state.getDrawValue(new ascii.Vector(i, j));
if (cellValue != null && (!cell.isSpecial() || drawSpecials)) {
this.context.fillStyle = '#000000';
context.fillText(cellValue,
i * CHAR_PIXELS_H - this.offset.x,
j * CHAR_PIXELS_V - this.offset.y - 3);
context.moveTo(
0 - this.offset.x,
j * c.CHAR_PIXELS_V - this.offset.y);
context.lineTo(
this.state.cells.length * c.CHAR_PIXELS_H - this.offset.x,
j * c.CHAR_PIXELS_V - this.offset.y);
}
this.context.stroke();
this.renderText(context, startOffset, endOffset, !this.useLines);
if (this.useLines) {
this.renderCellsAsLines(context, startOffset, endOffset);
}
}
renderText(context, startOffset, endOffset, drawSpecials) {
// Render cells.
context.font = '15px Courier New';
for (var i = startOffset.x; i < endOffset.x; i++) {
for (var j = startOffset.y; j < endOffset.y; j++) {
var cell = this.state.getCell(new Vector(i, j));
// Highlight the cell if it is special (grey) or it is part
// of a visible edit (blue).
if (cell.isSpecial() ||
(cell.hasScratch() && cell.getRawValue() != ' ')) {
this.context.fillStyle = cell.hasScratch() ? '#DEF' : '#F5F5F5';
context.fillRect(
i * c.CHAR_PIXELS_H - this.offset.x,
(j - 1) * c.CHAR_PIXELS_V - this.offset.y,
c.CHAR_PIXELS_H, c.CHAR_PIXELS_V);
}
var cellValue = this.state.getDrawValue(new Vector(i, j));
if (cellValue != null && (!cell.isSpecial() || drawSpecials)) {
this.context.fillStyle = '#000000';
context.fillText(cellValue,
i * c.CHAR_PIXELS_H - this.offset.x,
j * c.CHAR_PIXELS_V - this.offset.y - 3);
}
}
}
}
renderCellsAsLines(context, startOffset, endOffset) {
context.lineWidth = '1';
context.strokeStyle = '#000000';
context.beginPath();
for (var i = startOffset.x; i < endOffset.x; i++) {
var startY = false;
for (var j = startOffset.y; j < endOffset.y; j++) {
var cell = this.state.getCell(new Vector(i, j));
if ((!cell.isSpecial() || j == endOffset.y - 1) && startY) {
context.moveTo(
i * c.CHAR_PIXELS_H - this.offset.x + c.CHAR_PIXELS_H/2,
startY * c.CHAR_PIXELS_V - this.offset.y - c.CHAR_PIXELS_V/2);
context.lineTo(
i * c.CHAR_PIXELS_H - this.offset.x + c.CHAR_PIXELS_H/2,
(j - 1) * c.CHAR_PIXELS_V - this.offset.y - c.CHAR_PIXELS_V/2);
startY = false;
}
if (cell.isSpecial() && !startY) {
startY = j;
}
}
}
for (var j = startOffset.y; j < endOffset.y; j++) {
var startX = false;
for (var i = startOffset.x; i < endOffset.x; i++) {
var cell = this.state.getCell(new Vector(i, j));
if ((!cell.isSpecial() || i == endOffset.x - 1) && startX) {
context.moveTo(
startX * c.CHAR_PIXELS_H - this.offset.x + c.CHAR_PIXELS_H/2,
j * c.CHAR_PIXELS_V - this.offset.y - c.CHAR_PIXELS_V/2);
context.lineTo(
(i -1) * c.CHAR_PIXELS_H - this.offset.x + c.CHAR_PIXELS_H/2,
j * c.CHAR_PIXELS_V - this.offset.y - c.CHAR_PIXELS_V/2);
startX = false;
}
if (cell.isSpecial() && !startX) {
startX = i;
}
}
}
this.context.stroke();
}
/**
* @param {number} zoom
*/
setZoom(zoom) {
this.zoom = zoom;
this.dirty = true;
}
/**
* @param {Vector} offset
*/
setOffset(offset) {
this.offset = offset;
this.dirty = true;
};
/**
* @param {boolean} useLines
*/
setUseLines(useLines) {
this.useLines = useLines;
this.dirty = true;
}
/**
* Given a screen coordinate, find the frame coordinates.
* @param {Vector} vector
* @return {Vector}
*/
screenToFrame(vector) {
return new Vector(
(vector.x - this.canvas.width / 2) / this.zoom + this.offset.x,
(vector.y - this.canvas.height / 2) / this.zoom + this.offset.y);
}
/**
* Given a frame coordinate, find the screen coordinates.
* @param {Vector} vector
* @return {Vector}
*/
frameToScreen(vector) {
return new Vector(
(vector.x - this.offset.x) * this.zoom + this.canvas.width / 2,
(vector.y - this.offset.y) * this.zoom + this.canvas.height / 2);
}
/**
* Given a frame coordinate, return the indices for the nearest cell.
* @param {Vector} vector
* @return {Vector}
*/
frameToCell(vector) {
// We limit the edges in a bit, as most drawing needs a full context to work.
return new Vector(
Math.min(Math.max(1,
Math.round((vector.x - c.CHAR_PIXELS_H / 2) / c.CHAR_PIXELS_H)),
c.MAX_GRID_WIDTH - 2),
Math.min(Math.max(1,
Math.round((vector.y + c.CHAR_PIXELS_V / 2) / c.CHAR_PIXELS_V)),
c.MAX_GRID_HEIGHT - 2));
}
/**
* Given a cell coordinate, return the frame coordinates.
* @param {Vector} vector
* @return {Vector}
*/
cellToFrame(vector) {
return new Vector(
Math.round(vector.x * c.CHAR_PIXELS_H),
Math.round(vector.y * c.CHAR_PIXELS_V));
}
/**
* Given a screen coordinate, return the indices for the nearest cell.
* @param {Vector} vector
* @return {Vector}
*/
screenToCell(vector) {
return this.frameToCell(this.screenToFrame(vector));
}
/**
* Given a cell coordinate, return the on screen coordinates.
* @param {Vector} vector
* @return {Vector}
*/
cellToScreen(vector) {
return this.frameToScreen(this.cellToFrame(vector));
}
}
ascii.View.prototype.renderCellsAsLines = function(context, startOffset, endOffset) {
context.lineWidth = '1';
context.strokeStyle = '#000000';
context.beginPath();
for (var i = startOffset.x; i < endOffset.x; i++) {
var startY = false;
for (var j = startOffset.y; j < endOffset.y; j++) {
var cell = this.state.getCell(new ascii.Vector(i, j));
if ((!cell.isSpecial() || j == endOffset.y - 1) && startY) {
context.moveTo(
i * CHAR_PIXELS_H - this.offset.x + CHAR_PIXELS_H/2,
startY * CHAR_PIXELS_V - this.offset.y - CHAR_PIXELS_V/2);
context.lineTo(
i * CHAR_PIXELS_H - this.offset.x + CHAR_PIXELS_H/2,
(j - 1) * CHAR_PIXELS_V - this.offset.y - CHAR_PIXELS_V/2);
startY = false;
}
if (cell.isSpecial() && !startY) {
startY = j;
}
}
}
for (var j = startOffset.y; j < endOffset.y; j++) {
var startX = false;
for (var i = startOffset.x; i < endOffset.x; i++) {
var cell = this.state.getCell(new ascii.Vector(i, j));
if ((!cell.isSpecial() || i == endOffset.x - 1) && startX) {
context.moveTo(
startX * CHAR_PIXELS_H - this.offset.x + CHAR_PIXELS_H/2,
j * CHAR_PIXELS_V - this.offset.y - CHAR_PIXELS_V/2);
context.lineTo(
(i -1) * CHAR_PIXELS_H - this.offset.x + CHAR_PIXELS_H/2,
j * CHAR_PIXELS_V - this.offset.y - CHAR_PIXELS_V/2);
startX = false;
}
if (cell.isSpecial() && !startX) {
startX = i;
}
}
}
this.context.stroke();
};
/**
* @param {number} zoom
*/
ascii.View.prototype.setZoom = function(zoom) {
this.zoom = zoom;
this.dirty = true;
};
/**
* @param {ascii.Vector} offset
*/
ascii.View.prototype.setOffset = function(offset) {
this.offset = offset;
this.dirty = true;
};
/**
* @param {boolean} useLines
*/
ascii.View.prototype.setUseLines = function(useLines) {
this.useLines = useLines;
this.dirty = true;
};
/**
* Given a screen coordinate, find the frame coordinates.
* @param {ascii.Vector} vector
* @return {ascii.Vector}
*/
ascii.View.prototype.screenToFrame = function(vector) {
return new ascii.Vector(
(vector.x - this.canvas.width / 2) / this.zoom + this.offset.x,
(vector.y - this.canvas.height / 2) / this.zoom + this.offset.y);
};
/**
* Given a frame coordinate, find the screen coordinates.
* @param {ascii.Vector} vector
* @return {ascii.Vector}
*/
ascii.View.prototype.frameToScreen = function(vector) {
return new ascii.Vector(
(vector.x - this.offset.x) * this.zoom + this.canvas.width / 2,
(vector.y - this.offset.y) * this.zoom + this.canvas.height / 2);
};
/**
* Given a frame coordinate, return the indices for the nearest cell.
* @param {ascii.Vector} vector
* @return {ascii.Vector}
*/
ascii.View.prototype.frameToCell = function(vector) {
// We limit the edges in a bit, as most drawing needs a full context to work.
return new ascii.Vector(
Math.min(Math.max(1,
Math.round((vector.x - CHAR_PIXELS_H / 2) / CHAR_PIXELS_H)),
MAX_GRID_WIDTH - 2),
Math.min(Math.max(1,
Math.round((vector.y + CHAR_PIXELS_V / 2) / CHAR_PIXELS_V)),
MAX_GRID_HEIGHT - 2));
};
/**
* Given a cell coordinate, return the frame coordinates.
* @param {ascii.Vector} vector
* @return {ascii.Vector}
*/
ascii.View.prototype.cellToFrame = function(vector) {
return new ascii.Vector(
Math.round(vector.x * CHAR_PIXELS_H),
Math.round(vector.y * CHAR_PIXELS_V));
};
/**
* Given a screen coordinate, return the indices for the nearest cell.
* @param {ascii.Vector} vector
* @return {ascii.Vector}
*/
ascii.View.prototype.screenToCell = function(vector) {
return this.frameToCell(this.screenToFrame(vector));
};
/**
* Given a cell coordinate, return the on screen coordinates.
* @param {ascii.Vector} vector
* @return {ascii.Vector}
*/
ascii.View.prototype.cellToScreen = function(vector) {
return this.frameToScreen(this.cellToFrame(vector));
};