diff --git a/css/style.css b/css/style.css index fc983431..7859b01a 100644 --- a/css/style.css +++ b/css/style.css @@ -127,6 +127,13 @@ ul, li { z-index: 1; } +.canvas-main { + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + .canvas-overlay { position: absolute; top: 0; diff --git a/index.html b/index.html index 209d828a..f4cca82f 100644 --- a/index.html +++ b/index.html @@ -75,6 +75,9 @@ + + + diff --git a/js/FrameSheetModel.js b/js/FrameSheetModel.js index efd77a4e..920d7445 100644 --- a/js/FrameSheetModel.js +++ b/js/FrameSheetModel.js @@ -8,24 +8,6 @@ pskl.FrameSheetModel = (function() { var width; var height; - /** - * Create empty frame of dimension [width * height] with Constants.TRANSPARENT_COLOR - * as a default value. - * - * @private - */ - var createEmptyFrame_ = function() { - var emptyFrame = []; //new Array(width); - for (var columnIndex=0; columnIndex < width; columnIndex++) { - var columnArray = []; - for(var heightIndex = 0; heightIndex < height; heightIndex++) { - columnArray.push(Constants.TRANSPARENT_COLOR); - } - emptyFrame[columnIndex] = columnArray; - } - return emptyFrame; - }; - /** * @private */ @@ -38,33 +20,15 @@ pskl.FrameSheetModel = (function() { return true; // I'm always right dude }, - getAllPixels : function () { - var pixels = []; - for (var i = 0 ; i < frames.length ; i++) { - pixels = pixels.concat(this._getFramePixels(frames[i])); - } - return pixels; - }, - - _getFramePixels : function (frame) { - var pixels = []; - for (var i = 0 ; i < frame.length ; i++) { - var line = frame[i]; - for (var j = 0 ; j < line.length ; j++) { - pixels.push(line[j]); - } - } - return pixels; - }, - getUsedColors: function() { var colors = {}; for (var frameIndex=0; frameIndex < frames.length; frameIndex++) { - var currentFrame = frames[frameIndex]; - for (var i = 0 ; i < currentFrame.length ; i++) { - var line = currentFrame[i]; - for (var j = 0 ; j < line.length ; j++) { - colors[line[j]] = line[j]; + var frame = frames[frameIndex]; + for (var i = 0, width = frame.getWidth(); i < width ; i++) { + var line = frame[i]; + for (var j = 0, height = frame.getHeight() ; j < height ; j++) { + var pixel = frame.getPixel(i, j); + colors[pixel] = pixel; } } } @@ -92,7 +56,7 @@ pskl.FrameSheetModel = (function() { }, addEmptyFrame: function() { - frames.push(createEmptyFrame_()); + frames.push(pskl.rendering.Frame.createEmpty(width, height)); }, getFrameCount: function() { @@ -101,9 +65,9 @@ pskl.FrameSheetModel = (function() { getFrameByIndex: function(index) { if (isNaN(index)) { - throw "Bad argument value for getFrameByIndex method: <" + index + ">" + throw "Bad argument value for getFrameByIndex method: <" + index + ">"; } else if (index < 0 || index > frames.length) { - throw "Out of bound index for frameSheet object." + throw "Out of bound index for frameSheet object."; } return frames[index]; @@ -116,17 +80,12 @@ pskl.FrameSheetModel = (function() { frames.splice(index, 1); }, - duplicateFrameByIndex: function(frameToDuplicateIndex) { - var frame = inst.getFrameByIndex(frameToDuplicateIndex); - var clonedFrame = []; - for(var i=0, l=frame.length; i= frame_.length || - row_ < 0 || - row_ >= frame_[0].length) { - return false; - } - return true; - } - queue.push({"col": col, "row": row}); var loopCount = 0; - var cellCount = frame.length * frame[0].length; + var cellCount = frame.getWidth() * frame.getHeight(); while(queue.length > 0) { loopCount ++; var currentItem = queue.pop(); - frame[currentItem.col][currentItem.row] = replacementColor; + frame.setPixel(currentItem.col, currentItem.row, replacementColor); for (var i = 0; i < 4; i++) { var nextCol = currentItem.col + dx[i] var nextRow = currentItem.row + dy[i] try { - if (isInFrameBound(frame, nextCol, nextRow) && frame[nextCol][nextRow] == targetColor) { + if (frame.isInFrame(nextCol, nextRow) && frame.getPixel(nextCol, nextRow) == targetColor) { queue.push({"col": nextCol, "row": nextRow }); } } catch(e) { diff --git a/js/drawingtools/Rectangle.js b/js/drawingtools/Rectangle.js index d3c9622a..85dce1ac 100644 --- a/js/drawingtools/Rectangle.js +++ b/js/drawingtools/Rectangle.js @@ -12,11 +12,6 @@ // Rectangle's first point coordinates (set in applyToolAt) this.startCol = null; this.startRow = null; - // Rectangle's second point coordinates (changing dynamically in moveToolAt) - this.endCol = null; - this.endRow = null; - - this.canvasOverlay = null; }; pskl.utils.inherit(ns.Rectangle, ns.BaseTool); @@ -24,26 +19,22 @@ /** * @override */ - ns.Rectangle.prototype.applyToolAt = function(col, row, frame, color, canvas, dpi) { + ns.Rectangle.prototype.applyToolAt = function(col, row, color, drawer) { this.startCol = col; this.startRow = row; - // The fake canvas where we will draw the preview of the rectangle: - this.canvasOverlay = this.createCanvasOverlay(canvas); // Drawing the first point of the rectangle in the fake overlay canvas: - this.drawPixelInCanvas(col, row, this.canvasOverlay, color, dpi); + drawer.overlay.setPixel(col, row, color); + drawer.renderOverlay(); }; - ns.Rectangle.prototype.moveToolAt = function(col, row, frame, color, canvas, dpi) { - this.endCol = col; - this.endRow = row; - // When the user moussemove (before releasing), we dynamically compute the - // pixel to draw the line and draw this line in the overlay canvas: - var strokePoints = this.getRectanglePixels_(this.startCol, this.endCol, this.startRow, this.endRow); - + ns.Rectangle.prototype.moveToolAt = function(col, row, color, drawer) { // Clean overlay canvas: - this.canvasOverlay.getContext("2d").clearRect( - 0, 0, this.canvasOverlay.width, this.canvasOverlay.height); + drawer.clearOverlay(); + + // When the user moussemove (before releasing), we dynamically compute the + // pixel to draw the line and draw this line in the overlay : + var strokePoints = this.getRectanglePixels_(this.startCol, col, this.startRow, row); // Drawing current stroke: for(var i = 0; i< strokePoints.length; i++) { @@ -51,39 +42,29 @@ if(color == Constants.TRANSPARENT_COLOR) { color = Constants.SELECTION_TRANSPARENT_COLOR; } - this.drawPixelInCanvas(strokePoints[i].col, strokePoints[i].row, this.canvasOverlay, color, dpi); + drawer.overlay.setPixel(strokePoints[i].col, strokePoints[i].row, color); } + drawer.renderOverlay(); }; /** * @override */ - ns.Rectangle.prototype.releaseToolAt = function(col, row, frame, color, canvas, dpi) { - this.endCol = col; - this.endRow = row; - + ns.Rectangle.prototype.releaseToolAt = function(col, row, color, drawer) { // If the stroke tool is released outside of the canvas, we cancel the stroke: - // TODO: Mutualize this check in common method - if(col < 0 || row < 0 || col > frame.length || row > frame[0].length) { - this.removeCanvasOverlays(); - return; - } - - // 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) - var strokePoints = this.getRectanglePixels_(this.startCol, this.endCol, this.startRow, this.endRow); - - for(var i = 0; i< strokePoints.length; i++) { - // Change model: - frame[strokePoints[i].col][strokePoints[i].row] = color; - + if(drawer.frame.isInFrame(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); + } + // 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 - this.drawPixelInCanvas(strokePoints[i].col, strokePoints[i].row, canvas, color, dpi); + drawer.renderFrame(); } - - // For now, we are done with the stroke tool and don't need an overlay anymore: - this.removeCanvasOverlays(); + drawer.clearOverlay(); }; /** diff --git a/js/drawingtools/SimplePen.js b/js/drawingtools/SimplePen.js index 309df10a..ec5d5485 100644 --- a/js/drawingtools/SimplePen.js +++ b/js/drawingtools/SimplePen.js @@ -7,7 +7,7 @@ var ns = $.namespace("pskl.drawingtools"); ns.SimplePen = function() { - this.toolId = "tool-pen" + this.toolId = "tool-pen"; }; this.previousCol = null; @@ -18,20 +18,17 @@ /** * @override */ - ns.SimplePen.prototype.applyToolAt = function(col, row, frame, color, canvas, dpi) { - + ns.SimplePen.prototype.applyToolAt = function(col, row, color, drawer) { this.previousCol = col; this.previousRow = row; - if (color != frame[col][row]) { - frame[col][row] = color; - } + drawer.frame.setPixel(col, row, color); // Draw on canvas: // TODO: Remove that when we have the centralized redraw loop - this.drawPixelInCanvas(col, row, canvas, color, dpi); + drawer.renderFramePixel(col, row); }; - ns.SimplePen.prototype.moveToolAt = function(col, row, frame, color, canvas, dpi) { + ns.SimplePen.prototype.moveToolAt = function(col, row, color, drawer) { 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 @@ -39,11 +36,11 @@ // 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 frame.length || row > frame[0].length) { - this.removeCanvasOverlays(); - return; - } - - // 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) - var strokePoints = this.getLinePixels_(this.startCol, this.endCol, this.startRow, this.endRow); - - for(var i = 0; i< strokePoints.length; i++) { - // Change model: - frame[strokePoints[i].col][strokePoints[i].row] = color; - + if(drawer.frame.isInFrame(col, row)) { + // 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) + var strokePoints = this.getLinePixels_(this.startCol, col, this.startRow, row); + for(var i = 0; i< strokePoints.length; i++) { + // Change model: + drawer.updateFrame(strokePoints[i].col, strokePoints[i].row, color); + } // Draw in canvas: // TODO: Remove that when we have the centralized redraw loop - this.drawPixelInCanvas(strokePoints[i].col, strokePoints[i].row, canvas, color, dpi); - } - + drawer.renderFrame(); + } // For now, we are done with the stroke tool and don't need an overlay anymore: - this.removeCanvasOverlays(); + drawer.clearOverlay(); }; - })(); diff --git a/js/piskel.js b/js/piskel.js index fe172179..2554f008 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -89,6 +89,13 @@ $.namespace("pskl"); this.initPreviewSlideshow(); this.initAnimationPreview(); this.startAnimation(); + + var frame = frameSheet.getFrameByIndex(this.getActiveFrameIndex()); + this.drawer = new pskl.rendering.DrawingController( + frame, + $('#drawing-canvas-container')[0], + drawingCanvasDpi + ); pskl.ToolSelector.init(); pskl.Palette.init(frameSheet); @@ -153,23 +160,13 @@ $.namespace("pskl"); initDrawingArea : function() { drawingAreaContainer = $('#drawing-canvas-container')[0]; - - drawingAreaCanvas = document.createElement("canvas"); - drawingAreaCanvas.className = 'canvas'; - drawingAreaCanvas.setAttribute('width', '' + framePixelWidth * drawingCanvasDpi); - drawingAreaCanvas.setAttribute('height', '' + framePixelHeight * drawingCanvasDpi); - - drawingAreaContainer.setAttribute('style', - 'width:' + framePixelWidth * drawingCanvasDpi + 'px; height:' + framePixelHeight * drawingCanvasDpi + 'px;'); - - drawingAreaCanvas.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(event)'); - drawingAreaContainer.appendChild(drawingAreaCanvas); - var body = document.getElementsByTagName('body')[0]; body.setAttribute('onmouseup', 'piskel.onDocumentBodyMouseup(event)'); + drawingAreaContainer.style.width = framePixelWidth * drawingCanvasDpi + "px"; + drawingAreaContainer.style.height = framePixelHeight * drawingCanvasDpi + "px"; + drawingAreaContainer.setAttribute('oncontextmenu', 'piskel.onCanvasContextMenu(event)'); drawingAreaContainer.setAttribute('onmousedown', 'piskel.onCanvasMousedown(event)'); drawingAreaContainer.setAttribute('onmousemove', 'piskel.onCanvasMousemove(event)'); - this.drawFrameToCanvas(currentFrame, drawingAreaCanvas, drawingCanvasDpi); }, initPreviewSlideshow: function() { @@ -318,12 +315,11 @@ $.namespace("pskl"); } var spriteCoordinate = this.getSpriteCoordinate(event); currentToolBehavior.applyToolAt( - spriteCoordinate.col, - spriteCoordinate.row, - currentFrame, - penColor, - drawingAreaCanvas, - drawingCanvasDpi); + spriteCoordinate.col, + spriteCoordinate.row, + penColor, + this.drawer + ); $.publish(Events.LOCALSTORAGE_REQUEST); }, @@ -337,12 +333,11 @@ $.namespace("pskl"); if (isClicked) { var spriteCoordinate = this.getSpriteCoordinate(event); currentToolBehavior.moveToolAt( - spriteCoordinate.col, - spriteCoordinate.row, - currentFrame, - penColor, - drawingAreaCanvas, - drawingCanvasDpi); + spriteCoordinate.col, + spriteCoordinate.row, + penColor, + this.drawer + ); // 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, @@ -372,10 +367,9 @@ $.namespace("pskl"); currentToolBehavior.releaseToolAt( spriteCoordinate.col, spriteCoordinate.row, - currentFrame, penColor, - drawingAreaCanvas, - drawingCanvasDpi); + this.drawer + ); }, // TODO(vincz/julz): Refactor to make this disappear in a big event-driven redraw loop @@ -408,7 +402,7 @@ $.namespace("pskl"); }, getRelativeCoordinates : function (x, y) { - var canvasRect = drawingAreaCanvas.getBoundingClientRect(); + var canvasRect = $(".canvas-main")[0].getBoundingClientRect(); return { x : x - canvasRect.left, y : y - canvasRect.top diff --git a/js/rendering/DrawingController.js b/js/rendering/DrawingController.js new file mode 100644 index 00000000..f12fafe1 --- /dev/null +++ b/js/rendering/DrawingController.js @@ -0,0 +1,64 @@ +(function () { + var ns = $.namespace("pskl.rendering"); + ns.DrawingController = function (frame, container, dpi) { + this.dpi = dpi; + + // Public + this.frame = frame; + this.overlay = ns.Frame.createEmptyFromFrame(frame); + + // Private + this.container = container; + this.mainCanvas = this.createMainCanvas(); + this.overlayCanvas = this.createOverlayCanvas(); + this.renderer = new ns.FrameRenderer(); + }; + + ns.DrawingController.prototype.renderFrame = function () { + this.renderer.render(this.frame, this.mainCanvas, this.dpi); + }; + + ns.DrawingController.prototype.renderFramePixel = function (col, row) { + this.renderer.drawPixel(col, row, this.frame, this.mainCanvas, this.dpi); + }; + + ns.DrawingController.prototype.renderOverlay = function () { + this.renderer.render(this.overlay, this.overlayCanvas, this.dpi); + }; + + ns.DrawingController.prototype.clearOverlay = function () { + this.overlay = ns.Frame.createEmptyFromFrame(this.frame); + this.overlayCanvas.getContext("2d").clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height); + }; + + ns.DrawingController.prototype.createMainCanvas = function () { + var mainCanvas = this.createCanvas(); + mainCanvas.className = "canvas-main"; + this.container.appendChild(mainCanvas); + return mainCanvas; + }; + + // For some tools, we need a fake canvas that overlay the drawing canvas. These tools are + // generally 'drap and release' based tools (stroke, selection, etc) and the fake canvas + // will help to visualize the tool interaction (without modifying the canvas). + ns.DrawingController.prototype.createOverlayCanvas = function () { + var overlayCanvas = this.createCanvas(); + overlayCanvas.className = "canvas-overlay"; + this.container.appendChild(overlayCanvas); + return overlayCanvas; + }; + + // For some tools, we need a fake canvas that overlay the drawing canvas. These tools are + // generally 'drap and release' based tools (stroke, selection, etc) and the fake canvas + // will help to visualize the tool interaction (without modifying the canvas). + ns.DrawingController.prototype.createCanvas = function () { + var width = this.frame.getWidth(), + height = this.frame.getHeight(); + + var canvas = document.createElement("canvas"); + canvas.setAttribute("width", width * this.dpi); + canvas.setAttribute("height", height * this.dpi); + + return canvas; + }; +})(); \ No newline at end of file diff --git a/js/rendering/Frame.js b/js/rendering/Frame.js new file mode 100644 index 00000000..93d90c27 --- /dev/null +++ b/js/rendering/Frame.js @@ -0,0 +1,53 @@ +(function () { + var ns = $.namespace("pskl.rendering"); + ns.Frame = function (pixels) { + this.pixels = pixels; + }; + + ns.Frame.createEmpty = function (width, height) { + var pixels = []; //new Array(width); + for (var columnIndex=0; columnIndex < width; columnIndex++) { + var columnArray = []; + for(var heightIndex = 0; heightIndex < height; heightIndex++) { + columnArray.push(Constants.TRANSPARENT_COLOR); + } + pixels[columnIndex] = columnArray; + } + return new ns.Frame(pixels); + }; + + ns.Frame.createEmptyFromFrame = function (frame) { + return ns.Frame.createEmpty(frame.getWidth(), frame.getHeight()); + }; + + ns.Frame.prototype.clone = function () { + var clone = ns.Frame.createEmptyFromFrame(this); + for (var col = 0 ; col < clone.getWidth() ; col++) { + for (var row = 0 ; row < clone.getHeight() ; row++) { + clone.setPixel(col, row, this.getPixel(col, row)); + } + } + return clone; + }; + + ns.Frame.prototype.setPixel = function (col, row, color) { + this.pixels[col][row] = color; + }; + + ns.Frame.prototype.getPixel = function (col, row) { + return this.pixels[col][row]; + }; + + ns.Frame.prototype.getWidth = function () { + return this.pixels.length; + }; + + ns.Frame.prototype.getHeight = function () { + return this.pixels[0].length; + }; + + ns.Frame.prototype.isInFrame = function (col, row) { + return col >= 0 && row >= 0 && col <= this.pixels.length && row <= this.pixels[0].length; + }; + +})(); \ No newline at end of file diff --git a/js/rendering/FrameRenderer.js b/js/rendering/FrameRenderer.js index f5c07c54..8b630579 100644 --- a/js/rendering/FrameRenderer.js +++ b/js/rendering/FrameRenderer.js @@ -1,31 +1,28 @@ -(function () { - var ns = $.namespace("pskl.rendering"); - ns.FrameRenderer = function () {}; - - ns.FrameRenderer.prototype.render = function (frame, canvas, dpi) { - var color; - for(var col = 0, num_col = frame.length; col < num_col; col++) { - for(var row = 0, num_row = frame[col].length; row < num_row; row++) { - color = frame[col][row]; - this.drawPixelInCanvas(col, row, canvas, color, dpi); - } - } - }; - - ns.FrameRenderer.prototype.drawPixelInCanvas = function () { - var context = canvas.getContext('2d'); - if(color == Constants.TRANSPARENT_COLOR) { - context.clearRect(col * dpi, row * dpi, dpi, 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.fillStyle = color; - context.fillRect(col * dpi, row * dpi, dpi, dpi); - } - - } - +(function () { + var ns = $.namespace("pskl.rendering"); + ns.FrameRenderer = function () {}; + + ns.FrameRenderer.prototype.render = function (frame, canvas, dpi) { + 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, canvas, dpi); + } + } + }; + + ns.FrameRenderer.prototype.drawPixel = function (col, row, frame, canvas, dpi) { + var context = canvas.getContext('2d'); + var color = frame.getPixel(col, row); + if(color == Constants.TRANSPARENT_COLOR) { + context.clearRect(col * dpi, row * dpi, dpi, 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.fillStyle = color; + context.fillRect(col * dpi, row * dpi, dpi, dpi); + } + }; })(); \ No newline at end of file