diff --git a/css/reset.css b/css/reset.css index b0c09cce..6c81940f 100644 --- a/css/reset.css +++ b/css/reset.css @@ -32,5 +32,5 @@ ul, li { } ::-webkit-scrollbar-track { - background-color: rgba(50, 50, 50, 0.4);; + background-color: rgba(50, 50, 50, 0.4); } \ No newline at end of file diff --git a/css/tools.css b/css/tools.css index 76165c7b..3ee94ddf 100644 --- a/css/tools.css +++ b/css/tools.css @@ -19,7 +19,6 @@ .tool-icon.selected { cursor: default; background-color: #444; - cursor: normal; border: 1px gold solid; margin: 0; } @@ -151,7 +150,7 @@ } -.palette .palette-color.transparent-color { +.palette-color[data-color=TRANSPARENT] { position: relative; top: 10px; left: 10px; diff --git a/js/Constants.js b/js/Constants.js index 5b333e5c..e3d00e21 100644 --- a/js/Constants.js +++ b/js/Constants.js @@ -1,10 +1,13 @@ // TODO(grosbouddha): put under pskl namespace. var Constants = { - DEFAULT_SIZE : { - height : 32, - width : 32 + DEFAULT : { + HEIGHT : 32, + WIDTH : 32, + FPS : 12 }, + MODEL_VERSION : 1, + MAX_HEIGHT : 128, MAX_WIDTH : 128, diff --git a/js/piskel.js b/js/app.js similarity index 73% rename from js/piskel.js rename to js/app.js index 9f32ad99..fb09f694 100644 --- a/js/piskel.js +++ b/js/app.js @@ -4,43 +4,39 @@ */ (function () { var ns = $.namespace("pskl"); - /** - * FrameSheetModel instance. - */ - var frameSheet; /** * Main application controller */ ns.app = { init : function () { - var frameSize = this.readSizeFromURL_(); - frameSheet = new pskl.model.FrameSheet(frameSize.height, frameSize.width); - frameSheet.addEmptyFrame(); - frameSheet.setCurrentFrameIndex(0); + var size = this.readSizeFromURL_(); + var piskel = new pskl.model.Piskel(size.width, size.height, Constants.DEFAULT.FPS); - /** - * True when piskel is running in static mode (no back end needed). - * When started from APP Engine, appEngineToken_ (Boolean) should be set on window.pskl - */ - this.isStaticVersion = !pskl.appEngineToken_; + var layer = new pskl.model.Layer("Default layer"); + var frame = new pskl.model.Frame(size.width, size.height); + layer.addFrame(frame); - this.drawingController = new pskl.controller.DrawingController(frameSheet, $('#drawing-canvas-container')); + piskel.addLayer(layer); + + this.piskelController = new pskl.controller.PiskelController(piskel); + + this.drawingController = new pskl.controller.DrawingController(this.piskelController, $('#drawing-canvas-container')); this.drawingController.init(); - this.animationController = new pskl.controller.AnimatedPreviewController(frameSheet, $('#preview-canvas-container')); + this.animationController = new pskl.controller.AnimatedPreviewController(this.piskelController, $('#preview-canvas-container')); this.animationController.init(); - this.previewsController = new pskl.controller.PreviewFilmController(frameSheet, $('#preview-list')); + this.previewsController = new pskl.controller.PreviewFilmController(this.piskelController, $('#preview-list')); this.previewsController.init(); - this.settingsController = new pskl.controller.SettingsController(frameSheet); + this.settingsController = new pskl.controller.SettingsController(this.piskelController); this.settingsController.init(); - this.selectionManager = new pskl.selection.SelectionManager(frameSheet); + this.selectionManager = new pskl.selection.SelectionManager(this.piskelController); this.selectionManager.init(); - this.historyService = new pskl.service.HistoryService(frameSheet); + this.historyService = new pskl.service.HistoryService(this.piskelController); this.historyService.init(); this.keyboardEventService = new pskl.service.KeyboardEventService(); @@ -49,7 +45,7 @@ this.notificationController = new pskl.controller.NotificationController(); this.notificationController.init(); - this.localStorageService = new pskl.service.LocalStorageService(frameSheet); + this.localStorageService = new pskl.service.LocalStorageService(this.piskelController); this.localStorageService.init(); this.imageUploadService = new pskl.service.ImageUploadService(); @@ -59,17 +55,14 @@ this.toolController.init(); this.paletteController = new pskl.controller.PaletteController(); - this.paletteController.init(frameSheet); + this.paletteController.init(); var drawingLoop = new pskl.rendering.DrawingLoop(); drawingLoop.addCallback(this.render, this); drawingLoop.start(); - // Init (event-delegated) bootstrap tooltips: - $('body').tooltip({ - selector: '[rel=tooltip]' - }); - + this.initBootstrapTooltips_(); + /** * True when piskel is running in static mode (no back end needed). @@ -89,12 +82,18 @@ } } else { if (pskl.framesheetData_ && pskl.framesheetData_.content) { - frameSheet.load(pskl.framesheetData_.content); + this.piskelController.load(pskl.framesheetData_.content); pskl.app.animationController.setFPS(pskl.framesheetData_.fps); } } }, + initBootstrapTooltips_ : function () { + $('body').tooltip({ + selector: '[rel=tooltip]' + }); + }, + render : function (delta) { this.drawingController.render(delta); this.animationController.render(delta); @@ -115,7 +114,10 @@ width : Math.min(width, Constants.MAX_WIDTH) }; } else { - size = Constants.DEFAULT_SIZE; + size = { + height : Constants.DEFAULT.HEIGHT, + width : Constants.DEFAULT.WIDTH + }; } return size; }, @@ -141,10 +143,10 @@ var xhr = new XMLHttpRequest(); xhr.open('GET', Constants.PISKEL_SERVICE_URL + '/get?l=' + frameId, true); xhr.responseType = 'text'; - + var piskelController = this.piskelController; xhr.onload = function (e) { var res = JSON.parse(this.responseText); - frameSheet.load(res.framesheet); + piskelController.deserialize(JSON.stringify(res.framesheet)); pskl.app.animationController.setFPS(res.fps); $.publish(Events.HIDE_NOTIFICATION); }; @@ -156,21 +158,15 @@ xhr.send(); }, - loadFramesheet : function (framesheet) { - frameSheet.load(framesheet); - }, - getFirstFrameAsPNGData_ : function () { - var tmpSheet = new pskl.model.FrameSheet(frameSheet.getHeight(), frameSheet.getWidth()); - tmpSheet.addFrame(frameSheet.getFrameByIndex(0)); - return (new pskl.rendering.SpritesheetRenderer(tmpSheet)).renderAsImageDataSpritesheetPNG(); + throw 'getFirstFrameAsPNGData_ not implemented'; }, // TODO(julz): Create package ? storeSheet : function (event) { var xhr = new XMLHttpRequest(); var formData = new FormData(); - formData.append('framesheet_content', frameSheet.serialize()); + formData.append('framesheet_content', this.piskelController.serialize()); formData.append('fps_speed', $('#preview-fps').val()); if (this.isStaticVersion) { @@ -179,12 +175,12 @@ } else { // additional values only used with latest app-engine backend formData.append('name', $('#piskel-name').val()); - formData.append('frames', frameSheet.getFrameCount()); + formData.append('frames', this.piskelController.getFrameCount()); + // Get image/png data for first frame - formData.append('preview', this.getFirstFrameAsPNGData_()); - var imageData = (new pskl.rendering.SpritesheetRenderer(frameSheet)).renderAsImageDataSpritesheetPNG(); + var imageData = (new pskl.rendering.SpritesheetRenderer(this.piskelController)).renderAsImageDataSpritesheetPNG(); formData.append('framesheet', imageData); xhr.open('POST', "save", true); @@ -214,25 +210,16 @@ return false; }, - uploadAsAnimatedGIF : function () { - var fps = pskl.app.animationController.fps; - var renderer = new pskl.rendering.SpritesheetRenderer(frameSheet); - - renderer.renderAsImageDataAnimatedGIF(fps, function (imageData) { - this.imageUploadService.upload(imageData, this.openWindow); - }.bind(this)); - }, - uploadAsSpritesheetPNG : function () { - var imageData = (new pskl.rendering.SpritesheetRenderer(frameSheet)).renderAsImageDataSpritesheetPNG(); - this.imageUploadService.upload(imageData, this.openWindow); + var imageData = (new pskl.rendering.SpritesheetRenderer(this.piskelController)).renderAsImageDataSpritesheetPNG(); + this.imageUploadService.upload(imageData, this.openWindow.bind(this)); }, openWindow : function (url) { var options = [ "dialog=yes", "scrollbars=no", "status=no", - "width=" + frameSheet.getWidth() * frameSheet.getFrameCount(), - "height=" + frameSheet.getHeight() + "width=" + this.piskelController.getWidth() * this.piskelController.getFrameCount(), + "height=" + this.piskelController.getHeight() ].join(","); window.open(url, "piskel-export", options); diff --git a/js/controller/AnimatedPreviewController.js b/js/controller/AnimatedPreviewController.js index 9b50961a..2cb235a9 100644 --- a/js/controller/AnimatedPreviewController.js +++ b/js/controller/AnimatedPreviewController.js @@ -1,7 +1,7 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.AnimatedPreviewController = function (framesheet, container, dpi) { - this.framesheet = framesheet; + ns.AnimatedPreviewController = function (piskelController, container, dpi) { + this.piskelController = piskelController; this.container = container; this.elapsedTime = 0; @@ -39,11 +39,11 @@ var index = Math.floor(this.elapsedTime / (1000/this.fps)); if (index != this.currentIndex) { this.currentIndex = index; - if (!this.framesheet.hasFrameAtIndex(this.currentIndex)) { + if (!this.piskelController.hasFrameAt(this.currentIndex)) { this.currentIndex = 0; this.elapsedTime = 0; } - this.renderer.render(this.framesheet.getFrameByIndex(this.currentIndex)); + this.renderer.render(this.piskelController.getFrameAt(this.currentIndex)); } }; @@ -52,8 +52,8 @@ */ ns.AnimatedPreviewController.prototype.calculateDPI_ = function () { var previewSize = 200, - framePixelHeight = this.framesheet.getCurrentFrame().getHeight(), - framePixelWidth = this.framesheet.getCurrentFrame().getWidth(); + framePixelHeight = this.piskelController.getCurrentFrame().getHeight(), + framePixelWidth = this.piskelController.getCurrentFrame().getWidth(); // TODO (julz) : should have a utility to get a Size from framesheet easily (what about empty framesheets though ?) //return pskl.PixelUtils.calculateDPIForContainer($(".preview-container"), framePixelHeight, framePixelWidth); diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index fdedeb2c..f227f623 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -1,15 +1,15 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.DrawingController = function (framesheet, container) { + ns.DrawingController = function (piskelController, container) { /** * @public */ - this.framesheet = framesheet; + this.piskelController = piskelController; /** * @public */ - this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(framesheet.getCurrentFrame()); + this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(piskelController.getCurrentFrame()); /** * @private @@ -36,7 +36,7 @@ }; ns.DrawingController.prototype.init = function () { - this.renderer.render(this.framesheet.getCurrentFrame()); + this.renderer.render(this.piskelController.getCurrentFrame()); this.overlayRenderer.render(this.overlayFrame); this.initMouseBehavior(); @@ -114,7 +114,7 @@ this.currentToolBehavior.applyToolAt( coords.col, coords.row, this.getCurrentColor_(), - this.framesheet.getCurrentFrame(), + this.piskelController.getCurrentFrame(), this.overlayFrame, this.wrapEvtInfo_(event) ); @@ -135,7 +135,7 @@ this.currentToolBehavior.moveToolAt( coords.col, coords.row, this.getCurrentColor_(), - this.framesheet.getCurrentFrame(), + this.piskelController.getCurrentFrame(), this.overlayFrame, this.wrapEvtInfo_(event) ); @@ -149,7 +149,7 @@ this.currentToolBehavior.moveUnactiveToolAt( coords.col, coords.row, this.getCurrentColor_(), - this.framesheet.getCurrentFrame(), + this.piskelController.getCurrentFrame(), this.overlayFrame, this.wrapEvtInfo_(event) ); @@ -176,7 +176,7 @@ this.currentToolBehavior.releaseToolAt( coords.col, coords.row, this.getCurrentColor_(), - this.framesheet.getCurrentFrame(), + this.piskelController.getCurrentFrame(), this.overlayFrame, this.wrapEvtInfo_(event) ); @@ -247,7 +247,7 @@ }; ns.DrawingController.prototype.renderFrame = function () { - var frame = this.framesheet.getCurrentFrame(); + var frame = this.piskelController.getCurrentFrame(); var serializedFrame = frame.serialize(); if (this.serializedFrame != serializedFrame) { if (!frame.isSameSize(this.overlayFrame)) { @@ -278,8 +278,8 @@ leftSectionWidth = $('.left-column').outerWidth(true), rightSectionWidth = $('.right-column').outerWidth(true), availableViewportWidth = $('#main-wrapper').width() - leftSectionWidth - rightSectionWidth, - framePixelHeight = this.framesheet.getCurrentFrame().getHeight(), - framePixelWidth = this.framesheet.getCurrentFrame().getWidth(); + framePixelHeight = this.piskelController.getCurrentFrame().getHeight(), + framePixelWidth = this.piskelController.getCurrentFrame().getWidth(); if (pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)) { availableViewportWidth = availableViewportWidth - (framePixelWidth * Constants.GRID_STROKE_WIDTH); @@ -300,7 +300,7 @@ this.renderer.updateDPI(dpi); this.overlayRenderer.updateDPI(dpi); - var currentFrameHeight = this.framesheet.getCurrentFrame().getHeight(); + var currentFrameHeight = this.piskelController.getCurrentFrame().getHeight(); var canvasHeight = currentFrameHeight * dpi; if (pskl.UserSettings.get(pskl.UserSettings.SHOW_GRID)) { canvasHeight += Constants.GRID_STROKE_WIDTH * currentFrameHeight; diff --git a/js/controller/NotificationController.js b/js/controller/NotificationController.js index 15c106cf..8b3ba25a 100644 --- a/js/controller/NotificationController.js +++ b/js/controller/NotificationController.js @@ -3,6 +3,14 @@ ns.NotificationController = function () {}; + /** + * @public + */ + ns.NotificationController.prototype.init = function() { + $.subscribe(Events.SHOW_NOTIFICATION, $.proxy(this.displayMessage_, this)); + $.subscribe(Events.HIDE_NOTIFICATION, $.proxy(this.removeMessage_, this)); + }; + /** * @private */ @@ -28,12 +36,4 @@ message.remove(); } }; - - /** - * @public - */ - ns.NotificationController.prototype.init = function() { - $.subscribe(Events.SHOW_NOTIFICATION, $.proxy(this.displayMessage_, this)); - $.subscribe(Events.HIDE_NOTIFICATION, $.proxy(this.removeMessage_, this)); - }; })(); diff --git a/js/controller/PaletteController.js b/js/controller/PaletteController.js index 8eb5a960..813f8cb9 100644 --- a/js/controller/PaletteController.js +++ b/js/controller/PaletteController.js @@ -1,9 +1,35 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.PaletteController = function () { - this.paletteRoot = null; - this.paletteColors = []; + ns.PaletteController = function () {}; + + /** + * @public + */ + ns.PaletteController.prototype.init = function() { + var transparentColorPalette = $(".palette-color[data-color=TRANSPARENT]"); + transparentColorPalette.mouseup($.proxy(this.onPaletteColorClick_, this)); + + $.subscribe(Events.PRIMARY_COLOR_UPDATED, $.proxy(function(evt, color) { + this.updateColorPicker_(color, $('#color-picker')); + this.addColorToPalette_(color); + }, this)); + + $.subscribe(Events.SECONDARY_COLOR_UPDATED, $.proxy(function(evt, color) { + this.updateColorPicker_(color, $('#secondary-color-picker')); + this.addColorToPalette_(color); + }, this)); + + // Initialize colorpickers: + var colorPicker = $('#color-picker'); + colorPicker.val(Constants.DEFAULT_PEN_COLOR); + colorPicker.change({isPrimary : true}, $.proxy(this.onPickerChange_, this)); + + + var secondaryColorPicker = $('#secondary-color-picker'); + secondaryColorPicker.val(Constants.TRANSPARENT_COLOR); + secondaryColorPicker.change({isPrimary : false}, $.proxy(this.onPickerChange_, this)); + }; /** @@ -13,29 +39,10 @@ var inputPicker = $(evt.target); if(evt.data.isPrimary) { $.publish(Events.PRIMARY_COLOR_SELECTED, [inputPicker.val()]); - } - else { + } else { $.publish(Events.SECONDARY_COLOR_SELECTED, [inputPicker.val()]); } }; - - /** - * @private - */ - ns.PaletteController.prototype.addColorToPalette_ = function (color) { - if (this.paletteColors.indexOf(color) == -1 && color != Constants.TRANSPARENT_COLOR) { - this.paletteColors.push(color); - } - }; - - /** - * @private - */ - ns.PaletteController.prototype.addColorsToPalette_ = function (colors) { - for(var color in colors) { - this.addColorToPalette_(color); - } - }; /** * @private @@ -71,45 +78,6 @@ colorPicker[0].color.fromString(color); } }; - - /** - * @public - */ - ns.PaletteController.prototype.init = function(framesheet) { - - this.paletteRoot = $("#palette"); - this.framesheet = framesheet; - - // Initialize palette: - this.addColorsToPalette_(this.framesheet.getUsedColors()); - - $.subscribe(Events.FRAMESHEET_RESET, $.proxy(function(evt) { - this.addColorsToPalette_(this.framesheet.getUsedColors()); - }, this)); - - this.paletteRoot.mouseup($.proxy(this.onPaletteColorClick_, this)); - - $.subscribe(Events.PRIMARY_COLOR_UPDATED, $.proxy(function(evt, color) { - this.updateColorPicker_(color, $('#color-picker')); - this.addColorToPalette_(color); - }, this)); - - $.subscribe(Events.SECONDARY_COLOR_UPDATED, $.proxy(function(evt, color) { - this.updateColorPicker_(color, $('#secondary-color-picker')); - this.addColorToPalette_(color); - }, this)); - - // Initialize colorpickers: - var colorPicker = $('#color-picker'); - colorPicker.val(Constants.DEFAULT_PEN_COLOR); - colorPicker.change({isPrimary : true}, $.proxy(this.onPickerChange_, this)); - - - var secondaryColorPicker = $('#secondary-color-picker'); - secondaryColorPicker.val(Constants.TRANSPARENT_COLOR); - secondaryColorPicker.change({isPrimary : false}, $.proxy(this.onPickerChange_, this)); - - }; })(); diff --git a/js/controller/PiskelController.js b/js/controller/PiskelController.js new file mode 100644 index 00000000..6ad47e65 --- /dev/null +++ b/js/controller/PiskelController.js @@ -0,0 +1,125 @@ +(function () { + var ns = $.namespace('pskl.controller'); + + ns.PiskelController = function (piskel) { + this.setPiskel(piskel); + }; + + ns.PiskelController.prototype.setPiskel = function (piskel) { + this.piskel = piskel; + this.currentLayerIndex = 0; + this.currentFrameIndex = 0; + + $.publish(Events.FRAMESHEET_RESET); + $.publish(Events.FRAME_SIZE_CHANGED); + }; + + ns.PiskelController.prototype.getHeight = function () { + return this.piskel.getHeight(); + }; + + ns.PiskelController.prototype.getWidth = function () { + return this.piskel.getWidth(); + }; + + ns.PiskelController.prototype.getCurrentLayer = function () { + return this.piskel.getLayerAt(this.currentLayerIndex); + }; + + ns.PiskelController.prototype.getCurrentFrame = function () { + var layer = this.getCurrentLayer(); + return layer.getFrameAt(this.currentFrameIndex); + }; + + ns.PiskelController.prototype.getFrameAt = function (index) { + var frames = this.piskel.getLayers().map(function (l) { + return l.getFrameAt(index); + }); + return pskl.utils.FrameUtils.merge(frames); + }; + + ns.PiskelController.prototype.hasFrameAt = function (index) { + return !!this.getCurrentLayer().getFrameAt(index); + }; + + // backward from framesheet + ns.PiskelController.prototype.getFrameByIndex = + ns.PiskelController.prototype.getMergedFrameAt; + + ns.PiskelController.prototype.addEmptyFrame = function () { + var layers = this.piskel.getLayers(); + layers.forEach(function (l) { + l.addFrame(this.createEmptyFrame_()); + }.bind(this)); + }; + + ns.PiskelController.prototype.createEmptyFrame_ = function () { + var w = this.piskel.getWidth(), h = this.piskel.getHeight(); + return new pskl.model.Frame(w, h); + }; + + ns.PiskelController.prototype.removeFrameAt = function (index) { + var layers = this.piskel.getLayers(); + layers.forEach(function (l) { + l.removeFrameAt(index); + }); + // Current frame index is impacted if the removed frame was before the current frame + if (this.currentFrameIndex >= index) { + this.setCurrentFrameIndex(this.currentFrameIndex - 1); + } + + $.publish(Events.FRAMESHEET_RESET); + }; + + ns.PiskelController.prototype.duplicateFrameAt = function (index) { + var layers = this.piskel.getLayers(); + layers.forEach(function (l) { + l.duplicateFrameAt(index); + }); + }; + + ns.PiskelController.prototype.moveFrame = function (fromIndex, toIndex) { + var layers = this.piskel.getLayers(); + layers.forEach(function (l) { + l.moveFrame(fromIndex, toIndex); + }); + }; + + ns.PiskelController.prototype.getFrameCount = function () { + var layer = this.piskel.getLayerAt(0); + return layer.length(); + }; + + ns.PiskelController.prototype.setCurrentFrameIndex = function (index) { + this.currentFrameIndex = index; + $.publish(Events.FRAMESHEET_RESET); + }; + + ns.PiskelController.prototype.setCurrentLayerIndex = function (index) { + this.currentLayerIndex = index; + $.publish(Events.FRAMESHEET_RESET); + }; + + ns.PiskelController.prototype.createLayer = function (name) { + var layer = new pskl.model.Layer(name); + for (var i = 0 ; i < this.getFrameCount() ; i++) { + layer.addFrame(this.createEmptyFrame_()); + } + this.piskel.addLayer(layer); + this.setCurrentLayerIndex(this.piskel.getLayers().length - 1); + }; + + ns.PiskelController.prototype.serialize = function () { + return pskl.utils.Serializer.serializePiskel(this.piskel); + }; + + ns.PiskelController.prototype.deserialize = function (json) { + try { + var piskel = pskl.utils.Serializer.deserializePiskel(json); + this.setPiskel(piskel); + } catch (e) { + console.error('Failed to deserialize'); + console.error(e.stack); + } + }; +})(); \ No newline at end of file diff --git a/js/controller/PreviewFilmController.js b/js/controller/PreviewFilmController.js index b4e6f0f5..3172f212 100644 --- a/js/controller/PreviewFilmController.js +++ b/js/controller/PreviewFilmController.js @@ -1,8 +1,8 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.PreviewFilmController = function (framesheet, container, dpi) { + ns.PreviewFilmController = function (piskelController, container, dpi) { - this.framesheet = framesheet; + this.piskelController = piskelController; this.container = container; this.dpi = this.calculateDPI_(); @@ -19,8 +19,8 @@ }; ns.PreviewFilmController.prototype.addFrame = function () { - this.framesheet.addEmptyFrame(); - this.framesheet.setCurrentFrameIndex(this.framesheet.getFrameCount() - 1); + this.piskelController.addEmptyFrame(); + this.piskelController.setCurrentFrameIndex(this.piskelController.getFrameCount() - 1); this.updateScrollerOverflows(); }; @@ -68,7 +68,7 @@ // Manually remove tooltips since mouseout events were shortcut by the DOM refresh: $(".tooltip").remove(); - var frameCount = this.framesheet.getFrameCount(); + var frameCount = this.piskelController.getFrameCount(); for (var i = 0, l = frameCount; i < l ; i++) { this.container.append(this.createPreviewTile_(i)); @@ -110,8 +110,8 @@ var originFrameId = parseInt(ui.item.data("tile-number"), 10); var targetInsertionId = $('.preview-tile').index(ui.item); - this.framesheet.moveFrame(originFrameId, targetInsertionId); - this.framesheet.setCurrentFrameIndex(targetInsertionId); + this.piskelController.moveFrame(originFrameId, targetInsertionId); + this.piskelController.setCurrentFrameIndex(targetInsertionId); // TODO(grosbouddha): move localstorage request to the model layer? $.publish(Events.LOCALSTORAGE_REQUEST); @@ -123,13 +123,13 @@ * TODO(vincz): clean this giant rendering function & remove listeners. */ ns.PreviewFilmController.prototype.createPreviewTile_ = function(tileNumber) { - var currentFrame = this.framesheet.getFrameByIndex(tileNumber); + var currentFrame = this.piskelController.getCurrentLayer().getFrameAt(tileNumber); var previewTileRoot = document.createElement("li"); var classname = "preview-tile"; previewTileRoot.setAttribute("data-tile-number", tileNumber); - if (this.framesheet.getCurrentFrame() == currentFrame) { + if (this.piskelController.getCurrentFrame() == currentFrame) { classname += " selected"; } previewTileRoot.className = classname; @@ -159,7 +159,7 @@ previewTileRoot.appendChild(canvasContainer); - if(tileNumber > 0 || this.framesheet.getFrameCount() > 1) { + if(tileNumber > 0 || this.piskelController.getFrameCount() > 1) { // Add 'remove frame' button. var deleteButton = document.createElement("button"); deleteButton.setAttribute('rel', 'tooltip'); @@ -186,28 +186,28 @@ ns.PreviewFilmController.prototype.onPreviewClick_ = function (index, evt) { // has not class tile-action: if(!evt.target.classList.contains('tile-overlay')) { - this.framesheet.setCurrentFrameIndex(index); + this.piskelController.setCurrentFrameIndex(index); } }; ns.PreviewFilmController.prototype.onDeleteButtonClick_ = function (index, evt) { - this.framesheet.removeFrameByIndex(index); + this.piskelController.removeFrameAt(index); $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model this.updateScrollerOverflows(); }; ns.PreviewFilmController.prototype.onAddButtonClick_ = function (index, evt) { - this.framesheet.duplicateFrameByIndex(index); + this.piskelController.duplicateFrameAt(index); $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model - this.framesheet.setCurrentFrameIndex(index + 1); + this.piskelController.setCurrentFrameIndex(index + 1); this.updateScrollerOverflows(); }; /** - * Calculate the preview DPI depending on the framesheet size + * Calculate the preview DPI depending on the piskel size */ ns.PreviewFilmController.prototype.calculateDPI_ = function () { - var curFrame = this.framesheet.getCurrentFrame(), + var curFrame = this.piskelController.getCurrentFrame(), frameHeight = curFrame.getHeight(), frameWidth = curFrame.getWidth(), maxFrameDim = Math.max(frameWidth, frameHeight); diff --git a/js/controller/SettingsController.js b/js/controller/SettingsController.js index 2a5b4c01..853e4852 100644 --- a/js/controller/SettingsController.js +++ b/js/controller/SettingsController.js @@ -15,8 +15,8 @@ var SEL_SETTING_CLS = 'has-expanded-drawer'; var EXP_DRAWER_CLS = 'expanded'; - ns.SettingsController = function (framesheet) { - this.framesheet = framesheet; + ns.SettingsController = function (piskelController) { + this.piskelController = piskelController; this.drawerContainer = document.getElementById("drawer-container"); this.settingsContainer = $('[data-pskl-controller=settings]'); this.expanded = false; @@ -48,7 +48,7 @@ ns.SettingsController.prototype.loadSetting = function (setting) { this.drawerContainer.innerHTML = pskl.utils.Template.get(settings[setting].template); - (new settings[setting].controller(this.framesheet)).init(); + (new settings[setting].controller(this.piskelController)).init(); this.settingsContainer.addClass(EXP_DRAWER_CLS); diff --git a/js/controller/settings/GifExportController.js b/js/controller/settings/GifExportController.js index ff59abf2..42360c2e 100644 --- a/js/controller/settings/GifExportController.js +++ b/js/controller/settings/GifExportController.js @@ -1,7 +1,7 @@ (function () { var ns = $.namespace("pskl.controller.settings"); - ns.GifExportController = function (framesheet) { - this.framesheet = framesheet; + ns.GifExportController = function (piskelController) { + this.piskelController = piskelController; }; ns.GifExportController.prototype.init = function () { @@ -61,7 +61,7 @@ }; ns.GifExportController.prototype.createRadioForDpi_ = function (dpi, template) { - var label = dpi[0]*this.framesheet.getWidth() + "x" + dpi[0]*this.framesheet.getHeight(); + var label = dpi[0]*this.piskelController.getWidth() + "x" + dpi[0]*this.piskelController.getHeight(); var value = dpi[0]; var radioHTML = pskl.utils.Template.replace(template, {value : value, label : label}); var radio = pskl.utils.Template.createFromHTML(radioHTML); @@ -86,12 +86,12 @@ var gif = new window.GIF({ workers: 2, quality: 10, - width: this.framesheet.getWidth()*dpi, - height: this.framesheet.getHeight()*dpi + width: this.piskelController.getWidth()*dpi, + height: this.piskelController.getHeight()*dpi }); - for (var i = 0; i < this.framesheet.frames.length; i++) { - var frame = this.framesheet.frames[i]; + for (var i = 0; i < this.piskelController.getFrameCount(); i++) { + var frame = this.piskelController.getFrameAt(i); var renderer = new pskl.rendering.CanvasRenderer(frame, dpi); gif.addFrame(renderer.render(), { delay: 1000 / fps diff --git a/js/model/Frame.js b/js/model/Frame.js index c94dd8eb..1a4f7b41 100644 --- a/js/model/Frame.js +++ b/js/model/Frame.js @@ -1,15 +1,28 @@ (function () { var ns = $.namespace("pskl.model"); - ns.Frame = function (pixels) { - this.pixels = pixels; - this.previousStates = [this.getPixels()]; - this.stateIndex = 0; + ns.Frame = function (width, height) { + if (width && height) { + this.width = width; + this.height = height; + + this.pixels = ns.Frame.createEmptyPixelGrid_(width, height); + this.previousStates = [this.getPixels()]; + this.stateIndex = 0; + } else { + throw 'Bad arguments in pskl.model.Frame constructor : ' + width + ', ' + height; + } }; - ns.Frame.createEmpty = function (width, height) { - var pixels = ns.Frame.createEmptyPixelGrid_(width, height); - return new ns.Frame(pixels); + ns.Frame.fromPixelGrid = function (pixels) { + if (pixels.length && pixels[0].length) { + var w = pixels.length, h = pixels[0].length; + var frame = new pskl.model.Frame(w, h); + frame.setPixels(pixels); + return frame; + } else { + throw 'Bad arguments in pskl.model.Frame.fromPixelGrid : ' + pixels; + } }; ns.Frame.createEmptyPixelGrid_ = function (width, height) { @@ -25,11 +38,13 @@ }; ns.Frame.createEmptyFromFrame = function (frame) { - return ns.Frame.createEmpty(frame.getWidth(), frame.getHeight()); + return new ns.Frame(frame.getWidth(), frame.getHeight()); }; ns.Frame.prototype.clone = function () { - return new ns.Frame(this.getPixels()); + var clone = new ns.Frame(this.width, this.height); + clone.setPixels(this.getPixels()); + return clone; }; /** @@ -46,8 +61,6 @@ this.pixels = this.clonePixels_(pixels); }; - - ns.Frame.prototype.clear = function () { var pixels = ns.Frame.createEmptyPixelGrid_(this.getWidth(), this.getHeight()); this.setPixels(pixels); @@ -77,12 +90,20 @@ return this.pixels[col][row]; }; + ns.Frame.prototype.forEachPixel = function (callback) { + for (var col = 0 ; col < this.getWidth() ; col++) { + for (var row = 0 ; row < this.getHeight() ; row++) { + callback(this.getPixel(col, row), col, row); + } + } + }; + ns.Frame.prototype.getWidth = function () { - return this.pixels.length; + return this.width; }; ns.Frame.prototype.getHeight = function () { - return this.pixels[0].length; + return this.height; }; ns.Frame.prototype.containsPixel = function (col, row) { diff --git a/js/model/FrameSheet.js b/js/model/FrameSheet.js index feb62056..f8f39b93 100644 --- a/js/model/FrameSheet.js +++ b/js/model/FrameSheet.js @@ -23,14 +23,14 @@ this.frames.push(frame); }; - ns.FrameSheet.prototype.getFrameCount = function () { - return this.frames.length; - }; - ns.FrameSheet.prototype.getCurrentFrame = function () { return this.frames[this.currentFrameIndex]; }; + ns.FrameSheet.prototype.getFrameCount = function () { + return this.frames.length; + }; + ns.FrameSheet.prototype.setCurrentFrameIndex = function (index) { this.currentFrameIndex = index; $.publish(Events.CURRENT_FRAME_SET, [this.getCurrentFrame()]); @@ -101,7 +101,7 @@ ns.FrameSheet.prototype.hasFrameAtIndex = function(index) { - return (index >= 0 && index < this.getFrameCount()); + return (index >= 0 && index < this.frames.length); }; ns.FrameSheet.prototype.getFrameByIndex = function(index) { @@ -122,10 +122,9 @@ } this.frames.splice(index, 1); - // Current frame index might not be valid anymore - if (!this.hasFrameAtIndex(this.currentFrameIndex)) { - // if not select last frame available - this.setCurrentFrameIndex(this.getFrameCount() - 1); + // Current frame index is impacted if the removed frame was before the current frame + if (this.currentFrameIndex >= index) { + this.setCurrentFrameIndex(this.currentFrameIndex - 1); } $.publish(Events.FRAMESHEET_RESET); diff --git a/js/model/Layer.js b/js/model/Layer.js new file mode 100644 index 00000000..57b5f66b --- /dev/null +++ b/js/model/Layer.js @@ -0,0 +1,82 @@ +(function () { + var ns = $.namespace('pskl.model'); + + ns.Layer = function (name) { + if (!name) { + throw 'Invalid arguments in Layer constructor : \'name\' is mandatory'; + } else { + this.name = name; + this.frames = []; + } + }; + + ns.Layer.prototype.getName = function () { + return this.name; + }; + + ns.Layer.prototype.getFrames = function () { + return this.frames; + }; + + ns.Layer.prototype.getFrameAt = function (index) { + return this.frames[index]; + }; + + ns.Layer.prototype.addFrame = function (frame) { + this.frames.push(frame); + }; + + ns.Layer.prototype.addFrameAt = function (frame, index) { + this.frames.splice(index, 0, frame); + }; + + ns.Layer.prototype.removeFrame = function (frame) { + var index = this.frames.indexOf(frame); + this.removeFrameAt(index); + }; + + ns.Layer.prototype.removeFrameAt = function (index) { + if (this.frames[index]) { + this.frames.splice(index, 1); + } else { + throw 'Invalid index in removeFrameAt : ' + index + ' (size : ' + this.length() + ')'; + } + }; + + ns.Layer.prototype.moveFrame = function (fromIndex, toIndex) { + var frame = this.frames.splice(fromIndex, 1)[0]; + this.frames.splice(toIndex, 0, frame); + }; + + ns.Layer.prototype.swapFramesAt = function (fromIndex, toIndex) { + var fromFrame = this.frames[fromIndex]; + var toFrame = this.frames[toIndex]; + if (fromFrame && toFrame) { + this.frames[toIndex] = fromFrame; + this.frames[fromIndex] = toFrame; + } else { + console.log('frames', this.frames); + console.log('fromIndex', fromIndex, 'toIndex', toIndex); + throw 'Frame not found in moveFrameAt'; + } + }; + + ns.Layer.prototype.duplicateFrame = function (frame) { + var index = this.frames.indexOf(frame); + this.duplicateFrameAt(); + }; + + ns.Layer.prototype.duplicateFrameAt = function (index) { + var frame = this.frames[index]; + if (frame) { + var clone = frame.clone(); + this.addFrameAt(clone, index); + } else { + throw 'Frame not found in duplicateFrameAt'; + } + }; + + ns.Layer.prototype.length = function () { + return this.frames.length; + }; +})(); \ No newline at end of file diff --git a/js/model/Piskel.js b/js/model/Piskel.js new file mode 100644 index 00000000..635a6ed7 --- /dev/null +++ b/js/model/Piskel.js @@ -0,0 +1,66 @@ +(function () { + var ns = $.namespace('pskl.model'); + + /** + * @constructor + * @param {Number} width + * @param {Number} height + */ + ns.Piskel = function (width, height, fps) { + if (width && height && fps) { + /** @type {Array} */ + this.layers = []; + + /** @type {Number} */ + this.fps = fps; + + /** @type {Number} */ + this.width = width; + + /** @type {Number} */ + this.height = height; + } else { + throw 'Missing arguments in Piskel constructor : ' + Array.prototype.join.call(arguments, ","); + } + }; + + ns.Piskel.prototype.getLayers = function () { + return this.layers; + }; + + ns.Piskel.prototype.getHeight = function () { + return this.height; + }; + + ns.Piskel.prototype.getWidth = function () { + return this.width; + }; + + ns.Piskel.prototype.getFps = function () { + return this.fps; + }; + + ns.Piskel.prototype.getLayers = function () { + return this.layers; + }; + + ns.Piskel.prototype.getLayerAt = function (index) { + return this.layers[index]; + }; + + ns.Piskel.prototype.addLayer = function (layer) { + this.layers.push(layer); + }; + + ns.Piskel.prototype.removeLayer = function (layer) { + var index = this.layers.indexOf(layer); + if (index != -1) { + this.layers.splice(index, 1); + } + }; + + ns.Piskel.prototype.removeLayerAt = function (index) { + this.layers.splice(index, 1); + }; + +})(); \ No newline at end of file diff --git a/js/rendering/SpritesheetRenderer.js b/js/rendering/SpritesheetRenderer.js index 0a966e42..8162f7fa 100644 --- a/js/rendering/SpritesheetRenderer.js +++ b/js/rendering/SpritesheetRenderer.js @@ -2,15 +2,15 @@ var ns = $.namespace("pskl.rendering"); - ns.SpritesheetRenderer = function (framesheet) { - this.framesheet = framesheet; + ns.SpritesheetRenderer = function (piskelController) { + this.piskelController = piskelController; }; ns.SpritesheetRenderer.prototype.renderAsImageDataSpritesheetPNG = function () { var canvas = this.createCanvas_(); - for (var i = 0 ; i < this.framesheet.getFrameCount() ; i++) { - var frame = this.framesheet.getFrameByIndex(i); - this.drawFrameInCanvas_(frame, canvas, i * this.framesheet.getWidth(), 0); + for (var i = 0 ; i < this.piskelController.getFrameCount() ; i++) { + var frame = this.piskelController.getFrameAt(i); + this.drawFrameInCanvas_(frame, canvas, i * this.piskelController.getWidth(), 0); } return canvas.toDataURL("image/png"); }; @@ -32,10 +32,10 @@ }; ns.SpritesheetRenderer.prototype.createCanvas_ = function () { - var frameCount = this.framesheet.getFrameCount(); + var frameCount = this.piskelController.getFrameCount(); if (frameCount > 0){ - var width = frameCount * this.framesheet.getWidth(); - var height = this.framesheet.getHeight(); + var width = frameCount * this.piskelController.getWidth(); + var height = this.piskelController.getHeight(); return pskl.CanvasUtils.createCanvas(width, height); } else { throw "Cannot render empty Spritesheet"; diff --git a/js/selection/SelectionManager.js b/js/selection/SelectionManager.js index 55362688..9a9273ba 100644 --- a/js/selection/SelectionManager.js +++ b/js/selection/SelectionManager.js @@ -1,9 +1,9 @@ (function () { var ns = $.namespace("pskl.selection"); - ns.SelectionManager = function (framesheet) { + ns.SelectionManager = function (piskelController) { - this.framesheet = framesheet; + this.piskelController = piskelController; this.currentSelection = null; }; @@ -52,10 +52,10 @@ ns.SelectionManager.prototype.onCut_ = function(evt) { if(this.currentSelection) { // Put cut target into the selection: - this.currentSelection.fillSelectionFromFrame(this.framesheet.getCurrentFrame()); + this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame()); var pixels = this.currentSelection.pixels; - var currentFrame = this.framesheet.getCurrentFrame(); + var currentFrame = this.piskelController.getCurrentFrame(); for(var i=0, l=pixels.length; i"; + if(piskelController === undefined) { + throw "Bad LocalStorageService initialization: "; } - this.framesheet = framesheet_; + this.piskelController = piskelController; this.localStorageThrottler_ = null; }; /** * @public */ - ns.LocalStorageService.prototype.init = function(framesheet_) { + ns.LocalStorageService.prototype.init = function(piskelController) { $.subscribe(Events.LOCALSTORAGE_REQUEST, $.proxy(this.persistToLocalStorageRequest_, this)); }; @@ -38,7 +38,7 @@ ns.LocalStorageService.prototype.persistToLocalStorage_ = function() { console.log('[LocalStorage service]: Snapshot stored'); - window.localStorage.snapShot = this.framesheet.serialize(); + window.localStorage.snapShot = this.piskelController.serialize(); }; /** @@ -46,8 +46,8 @@ */ ns.LocalStorageService.prototype.restoreFromLocalStorage_ = function() { - this.framesheet.deserialize(window.localStorage.snapShot); - this.framesheet.setCurrentFrameIndex(0); + this.piskelController.deserialize(window.localStorage.snapShot); + this.piskelController.setCurrentFrameIndex(0); }; /** diff --git a/js/utils/FrameUtils.js b/js/utils/FrameUtils.js new file mode 100644 index 00000000..33f262e4 --- /dev/null +++ b/js/utils/FrameUtils.js @@ -0,0 +1,22 @@ +(function () { + var ns = $.namespace('pskl.utils'); + + ns.FrameUtils = { + merge : function (frames) { + var merged = frames[0].clone(); + var w = merged.getWidth(), h = merged.getHeight(); + for (var i = 1 ; i < frames.length ; i++) { + pskl.utils.FrameUtils.mergeFrames_(merged, frames[i]); + } + return merged; + }, + + mergeFrames_ : function (frameA, frameB) { + frameB.forEachPixel(function (p, col, row) { + if (p != Constants.TRANSPARENT_COLOR) { + frameA.setPixel(col, row, p); + } + }); + } + }; +})(); \ No newline at end of file diff --git a/js/utils/Serializer.js b/js/utils/Serializer.js new file mode 100644 index 00000000..30c6a3f3 --- /dev/null +++ b/js/utils/Serializer.js @@ -0,0 +1,64 @@ +(function () { + var ns = $.namespace('pskl.utils'); + ns.Serializer = { + serializePiskel : function (piskel) { + var serializedLayers = piskel.getLayers().map(function (l) { + return pskl.utils.Serializer.serializeLayer(l); + }); + return JSON.stringify({ + modelVersion : Constants.MODEL_VERSION, + piskel : { + height : piskel.getHeight(), + width : piskel.getWidth(), + fps : piskel.getFps(), + layers : serializedLayers + } + }); + }, + + serializeLayer : function (layer) { + var serializedFrames = layer.getFrames().map(function (f) { + return f.serialize(); + }); + return JSON.stringify({ + name : layer.getName(), + frames : serializedFrames + }); + }, + + deserializePiskel : function (json) { + var data = JSON.parse(json); + if (data.modelVersion == Constants.MODEL_VERSION) { + var pData = data.piskel; + var layers = pData.layers.map(function (serializedLayer) { + return pskl.utils.Serializer.deserializeLayer(serializedLayer); + }); + var piskel = new pskl.model.Piskel(pData.width, pData.height, pData.fps); + layers.forEach(function (layer) { + piskel.addLayer(layer); + }); + return piskel; + } else { + // pre-layer implementation adapter + } + }, + + deserializeLayer : function (json) { + var lData = JSON.parse(json); + var frames = lData.frames.map(function (serializedFrame) { + return pskl.utils.Serializer.deserializeFrame(serializedFrame); + }); + + var layer = new pskl.model.Layer(lData.name); + frames.forEach(function (frame) { + layer.addFrame(frame); + }); + return layer; + }, + + deserializeFrame : function (json) { + var framePixelGrid = JSON.parse(json); + return pskl.model.Frame.fromPixelGrid(framePixelGrid); + } + }; +})(); \ No newline at end of file diff --git a/piskel-script-list.js b/piskel-script-list.js index fb53bda2..35eba20a 100644 --- a/piskel-script-list.js +++ b/piskel-script-list.js @@ -13,9 +13,11 @@ exports.scripts = [ // Libraries "js/utils/core.js", - "js/utils/Template.js", - "js/utils/PixelUtils.js", "js/utils/CanvasUtils.js", + "js/utils/FrameUtils.js", + "js/utils/PixelUtils.js", + "js/utils/Serializer.js", + "js/utils/Template.js", "js/utils/UserSettings.js", "js/lib/jsColor_1_4_0/jscolor.js", @@ -24,7 +26,10 @@ exports.scripts = [ // Models "js/model/Frame.js", - "js/model/FrameSheet.js", + "js/model/Layer.js", + "js/model/Piskel.js", + + // Selection "js/selection/SelectionManager.js", "js/selection/BaseSelection.js", "js/selection/RectangularSelection.js", @@ -36,6 +41,7 @@ exports.scripts = [ "js/rendering/SpritesheetRenderer.js", // Controllers + "js/controller/PiskelController.js", "js/controller/DrawingController.js", "js/controller/PreviewFilmController.js", "js/controller/AnimatedPreviewController.js", @@ -68,5 +74,5 @@ exports.scripts = [ "js/drawingtools/ColorPicker.js", // Application controller and initialization - "js/piskel.js" + "js/app.js" ]; \ No newline at end of file diff --git a/templates/drawing-tools.html b/templates/drawing-tools.html index af80f65f..b1050445 100644 --- a/templates/drawing-tools.html +++ b/templates/drawing-tools.html @@ -8,8 +8,8 @@
-
- +
+