diff --git a/src/js/Constants.js b/src/js/Constants.js index ad93d15d..2f112e3e 100644 --- a/src/js/Constants.js +++ b/src/js/Constants.js @@ -11,6 +11,8 @@ var Constants = { MAX_HEIGHT : 1024, MAX_WIDTH : 1024, + MAX_CURRENT_COLORS_DISPLAYED : 100, + MINIMUM_ZOOM : 1, PREVIEW_FILM_SIZE : 96, diff --git a/src/js/controller/PalettesListController.js b/src/js/controller/PalettesListController.js index 29ea5821..0901d3df 100644 --- a/src/js/controller/PalettesListController.js +++ b/src/js/controller/PalettesListController.js @@ -10,7 +10,6 @@ // I apologize to my future self for this one. var NO_SCROLL_MAX_COLORS = 20; - var MAX_COLORS = 100; ns.PalettesListController = function (paletteController, usedColorService) { this.usedColorService = usedColorService; @@ -80,8 +79,8 @@ } } - if (colors.length > MAX_COLORS) { - colors = colors.slice(0, MAX_COLORS); + if (colors.length > Constants.MAX_CURRENT_COLORS_DISPLAYED) { + colors = colors.slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED); } return colors; diff --git a/src/js/controller/PreviewFilmController.js b/src/js/controller/PreviewFilmController.js index bbca4eda..d61a80c6 100644 --- a/src/js/controller/PreviewFilmController.js +++ b/src/js/controller/PreviewFilmController.js @@ -15,6 +15,10 @@ this.refreshZoom_(); this.redrawFlag = true; + + this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor(); + this.cachedFrameProcessor.setFrameProcessor(this.frameToPreviewCanvas_.bind(this)); + this.cachedFrameProcessor.setOutputCloner(this.clonePreviewCanvas_.bind(this)); }; ns.PreviewFilmController.prototype.init = function() { @@ -39,7 +43,6 @@ ns.PreviewFilmController.prototype.render = function () { if (this.redrawFlag) { - // TODO(vincz): Full redraw on any drawing modification, optimize. this.createPreviews_(); this.redrawFlag = false; } @@ -175,11 +178,8 @@ cloneFrameButton.className = "tile-overlay duplicate-frame-action"; previewTileRoot.appendChild(cloneFrameButton); - var canvasRenderer = new pskl.rendering.CanvasRenderer(currentFrame, this.zoom); - canvasRenderer.drawTransparentAs(Constants.TRANSPARENT_COLOR); - var canvas = canvasRenderer.render(); - canvas.classList.add('tile-view', 'canvas'); - canvasContainer.appendChild(canvas); + + canvasContainer.appendChild(this.getCanvasForFrame(currentFrame)); previewTileRoot.appendChild(canvasContainer); if(tileNumber > 0 || this.piskelController.getFrameCount() > 1) { @@ -206,6 +206,25 @@ return previewTileRoot; }; + ns.PreviewFilmController.prototype.getCanvasForFrame = function (frame) { + var canvas = this.cachedFrameProcessor.get(frame, this.zoom); + return canvas; + }; + + ns.PreviewFilmController.prototype.frameToPreviewCanvas_ = function (frame) { + var canvasRenderer = new pskl.rendering.CanvasRenderer(frame, this.zoom); + canvasRenderer.drawTransparentAs(Constants.TRANSPARENT_COLOR); + var canvas = canvasRenderer.render(); + canvas.classList.add('tile-view', 'canvas'); + return canvas; + }; + + ns.PreviewFilmController.prototype.clonePreviewCanvas_ = function (canvas) { + var clone = pskl.CanvasUtils.clone(canvas); + clone.classList.add('tile-view', 'canvas'); + return clone; + }; + /** * Calculate the preview zoom depending on the piskel size */ diff --git a/src/js/controller/piskel/PiskelController.js b/src/js/controller/piskel/PiskelController.js index a66a5875..b337866e 100644 --- a/src/js/controller/piskel/PiskelController.js +++ b/src/js/controller/piskel/PiskelController.js @@ -77,10 +77,16 @@ }; ns.PiskelController.prototype.getFrameAt = function (index) { + var hash = []; var frames = this.getLayers().map(function (l) { - return l.getFrameAt(index); + var frame = l.getFrameAt(index); + hash.push(frame.getHash()); + return frame; }); - return pskl.utils.FrameUtils.merge(frames); + var mergedFrame = pskl.utils.FrameUtils.merge(frames); + mergedFrame.id = hash.join('-'); + mergedFrame.version = 0; + return mergedFrame; }; ns.PiskelController.prototype.hasFrameAt = function (index) { diff --git a/src/js/controller/settings/SaveController.js b/src/js/controller/settings/SaveController.js index b79a49f2..f3dd63ba 100644 --- a/src/js/controller/settings/SaveController.js +++ b/src/js/controller/settings/SaveController.js @@ -62,12 +62,26 @@ evt.preventDefault(); evt.stopPropagation(); - this.beforeSaving_(); - pskl.app.storageService.store({ - success : this.onSaveSuccess_.bind(this), - error : this.onSaveError_.bind(this), - after : this.afterSaving_.bind(this) - }); + var name = this.getName(); + + if (!name) { + name = window.prompt('Please specify a name', 'New piskel'); + } + + if (name) { + var description = this.getDescription(); + var isPublic = !!this.isPublicCheckbox.prop('checked'); + + var descriptor = new pskl.model.piskel.Descriptor(name, description, isPublic); + this.piskelController.getPiskel().setDescriptor(descriptor); + + this.beforeSaving_(); + pskl.app.storageService.store({ + success : this.onSaveSuccess_.bind(this), + error : this.onSaveError_.bind(this), + after : this.afterSaving_.bind(this) + }); + } }; ns.SaveController.prototype.onSaveLocalClick_ = function (evt) { diff --git a/src/js/drawingtools/Lighten.js b/src/js/drawingtools/Lighten.js index 78a3a7ed..fc897a2e 100644 --- a/src/js/drawingtools/Lighten.js +++ b/src/js/drawingtools/Lighten.js @@ -27,7 +27,9 @@ * @override */ ns.Lighten.prototype.applyToolAt = function(col, row, color, frame, overlay, event, mouseButton) { - var pixelColor = frame.getPixel(col, row); + var overlayColor = overlay.getPixel(col, row); + var frameColor = frame.getPixel(col, row); + var pixelColor = overlayColor === Constants.TRANSPARENT_COLOR ? frameColor : overlayColor; var isDarken = event.ctrlKey || event.cmdKey; var isSinglePass = event.shiftKey; @@ -55,7 +57,11 @@ }; ns.Lighten.prototype.releaseToolAt = function(col, row, color, frame, overlay, event) { + // apply on real frame + this.setPixelsToFrame_(frame, this.pixels); + this.resetUsedPixels_(); + $.publish(Events.PISKEL_SAVE_STATE, { type : pskl.service.HistoryService.SNAPSHOT }); diff --git a/src/js/drawingtools/SimplePen.js b/src/js/drawingtools/SimplePen.js index dd9ab9ce..f36ec667 100644 --- a/src/js/drawingtools/SimplePen.js +++ b/src/js/drawingtools/SimplePen.js @@ -22,12 +22,17 @@ * @override */ ns.SimplePen.prototype.applyToolAt = function(col, row, color, frame, overlay, event) { - frame.setPixel(col, row, color); + overlay.setPixel(col, row, color); + + if (color === Constants.TRANSPARENT_COLOR) { + frame.setPixel(col, row, color); + } this.previousCol = col; this.previousRow = row; this.pixels.push({ col : col, - row : row + row : row, + color : color }); }; @@ -55,17 +60,26 @@ ns.SimplePen.prototype.releaseToolAt = function(col, row, color, frame, overlay, event) { + // apply on real frame + this.setPixelsToFrame_(frame, this.pixels); + + // save state this.raiseSaveStateEvent({ pixels : this.pixels.slice(0), color : color }); + + // reset this.pixels = []; }; ns.SimplePen.prototype.replay = function (frame, replayData) { - var pixels = replayData.pixels; + this.setPixelsToFrame_(frame, replayData.pixels, replayData.color); + }; + + ns.SimplePen.prototype.setPixelsToFrame_ = function (frame, pixels, color) { pixels.forEach(function (pixel) { - frame.setPixel(pixel.col, pixel.row, replayData.color); + frame.setPixel(pixel.col, pixel.row, pixel.color); }); }; })(); diff --git a/src/js/model/frame/CachedFrameProcessor.js b/src/js/model/frame/CachedFrameProcessor.js new file mode 100644 index 00000000..3c0c07aa --- /dev/null +++ b/src/js/model/frame/CachedFrameProcessor.js @@ -0,0 +1,79 @@ +(function () { + var ns = $.namespace('pskl.model.frame'); + + + var DEFAULT_CLEAR_INTERVAL = 10 * 60 *1000; + + var DEFAULT_FRAME_PROCESSOR = function (frame) { + return pskl.utils.FrameUtils.toImage(frame); + }; + + var DEFAULT_OUTPUT_CLONER = function (o) {return o;}; + + var DEFAULT_NAMESPACE = '__cache_default__'; + + ns.CachedFrameProcessor = function (cacheResetInterval) { + this.cache_ = {}; + this.cacheResetInterval = cacheResetInterval || DEFAULT_CLEAR_INTERVAL; + this.frameProcessor = DEFAULT_FRAME_PROCESSOR; + this.outputCloner = DEFAULT_OUTPUT_CLONER; + + window.setInterval(this.clear.bind(this), this.cacheResetInterval); + }; + + ns.CachedFrameProcessor.prototype.clear = function () { + this.cache_ = {}; + }; + + /** + * Set the processor function that will be called when there is a cache miss + * Function with 1 argument : pskl.model.Frame + * @param {Function} frameProcessor + */ + ns.CachedFrameProcessor.prototype.setFrameProcessor = function (frameProcessor) { + this.frameProcessor = frameProcessor; + }; + + /** + * Set the cloner that will be called when there is a miss on the 1st level cache + * but a hit on the 2nd level cache. + * Function with 2 arguments : cached value, frame + * @param {Function} outputCloner + */ + ns.CachedFrameProcessor.prototype.setOutputCloner = function (outputCloner) { + this.outputCloner = outputCloner; + }; + + /** + * Retrieve the processed frame from the cache, in the (optional) namespace + * If the first level cache is empty, attempt to clone it from 2nd level cache. If second level cache is empty process the frame. + * @param {pskl.model.Frame} frame + * @param {String} namespace + * @return {Object} the processed frame + */ + ns.CachedFrameProcessor.prototype.get = function (frame, namespace) { + var processedFrame = null; + namespace = namespace || DEFAULT_NAMESPACE; + + if (!this.cache_[namespace]) { + this.cache_[namespace] = {}; + } + + var cache = this.cache_[namespace]; + + var cacheKey = frame.getHash(); + if (this.cache_[cacheKey]) { + processedFrame = this.cache_[cacheKey]; + } else { + var frameAsString = JSON.stringify(frame.getPixels()); + if (this.cache_[frameAsString]) { + processedFrame = this.outputCloner(this.cache_[frameAsString], frame); + } else { + processedFrame = this.frameProcessor(frame); + this.cache_[frameAsString] = processedFrame; + } + this.cache_[cacheKey] = processedFrame; + } + return processedFrame; + }; +})(); \ No newline at end of file diff --git a/src/js/rendering/frame/TiledFrameRenderer.js b/src/js/rendering/frame/TiledFrameRenderer.js index 4b41bb1d..9eabb8dc 100644 --- a/src/js/rendering/frame/TiledFrameRenderer.js +++ b/src/js/rendering/frame/TiledFrameRenderer.js @@ -8,11 +8,19 @@ this.displayContainer = document.createElement('div'); this.displayContainer.classList.add('tiled-frame-container'); container.get(0).appendChild(this.displayContainer); + + this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor(); + this.cachedFrameProcessor.setFrameProcessor(this.frameToDataUrl_.bind(this)); + }; + + ns.TiledFrameRenderer.prototype.frameToDataUrl_ = function (frame) { + var canvas = new pskl.utils.FrameUtils.toImage(frame, this.zoom); + return canvas.toDataURL('image/png'); }; ns.TiledFrameRenderer.prototype.render = function (frame) { - var canvas = new pskl.utils.FrameUtils.toImage(frame, this.zoom); - this.displayContainer.style.backgroundImage = 'url(' + canvas.toDataURL('image/png') + ')'; + var imageSrc = this.cachedFrameProcessor.get(frame, this.zoom); + this.displayContainer.style.backgroundImage = 'url(' + imageSrc + ')'; }; ns.TiledFrameRenderer.prototype.show = function () { diff --git a/src/js/service/BackupService.js b/src/js/service/BackupService.js index 8b884fbc..59e9a23a 100644 --- a/src/js/service/BackupService.js +++ b/src/js/service/BackupService.js @@ -1,6 +1,6 @@ (function () { var ns = $.namespace('pskl.service'); - var BACKUP_INTERVAL = 1000 * 30; + var BACKUP_INTERVAL = 1000 * 60; ns.BackupService = function (piskelController) { this.piskelController = piskelController; diff --git a/src/js/service/CurrentColorsService.js b/src/js/service/CurrentColorsService.js index e8f282cf..4aed52e7 100644 --- a/src/js/service/CurrentColorsService.js +++ b/src/js/service/CurrentColorsService.js @@ -4,6 +4,9 @@ ns.CurrentColorsService = function (piskelController) { this.piskelController = piskelController; this.currentColors = []; + this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor(); + this.cachedFrameProcessor.setFrameProcessor(this.frameToColors_.bind(this)); + this.framesColorsCache_ = {}; }; @@ -16,33 +19,35 @@ return this.currentColors; }; + ns.CurrentColorsService.prototype.frameToColors_ = function (frame) { + var frameColors = {}; + frame.forEachPixel(function (color, x, y) { + frameColors[color] = (frameColors[color] || 0) + 1; + }); + return frameColors; + }; + + ns.CurrentColorsService.prototype.onPiskelUpdated_ = function (evt) { var layers = this.piskelController.getLayers(); var frames = layers.map(function (l) {return l.getFrames();}).reduce(function (p, n) {return p.concat(n);}); var colors = {}; frames.forEach(function (f) { - var frameHash = f.getHash(); - if (!this.framesColorsCache_[frameHash]) { - var frameColors = {}; - f.forEachPixel(function (color, x, y) { - frameColors[color] = true; - }); - this.framesColorsCache_[frameHash] = frameColors; - } - Object.keys(this.framesColorsCache_[frameHash]).forEach(function (color) { - colors[color] = true; + var frameColors = this.cachedFrameProcessor.get(f); + Object.keys(frameColors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED).forEach(function (color) { + colors[color] = (colors[color] || 0) + frameColors[color]; }); }.bind(this)); + + // Remove transparent color from used colors delete colors[Constants.TRANSPARENT_COLOR]; - this.currentColors = Object.keys(colors); + + // limit the array to the max colors to display + this.currentColors = Object.keys(colors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED); + + // sort by most frequent color this.currentColors = this.currentColors.sort(function (c1, c2) { - if (c1 < c2) { - return -1; - } else if (c1 > c2) { - return 1; - } else { - return 0; - } + return colors[c2] - colors[c1]; }); // TODO : only fire if there was a change diff --git a/src/js/utils/CanvasUtils.js b/src/js/utils/CanvasUtils.js index a6eca20e..8ef9d161 100644 --- a/src/js/utils/CanvasUtils.js +++ b/src/js/utils/CanvasUtils.js @@ -48,6 +48,16 @@ } }, + clone : function (canvas) { + var clone = pskl.CanvasUtils.createCanvas(canvas.width, canvas.height); + + //apply the old canvas to the new one + clone.getContext('2d').drawImage(canvas, 0, 0); + + //return the new canvas + return clone; + }, + getImageDataFromCanvas : function (canvas) { var sourceContext = canvas.getContext('2d'); return sourceContext.getImageData(0, 0, canvas.width, canvas.height).data; diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index bde5fe67..cdad6616 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -48,6 +48,7 @@ "js/model/Frame.js", "js/model/Layer.js", "js/model/piskel/Descriptor.js", + "js/model/frame/CachedFrameProcessor.js", "js/model/Piskel.js", // Selection