From 7490651f83580a7eda69f476624a32b2b2e33a94 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Tue, 29 Oct 2013 22:16:39 +0100 Subject: [PATCH 01/16] Zoom initial implementation. No UI, only bound to mousewheel. Everything is broken, to amend ! --- js/Constants.js | 4 +- js/controller/AnimatedPreviewController.js | 6 +- js/controller/DrawingController.js | 110 ++++++++++----- js/controller/PreviewFilmController.js | 18 ++- js/rendering/FrameRenderer.js | 157 +++++++++++++++------ js/utils/CanvasUtils.js | 17 ++- 6 files changed, 219 insertions(+), 93 deletions(-) diff --git a/js/Constants.js b/js/Constants.js index fd0bcdc2..abf2726c 100644 --- a/js/Constants.js +++ b/js/Constants.js @@ -8,8 +8,8 @@ var Constants = { MODEL_VERSION : 1, - MAX_HEIGHT : 128, - MAX_WIDTH : 128, + MAX_HEIGHT : 1024, + MAX_WIDTH : 1024, PREVIEW_FILM_SIZE : 120, diff --git a/js/controller/AnimatedPreviewController.js b/js/controller/AnimatedPreviewController.js index 2732d9c0..e03154c1 100644 --- a/js/controller/AnimatedPreviewController.js +++ b/js/controller/AnimatedPreviewController.js @@ -10,7 +10,9 @@ this.setFPS(Constants.DEFAULT.FPS); var renderingOptions = { - "dpi": this.calculateDPI_() + "zoom": this.calculateZoom_(), + "height" : "auto", + "width" : "auto" }; this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions); @@ -54,7 +56,7 @@ /** * Calculate the preview DPI depending on the framesheet size */ - ns.AnimatedPreviewController.prototype.calculateDPI_ = function () { + ns.AnimatedPreviewController.prototype.calculateZoom_ = function () { var previewSize = 200, framePixelHeight = this.piskelController.getCurrentFrame().getHeight(), framePixelWidth = this.piskelController.getCurrentFrame().getWidth(); diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index 598b6122..637cd670 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -16,12 +16,18 @@ */ this.container = container; - this.dpi = this.calculateDPI_(); + this.zoom = this.calculateZoom_(); + this.xOffset = 0; + this.yOffset = 0; // TODO(vincz): Store user prefs in a localstorage string ? var renderingOptions = { - "dpi": this.dpi, - "supportGridRendering" : true + "zoom": this.zoom, + "supportGridRendering" : true, + "height" : this.getContainerHeight_(), + "width" : this.getContainerWidth_(), + "xOffset" : this.xOffset, + "yOffset" : this.yOffset }; this.overlayRenderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, ["canvas-overlay"]); @@ -66,28 +72,28 @@ $(window).resize($.proxy(this.startDPIUpdateTimer_, this)); $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); - $.subscribe(Events.FRAME_SIZE_CHANGED, $.proxy(this.updateDPI_, this)); + $.subscribe(Events.FRAME_SIZE_CHANGED, $.proxy(this.updateZoom_, this)); - this.updateDPI_(); + this.updateZoom_(); }; ns.DrawingController.prototype.initMouseBehavior = function() { var body = $('body'); this.container.mousedown($.proxy(this.onMousedown_, this)); this.container.mousemove($.proxy(this.onMousemove_, this)); + this.container.on('mousewheel', $.proxy(this.onMousewheel_, this)); + body.mouseup($.proxy(this.onMouseup_, this)); // Deactivate right click: body.contextmenu(this.onCanvasContextMenu_); }; - - ns.DrawingController.prototype.startDPIUpdateTimer_ = function () { - if (this.dpiUpdateTimer) { - window.clearInterval(this.dpiUpdateTimer); + if (this.zoomUpdateTimer) { + window.clearInterval(this.zoomUpdateTimer); } - this.dpiUpdateTimer = window.setTimeout($.proxy(this.updateDPI_, this), 200); + this.zoomUpdateTimer = window.setTimeout($.proxy(this.updateZoom_, this), 200); }, /** @@ -95,7 +101,7 @@ */ ns.DrawingController.prototype.onUserSettingsChange_ = function (evt, settingsName, settingsValue) { if(settingsName == pskl.UserSettings.SHOW_GRID) { - this.updateDPI_(); + this.updateZoom_(); } }, @@ -159,6 +165,16 @@ } }; + ns.DrawingController.prototype.onMousewheel_ = function (jQueryEvent) { + var event = jQueryEvent.originalEvent; + var delta = event.wheelDeltaY; + if (delta > 0) { + this.setZoom(this.zoom + 1); + } else if (delta < 0) { + this.setZoom(this.zoom - 1); + } + }; + /** * @private */ @@ -250,7 +266,7 @@ ns.DrawingController.prototype.renderFrame = function () { var frame = this.piskelController.getCurrentFrame(); - var serializedFrame = this.dpi + "-" + frame.serialize(); + var serializedFrame = [this.zoom, this.xOffset, this.yOffset, frame.serialize()].join('-'); if (this.serializedFrame != serializedFrame) { if (!frame.isSameSize(this.overlayFrame)) { this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(frame); @@ -261,7 +277,7 @@ }; ns.DrawingController.prototype.renderOverlay = function () { - var serializedOverlay = this.dpi + "-" + this.overlayFrame.serialize(); + var serializedOverlay = [this.zoom, this.xOffset, this.yOffset, this.overlayFrame.serialize()].join('-'); if (this.serializedOverlay != serializedOverlay) { this.serializedOverlay = serializedOverlay; this.overlayRenderer.render(this.overlayFrame); @@ -274,7 +290,7 @@ var currentLayerIndex = this.piskelController.currentLayerIndex; var serializedLayerFrame = [ - this.dpi, + this.zoom, currentFrameIndex, currentLayerIndex, layers.length @@ -308,38 +324,39 @@ /** * @private */ - ns.DrawingController.prototype.calculateDPI_ = function() { - var availableViewportHeight = $('#main-wrapper').height(), - leftSectionWidth = $('.left-column').outerWidth(true), - rightSectionWidth = $('.right-column').outerWidth(true), - availableViewportWidth = $('#main-wrapper').width() - leftSectionWidth - rightSectionWidth, - framePixelHeight = this.piskelController.getCurrentFrame().getHeight(), - framePixelWidth = this.piskelController.getCurrentFrame().getWidth(); + ns.DrawingController.prototype.calculateZoom_ = function() { + var frameHeight = this.piskelController.getCurrentFrame().getHeight(), + frameWidth = this.piskelController.getCurrentFrame().getWidth(); - if (pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)) { - availableViewportWidth = availableViewportWidth - (framePixelWidth * Constants.GRID_STROKE_WIDTH); - availableViewportHeight = availableViewportHeight - (framePixelHeight * Constants.GRID_STROKE_WIDTH); - } - - var dpi = pskl.PixelUtils.calculateDPI( - availableViewportHeight, availableViewportWidth, framePixelHeight, framePixelWidth); - - return dpi; + return Math.min(this.getAvailableWidth_()/frameWidth, this.getAvailableHeight_()/frameHeight); }; + ns.DrawingController.prototype.getAvailableHeight_ = function () { + return $('#main-wrapper').height(); + }; + + ns.DrawingController.prototype.getAvailableWidth_ = function () { + var leftSectionWidth = $('.left-column').outerWidth(true), + rightSectionWidth = $('.right-column').outerWidth(true), + availableWidth = $('#main-wrapper').width() - leftSectionWidth - rightSectionWidth; + return availableWidth; + }; + + ns.DrawingController.prototype.getContainerHeight_ = function () { + return this.calculateZoom_() * this.piskelController.getCurrentFrame().getHeight(); + }; + + ns.DrawingController.prototype.getContainerWidth_ = function () { + return this.calculateZoom_() * this.piskelController.getCurrentFrame().getWidth(); + }; /** * @private */ - ns.DrawingController.prototype.updateDPI_ = function() { - this.dpi = this.calculateDPI_(); - - this.overlayRenderer.setDPI(this.dpi); - this.renderer.setDPI(this.dpi); - this.layersAboveRenderer.setDPI(this.dpi); - this.layersBelowRenderer.setDPI(this.dpi); + ns.DrawingController.prototype.updateZoom_ = function() { + this.setZoom(this.calculateZoom_()); var currentFrameHeight = this.piskelController.getCurrentFrame().getHeight(); - var canvasHeight = currentFrameHeight * this.dpi; + var canvasHeight = currentFrameHeight * this.zoom; if (pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)) { canvasHeight += Constants.GRID_STROKE_WIDTH * currentFrameHeight; } @@ -350,4 +367,21 @@ 'height': canvasHeight + 'px' }); }; + + ns.DrawingController.prototype.setZoom = function (zoom) { + this.zoom = zoom; + this.overlayRenderer.setZoom(this.zoom); + this.renderer.setZoom(this.zoom); + this.layersAboveRenderer.setZoom(this.zoom); + this.layersBelowRenderer.setZoom(this.zoom); + }; + + ns.DrawingController.prototype.moveOffset = function (xOffset, yOffset) { + this.xOffset = xOffset; + this.yOffset = yOffset; + this.overlayRenderer.setDisplayOffset(xOffset, yOffset); + this.renderer.setDisplayOffset(xOffset, yOffset); + this.layersAboveRenderer.setDisplayOffset(xOffset, yOffset); + this.layersBelowRenderer.setDisplayOffset(xOffset, yOffset); + }; })(); \ No newline at end of file diff --git a/js/controller/PreviewFilmController.js b/js/controller/PreviewFilmController.js index 7d1d1310..c231adbb 100644 --- a/js/controller/PreviewFilmController.js +++ b/js/controller/PreviewFilmController.js @@ -1,10 +1,10 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.PreviewFilmController = function (piskelController, container, dpi) { + ns.PreviewFilmController = function (piskelController, container) { this.piskelController = piskelController; this.container = container; - this.dpi = this.calculateDPI_(); + this.refreshZoom_(); this.redrawFlag = true; }; @@ -12,7 +12,7 @@ ns.PreviewFilmController.prototype.init = function() { $.subscribe(Events.TOOL_RELEASED, this.flagForRedraw_.bind(this)); $.subscribe(Events.PISKEL_RESET, this.flagForRedraw_.bind(this)); - $.subscribe(Events.PISKEL_RESET, this.refreshDPI_.bind(this)); + $.subscribe(Events.PISKEL_RESET, this.refreshZoom_.bind(this)); $('#preview-list-scroller').scroll(this.updateScrollerOverflows.bind(this)); this.updateScrollerOverflows(); @@ -28,8 +28,8 @@ this.redrawFlag = true; }; - ns.PreviewFilmController.prototype.refreshDPI_ = function () { - this.dpi = this.calculateDPI_(); + ns.PreviewFilmController.prototype.refreshZoom_ = function () { + this.zoom = this.calculateZoom_(); }; ns.PreviewFilmController.prototype.render = function () { @@ -153,7 +153,11 @@ // TODO(vincz): Eventually optimize this part by not recreating a FrameRenderer. Note that the real optim // is to make this update function (#createPreviewTile) less aggressive. - var renderingOptions = {"dpi": this.dpi }; + var renderingOptions = { + "zoom" : this.zoom, + "height" : "auto", + "width" : "auto" + }; var currentFrameRenderer = new pskl.rendering.FrameRenderer($(canvasContainer), renderingOptions, ["tile-view"]); currentFrameRenderer.render(currentFrame); @@ -206,7 +210,7 @@ /** * Calculate the preview DPI depending on the piskel size */ - ns.PreviewFilmController.prototype.calculateDPI_ = function () { + ns.PreviewFilmController.prototype.calculateZoom_ = function () { var curFrame = this.piskelController.getCurrentFrame(), frameHeight = curFrame.getHeight(), frameWidth = curFrame.getWidth(), diff --git a/js/rendering/FrameRenderer.js b/js/rendering/FrameRenderer.js index 2747d6fa..b891f040 100644 --- a/js/rendering/FrameRenderer.js +++ b/js/rendering/FrameRenderer.js @@ -9,46 +9,107 @@ */ ns.FrameRenderer = function (container, renderingOptions, classes) { this.defaultRenderingOptions = { - 'supportGridRendering' : false + 'supportGridRendering' : false, + 'zoom' : 1, + 'xOffset' : 0, + 'yOffset' : 0 }; + renderingOptions = $.extend(true, {}, this.defaultRenderingOptions, renderingOptions); if(container === undefined) { throw 'Bad FrameRenderer initialization. undefined.'; } - if(isNaN(renderingOptions.dpi)) { - throw 'Bad FrameRenderer initialization. not well defined.'; + if(isNaN(renderingOptions.zoom)) { + throw 'Bad FrameRenderer initialization. not well defined.'; } this.container = container; - this.dpi = renderingOptions.dpi; + this.zoom = renderingOptions.zoom; + this.xOffset = renderingOptions.xOffset; + this.yOffset = renderingOptions.yOffset; + + this.pixelOffsetHeight = 0; + this.pixelOffsetWidth = 0; + this.supportGridRendering = renderingOptions.supportGridRendering; this.classes = classes || []; this.classes.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.scaledCanvas = null; + this.setDisplaySize(renderingOptions.width, renderingOptions.height); + this.enableGrid(pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)); - // Flag to know if the config was altered - this.canvasConfigDirty = true; this.updateBackgroundClass_(pskl.UserSettings.get(pskl.UserSettings.CANVAS_BACKGROUND)); $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); }; - ns.FrameRenderer.prototype.setDPI = function (dpi) { - this.dpi = dpi; - this.canvasConfigDirty = true; + ns.FrameRenderer.prototype.setZoom = function (zoom) { + this.zoom = zoom; + }; + + ns.FrameRenderer.prototype.isAutoSized_ = function () { + return this.displayHeight === 'auto' && this.displayWidth === 'auto'; + }; + + ns.FrameRenderer.prototype.setDisplaySize = function (width, height) { + this.displayHeight = height; + this.displayWidth = width; + if (this.scaledCanvas) { + $(this.scaledCanvas).remove(); + this.scaledCanvas = null; + } + }; + + ns.FrameRenderer.prototype.updatePixelOffsets_ = function () { + if (!this.isAutoSized_()) { + var deltaH = this.displayHeight - (this.zoom * this.canvas.height); + this.pixelOffsetHeight = Math.max(0, deltaH) / 2; + + var deltaW = this.displayWidth - (this.zoom * this.canvas.width); + this.pixelOffsetWidth = Math.max(0, deltaW) / 2; + } + }; + + ns.FrameRenderer.prototype.createScaledCanvas_ = function () { + var height = this.displayHeight; + var width = this.displayWidth; + + if (this.isAutoSized_()) { + height = this.zoom * this.canvas.height; + width = this.zoom * this.canvas.width; + } + + this.scaledCanvas = pskl.CanvasUtils.createCanvas(width, height, this.classes); + if (true || this.zoom > 2) { + pskl.CanvasUtils.disableImageSmoothing(this.scaledCanvas); + } + this.container.append(this.scaledCanvas); + }; + + ns.FrameRenderer.prototype.setDisplayOffset = function (xOffset, yOffset) { + this.xOffset = xOffset; + this.yOffset = yOffset; }; /** * @private */ ns.FrameRenderer.prototype.onUserSettingsChange_ = function (evt, settingName, settingValue) { - if(settingName == pskl.UserSettings.SHOW_GRID) { this.enableGrid(settingValue); } @@ -77,27 +138,20 @@ ns.FrameRenderer.prototype.render = function (frame) { if (frame) { this.clear(); - var context = this.getCanvas_(frame).getContext('2d'); - for(var col = 0, width = frame.getWidth(); col < width; col++) { - for(var row = 0, height = frame.getHeight(); row < height; row++) { - var color = frame.getPixel(col, row); - this.renderPixel_(color, col, row, context); - } - } + this.drawFrameInCanvas_(frame); this.lastRenderedFrame = frame; } }; + ns.FrameRenderer.prototype.clear = function () { + pskl.CanvasUtils.clear(this.canvas); + pskl.CanvasUtils.clear(this.scaledCanvas); + }; + ns.FrameRenderer.prototype.renderPixel_ = function (color, col, row, context) { if(color != Constants.TRANSPARENT_COLOR) { context.fillStyle = color; - context.fillRect(this.getFramePos_(col), this.getFramePos_(row), this.dpi, this.dpi); - } - }; - - ns.FrameRenderer.prototype.clear = function () { - if (this.canvas) { - this.canvas.getContext("2d").clearRect(0, 0, this.canvas.width, this.canvas.height); + context.fillRect(col, row, 1, 1); } }; @@ -107,10 +161,12 @@ * @public */ ns.FrameRenderer.prototype.convertPixelCoordinatesIntoSpriteCoordinate = function(coords) { - var cellSize = this.dpi + this.gridStrokeWidth; + var cellSize = this.zoom + this.gridStrokeWidth; + var xCoord = (coords.x - this.pixelOffsetWidth) + (this.xOffset * cellSize), + yCoord = (coords.y - this.pixelOffsetHeight) + (this.yOffset * cellSize); return { - "col" : (coords.x - coords.x % cellSize) / cellSize, - "row" : (coords.y - coords.y % cellSize) / cellSize + "col" : (xCoord - xCoord % cellSize) / cellSize, + "row" : (yCoord - yCoord % cellSize) / cellSize }; }; @@ -124,22 +180,37 @@ /** * @private */ - ns.FrameRenderer.prototype.getCanvas_ = function (frame) { - if(this.canvasConfigDirty) { - $(this.canvas).remove(); - - var col = frame.getWidth(), - row = frame.getHeight(); - - var pixelWidth = col * this.dpi + this.gridStrokeWidth * (col - 1); - var pixelHeight = row * this.dpi + this.gridStrokeWidth * (row - 1); - - var canvas = pskl.CanvasUtils.createCanvas(pixelWidth, pixelHeight, this.classes); - this.container.append(canvas); - - this.canvas = canvas; - this.canvasConfigDirty = false; + ns.FrameRenderer.prototype.drawFrameInCanvas_ = function (frame) { + if (!this.canvas) { + this.canvas = pskl.CanvasUtils.createCanvas(frame.getWidth(), frame.getHeight()); } - return this.canvas; + + if (!this.scaledCanvas) { + this.createScaledCanvas_(); + } + + this.updatePixelOffsets_(); + + var context = this.canvas.getContext('2d'); + for(var col = 0, width = frame.getWidth(); col < width; col++) { + for(var row = 0, height = frame.getHeight(); row < height; row++) { + var color = frame.getPixel(col, row); + this.renderPixel_(color, col, row, context); + } + } + + context = this.scaledCanvas.getContext('2d'); + context.save(); + // zoom < 1 + context.fillStyle = "#aaa"; + // zoom < 1 + context.fillRect(0,0,this.scaledCanvas.width, this.scaledCanvas.height); + context.translate(this.pixelOffsetWidth, this.pixelOffsetHeight); + context.scale(this.zoom, this.zoom); + context.translate(-this.xOffset, -this.yOffset); + // zoom < 1 + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + context.drawImage(this.canvas, 0, 0); + context.restore(); }; })(); \ No newline at end of file diff --git a/js/utils/CanvasUtils.js b/js/utils/CanvasUtils.js index dd03612b..f65c4081 100644 --- a/js/utils/CanvasUtils.js +++ b/js/utils/CanvasUtils.js @@ -15,8 +15,23 @@ canvas.classList.add(classList[i]); } } - + return canvas; + }, + + disableImageSmoothing : function (canvas) { + var context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.mozImageSmoothingEnabled = false; + context.oImageSmoothingEnabled = false; + context.webkitImageSmoothingEnabled = false; + context.msImageSmoothingEnabled = false; + }, + + clear : function (canvas) { + if (canvas) { + canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); + } } }; })(); \ No newline at end of file From 3ce9aaa84385e519426efe62dedbaccfd7f3fd9c Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 1 Nov 2013 11:17:50 +0100 Subject: [PATCH 02/16] Added utilities for alpha-composition in FrameUtils, for future usage ... maybe --- js/rendering/FrameRenderer.js | 80 ++++++++++++++++++----------------- js/utils/FrameUtils.js | 77 +++++++++++++++++++++++++++++++-- 2 files changed, 115 insertions(+), 42 deletions(-) diff --git a/js/rendering/FrameRenderer.js b/js/rendering/FrameRenderer.js index b891f040..7e49416f 100644 --- a/js/rendering/FrameRenderer.js +++ b/js/rendering/FrameRenderer.js @@ -10,9 +10,7 @@ ns.FrameRenderer = function (container, renderingOptions, classes) { this.defaultRenderingOptions = { 'supportGridRendering' : false, - 'zoom' : 1, - 'xOffset' : 0, - 'yOffset' : 0 + 'zoom' : 1 }; renderingOptions = $.extend(true, {}, this.defaultRenderingOptions, renderingOptions); @@ -28,11 +26,12 @@ this.container = container; this.zoom = renderingOptions.zoom; - this.xOffset = renderingOptions.xOffset; - this.yOffset = renderingOptions.yOffset; - this.pixelOffsetHeight = 0; - this.pixelOffsetWidth = 0; + this.frameOffsetX = 0; + this.frameOffsetY = 0; + + this.marginY = 0; + this.marginX = 0; this.supportGridRendering = renderingOptions.supportGridRendering; @@ -49,7 +48,7 @@ * Displayed canvas, scaled-up from the offdom canvas * @type {HTMLElement} */ - this.scaledCanvas = null; + this.displayCanvas = null; this.setDisplaySize(renderingOptions.width, renderingOptions.height); this.enableGrid(pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)); @@ -69,23 +68,23 @@ ns.FrameRenderer.prototype.setDisplaySize = function (width, height) { this.displayHeight = height; this.displayWidth = width; - if (this.scaledCanvas) { - $(this.scaledCanvas).remove(); - this.scaledCanvas = null; + if (this.displayCanvas) { + $(this.displayCanvas).remove(); + this.displayCanvas = null; } }; - ns.FrameRenderer.prototype.updatePixelOffsets_ = function () { + ns.FrameRenderer.prototype.updateMargins_ = function () { if (!this.isAutoSized_()) { - var deltaH = this.displayHeight - (this.zoom * this.canvas.height); - this.pixelOffsetHeight = Math.max(0, deltaH) / 2; + var deltaX = this.displayWidth - (this.zoom * this.canvas.width); + this.marginX = Math.max(0, deltaX) / 2; - var deltaW = this.displayWidth - (this.zoom * this.canvas.width); - this.pixelOffsetWidth = Math.max(0, deltaW) / 2; + var deltaY = this.displayHeight - (this.zoom * this.canvas.height); + this.marginY = Math.max(0, deltaY) / 2; } }; - ns.FrameRenderer.prototype.createScaledCanvas_ = function () { + ns.FrameRenderer.prototype.createDisplayCanvas_ = function () { var height = this.displayHeight; var width = this.displayWidth; @@ -94,16 +93,20 @@ width = this.zoom * this.canvas.width; } - this.scaledCanvas = pskl.CanvasUtils.createCanvas(width, height, this.classes); + this.displayCanvas = pskl.CanvasUtils.createCanvas(width, height, this.classes); if (true || this.zoom > 2) { - pskl.CanvasUtils.disableImageSmoothing(this.scaledCanvas); + pskl.CanvasUtils.disableImageSmoothing(this.displayCanvas); } - this.container.append(this.scaledCanvas); + this.container.append(this.displayCanvas); }; - ns.FrameRenderer.prototype.setDisplayOffset = function (xOffset, yOffset) { - this.xOffset = xOffset; - this.yOffset = yOffset; + ns.FrameRenderer.prototype.setDisplayOffset = function (frameOffsetX, frameOffsetY) { + this.frameOffsetX = frameOffsetX; + this.frameOffsetY = frameOffsetY; + }; + + ns.FrameRenderer.prototype.moveOffset = function (frameOffsetX, frameOffsetY) { + this.setDisplayOffset(this.frameOffsetX + frameOffsetX, this.frameOffsetY + frameOffsetY); }; /** @@ -138,14 +141,13 @@ ns.FrameRenderer.prototype.render = function (frame) { if (frame) { this.clear(); - this.drawFrameInCanvas_(frame); - this.lastRenderedFrame = frame; + this.renderFrame_(frame); } }; ns.FrameRenderer.prototype.clear = function () { pskl.CanvasUtils.clear(this.canvas); - pskl.CanvasUtils.clear(this.scaledCanvas); + pskl.CanvasUtils.clear(this.displayCanvas); }; ns.FrameRenderer.prototype.renderPixel_ = function (color, col, row, context) { @@ -162,8 +164,8 @@ */ ns.FrameRenderer.prototype.convertPixelCoordinatesIntoSpriteCoordinate = function(coords) { var cellSize = this.zoom + this.gridStrokeWidth; - var xCoord = (coords.x - this.pixelOffsetWidth) + (this.xOffset * cellSize), - yCoord = (coords.y - this.pixelOffsetHeight) + (this.yOffset * cellSize); + var xCoord = (coords.x - this.marginX) + (this.frameOffsetX * cellSize), + yCoord = (coords.y - this.marginY) + (this.frameOffsetY * cellSize); return { "col" : (xCoord - xCoord % cellSize) / cellSize, "row" : (yCoord - yCoord % cellSize) / cellSize @@ -180,17 +182,11 @@ /** * @private */ - ns.FrameRenderer.prototype.drawFrameInCanvas_ = function (frame) { + ns.FrameRenderer.prototype.renderFrame_ = function (frame) { if (!this.canvas) { this.canvas = pskl.CanvasUtils.createCanvas(frame.getWidth(), frame.getHeight()); } - if (!this.scaledCanvas) { - this.createScaledCanvas_(); - } - - this.updatePixelOffsets_(); - var context = this.canvas.getContext('2d'); for(var col = 0, width = frame.getWidth(); col < width; col++) { for(var row = 0, height = frame.getHeight(); row < height; row++) { @@ -199,15 +195,21 @@ } } - context = this.scaledCanvas.getContext('2d'); + if (!this.displayCanvas) { + this.createDisplayCanvas_(); + } + + this.updateMargins_(); + + context = this.displayCanvas.getContext('2d'); context.save(); // zoom < 1 context.fillStyle = "#aaa"; // zoom < 1 - context.fillRect(0,0,this.scaledCanvas.width, this.scaledCanvas.height); - context.translate(this.pixelOffsetWidth, this.pixelOffsetHeight); + context.fillRect(0,0,this.displayCanvas.width, this.displayCanvas.height); + context.translate(this.marginX, this.marginY); context.scale(this.zoom, this.zoom); - context.translate(-this.xOffset, -this.yOffset); + context.translate(-this.frameOffsetX, -this.frameOffsetY); // zoom < 1 context.clearRect(0, 0, this.canvas.width, this.canvas.height); context.drawImage(this.canvas, 0, 0); diff --git a/js/utils/FrameUtils.js b/js/utils/FrameUtils.js index cb615527..7f422006 100644 --- a/js/utils/FrameUtils.js +++ b/js/utils/FrameUtils.js @@ -1,11 +1,11 @@ (function () { var ns = $.namespace('pskl.utils'); - + var colorCache = {}; ns.FrameUtils = { merge : function (frames) { var merged = null; if (frames.length) { - merged = frames[0].clone(); + merged = frames[0]; var w = merged.getWidth(), h = merged.getHeight(); for (var i = 1 ; i < frames.length ; i++) { pskl.utils.FrameUtils.mergeFrames_(merged, frames[i]); @@ -20,6 +20,77 @@ frameA.setPixel(col, row, p); } }); + }, + + /** + * Alpha compositing using porter duff algorithm : + * http://en.wikipedia.org/wiki/Alpha_compositing + * http://keithp.com/~keithp/porterduff/p253-porter.pdf + * @param {String} strColor1 color over + * @param {String} strColor2 color under + * @return {String} the composite color + */ + mergePixels : function (strColor1, strColor2, globalOpacity1) { + var col1 = pskl.utils.FrameUtils.toRgba(strColor1); + var col2 = pskl.utils.FrameUtils.toRgba(strColor2); + if (typeof globalOpacity1 == 'number') { + col1 = JSON.parse(JSON.stringify(col1)); + col1.a = globalOpacity1 * col1.a; + } + var a = col1.a + col2.a * (1 - col1.a); + + var r = ((col1.r * col1.a + col2.r * col2.a * (1 - col1.a)) / a)|0; + var g = ((col1.g * col1.a + col2.g * col2.a * (1 - col1.a)) / a)|0; + var b = ((col1.b * col1.a + col2.b * col2.a * (1 - col1.a)) / a)|0; + + return 'rgba('+r+','+g+','+b+','+a+')'; + }, + + /** + * Convert a color defined as a string (hex, rgba, rgb, 'TRANSPARENT') to an Object with r,g,b,a properties. + * r, g and b are integers between 0 and 255, a is a float between 0 and 1 + * @param {String} c color as a string + * @return {Object} {r:Number,g:Number,b:Number,a:Number} + */ + toRgba : function (c) { + if (colorCache[c]) { + return colorCache[c]; + } + var color, matches; + if (c === 'TRANSPARENT') { + color = { + r : 0, + g : 0, + b : 0, + a : 0 + }; + } else if (c.indexOf('rgba(') != -1) { + matches = /rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(1|0\.\d+)\s*\)/.exec(c); + color = { + r : parseInt(matches[1],10), + g : parseInt(matches[2],10), + b : parseInt(matches[3],10), + a : parseFloat(matches[4]) + }; + } else if (c.indexOf('rgb(') != -1) { + matches = /rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(c); + color = { + r : parseInt(matches[1],10), + g : parseInt(matches[2],10), + b : parseInt(matches[3],10), + a : 1 + }; + } else { + matches = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(c); + color = { + r : parseInt(matches[1], 16), + g : parseInt(matches[2], 16), + b : parseInt(matches[3], 16), + a : 1 + }; + } + colorCache[c] = color; + return color; } }; -})(); \ No newline at end of file +})(); From 51f86afe6e9a8b3e6a7682ef5fea25e722ff121c Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 1 Nov 2013 15:39:42 +0100 Subject: [PATCH 03/16] feature : zoom - Created AbstractRenderer in rendering package - Created CachedRenderer and CachedFrameRenderer to extract basic frame caching logic from DrawingController - Created RendererManager to synchronize updates made to several Renderer settings - Moved FrameRenderer from pskl.rendering to pskl.rendering.frame - Fixed the resize of the drawing area when the window is resized --- js/app.js | 6 +- js/controller/AnimatedPreviewController.js | 25 +- js/controller/DrawingController.js | 132 +++++------ js/controller/PreviewFilmController.js | 6 +- .../settings/ApplicationSettingsController.js | 35 +-- js/rendering/AbstractRenderer.js | 22 ++ js/rendering/CachedRenderer.js | 64 +++++ js/rendering/RendererManager.js | 30 +++ js/rendering/frame/CachedFrameRenderer.js | 18 ++ js/rendering/{ => frame}/FrameRenderer.js | 219 +++++++++--------- piskel-script-list.js | 6 +- 11 files changed, 345 insertions(+), 218 deletions(-) create mode 100644 js/rendering/AbstractRenderer.js create mode 100644 js/rendering/CachedRenderer.js create mode 100644 js/rendering/RendererManager.js create mode 100644 js/rendering/frame/CachedFrameRenderer.js rename js/rendering/{ => frame}/FrameRenderer.js (67%) diff --git a/js/app.js b/js/app.js index def736df..93444c84 100644 --- a/js/app.js +++ b/js/app.js @@ -232,9 +232,9 @@ getFirstFrameAsPng : function () { var firstFrame = this.piskelController.getFrameAt(0); - var frameRenderer = new pskl.rendering.CanvasRenderer(firstFrame, 1); - frameRenderer.drawTransparentAs('rgba(0,0,0,0)'); - var firstFrameCanvas = frameRenderer.render().canvas; + var canvasRenderer = new pskl.rendering.CanvasRenderer(firstFrame, 1); + canvasRenderer.drawTransparentAs('rgba(0,0,0,0)'); + var firstFrameCanvas = canvasRenderer.render().canvas; return firstFrameCanvas.toDataURL("image/png"); }, diff --git a/js/controller/AnimatedPreviewController.js b/js/controller/AnimatedPreviewController.js index e03154c1..14749608 100644 --- a/js/controller/AnimatedPreviewController.js +++ b/js/controller/AnimatedPreviewController.js @@ -9,14 +9,15 @@ this.setFPS(Constants.DEFAULT.FPS); + var zoom = this.calculateZoom_(); var renderingOptions = { - "zoom": this.calculateZoom_(), - "height" : "auto", - "width" : "auto" + "zoom": zoom, + "height" : this.piskelController.getCurrentFrame().getHeight() * zoom, + "width" : this.piskelController.getCurrentFrame().getWidth() * zoom }; - this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions); + this.renderer = new pskl.rendering.frame.FrameRenderer(this.container, renderingOptions); - $.subscribe(Events.FRAME_SIZE_CHANGED, this.updateDPI_.bind(this)); + $.subscribe(Events.FRAME_SIZE_CHANGED, this.updateZoom_.bind(this)); }; ns.AnimatedPreviewController.prototype.init = function () { @@ -58,16 +59,14 @@ */ ns.AnimatedPreviewController.prototype.calculateZoom_ = function () { var previewSize = 200, - framePixelHeight = this.piskelController.getCurrentFrame().getHeight(), - framePixelWidth = this.piskelController.getCurrentFrame().getWidth(); - // TODO (julz) : should have a utility to get a Size from framesheet easily (what about empty framesheets though ?) + hZoom = previewSize / this.piskelController.getCurrentFrame().getHeight(), + wZoom = previewSize / this.piskelController.getCurrentFrame().getWidth(); - //return pskl.PixelUtils.calculateDPIForContainer($(".preview-container"), framePixelHeight, framePixelWidth); - return pskl.PixelUtils.calculateDPI(previewSize, previewSize, framePixelHeight, framePixelWidth); + return Math.min(hZoom, wZoom); }; - ns.AnimatedPreviewController.prototype.updateDPI_ = function () { - this.dpi = this.calculateDPI_(); - this.renderer.setDPI(this.dpi); + ns.AnimatedPreviewController.prototype.updateZoom_ = function () { + var zoom = this.calculateZoom_(); + this.renderer.setZoom(zoom); }; })(); \ No newline at end of file diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index 637cd670..e3520975 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -16,25 +16,28 @@ */ this.container = container; - this.zoom = this.calculateZoom_(); - this.xOffset = 0; - this.yOffset = 0; - // TODO(vincz): Store user prefs in a localstorage string ? var renderingOptions = { - "zoom": this.zoom, + "zoom": this.calculateZoom_(), "supportGridRendering" : true, "height" : this.getContainerHeight_(), "width" : this.getContainerWidth_(), - "xOffset" : this.xOffset, - "yOffset" : this.yOffset + "xOffset" : 0, + "yOffset" : 0 }; - this.overlayRenderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, ["canvas-overlay"]); - this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, ["drawing-canvas"]); - this.layersBelowRenderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, ["layers-canvas", "layers-below-canvas"]); - this.layersAboveRenderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, ["layers-canvas", "layers-above-canvas"]); + this.overlayRenderer = new pskl.rendering.frame.CachedFrameRenderer(this.container, renderingOptions, ["canvas-overlay"]); + this.renderer = new pskl.rendering.frame.CachedFrameRenderer(this.container, renderingOptions, ["drawing-canvas"]); + this.layersBelowRenderer = new pskl.rendering.frame.FrameRenderer(this.container, renderingOptions, ["layers-canvas", "layers-below-canvas"]); + this.layersAboveRenderer = new pskl.rendering.frame.FrameRenderer(this.container, renderingOptions, ["layers-canvas", "layers-above-canvas"]); + + this.rendererManager = new pskl.rendering.RendererManager(); + this.rendererManager + .add(this.overlayRenderer) + .add(this.renderer) + .add(this.layersBelowRenderer) + .add(this.layersAboveRenderer); // State of drawing controller: this.isClicked = false; @@ -69,12 +72,12 @@ $.publish(Events.SECONDARY_COLOR_UPDATED, [color]); }, this)); - $(window).resize($.proxy(this.startDPIUpdateTimer_, this)); + $(window).resize($.proxy(this.startResizeTimer_, this)); $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); - $.subscribe(Events.FRAME_SIZE_CHANGED, $.proxy(this.updateZoom_, this)); + $.subscribe(Events.FRAME_SIZE_CHANGED, $.proxy(this.onFrameSizeChanged_, this)); - this.updateZoom_(); + this.centerColumnWrapperHorizontally_(); }; ns.DrawingController.prototype.initMouseBehavior = function() { @@ -89,11 +92,15 @@ body.contextmenu(this.onCanvasContextMenu_); }; - ns.DrawingController.prototype.startDPIUpdateTimer_ = function () { - if (this.zoomUpdateTimer) { - window.clearInterval(this.zoomUpdateTimer); + ns.DrawingController.prototype.startResizeTimer_ = function () { + if (this.resizeTimer) { + window.clearInterval(this.resizeTimer); } - this.zoomUpdateTimer = window.setTimeout($.proxy(this.updateZoom_, this), 200); + this.resizeTimer = window.setTimeout($.proxy(this.afterWindowResize_, this), 200); + }, + + ns.DrawingController.prototype.afterWindowResize_ = function () { + this.rendererManager.setDisplaySize(this.getContainerWidth_(), this.getContainerHeight_()); }, /** @@ -101,7 +108,7 @@ */ ns.DrawingController.prototype.onUserSettingsChange_ = function (evt, settingsName, settingsValue) { if(settingsName == pskl.UserSettings.SHOW_GRID) { - this.updateZoom_(); + console.warn('DrawingController:onUserSettingsChange_ not implemented !'); } }, @@ -116,10 +123,11 @@ $.publish(Events.CANVAS_RIGHT_CLICKED); } - var coords = this.getSpriteCoordinates(event); + var coords = this.renderer.getCoordinates(event.clientX, event.clientY); this.currentToolBehavior.applyToolAt( - coords.col, coords.row, + coords.x, + coords.y, this.getCurrentColor_(), this.piskelController.getCurrentFrame(), this.overlayFrame, @@ -136,11 +144,13 @@ var currentTime = new Date().getTime(); // Throttling of the mousemove event: if ((currentTime - this.previousMousemoveTime) > 40 ) { - var coords = this.getSpriteCoordinates(event); + var coords = this.renderer.getCoordinates(event.clientX, event.clientY); + if (this.isClicked) { this.currentToolBehavior.moveToolAt( - coords.col, coords.row, + coords.x, + coords.y, this.getCurrentColor_(), this.piskelController.getCurrentFrame(), this.overlayFrame, @@ -154,7 +164,8 @@ } else { this.currentToolBehavior.moveUnactiveToolAt( - coords.col, coords.row, + coords.x, + coords.y, this.getCurrentColor_(), this.piskelController.getCurrentFrame(), this.overlayFrame, @@ -168,10 +179,11 @@ ns.DrawingController.prototype.onMousewheel_ = function (jQueryEvent) { var event = jQueryEvent.originalEvent; var delta = event.wheelDeltaY; + var currentZoom = this.renderer.getZoom(); if (delta > 0) { - this.setZoom(this.zoom + 1); + this.rendererManager.setZoom(currentZoom + 1); } else if (delta < 0) { - this.setZoom(this.zoom - 1); + this.rendererManager.setZoom(currentZoom - 1); } }; @@ -188,10 +200,11 @@ this.isClicked = false; this.isRightClicked = false; - var coords = this.getSpriteCoordinates(event); + var coords = this.renderer.getCoordinates(event.clientX, event.clientY); //console.log("mousemove: col: " + spriteCoordinate.col + " - row: " + spriteCoordinate.row); this.currentToolBehavior.releaseToolAt( - coords.col, coords.row, + coords.x, + coords.y, this.getCurrentColor_(), this.piskelController.getCurrentFrame(), this.overlayFrame, @@ -215,23 +228,12 @@ return evtInfo; }; - /** - * @private - */ - ns.DrawingController.prototype.getRelativeCoordinates = function (clientX, clientY) { - var canvasPageOffset = this.container.offset(); - return { - x : clientX - canvasPageOffset.left, - y : clientY - canvasPageOffset.top - }; - }; /** * @private */ ns.DrawingController.prototype.getSpriteCoordinates = function(event) { - var coords = this.getRelativeCoordinates(event.clientX, event.clientY); - return this.renderer.convertPixelCoordinatesIntoSpriteCoordinate(coords); + return this.renderer.getCoordinates(event.clientX, event.clientY); }; /** @@ -265,23 +267,15 @@ }; ns.DrawingController.prototype.renderFrame = function () { - var frame = this.piskelController.getCurrentFrame(); - var serializedFrame = [this.zoom, this.xOffset, this.yOffset, frame.serialize()].join('-'); - if (this.serializedFrame != serializedFrame) { - if (!frame.isSameSize(this.overlayFrame)) { - this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(frame); - } - this.serializedFrame = serializedFrame; - this.renderer.render(frame); - } + this.renderer.render(this.piskelController.getCurrentFrame()); }; ns.DrawingController.prototype.renderOverlay = function () { - var serializedOverlay = [this.zoom, this.xOffset, this.yOffset, this.overlayFrame.serialize()].join('-'); - if (this.serializedOverlay != serializedOverlay) { - this.serializedOverlay = serializedOverlay; - this.overlayRenderer.render(this.overlayFrame); + var currentFrame = this.piskelController.getCurrentFrame(); + if (!currentFrame.isSameSize(this.overlayFrame)) { + this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(currentFrame); } + this.overlayRenderer.render(this.overlayFrame); }; ns.DrawingController.prototype.renderLayers = function () { @@ -349,39 +343,19 @@ ns.DrawingController.prototype.getContainerWidth_ = function () { return this.calculateZoom_() * this.piskelController.getCurrentFrame().getWidth(); }; + /** * @private */ - ns.DrawingController.prototype.updateZoom_ = function() { - this.setZoom(this.calculateZoom_()); - - var currentFrameHeight = this.piskelController.getCurrentFrame().getHeight(); - var canvasHeight = currentFrameHeight * this.zoom; - if (pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)) { - canvasHeight += Constants.GRID_STROKE_WIDTH * currentFrameHeight; - } - - var verticalGapInPixel = Math.floor(($('#main-wrapper').height() - canvasHeight) / 2); + ns.DrawingController.prototype.centerColumnWrapperHorizontally_ = function() { + var containerHeight = this.getContainerHeight_(); + var verticalGapInPixel = Math.floor(($('#main-wrapper').height() - containerHeight) / 2); $('#column-wrapper').css({ - 'top': verticalGapInPixel + 'px', - 'height': canvasHeight + 'px' + 'top': verticalGapInPixel + 'px' }); }; - ns.DrawingController.prototype.setZoom = function (zoom) { - this.zoom = zoom; - this.overlayRenderer.setZoom(this.zoom); - this.renderer.setZoom(this.zoom); - this.layersAboveRenderer.setZoom(this.zoom); - this.layersBelowRenderer.setZoom(this.zoom); - }; - ns.DrawingController.prototype.moveOffset = function (xOffset, yOffset) { - this.xOffset = xOffset; - this.yOffset = yOffset; - this.overlayRenderer.setDisplayOffset(xOffset, yOffset); - this.renderer.setDisplayOffset(xOffset, yOffset); - this.layersAboveRenderer.setDisplayOffset(xOffset, yOffset); - this.layersBelowRenderer.setDisplayOffset(xOffset, yOffset); + this.rendererManager.moveOffset(xOffset, yOffset); }; })(); \ No newline at end of file diff --git a/js/controller/PreviewFilmController.js b/js/controller/PreviewFilmController.js index c231adbb..64aa1925 100644 --- a/js/controller/PreviewFilmController.js +++ b/js/controller/PreviewFilmController.js @@ -155,10 +155,10 @@ // is to make this update function (#createPreviewTile) less aggressive. var renderingOptions = { "zoom" : this.zoom, - "height" : "auto", - "width" : "auto" + "height" : this.piskelController.getCurrentFrame().getHeight() * this.zoom, + "width" : this.piskelController.getCurrentFrame().getWidth() * this.zoom }; - var currentFrameRenderer = new pskl.rendering.FrameRenderer($(canvasContainer), renderingOptions, ["tile-view"]); + var currentFrameRenderer = new pskl.rendering.frame.FrameRenderer($(canvasContainer), renderingOptions, ["tile-view"]); currentFrameRenderer.render(currentFrame); previewTileRoot.appendChild(canvasContainer); diff --git a/js/controller/settings/ApplicationSettingsController.js b/js/controller/settings/ApplicationSettingsController.js index 7329b596..27183642 100644 --- a/js/controller/settings/ApplicationSettingsController.js +++ b/js/controller/settings/ApplicationSettingsController.js @@ -1,6 +1,6 @@ (function () { var ns = $.namespace("pskl.controller.settings"); - + ns.ApplicationSettingsController = function () {}; /** @@ -18,21 +18,26 @@ $('#show-grid').prop('checked', show_grid); // Handle grid display changes: - $('#show-grid').change($.proxy(function(evt) { - var checked = $('#show-grid').prop('checked'); - pskl.UserSettings.set(pskl.UserSettings.SHOW_GRID, checked); - }, this)); + $('#show-grid').change(this.onShowGridClick.bind(this)); // Handle canvas background changes: - $('#background-picker-wrapper').click(function(evt) { - var target = $(evt.target).closest('.background-picker'); - if (target.length) { - var backgroundClass = target.data('background-class'); - pskl.UserSettings.set(pskl.UserSettings.CANVAS_BACKGROUND, backgroundClass); - - $('.background-picker').removeClass('selected'); - target.addClass('selected'); - } - }); + $('#background-picker-wrapper').click(this.onBackgroundClick.bind(this)); }; + + ns.ApplicationSettingsController.prototype.onShowGridClick = function (evt) { + var checked = $('#show-grid').prop('checked'); + pskl.UserSettings.set(pskl.UserSettings.SHOW_GRID, checked); + }; + + ns.ApplicationSettingsController.prototype.onBackgroundClick = function (evt) { + var target = $(evt.target).closest('.background-picker'); + if (target.length) { + var backgroundClass = target.data('background-class'); + pskl.UserSettings.set(pskl.UserSettings.CANVAS_BACKGROUND, backgroundClass); + + $('.background-picker').removeClass('selected'); + target.addClass('selected'); + } + }; + })(); \ No newline at end of file diff --git a/js/rendering/AbstractRenderer.js b/js/rendering/AbstractRenderer.js new file mode 100644 index 00000000..cb54169c --- /dev/null +++ b/js/rendering/AbstractRenderer.js @@ -0,0 +1,22 @@ +(function () { + var ns = $.namespace('pskl.rendering'); + + ns.AbstractRenderer = function () {}; + + ns.AbstractRenderer.prototype.render = function (frame) {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.clear = function () {throw 'abstract method should be implemented';}; + + ns.AbstractRenderer.prototype.getCoordinates = function (x, y) {throw 'abstract method should be implemented';}; + + ns.AbstractRenderer.prototype.setGridEnabled = function (b) {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.isGridEnabled = function () {throw 'abstract method should be implemented';}; + + ns.AbstractRenderer.prototype.setZoom = function (zoom) {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.getZoom = function () {throw 'abstract method should be implemented';}; + + ns.AbstractRenderer.prototype.moveOffset = function (x, y) {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.getOffset = function () {throw 'abstract method should be implemented';}; + + ns.AbstractRenderer.prototype.setDisplaySize = function (w, h) {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.getDisplaySize = function () {throw 'abstract method should be implemented';}; +})(); \ No newline at end of file diff --git a/js/rendering/CachedRenderer.js b/js/rendering/CachedRenderer.js new file mode 100644 index 00000000..e02040f5 --- /dev/null +++ b/js/rendering/CachedRenderer.js @@ -0,0 +1,64 @@ +(function () { + var ns = $.namespace('pskl.rendering'); + + /** + * Decorator on a renderer that will only render the frame if something has changed in the frame itself or in the renderer's configuration + * @param {pskl.rendering.AbstractRenderer} renderer + */ + ns.CachedRenderer = function (renderer) { + this.decoratedRenderer = renderer; + this.serializedFrame = ''; + }; + + pskl.utils.inherit(ns.CachedRenderer, ns.AbstractRenderer); + + ns.CachedRenderer.prototype.render = function (frame) { + var offset = this.getOffset(); + var size = this.getDisplaySize(); + var serializedFrame = [this.getZoom(), offset.x, offset.y, size.width, size.height, frame.serialize()].join('-'); + if (this.serializedFrame != serializedFrame) { + this.serializedFrame = serializedFrame; + this.decoratedRenderer.render(frame); + } + }; + + ns.CachedRenderer.prototype.clear = function () { + this.decoratedRenderer.clear(); + }; + + ns.CachedRenderer.prototype.getCoordinates = function (x, y) { + return this.decoratedRenderer.getCoordinates(x, y); + }; + + ns.CachedRenderer.prototype.setGridEnabled = function (b) { + this.decoratedRenderer.setGridEnabled(b); + }; + + ns.CachedRenderer.prototype.isGridEnabled = function () { + return this.decoratedRenderer.isGridEnabled(); + }; + + ns.CachedRenderer.prototype.getZoom = function () { + return this.decoratedRenderer.getZoom(); + }; + + ns.CachedRenderer.prototype.setZoom = function (zoom) { + return this.decoratedRenderer.setZoom(zoom); + }; + + ns.CachedRenderer.prototype.moveOffset = function (x, y) { + this.decoratedRenderer.moveOffset(x, y); + }; + + ns.CachedRenderer.prototype.getOffset = function () { + return this.decoratedRenderer.getOffset(); + }; + + ns.CachedRenderer.prototype.setDisplaySize = function (w, h) { + this.decoratedRenderer.setDisplaySize(w, h); + }; + + ns.CachedRenderer.prototype.getDisplaySize = function () { + return this.decoratedRenderer.getDisplaySize(); + }; +})(); \ No newline at end of file diff --git a/js/rendering/RendererManager.js b/js/rendering/RendererManager.js new file mode 100644 index 00000000..f7a9dd1f --- /dev/null +++ b/js/rendering/RendererManager.js @@ -0,0 +1,30 @@ +(function () { + var ns = $.namespace('pskl.rendering'); + + ns.RendererManager = function () { + this.renderers = []; + }; + + ns.RendererManager.prototype.add = function (renderer) { + this.renderers.push(renderer); + return this; + }; + + ns.RendererManager.prototype.setZoom = function (zoom) { + this.renderers.forEach(function (renderer) { + renderer.setZoom(zoom); + }); + }; + + ns.RendererManager.prototype.setDisplaySize = function (w, h) { + this.renderers.forEach(function (renderer) { + renderer.setDisplaySize(w, h); + }); + }; + + ns.RendererManager.prototype.moveOffset = function (offsetX, offsetY) { + this.renderers.forEach(function (renderer) { + renderer.moveOffset(offsetX, offsetY); + }); + }; +})(); \ No newline at end of file diff --git a/js/rendering/frame/CachedFrameRenderer.js b/js/rendering/frame/CachedFrameRenderer.js new file mode 100644 index 00000000..4949549d --- /dev/null +++ b/js/rendering/frame/CachedFrameRenderer.js @@ -0,0 +1,18 @@ +(function () { + var ns = $.namespace('pskl.rendering.frame'); + + /** + * Cached renderer that can uses the same constructor as pskl.rendering.FrameRenderer + * It will build a FrameRenderer on the fly to use as decorated renderer + * @param {HtmlElement} container HtmlElement to use as parentNode of the Frame + * @param {Object} renderingOptions + * @param {Array} classes array of strings to use for css classes + */ + ns.CachedFrameRenderer = function (container, renderingOptions, classes) { + var frameRenderer = new pskl.rendering.frame.FrameRenderer(container, renderingOptions, classes); + pskl.rendering.CachedRenderer.call(this, frameRenderer); + }; + + pskl.utils.inherit(pskl.rendering.frame.CachedFrameRenderer, pskl.rendering.CachedRenderer); + +})(); diff --git a/js/rendering/FrameRenderer.js b/js/rendering/frame/FrameRenderer.js similarity index 67% rename from js/rendering/FrameRenderer.js rename to js/rendering/frame/FrameRenderer.js index 7e49416f..3308ea68 100644 --- a/js/rendering/FrameRenderer.js +++ b/js/rendering/frame/FrameRenderer.js @@ -1,5 +1,5 @@ (function () { - var ns = $.namespace("pskl.rendering"); + var ns = $.namespace("pskl.rendering.frame"); /** * FrameRenderer will display a given frame inside a canvas element. @@ -27,11 +27,15 @@ this.zoom = renderingOptions.zoom; - this.frameOffsetX = 0; - this.frameOffsetY = 0; + this.offset = { + x : 0, + y : 0 + }; - this.marginY = 0; - this.marginX = 0; + this.margin = { + x : 0, + y : 0 + }; this.supportGridRendering = renderingOptions.supportGridRendering; @@ -51,93 +55,12 @@ this.displayCanvas = null; this.setDisplaySize(renderingOptions.width, renderingOptions.height); - this.enableGrid(pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)); + this.setGridEnabled(pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)); this.updateBackgroundClass_(pskl.UserSettings.get(pskl.UserSettings.CANVAS_BACKGROUND)); $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); }; - ns.FrameRenderer.prototype.setZoom = function (zoom) { - this.zoom = zoom; - }; - - ns.FrameRenderer.prototype.isAutoSized_ = function () { - return this.displayHeight === 'auto' && this.displayWidth === 'auto'; - }; - - ns.FrameRenderer.prototype.setDisplaySize = function (width, height) { - this.displayHeight = height; - this.displayWidth = width; - if (this.displayCanvas) { - $(this.displayCanvas).remove(); - this.displayCanvas = null; - } - }; - - ns.FrameRenderer.prototype.updateMargins_ = function () { - if (!this.isAutoSized_()) { - var deltaX = this.displayWidth - (this.zoom * this.canvas.width); - this.marginX = Math.max(0, deltaX) / 2; - - var deltaY = this.displayHeight - (this.zoom * this.canvas.height); - this.marginY = Math.max(0, deltaY) / 2; - } - }; - - ns.FrameRenderer.prototype.createDisplayCanvas_ = function () { - var height = this.displayHeight; - var width = this.displayWidth; - - if (this.isAutoSized_()) { - height = this.zoom * this.canvas.height; - width = this.zoom * this.canvas.width; - } - - this.displayCanvas = pskl.CanvasUtils.createCanvas(width, height, this.classes); - if (true || this.zoom > 2) { - pskl.CanvasUtils.disableImageSmoothing(this.displayCanvas); - } - this.container.append(this.displayCanvas); - }; - - ns.FrameRenderer.prototype.setDisplayOffset = function (frameOffsetX, frameOffsetY) { - this.frameOffsetX = frameOffsetX; - this.frameOffsetY = frameOffsetY; - }; - - ns.FrameRenderer.prototype.moveOffset = function (frameOffsetX, frameOffsetY) { - this.setDisplayOffset(this.frameOffsetX + frameOffsetX, this.frameOffsetY + frameOffsetY); - }; - - /** - * @private - */ - ns.FrameRenderer.prototype.onUserSettingsChange_ = function (evt, settingName, settingValue) { - if(settingName == pskl.UserSettings.SHOW_GRID) { - this.enableGrid(settingValue); - } - else if (settingName == pskl.UserSettings.CANVAS_BACKGROUND) { - this.updateBackgroundClass_(settingValue); - } - }; - - /** - * @private - */ - ns.FrameRenderer.prototype.updateBackgroundClass_ = function (newClass) { - var currentClass = this.container.data('current-background-class'); - if (currentClass) { - this.container.removeClass(currentClass); - } - this.container.addClass(newClass); - this.container.data('current-background-class', newClass); - }; - - ns.FrameRenderer.prototype.enableGrid = function (flag) { - this.gridStrokeWidth = (flag && this.supportGridRendering) ? Constants.GRID_STROKE_WIDTH : 0; - this.canvasConfigDirty = true; - }; - ns.FrameRenderer.prototype.render = function (frame) { if (frame) { this.clear(); @@ -150,10 +73,92 @@ pskl.CanvasUtils.clear(this.displayCanvas); }; - ns.FrameRenderer.prototype.renderPixel_ = function (color, col, row, context) { + ns.FrameRenderer.prototype.setZoom = function (zoom) { + this.zoom = 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.moveOffset = function (x, y) { + this.setOffset_(this.offset.x + x, this.offset.y + y); + }; + + ns.FrameRenderer.prototype.setGridEnabled = function (flag) { + this.gridStrokeWidth = (flag && this.supportGridRendering) ? Constants.GRID_STROKE_WIDTH : 0; + this.canvasConfigDirty = true; + }; + + ns.FrameRenderer.prototype.updateMargins_ = function () { + var deltaX = this.displayWidth - (this.zoom * this.canvas.width); + this.margin.x = Math.max(0, deltaX) / 2; + + var deltaY = this.displayHeight - (this.zoom * this.canvas.height); + this.margin.y = Math.max(0, deltaY) / 2; + }; + + ns.FrameRenderer.prototype.createDisplayCanvas_ = function () { + var height = this.displayHeight; + var width = this.displayWidth; + + this.displayCanvas = pskl.CanvasUtils.createCanvas(width, height, this.classes); + if (true || this.zoom > 2) { + pskl.CanvasUtils.disableImageSmoothing(this.displayCanvas); + } + this.container.append(this.displayCanvas); + }; + + ns.FrameRenderer.prototype.setOffset_ = function (x, y) { + this.offset.x = x; + this.offset.y = y; + }; + + ns.FrameRenderer.prototype.onUserSettingsChange_ = function (evt, settingName, settingValue) { + if(settingName == pskl.UserSettings.SHOW_GRID) { + this.setGridEnabled(settingValue); + } else if (settingName == pskl.UserSettings.CANVAS_BACKGROUND) { + this.updateBackgroundClass_(settingValue); + } + }; + + ns.FrameRenderer.prototype.updateBackgroundClass_ = function (newClass) { + var currentClass = this.container.data('current-background-class'); + if (currentClass) { + this.container.removeClass(currentClass); + } + this.container.addClass(newClass); + this.container.data('current-background-class', newClass); + }; + + ns.FrameRenderer.prototype.renderPixel_ = function (color, x, y, context) { if(color != Constants.TRANSPARENT_COLOR) { context.fillStyle = color; - context.fillRect(col, row, 1, 1); + context.fillRect(x, y, 1, 1); } }; @@ -162,13 +167,23 @@ * frame) into a sprite coordinate in column and row. * @public */ - ns.FrameRenderer.prototype.convertPixelCoordinatesIntoSpriteCoordinate = function(coords) { + 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 + this.gridStrokeWidth; - var xCoord = (coords.x - this.marginX) + (this.frameOffsetX * cellSize), - yCoord = (coords.y - this.marginY) + (this.frameOffsetY * cellSize); + // apply frame offset + x = x + this.offset.x * cellSize; + y = y + this.offset.y * cellSize; + return { - "col" : (xCoord - xCoord % cellSize) / cellSize, - "row" : (yCoord - yCoord % cellSize) / cellSize + x : (x / cellSize) | 0, + y : (y / cellSize) | 0 }; }; @@ -188,17 +203,13 @@ } var context = this.canvas.getContext('2d'); - for(var col = 0, width = frame.getWidth(); col < width; col++) { - for(var row = 0, height = frame.getHeight(); row < height; row++) { - var color = frame.getPixel(col, row); - this.renderPixel_(color, col, row, context); + for(var x = 0, width = frame.getWidth(); x < width; x++) { + for(var y = 0, height = frame.getHeight(); y < height; y++) { + var color = frame.getPixel(x, y); + this.renderPixel_(color, x, y, context); } } - if (!this.displayCanvas) { - this.createDisplayCanvas_(); - } - this.updateMargins_(); context = this.displayCanvas.getContext('2d'); @@ -207,9 +218,9 @@ context.fillStyle = "#aaa"; // zoom < 1 context.fillRect(0,0,this.displayCanvas.width, this.displayCanvas.height); - context.translate(this.marginX, this.marginY); + context.translate(this.margin.x, this.margin.y); context.scale(this.zoom, this.zoom); - context.translate(-this.frameOffsetX, -this.frameOffsetY); + context.translate(-this.offset.x, -this.offset.y); // zoom < 1 context.clearRect(0, 0, this.canvas.width, this.canvas.height); context.drawImage(this.canvas, 0, 0); diff --git a/piskel-script-list.js b/piskel-script-list.js index a35ebdcf..9ab2f1b2 100644 --- a/piskel-script-list.js +++ b/piskel-script-list.js @@ -36,8 +36,12 @@ exports.scripts = [ "js/selection/ShapeSelection.js", // Rendering + "js/rendering/RendererManager.js", + "js/rendering/AbstractRenderer.js", + "js/rendering/CachedRenderer.js", + "js/rendering/frame/FrameRenderer.js", + "js/rendering/frame/CachedFrameRenderer.js", "js/rendering/CanvasRenderer.js", - "js/rendering/FrameRenderer.js", "js/rendering/SpritesheetRenderer.js", // Controllers From b7e8310b612477a84b4ffd14d6938946f895032d Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 1 Nov 2013 16:27:23 +0100 Subject: [PATCH 04/16] feature : zoom : continued - simplified Renderer(s) architecture (removed decorator, CachedFrameRenderer simply inherits from FrameRenderer now) - keeping AbstractRenderer to act as interface - fixed issue with layers : forgot to clone the first frame while merging and therefore was modifying the original frame when I just wanted to create a tmp frame (FrameUtils.js) - extracted the mousemove throttling delay used in DrawingController to Constants.js and reduced it from 40ms to 10ms --- js/Constants.js | 3 +- js/controller/DrawingController.js | 2 +- js/rendering/CachedRenderer.js | 64 ----------------------- js/rendering/frame/CachedFrameRenderer.js | 18 +++++-- js/utils/FrameUtils.js | 2 +- piskel-script-list.js | 1 - 6 files changed, 17 insertions(+), 73 deletions(-) delete mode 100644 js/rendering/CachedRenderer.js diff --git a/js/Constants.js b/js/Constants.js index abf2726c..6ca8844e 100644 --- a/js/Constants.js +++ b/js/Constants.js @@ -48,5 +48,6 @@ var Constants = { GRID_STROKE_WIDTH: 1, LEFT_BUTTON : 'left_button_1', - RIGHT_BUTTON : 'right_button_2' + RIGHT_BUTTON : 'right_button_2', + MOUSEMOVE_THROTTLING : 10 }; \ No newline at end of file diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index e3520975..11e773e3 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -143,7 +143,7 @@ ns.DrawingController.prototype.onMousemove_ = function (event) { var currentTime = new Date().getTime(); // Throttling of the mousemove event: - if ((currentTime - this.previousMousemoveTime) > 40 ) { + if ((currentTime - this.previousMousemoveTime) > Constants.MOUSEMOVE_THROTTLING ) { var coords = this.renderer.getCoordinates(event.clientX, event.clientY); if (this.isClicked) { diff --git a/js/rendering/CachedRenderer.js b/js/rendering/CachedRenderer.js deleted file mode 100644 index e02040f5..00000000 --- a/js/rendering/CachedRenderer.js +++ /dev/null @@ -1,64 +0,0 @@ -(function () { - var ns = $.namespace('pskl.rendering'); - - /** - * Decorator on a renderer that will only render the frame if something has changed in the frame itself or in the renderer's configuration - * @param {pskl.rendering.AbstractRenderer} renderer - */ - ns.CachedRenderer = function (renderer) { - this.decoratedRenderer = renderer; - this.serializedFrame = ''; - }; - - pskl.utils.inherit(ns.CachedRenderer, ns.AbstractRenderer); - - ns.CachedRenderer.prototype.render = function (frame) { - var offset = this.getOffset(); - var size = this.getDisplaySize(); - var serializedFrame = [this.getZoom(), offset.x, offset.y, size.width, size.height, frame.serialize()].join('-'); - if (this.serializedFrame != serializedFrame) { - this.serializedFrame = serializedFrame; - this.decoratedRenderer.render(frame); - } - }; - - ns.CachedRenderer.prototype.clear = function () { - this.decoratedRenderer.clear(); - }; - - ns.CachedRenderer.prototype.getCoordinates = function (x, y) { - return this.decoratedRenderer.getCoordinates(x, y); - }; - - ns.CachedRenderer.prototype.setGridEnabled = function (b) { - this.decoratedRenderer.setGridEnabled(b); - }; - - ns.CachedRenderer.prototype.isGridEnabled = function () { - return this.decoratedRenderer.isGridEnabled(); - }; - - ns.CachedRenderer.prototype.getZoom = function () { - return this.decoratedRenderer.getZoom(); - }; - - ns.CachedRenderer.prototype.setZoom = function (zoom) { - return this.decoratedRenderer.setZoom(zoom); - }; - - ns.CachedRenderer.prototype.moveOffset = function (x, y) { - this.decoratedRenderer.moveOffset(x, y); - }; - - ns.CachedRenderer.prototype.getOffset = function () { - return this.decoratedRenderer.getOffset(); - }; - - ns.CachedRenderer.prototype.setDisplaySize = function (w, h) { - this.decoratedRenderer.setDisplaySize(w, h); - }; - - ns.CachedRenderer.prototype.getDisplaySize = function () { - return this.decoratedRenderer.getDisplaySize(); - }; -})(); \ No newline at end of file diff --git a/js/rendering/frame/CachedFrameRenderer.js b/js/rendering/frame/CachedFrameRenderer.js index 4949549d..819f66ea 100644 --- a/js/rendering/frame/CachedFrameRenderer.js +++ b/js/rendering/frame/CachedFrameRenderer.js @@ -2,17 +2,25 @@ var ns = $.namespace('pskl.rendering.frame'); /** - * Cached renderer that can uses the same constructor as pskl.rendering.FrameRenderer - * It will build a FrameRenderer on the fly to use as decorated renderer + * FrameRenderer implementation that prevents unnecessary redraws. * @param {HtmlElement} container HtmlElement to use as parentNode of the Frame * @param {Object} renderingOptions * @param {Array} classes array of strings to use for css classes */ ns.CachedFrameRenderer = function (container, renderingOptions, classes) { - var frameRenderer = new pskl.rendering.frame.FrameRenderer(container, renderingOptions, classes); - pskl.rendering.CachedRenderer.call(this, frameRenderer); + pskl.rendering.frame.FrameRenderer.call(this, container, renderingOptions, classes); + this.serializedFrame = ''; }; - pskl.utils.inherit(pskl.rendering.frame.CachedFrameRenderer, pskl.rendering.CachedRenderer); + pskl.utils.inherit(pskl.rendering.frame.CachedFrameRenderer, pskl.rendering.frame.FrameRenderer); + ns.CachedFrameRenderer.prototype.render = function (frame) { + var offset = this.getOffset(); + var size = this.getDisplaySize(); + var serializedFrame = [this.getZoom(), offset.x, offset.y, size.width, size.height, frame.serialize()].join('-'); + if (this.serializedFrame != serializedFrame) { + this.serializedFrame = serializedFrame; + this.superclass.render.call(this, frame); + } + }; })(); diff --git a/js/utils/FrameUtils.js b/js/utils/FrameUtils.js index 7f422006..b28d154d 100644 --- a/js/utils/FrameUtils.js +++ b/js/utils/FrameUtils.js @@ -5,7 +5,7 @@ merge : function (frames) { var merged = null; if (frames.length) { - merged = frames[0]; + merged = frames[0].clone(); var w = merged.getWidth(), h = merged.getHeight(); for (var i = 1 ; i < frames.length ; i++) { pskl.utils.FrameUtils.mergeFrames_(merged, frames[i]); diff --git a/piskel-script-list.js b/piskel-script-list.js index 9ab2f1b2..fe681dd6 100644 --- a/piskel-script-list.js +++ b/piskel-script-list.js @@ -38,7 +38,6 @@ exports.scripts = [ // Rendering "js/rendering/RendererManager.js", "js/rendering/AbstractRenderer.js", - "js/rendering/CachedRenderer.js", "js/rendering/frame/FrameRenderer.js", "js/rendering/frame/CachedFrameRenderer.js", "js/rendering/CanvasRenderer.js", From 472906957ae760a987b544276fe2c298ece911ef Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 1 Nov 2013 17:12:59 +0100 Subject: [PATCH 05/16] feature : zoom : - Extracted layers rendering logic from DrawingController to dedicated class - Turned RendererManager into Composite renderer (extends AbstractRenderer) - AbstractRenderer no longer contains a render(frame) method, implementing the render differs too much between my current renderers to impose a single signature, but I should improve this later if too much time on ma hands --- js/controller/DrawingController.js | 69 +++++------------------------ js/rendering/AbstractRenderer.js | 1 - js/rendering/RendererManager.js | 30 ------------- js/rendering/frame/FrameRenderer.js | 2 + piskel-script-list.js | 3 +- 5 files changed, 15 insertions(+), 90 deletions(-) delete mode 100644 js/rendering/RendererManager.js diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index 11e773e3..ab1d65ea 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -28,16 +28,13 @@ this.overlayRenderer = new pskl.rendering.frame.CachedFrameRenderer(this.container, renderingOptions, ["canvas-overlay"]); this.renderer = new pskl.rendering.frame.CachedFrameRenderer(this.container, renderingOptions, ["drawing-canvas"]); + this.layersRenderer = new pskl.rendering.layer.LayersRenderer(this.container, renderingOptions, piskelController); - this.layersBelowRenderer = new pskl.rendering.frame.FrameRenderer(this.container, renderingOptions, ["layers-canvas", "layers-below-canvas"]); - this.layersAboveRenderer = new pskl.rendering.frame.FrameRenderer(this.container, renderingOptions, ["layers-canvas", "layers-above-canvas"]); - - this.rendererManager = new pskl.rendering.RendererManager(); - this.rendererManager + this.compositeRenderer = new pskl.rendering.CompositeRenderer(); + this.compositeRenderer .add(this.overlayRenderer) .add(this.renderer) - .add(this.layersBelowRenderer) - .add(this.layersAboveRenderer); + .add(this.layersRenderer); // State of drawing controller: this.isClicked = false; @@ -100,7 +97,7 @@ }, ns.DrawingController.prototype.afterWindowResize_ = function () { - this.rendererManager.setDisplaySize(this.getContainerWidth_(), this.getContainerHeight_()); + this.compositeRenderer.setDisplaySize(this.getContainerWidth_(), this.getContainerHeight_()); }, /** @@ -181,9 +178,9 @@ var delta = event.wheelDeltaY; var currentZoom = this.renderer.getZoom(); if (delta > 0) { - this.rendererManager.setZoom(currentZoom + 1); + this.compositeRenderer.setZoom(currentZoom + 1); } else if (delta < 0) { - this.rendererManager.setZoom(currentZoom - 1); + this.compositeRenderer.setZoom(currentZoom - 1); } }; @@ -261,60 +258,16 @@ }; ns.DrawingController.prototype.render = function () { - this.renderLayers(); - this.renderFrame(); - this.renderOverlay(); - }; - - ns.DrawingController.prototype.renderFrame = function () { - this.renderer.render(this.piskelController.getCurrentFrame()); - }; - - ns.DrawingController.prototype.renderOverlay = function () { var currentFrame = this.piskelController.getCurrentFrame(); if (!currentFrame.isSameSize(this.overlayFrame)) { this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(currentFrame); } + + this.layersRenderer.render(); + this.renderer.render(currentFrame); this.overlayRenderer.render(this.overlayFrame); }; - ns.DrawingController.prototype.renderLayers = function () { - var layers = this.piskelController.getLayers(); - var currentFrameIndex = this.piskelController.currentFrameIndex; - var currentLayerIndex = this.piskelController.currentLayerIndex; - - var serializedLayerFrame = [ - this.zoom, - currentFrameIndex, - currentLayerIndex, - layers.length - ].join("-"); - - if (this.serializedLayerFrame != serializedLayerFrame) { - this.layersAboveRenderer.clear(); - this.layersBelowRenderer.clear(); - - var downLayers = layers.slice(0, currentLayerIndex); - var downFrame = this.getFrameForLayersAt_(currentFrameIndex, downLayers); - this.layersBelowRenderer.render(downFrame); - - if (currentLayerIndex + 1 < layers.length) { - var upLayers = layers.slice(currentLayerIndex + 1, layers.length); - var upFrame = this.getFrameForLayersAt_(currentFrameIndex, upLayers); - this.layersAboveRenderer.render(upFrame); - } - - this.serializedLayerFrame = serializedLayerFrame; - } - }; - - ns.DrawingController.prototype.getFrameForLayersAt_ = function (frameIndex, layers) { - var frames = layers.map(function (l) { - return l.getFrameAt(frameIndex); - }); - return pskl.utils.FrameUtils.merge(frames); - }; - /** * @private */ @@ -356,6 +309,6 @@ }; ns.DrawingController.prototype.moveOffset = function (xOffset, yOffset) { - this.rendererManager.moveOffset(xOffset, yOffset); + this.compositeRenderer.moveOffset(xOffset, yOffset); }; })(); \ No newline at end of file diff --git a/js/rendering/AbstractRenderer.js b/js/rendering/AbstractRenderer.js index cb54169c..ffb4dcb5 100644 --- a/js/rendering/AbstractRenderer.js +++ b/js/rendering/AbstractRenderer.js @@ -3,7 +3,6 @@ ns.AbstractRenderer = function () {}; - ns.AbstractRenderer.prototype.render = function (frame) {throw 'abstract method should be implemented';}; ns.AbstractRenderer.prototype.clear = function () {throw 'abstract method should be implemented';}; ns.AbstractRenderer.prototype.getCoordinates = function (x, y) {throw 'abstract method should be implemented';}; diff --git a/js/rendering/RendererManager.js b/js/rendering/RendererManager.js deleted file mode 100644 index f7a9dd1f..00000000 --- a/js/rendering/RendererManager.js +++ /dev/null @@ -1,30 +0,0 @@ -(function () { - var ns = $.namespace('pskl.rendering'); - - ns.RendererManager = function () { - this.renderers = []; - }; - - ns.RendererManager.prototype.add = function (renderer) { - this.renderers.push(renderer); - return this; - }; - - ns.RendererManager.prototype.setZoom = function (zoom) { - this.renderers.forEach(function (renderer) { - renderer.setZoom(zoom); - }); - }; - - ns.RendererManager.prototype.setDisplaySize = function (w, h) { - this.renderers.forEach(function (renderer) { - renderer.setDisplaySize(w, h); - }); - }; - - ns.RendererManager.prototype.moveOffset = function (offsetX, offsetY) { - this.renderers.forEach(function (renderer) { - renderer.moveOffset(offsetX, offsetY); - }); - }; -})(); \ No newline at end of file diff --git a/js/rendering/frame/FrameRenderer.js b/js/rendering/frame/FrameRenderer.js index 3308ea68..038ff097 100644 --- a/js/rendering/frame/FrameRenderer.js +++ b/js/rendering/frame/FrameRenderer.js @@ -61,6 +61,8 @@ $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); }; + pskl.utils.inherit(pskl.rendering.frame.FrameRenderer, pskl.rendering.AbstractRenderer); + ns.FrameRenderer.prototype.render = function (frame) { if (frame) { this.clear(); diff --git a/piskel-script-list.js b/piskel-script-list.js index fe681dd6..e4c84c2f 100644 --- a/piskel-script-list.js +++ b/piskel-script-list.js @@ -36,8 +36,9 @@ exports.scripts = [ "js/selection/ShapeSelection.js", // Rendering - "js/rendering/RendererManager.js", "js/rendering/AbstractRenderer.js", + "js/rendering/CompositeRenderer.js", + "js/rendering/layer/LayersRenderer.js", "js/rendering/frame/FrameRenderer.js", "js/rendering/frame/CachedFrameRenderer.js", "js/rendering/CanvasRenderer.js", From 68edbe62c7e7a0434bdccd1a3454560a86e54179 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 1 Nov 2013 17:17:41 +0100 Subject: [PATCH 06/16] Forgot 2 files ! --- js/rendering/CompositeRenderer.js | 69 ++++++++++++++++++++++++++++ js/rendering/layer/LayersRenderer.js | 58 +++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 js/rendering/CompositeRenderer.js create mode 100644 js/rendering/layer/LayersRenderer.js diff --git a/js/rendering/CompositeRenderer.js b/js/rendering/CompositeRenderer.js new file mode 100644 index 00000000..834d2c73 --- /dev/null +++ b/js/rendering/CompositeRenderer.js @@ -0,0 +1,69 @@ +(function () { + var ns = $.namespace('pskl.rendering'); + + ns.CompositeRenderer = function () { + this.renderers = []; + }; + + pskl.utils.inherit(pskl.rendering.CompositeRenderer, pskl.rendering.AbstractRenderer); + + ns.CompositeRenderer.prototype.add = function (renderer) { + this.renderers.push(renderer); + return this; + }; + + ns.CompositeRenderer.prototype.clear = function () { + this.renderers.forEach(function (renderer) { + renderer.clear(); + }); + }; + + ns.CompositeRenderer.prototype.setZoom = function (zoom) { + this.renderers.forEach(function (renderer) { + renderer.setZoom(zoom); + }); + }; + + ns.CompositeRenderer.prototype.getZoom = function () { + return this.getSampleRenderer_().getZoom(); + }; + + ns.CompositeRenderer.prototype.setDisplaySize = function (w, h) { + this.renderers.forEach(function (renderer) { + renderer.setDisplaySize(w, h); + }); + }; + + ns.CompositeRenderer.prototype.getDisplaySize = function () { + return this.getSampleRenderer_().getDisplaySize(); + }; + + ns.CompositeRenderer.prototype.moveOffset = function (offsetX, offsetY) { + this.renderers.forEach(function (renderer) { + renderer.moveOffset(offsetX, offsetY); + }); + }; + + ns.CompositeRenderer.prototype.getOffset = function () { + return this.getSampleRenderer_().getOffset(); + }; + + + ns.CompositeRenderer.prototype.setGridEnabled = function (b) { + this.renderers.forEach(function (renderer) { + renderer.setGridEnabled(b); + }); + }; + + ns.CompositeRenderer.prototype.isGridEnabled = function () { + return this.getSampleRenderer_().isGridEnabled(); + }; + + ns.CompositeRenderer.prototype.getSampleRenderer_ = function () { + if (this.renderers.length > 0) { + return this.renderers[0]; + } else { + throw 'Renderer manager is empty'; + } + }; +})(); \ No newline at end of file diff --git a/js/rendering/layer/LayersRenderer.js b/js/rendering/layer/LayersRenderer.js new file mode 100644 index 00000000..12c8b38d --- /dev/null +++ b/js/rendering/layer/LayersRenderer.js @@ -0,0 +1,58 @@ +(function () { + var ns = $.namespace('pskl.rendering.layer'); + + ns.LayersRenderer = function (container, renderingOptions, piskelController) { + pskl.rendering.CompositeRenderer.call(this); + + this.piskelController = piskelController; + + this.belowRenderer = new pskl.rendering.frame.CachedFrameRenderer(container, renderingOptions, ["layers-canvas", "layers-below-canvas"]); + this.aboveRenderer = new pskl.rendering.frame.CachedFrameRenderer(container, renderingOptions, ["layers-canvas", "layers-above-canvas"]); + + this.add(this.belowRenderer); + this.add(this.aboveRenderer); + + this.serializedRendering = ''; + }; + + pskl.utils.inherit(pskl.rendering.layer.LayersRenderer, pskl.rendering.CompositeRenderer); + + ns.LayersRenderer.prototype.render = function () { + var layers = this.piskelController.getLayers(); + var currentFrameIndex = this.piskelController.currentFrameIndex; + var currentLayerIndex = this.piskelController.currentLayerIndex; + + var serializedRendering = [ + this.getZoom(), + currentFrameIndex, + currentLayerIndex, + layers.length + ].join("-"); + + if (this.serializedRendering != serializedRendering) { + this.serializedRendering = serializedRendering; + + this.clear(); + + var downLayers = layers.slice(0, currentLayerIndex); + if (downLayers.length > 0) { + var downFrame = this.getFrameForLayersAt_(currentFrameIndex, downLayers); + this.belowRenderer.render(downFrame); + } + + var upLayers = layers.slice(currentLayerIndex + 1, layers.length); + if (upLayers.length > 0) { + var upFrame = this.getFrameForLayersAt_(currentFrameIndex, upLayers); + this.aboveRenderer.render(upFrame); + } + + } + }; + + ns.LayersRenderer.prototype.getFrameForLayersAt_ = function (frameIndex, layers) { + var frames = layers.map(function (l) { + return l.getFrameAt(frameIndex); + }); + return pskl.utils.FrameUtils.merge(frames); + }; +})(); From bd990278523a1eea8516727167566772a2c3bbfc Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 1 Nov 2013 23:11:11 +0100 Subject: [PATCH 07/16] feature : zoom - Added MinimapController that displays a frame on the animated preview when zoomed in - Added bounds for the offset to make sure it doesn't go crazy - Added new utility Math.js with a minmax function - TODO : the minimap controller has a lot of dependencies, see if could be cleaned up - TODO : DrawingController knows the size of the picture it has to render only indirectly, which makes it hard in some cases (such as boundary checking performed during setOffset) --- css/minimap.css | 8 +++ css/style.css | 5 ++ index.html | 1 + js/app.js | 3 + js/controller/DrawingController.js | 13 ++++- js/controller/MinimapController.js | 85 +++++++++++++++++++++++++++++ js/rendering/AbstractRenderer.js | 1 + js/rendering/CompositeRenderer.js | 10 +++- js/rendering/frame/FrameRenderer.js | 40 +++++++++----- js/utils/Math.js | 9 +++ piskel-script-list.js | 2 + 11 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 css/minimap.css create mode 100644 js/controller/MinimapController.js create mode 100644 js/utils/Math.js diff --git a/css/minimap.css b/css/minimap.css new file mode 100644 index 00000000..0383e127 --- /dev/null +++ b/css/minimap.css @@ -0,0 +1,8 @@ +.minimap-crop-frame { + position:absolute; + border:1px solid red; + z-index:9999; + box-sizing : border-box; + -moz-box-sizing : border-box; + cursor : pointer; +} \ No newline at end of file diff --git a/css/style.css b/css/style.css index 1b810419..6fc8e490 100644 --- a/css/style.css +++ b/css/style.css @@ -174,6 +174,10 @@ body { font-size: 0; } +.preview-container .canvas-container { + overflow : hidden; +} + .preview-container canvas { border : 0px Solid transparent; } @@ -189,6 +193,7 @@ body { .range-fps { overflow: hidden; + width: 120px; } /** diff --git a/index.html b/index.html index 50c1ddd6..cb8f278c 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,7 @@ + diff --git a/js/app.js b/js/app.js index 93444c84..b783d785 100644 --- a/js/app.js +++ b/js/app.js @@ -27,6 +27,9 @@ this.animationController = new pskl.controller.AnimatedPreviewController(this.piskelController, $('#preview-canvas-container')); this.animationController.init(); + this.minimapController = new pskl.controller.MinimapController(this.piskelController, this.animationController, this.drawingController, $('#preview-canvas-container')); + this.minimapController.init(); + this.previewsController = new pskl.controller.PreviewFilmController(this.piskelController, $('#preview-list')); this.previewsController.init(); diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index ab1d65ea..98efb7f4 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -82,6 +82,7 @@ this.container.mousedown($.proxy(this.onMousedown_, this)); this.container.mousemove($.proxy(this.onMousemove_, this)); this.container.on('mousewheel', $.proxy(this.onMousewheel_, this)); + this.container.on('wheel', $.proxy(this.onMousewheel_, this)); body.mouseup($.proxy(this.onMouseup_, this)); @@ -175,13 +176,14 @@ ns.DrawingController.prototype.onMousewheel_ = function (jQueryEvent) { var event = jQueryEvent.originalEvent; - var delta = event.wheelDeltaY; + var delta = event.wheelDeltaY || (-2 * event.deltaY); var currentZoom = this.renderer.getZoom(); if (delta > 0) { this.compositeRenderer.setZoom(currentZoom + 1); } else if (delta < 0) { this.compositeRenderer.setZoom(currentZoom - 1); } + pskl.app.minimapController.onDrawingControllerMove_(); }; /** @@ -308,7 +310,12 @@ }); }; - ns.DrawingController.prototype.moveOffset = function (xOffset, yOffset) { - this.compositeRenderer.moveOffset(xOffset, yOffset); + ns.DrawingController.prototype.getRenderer = function () { + return this.compositeRenderer; + }; + + ns.DrawingController.prototype.setOffset = function (x, y) { + this.compositeRenderer.setOffset(x, y); + pskl.app.minimapController.onDrawingControllerMove_(); }; })(); \ No newline at end of file diff --git a/js/controller/MinimapController.js b/js/controller/MinimapController.js new file mode 100644 index 00000000..8128790b --- /dev/null +++ b/js/controller/MinimapController.js @@ -0,0 +1,85 @@ +(function () { + var ns = $.namespace('pskl.controller'); + + ns.MinimapController = function (piskelController, animationController, drawingController, container) { + this.piskelController = piskelController; + this.animationController = animationController; + this.drawingController = drawingController; + this.container = container; + + this.isClicked = false; + }; + + ns.MinimapController.prototype.init = function () { + this.cropFrame = document.createElement('DIV'); + this.cropFrame.className = 'minimap-crop-frame'; + this.cropFrame.style.display = 'none'; + $(this.container).mousedown(this.onMinimapMousedown_.bind(this)); + $(this.container).mousemove(this.onMinimapMousemove_.bind(this)); + $(this.container).mouseup(this.onMinimapMouseup_.bind(this)); + $(this.container).append(this.cropFrame); + }; + + ns.MinimapController.prototype.onDrawingControllerMove_ = function () { + var zoomRatio = this.getDrawingAreaZoomRatio_(); + if (zoomRatio > 1) { + this.displayCropFrame_(zoomRatio, this.drawingController.getRenderer().getOffset()); + } else { + this.hideCropFrame_(); + } + }; + + ns.MinimapController.prototype.displayCropFrame_ = function (ratio, offset) { + this.cropFrame.style.display = 'block'; + this.cropFrame.style.top = (offset.y * this.animationController.renderer.getZoom()) + 'px'; + this.cropFrame.style.left = (offset.x * this.animationController.renderer.getZoom()) + 'px'; + var zoomRatio = this.getDrawingAreaZoomRatio_(); + this.cropFrame.style.width = (this.container.width() / zoomRatio) + 'px'; + this.cropFrame.style.height = (this.container.height() / zoomRatio) + 'px'; + + }; + + ns.MinimapController.prototype.hideCropFrame_ = function () { + this.cropFrame.style.display = 'none'; + }; + + ns.MinimapController.prototype.onMinimapMousemove_ = function (evt) { + if (this.isClicked) { + if (this.getDrawingAreaZoomRatio_() > 1) { + var coords = this.getCoordinatesCenteredAround_(evt.clientX, evt.clientY); + this.drawingController.setOffset(coords.x, coords.y); + } + } + }; + + ns.MinimapController.prototype.onMinimapMousedown_ = function (evt) { + this.isClicked = true; + }; + + ns.MinimapController.prototype.onMinimapMouseup_ = function (evt) { + this.isClicked = false; + }; + + ns.MinimapController.prototype.getCoordinatesCenteredAround_ = function (x, y) { + var frameCoords = this.animationController.renderer.getCoordinates(x, y); + var zoomRatio = this.getDrawingAreaZoomRatio_(); + var frameWidth = this.piskelController.getCurrentFrame().getWidth(); + var frameHeight = this.piskelController.getCurrentFrame().getHeight(); + + var width = frameWidth / zoomRatio; + var height = frameHeight / zoomRatio; + + return { + x : frameCoords.x - (width/2), + y : frameCoords.y - (height/2) + }; + }; + + ns.MinimapController.prototype.getDrawingAreaZoomRatio_ = function () { + var drawingAreaZoom = this.drawingController.getRenderer().getZoom(); + var drawingAreaFullHeight = this.piskelController.getCurrentFrame().getHeight() * drawingAreaZoom; + var zoomRatio = drawingAreaFullHeight / this.drawingController.getRenderer().getDisplaySize().height; + + return zoomRatio; + }; +})(); \ No newline at end of file diff --git a/js/rendering/AbstractRenderer.js b/js/rendering/AbstractRenderer.js index ffb4dcb5..a3539402 100644 --- a/js/rendering/AbstractRenderer.js +++ b/js/rendering/AbstractRenderer.js @@ -14,6 +14,7 @@ ns.AbstractRenderer.prototype.getZoom = function () {throw 'abstract method should be implemented';}; ns.AbstractRenderer.prototype.moveOffset = function (x, y) {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.setOffset = function (x, y) {throw 'abstract method should be implemented';}; ns.AbstractRenderer.prototype.getOffset = function () {throw 'abstract method should be implemented';}; ns.AbstractRenderer.prototype.setDisplaySize = function (w, h) {throw 'abstract method should be implemented';}; diff --git a/js/rendering/CompositeRenderer.js b/js/rendering/CompositeRenderer.js index 834d2c73..d5a770f4 100644 --- a/js/rendering/CompositeRenderer.js +++ b/js/rendering/CompositeRenderer.js @@ -38,9 +38,15 @@ return this.getSampleRenderer_().getDisplaySize(); }; - ns.CompositeRenderer.prototype.moveOffset = function (offsetX, offsetY) { + ns.CompositeRenderer.prototype.moveOffset = function (x, y) { this.renderers.forEach(function (renderer) { - renderer.moveOffset(offsetX, offsetY); + renderer.moveOffset(x, y); + }); + }; + + ns.CompositeRenderer.prototype.setOffset = function (x, y) { + this.renderers.forEach(function (renderer) { + renderer.setOffset(x, y); }); }; diff --git a/js/rendering/frame/FrameRenderer.js b/js/rendering/frame/FrameRenderer.js index 038ff097..9d3bb57d 100644 --- a/js/rendering/frame/FrameRenderer.js +++ b/js/rendering/frame/FrameRenderer.js @@ -76,7 +76,17 @@ }; ns.FrameRenderer.prototype.setZoom = function (zoom) { - this.zoom = zoom; + // 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 = Math.max(1, zoom); + + // recenter + this.setOffset( + centerX - (this.displayWidth/(2*this.zoom)), + centerY - (this.displayHeight/(2*this.zoom)) + ); }; ns.FrameRenderer.prototype.getZoom = function () { @@ -108,7 +118,21 @@ }, ns.FrameRenderer.prototype.moveOffset = function (x, y) { - this.setOffset_(this.offset.x + x, this.offset.y + y); + this.setOffset(this.offset.x + x, this.offset.y + y); + }; + + ns.FrameRenderer.prototype.setOffset = function (x, y) { + // TODO : provide frame size information to the FrameRenderer constructor + // here I first need to verify I have a 'canvas' which I can use to infer the frame information + // and then perform my boundaries checking. This sucks + if (this.canvas) { + var maxX = this.canvas.width - (this.displayWidth/this.zoom); + x = pskl.utils.Math.minmax(x, 0, maxX); + var maxY = this.canvas.height - (this.displayHeight/this.zoom); + y = pskl.utils.Math.minmax(y, 0, maxY); + } + this.offset.x = x; + this.offset.y = y; }; ns.FrameRenderer.prototype.setGridEnabled = function (flag) { @@ -135,11 +159,6 @@ this.container.append(this.displayCanvas); }; - ns.FrameRenderer.prototype.setOffset_ = function (x, y) { - this.offset.x = x; - this.offset.y = y; - }; - ns.FrameRenderer.prototype.onUserSettingsChange_ = function (evt, settingName, settingValue) { if(settingName == pskl.UserSettings.SHOW_GRID) { this.setGridEnabled(settingValue); @@ -189,13 +208,6 @@ }; }; - /** - * @private - */ - ns.FrameRenderer.prototype.getFramePos_ = function(index) { - return index * this.dpi + ((index - 1) * this.gridStrokeWidth); - }; - /** * @private */ diff --git a/js/utils/Math.js b/js/utils/Math.js new file mode 100644 index 00000000..b333a333 --- /dev/null +++ b/js/utils/Math.js @@ -0,0 +1,9 @@ +(function () { + var ns = $.namespace('pskl.utils'); + + ns.Math = { + minmax : function (val, min, max) { + return Math.max(Math.min(val, max), min); + } + }; +})(); \ No newline at end of file diff --git a/piskel-script-list.js b/piskel-script-list.js index e4c84c2f..9f3288d8 100644 --- a/piskel-script-list.js +++ b/piskel-script-list.js @@ -14,6 +14,7 @@ exports.scripts = [ // Libraries "js/utils/core.js", "js/utils/CanvasUtils.js", + "js/utils/Math.js", "js/utils/FrameUtils.js", "js/utils/PixelUtils.js", "js/utils/Serializer.js", @@ -50,6 +51,7 @@ exports.scripts = [ "js/controller/PreviewFilmController.js", "js/controller/LayersListController.js", "js/controller/AnimatedPreviewController.js", + "js/controller/MinimapController.js", "js/controller/ToolController.js", "js/controller/PaletteController.js", "js/controller/NotificationController.js", From 5693f34fb91ef77925ccae4e98cf4bfb2c427192 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 1 Nov 2013 23:30:33 +0100 Subject: [PATCH 08/16] fix : LocalStorageService deserialization --- js/service/LocalStorageService.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/service/LocalStorageService.js b/js/service/LocalStorageService.js index 6267773f..bc43beca 100644 --- a/js/service/LocalStorageService.js +++ b/js/service/LocalStorageService.js @@ -36,7 +36,7 @@ * @private */ ns.LocalStorageService.prototype.persistToLocalStorage_ = function() { - + console.log('[LocalStorage service]: Snapshot stored'); window.localStorage.snapShot = this.piskelController.serialize(); }; @@ -45,9 +45,9 @@ * @private */ ns.LocalStorageService.prototype.restoreFromLocalStorage_ = function() { - - this.piskelController.deserialize(window.localStorage.snapShot); - this.piskelController.setCurrentFrameIndex(0); + var framesheet = JSON.parse(window.localStorage.snapShot); + var piskel = pskl.utils.Serializer.createPiskel(framesheet); + pskl.app.piskelController.setPiskel(piskel); }; /** From e0d63bf2956d6ae2278e8451301bd3624649544c Mon Sep 17 00:00:00 2001 From: jdescottes Date: Sat, 2 Nov 2013 00:00:38 +0100 Subject: [PATCH 09/16] fix : renderer resize on window resize --- js/controller/AnimatedPreviewController.js | 12 ++++++++---- js/controller/DrawingController.js | 5 +++++ js/rendering/frame/FrameRenderer.js | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/js/controller/AnimatedPreviewController.js b/js/controller/AnimatedPreviewController.js index 14749608..c61e789a 100644 --- a/js/controller/AnimatedPreviewController.js +++ b/js/controller/AnimatedPreviewController.js @@ -10,10 +10,11 @@ this.setFPS(Constants.DEFAULT.FPS); var zoom = this.calculateZoom_(); + var frame = this.piskelController.getCurrentFrame(); var renderingOptions = { "zoom": zoom, - "height" : this.piskelController.getCurrentFrame().getHeight() * zoom, - "width" : this.piskelController.getCurrentFrame().getWidth() * zoom + "height" : frame.getHeight() * zoom, + "width" : frame.getWidth() * zoom }; this.renderer = new pskl.rendering.frame.FrameRenderer(this.container, renderingOptions); @@ -58,15 +59,18 @@ * Calculate the preview DPI depending on the framesheet size */ ns.AnimatedPreviewController.prototype.calculateZoom_ = function () { + var frame = this.piskelController.getCurrentFrame(); var previewSize = 200, - hZoom = previewSize / this.piskelController.getCurrentFrame().getHeight(), - wZoom = previewSize / this.piskelController.getCurrentFrame().getWidth(); + hZoom = previewSize / frame.getHeight(), + wZoom = previewSize / frame.getWidth(); return Math.min(hZoom, wZoom); }; ns.AnimatedPreviewController.prototype.updateZoom_ = function () { + var frame = this.piskelController.getCurrentFrame(); var zoom = this.calculateZoom_(); this.renderer.setZoom(zoom); + this.renderer.setDisplaySize(frame.getWidth() * zoom, frame.getHeight() * zoom); }; })(); \ No newline at end of file diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index 98efb7f4..417e0b44 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -110,6 +110,11 @@ } }, + ns.DrawingController.prototype.onFrameSizeChanged_ = function () { + this.compositeRenderer.setZoom(this.calculateZoom_()); + this.compositeRenderer.setDisplaySize(this.getContainerWidth_(), this.getContainerHeight_()); + }; + /** * @private */ diff --git a/js/rendering/frame/FrameRenderer.js b/js/rendering/frame/FrameRenderer.js index 9d3bb57d..eb0eb03d 100644 --- a/js/rendering/frame/FrameRenderer.js +++ b/js/rendering/frame/FrameRenderer.js @@ -212,7 +212,7 @@ * @private */ ns.FrameRenderer.prototype.renderFrame_ = function (frame) { - if (!this.canvas) { + if (!this.canvas || frame.getWidth() != this.canvas.width || frame.getHeight() != this.canvas.height) { this.canvas = pskl.CanvasUtils.createCanvas(frame.getWidth(), frame.getHeight()); } From cd4952cc7ba6467591c5e9f5b1b8cdb32549dd43 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Sat, 2 Nov 2013 11:24:02 +0100 Subject: [PATCH 10/16] feature : zoom-level - Minimap usability : mouseup and mousemove events are now plugged on document.body instead of the minimap's controller. This way the user can move his mouse outside the container to keep moving the map's frame. Also the mouseup information is no longer lost if it occurs outside for the minimap. --- js/controller/MinimapController.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/js/controller/MinimapController.js b/js/controller/MinimapController.js index 8128790b..6b1fc892 100644 --- a/js/controller/MinimapController.js +++ b/js/controller/MinimapController.js @@ -11,13 +11,16 @@ }; ns.MinimapController.prototype.init = function () { + // Create minimap DOM elements this.cropFrame = document.createElement('DIV'); this.cropFrame.className = 'minimap-crop-frame'; this.cropFrame.style.display = 'none'; - $(this.container).mousedown(this.onMinimapMousedown_.bind(this)); - $(this.container).mousemove(this.onMinimapMousemove_.bind(this)); - $(this.container).mouseup(this.onMinimapMouseup_.bind(this)); $(this.container).append(this.cropFrame); + + // Init mouse events + $(this.container).mousedown(this.onMinimapMousedown_.bind(this)); + $('body').mousemove(this.onMinimapMousemove_.bind(this)); + $('body').mouseup(this.onMinimapMouseup_.bind(this)); }; ns.MinimapController.prototype.onDrawingControllerMove_ = function () { From 44722ab88e565847fc961182d28ae3800bec4936 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Sat, 2 Nov 2013 11:39:35 +0100 Subject: [PATCH 11/16] feature : zoom-level - Fixed bug with layer rendering when moving drawing offset (bad redraw-flag checking) --- js/rendering/layer/LayersRenderer.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/js/rendering/layer/LayersRenderer.js b/js/rendering/layer/LayersRenderer.js index 12c8b38d..0963ed05 100644 --- a/js/rendering/layer/LayersRenderer.js +++ b/js/rendering/layer/LayersRenderer.js @@ -6,8 +6,9 @@ this.piskelController = piskelController; - this.belowRenderer = new pskl.rendering.frame.CachedFrameRenderer(container, renderingOptions, ["layers-canvas", "layers-below-canvas"]); - this.aboveRenderer = new pskl.rendering.frame.CachedFrameRenderer(container, renderingOptions, ["layers-canvas", "layers-above-canvas"]); + // Do not use CachedFrameRenderers here, since the caching will be performed in the render method of LayersRenderer + this.belowRenderer = new pskl.rendering.frame.FrameRenderer(container, renderingOptions, ["layers-canvas", "layers-below-canvas"]); + this.aboveRenderer = new pskl.rendering.frame.FrameRenderer(container, renderingOptions, ["layers-canvas", "layers-above-canvas"]); this.add(this.belowRenderer); this.add(this.aboveRenderer); @@ -18,18 +19,26 @@ pskl.utils.inherit(pskl.rendering.layer.LayersRenderer, pskl.rendering.CompositeRenderer); ns.LayersRenderer.prototype.render = function () { + var offset = this.getOffset(); + var size = this.getDisplaySize(); var layers = this.piskelController.getLayers(); var currentFrameIndex = this.piskelController.currentFrameIndex; var currentLayerIndex = this.piskelController.currentLayerIndex; var serializedRendering = [ this.getZoom(), + offset.x, + offset.y, + size.width, + size.height, currentFrameIndex, currentLayerIndex, layers.length ].join("-"); + if (this.serializedRendering != serializedRendering) { + console.log(serializedRendering); this.serializedRendering = serializedRendering; this.clear(); From 2248f41e68f2d679b8b38babf3c07b27caaebb81 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Sat, 2 Nov 2013 11:39:35 +0100 Subject: [PATCH 12/16] feature : zoom-level - Fixed bug with layer rendering when moving drawing offset (bad redraw-flag checking) --- js/rendering/layer/LayersRenderer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/rendering/layer/LayersRenderer.js b/js/rendering/layer/LayersRenderer.js index 0963ed05..dc43f038 100644 --- a/js/rendering/layer/LayersRenderer.js +++ b/js/rendering/layer/LayersRenderer.js @@ -38,7 +38,6 @@ if (this.serializedRendering != serializedRendering) { - console.log(serializedRendering); this.serializedRendering = serializedRendering; this.clear(); From 6e1ce724cb93f90fe6605097046af8bc1f629b07 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Tue, 5 Nov 2013 00:05:49 +0100 Subject: [PATCH 13/16] feature : zoom level - fix : removed duplicated code between ImageResizer and CanvasUtils (disabledImageSmoothing utility method) - added pskl.utils.UserAgent, basic user agent sniffer. I need it to sniff out IE10 for frame rendering (and it's not possible to feature detect here). Can check isChrome, isFirefox, isIE and get the version for each of them - added resizeNearestNeighbour in ImageResizer. Readapted from piskel website, this allows us to 1 - resize without AA on IE10, and 2 - add a pixel gap to reenable the GRID - finally : added back support for GRID ! - also extracted the 'zoomed out background color' as a constant in Constant.js --- js/Constants.js | 1 + js/rendering/frame/CachedFrameRenderer.js | 8 ++- js/rendering/frame/FrameRenderer.js | 44 +++++++++++------ js/rendering/layer/LayersRenderer.js | 1 + js/utils/CanvasUtils.js | 5 ++ js/utils/ImageResizer.js | 60 ++++++++++++++++++++--- js/utils/UserAgent.js | 20 ++++++++ piskel-script-list.js | 1 + 8 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 js/utils/UserAgent.js diff --git a/js/Constants.js b/js/Constants.js index 6ca8844e..294d347f 100644 --- a/js/Constants.js +++ b/js/Constants.js @@ -46,6 +46,7 @@ var Constants = { IMAGE_SERVICE_GET_URL : 'http://screenletstore.appspot.com/img/', GRID_STROKE_WIDTH: 1, + ZOOMED_OUT_BACKGROUND_COLOR : '#A0A0A0', LEFT_BUTTON : 'left_button_1', RIGHT_BUTTON : 'right_button_2', diff --git a/js/rendering/frame/CachedFrameRenderer.js b/js/rendering/frame/CachedFrameRenderer.js index 819f66ea..58b3477d 100644 --- a/js/rendering/frame/CachedFrameRenderer.js +++ b/js/rendering/frame/CachedFrameRenderer.js @@ -17,7 +17,13 @@ ns.CachedFrameRenderer.prototype.render = function (frame) { var offset = this.getOffset(); var size = this.getDisplaySize(); - var serializedFrame = [this.getZoom(), offset.x, offset.y, size.width, size.height, frame.serialize()].join('-'); + var serializedFrame = [ + this.getZoom(), + this.isGridEnabled(), + offset.x, offset.y, + size.width, size.height, + frame.serialize() + ].join('-'); if (this.serializedFrame != serializedFrame) { this.serializedFrame = serializedFrame; this.superclass.render.call(this, frame); diff --git a/js/rendering/frame/FrameRenderer.js b/js/rendering/frame/FrameRenderer.js index eb0eb03d..009a6079 100644 --- a/js/rendering/frame/FrameRenderer.js +++ b/js/rendering/frame/FrameRenderer.js @@ -37,6 +37,7 @@ y : 0 }; + this.isGridEnabled_ = false; this.supportGridRendering = renderingOptions.supportGridRendering; this.classes = classes || []; @@ -115,7 +116,7 @@ x : this.offset.x, y : this.offset.y }; - }, + }; ns.FrameRenderer.prototype.moveOffset = function (x, y) { this.setOffset(this.offset.x + x, this.offset.y + y); @@ -136,8 +137,11 @@ }; ns.FrameRenderer.prototype.setGridEnabled = function (flag) { - this.gridStrokeWidth = (flag && this.supportGridRendering) ? Constants.GRID_STROKE_WIDTH : 0; - this.canvasConfigDirty = true; + this.isGridEnabled_ = flag && this.supportGridRendering; + }; + + ns.FrameRenderer.prototype.isGridEnabled = function () { + return this.isGridEnabled_; }; ns.FrameRenderer.prototype.updateMargins_ = function () { @@ -197,7 +201,7 @@ x = x - this.margin.x; y = y - this.margin.y; - var cellSize = this.zoom + this.gridStrokeWidth; + var cellSize = this.zoom; // apply frame offset x = x + this.offset.x * cellSize; y = y + this.offset.y * cellSize; @@ -228,16 +232,28 @@ context = this.displayCanvas.getContext('2d'); context.save(); - // zoom < 1 - context.fillStyle = "#aaa"; - // zoom < 1 - context.fillRect(0,0,this.displayCanvas.width, this.displayCanvas.height); - context.translate(this.margin.x, this.margin.y); - context.scale(this.zoom, this.zoom); - context.translate(-this.offset.x, -this.offset.y); - // zoom < 1 - context.clearRect(0, 0, this.canvas.width, this.canvas.height); - context.drawImage(this.canvas, 0, 0); + + if (this.canvas.width*this.zoom < this.displayCanvas.width) { + context.fillStyle = Constants.ZOOMED_OUT_BACKGROUND_COLOR; + context.fillRect(0,0,this.displayCanvas.width, this.displayCanvas.height); + } + + context.translate( + this.margin.x-this.offset.x*this.zoom, + this.margin.y-this.offset.y*this.zoom + ); + + context.clearRect(0, 0, this.canvas.width*this.zoom, this.canvas.height*this.zoom); + + var isIE10 = pskl.utils.UserAgent.isIE && pskl.utils.UserAgent.version === 10; + if (this.isGridEnabled() || isIE10) { + var gridWidth = this.isGridEnabled() ? Constants.GRID_STROKE_WIDTH : 0; + var scaled = pskl.utils.ImageResizer.resizeNearestNeighbour(this.canvas, this.zoom, gridWidth); + context.drawImage(scaled, 0, 0); + } else { + context.scale(this.zoom, this.zoom); + context.drawImage(this.canvas, 0, 0); + } context.restore(); }; })(); \ No newline at end of file diff --git a/js/rendering/layer/LayersRenderer.js b/js/rendering/layer/LayersRenderer.js index dc43f038..69256309 100644 --- a/js/rendering/layer/LayersRenderer.js +++ b/js/rendering/layer/LayersRenderer.js @@ -27,6 +27,7 @@ var serializedRendering = [ this.getZoom(), + this.isGridEnabled(), offset.x, offset.y, size.width, diff --git a/js/utils/CanvasUtils.js b/js/utils/CanvasUtils.js index f65c4081..19cca6b3 100644 --- a/js/utils/CanvasUtils.js +++ b/js/utils/CanvasUtils.js @@ -32,6 +32,11 @@ if (canvas) { canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); } + }, + + getImageDataFromCanvas : function (canvas) { + var sourceContext = canvas.getContext('2d'); + return sourceContext.getImageData(0, 0, canvas.width, canvas.height).data; } }; })(); \ No newline at end of file diff --git a/js/utils/ImageResizer.js b/js/utils/ImageResizer.js index c3c06f45..b562ad9a 100644 --- a/js/utils/ImageResizer.js +++ b/js/utils/ImageResizer.js @@ -8,7 +8,7 @@ context.save(); if (!smoothingEnabled) { - this.disableSmoothingOnContext(context); + pskl.CanvasUtils.disableImageSmoothing(canvas); } context.translate(canvas.width / 2, canvas.height / 2); @@ -19,12 +19,58 @@ return canvas; }, - disableSmoothingOnContext : function (context) { - context.imageSmoothingEnabled = false; - context.mozImageSmoothingEnabled = false; - context.oImageSmoothingEnabled = false; - context.webkitImageSmoothingEnabled = false; - context.msImageSmoothingEnabled = false; + /** + * Manual implementation of resize using a nearest neighbour algorithm + * It is slower than relying on the native 'disabledImageSmoothing' available on CanvasRenderingContext2d. + * But it can be useful if : + * - IE < 11 (doesn't support msDisableImageSmoothing) + * - need to display a gap between pixel + * + * @param {Canvas2d} source original image to be resized, as a 2d canvas + * @param {Number} zoom ratio between desired dim / source dim + * @param {Number} margin gap to be displayed between pixels + * @return {Canvas2d} the resized canvas + */ + resizeNearestNeighbour : function (source, zoom, margin) { + margin = margin || 0; + var canvas = pskl.CanvasUtils.createCanvas(zoom*source.width, zoom*source.height); + var context = canvas.getContext('2d'); + + var imgData = pskl.CanvasUtils.getImageDataFromCanvas(source); + + var yRanges = {}, + xOffset = 0, + yOffset = 0, + xRange, + yRange; + // Draw the zoomed-up pixels to a different canvas context + for (var x = 0; x < source.width; x++) { + // Calculate X Range + xRange = (((x + 1) * zoom) | 0) - xOffset; + + for (var y = 0; y < source.height; y++) { + // Calculate Y Range + if (!yRanges[y + ""]) { + // Cache Y Range + yRanges[y + ""] = (((y + 1) * zoom) | 0) - yOffset; + } + yRange = yRanges[y + ""]; + + var i = (y * source.width + x) * 4; + var r = imgData[i]; + var g = imgData[i + 1]; + var b = imgData[i + 2]; + var a = imgData[i + 3]; + + context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + (a / 255) + ")"; + context.fillRect(xOffset, yOffset, xRange-margin, yRange-margin); + + yOffset += yRange; + } + yOffset = 0; + xOffset += xRange; + } + return canvas; } }; })(); \ No newline at end of file diff --git a/js/utils/UserAgent.js b/js/utils/UserAgent.js new file mode 100644 index 00000000..472beba3 --- /dev/null +++ b/js/utils/UserAgent.js @@ -0,0 +1,20 @@ +(function () { + var ns = $.namespace('pskl.utils'); + var ua = navigator.userAgent; + + ns.UserAgent = { + isIE : /MSIE/i.test( ua ), + isChrome : /Chrome/i.test( ua ), + isFirefox : /Firefox/i.test( ua ) + }; + + ns.UserAgent.version = (function () { + if (pskl.utils.UserAgent.isIE) { + return parseInt(/MSIE\s?(\d+)/i.exec( ua )[1], 10); + } else if (pskl.utils.UserAgent.isChrome) { + return parseInt(/Chrome\/(\d+)/i.exec( ua )[1], 10); + } else if (pskl.utils.UserAgent.isFirefox) { + return parseInt(/Firefox\/(\d+)/i.exec( ua )[1], 10); + } + })(); +})(); \ No newline at end of file diff --git a/piskel-script-list.js b/piskel-script-list.js index 813e1bec..1e0b3b85 100644 --- a/piskel-script-list.js +++ b/piskel-script-list.js @@ -13,6 +13,7 @@ exports.scripts = [ // Libraries "js/utils/core.js", + "js/utils/UserAgent.js", "js/utils/CanvasUtils.js", "js/utils/Math.js", "js/utils/FileUtils.js", From c8a8d470a7fda95c9c11d81970305ee68a8b7b9f Mon Sep 17 00:00:00 2001 From: jdescottes Date: Tue, 5 Nov 2013 22:11:47 +0100 Subject: [PATCH 14/16] renamed all references to dpi to zoom for consistency (also wtf dpi anyway !) --- js/controller/AnimatedPreviewController.js | 4 +-- js/controller/PreviewFilmController.js | 4 +-- js/controller/settings/GifExportController.js | 36 +++++++++---------- js/rendering/CanvasRenderer.js | 10 +++--- js/utils/PixelUtils.js | 20 +++++------ templates/settings/export-gif.html | 2 +- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/js/controller/AnimatedPreviewController.js b/js/controller/AnimatedPreviewController.js index c61e789a..4fec557a 100644 --- a/js/controller/AnimatedPreviewController.js +++ b/js/controller/AnimatedPreviewController.js @@ -1,6 +1,6 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.AnimatedPreviewController = function (piskelController, container, dpi) { + ns.AnimatedPreviewController = function (piskelController, container) { this.piskelController = piskelController; this.container = container; @@ -56,7 +56,7 @@ }; /** - * Calculate the preview DPI depending on the framesheet size + * Calculate the preview zoom depending on the framesheet size */ ns.AnimatedPreviewController.prototype.calculateZoom_ = function () { var frame = this.piskelController.getCurrentFrame(); diff --git a/js/controller/PreviewFilmController.js b/js/controller/PreviewFilmController.js index 64aa1925..a7bd781c 100644 --- a/js/controller/PreviewFilmController.js +++ b/js/controller/PreviewFilmController.js @@ -208,7 +208,7 @@ }; /** - * Calculate the preview DPI depending on the piskel size + * Calculate the preview zoom depending on the piskel size */ ns.PreviewFilmController.prototype.calculateZoom_ = function () { var curFrame = this.piskelController.getCurrentFrame(), @@ -219,6 +219,6 @@ var previewHeight = Constants.PREVIEW_FILM_SIZE * frameHeight / maxFrameDim; var previewWidth = Constants.PREVIEW_FILM_SIZE * frameWidth / maxFrameDim; - return pskl.PixelUtils.calculateDPI(previewHeight, previewWidth, frameHeight, frameWidth) || 1; + return pskl.PixelUtils.calculateZoom(previewHeight, previewWidth, frameHeight, frameWidth) || 1; }; })(); \ No newline at end of file diff --git a/js/controller/settings/GifExportController.js b/js/controller/settings/GifExportController.js index 42e57d3d..636272f4 100644 --- a/js/controller/settings/GifExportController.js +++ b/js/controller/settings/GifExportController.js @@ -8,18 +8,18 @@ /** * List of Resolutions applicable for Gif export * @static - * @type {Array} array of Objects {dpi:{Number}, default:{Boolean}} + * @type {Array} array of Objects {zoom:{Number}, default:{Boolean}} */ ns.GifExportController.RESOLUTIONS = [ { - 'dpi' : 1 + 'zoom' : 1 },{ - 'dpi' : 5 + 'zoom' : 5 },{ - 'dpi' : 10, + 'zoom' : 10, 'default' : true },{ - 'dpi' : 20 + 'zoom' : 20 } ]; @@ -37,11 +37,11 @@ ns.GifExportController.prototype.onUploadFormSubmit_ = function (evt) { evt.originalEvent.preventDefault(); - var selectedDpi = this.getSelectedDpi_(), + var selectedZoom = this.getSelectedZoom_(), fps = this.piskelController.getFPS(), - dpi = selectedDpi; + zoom = selectedZoom; - this.renderAsImageDataAnimatedGIF(dpi, fps, this.onGifRenderingCompleted_.bind(this)); + this.renderAsImageDataAnimatedGIF(zoom, fps, this.onGifRenderingCompleted_.bind(this)); }; ns.GifExportController.prototype.onGifRenderingCompleted_ = function (imageData) { @@ -59,15 +59,15 @@ this.previewContainerEl.innerHTML = "
"; }; - ns.GifExportController.prototype.getSelectedDpi_ = function () { - var radiosColl = this.uploadForm.get(0).querySelectorAll("[name=gif-dpi]"), + ns.GifExportController.prototype.getSelectedZoom_ = function () { + var radiosColl = this.uploadForm.get(0).querySelectorAll("[name=gif-zoom-level]"), radios = Array.prototype.slice.call(radiosColl,0); var selectedRadios = radios.filter(function(radio) {return !!radio.checked;}); if (selectedRadios.length == 1) { return selectedRadios[0].value; } else { - throw "Unexpected error when retrieving selected dpi"; + throw "Unexpected error when retrieving selected zoom"; } }; @@ -80,9 +80,9 @@ }; ns.GifExportController.prototype.createRadioForResolution_ = function (resolution) { - var dpi = resolution.dpi; - var label = dpi*this.piskelController.getWidth() + "x" + dpi*this.piskelController.getHeight(); - var value = dpi; + var zoom = resolution.zoom; + var label = zoom*this.piskelController.getWidth() + "x" + zoom*this.piskelController.getHeight(); + var value = zoom; var radioHTML = pskl.utils.Template.replace(this.radioTemplate_, {value : value, label : label}); var radioEl = pskl.utils.Template.createFromHTML(radioHTML); @@ -104,17 +104,17 @@ reader.readAsDataURL(blob); }; - ns.GifExportController.prototype.renderAsImageDataAnimatedGIF = function(dpi, fps, cb) { + ns.GifExportController.prototype.renderAsImageDataAnimatedGIF = function(zoom, fps, cb) { var gif = new window.GIF({ workers: 2, quality: 10, - width: this.piskelController.getWidth()*dpi, - height: this.piskelController.getHeight()*dpi + width: this.piskelController.getWidth()*zoom, + height: this.piskelController.getHeight()*zoom }); for (var i = 0; i < this.piskelController.getFrameCount(); i++) { var frame = this.piskelController.getFrameAt(i); - var renderer = new pskl.rendering.CanvasRenderer(frame, dpi); + var renderer = new pskl.rendering.CanvasRenderer(frame, zoom); gif.addFrame(renderer.render(), { delay: 1000 / fps }); diff --git a/js/rendering/CanvasRenderer.js b/js/rendering/CanvasRenderer.js index 2153f207..76414535 100644 --- a/js/rendering/CanvasRenderer.js +++ b/js/rendering/CanvasRenderer.js @@ -1,9 +1,9 @@ (function () { var ns = $.namespace("pskl.rendering"); - ns.CanvasRenderer = function (frame, dpi) { + ns.CanvasRenderer = function (frame, zoom) { this.frame = frame; - this.dpi = dpi; + this.zoom = zoom; this.transparentColor_ = 'white'; }; @@ -36,12 +36,12 @@ } context.fillStyle = color; - context.fillRect(col * this.dpi, row * this.dpi, this.dpi, this.dpi); + context.fillRect(col * this.zoom, row * this.zoom, this.zoom, this.zoom); }; ns.CanvasRenderer.prototype.createCanvas_ = function () { - var width = this.frame.getWidth() * this.dpi; - var height = this.frame.getHeight() * this.dpi; + var width = this.frame.getWidth() * this.zoom; + var height = this.frame.getHeight() * this.zoom; return pskl.CanvasUtils.createCanvas(width, height); }; })(); \ No newline at end of file diff --git a/js/utils/PixelUtils.js b/js/utils/PixelUtils.js index c0c6973f..767e8b74 100644 --- a/js/utils/PixelUtils.js +++ b/js/utils/PixelUtils.js @@ -147,31 +147,31 @@ }, /** - * Calculate and return the maximal DPI to display a picture in a given container. + * Calculate and return the maximal zoom level to display a picture in a given container. * * @param container jQueryObject Container where the picture should be displayed * @param number pictureHeight height in pixels of the picture to display * @param number pictureWidth width in pixels of the picture to display - * @return number maximal dpi + * @return number maximal zoom */ - calculateDPIForContainer : function (container, pictureHeight, pictureWidth) { - return this.calculateDPI(container.height(), container.width(), pictureHeight, pictureWidth); + calculateZoomForContainer : function (container, pictureHeight, pictureWidth) { + return this.calculateZoom(container.height(), container.width(), pictureHeight, pictureWidth); }, /** - * Calculate and return the maximal DPI to display a picture for a given height and width. + * Calculate and return the maximal zoom to display a picture for a given height and width. * * @param height number Height available to display the picture * @param width number Width available to display the picture * @param number pictureHeight height in pixels of the picture to display * @param number pictureWidth width in pixels of the picture to display - * @return number maximal dpi + * @return number maximal zoom */ - calculateDPI : function (height, width, pictureHeight, pictureWidth) { - var heightBoundDpi = Math.floor(height / pictureHeight), - widthBoundDpi = Math.floor(width / pictureWidth); + calculateZoom : function (height, width, pictureHeight, pictureWidth) { + var heightRatio = Math.floor(height / pictureHeight), + widthRatio = Math.floor(width / pictureWidth); - return Math.min(heightBoundDpi, widthBoundDpi); + return Math.min(heightRatio, widthRatio); } }; })(); \ No newline at end of file diff --git a/templates/settings/export-gif.html b/templates/settings/export-gif.html index 95e1587d..516f237a 100644 --- a/templates/settings/export-gif.html +++ b/templates/settings/export-gif.html @@ -6,7 +6,7 @@
From 3af64d3f451d1023934f2040b7f3a1e30cf99a2b Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 15 Nov 2013 00:32:18 +0100 Subject: [PATCH 15/16] feature : zoom level : code review + added explanatory comment for CanvasUtils.disableImageSmoothing + detect browser for chosing wheel/mousewheel event in DrawingController + create ABSTRACT_FUNCTION constant to be reused forabstract methods --- js/Constants.js | 4 +++- js/controller/DrawingController.js | 8 ++++++-- js/rendering/AbstractRenderer.js | 22 +++++++++++----------- js/utils/CanvasUtils.js | 7 +++++++ 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/js/Constants.js b/js/Constants.js index 294d347f..1b7f8569 100644 --- a/js/Constants.js +++ b/js/Constants.js @@ -50,5 +50,7 @@ var Constants = { LEFT_BUTTON : 'left_button_1', RIGHT_BUTTON : 'right_button_2', - MOUSEMOVE_THROTTLING : 10 + MOUSEMOVE_THROTTLING : 10, + + ABSTRACT_FUNCTION : function () {throw 'abstract method should be implemented';} }; \ No newline at end of file diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index 417e0b44..f4eb5ba4 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -81,8 +81,12 @@ var body = $('body'); this.container.mousedown($.proxy(this.onMousedown_, this)); this.container.mousemove($.proxy(this.onMousemove_, this)); - this.container.on('mousewheel', $.proxy(this.onMousewheel_, this)); - this.container.on('wheel', $.proxy(this.onMousewheel_, this)); + + if (pskl.utils.UserAgent.isChrome) { + this.container.on('mousewheel', $.proxy(this.onMousewheel_, this)); + } else { + this.container.on('wheel', $.proxy(this.onMousewheel_, this)); + } body.mouseup($.proxy(this.onMouseup_, this)); diff --git a/js/rendering/AbstractRenderer.js b/js/rendering/AbstractRenderer.js index a3539402..6254ee39 100644 --- a/js/rendering/AbstractRenderer.js +++ b/js/rendering/AbstractRenderer.js @@ -3,20 +3,20 @@ ns.AbstractRenderer = function () {}; - ns.AbstractRenderer.prototype.clear = function () {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.clear = Constants.ABSTRACT_FUNCTION; - ns.AbstractRenderer.prototype.getCoordinates = function (x, y) {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.getCoordinates = Constants.ABSTRACT_FUNCTION; - ns.AbstractRenderer.prototype.setGridEnabled = function (b) {throw 'abstract method should be implemented';}; - ns.AbstractRenderer.prototype.isGridEnabled = function () {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.setGridEnabled = Constants.ABSTRACT_FUNCTION; + ns.AbstractRenderer.prototype.isGridEnabled = Constants.ABSTRACT_FUNCTION; - ns.AbstractRenderer.prototype.setZoom = function (zoom) {throw 'abstract method should be implemented';}; - ns.AbstractRenderer.prototype.getZoom = function () {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.setZoom = Constants.ABSTRACT_FUNCTION; + ns.AbstractRenderer.prototype.getZoom = Constants.ABSTRACT_FUNCTION; - ns.AbstractRenderer.prototype.moveOffset = function (x, y) {throw 'abstract method should be implemented';}; - ns.AbstractRenderer.prototype.setOffset = function (x, y) {throw 'abstract method should be implemented';}; - ns.AbstractRenderer.prototype.getOffset = function () {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.moveOffset = Constants.ABSTRACT_FUNCTION; + ns.AbstractRenderer.prototype.setOffset = Constants.ABSTRACT_FUNCTION; + ns.AbstractRenderer.prototype.getOffset = Constants.ABSTRACT_FUNCTION; - ns.AbstractRenderer.prototype.setDisplaySize = function (w, h) {throw 'abstract method should be implemented';}; - ns.AbstractRenderer.prototype.getDisplaySize = function () {throw 'abstract method should be implemented';}; + ns.AbstractRenderer.prototype.setDisplaySize = Constants.ABSTRACT_FUNCTION; + ns.AbstractRenderer.prototype.getDisplaySize = Constants.ABSTRACT_FUNCTION; })(); \ No newline at end of file diff --git a/js/utils/CanvasUtils.js b/js/utils/CanvasUtils.js index 19cca6b3..be3e041d 100644 --- a/js/utils/CanvasUtils.js +++ b/js/utils/CanvasUtils.js @@ -19,6 +19,13 @@ return canvas; }, + /** + * By default, all scaling operations on a Canvas 2D Context are performed using antialiasing. + * Resizing a 32x32 image to 320x320 will lead to a blurry output. + * On Chrome, FF and IE>=11, this can be disabled by setting a property on the Canvas 2D Context. + * In this case the browser will use a nearest-neighbor scaling. + * @param {Canvas} canvas + */ disableImageSmoothing : function (canvas) { var context = canvas.getContext('2d'); context.imageSmoothingEnabled = false; From e68ae7f31df3927c832d28c77c5bff3715091374 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Fri, 15 Nov 2013 00:39:43 +0100 Subject: [PATCH 16/16] feature : zoom level : code review + Switched from BITWISE OR 0 (x | 0) to Math.floor for readability --- js/rendering/frame/FrameRenderer.js | 4 ++-- js/utils/ImageResizer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/rendering/frame/FrameRenderer.js b/js/rendering/frame/FrameRenderer.js index 009a6079..09476e59 100644 --- a/js/rendering/frame/FrameRenderer.js +++ b/js/rendering/frame/FrameRenderer.js @@ -207,8 +207,8 @@ y = y + this.offset.y * cellSize; return { - x : (x / cellSize) | 0, - y : (y / cellSize) | 0 + x : Math.floor(x / cellSize), + y : Math.floor(y / cellSize) }; }; diff --git a/js/utils/ImageResizer.js b/js/utils/ImageResizer.js index b562ad9a..1734bb3c 100644 --- a/js/utils/ImageResizer.js +++ b/js/utils/ImageResizer.js @@ -46,13 +46,13 @@ // Draw the zoomed-up pixels to a different canvas context for (var x = 0; x < source.width; x++) { // Calculate X Range - xRange = (((x + 1) * zoom) | 0) - xOffset; + xRange = Math.floor((x + 1) * zoom) - xOffset; for (var y = 0; y < source.height; y++) { // Calculate Y Range if (!yRanges[y + ""]) { // Cache Y Range - yRanges[y + ""] = (((y + 1) * zoom) | 0) - yOffset; + yRanges[y + ""] = Math.floor((y + 1) * zoom) - yOffset; } yRange = yRanges[y + ""];