Feature : undo redo including frame/layer actions

- Frame and Layer CRUD actions are now registered and can be cancelled
- Limited performance impact while drawing
- Improved frame cache invalidation
This commit is contained in:
jdescottes 2014-04-22 23:57:30 +02:00
parent c2a3ccc8d0
commit 5541d030a5
11 changed files with 116 additions and 93 deletions

3
.gitignore vendored
View File

@ -16,6 +16,9 @@ npm-debug.log
build/*.js
build/*.css
# diffs
diff.txt
# Closure compiler generated JS binary.
build/closure/closure_compiled_binary.js

View File

@ -157,8 +157,8 @@
// Warning : do not call setCurrentButton here
// mousemove do not have the correct mouse button information on all browsers
this.currentToolBehavior.moveToolAt(
coords.x,
coords.y,
coords.x | 0,
coords.y | 0,
this.getCurrentColor_(event),
currentFrame,
this.overlayFrame,

View File

@ -225,7 +225,7 @@
}
};
ns.PiskelController.prototype.serialize = function (compressed) {
return pskl.utils.Serializer.serializePiskel(this.piskel, compressed);
ns.PiskelController.prototype.serialize = function (expanded) {
return pskl.utils.Serializer.serializePiskel(this.piskel, expanded);
};
})();

View File

@ -19,12 +19,12 @@
$.publish(Events.FRAME_SIZE_CHANGED);
$.publish(Events.PISKEL_RESET);
$.publish(Events.PISKEL_SAVE_STATE, {
type : 'FULL'
type : pskl.service.HistoryService.SNAPSHOT
});
};
ns.PublicPiskelController.prototype.addFrame = function () {
this.addFrameAt(this.piskelController.getFrameCount());
this.addFrameAt(this.getFrameCount());
};
ns.PublicPiskelController.prototype.addFrameAtCurrentIndex = function () {
@ -32,14 +32,14 @@
};
ns.PublicPiskelController.prototype.addFrameAt = function (index) {
this.piskelController.addFrameAt(index);
this.raiseSaveStateEvent_(this.piskelController.addFrameAt, [index]);
this.piskelController.addFrameAt(index);
$.publish(Events.PISKEL_RESET);
};
ns.PublicPiskelController.prototype.removeFrameAt = function (index) {
this.piskelController.removeFrameAt(index);
this.raiseSaveStateEvent_(this.piskelController.removeFrameAt, [index]);
this.piskelController.removeFrameAt(index);
$.publish(Events.PISKEL_RESET);
};
@ -49,7 +49,7 @@
ns.PublicPiskelController.prototype.raiseSaveStateEvent_ = function (fn, args) {
$.publish(Events.PISKEL_SAVE_STATE, {
type : 'REPLAY',
type : pskl.service.HistoryService.REPLAY,
scope : this,
replay : {
fn : fn,
@ -63,14 +63,14 @@
};
ns.PublicPiskelController.prototype.duplicateFrameAt = function (index) {
this.piskelController.duplicateFrameAt(index);
this.raiseSaveStateEvent_(this.piskelController.duplicateFrameAt, [index]);
this.piskelController.duplicateFrameAt(index);
$.publish(Events.PISKEL_RESET);
};
ns.PublicPiskelController.prototype.moveFrame = function (fromIndex, toIndex) {
this.piskelController.moveFrame(fromIndex, toIndex);
this.raiseSaveStateEvent_(this.piskelController.moveFrame, [fromIndex, toIndex]);
this.piskelController.moveFrame(fromIndex, toIndex);
$.publish(Events.PISKEL_RESET);
};
@ -100,31 +100,31 @@
};
ns.PublicPiskelController.prototype.renameLayerAt = function (index, name) {
this.piskelController.renameLayerAt(index, name);
this.raiseSaveStateEvent_(this.piskelController.renameLayerAt, [index, name]);
this.piskelController.renameLayerAt(index, name);
};
ns.PublicPiskelController.prototype.createLayer = function (name) {
this.piskelController.createLayer(name);
this.raiseSaveStateEvent_(this.piskelController.createLayer, [name]);
this.piskelController.createLayer(name);
$.publish(Events.PISKEL_RESET);
};
ns.PublicPiskelController.prototype.moveLayerUp = function () {
this.piskelController.moveLayerUp();
this.raiseSaveStateEvent_(this.piskelController.moveLayerUp, []);
this.piskelController.moveLayerUp();
$.publish(Events.PISKEL_RESET);
};
ns.PublicPiskelController.prototype.moveLayerDown = function () {
this.piskelController.moveLayerDown();
this.raiseSaveStateEvent_(this.piskelController.moveLayerDown, []);
this.piskelController.moveLayerDown();
$.publish(Events.PISKEL_RESET);
};
ns.PublicPiskelController.prototype.removeCurrentLayer = function () {
this.piskelController.removeCurrentLayer();
this.raiseSaveStateEvent_(this.piskelController.removeCurrentLayer, []);
this.piskelController.removeCurrentLayer();
$.publish(Events.PISKEL_RESET);
};

View File

@ -52,7 +52,7 @@
ns.BaseTool.prototype.raiseSaveStateEvent = function (replayData) {
$.publish(Events.PISKEL_SAVE_STATE, {
type : 'REPLAY',
type : pskl.service.HistoryService.REPLAY,
scope : this,
replay : replayData
});

View File

@ -42,7 +42,7 @@
var interpolatedPixels = this.getLinePixels_(col, this.previousCol, row, this.previousRow);
for(var i=0, l=interpolatedPixels.length; i<l; i++) {
var coords = interpolatedPixels[i];
this.applyToolAt(coords.col, coords.row, color, frame, overlay);
this.applyToolAt(coords.col, coords.row, color, frame, overlay, event);
}
}
else {

View File

@ -60,7 +60,7 @@
}
$.publish(Events.PISKEL_SAVE_STATE, {
type : 'REPLAY',
type : pskl.service.HistoryService.REPLAY,
scope : this,
replay : {
type : SELECTION_REPLAY.ERASE,
@ -86,7 +86,7 @@
var currentFrame = this.piskelController.getCurrentFrame();
$.publish(Events.PISKEL_SAVE_STATE, {
type : 'REPLAY',
type : pskl.service.HistoryService.REPLAY,
scope : this,
replay : {
type : SELECTION_REPLAY.PASTE,

View File

@ -8,31 +8,41 @@
this.piskelController = piskelController;
this.stateQueue = [];
this.currentIndex = -1;
this.saveState__b = this.saveState.bind(this);
this.saveState__b = this.onSaveStateEvent.bind(this);
this.lastLoadState = -1;
};
ns.HistoryService.prototype.init = function () {
ns.HistoryService.SNAPSHOT = 'SNAPSHOT';
ns.HistoryService.REPLAY = 'REPLAY';
ns.HistoryService.prototype.init = function () {
$.subscribe(Events.PISKEL_SAVE_STATE, this.saveState__b);
pskl.app.shortcutService.addShortcut('ctrl+Z', this.undo.bind(this));
pskl.app.shortcutService.addShortcut('ctrl+Y', this.redo.bind(this));
this.saveState({
type : ns.HistoryService.SNAPSHOT
});
};
ns.HistoryService.prototype.saveState = function (evt, actionInfo) {
ns.HistoryService.prototype.onSaveStateEvent = function (evt, stateInfo) {
this.saveState(stateInfo);
};
ns.HistoryService.prototype.saveState = function (stateInfo) {
this.stateQueue = this.stateQueue.slice(0, this.currentIndex + 1);
this.currentIndex = this.currentIndex + 1;
var state = {
action : actionInfo,
action : stateInfo,
frameIndex : this.piskelController.currentFrameIndex,
layerIndex : this.piskelController.currentLayerIndex
};
if (actionInfo.type === 'FULL' || this.currentIndex % SNAPSHOT_PERIOD === 0) {
state.piskel = this.piskelController.serialize(false);
if (stateInfo.type === ns.HistoryService.SNAPSHOT || this.currentIndex % SNAPSHOT_PERIOD === 0) {
state.piskel = this.piskelController.serialize(true);
}
this.stateQueue.push(state);
@ -46,26 +56,6 @@
this.loadState(this.currentIndex + 1);
};
ns.HistoryService.prototype.loadState = function (index) {
if (this.isLoadStateAllowed_(index)) {
this.lastLoadState = Date.now();
var snapshotIndex = this.getPreviousSnapshotIndex_(index);
if (snapshotIndex < 0) {
throw 'Could not find previous SNAPSHOT saved in history stateQueue';
}
var serializedPiskel = this.stateQueue[snapshotIndex].piskel;
if (typeof serializedPiskel === "string") {
this.stateQueue[snapshotIndex].piskel = JSON.parse(serializedPiskel);
serializedPiskel = this.stateQueue[snapshotIndex].piskel;
}
this.loadPiskel(serializedPiskel, this.onPiskelLoadedCallback.bind(this, index, snapshotIndex));
}
};
ns.HistoryService.prototype.isLoadStateAllowed_ = function (index) {
var timeOk = (Date.now() - this.lastLoadState) > LOAD_STATE_INTERVAL;
var indexInRange = index >= 0 && index < this.stateQueue.length;
@ -79,6 +69,44 @@
return index;
};
ns.HistoryService.prototype.loadState = function (index) {
if (this.isLoadStateAllowed_(index)) {
this.lastLoadState = Date.now();
var snapshotIndex = this.getPreviousSnapshotIndex_(index);
if (snapshotIndex < 0) {
throw 'Could not find previous SNAPSHOT saved in history stateQueue';
}
var piskelSnapshot = this.getSnapshotFromState_(snapshotIndex);
this.loadPiskel(piskelSnapshot, this.onPiskelLoadedCallback.bind(this, index, snapshotIndex));
}
};
ns.HistoryService.prototype.getSnapshotFromState_ = function (stateIndex) {
var state = this.stateQueue[stateIndex];
var piskelSnapshot = state.piskel;
// If the snapshot is stringified, parse it and backup the result for faster access next time
// FIXME : Memory consumption might go crazy if we keep unpacking big piskels indefinitely
// ==> should ensure I remove some of them :)
if (typeof piskelSnapshot === "string") {
piskelSnapshot = JSON.parse(piskelSnapshot);
state.piskel = piskelSnapshot;
}
return piskelSnapshot;
};
ns.HistoryService.prototype.loadPiskel = function (piskel, callback) {
var descriptor = this.piskelController.piskel.getDescriptor();
pskl.utils.serialization.Deserializer.deserialize(piskel, function (deserializedPiskel) {
deserializedPiskel.setDescriptor(descriptor);
this.piskelController.setPiskel(deserializedPiskel);
callback(deserializedPiskel);
}.bind(this));
};
ns.HistoryService.prototype.onPiskelLoadedCallback = function (index, snapshotIndex, piskel) {
for (var i = snapshotIndex + 1 ; i <= index ; i++) {
var state = this.stateQueue[i];
@ -97,15 +125,6 @@
this.piskelController.setCurrentLayerIndex(state.layerIndex);
};
ns.HistoryService.prototype.loadPiskel = function (piskel, callback) {
var descriptor = this.piskelController.piskel.getDescriptor();
pskl.utils.serialization.Deserializer.deserialize(piskel, function (piskel) {
piskel.setDescriptor(descriptor);
this.piskelController.setPiskel(piskel);
callback(piskel);
}.bind(this));
};
ns.HistoryService.prototype.replayState = function (state) {
var action = state.action;
var type = action.type;

View File

@ -49,7 +49,7 @@ if (typeof Function.prototype.bind !== "function") {
ns.wrap = function (wrapper, wrappedObject) {
for (var prop in wrappedObject) {
if (typeof wrappedObject[prop] === 'function') {
if (typeof wrappedObject[prop] === 'function' && typeof wrapper[prop] === 'undefined') {
wrapper[prop] = wrappedObject[prop].bind(wrappedObject);
}
}

View File

@ -29,39 +29,42 @@
this.piskel_ = new pskl.model.Piskel(piskelData.width, piskelData.height, descriptor);
this.layersToLoad_ = piskelData.layers.length;
piskelData.layers.forEach(function (serializedLayer) {
this.deserializeLayer(serializedLayer);
}.bind(this));
if (piskelData.expanded) {
piskelData.layers.forEach(this.loadExpandedLayer.bind(this));
} else {
piskelData.layers.forEach(this.deserializeLayer.bind(this));
}
};
ns.Deserializer.prototype.deserializeLayer = function (layerString) {
var layerData = typeof layerString === "string" ? JSON.parse(layerString) : layerString;
var layerData = JSON.parse(layerString);
var layer = new pskl.model.Layer(layerData.name);
var isCompressedLayer = !!layerData.base64PNG;
// 1 - create an image to load the base64PNG representing the layer
var base64PNG = layerData.base64PNG;
var image = new Image();
if (isCompressedLayer) {
// 1 - create an image to load the base64PNG representing the layer
var base64PNG = layerData.base64PNG;
var image = new Image();
// 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.createFromImage(image, layerData.frameCount);
// 6 - add each image to the layer
this.addFramesToLayer(frames, layer);
}.bind(this);
// 3 - set the source of the image
image.src = base64PNG;
} else {
var frames = layerData.grids.map(function (grid) {
return pskl.model.Frame.fromPixelGrid(grid);
});
// 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.createFromImage(image, layerData.frameCount);
// 6 - add each image to the layer
this.addFramesToLayer(frames, layer);
}
}.bind(this);
// 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) {
var layer = new pskl.model.Layer(layerData.name);
var frames = layerData.grids.map(function (grid) {
return pskl.model.Frame.fromPixelGrid(grid);
});
this.addFramesToLayer(frames, layer);
// 4 - return a pointer to the new layer instance
return layer;

View File

@ -2,36 +2,34 @@
var ns = $.namespace('pskl.utils');
ns.Serializer = {
serializePiskel : function (piskel, compressed) {
serializePiskel : function (piskel, expanded) {
var serializedLayers = piskel.getLayers().map(function (l) {
return pskl.utils.Serializer.serializeLayer(l, compressed);
return pskl.utils.Serializer.serializeLayer(l, expanded);
});
return JSON.stringify({
modelVersion : Constants.MODEL_VERSION,
piskel : {
height : piskel.getHeight(),
width : piskel.getWidth(),
layers : serializedLayers
layers : serializedLayers,
expanded : expanded
}
});
},
serializeLayer : function (layer, compressed) {
if (compressed !== false) {
compressed = true;
}
serializeLayer : function (layer, expanded) {
var frames = layer.getFrames();
var renderer = new pskl.rendering.FramesheetRenderer(frames);
var layerToSerialize = {
name : layer.getName(),
frameCount : frames.length
};
if (compressed) {
layerToSerialize.base64PNG = renderer.renderAsCanvas().toDataURL();
return JSON.stringify(layerToSerialize);
} else {
if (expanded) {
layerToSerialize.grids = frames.map(function (f) {return f.pixels;});
return layerToSerialize;
} else {
layerToSerialize.base64PNG = renderer.renderAsCanvas().toDataURL();
return JSON.stringify(layerToSerialize);
}
}
};