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",