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:
parent
8a50b5ab58
commit
141b8eff92
|
@ -3,3 +3,4 @@ closure-library/*
|
|||
.settings/*
|
||||
*~
|
||||
_site/*
|
||||
.DS_Store
|
||||
|
|
1
README
1
README
|
@ -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.
|
||||
|
|
Binary file not shown.
14
compile.sh
14
compile.sh
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
1420
js-compiled.js
1420
js-compiled.js
File diff suppressed because it is too large
Load Diff
315
js-lib/common.js
315
js-lib/common.js
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
872
js-lib/draw.js
872
js-lib/draw.js
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
})();
|
||||
|
|
641
js-lib/state.js
641
js-lib/state.js
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
508
js-lib/view.js
508
js-lib/view.js
|
@ -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));
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue