piskel/dev/js/service/HistoryService.js
2017-05-22 09:56:42 +02:00

200 lines
6.7 KiB
JavaScript

(function () {
var ns = $.namespace('pskl.service');
ns.HistoryService = function (piskelController, shortcutService, deserializer, serializer) {
// Use the real piskel controller that will not fire events when calling setters
this.piskelController = piskelController.getWrappedPiskelController();
this.shortcutService = shortcutService || pskl.app.shortcutService;
this.deserializer = deserializer || pskl.utils.serialization.arraybuffer.ArrayBufferDeserializer;
this.serializer = serializer || pskl.utils.serialization.arraybuffer.ArrayBufferSerializer;
this.stateQueue = [];
this.currentIndex = -1;
this.lastLoadState = -1;
};
// Force to save a state as a SNAPSHOT
ns.HistoryService.SNAPSHOT = 'SNAPSHOT';
// Default save state
ns.HistoryService.REPLAY = 'REPLAY';
// Period (in number of state saved) between two snapshots
ns.HistoryService.SNAPSHOT_PERIOD = 50;
// Interval/buffer (in milliseconds) between two state load (ctrl+z/y spamming)
ns.HistoryService.LOAD_STATE_INTERVAL = 50;
// Maximum number of states that can be recorded.
ns.HistoryService.MAX_SAVED_STATES = 500;
ns.HistoryService.prototype.init = function () {
$.subscribe(Events.PISKEL_SAVE_STATE, this.onSaveStateEvent.bind(this));
var shortcuts = pskl.service.keyboard.Shortcuts;
this.shortcutService.registerShortcut(shortcuts.MISC.UNDO, this.undo.bind(this));
this.shortcutService.registerShortcut(shortcuts.MISC.REDO, this.redo.bind(this));
this.saveState({
type : ns.HistoryService.SNAPSHOT
});
};
ns.HistoryService.prototype.onSaveStateEvent = function (evt, action) {
this.saveState(action);
};
ns.HistoryService.prototype.saveState = function (action) {
this.stateQueue = this.stateQueue.slice(0, this.currentIndex + 1);
this.currentIndex = this.currentIndex + 1;
var state = {
action : action,
frameIndex : action.state ? action.state.frameIndex : this.piskelController.currentFrameIndex,
layerIndex : action.state ? action.state.layerIndex : this.piskelController.currentLayerIndex,
fps : this.piskelController.getFPS(),
uuid: pskl.utils.Uuid.generate()
};
var isSnapshot = action.type === ns.HistoryService.SNAPSHOT;
var isAtAutoSnapshotInterval = this.currentIndex % ns.HistoryService.SNAPSHOT_PERIOD === 0;
if (isSnapshot || isAtAutoSnapshotInterval) {
var piskel = this.piskelController.getPiskel();
state.piskel = this.serializer.serialize(piskel);
}
// If the new state pushes over MAX_SAVED_STATES, erase all states between the first and
// second snapshot states.
if (this.stateQueue.length > ns.HistoryService.MAX_SAVED_STATES) {
var firstSnapshotIndex = this.getNextSnapshotIndex_(1);
this.stateQueue.splice(0, firstSnapshotIndex);
this.currentIndex = this.currentIndex - firstSnapshotIndex;
}
this.stateQueue.push(state);
$.publish(Events.HISTORY_STATE_SAVED);
};
ns.HistoryService.prototype.getCurrentStateId = function () {
var state = this.stateQueue[this.currentIndex];
if (!state) {
return false;
}
return state.uuid;
};
ns.HistoryService.prototype.undo = function () {
this.loadState(this.currentIndex - 1);
};
ns.HistoryService.prototype.redo = function () {
this.loadState(this.currentIndex + 1);
};
ns.HistoryService.prototype.isLoadStateAllowed_ = function (index) {
var timeOk = (Date.now() - this.lastLoadState) > ns.HistoryService.LOAD_STATE_INTERVAL;
var indexInRange = index >= 0 && index < this.stateQueue.length;
return timeOk && indexInRange;
};
ns.HistoryService.prototype.getPreviousSnapshotIndex_ = function (index) {
while (this.stateQueue[index] && !this.stateQueue[index].piskel) {
index = index - 1;
}
return index;
};
ns.HistoryService.prototype.getNextSnapshotIndex_ = function (index) {
while (this.stateQueue[index] && !this.stateQueue[index].piskel) {
index = index + 1;
}
return index;
};
ns.HistoryService.prototype.loadState = function (index) {
try {
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.getSnapshotFromState_(snapshotIndex);
var onPiskelLoadedCb = this.onPiskelLoaded_.bind(this, index, snapshotIndex);
this.deserializer.deserialize(serializedPiskel, onPiskelLoadedCb);
}
} catch (error) {
console.error('[CRITICAL ERROR] : Unable to load a history state.');
this.logError_(error);
this.stateQueue = [];
this.currentIndex = -1;
}
};
ns.HistoryService.prototype.logError_ = function (error) {
if (typeof error === 'string') {
console.error(error);
} else {
console.error(error.message);
console.error(error.stack);
}
};
ns.HistoryService.prototype.getSnapshotFromState_ = function (stateIndex) {
var state = this.stateQueue[stateIndex];
var piskelSnapshot = state.piskel;
state.piskel = piskelSnapshot;
return piskelSnapshot;
};
ns.HistoryService.prototype.onPiskelLoaded_ = function (index, snapshotIndex, piskel) {
var originalSize = this.getPiskelSize_();
piskel.setDescriptor(this.piskelController.piskel.getDescriptor());
// propagate save path to the new piskel instance
piskel.savePath = this.piskelController.piskel.savePath;
this.piskelController.setPiskel(piskel);
for (var i = snapshotIndex + 1 ; i <= index ; i++) {
var state = this.stateQueue[i];
this.setupState(state);
this.replayState(state);
}
// Should only do this when going backwards
var lastState = this.stateQueue[index + 1];
if (lastState) {
this.setupState(lastState);
}
this.currentIndex = index;
$.publish(Events.PISKEL_RESET);
$.publish(Events.HISTORY_STATE_LOADED);
if (originalSize !== this.getPiskelSize_()) {
$.publish(Events.FRAME_SIZE_CHANGED);
}
};
ns.HistoryService.prototype.getPiskelSize_ = function () {
return this.piskelController.getWidth() + 'x' + this.piskelController.getHeight();
};
ns.HistoryService.prototype.setupState = function (state) {
this.piskelController.setCurrentFrameIndex(state.frameIndex);
this.piskelController.setCurrentLayerIndex(state.layerIndex);
this.piskelController.setFPS(state.fps);
};
ns.HistoryService.prototype.replayState = function (state) {
var action = state.action;
var type = action.type;
var layer = this.piskelController.getLayerAt(state.layerIndex);
var frame = layer.getFrameAt(state.frameIndex);
action.scope.replay(frame, action.replay);
};
})();