fix : reduce piskel model size

- Initial implementation : working but ...
- MODEL_VERSION has been bumped to 2
- The loading process is now theoretically asynchronous (loading images to
  read the content of the layers), but for now, the asynchronous behaviour
  is hidden behind a nasty hack, which is somehow similar to lazy loading.
  When loading the piskel, a Piskel is created synchronously, with fake
  empty frames, and as the images will get loaded, the fake frames will be
  replaced by the actual frames.

  I really don't like this, and the asynchronous nature of the loading
  should be clearly expressed
- There is no backward compatible deserializer for the previous version of
  the model (1)
- The Serializer utils is just badly designed. Serialization and
  deserialization should be splitted into two different classes
- Saving & loading are still done in app.js and should be moved to
  services

BUT : the size of the piskels is now pretty small. A piskel which was
using 890kB previously is now using only 10kB. Although it should be
noted, that after gzip there is no significant difference between this
version and the existing one. The only gains we can really expect with
this are : less disk space used on appengine, ability to reuse the
layers' pngs directly on piskel-website (but to be honest I can't see any
valid use case for this)
This commit is contained in:
jdescottes 2013-11-08 00:44:24 +01:00
parent eb559eee0c
commit 4f54715f70
12 changed files with 175 additions and 91 deletions

View File

@ -6,7 +6,7 @@ var Constants = {
FPS : 12
},
MODEL_VERSION : 1,
MODEL_VERSION : 2,
MAX_HEIGHT : 128,
MAX_WIDTH : 128,

View File

@ -239,8 +239,8 @@
},
getFramesheetAsPng : function () {
var renderer = new pskl.rendering.SpritesheetRenderer(this.piskelController);
var framesheetCanvas = renderer.render();
var renderer = new pskl.rendering.PiskelRenderer(this.piskelController);
var framesheetCanvas = renderer.renderAsCanvas();
return framesheetCanvas.toDataURL("image/png");
},

View File

@ -114,8 +114,9 @@
for (var i = 0; i < this.piskelController.getFrameCount(); i++) {
var frame = this.piskelController.getFrameAt(i);
var renderer = new pskl.rendering.CanvasRenderer(frame, dpi);
gif.addFrame(renderer.render(), {
var canvasRenderer = new pskl.rendering.CanvasRenderer(frame, dpi);
var canvas = canvasRenderer.render();
gif.addFrame(canvas.getContext('2d'), {
delay: 1000 / fps
});
}

View File

@ -19,24 +19,20 @@
ns.CanvasRenderer.prototype.render = function () {
var canvas = this.createCanvas_();
var context = canvas.getContext('2d');
for(var col = 0, width = this.frame.getWidth(); col < width; col++) {
for(var row = 0, height = this.frame.getHeight(); row < height; row++) {
var color = this.frame.getPixel(col, row);
this.renderPixel_(color, col, row, context);
}
}
return context;
this.frame.forEachPixel(function (color, x, y) {
this.renderPixel_(color, x, y, context);
}.bind(this));
return canvas;
};
ns.CanvasRenderer.prototype.renderPixel_ = function (color, col, row, context) {
ns.CanvasRenderer.prototype.renderPixel_ = function (color, x, y, context) {
if(color == Constants.TRANSPARENT_COLOR) {
color = this.transparentColor_;
}
context.fillStyle = color;
context.fillRect(col * this.dpi, row * this.dpi, this.dpi, this.dpi);
context.fillRect(x * this.dpi, y * this.dpi, this.dpi, this.dpi);
};
ns.CanvasRenderer.prototype.createCanvas_ = function () {

View File

@ -0,0 +1,43 @@
(function () {
var ns = $.namespace('pskl.rendering');
/**
* Render an array of frames
* @param {Array.<pskl.model.Frame>} frames
*/
ns.FramesheetRenderer = function (frames) {
if (frames.length > 0) {
this.frames = frames;
} else {
throw 'FramesheetRenderer : Invalid argument : frames is empty';
}
};
ns.FramesheetRenderer.prototype.renderAsCanvas = function () {
var canvas = this.createCanvas_();
for (var i = 0 ; i < this.frames.length ; i++) {
var frame = this.frames[i];
this.drawFrameInCanvas_(frame, canvas, i * frame.getWidth(), 0);
}
return canvas;
};
ns.FramesheetRenderer.prototype.drawFrameInCanvas_ = function (frame, canvas, offsetWidth, offsetHeight) {
var context = canvas.getContext('2d');
frame.forEachPixel(function (color, x, y) {
if(color != Constants.TRANSPARENT_COLOR) {
context.fillStyle = color;
context.fillRect(x + offsetWidth, y + offsetHeight, 1, 1);
}
});
};
ns.FramesheetRenderer.prototype.createCanvas_ = function () {
var sampleFrame = this.frames[0];
var count = this.frames.length;
var width = count * sampleFrame.getWidth();
var height = sampleFrame.getHeight();
return pskl.CanvasUtils.createCanvas(width, height);
};
})();

View File

@ -0,0 +1,14 @@
(function () {
var ns = $.namespace("pskl.rendering");
ns.PiskelRenderer = function (piskelController) {
var frames = [];
for (var i = 0 ; i < piskelController.getFrameCount() ; i++) {
frames.push(this.piskelController.getFrameAt(i));
}
ns.FramesheetRenderer.call(this, frames);
};
pskl.utils.inherit(ns.PiskelRenderer, ns.FramesheetRenderer);
})();

View File

@ -1,44 +0,0 @@
(function () {
var ns = $.namespace("pskl.rendering");
ns.SpritesheetRenderer = function (piskelController) {
this.piskelController = piskelController;
};
ns.SpritesheetRenderer.prototype.render = function () {
var canvas = this.createCanvas_();
for (var i = 0 ; i < this.piskelController.getFrameCount() ; i++) {
var frame = this.piskelController.getFrameAt(i);
this.drawFrameInCanvas_(frame, canvas, i * this.piskelController.getWidth(), 0);
}
return canvas;
};
/**
* TODO(juliandescottes): Mutualize with code already present in FrameRenderer
*/
ns.SpritesheetRenderer.prototype.drawFrameInCanvas_ = function (frame, canvas, offsetWidth, offsetHeight) {
var context = 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);
if(color != Constants.TRANSPARENT_COLOR) {
context.fillStyle = color;
context.fillRect(col + offsetWidth, row + offsetHeight, 1, 1);
}
}
}
};
ns.SpritesheetRenderer.prototype.createCanvas_ = function () {
var frameCount = this.piskelController.getFrameCount();
if (frameCount > 0){
var width = frameCount * this.piskelController.getWidth();
var height = this.piskelController.getHeight();
return pskl.CanvasUtils.createCanvas(width, height);
} else {
throw "Cannot render empty Spritesheet";
}
};
})();

View File

@ -36,7 +36,6 @@
* @private
*/
ns.LocalStorageService.prototype.persistToLocalStorage_ = function() {
console.log('[LocalStorage service]: Snapshot stored');
window.localStorage.snapShot = this.piskelController.serialize();
};
@ -45,9 +44,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);
};
/**

View File

@ -36,21 +36,25 @@
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);
},
createFromImageData : function (imageData, width, height) {
// Draw the zoomed-up pixels to a different canvas context
var frame = [];
for (var x=0;x<image.width;++x){
for (var x = 0 ; x < width ; x++){
frame[x] = [];
for (var y=0;y<image.height;++y){
for (var y = 0 ; y < height ; y++){
// Find the starting index in the one-dimensional image data
var i = (y*image.width + x)*4;
var r = imgData[i ];
var g = imgData[i+1];
var b = imgData[i+2];
var a = imgData[i+3];
var i = (y * width + x)*4;
var r = imageData[i ];
var g = imageData[i+1];
var b = imageData[i+2];
var a = imageData[i+3];
if (a < 125) {
frame[x][y] = "TRANSPARENT";
frame[x][y] = Constants.TRANSPARENT_COLOR;
} else {
frame[x][y] = this.rgbToHex(r,g,b);
frame[x][y] = pskl.utils.FrameUtils.rgbToHex(r,g,b);
}
}
}

31
js/utils/LayerUtils.js Normal file
View File

@ -0,0 +1,31 @@
(function () {
var ns = $.namespace('pskl.utils');
ns.LayerUtils = {
/**
* Create a pskl.model.Layer from an Image object.
* Transparent pixels will either be converted to completely opaque or completely transparent pixels.
* @param {Image} image source image
* @return {pskl.model.Frame} corresponding frame
*/
createFromImage : function (image, frameCount) {
var w = image.width,
h = image.height,
frameWidth = w / frameCount;
var canvas = pskl.CanvasUtils.createCanvas(w, h);
var context = canvas.getContext('2d');
context.drawImage(image, 0,0,w,h,0,0,w,h);
// Draw the zoomed-up pixels to a different canvas context
var frames = [];
for (var i = 0 ; i < frameCount ; i++) {
var imgData = context.getImageData(frameWidth*i,0,frameWidth,h).data;
var frame = pskl.utils.FrameUtils.createFromImageData(imgData, frameWidth, h);
frames.push(frame);
}
return frames;
}
};
})();

View File

@ -1,5 +1,8 @@
(function () {
var ns = $.namespace('pskl.utils');
var layersToLoad = 0;
ns.Serializer = {
serializePiskel : function (piskel) {
var serializedLayers = piskel.getLayers().map(function (l) {
@ -16,47 +19,82 @@
},
serializeLayer : function (layer) {
var serializedFrames = layer.getFrames().map(function (f) {
return f.serialize();
});
var frames = layer.getFrames();
var renderer = new pskl.rendering.FramesheetRenderer(frames);
var base64PNG = renderer.renderAsCanvas().toDataURL();
return JSON.stringify({
name : layer.getName(),
frames : serializedFrames
base64PNG : base64PNG,
frameCount : frames.length
});
},
deserializePiskel : function (piskelString) {
var piskelData = JSON.parse(piskelString);
return this.createPiskelFromData(piskelData);
var data = JSON.parse(piskelString);
return this.createPiskel(data);
},
/**
* Similar to deserializePiskel, but dealing directly with a parsed piskel
* @param {Object} piskelData JSON.parse of a serialized piskel
* @param {Object} data JSON.parse of a serialized piskel
* @return {pskl.model.Piskel} a piskel
*/
createPiskel : function (piskelData) {
createPiskel : function (data) {
var piskel = null;
if (piskelData.modelVersion == Constants.MODEL_VERSION) {
var pData = piskelData.piskel;
piskel = new pskl.model.Piskel(pData.width, pData.height);
pData.layers.forEach(function (serializedLayer) {
if (data.modelVersion == Constants.MODEL_VERSION) {
var piskelData = data.piskel;
piskel = new pskl.model.Piskel(piskelData.width, piskelData.height);
layersToLoad = piskelData.layers;
piskelData.layers.forEach(function (serializedLayer) {
var layer = pskl.utils.Serializer.deserializeLayer(serializedLayer);
piskel.addLayer(layer);
});
} else if (data.modelVersion == 1) {
throw 'No backward compatible adapter for modelVersion 1';
} else {
piskel = pskl.utils.Serializer.backwardDeserializer_(piskelData);
piskel = pskl.utils.Serializer.backwardDeserializer_(data);
}
return piskel;
},
deserializeLayer : function (layerString) {
var lData = JSON.parse(layerString);
var layer = new pskl.model.Layer(lData.name);
var layerData = JSON.parse(layerString);
var layer = new pskl.model.Layer(layerData.name);
// TODO : nasty trick to keep the whole loading process lazily synchronous
// 1 - adding a fake frame so that the rendering can start
layer.addFrame(new pskl.model.Frame(32,32));
lData.frames.forEach(function (serializedFrame) {
// 2 - create an image to load the base64PNG representing the layer
var base64PNG = layerData.base64PNG;
var image = new Image();
// 3 - attach the onload callback that will be triggered asynchronously
image.onload = function () {
// 6 - remove the fake frame
layer.removeFrameAt(0);
// 7 - extract the frames from the loaded image
var frames = pskl.utils.LayerUtils.createFromImage(image, layerData.frameCount);
// 8 - add each image to the layer
frames.forEach(function (frame) {
layer.addFrame(pskl.model.Frame.fromPixelGrid(frame));
});
};
// 4 - set the source of the image
image.src = base64PNG;
// 5 - return a pointer to the new layer instance, which at this point contains a fake frame
return layer;
},
deserializeLayer_v1 : function (layerString) {
var layerData = JSON.parse(layerString);
var layer = new pskl.model.Layer(layerData.name);
layerData.frames.forEach(function (serializedFrame) {
var frame = pskl.utils.Serializer.deserializeFrame(serializedFrame);
layer.addFrame(frame);
});

View File

@ -16,6 +16,7 @@ exports.scripts = [
"js/utils/CanvasUtils.js",
"js/utils/FileUtils.js",
"js/utils/FrameUtils.js",
"js/utils/LayerUtils.js",
"js/utils/ImageResizer.js",
"js/utils/PixelUtils.js",
"js/utils/Serializer.js",
@ -40,7 +41,8 @@ exports.scripts = [
// Rendering
"js/rendering/CanvasRenderer.js",
"js/rendering/FrameRenderer.js",
"js/rendering/SpritesheetRenderer.js",
"js/rendering/FramesheetRenderer.js",
"js/rendering/PiskelRenderer.js",
// Controllers
"js/controller/PiskelController.js",