Issue #311 : Add lasso tool. Implementation and cleanup

This commit is contained in:
jdescottes
2015-09-22 00:33:04 +02:00
parent 7554b3355c
commit 28912fc58f
11 changed files with 207 additions and 225 deletions

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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];
};
})();

View File

@@ -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_();
}

View File

@@ -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) {};
})();

View File

@@ -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

View File

@@ -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;
};
})();

View File

@@ -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]);
};
})();

View File

@@ -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';

View File

@@ -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;
},
/**

View File

@@ -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",