diff --git a/.gitignore b/.gitignore index 5509140f..69858933 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ +# mac artefacts *.DS_Store + +# sublime text stuff (the -project should actually be shared, but then we'd have to share the same disk location) +*.sublime-project +*.sublime-workspace + +# git stackdumps +*.stackdump \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..7f2ef5de --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 0.6 \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..fd40ada0 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +test: + jshint js/*.js \ No newline at end of file diff --git a/css/tools.css b/css/tools.css index 7aeb3f49..6b1f308b 100644 --- a/css/tools.css +++ b/css/tools.css @@ -77,6 +77,13 @@ cursor: url(../img/tools/cursors/hand.png) 14 12, pointer; } +.tool-grid, +.tool-grid label, +.tool-grid input { + line-height: 2.5; + cursor: pointer; +} + .tool-icon.selected { cursor: auto; background-color: #eee; diff --git a/index.html b/index.html index 91efbe58..d4cc28f7 100644 --- a/index.html +++ b/index.html @@ -52,6 +52,13 @@ 12 fps + + @@ -89,6 +96,7 @@ + diff --git a/js/Constants.js b/js/Constants.js index ffa5d03f..1f676f0e 100644 --- a/js/Constants.js +++ b/js/Constants.js @@ -1,16 +1,19 @@ var Constants = { - DEFAULT_PEN_COLOR : '#000000', - TRANSPARENT_COLOR : 'TRANSPARENT', - - /* - * Fake semi-transparent color used to highlight transparent - * strokes and rectangles: - */ - SELECTION_TRANSPARENT_COLOR: 'rgba(255, 255, 255, 0.6)', - - /* - * Default entry point for piskel web service: - */ - PISKEL_SERVICE_URL: 'http://2.piskel-app.appspot.com' + DEFAULT_PEN_COLOR : '#000000', + TRANSPARENT_COLOR : 'TRANSPARENT', + + /* + * Fake semi-transparent color used to highlight transparent + * strokes and rectangles: + */ + SELECTION_TRANSPARENT_COLOR: 'rgba(255, 255, 255, 0.6)', + + /* + * Default entry point for piskel web service: + */ + PISKEL_SERVICE_URL: 'http://2.piskel-app.appspot.com', + + GRID_STROKE_WIDTH: 1, + GRID_STROKE_COLOR: "lightgray" }; \ No newline at end of file diff --git a/js/Events.js b/js/Events.js index 2a1545b8..bb8ea4e5 100644 --- a/js/Events.js +++ b/js/Events.js @@ -1,33 +1,40 @@ Events = { - - TOOL_SELECTED : "TOOL_SELECTED", - TOOL_RELEASED : "TOOL_RELEASED", - COLOR_SELECTED: "COLOR_SELECTED", - COLOR_USED: "COLOR_USED", + + TOOL_SELECTED : "TOOL_SELECTED", + TOOL_RELEASED : "TOOL_RELEASED", + COLOR_SELECTED: "COLOR_SELECTED", - /** - * When this event is emitted, a request is sent to the localstorage - * Service to save the current framesheet. The storage service - * may not immediately store data (internal throttling of requests). - */ - LOCALSTORAGE_REQUEST: "LOCALSTORAGE_REQUEST", + /** + * When this event is emitted, a request is sent to the localstorage + * Service to save the current framesheet. The storage service + * may not immediately store data (internal throttling of requests). + */ + LOCALSTORAGE_REQUEST: "LOCALSTORAGE_REQUEST", - CANVAS_RIGHT_CLICKED: "CANVAS_RIGHT_CLICKED", - CANVAS_RIGHT_CLICK_RELEASED: "CANVAS_RIGHT_CLICK_RELEASED", + 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", + /** + * 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", - /** - * The framesheet was reseted and is now probably drastically different. - * Number of frames, content of frames, color used for the palette may have changed. - */ - FRAMESHEET_RESET: "FRAMESHEET_RESET", - - SHOW_NOTIFICATION: "SHOW_NOTIFICATION", - HIDE_NOTIFICATION: "HIDE_NOTIFICATION" + /** + * 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", + + GRID_DISPLAY_STATE_CHANGED: "GRID_DISPLAY_STATE_CHANGED", + + /** + * The framesheet was reseted and is now probably drastically different. + * Number of frames, content of frames, color used for the palette may have changed. + */ + FRAMESHEET_RESET: "FRAMESHEET_RESET", + + SHOW_NOTIFICATION: "SHOW_NOTIFICATION", + HIDE_NOTIFICATION: "HIDE_NOTIFICATION" }; \ No newline at end of file diff --git a/js/HistoryManager.js b/js/HistoryManager.js index 4a3c5c4b..d6a92208 100644 --- a/js/HistoryManager.js +++ b/js/HistoryManager.js @@ -1,40 +1,40 @@ (function () { var ns = $.namespace("pskl"); - ns.HistoryManager = function () {}; + ns.HistoryManager = function (framesheet) { + this.framesheet = framesheet; + }; ns.HistoryManager.prototype.init = function () { - document.body.addEventListener('keyup', this.onKeyup.bind(this)); - $.subscribe(Events.TOOL_RELEASED, this.saveState.bind(this)); + document.body.addEventListener('keyup', this.onKeyup.bind(this)); + $.subscribe(Events.TOOL_RELEASED, this.saveState.bind(this)); }; - ns.HistoryManager.prototype.saveState = function () { - piskel.getCurrentFrame().saveState(); - }; + ns.HistoryManager.prototype.saveState = function () { + this.framesheet.getCurrentFrame().saveState(); + }; ns.HistoryManager.prototype.onKeyup = function (evt) { - if (evt.ctrlKey && evt.keyCode == 90) { // CTRL + Z - this.undo(); - } + if (evt.ctrlKey && evt.keyCode == 90) { // CTRL + Z + this.undo(); + } - if (evt.ctrlKey && evt.keyCode == 89) { // CTRL+ Y - this.redo(); - } - }; + if (evt.ctrlKey && evt.keyCode == 89) { // CTRL+ Y + this.redo(); + } + }; - ns.HistoryManager.prototype.undo = function () { - piskel.getCurrentFrame().loadPreviousState(); - this.redraw(); - }; + ns.HistoryManager.prototype.undo = function () { + this.framesheet.getCurrentFrame().loadPreviousState(); + this.redraw(); + }; - ns.HistoryManager.prototype.redo = function () { - piskel.getCurrentFrame().loadNextState(); - this.redraw(); - }; + ns.HistoryManager.prototype.redo = function () { + this.framesheet.getCurrentFrame().loadNextState(); + this.redraw(); + }; - ns.HistoryManager.prototype.redraw = function () { - piskel.drawingController.renderFrame(); - piskel.previewsController.createPreviews(); - }; - - ns.HistoryManager = new ns.HistoryManager(); + ns.HistoryManager.prototype.redraw = function () { + this.framesheet.drawingController.renderFrame(); + this.framesheet.previewsController.createPreviews(); + }; })(); \ No newline at end of file diff --git a/js/LocalStorageService.js b/js/LocalStorageService.js index d697adf7..63cc3b59 100644 --- a/js/LocalStorageService.js +++ b/js/LocalStorageService.js @@ -8,20 +8,20 @@ $.namespace("pskl"); pskl.LocalStorageService = (function() { - var frameSheet_; + var frameSheet_; - /** - * @private - */ - var localStorageThrottler_ = null; - - /** - * @private - */ - var persistToLocalStorageRequest_ = function() { + /** + * @private + */ + var localStorageThrottler_ = null; + + /** + * @private + */ + var persistToLocalStorageRequest_ = function() { // Persist to localStorage when drawing. We throttle localStorage accesses // for high frequency drawing (eg mousemove). - if(localStorageThrottler_ != null) { + if(localStorageThrottler_ !== null) { window.clearTimeout(localStorageThrottler_); } localStorageThrottler_ = window.setTimeout(function() { @@ -31,65 +31,63 @@ pskl.LocalStorageService = (function() { }; /** - * @private - */ - var persistToLocalStorage_ = function() { - console.log('[LocalStorage service]: Snapshot stored') - window.localStorage['snapShot'] = frameSheet_.serialize(); + * @private + */ + var persistToLocalStorage_ = function() { + console.log('[LocalStorage service]: Snapshot stored'); + window.localStorage.snapShot = frameSheet_.serialize(); }; /** - * @private - */ - var restoreFromLocalStorage_ = function() { - frameSheet_.deserialize(window.localStorage['snapShot']); - // Model updated, redraw everything: - $.publish(Events.REFRESH); + * @private + */ + var restoreFromLocalStorage_ = function() { + frameSheet_.deserialize(window.localStorage.snapShot); }; /** - * @private - */ - var cleanLocalStorage_ = function() { - console.log('[LocalStorage service]: Snapshot removed') - delete window.localStorage['snapShot']; + * @private + */ + var cleanLocalStorage_ = function() { + console.log('[LocalStorage service]: Snapshot removed'); + delete window.localStorage.snapShot; }; - return { - init: function(frameSheet) { + return { + init: function(frameSheet) { - if(frameSheet == undefined) { - throw "Bad LocalStorageService initialization: " - } - frameSheet_ = frameSheet; + if(frameSheet === undefined) { + throw "Bad LocalStorageService initialization: "; + } + frameSheet_ = frameSheet; - $.subscribe(Events.LOCALSTORAGE_REQUEST, persistToLocalStorageRequest_); - }, + $.subscribe(Events.LOCALSTORAGE_REQUEST, persistToLocalStorageRequest_); + }, - // TODO(vincz): Find a good place to put this UI rendering, a service should not render UI. - displayRestoreNotification: function() { - if(window.localStorage && window.localStorage['snapShot']) { - var reloadLink = "reload"; - var discardLink = "discard"; - var content = "Non saved version found. " + reloadLink + " or " + discardLink; + // TODO(vincz): Find a good place to put this UI rendering, a service should not render UI. + displayRestoreNotification: function() { + if(window.localStorage && window.localStorage.snapShot) { + var reloadLink = "reload"; + var discardLink = "discard"; + var content = "Non saved version found. " + reloadLink + " or " + discardLink; - $.publish(Events.SHOW_NOTIFICATION, [{ - "content": content, - "behavior": function(rootNode) { - rootNode = $(rootNode); - rootNode.click(function(evt) { - var target = $(evt.target); - if(target.hasClass("localstorage-restore")) { - restoreFromLocalStorage_(); - } - else if (target.hasClass("localstorage-discard")) { - cleanLocalStorage_(); - } - $.publish(Events.HIDE_NOTIFICATION); - }); - } - }]); - } - } - }; + $.publish(Events.SHOW_NOTIFICATION, [{ + "content": content, + "behavior": function(rootNode) { + rootNode = $(rootNode); + rootNode.click(function(evt) { + var target = $(evt.target); + if(target.hasClass("localstorage-restore")) { + restoreFromLocalStorage_(); + } + else if (target.hasClass("localstorage-discard")) { + cleanLocalStorage_(); + } + $.publish(Events.HIDE_NOTIFICATION); + }); + } + }]); + } + } + }; })(); \ No newline at end of file diff --git a/js/Palette.js b/js/Palette.js index 7f1ffb6c..8460e398 100644 --- a/js/Palette.js +++ b/js/Palette.js @@ -7,37 +7,37 @@ $.namespace("pskl"); pskl.Palette = (function() { - - var paletteRoot, - paletteColors = []; + + var paletteRoot, + paletteColors = []; - /** - * @private - */ - var onPickerChange_ = function(evt, isPrimary) { + /** + * @private + */ + var onPickerChange_ = function(evt, isPrimary) { var inputPicker = $(evt.target); $.publish(Events.COLOR_SELECTED, [inputPicker.val(), evt.data.isPrimary]); - }; + }; - /** - * @private - */ - var createPalette_ = function (colors) { - // Always adding transparent color - paletteRoot.html(''); - for(var color in colors) { - if(color != Constants.TRANSPARENT_COLOR) { - addColorToPalette_(color); - } - } - }; + /** + * @private + */ + var createPalette_ = function (colors) { + // Always adding transparent color + paletteRoot.html(''); + for(var color in colors) { + if(color != Constants.TRANSPARENT_COLOR) { + addColorToPalette_(color); + } + } + }; - /** - * @private - */ - var addColorToPalette_ = function (color) { + /** + * @private + */ + var addColorToPalette_ = function (color) { if (paletteColors.indexOf(color) == -1 && color != Constants.TRANSPARENT_COLOR) { - var colorEl = document.createElement("li"); + var colorEl = document.createElement("li"); colorEl.className = "palette-color"; colorEl.setAttribute("data-color", color); colorEl.setAttribute("title", color); @@ -77,37 +77,37 @@ pskl.Palette = (function() { } else { colorPicker[0].color.fromString(color); } - } + }; - return { - init: function(framesheet) { - - paletteRoot = $("#palette"); + return { + init: function(framesheet) { + + paletteRoot = $("#palette"); - // Initialize palette: - createPalette_(framesheet.getUsedColors()); + // Initialize palette: + createPalette_(framesheet.getUsedColors()); - $.subscribe(Events.FRAMESHEET_RESET, function(evt) { - createPalette_(framesheet.getUsedColors()); - }); + $.subscribe(Events.FRAMESHEET_RESET, function(evt) { + createPalette_(framesheet.getUsedColors()); + }); - paletteRoot.mouseup(onPaletteColorClick_); - $.subscribe(Events.COLOR_SELECTED, function(evt, color) { - addColorToPalette_(color); - }); + paletteRoot.mouseup(onPaletteColorClick_); + $.subscribe(Events.COLOR_SELECTED, function(evt, color) { + addColorToPalette_(color); + }); - // Initialize colorpicker: - var colorPicker = $('#color-picker'); - colorPicker.val(Constants.DEFAULT_PEN_COLOR); - colorPicker.change({isPrimary : true}, onPickerChange_); + // Initialize colorpicker: + var colorPicker = $('#color-picker'); + colorPicker.val(Constants.DEFAULT_PEN_COLOR); + colorPicker.change({isPrimary : true}, onPickerChange_); var secondaryColorPicker = $('#secondary-color-picker'); secondaryColorPicker.val(Constants.TRANSPARENT_COLOR); secondaryColorPicker.change({isPrimary : false}, onPickerChange_); - } - }; + } + }; })(); diff --git a/js/ToolSelector.js b/js/ToolSelector.js index 0f69feaf..e2a9befa 100644 --- a/js/ToolSelector.js +++ b/js/ToolSelector.js @@ -8,75 +8,83 @@ $.namespace("pskl"); pskl.ToolSelector = (function() { - - var toolInstances = { - "simplePen" : new pskl.drawingtools.SimplePen(), - "eraser" : new pskl.drawingtools.Eraser(), - "paintBucket" : new pskl.drawingtools.PaintBucket(), - "stroke" : new pskl.drawingtools.Stroke(), - "rectangle" : new pskl.drawingtools.Rectangle(), - "move" : new pskl.drawingtools.Move() - }; - var currentSelectedTool = toolInstances.simplePen; - var previousSelectedTool = toolInstances.simplePen; + + var toolInstances = { + "simplePen" : new pskl.drawingtools.SimplePen(), + "eraser" : new pskl.drawingtools.Eraser(), + "paintBucket" : new pskl.drawingtools.PaintBucket(), + "stroke" : new pskl.drawingtools.Stroke(), + "rectangle" : new pskl.drawingtools.Rectangle(), + "move" : new pskl.drawingtools.Move() + }; + var currentSelectedTool = toolInstances.simplePen; + var previousSelectedTool = toolInstances.simplePen; - var selectTool_ = function(tool) { - var maincontainer = $("body"); - var previousSelectedToolClass = maincontainer.data("selected-tool-class"); - if(previousSelectedToolClass) { - maincontainer.removeClass(previousSelectedToolClass); - } - maincontainer.addClass(toolBehavior.toolId); - $("#drawing-canvas-container").data("selected-tool-class", toolBehavior.toolId); - }; - - var activateToolOnStage_ = function(tool) { - var stage = $("body"); + var activateToolOnStage_ = function(tool) { + var stage = $("body"); var previousSelectedToolClass = stage.data("selected-tool-class"); if(previousSelectedToolClass) { stage.removeClass(previousSelectedToolClass); } stage.addClass(tool.toolId); stage.data("selected-tool-class", tool.toolId); - }; + }; - var selectTool_ = function(tool) { - console.log("Selecting Tool:" , currentSelectedTool); - currentSelectedTool = tool; - activateToolOnStage_(currentSelectedTool); - $.publish(Events.TOOL_SELECTED, [tool]); - }; + var selectTool_ = function(tool) { + console.log("Selecting Tool:" , currentSelectedTool); + currentSelectedTool = tool; + activateToolOnStage_(currentSelectedTool); + $.publish(Events.TOOL_SELECTED, [tool]); + }; - /** - * @private - */ - var onToolIconClicked_ = function(evt) { - var target = $(evt.target); - var clickedTool = target.closest(".tool-icon"); + /** + * @private + */ + var onToolIconClicked_ = function(evt) { + var target = $(evt.target); + var clickedTool = target.closest(".tool-icon"); - if(clickedTool.length) { - for(var tool in toolInstances) { - if (toolInstances[tool].toolId == clickedTool.data()["toolId"]) { - selectTool_(toolInstances[tool]); + if(clickedTool.length) { + for(var tool in toolInstances) { + if (toolInstances[tool].toolId == clickedTool.data().toolId) { + selectTool_(toolInstances[tool]); - // Show tool as selected: - $("#tools-container .tool-icon.selected").removeClass("selected"); - clickedTool.addClass("selected"); - } - } - } - }; + // Show tool as selected: + $("#tools-container .tool-icon.selected").removeClass("selected"); + clickedTool.addClass("selected"); + } + } + } + }; - return { - init: function() { - - // Initialize tool: - // Set SimplePen as default selected tool: - selectTool_(toolInstances.simplePen); - // Activate listener on tool panel: - $("#tools-container").click(onToolIconClicked_); - } - }; + /** + * Get state for the checkbox that control the display of the grid + * on the drawing canvas. + * @private + */ + var isShowGridChecked_ = function() { + var showGridCheckbox = $('#show-grid'); + var isChecked = showGridCheckbox.is(':checked'); + return isChecked; + }; + + return { + init: function() { + + // Initialize tool: + // Set SimplePen as default selected tool: + selectTool_(toolInstances.simplePen); + // Activate listener on tool panel: + $("#tools-container").click(onToolIconClicked_); + + // Show/hide the grid on drawing canvas: + $.publish(Events.GRID_DISPLAY_STATE_CHANGED, [isShowGridChecked_()]); + $('#show-grid').change(function(evt) { + var checked = isShowGridChecked_(); + $.publish(Events.GRID_DISPLAY_STATE_CHANGED, [checked]); + }); + } + }; })(); diff --git a/js/controller/AnimatedPreviewController.js b/js/controller/AnimatedPreviewController.js index c8b7f51a..34b84eef 100644 --- a/js/controller/AnimatedPreviewController.js +++ b/js/controller/AnimatedPreviewController.js @@ -3,10 +3,12 @@ ns.AnimatedPreviewController = function (framesheet, container, dpi) { this.framesheet = framesheet; this.container = container; - this.animIndex = 0; + + this.elapsedTime = 0; + this.currentIndex = 0; this.fps = parseInt($("#preview-fps")[0].value, 10); - + var renderingOptions = { "dpi": dpi }; @@ -14,40 +16,24 @@ }; ns.AnimatedPreviewController.prototype.init = function () { - this.initDom(); - - this.renderer.init(this.framesheet.getFrameByIndex(this.animIndex)); - - this.startAnimationTimer(); - }; - - ns.AnimatedPreviewController.prototype.initDom = function () { $("#preview-fps")[0].addEventListener('change', this.onFPSSliderChange.bind(this)); }; - ns.AnimatedPreviewController.prototype.startAnimationTimer = function () { - this.stopAnimationTimer(); - this.animationTimer = window.setTimeout(this.refreshAnimatedPreview.bind(this), 1000/this.fps); - }; - - ns.AnimatedPreviewController.prototype.stopAnimationTimer = function () { - if (this.animationTimer) { - window.clearInterval(this.animationTimer); - this.animationTimer = null; - } - }; - ns.AnimatedPreviewController.prototype.onFPSSliderChange = function(evt) { this.fps = parseInt($("#preview-fps")[0].value, 10); }; - ns.AnimatedPreviewController.prototype.refreshAnimatedPreview = function () { - if (!this.framesheet.hasFrameAtIndex(this.animIndex)) { - this.animIndex = 0; + ns.AnimatedPreviewController.prototype.render = function (delta) { + this.elapsedTime += delta; + var index = Math.floor(this.elapsedTime / (1000/this.fps)); + if (index != this.currentIndex) { + this.currentIndex = index; + if (!this.framesheet.hasFrameAtIndex(this.currentIndex)) { + this.currentIndex = 0; + this.elapsedTime = 0; + } + this.renderer.render(this.framesheet.getFrameByIndex(this.currentIndex)); } - this.renderer.render(this.framesheet.getFrameByIndex(this.animIndex)); - this.animIndex++; - this.startAnimationTimer(); }; })(); \ No newline at end of file diff --git a/js/controller/DrawingController.js b/js/controller/DrawingController.js index d6803593..e77e4e85 100644 --- a/js/controller/DrawingController.js +++ b/js/controller/DrawingController.js @@ -1,48 +1,213 @@ (function () { var ns = $.namespace("pskl.controller"); - ns.DrawingController = function (frame, container, dpi) { - this.dpi = dpi; - + ns.DrawingController = function (framesheet, container, dpi) { + // TODO(vincz): Store user prefs in a localstorage string ? var renderingOptions = { "dpi": dpi, - "displayGrid": true // Retrieve from localsotrage config - } + "hasGrid" : true + }; - // Public - this.frame = frame; - this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(frame); // Type is frame + /** + * @public + */ + this.framesheet = framesheet; + + /** + * @public + */ + this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(framesheet.getCurrentFrame()); - // Private + /** + * @private + */ this.container = container; + this.renderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, "drawing-canvas"); this.overlayRenderer = new pskl.rendering.FrameRenderer(this.container, renderingOptions, "canvas-overlay"); - this.renderer.init(this.frame); - this.overlayRenderer.init(this.frame); + this.renderer.init(framesheet.getCurrentFrame()); + this.overlayRenderer.init(this.overlayFrame); + + // State of drawing controller: + this.isClicked = false; + this.isRightClicked = false; + this.previousMousemoveTime = 0; + this.currentToolBehavior = null; + this.primaryColor = Constants.DEFAULT_PEN_COLOR; + this.secondaryColor = Constants.TRANSPARENT_COLOR; + + this.initMouseBehavior(); + + $.subscribe(Events.TOOL_SELECTED, $.proxy(function(evt, toolBehavior) { + console.log("Tool selected: ", toolBehavior); + this.currentToolBehavior = toolBehavior; + }, this)); + + $.subscribe(Events.COLOR_SELECTED, $.proxy(function(evt, color, isPrimary) { + console.log("Color selected: ", color); + if (isPrimary) { + this.primaryColor = color; + } else { + this.secondaryColor = color; + } + }, this)); }; + ns.DrawingController.prototype.initMouseBehavior = function() { + var body = $('body'); + this.container.mousedown($.proxy(this.onMousedown_, this)); + this.container.mousemove($.proxy(this.onMousemove_, this)); + body.mouseup($.proxy(this.onMouseup_, this)); + + // Deactivate right click: + this.container.contextmenu(this.onCanvasContextMenu_); + }; + + /** + * @private + */ + ns.DrawingController.prototype.onMousedown_ = function (event) { + this.isClicked = true; + + if(event.button == 2) { // right click + this.isRightClicked = true; + $.publish(Events.CANVAS_RIGHT_CLICKED); + } + + var coords = this.getSpriteCoordinates(event); + + this.currentToolBehavior.applyToolAt( + coords.col, coords.row, + this.getCurrentColor_(), + this.framesheet.getCurrentFrame(), + this.overlayFrame + ); + + $.publish(Events.LOCALSTORAGE_REQUEST); + }; + + /** + * @private + */ + ns.DrawingController.prototype.onMousemove_ = function (event) { + var currentTime = new Date().getTime(); + // Throttling of the mousemove event: + if ((currentTime - this.previousMousemoveTime) > 40 ) { + var coords = this.getSpriteCoordinates(event); + if (this.isClicked) { + + this.currentToolBehavior.moveToolAt( + coords.col, coords.row, + this.getCurrentColor_(), + this.framesheet.getCurrentFrame(), + this.overlayFrame + ); + + // TODO(vincz): Find a way to move that to the model instead of being at the interaction level. + // Eg when drawing, it may make sense to have it here. However for a non drawing tool, + // you don't need to draw anything when mousemoving and you request useless localStorage. + $.publish(Events.LOCALSTORAGE_REQUEST); + } + this.previousMousemoveTime = currentTime; + } + }; + + /** + * @private + */ + ns.DrawingController.prototype.onMouseup_ = function (event) { + if(this.isClicked || this.isRightClicked) { + // A mouse button was clicked on the drawing canvas before this mouseup event, + // the user was probably drawing on the canvas. + // Note: The mousemove movement (and the mouseup) may end up outside + // of the drawing canvas. + + this.isClicked = false; + this.isRightClicked = false; + + var coords = this.getSpriteCoordinates(event); + //console.log("mousemove: col: " + spriteCoordinate.col + " - row: " + spriteCoordinate.row); + this.currentToolBehavior.releaseToolAt( + coords.col, coords.row, + this.getCurrentColor_(), + this.framesheet.getCurrentFrame(), + this.overlayFrame + ); + + $.publish(Events.TOOL_RELEASED); + } + }, + + /** + * @private + */ + ns.DrawingController.prototype.getRelativeCoordinates = function (clientX, clientY) { + var canvasPageOffset = this.container.offset(); + return { + x : clientX - canvasPageOffset.left, + y : clientY - canvasPageOffset.top + }; + }; + + /** + * @private + */ + ns.DrawingController.prototype.getSpriteCoordinates = function(event) { + var coords = this.getRelativeCoordinates(event.clientX, event.clientY); + return this.renderer.convertPixelCoordinatesIntoSpriteCoordinate(coords); + }; + + /** + * @private + */ + ns.DrawingController.prototype.getCurrentColor_ = function () { + if(this.isRightClicked) { + return this.secondaryColor; + } else { + return this.primaryColor; + } + }; + + /** + * @private + */ + ns.DrawingController.prototype.onCanvasContextMenu_ = function (event) { + event.preventDefault(); + event.stopPropagation(); + event.cancelBubble = true; + return false; + }; + ns.DrawingController.prototype.updateDPI = function (newDPI) { this.renderer.updateDPI(newDPI); this.overlayRenderer.updateDPI(newDPI); - this.renderer.render(this.frame); - this.overlayRenderer.render(this.overlayFrame); + this.render(); }; + ns.DrawingController.prototype.render = function () { + try { + this.renderFrame(); + this.renderOverlay(); + } catch (e) { + // TODO : temporary t/c for integration + } + }; + ns.DrawingController.prototype.renderFrame = function () { - this.renderer.render(this.frame); - }; - - ns.DrawingController.prototype.renderFramePixel = function (col, row) { - this.renderer.drawPixel(col, row, this.frame); + var frame = this.framesheet.getCurrentFrame(); + var serializedFrame = frame.serialize(); + if (this.serializedFrame != serializedFrame) { + this.serializedFrame = serializedFrame + this.renderer.render(frame); + } }; ns.DrawingController.prototype.renderOverlay = function () { - this.overlayRenderer.render(this.overlayFrame); - }; - - ns.DrawingController.prototype.clearOverlay = function () { - this.overlayFrame = pskl.model.Frame.createEmptyFromFrame(this.frame); - this.overlayRenderer.clear(); + var serializedOverlay = this.overlayFrame.serialize(); + if (this.serializedOverlay != serializedOverlay) { + this.serializedOverlay = serializedOverlay + this.overlayRenderer.render(this.overlayFrame); + } }; })(); \ No newline at end of file diff --git a/js/controller/PreviewFilmController.js b/js/controller/PreviewFilmController.js index 75383df0..389a12f8 100644 --- a/js/controller/PreviewFilmController.js +++ b/js/controller/PreviewFilmController.js @@ -5,49 +5,62 @@ this.dpi = dpi; this.framesheet = framesheet; this.container = container; + + this.redrawFlag = false; + + $.subscribe(Events.TOOL_RELEASED, this.flagForRedraw_.bind(this)); + $.subscribe(Events.FRAMESHEET_RESET, this.flagForRedraw_.bind(this)); }; ns.PreviewFilmController.prototype.init = function() { - var addFrameButton = $('#add-frame-button')[0]; - addFrameButton.addEventListener('mousedown', this.addFrame.bind(this)); - this.createPreviews(); - - + var addFrameButton = $('#add-frame-button')[0]; + addFrameButton.addEventListener('mousedown', this.addFrame.bind(this)); }; ns.PreviewFilmController.prototype.addFrame = function () { this.framesheet.addEmptyFrame(); - piskel.setActiveFrameAndRedraw(this.framesheet.getFrameCount() - 1); + this.framesheet.setCurrentFrameIndex(this.framesheet.getFrameCount() - 1); }; - ns.PreviewFilmController.prototype.createPreviews = function () { - // TODO(vincz): Full redraw on any drawing modification, optimize. - this.container.html(""); + ns.PreviewFilmController.prototype.flagForRedraw_ = function () { + this.redrawFlag = true; + }; - var frameCount = this.framesheet.getFrameCount(); - - for (var i = 0, l = frameCount; i < l ; i++) { - this.container.append(this.createInterstitialTile_(i)); - this.container.append(this.createPreviewTile_(i)); - } - this.container.append(this.createInterstitialTile_(frameCount)); + ns.PreviewFilmController.prototype.render = function () { + if (this.redrawFlag) { + // TODO(vincz): Full redraw on any drawing modification, optimize. + this.createPreviews_(); + this.redrawFlag = false; + } + }; - var needDragndropBehavior = !!(frameCount > 1); - if(needDragndropBehavior) { - this.initDragndropBehavior_(); - } + ns.PreviewFilmController.prototype.createPreviews_ = function () { + this.container.html(""); + + var frameCount = this.framesheet.getFrameCount(); + + for (var i = 0, l = frameCount; i < l ; i++) { + this.container.append(this.createInterstitialTile_(i)); + this.container.append(this.createPreviewTile_(i)); + } + this.container.append(this.createInterstitialTile_(frameCount)); + + var needDragndropBehavior = !!(frameCount > 1); + if(needDragndropBehavior) { + this.initDragndropBehavior_(); + } }; /** * @private */ ns.PreviewFilmController.prototype.createInterstitialTile_ = function (tileNumber) { - var initerstitialTile = document.createElement("div"); - initerstitialTile.className = "interstitial-tile" - initerstitialTile.setAttribute("data-tile-type", "interstitial"); - initerstitialTile.setAttribute("data-inject-drop-tile-at", tileNumber); + var interstitialTile = document.createElement("div"); + interstitialTile.className = "interstitial-tile" + interstitialTile.setAttribute("data-tile-type", "interstitial"); + interstitialTile.setAttribute("data-inject-drop-tile-at", tileNumber); - return initerstitialTile; + return interstitialTile; }; /** @@ -96,8 +109,8 @@ // inside the drag target. We normalize that by taking the correct ancestor: var originTile = $(event.srcElement).closest(".preview-tile"); var originFrameId = parseInt(originTile.data("tile-number"), 10); + var dropTarget = $(event.target); - if(dropTarget.data("tile-type") == "interstitial") { var targetInsertionId = parseInt(dropTarget.data("inject-drop-tile-at"), 10); // In case we drop outside of the tile container @@ -115,8 +128,7 @@ if(activeFrame > (this.framesheet.getFrameCount() - 1)) { activeFrame = targetInsertionId - 1; } - } - else { + } else { var targetSwapId = parseInt(dropTarget.data("tile-number"), 10); // In case we drop outside of the tile container if(isNaN(originFrameId) || isNaN(targetSwapId)) { @@ -130,12 +142,10 @@ $('#preview-list').removeClass("show-interstitial-tiles"); - // TODO(vincz): deprecate. - piskel.setActiveFrameAndRedraw(activeFrame); + this.framesheet.setCurrentFrameIndex(activeFrame); // TODO(vincz): move localstorage request to the model layer? $.publish(Events.LOCALSTORAGE_REQUEST); - }; /** @@ -144,54 +154,35 @@ */ ns.PreviewFilmController.prototype.createPreviewTile_ = function(tileNumber) { var currentFrame = this.framesheet.getFrameByIndex(tileNumber); - //var width = frame.getWidth() * this.dpi, - // height = frame.getHeight() * this.dpi; - + var previewTileRoot = document.createElement("li"); var classname = "preview-tile"; previewTileRoot.setAttribute("data-tile-number", tileNumber); - if (piskel.getActiveFrameIndex() == tileNumber) { + if (this.framesheet.getCurrentFrame() == currentFrame) { classname += " selected"; } previewTileRoot.className = classname; var canvasContainer = document.createElement("div"); canvasContainer.className = "canvas-container"; - //canvasContainer.setAttribute('style', 'width:' + width + 'px; height:' + height + 'px;'); - + var canvasBackground = document.createElement("div"); canvasBackground.className = "canvas-background"; canvasContainer.appendChild(canvasBackground); - /* - var canvasPreview = document.createElement("canvas"); - canvasPreview.className = "canvas tile-view" - - canvasPreview.setAttribute('width', width); - canvasPreview.setAttribute('height', height); - */ - previewTileRoot.addEventListener('click', function(evt) { - // has not class tile-action: - if(!evt.target.classList.contains('tile-action')) { - piskel.setActiveFrameAndRedraw(tileNumber); - } - }); + previewTileRoot.addEventListener('click', this.onPreviewClick_.bind(this, tileNumber)); var canvasPreviewDuplicateAction = document.createElement("button"); canvasPreviewDuplicateAction.className = "tile-action" canvasPreviewDuplicateAction.innerHTML = "dup" - canvasPreviewDuplicateAction.addEventListener('click', function(evt) { - piskel.duplicateFrame(tileNumber); - }); + canvasPreviewDuplicateAction.addEventListener('click', this.onAddButtonClick_.bind(this, tileNumber)); - //this.renderer.render(this.framesheet.getFrameByIndex(tileNumber), canvasPreview); - // TODO(vincz): Eventually optimize this part by not recreating a FrameRenderer. Note that the real optim // is to make this update function (#createPreviewTile) less aggressive. var renderingOptions = {"dpi": this.dpi }; - var currentFrameRenderer = new pskl.rendering.FrameRenderer(canvasContainer, renderingOptions, "tile-view"); + var currentFrameRenderer = new pskl.rendering.FrameRenderer($(canvasContainer), renderingOptions, "tile-view"); currentFrameRenderer.init(currentFrame); previewTileRoot.appendChild(canvasContainer); @@ -201,12 +192,29 @@ var canvasPreviewDeleteAction = document.createElement("button"); canvasPreviewDeleteAction.className = "tile-action" canvasPreviewDeleteAction.innerHTML = "del" - canvasPreviewDeleteAction.addEventListener('click', function(evt) { - piskel.removeFrame(tileNumber); - }); + canvasPreviewDeleteAction.addEventListener('click', this.onDeleteButtonClick_.bind(this, tileNumber)); previewTileRoot.appendChild(canvasPreviewDeleteAction); } return previewTileRoot; }; + + ns.PreviewFilmController.prototype.onPreviewClick_ = function (index, evt) { + // has not class tile-action: + if(!evt.target.classList.contains('tile-action')) { + this.framesheet.setCurrentFrameIndex(index); + } + }; + + ns.PreviewFilmController.prototype.onDeleteButtonClick_ = function (index, evt) { + this.framesheet.removeFrameByIndex(index); + $.publish(Events.FRAMESHEET_RESET); + $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model + }; + + ns.PreviewFilmController.prototype.onAddButtonClick_ = function (index, evt) { + this.framesheet.duplicateFrameByIndex(index); + $.publish(Events.LOCALSTORAGE_REQUEST); // Should come from model + this.framesheet.setCurrentFrameIndex(index + 1); + }; })(); \ No newline at end of file diff --git a/js/drawingtools/BaseTool.js b/js/drawingtools/BaseTool.js index 905aa162..84f96824 100644 --- a/js/drawingtools/BaseTool.js +++ b/js/drawingtools/BaseTool.js @@ -8,11 +8,11 @@ ns.BaseTool = function() {}; - ns.BaseTool.prototype.applyToolAt = function(col, row, frame) {}; + ns.BaseTool.prototype.applyToolAt = function(col, row, color, frame, overlay) {}; - ns.BaseTool.prototype.moveToolAt = function(col, row, frame) {}; + ns.BaseTool.prototype.moveToolAt = function(col, row, color, frame, overlay) {}; - ns.BaseTool.prototype.releaseToolAt = function(col, row, frame) {}; + ns.BaseTool.prototype.releaseToolAt = function(col, row, color, frame, overlay) {}; /** * Bresenham line algorihtm: Get an array of pixels from diff --git a/js/drawingtools/Eraser.js b/js/drawingtools/Eraser.js index e83cf617..064adf85 100644 --- a/js/drawingtools/Eraser.js +++ b/js/drawingtools/Eraser.js @@ -16,7 +16,7 @@ /** * @override */ - ns.Eraser.prototype.applyToolAt = function(col, row, color, drawer) { - this.superclass.applyToolAt.call(this, col, row, Constants.TRANSPARENT_COLOR, drawer); + ns.Eraser.prototype.applyToolAt = function(col, row, color, frame, overlay) { + this.superclass.applyToolAt.call(this, col, row, Constants.TRANSPARENT_COLOR, frame, overlay); }; })(); \ No newline at end of file diff --git a/js/drawingtools/Move.js b/js/drawingtools/Move.js index 2ff9ca73..ec120287 100644 --- a/js/drawingtools/Move.js +++ b/js/drawingtools/Move.js @@ -19,17 +19,16 @@ /** * @override */ - ns.Move.prototype.applyToolAt = function(col, row, color, drawer) { + ns.Move.prototype.applyToolAt = function(col, row, color, frame, overlay) { this.startCol = col; this.startRow = row; - this.frameClone = drawer.frame.clone(); + this.frameClone = frame.clone(); }; - ns.Move.prototype.moveToolAt = function(col, row, color, drawer) { + ns.Move.prototype.moveToolAt = function(col, row, color, frame, overlay) { var colDiff = col - this.startCol, rowDiff = row - this.startRow; if (colDiff != 0 || rowDiff != 0) { - this.shiftFrame(colDiff, rowDiff, drawer.frame, this.frameClone); - drawer.renderFrame(); + this.shiftFrame(colDiff, rowDiff, frame, this.frameClone); } }; @@ -50,7 +49,7 @@ /** * @override */ - ns.Move.prototype.releaseToolAt = function(col, row, color, drawer) { - this.moveToolAt(col, row, color, drawer); + ns.Move.prototype.releaseToolAt = function(col, row, color, frame, overlay) { + this.moveToolAt(col, row, color, frame, overlay); }; })(); diff --git a/js/drawingtools/PaintBucket.js b/js/drawingtools/PaintBucket.js index a4523bd7..b3176324 100644 --- a/js/drawingtools/PaintBucket.js +++ b/js/drawingtools/PaintBucket.js @@ -15,16 +15,11 @@ /** * @override */ - ns.PaintBucket.prototype.applyToolAt = function(col, row, color, drawer) { + ns.PaintBucket.prototype.applyToolAt = function(col, row, color, frame, overlay) { // Change model: - var targetColor = drawer.frame.getPixel(col, row); - //this.recursiveFloodFill_(frame, col, row, targetColor, color); - this.queueLinearFloodFill_(drawer.frame, col, row, targetColor, color); - - // Draw in canvas: - // TODO: Remove that when we have the centralized redraw loop - drawer.renderFrame(); + var targetColor = frame.getPixel(col, row); + this.queueLinearFloodFill_(frame, col, row, targetColor, color); }; /** diff --git a/js/drawingtools/Rectangle.js b/js/drawingtools/Rectangle.js index 36967878..f5fd906d 100644 --- a/js/drawingtools/Rectangle.js +++ b/js/drawingtools/Rectangle.js @@ -19,18 +19,16 @@ /** * @override */ - ns.Rectangle.prototype.applyToolAt = function(col, row, color, drawer) { + ns.Rectangle.prototype.applyToolAt = function(col, row, color, frame, overlay) { this.startCol = col; this.startRow = row; // Drawing the first point of the rectangle in the fake overlay canvas: - drawer.overlayFrame.setPixel(col, row, color); - drawer.renderOverlay(); + overlay.setPixel(col, row, color); }; - ns.Rectangle.prototype.moveToolAt = function(col, row, color, drawer) { - // Clean overlay canvas: - drawer.clearOverlay(); + ns.Rectangle.prototype.moveToolAt = function(col, row, color, frame, overlay) { + overlay.clear(); // When the user moussemove (before releasing), we dynamically compute the // pixel to draw the line and draw this line in the overlay : @@ -42,29 +40,25 @@ if(color == Constants.TRANSPARENT_COLOR) { color = Constants.SELECTION_TRANSPARENT_COLOR; } - drawer.overlayFrame.setPixel(strokePoints[i].col, strokePoints[i].row, color); + overlay.setPixel(strokePoints[i].col, strokePoints[i].row, color); } - drawer.renderOverlay(); }; /** * @override */ - ns.Rectangle.prototype.releaseToolAt = function(col, row, color, drawer) { + ns.Rectangle.prototype.releaseToolAt = function(col, row, color, frame, overlay) { + overlay.clear(); // If the stroke tool is released outside of the canvas, we cancel the stroke: - if(drawer.frame.containsPixel(col, row)) { + if(frame.containsPixel(col, row)) { var strokePoints = this.getRectanglePixels_(this.startCol, col, this.startRow, row); for(var i = 0; i< strokePoints.length; i++) { // Change model: - drawer.frame.setPixel(strokePoints[i].col, strokePoints[i].row, color); + frame.setPixel(strokePoints[i].col, strokePoints[i].row, color); } // The user released the tool to draw a line. We will compute the pixel coordinate, impact - // the model and draw them in the drawing canvas (not the fake overlay anymore) - // Draw in canvas: - // TODO: Remove that when we have the centralized redraw loop - drawer.renderFrame(); + // the model and draw them in the drawing canvas (not the fake overlay anymore) } - drawer.clearOverlay(); }; /** diff --git a/js/drawingtools/SimplePen.js b/js/drawingtools/SimplePen.js index 39a27363..ffc0047f 100644 --- a/js/drawingtools/SimplePen.js +++ b/js/drawingtools/SimplePen.js @@ -18,31 +18,27 @@ /** * @override */ - ns.SimplePen.prototype.applyToolAt = function(col, row, color, drawer) { - if (drawer.frame.containsPixel(col, row)) { + ns.SimplePen.prototype.applyToolAt = function(col, row, color, frame, overlay) { + if (frame.containsPixel(col, row)) { this.previousCol = col; this.previousRow = row; - drawer.frame.setPixel(col, row, color); - - // Draw on canvas: - // TODO: Remove that when we have the centralized redraw loop - drawer.renderFramePixel(col, row); + frame.setPixel(col, row, color); } }; - ns.SimplePen.prototype.moveToolAt = function(col, row, color, drawer) { - + ns.SimplePen.prototype.moveToolAt = function(col, row, color, frame, overlay) { if((Math.abs(col - this.previousCol) > 1) || (Math.abs(row - this.previousRow) > 1)) { // The pen movement is too fast for the mousemove frequency, there is a gap between the // current point and the previously drawn one. // We fill the gap by calculating missing dots (simple linear interpolation) and draw them. var interpolatedPixels = this.getLinePixels_(col, this.previousCol, row, this.previousRow); for(var i=0, l=interpolatedPixels.length; i 40 ) { - var spriteCoordinate = this.getSpriteCoordinate(event); - if (isClicked) { - - currentToolBehavior.moveToolAt( - spriteCoordinate.col, - spriteCoordinate.row, - this.getCurrentColor(), - this.drawingController - ); - - // TODO(vincz): Find a way to move that to the model instead of being at the interaction level. - // Eg when drawing, it may make sense to have it here. However for a non drawing tool, - // you don't need to draw anything when mousemoving and you request useless localStorage. - $.publish(Events.LOCALSTORAGE_REQUEST); - } else { - // debug mode to see the selected pixel - // this.drawingController.clearOverlay(); - // this.drawingController.overlay.setPixel( spriteCoordinate.col,spriteCoordinate.row, "#ff0000"); - // this.drawingController.renderOverlay(); - } - previousMousemoveTime = currentTime; - } - }, - - onMouseup : function (event) { - if(isClicked || isRightClicked) { - // A mouse button was clicked on the drawing canvas before this mouseup event, - // the user was probably drawing on the canvas. - // Note: The mousemove movement (and the mouseup) may end up outside - // of the drawing canvas. - if(isRightClicked) { - $.publish(Events.CANVAS_RIGHT_CLICK_RELEASED); - } - - - isClicked = false; - isRightClicked = false; - var spriteCoordinate = this.getSpriteCoordinate(event); - currentToolBehavior.releaseToolAt( - spriteCoordinate.col, - spriteCoordinate.row, - this.getCurrentColor(), - this.drawingController - ); - - - $.publish(Events.TOOL_RELEASED); - // TODO: Remove that when we have the centralized redraw loop - this.previewsController.createPreviews(); - } - }, - - onCanvasContextMenu : function (event) { - event.preventDefault(); - event.stopPropagation(); - event.cancelBubble = true; - return false; - }, - - getRelativeCoordinates : function (x, y) { - var canvasRect = $(".drawing-canvas")[0].getBoundingClientRect(); - return { - x : x - canvasRect.left, - y : y - canvasRect.top - } - }, - - getSpriteCoordinate : function(event) { - var coord = this.getRelativeCoordinates(event.x, event.y); - var coords = this.getRelativeCoordinates(event.clientX, event.clientY); - return { - "col" : (coords.x - coords.x%drawingCanvasDpi) / drawingCanvasDpi, - "row" : (coords.y - coords.y%drawingCanvasDpi) / drawingCanvasDpi - } - }, - // TODO(julz): Create package ? storeSheet : function (event) { // TODO Refactor using jquery ? diff --git a/js/rendering/DrawingLoop.js b/js/rendering/DrawingLoop.js new file mode 100644 index 00000000..9d13a239 --- /dev/null +++ b/js/rendering/DrawingLoop.js @@ -0,0 +1,58 @@ +(function () { + var ns = $.namespace("pskl.rendering"); + + ns.DrawingLoop = function () { + this.requestAnimationFrame = this.getRequestAnimationFrameShim_(); + this.isRunning = false; + this.previousTime = 0; + this.callbacks = []; + }; + + ns.DrawingLoop.prototype.addCallback = function (callback, scope, args) { + var callbackObj = { + fn : callback, + scope : scope, + args : args + }; + this.callbacks.push(callbackObj); + return callbackObj; + }; + + ns.DrawingLoop.prototype.removeCallback = function (callbackObj) { + var index = this.callbacks.indexOf(callbackObj); + if (index != -1) { + this.callbacks.splice(index, 1); + } + }; + + ns.DrawingLoop.prototype.start = function () { + this.isRunning = true; + this.loop_(); + }; + + ns.DrawingLoop.prototype.loop_ = function () { + var currentTime = Date.now(); + var delta = currentTime - this.previousTime; + this.executeCallbacks_(delta); + this.previousTime = currentTime; + this.requestAnimationFrame.call(window, this.loop_.bind(this)); + }; + + ns.DrawingLoop.prototype.executeCallbacks_ = function (deltaTime) { + for (var i = 0 ; i < this.callbacks.length ; i++) { + var cb = this.callbacks[i]; + cb.fn.call(cb.scope, deltaTime, cb.args); + } + }; + + ns.DrawingLoop.prototype.stop = function () { + this.isRunning = false; + }; + + ns.DrawingLoop.prototype.getRequestAnimationFrameShim_ = function () { + var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000/60)}; + + return requestAnimationFrame; + } +})() \ No newline at end of file diff --git a/js/rendering/FrameRenderer.js b/js/rendering/FrameRenderer.js index 63b96533..20d42ad4 100644 --- a/js/rendering/FrameRenderer.js +++ b/js/rendering/FrameRenderer.js @@ -1,29 +1,41 @@ (function () { var ns = $.namespace("pskl.rendering"); - this.dpi = null; - this.canvas = null; - ns.FrameRenderer = function (container, renderingOptions, className) { + + this.defaultRenderingOptions = { + "hasGrid" : false + }; + renderingOptions = $.extend(true, {}, this.defaultRenderingOptions, renderingOptions); + if(container == undefined) { throw "Bad FrameRenderer initialization. undefined."; } - this.container = container; - - if(renderingOptions == undefined || renderingOptions.dpi == undefined || isNaN(dpi)) { + + if(isNaN(renderingOptions.dpi)) { throw "Bad FrameRenderer initialization. not well defined."; } - this.displayGrid = !!renderingOptions.displayGrid; + this.container = container; this.dpi = renderingOptions.dpi; this.className = className; + this.canvas = null; + this.hasGrid = renderingOptions.hasGrid; + this.gridStrokeWidth = 0; + + this.lastRenderedFrame = null; // Flag to know if the config was altered this.canvasConfigDirty = true; + + if(this.hasGrid) { + $.subscribe(Events.GRID_DISPLAY_STATE_CHANGED, $.proxy(this.showGrid, this)); + } }; ns.FrameRenderer.prototype.init = function (frame) { this.render(frame); + this.lastRenderedFrame = frame; }; ns.FrameRenderer.prototype.updateDPI = function (newDPI) { @@ -31,57 +43,124 @@ this.canvasConfigDirty = true; }; + ns.FrameRenderer.prototype.showGrid = function (evt, show) { + + this.gridStrokeWidth = 0; + if(show) { + this.gridStrokeWidth = Constants.GRID_STROKE_WIDTH; + } + + this.canvasConfigDirty = true; + + if(this.lastRenderedFrame) { + this.render(this.lastRenderedFrame); + } + }; + ns.FrameRenderer.prototype.render = function (frame) { + this.clear(frame); + var context = this.getCanvas_(frame).getContext('2d'); for(var col = 0, width = frame.getWidth(); col < width; col++) { for(var row = 0, height = frame.getHeight(); row < height; row++) { - this.drawPixel(col, row, frame, this.getCanvas_(frame), this.dpi); + var color = frame.getPixel(col, row); + this.renderPixel_(color, col, row, context); } } + this.lastRenderedFrame = frame; }; ns.FrameRenderer.prototype.drawPixel = function (col, row, frame) { var context = this.getCanvas_(frame).getContext('2d'); var color = frame.getPixel(col, row); if(color == Constants.TRANSPARENT_COLOR) { - context.clearRect(col * this.dpi, row * this.dpi, this.dpi, this.dpi); - } - else { - if(color != Constants.SELECTION_TRANSPARENT_COLOR) { - // TODO(vincz): Found a better design to update the palette, it's called too frequently. - $.publish(Events.COLOR_USED, [color]); - } + context.clearRect(this.getFramePos_(col), this.getFramePos_(row), this.dpi, this.dpi); + } else { + this.renderPixel_(color, col, row, context); + } + this.lastRenderedFrame = frame; + }; + + ns.FrameRenderer.prototype.renderPixel_ = function (color, col, row, context) { + if(color != Constants.TRANSPARENT_COLOR) { context.fillStyle = color; - context.fillRect(col * this.dpi, row * this.dpi, this.dpi, this.dpi); + context.fillRect(this.getFramePos_(col), this.getFramePos_(row), this.dpi, this.dpi); } }; - ns.FrameRenderer.prototype.clear = function (col, row, frame) { - var canvas = this.getCanvas_(frame) + ns.FrameRenderer.prototype.clear = function (frame) { + var canvas = this.getCanvas_(frame); canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); }; + /** + * Transform a screen pixel-based coordinate (relative to the top-left corner of the rendered + * frame) into a sprite coordinate in column and row. + * @public + */ + ns.FrameRenderer.prototype.convertPixelCoordinatesIntoSpriteCoordinate = function(coords) { + var cellSize = this.dpi + this.gridStrokeWidth; + return { + "col" : (coords.x - coords.x % cellSize) / cellSize, + "row" : (coords.y - coords.y % cellSize) / cellSize + }; + }; + + /** + * @private + */ + ns.FrameRenderer.prototype.getFramePos_ = function(index) { + return index * this.dpi + ((index - 1) * this.gridStrokeWidth); + }; + + /** + * @private + */ + ns.FrameRenderer.prototype.drawGrid_ = function(canvas, width, height, col, row) { + var ctx = canvas.getContext("2d"); + ctx.lineWidth = Constants.GRID_STROKE_WIDTH; + ctx.strokeStyle = Constants.GRID_STROKE_COLOR; + for(var c=1; c < col; c++) { + ctx.moveTo(this.getFramePos_(c), 0); + ctx.lineTo(this.getFramePos_(c), height); + ctx.stroke(); + } + + for(var r=1; r < row; r++) { + ctx.moveTo(0, this.getFramePos_(r)); + ctx.lineTo(width, this.getFramePos_(r)); + ctx.stroke(); + } + }; + /** * @private */ ns.FrameRenderer.prototype.getCanvas_ = function (frame) { if(this.canvasConfigDirty) { $(this.canvas).remove(); - var width = frame.getWidth(), - height = frame.getHeight(); + + var col = frame.getWidth(), + row = frame.getHeight(); var canvas = document.createElement("canvas"); - canvas.setAttribute("width", width * this.dpi); - canvas.setAttribute("height", height * this.dpi); + var pixelWidth = col * this.dpi + this.gridStrokeWidth * (col - 1); + var pixelHeight = row * this.dpi + this.gridStrokeWidth * (row - 1); + canvas.setAttribute("width", pixelWidth); + canvas.setAttribute("height", pixelHeight); var canvasClassname = "canvas"; if(this.className) { canvasClassname += " " + this.className; } canvas.setAttribute("class", canvasClassname); - - this.canvas = canvas; - this.container.appendChild(this.canvas); + this.container.append(canvas); + if(this.gridStrokeWidth > 0) { + this.drawGrid_(canvas, pixelWidth, pixelHeight, col, row); + } + + + this.canvas = canvas; this.canvasConfigDirty = false; } return this.canvas; diff --git a/js/utils.js b/js/utils.js index 2b475b1a..9fb8da68 100644 --- a/js/utils.js +++ b/js/utils.js @@ -16,7 +16,7 @@ jQuery.namespace = function() { * * @require Constants */ -(function(ns) { // namespace: pskl.utils +(function() { // namespace: pskl.utils var ns = $.namespace("pskl.utils"); @@ -35,5 +35,5 @@ jQuery.namespace = function() { //prototypeskl.ToolBehavior.Eraser.prototype.constructor = pskl.ToolBehavior.Eraser; }; -})() +})(); diff --git a/package.json b/package.json new file mode 100644 index 00000000..3de5a1df --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "author": "People", + "name": "piskel", + "description": "Web based 2d animations editor", + "version": "0.0.1", + "homepage": "http://github.com/juliandescottes/piskel", + "repository": { + "type": "git", + "url": "http://github.com/juliandescottes/piskel.git" + }, + "scripts": { "test": "make test" }, + "devDependencies": { + "jshint": "0.6.1" + } +} \ No newline at end of file