Merge pull request #593 from juliandescottes/save-split

Save split
This commit is contained in:
Julian Descottes 2016-12-21 12:25:54 +01:00 committed by GitHub
commit 83c7e950f0
12 changed files with 252 additions and 134 deletions

View File

@ -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'
]
},

View File

@ -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

View File

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

View File

@ -10,6 +10,31 @@
match = filtered[0];
}
return match;
},
/**
* Split a provided array in a given amount of chunks.
* For instance [1,2,3,4] chunked in 2 parts will be [1,2] & [3,4].
* @param {Array} array the array to chunk
* @param {Number} chunksCount the number of chunks to create
* @return {Array<Array>} array of arrays containing the items of the original array
*/
chunk : function (array, chunksCount) {
var chunks = [];
// We cannot have more chunks than array items.
chunksCount = Math.min(chunksCount, array.length);
// chunksCount should be at least 1
chunksCount = Math.max(1, chunksCount);
var step = Math.round(array.length / chunksCount);
for (var i = 0 ; i < chunksCount ; i++) {
var isLast = i == chunksCount - 1;
var end = isLast ? array.length : (i + 1) * step;
chunks.push(array.slice(i * step, end));
}
return chunks;
}
};

View File

@ -177,74 +177,61 @@
},
/**
* 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<Frame>}
*/
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 <Array>} layout description of the frame indexes expected to be found in the chunk
* @return {Array<Object>} 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, frameHeight);
var frame = pskl.utils.FrameUtils.createFromCanvas(canvas, 0, 0, frameWidth, frameHeight);
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;
}
};
})();

View File

@ -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<Frame>}
*/
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();

View File

@ -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
}];
};
})();

View File

@ -1,6 +1,21 @@
(function () {
var ns = $.namespace('pskl.utils.serialization');
var areChunksValid = function (chunks) {
return chunks.every(function (chunk) {
return chunk.base64PNG;
});
};
var createLineLayout = function (size, offset) {
var layout = [];
for (var i = 0 ; i < size ; i++) {
layout.push([i + offset]);
}
return layout;
};
ns.Serializer = {
serialize : function (piskel) {
var serializedLayers = piskel.getLayers().map(function (l) {
@ -26,8 +41,42 @@
opacity : layer.getOpacity(),
frameCount : frames.length
};
var renderer = new pskl.rendering.FramesheetRenderer(frames);
layerToSerialize.base64PNG = renderer.renderAsCanvas().toDataURL();
// A layer spritesheet data can be chunked in case the spritesheet PNG is to big to be
// converted to a dataURL.
// Frames are divided equally amongst chunks and each chunk is converted to a spritesheet
// PNG. If any chunk contains an invalid base64 PNG, we increase the number of chunks and
// retry.
var chunks = [];
while (!areChunksValid(chunks)) {
if (chunks.length > frames.length) {
// Something went horribly wrong.
chunks = [];
break;
}
// Chunks are invalid, increase the number of chunks by one, and chunk the frames array.
var frameChunks = pskl.utils.Array.chunk(frames, chunks.length + 1);
// Reset chunks array.
chunks = [];
// After each chunk update the offset by te number of frames that have been processed.
var offset = 0;
for (var i = 0 ; i < frameChunks.length ; i++) {
var chunkFrames = frameChunks[i];
var renderer = new pskl.rendering.FramesheetRenderer(chunkFrames);
chunks.push({
base64PNG : renderer.renderAsCanvas().toDataURL(),
// create a layout array, containing the indices of the frames extracted in this chunk
layout : createLineLayout(chunkFrames.length, offset),
});
offset += chunkFrames.length;
}
}
layerToSerialize.chunks = chunks;
return JSON.stringify(layerToSerialize);
}
};

View File

@ -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);
});

View File

@ -85,7 +85,7 @@
return bytes;
},
serialize : function (piskel, expanded) {
serialize : function (piskel) {
var i;
var j;
var layers;

View File

@ -0,0 +1,68 @@
describe("Array utils", function() {
beforeEach(function() {});
afterEach(function() {});
it("chunks correctly", function() {
// when
var array = [1, 2, 3, 4];
// then
var chunks = pskl.utils.Array.chunk(array, 1);
// verify
expect(chunks.length).toBe(1);
expect(chunks[0]).toEqual([1, 2, 3, 4]);
// then
chunks = pskl.utils.Array.chunk(array, 2);
// verify
expect(chunks.length).toBe(2);
expect(chunks[0]).toEqual([1, 2]);
expect(chunks[1]).toEqual([3, 4]);
// then
chunks = pskl.utils.Array.chunk(array, 3);
// verify
expect(chunks.length).toBe(3);
expect(chunks[0]).toEqual([1]);
expect(chunks[1]).toEqual([2]);
expect(chunks[2]).toEqual([3, 4]);
// then
chunks = pskl.utils.Array.chunk(array, 4);
// verify
expect(chunks.length).toBe(4);
expect(chunks[0]).toEqual([1]);
expect(chunks[1]).toEqual([2]);
expect(chunks[2]).toEqual([3]);
expect(chunks[3]).toEqual([4]);
// then
chunks = pskl.utils.Array.chunk(array, 5);
// verify
expect(chunks.length).toBe(4);
expect(chunks[0]).toEqual([1]);
expect(chunks[1]).toEqual([2]);
expect(chunks[2]).toEqual([3]);
expect(chunks[3]).toEqual([4]);
// then
chunks = pskl.utils.Array.chunk(array, 0);
// verify
expect(chunks.length).toBe(1);
expect(chunks[0]).toEqual([1, 2, 3, 4]);
// then
chunks = pskl.utils.Array.chunk(array, -1);
// verify
expect(chunks.length).toBe(1);
expect(chunks[0]).toEqual([1, 2, 3, 4]);
});
});

View File

@ -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);