mirror of
https://github.com/piskelapp/piskel.git
synced 2023-08-10 21:12:52 +03:00
200 lines
6.7 KiB
JavaScript
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);
|
||
|
};
|
||
|
|
||
|
})();
|