diff --git a/src/js/app.js b/src/js/app.js index 0e7b8defe..d39499e59 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -203,9 +203,14 @@ }, getFirstFrameAsPng : function () { - var firstFrame = this.piskelController.getMergedFrameAt(0); - var firstFrameCanvas = pskl.utils.FrameUtils.toImage(firstFrame); - return firstFrameCanvas.toDataURL('image/png'); + var frame = pskl.utils.LayerUtils.mergeFrameAt(this.piskelController.getLayers(), 0); + var canvas; + if (frame instanceof pskl.model.frame.RenderedFrame) { + canvas = pskl.utils.CanvasUtils.createFromImage(frame.getRenderedFrame()); + } else { + canvas = pskl.utils.FrameUtils.toImage(frame); + } + return canvas.toDataURL('image/png'); }, getFramesheetAsPng : function () { diff --git a/src/js/controller/piskel/PiskelController.js b/src/js/controller/piskel/PiskelController.js index d2b007c8f..6d3230e78 100644 --- a/src/js/controller/piskel/PiskelController.js +++ b/src/js/controller/piskel/PiskelController.js @@ -90,17 +90,10 @@ return this.piskel; }; - ns.PiskelController.prototype.getMergedFrameAt = function (index) { - var hash = []; - var frames = this.getLayers().map(function (l) { - var frame = l.getFrameAt(index); - hash.push(frame.getHash()); - return frame; + ns.PiskelController.prototype.isTransparent = function () { + return this.getLayers().some(function (l) { + return l.isTransparent(); }); - var mergedFrame = pskl.utils.FrameUtils.merge(frames); - mergedFrame.id = hash.join('-'); - mergedFrame.version = 0; - return mergedFrame; }; ns.PiskelController.prototype.renderFrameAt = function (index, preserveOpacity) { diff --git a/src/js/controller/preview/PreviewController.js b/src/js/controller/preview/PreviewController.js index dd252e703..6efcaeec5 100644 --- a/src/js/controller/preview/PreviewController.js +++ b/src/js/controller/preview/PreviewController.js @@ -168,9 +168,9 @@ ns.PreviewController.prototype.render = function (delta) { this.elapsedTime += delta; var index = this.getNextIndex_(delta); - if (this.shoudlRender_() || this.currentIndex != index) { + if (this.shouldRender_() || this.currentIndex != index) { this.currentIndex = index; - var frame = this.piskelController.getMergedFrameAt(this.currentIndex); + var frame = pskl.utils.LayerUtils.mergeFrameAt(this.piskelController.getLayers(), index); this.renderer.render(frame); this.renderFlag = false; @@ -240,7 +240,7 @@ this.renderFlag = bool; }; - ns.PreviewController.prototype.shoudlRender_ = function () { + ns.PreviewController.prototype.shouldRender_ = function () { return this.renderFlag || this.popupPreviewController.renderFlag; }; diff --git a/src/js/controller/settings/exportimage/GifExportController.js b/src/js/controller/settings/exportimage/GifExportController.js index cb4c061a9..f74a5148e 100644 --- a/src/js/controller/settings/exportimage/GifExportController.js +++ b/src/js/controller/settings/exportimage/GifExportController.js @@ -105,11 +105,9 @@ ns.GifExportController.prototype.renderAsImageDataAnimatedGIF = function(zoom, fps, cb) { var currentColors = pskl.app.currentColorsService.getCurrentColors(); - var hasTransparency = this.piskelController.getLayers().some(function (l) { - var opacity = l.getOpacity(); - return opacity > 0 && opacity < 1; - }); - var preserveColors = !hasTransparency && currentColors.length < MAX_GIF_COLORS; + var layers = this.piskelController.getLayers(); + var isTransparent = layers.some(function (l) {return l.isTransparent();}); + var preserveColors = !isTransparent && currentColors.length < MAX_GIF_COLORS; var transparentColor, transparent; // transparency only supported if preserveColors is true, see Issue #357 diff --git a/src/js/model/Layer.js b/src/js/model/Layer.js index fdf947278..98019ada7 100644 --- a/src/js/model/Layer.js +++ b/src/js/model/Layer.js @@ -43,6 +43,10 @@ this.opacity = opacity; }; + ns.Layer.prototype.isTransparent = function () { + return this.opacity > 0 && this.opacity < 1; + }; + ns.Layer.prototype.getFrames = function () { return this.frames; }; diff --git a/src/js/model/frame/AsyncCachedFrameProcessor.js b/src/js/model/frame/AsyncCachedFrameProcessor.js index 14c92520a..f1854cecb 100644 --- a/src/js/model/frame/AsyncCachedFrameProcessor.js +++ b/src/js/model/frame/AsyncCachedFrameProcessor.js @@ -30,6 +30,10 @@ var key1 = frame.getHash(); if (cache[key1]) { processedFrame = cache[key1]; + } else if (frame instanceof pskl.model.frame.RenderedFrame) { + // Cannot use 2nd level cache with rendered frames + var callbackFirstLvlCacheOnly = this.onProcessorComplete_.bind(this, deferred, cache, key1, key1); + this.frameProcessor(frame, callbackFirstLvlCacheOnly); } else { var framePixels = JSON.stringify(frame.getPixels()); var key2 = pskl.utils.hashCode(framePixels); diff --git a/src/js/model/frame/CachedFrameProcessor.js b/src/js/model/frame/CachedFrameProcessor.js index 27df5ff67..5a1ff57c0 100644 --- a/src/js/model/frame/CachedFrameProcessor.js +++ b/src/js/model/frame/CachedFrameProcessor.js @@ -66,6 +66,10 @@ var cacheKey = frame.getHash(); if (cache[cacheKey]) { processedFrame = cache[cacheKey]; + } else if (frame instanceof pskl.model.frame.RenderedFrame) { + // Cannot use 2nd level cache with rendered frames + processedFrame = this.frameProcessor(frame); + cache[cacheKey] = processedFrame; } else { var framePixels = JSON.stringify(frame.getPixels()); var frameAsString = pskl.utils.hashCode(framePixels); diff --git a/src/js/model/frame/RenderedFrame.js b/src/js/model/frame/RenderedFrame.js new file mode 100644 index 000000000..47a75ca4c --- /dev/null +++ b/src/js/model/frame/RenderedFrame.js @@ -0,0 +1,49 @@ +(function () { + var ns = $.namespace('pskl.model.frame'); + + /** + * Create a frame instance that provides an image getter. Can be faster + * to use after merging using transparency. Transparent frames are merged to + * an image and this allows to reuse the image rather than retransform into + * a frame before calling the renderers. + * + * This rendered frame should only be used with renderers that support it. + * + * @param {Function} imageFn getter that will create the image + * @param {Number} width image width in pixels + * @param {Number} height image height in pixels + * @param {String} id will be used as hash, so should be as unique as possible + */ + ns.RenderedFrame = function (renderFn, width, height, id) { + this.width = width; + this.height = height; + this.id = id; + this.renderFn = renderFn; + }; + + ns.RenderedFrame.prototype.getRenderedFrame = function () { + return this.renderFn(); + }; + + ns.RenderedFrame.prototype.getHash = function () { + return this.id; + }; + + ns.RenderedFrame.prototype.getWidth = function () { + return this.width; + }; + + ns.RenderedFrame.prototype.getHeight = function () { + return this.height; + }; + + ns.RenderedFrame.prototype.getPixels = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.containsPixel = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.isSameSize = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.clone = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.setPixels = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.clear = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.setPixel = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.getPixel = Constants.ABSTRACT_FUNCTION; + ns.RenderedFrame.prototype.forEachPixel = Constants.ABSTRACT_FUNCTION; +})(); diff --git a/src/js/rendering/frame/BackgroundImageFrameRenderer.js b/src/js/rendering/frame/BackgroundImageFrameRenderer.js index f03e18275..96f6ff74a 100644 --- a/src/js/rendering/frame/BackgroundImageFrameRenderer.js +++ b/src/js/rendering/frame/BackgroundImageFrameRenderer.js @@ -16,7 +16,12 @@ }; ns.BackgroundImageFrameRenderer.prototype.frameToDataUrl_ = function (frame) { - var canvas = new pskl.utils.FrameUtils.toImage(frame, this.zoom); + var canvas; + if (frame instanceof pskl.model.frame.RenderedFrame) { + canvas = pskl.utils.ImageResizer.scale(frame.getRenderedFrame(), this.zoom); + } else { + canvas = pskl.utils.FrameUtils.toImage(frame, this.zoom); + } return canvas.toDataURL('image/png'); }; diff --git a/src/js/rendering/layer/LayersRenderer.js b/src/js/rendering/layer/LayersRenderer.js index d7a5688d9..1bfe8fbdb 100644 --- a/src/js/rendering/layer/LayersRenderer.js +++ b/src/js/rendering/layer/LayersRenderer.js @@ -32,11 +32,11 @@ var offset = this.getOffset(); var size = this.getDisplaySize(); var layers = this.piskelController.getLayers(); - var currentFrameIndex = this.piskelController.getCurrentFrameIndex(); - var currentLayerIndex = this.piskelController.getCurrentLayerIndex(); + var frameIndex = this.piskelController.getCurrentFrameIndex(); + var layerIndex = this.piskelController.getCurrentLayerIndex(); - var belowLayers = layers.slice(0, currentLayerIndex); - var aboveLayers = layers.slice(currentLayerIndex + 1, layers.length); + var belowLayers = layers.slice(0, layerIndex); + var aboveLayers = layers.slice(layerIndex + 1, layers.length); var serializedRendering = [ this.getZoom(), @@ -45,8 +45,8 @@ offset.y, size.width, size.height, - this.getHashForLayersAt_(currentFrameIndex, belowLayers), - this.getHashForLayersAt_(currentFrameIndex, aboveLayers), + pskl.utils.LayerUtils.getFrameHashAt(belowLayers, frameIndex), + pskl.utils.LayerUtils.getFrameHashAt(aboveLayers, frameIndex), layers.length ].join('-'); @@ -56,12 +56,12 @@ this.clear(); if (belowLayers.length > 0) { - var belowFrame = this.getFrameForLayersAt_(currentFrameIndex, belowLayers); + var belowFrame = pskl.utils.LayerUtils.mergeFrameAt(belowLayers, frameIndex); this.belowRenderer.render(belowFrame); } if (aboveLayers.length > 0) { - var aboveFrame = this.getFrameForLayersAt_(currentFrameIndex, aboveLayers); + var aboveFrame = pskl.utils.LayerUtils.mergeFrameAt(aboveLayers, frameIndex); this.aboveRenderer.render(aboveFrame); } } @@ -80,20 +80,6 @@ } }; - ns.LayersRenderer.prototype.getFrameForLayersAt_ = function (frameIndex, layers) { - var frames = layers.map(function (l) { - return l.getFrameAt(frameIndex); - }); - return pskl.utils.FrameUtils.merge(frames); - }; - - ns.LayersRenderer.prototype.getHashForLayersAt_ = function (frameIndex, layers) { - var hash = layers.map(function (l) { - return l.getFrameAt(frameIndex).getHash(); - }); - return hash.join('-'); - }; - ns.LayersRenderer.prototype.onUserSettingsChange_ = function (evt, settingsName, settingsValue) { if (settingsName == pskl.UserSettings.LAYER_OPACITY) { this.updateLayersCanvasOpacity_(settingsValue); diff --git a/src/js/utils/FrameUtils.js b/src/js/utils/FrameUtils.js index a3f0189cd..bc5d97b2e 100644 --- a/src/js/utils/FrameUtils.js +++ b/src/js/utils/FrameUtils.js @@ -22,37 +22,43 @@ /** * Draw the provided frame in a 2d canvas * - * @param {Frame} frame the frame to draw + * @param {Frame|RenderedFrame} frame the frame to draw * @param {Canvas} canvas the canvas target * @param {String} transparentColor (optional) color to use to represent transparent pixels. - * @param {String} opacity (optional) global frame opacity + * @param {String} globalAlpha (optional) global frame opacity */ - drawToCanvas : function (frame, canvas, transparentColor, opacity) { + drawToCanvas : function (frame, canvas, transparentColor, globalAlpha) { var context = canvas.getContext('2d'); - opacity = isNaN(opacity) ? 1 : opacity; - context.globalAlpha = opacity; - + globalAlpha = isNaN(globalAlpha) ? 1 : globalAlpha; + context.globalAlpha = globalAlpha; transparentColor = transparentColor || Constants.TRANSPARENT_COLOR; - 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); - - // accumulate all the pixels of the same color to speed up rendering - // by reducting fillRect calls - var w = 1; - while (color === frame.getPixel(x, y + w) && (y + w) < height) { - w++; - } - if (color == Constants.TRANSPARENT_COLOR) { - color = transparentColor; + if (frame instanceof pskl.model.frame.RenderedFrame) { + context.fillRect(transparentColor, 0, 0, frame.getWidth(), frame.getHeight()); + context.drawImage(frame.getRenderedFrame(), 0, 0); + } else { + 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); + + // accumulate all the pixels of the same color to speed up rendering + // by reducting fillRect calls + var w = 1; + while (color === frame.getPixel(x, y + w) && (y + w) < height) { + w++; + } + + if (color == Constants.TRANSPARENT_COLOR) { + color = transparentColor; + } + + pskl.utils.FrameUtils.renderLine_(color, x, y, w, context); + y = y + w - 1; } - - pskl.utils.FrameUtils.renderLine_(color, x, y, w, context); - y = y + w - 1; } + + context.globalAlpha = 1; } - context.globalAlpha = 1; }, /** @@ -98,12 +104,16 @@ }, /* - * Create a pskl.model.Frame from an Image object. - * Transparent pixels will either be converted to completely opaque or completely transparent pixels. + * Create a pskl.model.Frame from an Image object. By default transparent + * pixels will be converted to completely opaque or completely transparent + * pixels. If preserveOpacity is true the actual opacity of the pixel will + * be used and the generated frame will contain rgba pixels. + * * @param {Image} image source image + * @param {boolean} preserveOpacity set to true to preserve the opacity * @return {pskl.model.Frame} corresponding frame */ - createFromImage : function (image) { + createFromImage : function (image, preserveOpacity) { var w = image.width; var h = image.height; var canvas = pskl.utils.CanvasUtils.createCanvas(w, h); @@ -111,10 +121,10 @@ context.drawImage(image, 0, 0, w, h, 0, 0, w, h); var imgData = context.getImageData(0, 0, w, h).data; - return pskl.utils.FrameUtils.createFromImageData_(imgData, w, h); + return pskl.utils.FrameUtils.createFromImageData_(imgData, w, h, preserveOpacity); }, - createFromImageData_ : function (imageData, width, height) { + createFromImageData_ : function (imageData, width, height, preserveOpacity) { // Draw the zoomed-up pixels to a different canvas context var grid = []; for (var x = 0 ; x < width ; x++) { @@ -126,10 +136,18 @@ var g = imageData[i + 1]; var b = imageData[i + 2]; var a = imageData[i + 3]; - if (a < 125) { - grid[x][y] = Constants.TRANSPARENT_COLOR; + if (preserveOpacity) { + if (a === 0) { + grid[x][y] = Constants.TRANSPARENT_COLOR; + } else { + grid[x][y] = 'rgba(' + [r, g, b, a / 255].join(',') + ')'; + } } else { - grid[x][y] = pskl.utils.rgbToHex(r, g, b); + if (a < 125) { + grid[x][y] = Constants.TRANSPARENT_COLOR; + } else { + grid[x][y] = pskl.utils.rgbToHex(r, g, b); + } } } } diff --git a/src/js/utils/LayerUtils.js b/src/js/utils/LayerUtils.js index e05ab37c7..bb9673724 100644 --- a/src/js/utils/LayerUtils.js +++ b/src/js/utils/LayerUtils.js @@ -42,6 +42,52 @@ return mergedLayer; }, + getFrameHashAt : function (layers, index) { + var hashBuffer = []; + layers.forEach(function (l) { + var frame = l.getFrameAt(index); + hashBuffer.push(frame.getHash()); + hashBuffer.push(l.getOpacity()); + return frame; + }); + return hashBuffer.join('-'); + }, + + /** + * Create a frame instance merging all the frames from the layers array at + * the provided index. + * + * @param {Array} layers array of layers to use + * @param {Number} index frame index to merge + * @return {Frame} Frame instance (can be a fake frame when using + * transparency) + */ + mergeFrameAt : function (layers, index) { + var isTransparent = layers.some(function (l) {return l.isTransparent();}); + if (isTransparent) { + return pskl.utils.LayerUtils.mergeTransparentFrameAt_(layers, index); + } else { + return pskl.utils.LayerUtils.mergeOpaqueFrameAt_(layers, index); + } + }, + + mergeTransparentFrameAt_ : function (layers, index) { + var hash = pskl.utils.LayerUtils.getFrameHashAt(layers, index); + var width = layers[0].frames[0].getWidth(); + var height = layers[0].frames[0].getHeight(); + var renderFn = function () {return pskl.utils.LayerUtils.flattenFrameAt(layers, index, true);}; + return new pskl.model.frame.RenderedFrame(renderFn, width, height, hash); + }, + + mergeOpaqueFrameAt_ : function (layers, index) { + var hash = pskl.utils.LayerUtils.getFrameHashAt(layers, index); + var frames = layers.map(function(l) {return l.getFrameAt(index);}); + var mergedFrame = pskl.utils.FrameUtils.merge(frames); + mergedFrame.id = hash; + mergedFrame.version = 0; + return mergedFrame; + }, + renderFrameAt : function (layer, index, preserveOpacity) { var opacity = preserveOpacity ? layer.getOpacity() : 1; var frame = layer.getFrameAt(index); diff --git a/src/js/utils/serialization/Deserializer.js b/src/js/utils/serialization/Deserializer.js index d42408649..01db4abe0 100644 --- a/src/js/utils/serialization/Deserializer.js +++ b/src/js/utils/serialization/Deserializer.js @@ -56,19 +56,16 @@ // 3 - set the source of the image image.src = base64PNG; - - // 4 - return a pointer to the new layer instance return layer; }; ns.Deserializer.prototype.loadExpandedLayer = function (layerData, index) { var layer = new pskl.model.Layer(layerData.name); + layer.setOpacity(layerData.opacity); var frames = layerData.grids.map(function (grid) { return pskl.model.Frame.fromPixelGrid(grid); }); this.addFramesToLayer(frames, layer, index); - - // 4 - return a pointer to the new layer instance return layer; }; diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index c9929859a..144895b7e 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -58,7 +58,7 @@ // Promises "js/lib/q.js", - // Application libraries--> + // Application libraries "js/rendering/DrawingLoop.js", // Models @@ -67,6 +67,7 @@ "js/model/piskel/Descriptor.js", "js/model/frame/CachedFrameProcessor.js", "js/model/frame/AsyncCachedFrameProcessor.js", + "js/model/frame/RenderedFrame.js", "js/model/Palette.js", "js/model/Piskel.js",