diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5509140 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.DS_Store diff --git a/css/style.css b/css/style.css index ce04f1d..4b6d9d0 100644 --- a/css/style.css +++ b/css/style.css @@ -127,27 +127,39 @@ ul, li { z-index: 1; } +.canvas-overlay { + position: absolute; + top: 0; + left: 0; + z-index: 10; +} + .drawing-canvas-container { float: left; } .tool-paint-bucket .drawing-canvas-container:hover { - cursor: url(../img/tools/paint-bucket.png) 18 17, pointer; + cursor: url(../img/tools/cursors/paint-bucket.png) 18 17, pointer; } .tool-pen .drawing-canvas-container:hover { - cursor: url(../img/tools/pen.png) 7 21, pointer; + cursor: url(../img/tools/cursors/pen.png) 7 21, pointer; } .tool-eraser .drawing-canvas-container:hover { - cursor: url(../img/tools/eraser.png) 5 21, pointer; + cursor: url(../img/tools/cursors/eraser.png) 5 21, pointer; } +.tool-stroke .drawing-canvas-container:hover { + cursor: url(../img/tools/cursors/pen.png) 5 21, pointer; +} + + /** * Tool section: */ -#palette li { +.palette .palette-color { display : inline-block; height : 20px; width : 20px; @@ -185,15 +197,19 @@ ul, li { } .tool-icon.tool-pen { - background: #fff url(../img/tools/pen.png) 3px 3px no-repeat; + background: #fff url(../img/tools/icons/pen.png) 3px 3px no-repeat; } .tool-icon.tool-paint-bucket { - background: #fff url(../img/tools/paint-bucket.png) 3px 3px no-repeat; + background: #fff url(../img/tools/icons/paint-bucket.png) 3px 3px no-repeat; } .tool-icon.tool-eraser { - background: #fff url(../img/tools/eraser.png) 3px 3px no-repeat; + background: #fff url(../img/tools/icons/eraser.png) 3px 3px no-repeat; +} + +.tool-icon.tool-stroke { + background: #fff url(../img/tools/icons/stroke.png) 3px 3px no-repeat; } #preview-fps { diff --git a/img/tools/eraser.png b/img/tools/cursors/eraser.png similarity index 100% rename from img/tools/eraser.png rename to img/tools/cursors/eraser.png diff --git a/img/tools/paint-bucket.png b/img/tools/cursors/paint-bucket.png similarity index 100% rename from img/tools/paint-bucket.png rename to img/tools/cursors/paint-bucket.png diff --git a/img/tools/pen.png b/img/tools/cursors/pen.png similarity index 100% rename from img/tools/pen.png rename to img/tools/cursors/pen.png diff --git a/img/tools/icons/eraser.png b/img/tools/icons/eraser.png new file mode 100644 index 0000000..b9b7522 Binary files /dev/null and b/img/tools/icons/eraser.png differ diff --git a/img/tools/icons/paint-bucket.png b/img/tools/icons/paint-bucket.png new file mode 100644 index 0000000..4d3e690 Binary files /dev/null and b/img/tools/icons/paint-bucket.png differ diff --git a/img/tools/icons/pen.png b/img/tools/icons/pen.png new file mode 100644 index 0000000..7d01627 Binary files /dev/null and b/img/tools/icons/pen.png differ diff --git a/img/tools/icons/stroke.png b/img/tools/icons/stroke.png new file mode 100644 index 0000000..327b447 Binary files /dev/null and b/img/tools/icons/stroke.png differ diff --git a/index.html b/index.html index 80c6185..f7fd8f8 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ Piskel - + @@ -15,24 +15,36 @@
+ + - + + +
+ +
+ +
-
-
-
+
+
+
+
+ - + +
+ +
@@ -45,21 +57,28 @@
+ + + + - + + + + diff --git a/js/ToolSelector.js b/js/ToolSelector.js index 45bae85..0532980 100644 --- a/js/ToolSelector.js +++ b/js/ToolSelector.js @@ -11,7 +11,8 @@ pskl.ToolSelector = (function() { var toolInstances = { "simplePen" : new pskl.drawingtools.SimplePen(), "eraser" : new pskl.drawingtools.Eraser(), - "paintBucket" : new pskl.drawingtools.PaintBucket() + "paintBucket" : new pskl.drawingtools.PaintBucket(), + "stroke" : new pskl.drawingtools.Stroke() }; var currentSelectedTool = toolInstances.simplePen; var previousSelectedTool = toolInstances.simplePen; diff --git a/js/drawingtools/BaseTool.js b/js/drawingtools/BaseTool.js index cd10251..81a7188 100644 --- a/js/drawingtools/BaseTool.js +++ b/js/drawingtools/BaseTool.js @@ -8,12 +8,13 @@ ns.BaseTool = function() {}; - ns.BaseTool.prototype.applyToolOnFrameAt = function(col, row, frame, color) {}; + ns.BaseTool.prototype.applyToolAt = function(col, row, frame, color, canvas, dpi) {}; - ns.BaseTool.prototype.applyToolOnCanvasAt = function(col, row, canvas, color, dpi) {}; - - ns.BaseTool.prototype.releaseToolAt = function() {}; + ns.BaseTool.prototype.moveToolAt = function(col, row, frame, color, canvas, dpi) {}; + ns.BaseTool.prototype.releaseToolAt = function(col, row, frame, color, canvas, dpi) {}; + + // TODO: Remove that when we have the centralized redraw loop ns.BaseTool.prototype.drawPixelInCanvas = function (col, row, canvas, color, dpi) { var context = canvas.getContext('2d'); if(color == undefined || color == Constants.TRANSPARENT_COLOR) { @@ -27,6 +28,7 @@ } }; + // TODO: Remove that when we have the centralized redraw loop ns.BaseTool.prototype.drawFrameInCanvas = function (frame, canvas, dpi) { var color; for(var col = 0, num_col = frame.length; col < num_col; col++) { @@ -36,4 +38,21 @@ } } }; + + // 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.BaseTool.prototype.createCanvasOverlay = function (canvas) { + var overlayCanvas = document.createElement("canvas"); + overlayCanvas.className = "canvas-overlay"; + overlayCanvas.setAttribute("width", canvas.width); + overlayCanvas.setAttribute("height", canvas.height); + + canvas.parentNode.appendChild(overlayCanvas); + return overlayCanvas; + }; + + ns.BaseTool.prototype.removeCanvasOverlays = function () { + $(".canvas-overlay").remove(); + }; })(); diff --git a/js/drawingtools/Eraser.js b/js/drawingtools/Eraser.js index c6592c3..cd2cdd7 100644 --- a/js/drawingtools/Eraser.js +++ b/js/drawingtools/Eraser.js @@ -16,23 +16,21 @@ /** * @override */ - ns.Eraser.prototype.applyToolOnFrameAt = function(col, row, frame, color) { + ns.Eraser.prototype.applyToolAt = function(col, row, frame, color, canvas, dpi) { + + // Change model: frame[col][row] = Constants.TRANSPARENT_COLOR; - }; - - /** - * @override - */ - ns.Eraser.prototype.applyToolOnCanvasAt = function(col, row, canvas, frame, color, dpi) { + // Draw on canvas: + // TODO: Remove that when we have the centralized redraw loop this.drawPixelInCanvas(col, row, canvas, Constants.TRANSPARENT_COLOR, dpi); }; /** * @override */ - ns.Eraser.prototype.releaseToolAt = function() { - // Do nothing - console.log('Eraser release'); + ns.Eraser.prototype.moveToolAt = function(col, row, frame, color, canvas, dpi) { + this.applyToolAt(col, row, frame, color, canvas, dpi); }; + })(); \ No newline at end of file diff --git a/js/drawingtools/PaintBucket.js b/js/drawingtools/PaintBucket.js index 0a55d41..ed01679 100644 --- a/js/drawingtools/PaintBucket.js +++ b/js/drawingtools/PaintBucket.js @@ -15,45 +15,38 @@ /** * @override */ - ns.PaintBucket.prototype.applyToolOnFrameAt = function(col, row, frame, color) {}; + ns.PaintBucket.prototype.applyToolAt = function(col, row, frame, color, canvas, dpi) { - /** - * @override - */ - ns.PaintBucket.prototype.applyToolOnCanvasAt = function(col, row, canvas, frame, replacementColor, dpi) { - + // Change model: var targetColor = pskl.utils.normalizeColor(frame[col][row]); - //this.recursiveFloodFill(frame, col, row, targetColor, replacementColor); - this.queueLinearFloodFill(frame, col, row, targetColor, replacementColor); + //this.recursiveFloodFill_(frame, col, row, targetColor, color); + this.queueLinearFloodFill_(frame, col, row, targetColor, color); + + // Draw in canvas: + // TODO: Remove that when we have the centralized redraw loop this.drawFrameInCanvas(frame, canvas, dpi); }; - /** - * @override - */ - ns.PaintBucket.prototype.releaseToolAt = function() { - // Do nothing - console.log('PaintBucket release'); - }; - /** * Flood-fill (node, target-color, replacement-color): - 1. Set Q to the empty queue. - 2. If the color of node is not equal to target-color, return. - 3. Add node to Q. - 4. For each element n of Q: - 5. If the color of n is equal to target-color: - 6. Set w and e equal to n. - 7. Move w to the west until the color of the node to the west of w no longer matches target-color. - 8. Move e to the east until the color of the node to the east of e no longer matches target-color. - 9. Set the color of nodes between w and e to replacement-color. - 10. For each node n between w and e: - 11. If the color of the node to the north of n is target-color, add that node to Q. - 12. If the color of the node to the south of n is target-color, add that node to Q. - 13. Continue looping until Q is exhausted. - 14. Return. + * 1. Set Q to the empty queue. + * 2. If the color of node is not equal to target-color, return. + * 3. Add node to Q. + * 4. For each element n of Q: + * 5. If the color of n is equal to target-color: + * 6. Set w and e equal to n. + * 7. Move w to the west until the color of the node to the west of w no longer matches target-color. + * 8. Move e to the east until the color of the node to the east of e no longer matches target-color. + * 9. Set the color of nodes between w and e to replacement-color. + * 10. For each node n between w and e: + * 11. If the color of the node to the north of n is target-color, add that node to Q. + * 12. If the color of the node to the south of n is target-color, add that node to Q. + * 13. Continue looping until Q is exhausted. + * 14. Return. + * + * @private */ - ns.PaintBucket.prototype.queueLinearFloodFill = function(frame, col, row, targetColor, replacementColor) { + ns.PaintBucket.prototype.queueLinearFloodFill_ = function(frame, col, row, targetColor, replacementColor) { var queue = []; var dy = [-1, 0, 1, 0]; @@ -104,7 +97,7 @@ break; } } - } + }; /** * Basic Flood-fill implementation (Stack explosion !): @@ -119,7 +112,7 @@ * * @private */ - ns.PaintBucket.prototype.recursiveFloodFill = function(frame, col, row, targetColor, replacementColor) { + ns.PaintBucket.prototype.recursiveFloodFill_ = function(frame, col, row, targetColor, replacementColor) { // Step 1: if( col < 0 || diff --git a/js/drawingtools/SimplePen.js b/js/drawingtools/SimplePen.js index c21c1cc..ed8dac3 100644 --- a/js/drawingtools/SimplePen.js +++ b/js/drawingtools/SimplePen.js @@ -15,27 +15,20 @@ /** * @override */ - ns.SimplePen.prototype.applyToolOnFrameAt = function(col, row, frame, color) { + ns.SimplePen.prototype.applyToolAt = function(col, row, frame, color, canvas, dpi) { + + // Change model: var color = pskl.utils.normalizeColor(color); if (color != frame[col][row]) { frame[col][row] = color; } + + // Draw on canvas: + // TODO: Remove that when we have the centralized redraw loop + this.drawPixelInCanvas(col, row, canvas, color, dpi); }; - /** - * @override - */ - ns.SimplePen.prototype.applyToolOnCanvasAt = function(col, row, canvas, frame, color, dpi) { - - this.drawPixelInCanvas(col, row, canvas, color, dpi); + ns.SimplePen.prototype.moveToolAt = function(col, row, frame, color, canvas, dpi) { + this.applyToolAt(col, row, frame, color, canvas, dpi); }; - - /** - * @override - */ - ns.SimplePen.prototype.releaseToolAt = function() { - // Do nothing - console.log('SimplePen release'); - }; - })(); diff --git a/js/drawingtools/Stroke.js b/js/drawingtools/Stroke.js new file mode 100644 index 0000000..68229d5 --- /dev/null +++ b/js/drawingtools/Stroke.js @@ -0,0 +1,128 @@ +/* + * @provide pskl.drawingtools.SimplePen + * + * @require pskl.utils + */ +(function() { + var ns = $.namespace("pskl.drawingtools"); + + ns.Stroke = function() { + this.toolId = "tool-stroke" + + // Stroke's first point coordinates (set in applyToolAt) + this.startCol = null; + this.startRow = null; + // Stroke's second point coordinates (changing dynamically in moveToolAt) + this.endCol = null; + this.endRow = null; + + this.canvasOverlay = null; + }; + + pskl.utils.inherit(ns.Stroke, ns.BaseTool); + + /** + * @override + */ + ns.Stroke.prototype.applyToolAt = function(col, row, frame, color, canvas, dpi) { + this.startCol = col; + this.startRow = row; + + // When drawing a stroke we don't change the model instantly, since the + // user can move his cursor to change the stroke direction and length + // dynamically. Instead we draw the (preview) stroke in a fake canvas that + // overlay the drawing canvas. + // We wait for the releaseToolAt callback to impact both the + // frame model and canvas rendering. + + // The fake canvas where we will draw the preview of the stroke: + this.canvasOverlay = this.createCanvasOverlay(canvas); + // Drawing the first point of the stroke in the fake overlay canvas: + this.drawPixelInCanvas(col, row, this.canvasOverlay, color, dpi); + }; + + ns.Stroke.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.getLinePixels_(this.startCol, this.endCol, this.startRow, this.endRow); + + // Clean overlay canvas: + this.canvasOverlay.getContext("2d").clearRect( + 0, 0, this.canvasOverlay.width, this.canvasOverlay.height); + + // Drawing current stroke: + for(var i = 0; i< strokePoints.length; i++) { + this.drawPixelInCanvas(strokePoints[i].col, strokePoints[i].row, this.canvasOverlay, color, dpi); + } + }; + + /** + * @override + */ + ns.Stroke.prototype.releaseToolAt = function(col, row, frame, color, canvas, dpi) { + this.endCol = col; + this.endRow = row; + + // If the stroke tool is released outside of the canvas, we cancel the stroke: + 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.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; + + // Draw in canvas: + // TODO: Remove that when we have the centralized redraw loop + this.drawPixelInCanvas(strokePoints[i].col, strokePoints[i].row, canvas, color, dpi); + } + + // For now, we are done with the stroke tool and don't need an overlay anymore: + this.removeCanvasOverlays(); + }; + + /** + * Bresenham line algorihtm: Get an array of pixels from + * start and end coordinates. + * + * http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + * http://stackoverflow.com/questions/4672279/bresenham-algorithm-in-javascript + * + * @private + */ + ns.Stroke.prototype.getLinePixels_ = function(x0, x1, y0, y1) { + + var pixels = []; + var dx = Math.abs(x1-x0); + var dy = Math.abs(y1-y0); + var sx = (x0 < x1) ? 1 : -1; + var sy = (y0 < y1) ? 1 : -1; + var err = dx-dy; + + while(true){ + + // Do what you need to for this + pixels.push({"col": x0, "row": y0}); + + if ((x0==x1) && (y0==y1)) break; + var e2 = 2*err; + if (e2>-dy){ + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + return pixels; + }; + +})(); diff --git a/js/piskel.js b/js/piskel.js index fd2fd40..6051bb5 100644 --- a/js/piskel.js +++ b/js/piskel.js @@ -204,7 +204,8 @@ $.namespace("pskl"); addColorToPalette : function (color) { if (color && color != Constants.TRANSPARENT_COLOR && paletteColors.indexOf(color) == -1) { - var colorEl = document.createElement("li"); + var colorEl = document.createElement("li"); + colorEl.className = "palette-color"; colorEl.setAttribute("data-color", color); colorEl.setAttribute("title", color); colorEl.style.background = color; @@ -379,11 +380,14 @@ $.namespace("pskl"); $.publish(Events.CANVAS_RIGHT_CLICKED); } var spriteCoordinate = this.getSpriteCoordinate(event); - currentToolBehavior.applyToolOnFrameAt( - spriteCoordinate.col, spriteCoordinate.row, currentFrame, penColor); - currentToolBehavior.applyToolOnCanvasAt( - spriteCoordinate.col, spriteCoordinate.row, drawingAreaCanvas, currentFrame, penColor, drawingCanvasDpi); - + currentToolBehavior.applyToolAt( + spriteCoordinate.col, + spriteCoordinate.row, + currentFrame, + penColor, + drawingAreaCanvas, + drawingCanvasDpi); + piskel.persistToLocalStorageRequest(); }, @@ -391,12 +395,18 @@ $.namespace("pskl"); //this.updateCursorInfo(event); if (isClicked) { - var spriteCoordinate = this.getSpriteCoordinate(event) - currentToolBehavior.applyToolOnFrameAt( - spriteCoordinate.col, spriteCoordinate.row, currentFrame, penColor); - currentToolBehavior.applyToolOnCanvasAt( - spriteCoordinate.col, spriteCoordinate.row, drawingAreaCanvas, currentFrame, penColor, drawingCanvasDpi); - + var spriteCoordinate = this.getSpriteCoordinate(event); + currentToolBehavior.moveToolAt( + spriteCoordinate.col, + spriteCoordinate.row, + currentFrame, + penColor, + drawingAreaCanvas, + drawingCanvasDpi); + + // 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. piskel.persistToLocalStorageRequest(); } }, @@ -407,15 +417,23 @@ $.namespace("pskl"); // the user was probably drawing on the canvas. // Note: The mousemove movement (and the mouseup) may end up outside // of the drawing canvas. + // TODO: Remove that when we have the centralized redraw loop this.createPreviews(); } + if(isRightClicked) { $.publish(Events.CANVAS_RIGHT_CLICK_RELEASED); } isClicked = false; isRightClicked = false; - var spriteCoordinate = this.getSpriteCoordinate(event) - currentToolBehavior.releaseToolAt(spriteCoordinate.col, spriteCoordinate.row, penColor); + var spriteCoordinate = this.getSpriteCoordinate(event); + currentToolBehavior.releaseToolAt( + spriteCoordinate.col, + spriteCoordinate.row, + currentFrame, + penColor, + drawingAreaCanvas, + drawingCanvasDpi); }, // TODO(vincz/julz): Refactor to make this disappear in a big event-driven redraw loop