diff --git a/src/css/tools.css b/src/css/tools.css index e4ad5bc8..dee76ffa 100644 --- a/src/css/tools.css +++ b/src/css/tools.css @@ -163,7 +163,8 @@ cursor: url(../img/icons/hand.png) 7 7, pointer; } -.tool-rectangle-select .drawing-canvas-container:hover { +.tool-rectangle-select .drawing-canvas-container:hover, +.tool-lasso-select .drawing-canvas-container:hover { cursor: crosshair; } diff --git a/src/js/controller/ToolController.js b/src/js/controller/ToolController.js index 04a1faee..75d11f6a 100644 --- a/src/js/controller/ToolController.js +++ b/src/js/controller/ToolController.js @@ -16,9 +16,9 @@ toDescriptor('rectangle', 'R', new pskl.tools.drawing.Rectangle()), toDescriptor('circle', 'C', new pskl.tools.drawing.Circle()), toDescriptor('move', 'M', new pskl.tools.drawing.Move()), - toDescriptor('rectangleSelect', 'S', new pskl.tools.drawing.RectangleSelect()), - toDescriptor('lassoSelect', 'S', new pskl.tools.drawing.LassoSelect()), - toDescriptor('shapeSelect', 'Z', new pskl.tools.drawing.ShapeSelect()), + toDescriptor('rectangleSelect', 'S', new pskl.tools.drawing.selection.RectangleSelect()), + toDescriptor('lassoSelect', 'H', new pskl.tools.drawing.selection.LassoSelect()), + toDescriptor('shapeSelect', 'Z', new pskl.tools.drawing.selection.ShapeSelect()), toDescriptor('lighten', 'U', new pskl.tools.drawing.Lighten()), toDescriptor('dithering', 'T', new pskl.tools.drawing.DitheringTool()), toDescriptor('colorPicker', 'O', new pskl.tools.drawing.ColorPicker()) @@ -97,22 +97,12 @@ }; ns.ToolController.prototype.onKeyboardShortcut_ = function(charkey) { - - var tools = []; for (var i = 0 ; i < this.tools.length ; i++) { var tool = this.tools[i]; if (tool.shortcut.toLowerCase() === charkey.toLowerCase()) { - tools.push(tool); + this.selectTool_(tool); } } - - if (tools.length === 0) { - return; - } - - var currentToolIndex = tools.indexOf(this.currentSelectedTool); - - this.selectTool_(tools[(currentToolIndex+1) % tools.length]); }; ns.ToolController.prototype.getToolById_ = function (toolId) { diff --git a/src/js/selection/LassoSelection.js b/src/js/selection/LassoSelection.js new file mode 100644 index 00000000..1a1df0d2 --- /dev/null +++ b/src/js/selection/LassoSelection.js @@ -0,0 +1,73 @@ +(function () { + var ns = $.namespace('pskl.selection'); + + var OUTSIDE = -1; + var INSIDE = 1; + var VISITED = 2; + + ns.LassoSelection = function (pixels, frame) { + // transform the selected pixels array to a Map to get a faster lookup + this.pixelsMap = {}; + pixels.forEach(function (pixel) { + this.setPixelInMap_(pixel, INSIDE); + }.bind(this)); + + this.pixels = this.getLassoPixels_(frame); + }; + + pskl.utils.inherit(ns.LassoSelection, ns.BaseSelection); + + ns.LassoSelection.prototype.getLassoPixels_ = function (frame) { + var lassoPixels = []; + + frame.forEachPixel(function (color, pixelCol, pixelRow) { + var pixel = {col : pixelCol, row : pixelRow}; + if (this.isInSelection_(pixel, frame)) { + lassoPixels.push(pixel); + } + }.bind(this)); + + return lassoPixels; + }; + + ns.LassoSelection.prototype.isInSelection_ = function (pixel, frame) { + var alreadyVisited = this.getPixelInMap_(pixel); + if (!alreadyVisited) { + this.visitPixel_(pixel, frame); + } + + return this.getPixelInMap_(pixel) == INSIDE; + }; + + ns.LassoSelection.prototype.visitPixel_ = function (pixel, frame) { + var frameBorderReached = false; + var visitedPixels = pskl.PixelUtils.visitConnectedPixels(pixel, frame, function (connectedPixel) { + var alreadyVisited = this.getPixelInMap_(connectedPixel); + if (alreadyVisited) { + return false; + } + + if (!frame.containsPixel(connectedPixel.col, connectedPixel.row)) { + frameBorderReached = true; + return false; + } + + this.setPixelInMap_(connectedPixel, VISITED); + return true; + }.bind(this)); + + visitedPixels.forEach(function (visitedPixel) { + this.setPixelInMap_(visitedPixel, frameBorderReached ? OUTSIDE : INSIDE); + }.bind(this)); + }; + + ns.LassoSelection.prototype.setPixelInMap_ = function (pixel, value) { + this.pixelsMap[pixel.col] = this.pixelsMap[pixel.col] || {}; + this.pixelsMap[pixel.col][pixel.row] = value; + }; + + ns.LassoSelection.prototype.getPixelInMap_ = function (pixel) { + return this.pixelsMap[pixel.col] && this.pixelsMap[pixel.col][pixel.row]; + }; + +})(); diff --git a/src/js/selection/SelectionManager.js b/src/js/selection/SelectionManager.js index a66d4e67..96d4b04c 100644 --- a/src/js/selection/SelectionManager.js +++ b/src/js/selection/SelectionManager.js @@ -41,7 +41,7 @@ * @private */ ns.SelectionManager.prototype.onToolSelected_ = function(evt, tool) { - var isSelectionTool = tool instanceof pskl.tools.drawing.BaseSelect; + var isSelectionTool = tool instanceof pskl.tools.drawing.selection.BaseSelect; if (!isSelectionTool) { this.cleanSelection_(); } diff --git a/src/js/tools/drawing/selection/AbstractDragSelect.js b/src/js/tools/drawing/selection/AbstractDragSelect.js new file mode 100644 index 00000000..9a753921 --- /dev/null +++ b/src/js/tools/drawing/selection/AbstractDragSelect.js @@ -0,0 +1,49 @@ +(function () { + var ns = $.namespace('pskl.tools.drawing.selection'); + + ns.AbstractDragSelect = function () { + ns.BaseSelect.call(this); + this.hasSelection = false; + }; + + pskl.utils.inherit(ns.AbstractDragSelect, ns.BaseSelect); + + /** + * @override + */ + ns.AbstractDragSelect.prototype.onSelectStart_ = function (col, row, color, frame, overlay) { + if (this.hasSelection) { + this.hasSelection = false; + overlay.clear(); + $.publish(Events.SELECTION_DISMISSED); + } else { + this.hasSelection = true; + this.startDragSelection_(col, row); + overlay.setPixel(col, row, this.getTransparentVariant_(Constants.SELECTION_TRANSPARENT_COLOR)); + } + }; + + ns.AbstractDragSelect.prototype.onSelect_ = function (col, row, color, frame, overlay) { + if (!this.hasSelection && (this.startCol !== col || this.startRow !== row)) { + this.hasSelection = true; + this.startDragSelection_(col, row); + } + + if (this.hasSelection) { + this.updateDragSelection_(col, row, color, frame, overlay); + } + }; + + ns.AbstractDragSelect.prototype.onSelectEnd_ = function (col, row, color, frame, overlay) { + if (this.hasSelection) { + this.endDragSelection_(col, row, color, frame, overlay); + } + }; + + /** @protected */ + ns.AbstractDragSelect.prototype.startDragSelection_ = function (col, row, color, frame, overlay) {}; + /** @protected */ + ns.AbstractDragSelect.prototype.updateDragSelection_ = function (col, row, color, frame, overlay) {}; + /** @protected */ + ns.AbstractDragSelect.prototype.endDragSelection_ = function (col, row, color, frame, overlay) {}; +})(); diff --git a/src/js/tools/drawing/selection/BaseSelect.js b/src/js/tools/drawing/selection/BaseSelect.js index 9d4a23d7..6a05541a 100644 --- a/src/js/tools/drawing/selection/BaseSelect.js +++ b/src/js/tools/drawing/selection/BaseSelect.js @@ -4,7 +4,7 @@ * @require pskl.utils */ (function() { - var ns = $.namespace('pskl.tools.drawing'); + var ns = $.namespace('pskl.tools.drawing.selection'); ns.BaseSelect = function() { this.secondaryToolId = pskl.tools.drawing.Move.TOOL_ID; @@ -23,7 +23,7 @@ ]; }; - pskl.utils.inherit(ns.BaseSelect, ns.BaseTool); + pskl.utils.inherit(ns.BaseSelect, pskl.tools.drawing.BaseTool); /** * @override diff --git a/src/js/tools/drawing/selection/LassoSelect.js b/src/js/tools/drawing/selection/LassoSelect.js index 2f10aa92..cbd01f40 100644 --- a/src/js/tools/drawing/selection/LassoSelect.js +++ b/src/js/tools/drawing/selection/LassoSelect.js @@ -4,179 +4,59 @@ * @require pskl.utils */ (function() { - var ns = $.namespace('pskl.tools.drawing'); + var ns = $.namespace('pskl.tools.drawing.selection'); ns.LassoSelect = function() { this.toolId = 'tool-lasso-select'; - this.helpText = 'Lasso selection'; - ns.BaseSelect.call(this); - this.hasSelection = false; - - this.selectionOrigin_ = null; + ns.AbstractDragSelect.call(this); }; - pskl.utils.inherit(ns.LassoSelect, ns.BaseSelect); + pskl.utils.inherit(ns.LassoSelect, ns.AbstractDragSelect); - /** - * @override - */ - ns.LassoSelect.prototype.onSelectStart_ = function (col, row, color, frame, overlay) { - this.selectionOrigin_ = { - col : col, - row : row - }; + ns.LassoSelect.prototype.startDragSelection_ = function (col, row) { + this.pixels = [{col : col, row : row}]; this.previousCol = col; this.previousRow = row; - if (this.hasSelection) { - this.hasSelection = false; - overlay.clear(); - $.publish(Events.SELECTION_DISMISSED); - } else { - this.startSelection_(col, row); - overlay.setPixel(col, row, color); - } - }; - - ns.LassoSelect.prototype.startSelection_ = function (col, row) { - this.hasSelection = true; - this.pixels = [{col : col, row : row}]; $.publish(Events.DRAG_START, [col, row]); - // Drawing the first point of the rectangle in the fake overlay canvas: }; - /** - * When creating the rectangle selection, we clear the current overlayFrame and - * redraw the current rectangle based on the orgin coordinate and - * the current mouse coordiinate in sprite. - * @override - */ - ns.LassoSelect.prototype.onSelect_ = function (col, row, color, frame, overlay) { - if (!this.hasSelection && (this.selectionOrigin_.col !== col || this.selectionOrigin_.row !== row)) { - this.startSelection_(col, row); - } + ns.LassoSelect.prototype.updateDragSelection_ = function (col, row, color, frame, overlay) { + col = pskl.utils.Math.minmax(col, 0, frame.getWidth() - 1); + row = pskl.utils.Math.minmax(row, 0, frame.getHeight() - 1); + this.addPixelToSelection_(col, row, frame); + var additionnalPixels = this.getLinePixels_(col, this.startCol, row, this.startRow); - if (this.hasSelection) { - 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); - this.pixels = this.pixels.concat(interpolatedPixels); - } else { - this.pixels.push({col : col, row : row}); - } + // during the selection, create simple ShapeSelection, containing only the pixels hovered by the user + this.selection = new pskl.selection.ShapeSelection(this.pixels.concat(additionnalPixels)); + $.publish(Events.SELECTION_CREATED, [this.selection]); - this.previousCol = col; - this.previousRow = row; - - // join lasso tail with origin - var additionnalPixels = this.getLinePixels_(col, this.selectionOrigin_.col, row, this.selectionOrigin_.row); - - overlay.clear(); - this.selection = new pskl.selection.ShapeSelection(this.pixels.concat(additionnalPixels)); - $.publish(Events.SELECTION_CREATED, [this.selection]); - this.drawSelectionOnOverlay_(overlay); - } + overlay.clear(); + this.drawSelectionOnOverlay_(overlay); }; - ns.LassoSelect.prototype.onSelectEnd_ = function (col, row, color, frame, overlay) { - if (this.hasSelection) { - 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); - this.pixels = this.pixels.concat(interpolatedPixels); - } else { - this.pixels.push({col : col, row : row}); - } + ns.LassoSelect.prototype.endDragSelection_ = function (col, row, color, frame, overlay) { + col = pskl.utils.Math.minmax(col, 0, frame.getWidth() - 1); + row = pskl.utils.Math.minmax(row, 0, frame.getHeight() - 1); + this.addPixelToSelection_(col, row, frame); + var additionnalPixels = this.getLinePixels_(col, this.startCol, row, this.startRow); + // finalize the selection, add all pixels contained inside the shape drawn by the user to the selection + this.selection = new pskl.selection.LassoSelection(this.pixels.concat(additionnalPixels), frame); + $.publish(Events.SELECTION_CREATED, [this.selection]); - var additionnalPixels = this.getLinePixels_(col, this.selectionOrigin_.col, row, this.selectionOrigin_.row); - this.pixels = this.pixels.concat(additionnalPixels); + overlay.clear(); + this.drawSelectionOnOverlay_(overlay); - var shapePixels = []; - var pixelsMap = {}; - this.pixels.forEach(function (p) { - pixelsMap[p.col] = pixelsMap[p.col] || {}; - pixelsMap[p.col][p.row] = 1; - }); - frame.forEachPixel(function (color, c, r) { - if(this.isInPoly_(c, r, pixelsMap, frame)) { - shapePixels.push({col : c, row : r}); - } - }.bind(this)); - - this.pixels = this.pixels.concat(shapePixels); - - this.selection = new pskl.selection.ShapeSelection(this.pixels); - $.publish(Events.SELECTION_CREATED, [this.selection]); - this.onSelect_(col, row, color, frame, overlay); - $.publish(Events.DRAG_END, [col, row]); - } + $.publish(Events.DRAG_END, [col, row]); }; - ns.LassoSelect.prototype.isInPoly_ = function (col, row, pixelsMap, frame) { + ns.LassoSelect.prototype.addPixelToSelection_ = function (col, row, frame) { + var interpolatedPixels = this.getLinePixels_(col, this.previousCol, row, this.previousRow); + this.pixels = this.pixels.concat(interpolatedPixels); - if (pixelsMap[col] && pixelsMap[col][row]) { - // already marked - return pixelsMap[col][row] == 1; - } - - var paintedPixels = []; - var queue = []; - var dy = [-1, 0, 1, 0]; - var dx = [0, 1, 0, -1]; - - queue.push({'col': col, 'row': row}); - var isOut = false; - var loopCount = 0; - var cellCount = frame.getWidth() * frame.getHeight(); - while (queue.length > 0) { - loopCount ++; - - var currentItem = queue.pop(); - paintedPixels.push({'col': currentItem.col, 'row': currentItem.row}); - - for (var i = 0; i < 4; i++) { - var nextCol = currentItem.col + dx[i]; - var nextRow = currentItem.row + dy[i]; - try { - var isMarked = pixelsMap[nextCol] && pixelsMap[nextCol][nextRow]; - if (frame.containsPixel(nextCol, nextRow) && !isMarked && !this.isInPixels_(nextCol, nextRow, pixelsMap)) { - queue.push({'col': nextCol, 'row': nextRow}); - - pixelsMap[nextCol] = pixelsMap[nextCol] || {}; - pixelsMap[nextCol][nextRow] = 2; - - if ((nextCol === 0 || nextCol == frame.getWidth() - 1) || (nextRow === 0 || nextRow == frame.getHeight() - 1)) { - isOut= true; - } - } - } catch (e) { - // Frame out of bound exception. - } - } - - // Security loop breaker: - if (loopCount > 10 * cellCount) { - console.log('loop breaker called'); - break; - } - } - - paintedPixels.forEach(function (p) { - pixelsMap[p.col] = pixelsMap[p.col] || {}; - pixelsMap[p.col][p.row] = isOut ? -1 : 1; - }); - - return !isOut; - }; - - - ns.LassoSelect.prototype.isInPixels_ = function (col, row, pixelsMap) { - return pixelsMap[col] && pixelsMap[col][row] === 1; + this.previousCol = col; + this.previousRow = row; }; })(); diff --git a/src/js/tools/drawing/selection/RectangleSelect.js b/src/js/tools/drawing/selection/RectangleSelect.js index d1e6ea23..42a9d8dc 100644 --- a/src/js/tools/drawing/selection/RectangleSelect.js +++ b/src/js/tools/drawing/selection/RectangleSelect.js @@ -4,43 +4,19 @@ * @require pskl.utils */ (function() { - var ns = $.namespace('pskl.tools.drawing'); + var ns = $.namespace('pskl.tools.drawing.selection'); ns.RectangleSelect = function() { this.toolId = 'tool-rectangle-select'; - this.helpText = 'Rectangle selection'; - ns.BaseSelect.call(this); - this.hasSelection = false; - - this.selectionOrigin_ = null; + ns.AbstractDragSelect.call(this); }; - pskl.utils.inherit(ns.RectangleSelect, ns.BaseSelect); + pskl.utils.inherit(ns.RectangleSelect, ns.AbstractDragSelect); - /** - * @override - */ - ns.RectangleSelect.prototype.onSelectStart_ = function (col, row, frame, overlay) { - this.selectionOrigin_ = { - col : col, - row : row - }; - if (this.hasSelection) { - this.hasSelection = false; - overlay.clear(); - $.publish(Events.SELECTION_DISMISSED); - } else { - this.startSelection_(col, row); - overlay.setPixel(col, row, Constants.SELECTION_TRANSPARENT_COLOR); - } - }; - - ns.RectangleSelect.prototype.startSelection_ = function (col, row) { - this.hasSelection = true; + ns.RectangleSelect.prototype.startDragSelection_ = function (col, row) { $.publish(Events.DRAG_START, [col, row]); - // Drawing the first point of the rectangle in the fake overlay canvas: }; /** @@ -49,25 +25,16 @@ * the current mouse coordiinate in sprite. * @override */ - ns.RectangleSelect.prototype.onSelect_ = function (col, row, frame, overlay) { - if (!this.hasSelection && (this.selectionOrigin_.col !== col || this.selectionOrigin_.row !== row)) { - this.startSelection_(col, row); - } - - if (this.hasSelection) { - overlay.clear(); - this.selection = new pskl.selection.RectangularSelection( - this.startCol, this.startRow, col, row); - $.publish(Events.SELECTION_CREATED, [this.selection]); - this.drawSelectionOnOverlay_(overlay); - } + ns.RectangleSelect.prototype.updateDragSelection_ = function (col, row, color, frame, overlay) { + overlay.clear(); + this.selection = new pskl.selection.RectangularSelection(this.startCol, this.startRow, col, row); + $.publish(Events.SELECTION_CREATED, [this.selection]); + this.drawSelectionOnOverlay_(overlay); }; - ns.RectangleSelect.prototype.onSelectEnd_ = function (col, row, frame, overlay) { - if (this.hasSelection) { - this.onSelect_(col, row, frame, overlay); - $.publish(Events.DRAG_END, [col, row]); - } + ns.RectangleSelect.prototype.onSelectEnd_ = function (col, row, color, frame, overlay) { + this.onSelect_(col, row, color, frame, overlay); + $.publish(Events.DRAG_END, [col, row]); }; })(); diff --git a/src/js/tools/drawing/selection/ShapeSelect.js b/src/js/tools/drawing/selection/ShapeSelect.js index 9b9eabf5..ef8fe667 100644 --- a/src/js/tools/drawing/selection/ShapeSelect.js +++ b/src/js/tools/drawing/selection/ShapeSelect.js @@ -4,7 +4,7 @@ * @require pskl.utils */ (function() { - var ns = $.namespace('pskl.tools.drawing'); + var ns = $.namespace('pskl.tools.drawing.selection'); ns.ShapeSelect = function() { this.toolId = 'tool-shape-select'; diff --git a/src/js/utils/PixelUtils.js b/src/js/utils/PixelUtils.js index 9400e709..755b347c 100644 --- a/src/js/utils/PixelUtils.js +++ b/src/js/utils/PixelUtils.js @@ -99,10 +99,6 @@ * 13. Continue looping until Q is exhausted. * 14. Return. */ - var paintedPixels = []; - var queue = []; - var dy = [-1, 0, 1, 0]; - var dx = [0, 1, 0, -1]; var targetColor; try { targetColor = frame.getPixel(col, row); @@ -114,22 +110,45 @@ return; } - queue.push({'col': col, 'row': row}); + var paintedPixels = pskl.PixelUtils.visitConnectedPixels({col:col, row:row}, frame, function (pixel) { + if (frame.containsPixel(pixel.col, pixel.row) && frame.getPixel(pixel.col, pixel.row) == targetColor) { + frame.setPixel(pixel.col, pixel.row, replacementColor); + return true; + } + return false; + }); + return paintedPixels; + }, + + visitConnectedPixels : function (pixel, frame, pixelVisitor) { + var col = pixel.col; + var row = pixel.row; + + var queue = []; + var visitedPixels = []; + var dy = [-1, 0, 1, 0]; + var dx = [0, 1, 0, -1]; + + queue.push(pixel); + visitedPixels.push(pixel); + pixelVisitor(pixel); + var loopCount = 0; var cellCount = frame.getWidth() * frame.getHeight(); while (queue.length > 0) { loopCount ++; var currentItem = queue.pop(); - frame.setPixel(currentItem.col, currentItem.row, replacementColor); - paintedPixels.push({'col': currentItem.col, 'row': currentItem.row}); for (var i = 0; i < 4; i++) { var nextCol = currentItem.col + dx[i]; var nextRow = currentItem.row + dy[i]; try { - if (frame.containsPixel(nextCol, nextRow) && frame.getPixel(nextCol, nextRow) == targetColor) { - queue.push({'col': nextCol, 'row': nextRow}); + var connectedPixel = {'col': nextCol, 'row': nextRow}; + var isValid = pixelVisitor(connectedPixel); + if (isValid) { + queue.push(connectedPixel); + visitedPixels.push(connectedPixel); } } catch (e) { // Frame out of bound exception. @@ -142,7 +161,8 @@ break; } } - return paintedPixels; + + return visitedPixels; }, /** diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index 0b07c2c8..2795063b 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -72,6 +72,7 @@ // Selection "js/selection/SelectionManager.js", "js/selection/BaseSelection.js", + "js/selection/LassoSelection.js", "js/selection/RectangularSelection.js", "js/selection/ShapeSelection.js", @@ -184,6 +185,7 @@ "js/tools/drawing/Circle.js", "js/tools/drawing/Move.js", "js/tools/drawing/selection/BaseSelect.js", + "js/tools/drawing/selection/AbstractDragSelect.js", "js/tools/drawing/selection/LassoSelect.js", "js/tools/drawing/selection/RectangleSelect.js", "js/tools/drawing/selection/ShapeSelect.js",