/** * Handles view operations, state and management of the screen. * * @constructor * @param {ascii.State} state */ ascii.View = function(state) { /** @type {ascii.State} */ this.state = 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( MAX_GRID_WIDTH * CHAR_PIXELS_H / 2, MAX_GRID_HEIGHT * 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; 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(); } 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); } 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++) { 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); } } } } 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)); };