Merge pull request #317 from juliandescottes/add-lasso-tool

Add lasso tool
This commit is contained in:
Julian Descottes 2015-10-06 00:51:43 +02:00
commit 2448e65ffa
19 changed files with 395 additions and 85 deletions

72
misc/icons/SVG/lasso.svg Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="224.38731"
height="177.96065"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="New document 1">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.175"
inkscape:cx="-1542.2107"
inkscape:cy="-1434.6156"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1148"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-212.79148,-238.81242)">
<path
style="fill:none;stroke:#000000;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:30, 30;stroke-dashoffset:0"
d="m 429.0234,294.50503 c 0,26.68038 -49.13123,48.3091 -109.73768,48.3091 -60.60644,0 -110.61221,-39.48861 -96.16624,-73.3091 19.3392,-45.27639 60.80353,-4.92189 116.16624,-16.88052 101.16819,-21.85285 89.73768,15.20015 89.73768,41.88052 z"
id="path3761"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssss" />
<path
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 309.32357,338.9218 c 0,0 8.97064,20.56224 -30.34229,45.31736 -39.31294,24.75512 -40.99309,30.13484 -40.99309,30.13484"
id="path3764"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -75,6 +75,10 @@
background-size: 24px 20px;
}
.tool-icon.tool-lasso-select {
background-image: url(../img/tools/lasso.png);
}
.tool-icon.tool-shape-select {
background-image: url(../img/tools/magicwand.png);
}
@ -159,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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
src/img/tools/lasso.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -58,7 +58,7 @@
this.redraw();
};
ns.CursorCoordinatesController.prototype.onDragEnd_ = function (event, x, y) {
ns.CursorCoordinatesController.prototype.onDragEnd_ = function (event) {
this.origin = null;
this.redraw();
};

View File

@ -16,8 +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('shapeSelect', 'Z', new pskl.tools.drawing.ShapeSelect()),
toDescriptor('shapeSelect', 'Z', new pskl.tools.drawing.selection.ShapeSelect()),
toDescriptor('rectangleSelect', 'S', new pskl.tools.drawing.selection.RectangleSelect()),
toDescriptor('lassoSelect', 'H', new pskl.tools.drawing.selection.LassoSelect()),
toDescriptor('lighten', 'U', new pskl.tools.drawing.Lighten()),
toDescriptor('dithering', 'T', new pskl.tools.drawing.DitheringTool()),
toDescriptor('colorPicker', 'O', new pskl.tools.drawing.ColorPicker())

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

@ -51,7 +51,7 @@
var color = this.getToolColor();
this.draw(coords.col, coords.row, color, frame);
$.publish(Events.DRAG_END, [coords.col, coords.row]);
$.publish(Events.DRAG_END);
this.raiseSaveStateEvent({
col : coords.col,
row : coords.row,

View File

@ -0,0 +1,61 @@
/**
* Base class for all select tools that use a dragging mechanism to define the selection.
*
* @provide pskl.tools.drawing.selection.AbstractDragSelect
*/
(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, frame, overlay) {
if (this.hasSelection) {
this.hasSelection = false;
overlay.clear();
$.publish(Events.SELECTION_DISMISSED);
} else {
this.hasSelection = true;
this.onDragSelectStart_(col, row);
overlay.setPixel(col, row, this.getTransparentVariant_(Constants.SELECTION_TRANSPARENT_COLOR));
}
};
/** @override */
ns.AbstractDragSelect.prototype.onSelect_ = function (col, row, frame, overlay) {
if (!this.hasSelection && (this.startCol !== col || this.startRow !== row)) {
this.hasSelection = true;
this.onDragSelectStart_(col, row);
}
if (this.hasSelection) {
this.onDragSelect_(col, row, frame, overlay);
}
};
/** @override */
ns.AbstractDragSelect.prototype.onSelectEnd_ = function (col, row, frame, overlay) {
if (this.hasSelection) {
this.onDragSelectEnd_(col, row, frame, overlay);
}
};
/** @private */
ns.AbstractDragSelect.prototype.startDragSelection_ = function (col, row, overlay) {
this.hasSelection = true;
this.onDragSelectStart_(col, row);
overlay.setPixel(col, row, this.getTransparentVariant_(Constants.SELECTION_TRANSPARENT_COLOR));
};
/** @protected */
ns.AbstractDragSelect.prototype.onDragSelectStart_ = function (col, row, frame, overlay) {};
/** @protected */
ns.AbstractDragSelect.prototype.onDragSelect_ = function (col, row, frame, overlay) {};
/** @protected */
ns.AbstractDragSelect.prototype.onDragSelectEnd_ = function (col, row, frame, overlay) {};
})();

View File

@ -1,10 +1,10 @@
/**
* @provide pskl.tools.drawing.BaseSelect
* @provide pskl.tools.drawing.selection.BaseSelect
*
* @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;
@ -14,6 +14,9 @@
this.startCol = null;
this.startRow = null;
this.lastMoveCol = null;
this.lastMoveRow = null;
this.selection = null;
this.tooltipDescriptors = [
@ -23,7 +26,7 @@
];
};
pskl.utils.inherit(ns.BaseSelect, ns.BaseTool);
pskl.utils.inherit(ns.BaseSelect, pskl.tools.drawing.BaseTool);
/**
* @override
@ -32,20 +35,20 @@
this.startCol = col;
this.startRow = row;
this.lastCol = col;
this.lastRow = row;
this.lastMoveCol = col;
this.lastMoveRow = row;
// The select tool can be in two different state.
// If the inital click of the tool is not on a selection, we go in 'select'
// mode to create a selection.
// If the initial click is on a previous selection, we go in 'moveSelection'
// mode to allow to move the selection by drag'n dropping it.
if (this.isInSelection(col, row)) {
this.mode = 'moveSelection';
this.onSelectionDragStart_(col, row, frame, overlay);
} else {
if (!this.isInSelection(col, row)) {
this.mode = 'select';
this.onSelectStart_(col, row, frame, overlay);
} else {
this.mode = 'moveSelection';
this.onSelectionMoveStart_(col, row, frame, overlay);
}
};
@ -56,7 +59,7 @@
if (this.mode == 'select') {
this.onSelect_(col, row, frame, overlay);
} else if (this.mode == 'moveSelection') {
this.onSelectionDrag_(col, row, frame, overlay);
this.onSelectionMove_(col, row, frame, overlay);
}
};
@ -67,7 +70,7 @@
if (this.mode == 'select') {
this.onSelectEnd_(col, row, frame, overlay);
} else if (this.mode == 'moveSelection') {
this.onSelectionDragEnd_(col, row, frame, overlay);
this.onSelectionMoveEnd_(col, row, frame, overlay);
}
};
@ -132,12 +135,13 @@
// The list of callbacks that define the drag'n drop behavior of the selection.
/** @private */
ns.BaseSelect.prototype.onSelectionDragStart_ = function (col, row, frame, overlay) {};
ns.BaseSelect.prototype.onSelectionMoveStart_ = function (col, row, frame, overlay) {};
/** @private */
ns.BaseSelect.prototype.onSelectionDrag_ = function (col, row, frame, overlay) {
var deltaCol = col - this.lastCol;
var deltaRow = row - this.lastRow;
ns.BaseSelect.prototype.onSelectionMove_ = function (col, row, frame, overlay) {
var deltaCol = col - this.lastMoveCol;
var deltaRow = row - this.lastMoveRow;
var colDiff = col - this.startCol;
var rowDiff = row - this.startRow;
@ -147,12 +151,12 @@
overlay.clear();
this.drawSelectionOnOverlay_(overlay);
this.lastCol = col;
this.lastRow = row;
this.lastMoveCol = col;
this.lastMoveRow = row;
};
/** @private */
ns.BaseSelect.prototype.onSelectionDragEnd_ = function (col, row, frame, overlay) {
this.onSelectionDrag_(col, row, frame, overlay);
ns.BaseSelect.prototype.onSelectionMoveEnd_ = function (col, row, frame, overlay) {
this.onSelectionMove_(col, row, frame, overlay);
};
})();

View File

@ -0,0 +1,87 @@
/**
* @provide pskl.tools.drawing.selection.LassoSelect
*
* @require pskl.utils
*/
(function() {
var ns = $.namespace('pskl.tools.drawing.selection');
ns.LassoSelect = function() {
this.toolId = 'tool-lasso-select';
this.helpText = 'Lasso selection';
ns.AbstractDragSelect.call(this);
};
pskl.utils.inherit(ns.LassoSelect, ns.AbstractDragSelect);
/** @override */
ns.LassoSelect.prototype.onDragSelectStart_ = function (col, row) {
this.pixels = [{col : col, row : row}];
this.startCol = col;
this.startRow = row;
this.previousCol = col;
this.previousRow = row;
$.publish(Events.DRAG_START, [col, row]);
};
/** @override */
ns.LassoSelect.prototype.onDragSelect_ = function (col, row, frame, overlay) {
this.addPixel_(col, row, frame);
// use ShapeSelection during selection, contains only the pixels hovered by the user
var selection = new pskl.selection.ShapeSelection(this.getLassoPixels_());
this.setSelection_(selection, overlay);
};
/** @override */
ns.LassoSelect.prototype.onDragSelectEnd_ = function (col, row, frame, overlay) {
this.addPixel_(col, row, frame);
// use LassoSelection to finalize selection, includes pixels inside the lasso shape
var selection = new pskl.selection.LassoSelection(this.getLassoPixels_(), frame);
this.setSelection_(selection, overlay);
$.publish(Events.DRAG_END);
};
/**
* Retrieve the lasso shape as an array of pixels. A line is added between the origin of the selection
* and the last known coordinate to make sure the shape is closed.
*
* @return {Array} array of pixels corresponding to the whole lasso shape
* @private
*/
ns.LassoSelect.prototype.getLassoPixels_ = function () {
var line = this.getLinePixels_(this.previousCol, this.startCol, this.previousRow, this.startRow);
return this.pixels.concat(line);
};
/**
* Add the provided pixel to the lasso pixels Array.
* @private
*/
ns.LassoSelect.prototype.addPixel_ = function (col, row, frame) {
// normalize coordinates to always remain inside the frame
col = pskl.utils.Math.minmax(col, 0, frame.getWidth() - 1);
row = pskl.utils.Math.minmax(row, 0, frame.getHeight() - 1);
// line interpolation needed in case mousemove was too fast
var interpolatedPixels = this.getLinePixels_(col, this.previousCol, row, this.previousRow);
this.pixels = this.pixels.concat(interpolatedPixels);
// update state
this.previousCol = col;
this.previousRow = row;
};
/** @private */
ns.LassoSelect.prototype.setSelection_ = function (selection, overlay) {
this.selection = selection;
$.publish(Events.SELECTION_CREATED, [this.selection]);
overlay.clear();
this.drawSelectionOnOverlay_(overlay);
};
})();

View File

@ -1,46 +1,23 @@
/**
* @provide pskl.tools.drawing.RectangleSelect
* @provide pskl.tools.drawing.selection.RectangleSelect
*
* @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;
/** @override */
ns.RectangleSelect.prototype.onDragSelectStart_ = function (col, row) {
$.publish(Events.DRAG_START, [col, row]);
// Drawing the first point of the rectangle in the fake overlay canvas:
};
/**
@ -49,25 +26,17 @@
* 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) {
ns.RectangleSelect.prototype.onDragSelect_ = function (col, row, frame, overlay) {
overlay.clear();
this.selection = new pskl.selection.RectangularSelection(
this.startCol, this.startRow, col, row);
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) {
/** @override */
ns.RectangleSelect.prototype.onDragSelectEnd_ = function (col, row, frame, overlay) {
this.onSelect_(col, row, frame, overlay);
$.publish(Events.DRAG_END, [col, row]);
}
$.publish(Events.DRAG_END);
};
})();

View File

@ -1,10 +1,10 @@
/**
* @provide pskl.tools.drawing.ShapeSelect
* @provide pskl.tools.drawing.selection.ShapeSelect
*
* @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,57 @@
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;
},
/**
* Starting from a provided origin, visit connected pixels using a visitor function.
* After visiting a pixel, select the 4 connected pixels (up, right, down, left).
* Call the provided visitor on each pixel. The visitor should return true if the
* pixel should be considered as connected. If the pixel is connected repeat the
* process with its own connected pixels
*
* TODO : Julian : The visitor is also responsible for making sure a pixel is never
* visited twice. This could be handled by default by this method.
*
* @return {Array} the array of visited pixels {col, row}
*/
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 +173,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,8 @@
"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",
"js/tools/drawing/ColorPicker.js",

View File

@ -22,5 +22,6 @@
"transform.rotate.alt.twice.undo.once.json",
"transform.flip.once.alt.json",
"transform.flip.twice.undo.once.json",
"transform.flip.thrice.undo.all.redo.all.json"
"transform.flip.thrice.undo.all.redo.all.json",
"selection.lasso.json"
]}

View File

@ -21,5 +21,6 @@
"transform.rotate.alt.twice.undo.once.json",
"transform.flip.once.alt.json",
"transform.flip.twice.undo.once.json",
"transform.flip.thrice.undo.all.redo.all.json"
"transform.flip.thrice.undo.all.redo.all.json",
"selection.lasso.json"
];

File diff suppressed because one or more lines are too long