(function () { var ns = $.namespace('pskl.service'); ns.HistoryService = function (piskelController, shortcutService, deserializer) { this.piskelController = piskelController || pskl.app.piskelController; this.shortcutService = shortcutService || pskl.app.shortcutService; this.deserializer = deserializer || pskl.utils.serialization.Deserializer; 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; ns.HistoryService.prototype.init = function () { $.subscribe(Events.PISKEL_SAVE_STATE, this.onSaveStateEvent.bind(this)); this.shortcutService.addShortcut('ctrl+Z', this.undo.bind(this)); this.shortcutService.addShortcut('ctrl+Y', 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 }; var isSnapshot = action.type === ns.HistoryService.SNAPSHOT; var isAtAutoSnapshotInterval = this.currentIndex % ns.HistoryService.SNAPSHOT_PERIOD === 0; if (isSnapshot || isAtAutoSnapshotInterval) { state.piskel = this.piskelController.serialize(true); } this.stateQueue.push(state); $.publish(Events.HISTORY_STATE_SAVED); }; 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.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; // 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.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); }; 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); }; })();