diff --git a/js-lib/common.js b/js-lib/common.js index 10f1fbd..5ddac21 100644 --- a/js-lib/common.js +++ b/js-lib/common.js @@ -31,3 +31,9 @@ ascii.Vector.prototype.add = function(other) { ascii.Vector.prototype.length = function() { return Math.sqrt(this.x * this.x + this.y * this.y); }; + +/** @return {ascii.Vector} */ +ascii.Vector.prototype.scale = function(scale) { + return new ascii.Vector(this.x * scale, this.y * scale); +}; + diff --git a/js-lib/controller.js b/js-lib/controller.js index a21ed26..1f03830 100644 --- a/js-lib/controller.js +++ b/js-lib/controller.js @@ -7,6 +7,9 @@ goog.provide('ascii.Controller'); goog.require('ascii.Vector'); goog.require('ascii.View'); +/** @const */ var DRAG_LATENCY = 200; // Milliseconds. +/** @const */ var DRAG_ACCURACY = 5; // Pixels. + /** * @constructor */ @@ -14,32 +17,56 @@ ascii.Controller = function(view, state) { /** @type {ascii.View} */ this.view = view; /** @type {ascii.State} */ this.state = state; + /** @type {ascii.Vector} */ this.dragOrigin; /** @type {ascii.Vector} */ this.pressVector; + /** @type {number} */ this.pressTimestamp; this.installDesktopBindings(); }; ascii.Controller.prototype.handlePress = function(x, y) { this.pressVector = new ascii.Vector(x, y); + this.pressTimestamp = $.now(); }; ascii.Controller.prototype.handleMove = function(x, y) { - if (this.pressVector != null) { - // Drag has started. - this.view.offset.x -= (x - this.pressVector.x)/this.view.zoom; - this.view.offset.y -= (y - this.pressVector.y)/this.view.zoom; - this.pressVector = new ascii.Vector(x, y); + var position = new ascii.Vector(x, y); + + if (this.pressVector == null) { return; } // No clicks, so just ignore. + + // Initiate a drag if we have moved enough, quickly enough. + if (this.dragOrigin == null && + ($.now() - this.pressTimestamp) < DRAG_LATENCY && + position.subtract(this.pressVector).length() > DRAG_ACCURACY) { + this.dragOrigin = this.view.offset; + } + + // Drag in progress, update the view origin. + if (this.dragOrigin != null) { + this.view.offset = this.dragOrigin.add( + this.pressVector + .subtract(new ascii.Vector(x, y)) + .scale(1/this.view.zoom)); + } + + // Drag wasn't initiated in time, treat this as a drawing event. + if (this.dragOrigin == null && ($.now() - this.pressTimestamp) >= DRAG_LATENCY) { + // TODO: Draw stuff. + this.state.getCell(this.view.screenToCell(position)).value = 'O'; } }; ascii.Controller.prototype.handleRelease = function(x, y) { var position = new ascii.Vector(x, y); - if (this.pressVector.equals(position)) { - // We should handle this as a 'click' as there was no dragging involved. - // Hand off to the state controller, as this will initiate a modification of the diagram itself. - this.state.getCell(this.view.screenToFrame(position)).value = 'P'; + // Drag wasn't initiated in time, treat this as a drawing event. + if (this.dragOrigin == null && + ($.now() - this.pressTimestamp) >= DRAG_LATENCY && + position.subtract(this.pressVector).length() > DRAG_ACCURACY) { + // TODO: Draw stuff. } this.pressVector = null; + this.pressTimestamp = 0; + this.dragOrigin = null; }; ascii.Controller.prototype.handleZoom = function(delta) { @@ -62,4 +89,5 @@ ascii.Controller.prototype.installDesktopBindings = function() { $(this.view.canvas).mousemove(function(e) { controller.handleMove(e.clientX, e.clientY); }); + $(window).resize(function(e) { controller.view.resizeCanvas() }); }; diff --git a/js-lib/state.js b/js-lib/state.js index aa5987c..0df4421 100644 --- a/js-lib/state.js +++ b/js-lib/state.js @@ -32,10 +32,6 @@ ascii.State = function() { } }; -/** - * @param {ascii.Vector} vector - * @return {ascii.Cell} - */ ascii.State.prototype.getCell = function(vector) { - return this.cells[Math.round((vector.x-7.5)/15)][Math.round((vector.y+7.5)/15)]; + return this.cells[vector.x][vector.y]; }; diff --git a/js-lib/view.js b/js-lib/view.js index 7353c4b..b8e56e0 100644 --- a/js-lib/view.js +++ b/js-lib/view.js @@ -1,20 +1,19 @@ -/** - * Functions relating to view operations and management of the screen. - */ goog.provide('ascii.View'); goog.require('ascii.Vector'); /** @const */ ascii.CHARACTER_PIXELS = 15; + /** + * Object relating to view operations and management of the screen. * @constructor */ ascii.View = function(state) { /** @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(2000, 2000); + /** @type {ascii.Vector} */ this.offset = new ascii.Vector(7500, 7500); /** @type {ascii.State} */ this.state = state; this.resizeCanvas(); }; @@ -24,12 +23,18 @@ ascii.View.prototype.resizeCanvas = function() { this.canvas.height = document.documentElement.clientHeight; }; +/** + * Starts the animation loop for the canvas. Should only be called once. + */ ascii.View.prototype.animate = function() { this.render(); var view = this; window.requestAnimationFrame(function() { view.animate(); }); }; +/** + * Renders the given state to the canvas. + */ ascii.View.prototype.render = function() { this.context.setTransform(1, 0, 0, 1, 0, 0); // Clear the visible area. @@ -38,9 +43,11 @@ ascii.View.prototype.render = function() { this.context.scale(this.zoom, this.zoom); this.context.translate(this.canvas.width/2/this.zoom, this.canvas.height/2/this.zoom); + // TODO: Only render grid lines and cells that are visible. + // Render the grid. this.context.lineWidth="1"; - this.context.strokeStyle="#DDDDDD"; + this.context.strokeStyle="#EEEEEE"; this.context.beginPath(); for (var i = 0; i < this.state.cells.length; i++) { this.context.moveTo( @@ -66,8 +73,8 @@ ascii.View.prototype.render = function() { for (var j = 0; j < this.state.cells[i].length; j++) { if (this.state.cells[i][j].value != null) { this.context.fillText(this.state.cells[i][j].value, - i*ascii.CHARACTER_PIXELS - this.offset.x, - j*ascii.CHARACTER_PIXELS - this.offset.y); + i*ascii.CHARACTER_PIXELS - this.offset.x + 3, + j*ascii.CHARACTER_PIXELS - this.offset.y - 2); } } } @@ -95,4 +102,23 @@ ascii.View.prototype.frameToScreen = function(vector) { (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) { + return new ascii.Vector(Math.round((vector.x-7.5)/15), Math.round((vector.y+7.5)/15)); +}; + +/** + * 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)); +}; + +