(function () { var ns = $.namespace('pskl.rendering.frame'); /** * FrameRenderer will display a given frame inside a canvas element. * @param {HtmlElement} container HtmlElement to use as parentNode of the Frame * @param {Object} renderingOptions * @param {Array} classList array of strings to use for css classList */ ns.FrameRenderer = function (container, renderingOptions, classList) { this.defaultRenderingOptions = { 'supportGridRendering' : false, 'zoom' : 1 }; renderingOptions = $.extend(true, {}, this.defaultRenderingOptions, renderingOptions); if (container === undefined) { throw 'Bad FrameRenderer initialization. undefined.'; } if (isNaN(renderingOptions.zoom)) { throw 'Bad FrameRenderer initialization. not well defined.'; } this.container = container; this.zoom = renderingOptions.zoom; this.offset = { x : 0, y : 0 }; this.margin = { x : 0, y : 0 }; this.supportGridRendering = renderingOptions.supportGridRendering; this.classList = classList || []; this.classList.push('canvas'); /** * Off dom canvas, will be used to draw the frame at 1:1 ratio * @type {HTMLElement} */ this.canvas = null; /** * Displayed canvas, scaled-up from the offdom canvas * @type {HTMLElement} */ this.displayCanvas = null; this.setDisplaySize(renderingOptions.width, renderingOptions.height); this.setGridWidth(pskl.UserSettings.get(pskl.UserSettings.GRID_WIDTH)); $.subscribe(Events.USER_SETTINGS_CHANGED, this.onUserSettingsChange_.bind(this)); }; pskl.utils.inherit(pskl.rendering.frame.FrameRenderer, pskl.rendering.AbstractRenderer); ns.FrameRenderer.prototype.render = function (frame) { if (frame) { this.clear(); this.renderFrame_(frame); } }; ns.FrameRenderer.prototype.clear = function () { pskl.utils.CanvasUtils.clear(this.canvas); pskl.utils.CanvasUtils.clear(this.displayCanvas); }; ns.FrameRenderer.prototype.setZoom = function (zoom) { // Minimum zoom is one to ensure one sprite pixel occupies at least one pixel on screen. var minimumZoom = 1; // Maximum zoom is relative to the display dimensions to ensure at least 10 pixels can // be drawn on screen. var maximumZoom = Math.min(this.displayWidth, this.displayHeight) / 10; zoom = pskl.utils.Math.minmax(zoom, minimumZoom, maximumZoom); if (zoom == this.zoom) { return; } // back up center coordinates var centerX = this.offset.x + (this.displayWidth / (2 * this.zoom)); var centerY = this.offset.y + (this.displayHeight / (2 * this.zoom)); this.zoom = zoom; // recenter this.setOffset( centerX - (this.displayWidth / (2 * this.zoom)), centerY - (this.displayHeight / (2 * this.zoom)) ); }; ns.FrameRenderer.prototype.getZoom = function () { return this.zoom; }; ns.FrameRenderer.prototype.setDisplaySize = function (width, height) { this.displayWidth = width; this.displayHeight = height; if (this.displayCanvas) { $(this.displayCanvas).remove(); this.displayCanvas = null; } this.createDisplayCanvas_(); }; ns.FrameRenderer.prototype.getDisplaySize = function () { return { height : this.displayHeight, width : this.displayWidth }; }; ns.FrameRenderer.prototype.getOffset = function () { return { x : this.offset.x, y : this.offset.y }; }; ns.FrameRenderer.prototype.setOffset = function (x, y) { var width = pskl.app.piskelController.getWidth(); var height = pskl.app.piskelController.getHeight(); var maxX = width - (this.displayWidth / this.zoom); x = pskl.utils.Math.minmax(x, 0, maxX); var maxY = height - (this.displayHeight / this.zoom); y = pskl.utils.Math.minmax(y, 0, maxY); this.offset.x = x; this.offset.y = y; }; ns.FrameRenderer.prototype.setGridWidth = function (value) { this.gridWidth_ = value; }; ns.FrameRenderer.prototype.getGridWidth = function () { if (!this.supportGridRendering) { return 0; } return this.gridWidth_; }; /** * Compute a grid width value best suited to the current display context, * particularly for the current zoom level */ ns.FrameRenderer.prototype.computeGridWidthForDisplay_ = function () { var gridWidth = this.getGridWidth(); while (this.zoom < 6 * gridWidth) { gridWidth--; } return gridWidth; }; ns.FrameRenderer.prototype.updateMargins_ = function (frame) { var deltaX = this.displayWidth - (this.zoom * frame.getWidth()); this.margin.x = Math.max(0, deltaX) / 2; var deltaY = this.displayHeight - (this.zoom * frame.getHeight()); this.margin.y = Math.max(0, deltaY) / 2; }; ns.FrameRenderer.prototype.createDisplayCanvas_ = function () { var height = this.displayHeight; var width = this.displayWidth; this.displayCanvas = pskl.utils.CanvasUtils.createCanvas(width, height, this.classList); pskl.utils.CanvasUtils.disableImageSmoothing(this.displayCanvas); this.container.append(this.displayCanvas); }; ns.FrameRenderer.prototype.onUserSettingsChange_ = function (evt, settingName, settingValue) { if (settingName == pskl.UserSettings.GRID_WIDTH) { this.setGridWidth(settingValue); } }; /** * Transform a screen pixel-based coordinate (relative to the top-left corner of the rendered * frame) into a sprite coordinate in column and row. * @public */ ns.FrameRenderer.prototype.getCoordinates = function(x, y) { var containerOffset = this.container.offset(); x = x - containerOffset.left; y = y - containerOffset.top; // apply margins x = x - this.margin.x; y = y - this.margin.y; var cellSize = this.zoom; // apply frame offset x = x + this.offset.x * cellSize; y = y + this.offset.y * cellSize; return { x : Math.floor(x / cellSize), y : Math.floor(y / cellSize) }; }; ns.FrameRenderer.prototype.reverseCoordinates = function(x, y) { var cellSize = this.zoom; x = x * cellSize; y = y * cellSize; x = x - this.offset.x * cellSize; y = y - this.offset.y * cellSize; x = x + this.margin.x; y = y + this.margin.y; var containerOffset = this.container.offset(); x = x + containerOffset.left; y = y + containerOffset.top; return { x : x + (cellSize / 2), y : y + (cellSize / 2) }; }; /** * @private */ ns.FrameRenderer.prototype.renderFrame_ = function (frame) { if (!this.canvas || frame.getWidth() != this.canvas.width || frame.getHeight() != this.canvas.height) { this.canvas = pskl.utils.CanvasUtils.createCanvas(frame.getWidth(), frame.getHeight()); } var w = this.canvas.width; var h = this.canvas.height; var z = this.zoom; // Draw in canvas pskl.utils.FrameUtils.drawToCanvas(frame, this.canvas); this.updateMargins_(frame); var displayContext = this.displayCanvas.getContext('2d'); displayContext.save(); var translateX = this.margin.x - this.offset.x * z; var translateY = this.margin.y - this.offset.y * z; var isZoomedOut = translateX > 0 || translateY > 0; // Draw the background / zoomed-out color only if needed. Otherwise the clearRect // happening after that will clear "out of bounds" and seems to be doing nothing // on some chromebooks (cf https://github.com/piskelapp/piskel/issues/651) if (isZoomedOut) { // Draw background displayContext.fillStyle = Constants.ZOOMED_OUT_BACKGROUND_COLOR; // The -1 on the width and height here is a workaround for a Chrome-only bug // that was potentially fixed, but is very hardware dependant. Seems to be // triggered when doing clear rect or fill rect using the full width & height // of a canvas. (https://bugs.chromium.org/p/chromium/issues/detail?id=469906) displayContext.fillRect(0, 0, this.displayCanvas.width - 1, this.displayCanvas.height - 1); } displayContext.translate(translateX, translateY); // Scale up to draw the canvas content displayContext.scale(z, z); if (pskl.UserSettings.get('SEAMLESS_MODE')) { displayContext.clearRect(-1 * w, -1 * h, 3 * w, 3 * h); } else { displayContext.clearRect(0, 0, w, h); } if (pskl.UserSettings.get('SEAMLESS_MODE')) { this.drawTiledFrames_(displayContext, this.canvas, w, h, 1); } displayContext.drawImage(this.canvas, 0, 0); // Draw grid. var gridWidth = this.computeGridWidthForDisplay_(); if (gridWidth > 0) { // Scale out before drawing the grid. displayContext.scale(1 / z, 1 / z); // Clear vertical lines. for (var i = 1 ; i < frame.getWidth() ; i++) { displayContext.clearRect((i * z) - (gridWidth / 2), 0, gridWidth, h * z); } // Clear horizontal lines. for (var j = 1 ; j < frame.getHeight() ; j++) { displayContext.clearRect(0, (j * z) - (gridWidth / 2), w * z, gridWidth); } } displayContext.restore(); }; /** * Draw repeatedly the provided image around the main drawing area. Used for seamless * drawing mode, to easily create seamless textures. A colored overlay is applied to * differentiate those additional frames from the main frame. */ ns.FrameRenderer.prototype.drawTiledFrames_ = function (context, image, w, h, z) { var opacity = pskl.UserSettings.get('SEAMLESS_OPACITY'); opacity = pskl.utils.Math.minmax(opacity, 0, 1); context.fillStyle = 'rgba(255, 255, 255, ' + opacity + ')'; [[0, -1], [0, 1], [-1, -1], [-1, 0], [-1, 1], [1, -1], [1, 0], [1, 1]].forEach(function (d) { context.drawImage(image, d[0] * w * z, d[1] * h * z); context.fillRect(d[0] * w * z, d[1] * h * z, w * z, h * z); }); }; })();