From 66c941dd253980176fcfc9855dcd6aeecf478676 Mon Sep 17 00:00:00 2001 From: juliandescottes Date: Sun, 18 Dec 2016 11:36:11 +0100 Subject: [PATCH] support chunked layerData in regular deserializer --- Gruntfile.js | 6 +- karma.conf.js | 6 +- package.json | 3 +- src/js/utils/FrameUtils.js | 108 ++++++++---------- src/js/utils/LayerUtils.js | 28 ----- src/js/utils/serialization/Deserializer.js | 82 +++++++------ .../arraybuffer/ArrayBufferDeserializer.js | 2 +- .../arraybuffer/ArrayBufferSerializer.js | 2 +- test/js/utils/FrameUtilsTest.js | 2 +- 9 files changed, 107 insertions(+), 132 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 10d5fc8..f03071d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -90,13 +90,15 @@ module.exports = function(grunt) { browser : true, trailing : true, curly : true, - globals : {'$':true, 'jQuery' : true, 'pskl':true, 'Events':true, 'Constants':true, 'console' : true, 'module':true, 'require':true, 'Q':true} + globals : {'$':true, 'jQuery' : true, 'pskl':true, 'Events':true, 'Constants':true, 'console' : true, 'module':true, 'require':true, 'Q':true, 'Promise': true} }, files: [ + // Includes 'Gruntfile.js', 'package.json', 'src/js/**/*.js', - '!src/js/**/lib/**/*.js' // Exclude lib folder (note the leading !) + // Excludes + '!src/js/**/lib/**/*.js' ] }, diff --git a/karma.conf.js b/karma.conf.js index a54f4be..5cfd162 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -11,7 +11,7 @@ module.exports = function(config) { // Polyfill for Object.assign (missing in PhantomJS) piskelScripts.push('./node_modules/phantomjs-polyfill-object-assign/object-assign-polyfill.js'); - + config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) @@ -24,7 +24,9 @@ module.exports = function(config) { // list of files / patterns to load in the browser - files: piskelScripts, + files: piskelScripts.concat([ + './node_modules/promise-polyfill/promise.js' + ]), // list of files to exclude diff --git a/package.json b/package.json index 44858ca..818e34a 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "karma-phantomjs-launcher": "0.2.3", "load-grunt-tasks": "3.5.0", "phantomjs": "2.1.7", - "phantomjs-polyfill-object-assign": "0.0.2" + "phantomjs-polyfill-object-assign": "0.0.2", + "promise-polyfill": "6.0.2" }, "window": { "title": "Piskel", diff --git a/src/js/utils/FrameUtils.js b/src/js/utils/FrameUtils.js index 810f0c3..c1f6bd6 100644 --- a/src/js/utils/FrameUtils.js +++ b/src/js/utils/FrameUtils.js @@ -177,74 +177,60 @@ }, /** - * 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 + * Create a Frame array from an Image object. + * Transparent pixels will either be converted to completely opaque or completely transparent pixels. + * + * @param {Image} image source image + * @param {Number} frameCount number of frames in the spritesheet + * @return {Array} */ - 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; + createFramesFromSpritesheet : function (image, frameCount) { + var layout = []; + for (var i = 0 ; i < frameCount ; i++) { + layout.push([i]); } - 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 + ')'; + var chunkFrames = pskl.utils.FrameUtils.createFramesFromChunk(image, layout); + return chunkFrames.map(function (chunkFrame) { + return chunkFrame.frame; + }); }, /** - * 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} + * Create a Frame array from an Image object. + * Transparent pixels will either be converted to completely opaque or completely transparent pixels. + * + * @param {Image} image source image + * @param {Array } layout description of the frame indexes expected to be found in the chunk + * @return {Array} array of objects containing: {index: frame index, frame: frame instance} */ - toRgba__ : function (c) { - if (colorCache[c]) { - return colorCache[c]; + createFramesFromChunk : function (image, layout) { + var width = image.width; + var height = image.height; + + // Recalculate the expected frame dimensions from the layout information + var frameWidth = width / layout.length; + var frameHeight = height / layout[0].length; + + // Create a canvas adapted to the image size + var canvas = pskl.utils.CanvasUtils.createCanvas(frameWidth, frameHeight); + var context = canvas.getContext('2d'); + + // Draw the zoomed-up pixels to a different canvas context + var chunkFrames = []; + for (var i = 0 ; i < layout.length ; i++) { + var row = layout[i]; + for (var j = 0 ; j < row.length ; j++) { + context.clearRect(0, 0 , frameWidth, frameHeight); + context.drawImage(image, frameWidth * i, frameHeight * j, frameWidth, frameHeight, 0, 0, frameWidth, height); + var frame = pskl.utils.FrameUtils.createFromCanvas(canvas, 0, 0, frameWidth, height); + chunkFrames.push({ + index : layout[i][j], + frame : frame + }); + } } - 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; + + return chunkFrames; } }; })(); diff --git a/src/js/utils/LayerUtils.js b/src/js/utils/LayerUtils.js index 5f4d713..08a4668 100644 --- a/src/js/utils/LayerUtils.js +++ b/src/js/utils/LayerUtils.js @@ -2,34 +2,6 @@ var ns = $.namespace('pskl.utils'); ns.LayerUtils = { - /** - * Create a Frame array from an Image object. - * Transparent pixels will either be converted to completely opaque or completely transparent pixels. - * TODO : move to FrameUtils - * - * @param {Image} image source image - * @param {Number} frameCount number of frames in the spritesheet - * @return {Array} - */ - createFramesFromSpritesheet : function (image, frameCount) { - var width = image.width; - var height = image.height; - var frameWidth = width / frameCount; - - var canvas = pskl.utils.CanvasUtils.createCanvas(frameWidth, height); - var context = canvas.getContext('2d'); - - // Draw the zoomed-up pixels to a different canvas context - var frames = []; - for (var i = 0 ; i < frameCount ; i++) { - context.clearRect(0, 0 , frameWidth, height); - context.drawImage(image, frameWidth * i, 0, frameWidth, height, 0, 0, frameWidth, height); - var frame = pskl.utils.FrameUtils.createFromCanvas(canvas, 0, 0, frameWidth, height); - frames.push(frame); - } - return frames; - }, - mergeLayers : function (layerA, layerB) { var framesA = layerA.getFrames(); var framesB = layerB.getFrames(); diff --git a/src/js/utils/serialization/Deserializer.js b/src/js/utils/serialization/Deserializer.js index 583ca59..3e3ad7d 100644 --- a/src/js/utils/serialization/Deserializer.js +++ b/src/js/utils/serialization/Deserializer.js @@ -31,11 +31,7 @@ this.piskel_ = new pskl.model.Piskel(piskelData.width, piskelData.height, piskelData.fps, descriptor); this.layersToLoad_ = piskelData.layers.length; - if (piskelData.expanded) { - piskelData.layers.forEach(this.loadExpandedLayer.bind(this)); - } else { - piskelData.layers.forEach(this.deserializeLayer.bind(this)); - } + piskelData.layers.forEach(this.deserializeLayer.bind(this)); }; ns.Deserializer.prototype.deserializeLayer = function (layerString, index) { @@ -43,42 +39,43 @@ var layer = new pskl.model.Layer(layerData.name); layer.setOpacity(layerData.opacity); - // 1 - create an image to load the base64PNG representing the layer - var base64PNG = layerData.base64PNG; - var image = new Image(); + // Backward compatibility: if the layerData is not chunked but contains a single base64PNG, + // create a fake chunk, expected to represent all frames side-by-side. + if (typeof layerData.chunks === 'undefined' && layerData.base64PNG) { + this.normalizeLayerData_(layerData); + } - // 2 - attach the onload callback that will be triggered asynchronously - image.onload = function () { - // 5 - extract the frames from the loaded image - var frames = pskl.utils.LayerUtils.createFramesFromSpritesheet(image, layerData.frameCount); - // 6 - add each image to the layer - this.addFramesToLayer(frames, layer, index); - }.bind(this); + var chunks = layerData.chunks; - // 3 - set the source of the image - image.src = base64PNG; - return layer; - }; - - ns.Deserializer.prototype.loadExpandedLayer = function (layerData, index) { - var width = this.piskel_.getWidth(); - var height = this.piskel_.getHeight(); - 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, width, height); + // Prepare a frames array to store frame objects extracted from the chunks. + var frames = []; + Promise.all(chunks.map(function (chunk) { + // Create a promise for each chunk. + return new Promise(function (resolve, reject) { + var image = new Image(); + // Load the chunk image in an Image object. + image.onload = function () { + // extract the chunkFrames from the chunk image + var chunkFrames = pskl.utils.FrameUtils.createFramesFromChunk(image, chunk.layout); + // add each image to the frames array, at the extracted index + chunkFrames.forEach(function (chunkFrame) { + frames[chunkFrame.index] = chunkFrame.frame; + }); + resolve(); + }; + image.src = chunk.base64PNG; + }); + })).then(function () { + frames.forEach(layer.addFrame.bind(layer)); + this.layers_[index] = layer; + this.onLayerLoaded_(); + }.bind(this)).catch(function () { + console.error('Failed to deserialize layer'); }); - this.addFramesToLayer(frames, layer, index); + return layer; }; - ns.Deserializer.prototype.addFramesToLayer = function (frames, layer, index) { - frames.forEach(layer.addFrame.bind(layer)); - - this.layers_[index] = layer; - this.onLayerLoaded_(); - }; - ns.Deserializer.prototype.onLayerLoaded_ = function () { this.layersToLoad_ = this.layersToLoad_ - 1; if (this.layersToLoad_ === 0) { @@ -88,4 +85,19 @@ this.callback_(this.piskel_); } }; + + /** + * Backward comptibility only. Create a chunk for layerData objects that only contain + * an single base64PNG without chunk/layout information. + */ + ns.Deserializer.prototype.normalizeLayerData_ = function (layerData) { + var layout = []; + for (var i = 0 ; i < layerData.frameCount ; i++) { + layout.push([i]); + } + layerData.chunks = [{ + base64PNG : layerData.base64PNG, + layout : layout + }]; + }; })(); diff --git a/src/js/utils/serialization/arraybuffer/ArrayBufferDeserializer.js b/src/js/utils/serialization/arraybuffer/ArrayBufferDeserializer.js index 7fb77d5..9a8f8d7 100644 --- a/src/js/utils/serialization/arraybuffer/ArrayBufferDeserializer.js +++ b/src/js/utils/serialization/arraybuffer/ArrayBufferDeserializer.js @@ -96,7 +96,7 @@ var loadLayerImage = function(layer, cb) { var image = new Image(); image.onload = function() { - var frames = pskl.utils.LayerUtils.createFramesFromSpritesheet(this, layer.frameCount); + var frames = pskl.utils.FrameUtils.createFramesFromSpritesheet(this, layer.frameCount); frames.forEach(function (frame) { layer.model.addFrame(frame); }); diff --git a/src/js/utils/serialization/arraybuffer/ArrayBufferSerializer.js b/src/js/utils/serialization/arraybuffer/ArrayBufferSerializer.js index 4d72772..d52c945 100644 --- a/src/js/utils/serialization/arraybuffer/ArrayBufferSerializer.js +++ b/src/js/utils/serialization/arraybuffer/ArrayBufferSerializer.js @@ -85,7 +85,7 @@ return bytes; }, - serialize : function (piskel, expanded) { + serialize : function (piskel) { var i; var j; var layers; diff --git a/test/js/utils/FrameUtilsTest.js b/test/js/utils/FrameUtilsTest.js index 8b4a329..05d5e5f 100644 --- a/test/js/utils/FrameUtilsTest.js +++ b/test/js/utils/FrameUtilsTest.js @@ -86,7 +86,7 @@ describe("FrameUtils suite", function() { var spritesheet = pskl.utils.FrameUtils.toImage(frame); // split the spritesheet by 4 - var frames = pskl.utils.LayerUtils.createFramesFromSpritesheet(spritesheet, 4); + var frames = pskl.utils.FrameUtils.createFramesFromSpritesheet(spritesheet, 4); // expect 4 frames of 1x2 expect(frames.length).toBe(4);