diff --git a/src/js/model/frame/CachedFrameProcessor.js b/src/js/model/frame/CachedFrameProcessor.js index 8a8ba61..eb88d1c 100644 --- a/src/js/model/frame/CachedFrameProcessor.js +++ b/src/js/model/frame/CachedFrameProcessor.js @@ -1,8 +1,8 @@ (function () { var ns = $.namespace('pskl.model.frame'); - // 10 * 60 * 1000 = 10 minutes - var DEFAULT_CLEAR_INTERVAL = 10 * 60 * 1000; + // Maximum number of cache entries + var MAX_CACHE_ENTRIES = 100; var DEFAULT_FRAME_PROCESSOR = function (frame) { return pskl.utils.FrameUtils.toImage(frame); @@ -12,14 +12,16 @@ var DEFAULT_NAMESPACE = '__cache_default__'; - ns.CachedFrameProcessor = function (cacheResetInterval) { + ns.CachedFrameProcessor = function () { + // Cache object. this.cache_ = {}; - this.cacheResetInterval = cacheResetInterval || DEFAULT_CLEAR_INTERVAL; + + // Array of [namespace, key] for each cached frame. + this.cacheQueue_ = []; + this.frameProcessor = DEFAULT_FRAME_PROCESSOR; this.outputCloner = DEFAULT_OUTPUT_CLONER; this.defaultNamespace = DEFAULT_NAMESPACE; - - window.setInterval(this.clear.bind(this), this.cacheResetInterval); }; ns.CachedFrameProcessor.prototype.clear = function () { @@ -69,6 +71,11 @@ } else { processedFrame = this.frameProcessor(frame); cache[cacheKey] = processedFrame; + this.cacheQueue_.unshift([namespace, cacheKey]); + if (this.cacheQueue_.length > MAX_CACHE_ENTRIES) { + var oldestItem = this.cacheQueue_.pop(); + this.cache_[oldestItem[0]][oldestItem[1]] = null; + } } return processedFrame; diff --git a/src/js/service/CurrentColorsService.js b/src/js/service/CurrentColorsService.js index b58d251..365bfb0 100644 --- a/src/js/service/CurrentColorsService.js +++ b/src/js/service/CurrentColorsService.js @@ -10,11 +10,16 @@ this.cachedFrameProcessor = new pskl.model.frame.AsyncCachedFrameProcessor(); this.cachedFrameProcessor.setFrameProcessor(this.getFrameColors_.bind(this)); + this.throttledUpdateCurrentColors_ = pskl.utils.FunctionUtils.throttle( + this.updateCurrentColors_.bind(this), + 1000 + ); + this.paletteService = pskl.app.paletteService; }; ns.CurrentColorsService.prototype.init = function () { - $.subscribe(Events.HISTORY_STATE_SAVED, this.updateCurrentColors_.bind(this)); + $.subscribe(Events.HISTORY_STATE_SAVED, this.throttledUpdateCurrentColors_); $.subscribe(Events.HISTORY_STATE_LOADED, this.loadColorsFromCache_.bind(this)); }; diff --git a/src/js/service/HistoryService.js b/src/js/service/HistoryService.js index debfd2a..b15e410 100644 --- a/src/js/service/HistoryService.js +++ b/src/js/service/HistoryService.js @@ -24,6 +24,9 @@ // 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)); @@ -58,6 +61,13 @@ 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); }; @@ -92,6 +102,13 @@ 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)) { diff --git a/src/js/utils/FunctionUtils.js b/src/js/utils/FunctionUtils.js index 71a05b1..9c8233e 100644 --- a/src/js/utils/FunctionUtils.js +++ b/src/js/utils/FunctionUtils.js @@ -2,6 +2,9 @@ var ns = $.namespace('pskl.utils'); ns.FunctionUtils = { + /** + * Returns a memoized version of the provided function. + */ memo : function (fn, cache, scope) { var memoized = function () { var key = Array.prototype.join.call(arguments, '-'); @@ -11,6 +14,27 @@ return cache[key]; }; return memoized; + }, + + /** + * Returns a throttled version of the provided method, that will be called at most + * every X milliseconds, where X is the provided interval. + */ + throttle : function (fn, interval) { + var last, timer; + return function () { + var now = Date.now(); + if (last && now < last + interval) { + clearTimeout(timer); + timer = setTimeout(function () { + last = now; + fn(); + }, interval); + } else { + last = now; + fn(); + } + }; } }; })(); diff --git a/src/js/utils/PiskelFileUtils.js b/src/js/utils/PiskelFileUtils.js index 07bf2ed..f9e7028 100644 --- a/src/js/utils/PiskelFileUtils.js +++ b/src/js/utils/PiskelFileUtils.js @@ -11,25 +11,30 @@ * @param {Function} onError NOT USED YET */ loadFromFile : function (file, onSuccess, onError) { - pskl.utils.FileUtils.readFileAsArrayBuffer(file, function (content) { + pskl.utils.FileUtils.readFile(file, function (content) { + var rawPiskel = pskl.utils.Base64.toText(content); ns.PiskelFileUtils.decodePiskelFile( - content, - function (piskel, extra) { + rawPiskel, + function (piskel, descriptor, fps) { // if using Node-Webkit, store the savePath on load // Note: the 'path' property is unique to Node-Webkit, and holds the full path if (pskl.utils.Environment.detectNodeWebkit()) { piskel.savePath = file.path; } - onSuccess(piskel, extra); + onSuccess(piskel, descriptor, fps); }, onError ); }); }, - decodePiskelFile : function (serializedPiskel, onSuccess, onError) { - pskl.utils.serialization.Deserializer.deserialize(serializedPiskel, function (piskel, extra) { - onSuccess(piskel, extra); + decodePiskelFile : function (rawPiskel, onSuccess, onError) { + var serializedPiskel = JSON.parse(rawPiskel); + var fps = serializedPiskel.piskel.fps; + var piskel = serializedPiskel.piskel; + var descriptor = new pskl.model.piskel.Descriptor(piskel.name, piskel.description, true); + pskl.utils.serialization.Deserializer.deserialize(serializedPiskel, function (piskel) { + onSuccess(piskel, descriptor, fps); }); } };