diff --git a/css/cheatsheet.css b/css/cheatsheet.css new file mode 100644 index 00000000..db130301 --- /dev/null +++ b/css/cheatsheet.css @@ -0,0 +1,77 @@ +#cheatsheet-wrapper { + position: absolute; + z-index: 10000; + + top: 0; + right: 0; + bottom: 0; + left: 0; + + padding: 50px; + box-sizing: border-box; + + color: white; +} + +.cheatsheet-container { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 20px 10%; + border-radius: 3px; + background: rgba(0,0,0,0.9); + overflow: auto; +} + +.cheatsheet-container h3 { + font-size:24px; + margin-top: 0; +} + +.cheatsheet-section { + float: left; + width : 50%; +} + +.cheatsheet-shortcut { + overflow: hidden; + margin: 10px 0; +} + +.cheatsheet-icon.tool-icon { + float: left; + display: inline-block; + + height: 30px; + width: 30px; + margin: 0 20px 0 0; + + background-size: 20px 20px; + background-position: 5px 5px; +} + +.cheatsheet-description { + font-family:Courier; + color: white; + font-size : 13px; + margin-left: 20px; + line-height : 30px; +} + +.cheatsheet-key { + display : inline-block; + height: 30px; + line-height: 30px; + padding: 0 10px; + + border : 1px solid gold; + border-radius: 2px; + + box-sizing: border-box; + + text-align: center; + font-family:Courier; + font-weight: bold; + font-size : 18px; + color: gold; +} \ No newline at end of file diff --git a/index.html b/index.html index 176f8927..5a240320 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ + @@ -55,9 +56,8 @@ - - + diff --git a/js/Constants.js b/js/Constants.js index 19ffe193..3ee2fc6b 100644 --- a/js/Constants.js +++ b/js/Constants.js @@ -6,7 +6,7 @@ var Constants = { FPS : 12 }, - MODEL_VERSION : 1, + MODEL_VERSION : 2, MAX_HEIGHT : 1024, MAX_WIDTH : 1024, diff --git a/js/Events.js b/js/Events.js index e7e5e7ea..28e1107f 100644 --- a/js/Events.js +++ b/js/Events.js @@ -3,10 +3,8 @@ var Events = { TOOL_SELECTED : "TOOL_SELECTED", TOOL_RELEASED : "TOOL_RELEASED", - PRIMARY_COLOR_SELECTED: "PRIMARY_COLOR_SELECTED", - PRIMARY_COLOR_UPDATED: "PRIMARY_COLOR_UPDATED", - SECONDARY_COLOR_SELECTED: "SECONDARY_COLOR_SELECTED", - SECONDARY_COLOR_UPDATED: "SECONDARY_COLOR_UPDATED", + SELECT_PRIMARY_COLOR: "SELECT_PRIMARY_COLOR", + SELECT_SECONDARY_COLOR: "SELECT_SECONDARY_COLOR", /** * When this event is emitted, a request is sent to the localstorage @@ -15,22 +13,6 @@ var Events = { */ LOCALSTORAGE_REQUEST: "LOCALSTORAGE_REQUEST", - CANVAS_RIGHT_CLICKED: "CANVAS_RIGHT_CLICKED", - - /** - * Event to request a refresh of the display. - * A bit overkill but, it's just workaround in our current drawing system. - * TODO: Remove or rework when redraw system is refactored. - */ - REFRESH: "REFRESH", - - /** - * Temporary event to bind the redraw of right preview film to the canvas. - * This redraw should be driven by model updates. - * TODO(vincz): Remove. - */ - REDRAW_PREVIEWFILM: "REDRAW_PREVIEWFILM", - /** * Fired each time a user setting change. * The payload will be: @@ -39,7 +21,6 @@ var Events = { */ USER_SETTINGS_CHANGED: "USER_SETTINGS_CHANGED", - /* Listened to by SettingsController */ CLOSE_SETTINGS_DRAWER : "CLOSE_SETTINGS_DRAWER", /** @@ -50,8 +31,6 @@ var Events = { FRAME_SIZE_CHANGED : "FRAME_SIZE_CHANGED", - CURRENT_FRAME_SET: "CURRENT_FRAME_SET", - SELECTION_CREATED: "SELECTION_CREATED", SELECTION_MOVE_REQUEST: "SELECTION_MOVE_REQUEST", SELECTION_DISMISSED: "SELECTION_DISMISSED", @@ -59,9 +38,6 @@ var Events = { SHOW_NOTIFICATION: "SHOW_NOTIFICATION", HIDE_NOTIFICATION: "HIDE_NOTIFICATION", - UNDO: "UNDO", - REDO: "REDO", - CUT: "CUT", - COPY: "COPY", - PASTE: "PASTE" + // Events triggered by keyboard + SELECT_TOOL : "SELECT_TOOL" }; \ No newline at end of file diff --git a/js/app.js b/js/app.js index 19cd93c8..ca11988c 100644 --- a/js/app.js +++ b/js/app.js @@ -10,6 +10,9 @@ ns.app = { init : function () { + this.shortcutService = new pskl.service.keyboard.ShortcutService(); + this.shortcutService.init(); + var size = this.readSizeFromURL_(); var piskel = new pskl.model.Piskel(size.width, size.height); @@ -20,8 +23,12 @@ piskel.addLayer(layer); this.piskelController = new pskl.controller.PiskelController(piskel); + this.piskelController.init(); - this.drawingController = new pskl.controller.DrawingController(this.piskelController, $('#drawing-canvas-container')); + this.paletteController = new pskl.controller.PaletteController(); + this.paletteController.init(); + + this.drawingController = new pskl.controller.DrawingController(this.piskelController, this.paletteController, $('#drawing-canvas-container')); this.drawingController.init(); this.animationController = new pskl.controller.AnimatedPreviewController(this.piskelController, $('#preview-canvas-container')); @@ -39,15 +46,15 @@ this.settingsController = new pskl.controller.settings.SettingsController(this.piskelController); this.settingsController.init(); + this.toolController = new pskl.controller.ToolController(); + this.toolController.init(); + this.selectionManager = new pskl.selection.SelectionManager(this.piskelController); this.selectionManager.init(); this.historyService = new pskl.service.HistoryService(this.piskelController); this.historyService.init(); - this.keyboardEventService = new pskl.service.KeyboardEventService(); - this.keyboardEventService.init(); - this.notificationController = new pskl.controller.NotificationController(); this.notificationController.init(); @@ -57,11 +64,10 @@ this.imageUploadService = new pskl.service.ImageUploadService(); this.imageUploadService.init(); - this.toolController = new pskl.controller.ToolController(); - this.toolController.init(); - this.paletteController = new pskl.controller.PaletteController(); - this.paletteController.init(); + this.cheatsheetService = new pskl.service.keyboard.CheatsheetService(); + this.cheatsheetService.init(); + var drawingLoop = new pskl.rendering.DrawingLoop(); drawingLoop.addCallback(this.render, this); @@ -96,9 +102,10 @@ finishInitAppEngine_ : function () { if (pskl.framesheetData_ && pskl.framesheetData_.content) { - var piskel = pskl.utils.Serializer.createPiskel(pskl.framesheetData_.content); - pskl.app.piskelController.setPiskel(piskel); - pskl.app.animationController.setFPS(pskl.framesheetData_.fps); + pskl.utils.serialization.Deserializer.deserialize(pskl.framesheetData_.content, function (piskel) { + pskl.app.piskelController.setPiskel(piskel); + pskl.app.animationController.setFPS(pskl.framesheetData_.fps); + }); } }, @@ -159,10 +166,11 @@ xhr.responseType = 'text'; xhr.onload = function (e) { var res = JSON.parse(this.responseText); - var piskel = pskl.utils.Serializer.createPiskel(res.framesheet); - pskl.app.piskelController.setPiskel(piskel); - pskl.app.animationController.setFPS(res.fps); - $.publish(Events.HIDE_NOTIFICATION); + pskl.utils.serialization.Deserializer.deserialize(res.framesheet, function (piskel) { + pskl.app.piskelController.setPiskel(piskel); + pskl.app.animationController.setFPS(res.fps); + $.publish(Events.HIDE_NOTIFICATION); + }); }; xhr.onerror = function () { @@ -242,8 +250,8 @@ }, getFramesheetAsPng : function () { - var renderer = new pskl.rendering.SpritesheetRenderer(this.piskelController); - var framesheetCanvas = renderer.render(); + var renderer = new pskl.rendering.PiskelRenderer(this.piskelController); + var framesheetCanvas = renderer.renderAsCanvas(); return framesheetCanvas.toDataURL("image/png"); }, diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index f4eb5ba4..53f7521f 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -1,11 +1,13 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.DrawingController = function (piskelController, container) { + ns.DrawingController = function (piskelController, paletteController,container) { /** * @public */ this.piskelController = piskelController; + this.paletteController = paletteController; + /** * @public */ @@ -41,34 +43,16 @@ this.isRightClicked = false; this.previousMousemoveTime = 0; this.currentToolBehavior = null; - this.primaryColor = Constants.DEFAULT_PEN_COLOR; - this.secondaryColor = Constants.TRANSPARENT_COLOR; }; ns.DrawingController.prototype.init = function () { this.initMouseBehavior(); $.subscribe(Events.TOOL_SELECTED, $.proxy(function(evt, toolBehavior) { - console.log("Tool selected: ", toolBehavior); this.currentToolBehavior = toolBehavior; this.overlayFrame.clear(); }, this)); - - /** - * TODO(grosbouddha): Primary/secondary color state are kept in this general controller. - * Find a better place to store that. Perhaps PaletteController? - */ - $.subscribe(Events.PRIMARY_COLOR_SELECTED, $.proxy(function(evt, color) { - console.log("Primary color selected: ", color); - this.primaryColor = color; - $.publish(Events.PRIMARY_COLOR_UPDATED, [color]); - }, this)); - $.subscribe(Events.SECONDARY_COLOR_SELECTED, $.proxy(function(evt, color) { - console.log("Secondary color selected: ", color); - this.secondaryColor = color; - $.publish(Events.SECONDARY_COLOR_UPDATED, [color]); - }, this)); - + $(window).resize($.proxy(this.startResizeTimer_, this)); $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); @@ -127,7 +111,6 @@ if(event.button == 2) { // right click this.isRightClicked = true; - $.publish(Events.CANVAS_RIGHT_CLICKED); } var coords = this.renderer.getCoordinates(event.clientX, event.clientY); @@ -249,9 +232,9 @@ */ ns.DrawingController.prototype.getCurrentColor_ = function () { if(this.isRightClicked) { - return this.secondaryColor; + return this.paletteController.getSecondaryColor(); } else { - return this.primaryColor; + return this.paletteController.getPrimaryColor(); } }; diff --git a/js/controller/PaletteController.js b/js/controller/PaletteController.js index f25707f9..82b9263d 100644 --- a/js/controller/PaletteController.js +++ b/js/controller/PaletteController.js @@ -1,7 +1,10 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.PaletteController = function () {}; + ns.PaletteController = function () { + this.primaryColor = Constants.DEFAULT_PEN_COLOR; + this.secondaryColor = Constants.TRANSPARENT_COLOR; + }; /** * @public @@ -10,13 +13,11 @@ 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)); + $.subscribe(Events.SELECT_PRIMARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:true})); + $.subscribe(Events.SELECT_SECONDARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:false})); - $.subscribe(Events.SECONDARY_COLOR_UPDATED, $.proxy(function(evt, color) { - this.updateColorPicker_(color, $('#secondary-color-picker')); - }, this)); + pskl.app.shortcutService.addShortcut('X', this.swapColors.bind(this)); + pskl.app.shortcutService.addShortcut('D', this.resetColors.bind(this)); var spectrumCfg = { showPalette: true, @@ -45,12 +46,53 @@ ns.PaletteController.prototype.onPickerChange_ = function(evt, isPrimary) { var inputPicker = $(evt.target); if(evt.data.isPrimary) { - $.publish(Events.PRIMARY_COLOR_SELECTED, [inputPicker.val()]); + this.setPrimaryColor(inputPicker.val()); } else { - $.publish(Events.SECONDARY_COLOR_SELECTED, [inputPicker.val()]); + this.setSecondaryColor(inputPicker.val()); } }; + /** + * @private + */ + ns.PaletteController.prototype.onColorSelected_ = function(args, evt, color) { + var inputPicker = $(evt.target); + if(args.isPrimary) { + this.setPrimaryColor(color); + } else { + this.setSecondaryColor(color); + } + }; + + ns.PaletteController.prototype.setPrimaryColor = function (color) { + this.primaryColor = color; + this.updateColorPicker_(color, $('#color-picker')); + }; + + ns.PaletteController.prototype.setSecondaryColor = function (color) { + this.secondaryColor = color; + this.updateColorPicker_(color, $('#secondary-color-picker')); + }; + + ns.PaletteController.prototype.getPrimaryColor = function () { + return this.primaryColor; + }; + + ns.PaletteController.prototype.getSecondaryColor = function () { + return this.secondaryColor; + }; + + ns.PaletteController.prototype.swapColors = function () { + var primaryColor = this.getPrimaryColor(); + this.setPrimaryColor(this.getSecondaryColor()); + this.setSecondaryColor(primaryColor); + }; + + ns.PaletteController.prototype.resetColors = function () { + this.setPrimaryColor(Constants.DEFAULT_PEN_COLOR); + this.setSecondaryColor(Constants.TRANSPARENT_COLOR); + }; + /** * @private */ diff --git a/js/controller/PiskelController.js b/js/controller/PiskelController.js index 8d102322..12c570ac 100644 --- a/js/controller/PiskelController.js +++ b/js/controller/PiskelController.js @@ -20,6 +20,13 @@ $.publish(Events.FRAME_SIZE_CHANGED); }; + ns.PiskelController.prototype.init = function () { + pskl.app.shortcutService.addShortcut('up', this.selectPreviousFrame.bind(this)); + pskl.app.shortcutService.addShortcut('down', this.selectNextFrame.bind(this)); + pskl.app.shortcutService.addShortcut('n', this.addFrameAtCurrentIndex.bind(this)); + pskl.app.shortcutService.addShortcut('shift+n', this.duplicateCurrentFrame.bind(this)); + }; + ns.PiskelController.prototype.getHeight = function () { return this.piskel.getHeight(); }; @@ -63,15 +70,21 @@ return !!this.getCurrentLayer().getFrameAt(index); }; - // backward from framesheet - ns.PiskelController.prototype.getFrameByIndex = - ns.PiskelController.prototype.getMergedFrameAt; + ns.PiskelController.prototype.addFrame = function () { + this.addFrameAt(this.getFrameCount()); + }; - ns.PiskelController.prototype.addEmptyFrame = function () { + ns.PiskelController.prototype.addFrameAtCurrentIndex = function () { + this.addFrameAt(this.currentFrameIndex + 1); + }; + + ns.PiskelController.prototype.addFrameAt = function (index) { var layers = this.getLayers(); layers.forEach(function (l) { - l.addFrame(this.createEmptyFrame_()); + l.addFrameAt(this.createEmptyFrame_(), index); }.bind(this)); + + $.publish(Events.PISKEL_RESET); }; ns.PiskelController.prototype.createEmptyFrame_ = function () { @@ -92,11 +105,17 @@ $.publish(Events.PISKEL_RESET); }; + ns.PiskelController.prototype.duplicateCurrentFrame = function () { + this.duplicateFrameAt(this.currentFrameIndex); + }; + ns.PiskelController.prototype.duplicateFrameAt = function (index) { var layers = this.getLayers(); layers.forEach(function (l) { l.duplicateFrameAt(index); }); + + $.publish(Events.PISKEL_RESET); }; ns.PiskelController.prototype.moveFrame = function (fromIndex, toIndex) { @@ -116,6 +135,20 @@ $.publish(Events.PISKEL_RESET); }; + ns.PiskelController.prototype.selectNextFrame = function () { + var nextIndex = this.currentFrameIndex + 1; + if (nextIndex < this.getFrameCount()) { + this.setCurrentFrameIndex(nextIndex); + } + }; + + ns.PiskelController.prototype.selectPreviousFrame = function () { + var nextIndex = this.currentFrameIndex - 1; + if (nextIndex >= 0) { + this.setCurrentFrameIndex(nextIndex); + } + }; + ns.PiskelController.prototype.setCurrentLayerIndex = function (index) { this.currentLayerIndex = index; $.publish(Events.PISKEL_RESET); diff --git a/js/controller/PreviewFilmController.js b/js/controller/PreviewFilmController.js index a7bd781c..67f0f03f 100644 --- a/js/controller/PreviewFilmController.js +++ b/js/controller/PreviewFilmController.js @@ -19,7 +19,7 @@ }; ns.PreviewFilmController.prototype.addFrame = function () { - this.piskelController.addEmptyFrame(); + this.piskelController.addFrame(); this.piskelController.setCurrentFrameIndex(this.piskelController.getFrameCount() - 1); this.updateScrollerOverflows(); }; diff --git a/js/controller/ToolController.js b/js/controller/ToolController.js index 4b38b2f9..0e2d42dc 100644 --- a/js/controller/ToolController.js +++ b/js/controller/ToolController.js @@ -1,36 +1,40 @@ (function () { var ns = $.namespace("pskl.controller"); - + ns.ToolController = function () { - - this.toolInstances = { - "simplePen" : new pskl.drawingtools.SimplePen(), - "verticalMirrorPen" : new pskl.drawingtools.VerticalMirrorPen(), - "eraser" : new pskl.drawingtools.Eraser(), - "paintBucket" : new pskl.drawingtools.PaintBucket(), - "stroke" : new pskl.drawingtools.Stroke(), - "rectangle" : new pskl.drawingtools.Rectangle(), - "circle" : new pskl.drawingtools.Circle(), - "move" : new pskl.drawingtools.Move(), - "rectangleSelect" : new pskl.drawingtools.RectangleSelect(), - "shapeSelect" : new pskl.drawingtools.ShapeSelect(), - "colorPicker" : new pskl.drawingtools.ColorPicker() + var toDescriptor = function (id, shortcut, instance) { + return {id:id, shortcut:shortcut, instance:instance}; }; - this.currentSelectedTool = this.toolInstances.simplePen; - this.previousSelectedTool = this.toolInstances.simplePen; + this.tools = [ + toDescriptor('simplePen', 'P', new pskl.drawingtools.SimplePen()), + toDescriptor('verticalMirrorPen', 'V', new pskl.drawingtools.VerticalMirrorPen()), + toDescriptor('eraser', 'E', new pskl.drawingtools.Eraser()), + toDescriptor('paintBucket', 'B', new pskl.drawingtools.PaintBucket()), + toDescriptor('stroke', 'L', new pskl.drawingtools.Stroke()), + toDescriptor('rectangle', 'R', new pskl.drawingtools.Rectangle()), + toDescriptor('circle', 'C', new pskl.drawingtools.Circle()), + toDescriptor('move', 'M', new pskl.drawingtools.Move()), + toDescriptor('rectangleSelect', 'S', new pskl.drawingtools.RectangleSelect()), + toDescriptor('shapeSelect', 'Z', new pskl.drawingtools.ShapeSelect()), + toDescriptor('colorPicker', 'O', new pskl.drawingtools.ColorPicker()) + ]; + + this.currentSelectedTool = this.tools[0]; + this.previousSelectedTool = this.tools[0]; }; /** * @public */ ns.ToolController.prototype.init = function() { - this.createToolMarkup_(); + this.createToolsDom_(); + this.addKeyboardShortcuts_(); // Initialize tool: // Set SimplePen as default selected tool: - this.selectTool_(this.toolInstances.simplePen); + this.selectTool_(this.tools[0]); // Activate listener on tool panel: $("#tool-section").click($.proxy(this.onToolIconClicked_, this)); }; @@ -44,18 +48,24 @@ if(previousSelectedToolClass) { stage.removeClass(previousSelectedToolClass); } - stage.addClass(tool.toolId); - stage.data("selected-tool-class", tool.toolId); + stage.addClass(tool.instance.toolId); + stage.data("selected-tool-class", tool.instance.toolId); }; /** * @private */ ns.ToolController.prototype.selectTool_ = function(tool) { - console.log("Selecting Tool:" , this.currentSelectedTool); this.currentSelectedTool = tool; this.activateToolOnStage_(this.currentSelectedTool); - $.publish(Events.TOOL_SELECTED, [tool]); + + var selectedToolElement = $('#tool-section .tool-icon.selected'); + var toolElement = $('[data-tool-id=' + tool.instance.toolId + ']'); + + selectedToolElement.removeClass('selected'); + toolElement.addClass('selected'); + + $.publish(Events.TOOL_SELECTED, [tool.instance]); }; /** @@ -70,18 +80,24 @@ var tool = this.getToolById_(toolId); if (tool) { this.selectTool_(tool); + } + } + }; - // Show tool as selected: - $('#tool-section .tool-icon.selected').removeClass('selected'); - clickedTool.addClass('selected'); + ns.ToolController.prototype.onKeyboardShortcut_ = function(charkey) { + for (var i = 0 ; i < this.tools.length ; i++) { + var tool = this.tools[i]; + if (tool.shortcut.toLowerCase() === charkey.toLowerCase()) { + this.selectTool_(tool); } } }; ns.ToolController.prototype.getToolById_ = function (toolId) { - for(var key in this.toolInstances) { - if (this.toolInstances[key].toolId == toolId) { - return this.toolInstances[key]; + for(var i = 0 ; i < this.tools.length ; i++) { + var tool = this.tools[i]; + if (tool.instance.toolId == toolId) { + return tool; } } return null; @@ -90,18 +106,32 @@ /** * @private */ - ns.ToolController.prototype.createToolMarkup_ = function() { - var currentTool, toolMarkup = '', extraClass; - // TODO(vincz): Tools rendering order is not enforced by the data stucture (this.toolInstances), fix that. - for (var toolKey in this.toolInstances) { - currentTool = this.toolInstances[toolKey]; - extraClass = currentTool.toolId; - if (this.currentSelectedTool == currentTool) { - extraClass = extraClass + " selected"; - } - toolMarkup += '
  • '; + ns.ToolController.prototype.createToolsDom_ = function() { + var toolMarkup = ''; + for(var i = 0 ; i < this.tools.length ; i++) { + toolMarkup += this.getToolMarkup_(this.tools[i]); } $('#tools-container').html(toolMarkup); }; + + /** + * @private + */ + ns.ToolController.prototype.getToolMarkup_ = function(tool) { + var instance = tool.instance; + + var classList = ['tool-icon', instance.toolId]; + if (this.currentSelectedTool == tool) { + classList.push('selected'); + } + + return '
  • '; + }; + + ns.ToolController.prototype.addKeyboardShortcuts_ = function () { + for(var i = 0 ; i < this.tools.length ; i++) { + pskl.app.shortcutService.addShortcut(this.tools[i].shortcut, this.onKeyboardShortcut_.bind(this)); + } + }; })(); \ No newline at end of file diff --git a/js/controller/settings/GifExportController.js b/js/controller/settings/GifExportController.js index 636272f4..ac7602e6 100644 --- a/js/controller/settings/GifExportController.js +++ b/js/controller/settings/GifExportController.js @@ -114,8 +114,9 @@ for (var i = 0; i < this.piskelController.getFrameCount(); i++) { var frame = this.piskelController.getFrameAt(i); - var renderer = new pskl.rendering.CanvasRenderer(frame, zoom); - gif.addFrame(renderer.render(), { + var canvasRenderer = new pskl.rendering.CanvasRenderer(frame, zoom); + var canvas = canvasRenderer.render(); + gif.addFrame(canvas.getContext('2d'), { delay: 1000 / fps }); } diff --git a/js/controller/settings/ImportController.js b/js/controller/settings/ImportController.js index 4a7e73cd..3c94eaa5 100644 --- a/js/controller/settings/ImportController.js +++ b/js/controller/settings/ImportController.js @@ -153,10 +153,11 @@ var image = pskl.utils.ImageResizer.resize(this.importedImage_, w, h, smoothing); var frame = pskl.utils.FrameUtils.createFromImage(image); - var piskel = pskl.utils.Serializer.createPiskel([frame]); + var layer = pskl.model.Layer.fromFrames('Layer 1', [frame]); + var piskel = pskl.model.Piskel.fromLayers([layer]); + pskl.app.piskelController.setPiskel(piskel); pskl.app.animationController.setFPS(Constants.DEFAULT.FPS); - this.reset_(); } } diff --git a/js/drawingtools/ColorPicker.js b/js/drawingtools/ColorPicker.js index 8bbedf0d..fba7d5a9 100644 --- a/js/drawingtools/ColorPicker.js +++ b/js/drawingtools/ColorPicker.js @@ -12,7 +12,7 @@ }; pskl.utils.inherit(ns.ColorPicker, ns.BaseTool); - + /** * @override */ @@ -20,9 +20,9 @@ if (frame.containsPixel(col, row)) { var sampledColor = frame.getPixel(col, row); if (context.button == Constants.LEFT_BUTTON) { - $.publish(Events.PRIMARY_COLOR_SELECTED, [sampledColor]); + $.publish(Events.SELECT_PRIMARY_COLOR, [sampledColor]); } else if (context.button == Constants.RIGHT_BUTTON) { - $.publish(Events.SECONDARY_COLOR_SELECTED, [sampledColor]); + $.publish(Events.SELECT_SECONDARY_COLOR, [sampledColor]); } } }; diff --git a/js/model/Layer.js b/js/model/Layer.js index 0132e6a6..3725622b 100644 --- a/js/model/Layer.js +++ b/js/model/Layer.js @@ -10,6 +10,19 @@ } }; + /** + * Create a Layer instance from an already existing set a Frames + * @static + * @param {String} name layer's name + * @param {Array} frames should all have the same dimensions + * @return {pskl.model.Layer} + */ + ns.Layer.fromFrames = function (name, frames) { + var layer = new ns.Layer(name); + frames.forEach(layer.addFrame.bind(layer)); + return layer; + }; + ns.Layer.prototype.getName = function () { return this.name; }; diff --git a/js/model/Piskel.js b/js/model/Piskel.js index 7748408f..77e7f261 100644 --- a/js/model/Piskel.js +++ b/js/model/Piskel.js @@ -21,6 +21,24 @@ } }; + /** + * Create a piskel instance from an existing set of (non empty) layers + * Layers should all be synchronized : same number of frames, same dimensions + * @param {Array} layers + * @return {pskl.model.Piskel} + */ + ns.Piskel.fromLayers = function (layers) { + var piskel = null; + if (layers.length > 0 && layers[0].length() > 0) { + var sampleFrame = layers[0].getFrameAt(0); + piskel = new pskl.model.Piskel(sampleFrame.getWidth(), sampleFrame.getHeight()); + layers.forEach(piskel.addLayer.bind(piskel)); + } else { + throw 'Piskel.fromLayers expects array of non empty pskl.model.Layer as first argument'; + } + return piskel; + }; + ns.Piskel.prototype.getLayers = function () { return this.layers; }; diff --git a/js/rendering/CanvasRenderer.js b/js/rendering/CanvasRenderer.js index 76414535..c57e372d 100644 --- a/js/rendering/CanvasRenderer.js +++ b/js/rendering/CanvasRenderer.js @@ -19,24 +19,20 @@ ns.CanvasRenderer.prototype.render = function () { var canvas = this.createCanvas_(); var context = canvas.getContext('2d'); - for(var col = 0, width = this.frame.getWidth(); col < width; col++) { - for(var row = 0, height = this.frame.getHeight(); row < height; row++) { - var color = this.frame.getPixel(col, row); - this.renderPixel_(color, col, row, context); - } - } - return context; + this.frame.forEachPixel(function (color, x, y) { + this.renderPixel_(color, x, y, context); + }.bind(this)); + + return canvas; }; - ns.CanvasRenderer.prototype.renderPixel_ = function (color, col, row, context) { - + ns.CanvasRenderer.prototype.renderPixel_ = function (color, x, y, context) { if(color == Constants.TRANSPARENT_COLOR) { color = this.transparentColor_; } context.fillStyle = color; - - context.fillRect(col * this.zoom, row * this.zoom, this.zoom, this.zoom); + context.fillRect(x * this.zoom, y * this.zoom, this.zoom, this.zoom); }; ns.CanvasRenderer.prototype.createCanvas_ = function () { diff --git a/js/rendering/FramesheetRenderer.js b/js/rendering/FramesheetRenderer.js new file mode 100644 index 00000000..06eb4d99 --- /dev/null +++ b/js/rendering/FramesheetRenderer.js @@ -0,0 +1,43 @@ +(function () { + var ns = $.namespace('pskl.rendering'); + + /** + * Render an array of frames + * @param {Array.} frames + */ + ns.FramesheetRenderer = function (frames) { + if (frames.length > 0) { + this.frames = frames; + } else { + throw 'FramesheetRenderer : Invalid argument : frames is empty'; + } + }; + + ns.FramesheetRenderer.prototype.renderAsCanvas = function () { + var canvas = this.createCanvas_(); + for (var i = 0 ; i < this.frames.length ; i++) { + var frame = this.frames[i]; + this.drawFrameInCanvas_(frame, canvas, i * frame.getWidth(), 0); + } + return canvas; + }; + + ns.FramesheetRenderer.prototype.drawFrameInCanvas_ = function (frame, canvas, offsetWidth, offsetHeight) { + var context = canvas.getContext('2d'); + frame.forEachPixel(function (color, x, y) { + if(color != Constants.TRANSPARENT_COLOR) { + context.fillStyle = color; + context.fillRect(x + offsetWidth, y + offsetHeight, 1, 1); + } + }); + }; + + ns.FramesheetRenderer.prototype.createCanvas_ = function () { + var sampleFrame = this.frames[0]; + var count = this.frames.length; + var width = count * sampleFrame.getWidth(); + var height = sampleFrame.getHeight(); + return pskl.CanvasUtils.createCanvas(width, height); + }; + +})(); \ No newline at end of file diff --git a/js/rendering/PiskelRenderer.js b/js/rendering/PiskelRenderer.js new file mode 100644 index 00000000..33a0f2f4 --- /dev/null +++ b/js/rendering/PiskelRenderer.js @@ -0,0 +1,14 @@ +(function () { + + var ns = $.namespace("pskl.rendering"); + + ns.PiskelRenderer = function (piskelController) { + var frames = []; + for (var i = 0 ; i < piskelController.getFrameCount() ; i++) { + frames.push(this.piskelController.getFrameAt(i)); + } + ns.FramesheetRenderer.call(this, frames); + }; + + pskl.utils.inherit(ns.PiskelRenderer, ns.FramesheetRenderer); +})(); \ No newline at end of file diff --git a/js/rendering/SpritesheetRenderer.js b/js/rendering/SpritesheetRenderer.js deleted file mode 100644 index d634da0f..00000000 --- a/js/rendering/SpritesheetRenderer.js +++ /dev/null @@ -1,44 +0,0 @@ -(function () { - - var ns = $.namespace("pskl.rendering"); - - ns.SpritesheetRenderer = function (piskelController) { - this.piskelController = piskelController; - }; - - ns.SpritesheetRenderer.prototype.render = function () { - var canvas = this.createCanvas_(); - 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; - }; - - /** - * TODO(juliandescottes): Mutualize with code already present in FrameRenderer - */ - ns.SpritesheetRenderer.prototype.drawFrameInCanvas_ = function (frame, canvas, offsetWidth, offsetHeight) { - var context = canvas.getContext('2d'); - for(var col = 0, width = frame.getWidth(); col < width; col++) { - for(var row = 0, height = frame.getHeight(); row < height; row++) { - var color = frame.getPixel(col, row); - if(color != Constants.TRANSPARENT_COLOR) { - context.fillStyle = color; - context.fillRect(col + offsetWidth, row + offsetHeight, 1, 1); - } - } - } - }; - - ns.SpritesheetRenderer.prototype.createCanvas_ = function () { - var frameCount = this.piskelController.getFrameCount(); - if (frameCount > 0){ - var width = frameCount * this.piskelController.getWidth(); - var height = this.piskelController.getHeight(); - return pskl.CanvasUtils.createCanvas(width, height); - } else { - throw "Cannot render empty Spritesheet"; - } - }; -})(); \ No newline at end of file diff --git a/js/selection/SelectionManager.js b/js/selection/SelectionManager.js index 5f6f3790..578e64b3 100644 --- a/js/selection/SelectionManager.js +++ b/js/selection/SelectionManager.js @@ -13,9 +13,9 @@ $.subscribe(Events.SELECTION_DISMISSED, $.proxy(this.onSelectionDismissed_, this)); $.subscribe(Events.SELECTION_MOVE_REQUEST, $.proxy(this.onSelectionMoved_, this)); - $.subscribe(Events.PASTE, $.proxy(this.onPaste_, this)); - $.subscribe(Events.COPY, $.proxy(this.onCopy_, this)); - $.subscribe(Events.CUT, $.proxy(this.onCut_, this)); + pskl.app.shortcutService.addShortcut('ctrl+V', this.paste.bind(this)); + pskl.app.shortcutService.addShortcut('ctrl+X', this.cut.bind(this)); + pskl.app.shortcutService.addShortcut('ctrl+C', this.copy.bind(this)); $.subscribe(Events.TOOL_SELECTED, $.proxy(this.onToolSelected_, this)); }; @@ -46,10 +46,7 @@ this.cleanSelection_(); }; - /** - * @private - */ - ns.SelectionManager.prototype.onCut_ = function(evt) { + ns.SelectionManager.prototype.cut = function() { if(this.currentSelection) { // Put cut target into the selection: this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame()); @@ -59,9 +56,8 @@ for(var i=0, l=pixels.length; i= 48 && keycode <= 57) { + // key is 0-9 + return (keycode - 48) + ""; + } else if (keycode >= 65 && keycode <= 90) { + // key is a-z, use base 36 to get the string representation + return (keycode - 65 + 10).toString(36); + } else { + return specialKeys[keycode]; + } + } + }; +})(); \ No newline at end of file diff --git a/js/service/keyboard/ShortcutService.js b/js/service/keyboard/ShortcutService.js new file mode 100644 index 00000000..07419af7 --- /dev/null +++ b/js/service/keyboard/ShortcutService.js @@ -0,0 +1,90 @@ +(function () { + var ns = $.namespace('pskl.service.keyboard'); + + ns.ShortcutService = function () { + this.shortcuts_ = {}; + }; + + /** + * @public + */ + ns.ShortcutService.prototype.init = function() { + $(document.body).keydown($.proxy(this.onKeyUp_, this)); + }; + + ns.ShortcutService.prototype.addShortcut = function (rawKey, callback) { + var parsedKey = this.parseKey_(rawKey.toLowerCase()); + + var key = parsedKey.key, + meta = parsedKey.meta; + + this.shortcuts_[key] = this.shortcuts_[key] || {}; + + if (this.shortcuts_[key][meta]) { + throw 'Shortcut ' + meta + ' + ' + key + ' already registered'; + } else { + this.shortcuts_[key][meta] = callback; + } + }; + + ns.ShortcutService.prototype.removeShortcut = function (rawKey) { + var parsedKey = this.parseKey_(rawKey.toLowerCase()); + + var key = parsedKey.key, + meta = parsedKey.meta; + + this.shortcuts_[key] = this.shortcuts_[key] || {}; + + this.shortcuts_[key][meta] = null; + }; + + ns.ShortcutService.prototype.parseKey_ = function (key) { + var meta = 'normal'; + if (key.indexOf('ctrl+') === 0) { + meta = 'ctrl'; + key = key.replace('ctrl+', ''); + } else if (key.indexOf('shift+') === 0) { + meta = 'shift'; + key = key.replace('shift+', ''); + } + return {meta : meta, key : key}; + }; + + /** + * @private + */ + ns.ShortcutService.prototype.onKeyUp_ = function(evt) { + // jquery names FTW ... + var keycode = evt.which; + var charkey = pskl.service.keyboard.KeycodeTranslator.toChar(keycode); + + var keyShortcuts = this.shortcuts_[charkey]; + if(keyShortcuts) { + var cb; + if (this.isCtrlKeyPressed_(evt)) { + cb = keyShortcuts.ctrl; + } else if (this.isShiftKeyPressed_(evt)) { + cb = keyShortcuts.shift; + } else { + cb = keyShortcuts.normal; + } + + if(cb) { + cb(charkey); + evt.preventDefault(); + } + } + }; + + ns.ShortcutService.prototype.isCtrlKeyPressed_ = function (evt) { + return this.isMac_() ? evt.metaKey : evt.ctrlKey; + }; + + ns.ShortcutService.prototype.isShiftKeyPressed_ = function (evt) { + return evt.shiftKey; + }; + + ns.ShortcutService.prototype.isMac_ = function () { + return navigator.appVersion.indexOf("Mac") != -1; + }; +})(); \ No newline at end of file diff --git a/js/utils/FrameUtils.js b/js/utils/FrameUtils.js index 270c6b16..353b6fe5 100644 --- a/js/utils/FrameUtils.js +++ b/js/utils/FrameUtils.js @@ -107,25 +107,29 @@ context.drawImage(image, 0,0,w,h,0,0,w,h); var imgData = context.getImageData(0,0,w,h).data; + return pskl.utils.FrameUtils.createFromImageData(imgData, w, h); + }, + + createFromImageData : function (imageData, width, height) { // Draw the zoomed-up pixels to a different canvas context - var frame = []; - for (var x=0;x "js/rendering/DrawingLoop.js", @@ -46,7 +50,8 @@ exports.scripts = [ "js/rendering/frame/FrameRenderer.js", "js/rendering/frame/CachedFrameRenderer.js", "js/rendering/CanvasRenderer.js", - "js/rendering/SpritesheetRenderer.js", + "js/rendering/FramesheetRenderer.js", + "js/rendering/PiskelRenderer.js", // Controllers "js/controller/PiskelController.js", @@ -69,7 +74,9 @@ exports.scripts = [ // Services "js/service/LocalStorageService.js", "js/service/HistoryService.js", - "js/service/KeyboardEventService.js", + "js/service/keyboard/ShortcutService.js", + "js/service/keyboard/KeycodeTranslator.js", + "js/service/keyboard/CheatsheetService.js", "js/service/ImageUploadService.js", // Tools diff --git a/templates/cheatsheet.html b/templates/cheatsheet.html new file mode 100644 index 00000000..cfa7dffa --- /dev/null +++ b/templates/cheatsheet.html @@ -0,0 +1,19 @@ + + \ No newline at end of file