Merge pull request #28 from outis1one/claude/migrate-to-minipaint-eYKWf
Add selection tools: Magic Wand, Lasso, Ellipse Select
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<ellipse cx="12" cy="12" rx="10" ry="7" stroke-dasharray="4 2"/>
|
||||||
|
<circle cx="2" cy="12" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="22" cy="12" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="12" cy="5" r="1.5" fill="currentColor"/>
|
||||||
|
<circle cx="12" cy="19" r="1.5" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 455 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 2C6.5 2 2 6.5 2 12c0 3.5 1.8 6.6 4.5 8.4"/>
|
||||||
|
<path d="M12 2c5.5 0 10 4.5 10 10 0 2.5-.9 4.8-2.4 6.5"/>
|
||||||
|
<circle cx="12" cy="20" r="2"/>
|
||||||
|
<path d="M12 18v-6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15 4V2"/>
|
||||||
|
<path d="M15 16v-2"/>
|
||||||
|
<path d="M8 9h2"/>
|
||||||
|
<path d="M20 9h2"/>
|
||||||
|
<path d="M17.8 11.8L19 13"/>
|
||||||
|
<path d="M15 9h.01"/>
|
||||||
|
<path d="M17.8 6.2L19 5"/>
|
||||||
|
<path d="M12.2 6.2L11 5"/>
|
||||||
|
<path d="M12.2 11.8L11 13"/>
|
||||||
|
<path d="M2 22l10-10"/>
|
||||||
|
<path d="M9 19l3-3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 449 B |
@@ -103,6 +103,28 @@ config.TOOLS = [
|
|||||||
on_activate: 'on_activate',
|
on_activate: 'on_activate',
|
||||||
attributes: {},
|
attributes: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'magic_wand',
|
||||||
|
title: 'Magic Wand (Color Select)',
|
||||||
|
attributes: {
|
||||||
|
tolerance: {
|
||||||
|
value: 30,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
contiguous: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lasso',
|
||||||
|
title: 'Lasso (Freehand Select)',
|
||||||
|
attributes: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ellipse_select',
|
||||||
|
title: 'Ellipse Selection',
|
||||||
|
attributes: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'brush',
|
name: 'brush',
|
||||||
attributes: {
|
attributes: {
|
||||||
|
|||||||
@@ -0,0 +1,657 @@
|
|||||||
|
/**
|
||||||
|
* Ellipse Selection Tool - Draw elliptical/circular selections
|
||||||
|
* Drag to create ellipse selection, Shift+Drag for perfect circle
|
||||||
|
* Hold Alt to draw from center
|
||||||
|
*/
|
||||||
|
|
||||||
|
import app from './../app.js';
|
||||||
|
import config from './../config.js';
|
||||||
|
import Base_tools_class from './../core/base-tools.js';
|
||||||
|
import Base_layers_class from './../core/base-layers.js';
|
||||||
|
import Helper_class from './../libs/helpers.js';
|
||||||
|
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||||
|
|
||||||
|
class Ellipse_select_class extends Base_tools_class {
|
||||||
|
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.Base_layers = new Base_layers_class();
|
||||||
|
this.Helper = new Helper_class();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.name = 'ellipse_select';
|
||||||
|
|
||||||
|
// Drawing state
|
||||||
|
this.isDrawing = false;
|
||||||
|
this.startPoint = null;
|
||||||
|
this.currentPoint = null;
|
||||||
|
this.isAdditive = false;
|
||||||
|
this.isCircle = false;
|
||||||
|
this.fromCenter = false;
|
||||||
|
|
||||||
|
// Store the current mask data
|
||||||
|
this.currentMask = null;
|
||||||
|
this.maskCanvas = null;
|
||||||
|
this.selectionBounds = null;
|
||||||
|
|
||||||
|
// Marching ants animation
|
||||||
|
this.marchingAntsOffset = 0;
|
||||||
|
|
||||||
|
// Edge canvas for drawing the mask outline
|
||||||
|
this.edgeCanvas = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
document.addEventListener('mousedown', function (e) {
|
||||||
|
_this.mousedown(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('mousemove', function (e) {
|
||||||
|
_this.mousemove(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('mouseup', function (e) {
|
||||||
|
_this.mouseup(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
document.addEventListener('touchstart', function (e) {
|
||||||
|
_this.mousedown(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('touchmove', function (e) {
|
||||||
|
_this.mousemove(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('touchend', function (e) {
|
||||||
|
_this.mouseup(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (config.TOOL.name != _this.name) return;
|
||||||
|
if (_this.Helper.is_input(e.target)) return;
|
||||||
|
|
||||||
|
var code = e.keyCode;
|
||||||
|
|
||||||
|
if (code == 46 && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.deleteSelection();
|
||||||
|
}
|
||||||
|
if (code == 27 && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.clearSelection();
|
||||||
|
}
|
||||||
|
if (code == 67 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.copyToLayer();
|
||||||
|
}
|
||||||
|
if (code == 88 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.cutToLayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startMarchingAnts();
|
||||||
|
}
|
||||||
|
|
||||||
|
startMarchingAnts() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
setInterval(function() {
|
||||||
|
if (_this.currentMask || _this.isDrawing) {
|
||||||
|
_this.marchingAntsOffset++;
|
||||||
|
if (_this.marchingAntsOffset > 16) {
|
||||||
|
_this.marchingAntsOffset = 0;
|
||||||
|
}
|
||||||
|
config.need_render = true;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown(e) {
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
|
||||||
|
if (config.TOOL.name != this.name) return;
|
||||||
|
if (mouse.click_valid == false) return;
|
||||||
|
|
||||||
|
if (config.layer.type != 'image') {
|
||||||
|
alertify.error('Please select an image layer first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDrawing = true;
|
||||||
|
this.isAdditive = e.shiftKey;
|
||||||
|
this.isCircle = false;
|
||||||
|
this.fromCenter = e.altKey;
|
||||||
|
|
||||||
|
this.startPoint = this.getImagePoint(mouse.x, mouse.y);
|
||||||
|
this.currentPoint = this.startPoint;
|
||||||
|
|
||||||
|
config.need_render = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove(e) {
|
||||||
|
if (!this.isDrawing) return;
|
||||||
|
if (config.TOOL.name != this.name) return;
|
||||||
|
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
|
||||||
|
this.currentPoint = this.getImagePoint(mouse.x, mouse.y);
|
||||||
|
this.isCircle = e.shiftKey;
|
||||||
|
this.fromCenter = e.altKey;
|
||||||
|
|
||||||
|
config.need_render = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup(e) {
|
||||||
|
if (!this.isDrawing) return;
|
||||||
|
if (config.TOOL.name != this.name) return;
|
||||||
|
|
||||||
|
this.isDrawing = false;
|
||||||
|
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
this.currentPoint = this.getImagePoint(mouse.x, mouse.y);
|
||||||
|
this.isCircle = e.shiftKey;
|
||||||
|
this.fromCenter = e.altKey;
|
||||||
|
|
||||||
|
// Calculate ellipse bounds
|
||||||
|
var ellipse = this.calculateEllipse();
|
||||||
|
|
||||||
|
if (ellipse.radiusX < 2 || ellipse.radiusY < 2) {
|
||||||
|
alertify.warning('Draw a larger selection');
|
||||||
|
config.need_render = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mask from ellipse
|
||||||
|
this.createMaskFromEllipse(ellipse, this.isAdditive);
|
||||||
|
|
||||||
|
this.startPoint = null;
|
||||||
|
this.currentPoint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getImagePoint(mouseX, mouseY) {
|
||||||
|
var x = mouseX - config.layer.x;
|
||||||
|
var y = mouseY - config.layer.y;
|
||||||
|
|
||||||
|
if (config.layer.width != config.layer.width_original) {
|
||||||
|
x = x * (config.layer.width_original / config.layer.width);
|
||||||
|
}
|
||||||
|
if (config.layer.height != config.layer.height_original) {
|
||||||
|
y = y * (config.layer.height_original / config.layer.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.max(0, Math.min(config.layer.width_original - 1, Math.round(x)));
|
||||||
|
y = Math.max(0, Math.min(config.layer.height_original - 1, Math.round(y)));
|
||||||
|
|
||||||
|
return { x: x, y: y };
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateEllipse() {
|
||||||
|
if (!this.startPoint || !this.currentPoint) {
|
||||||
|
return { centerX: 0, centerY: 0, radiusX: 0, radiusY: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
var x1 = this.startPoint.x;
|
||||||
|
var y1 = this.startPoint.y;
|
||||||
|
var x2 = this.currentPoint.x;
|
||||||
|
var y2 = this.currentPoint.y;
|
||||||
|
|
||||||
|
var width = Math.abs(x2 - x1);
|
||||||
|
var height = Math.abs(y2 - y1);
|
||||||
|
|
||||||
|
// If Shift is held, make it a circle (equal radii)
|
||||||
|
if (this.isCircle) {
|
||||||
|
var maxDim = Math.max(width, height);
|
||||||
|
width = maxDim;
|
||||||
|
height = maxDim;
|
||||||
|
}
|
||||||
|
|
||||||
|
var centerX, centerY, radiusX, radiusY;
|
||||||
|
|
||||||
|
if (this.fromCenter) {
|
||||||
|
// Draw from center
|
||||||
|
centerX = x1;
|
||||||
|
centerY = y1;
|
||||||
|
radiusX = width;
|
||||||
|
radiusY = height;
|
||||||
|
} else {
|
||||||
|
// Draw from corner
|
||||||
|
var left = Math.min(x1, x2);
|
||||||
|
var top = Math.min(y1, y2);
|
||||||
|
|
||||||
|
if (this.isCircle) {
|
||||||
|
// Adjust for circle from corner
|
||||||
|
if (x2 < x1) left = x1 - width;
|
||||||
|
if (y2 < y1) top = y1 - height;
|
||||||
|
}
|
||||||
|
|
||||||
|
centerX = left + width / 2;
|
||||||
|
centerY = top + height / 2;
|
||||||
|
radiusX = width / 2;
|
||||||
|
radiusY = height / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
centerX: centerX,
|
||||||
|
centerY: centerY,
|
||||||
|
radiusX: radiusX,
|
||||||
|
radiusY: radiusY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createMaskFromEllipse(ellipse, isAdditive) {
|
||||||
|
var width = config.layer.width_original;
|
||||||
|
var height = config.layer.height_original;
|
||||||
|
|
||||||
|
var newMaskCanvas = document.createElement('canvas');
|
||||||
|
newMaskCanvas.width = width;
|
||||||
|
newMaskCanvas.height = height;
|
||||||
|
var maskCtx = newMaskCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Draw filled ellipse
|
||||||
|
maskCtx.fillStyle = 'white';
|
||||||
|
maskCtx.beginPath();
|
||||||
|
maskCtx.ellipse(
|
||||||
|
ellipse.centerX,
|
||||||
|
ellipse.centerY,
|
||||||
|
ellipse.radiusX,
|
||||||
|
ellipse.radiusY,
|
||||||
|
0, 0, Math.PI * 2
|
||||||
|
);
|
||||||
|
maskCtx.fill();
|
||||||
|
|
||||||
|
// Combine with existing mask if additive
|
||||||
|
if (isAdditive && this.maskCanvas) {
|
||||||
|
var combinedCanvas = document.createElement('canvas');
|
||||||
|
combinedCanvas.width = width;
|
||||||
|
combinedCanvas.height = height;
|
||||||
|
var combinedCtx = combinedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
combinedCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
combinedCtx.globalCompositeOperation = 'lighter';
|
||||||
|
combinedCtx.drawImage(newMaskCanvas, 0, 0);
|
||||||
|
|
||||||
|
this.maskCanvas = combinedCanvas;
|
||||||
|
} else {
|
||||||
|
this.maskCanvas = newMaskCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentMask = {
|
||||||
|
canvas: this.maskCanvas
|
||||||
|
};
|
||||||
|
|
||||||
|
window.smartSelectMask = this.currentMask;
|
||||||
|
|
||||||
|
this.calculateSelectionBounds();
|
||||||
|
this.extractContourPath();
|
||||||
|
|
||||||
|
config.need_render = true;
|
||||||
|
this.Base_layers.render();
|
||||||
|
|
||||||
|
if (isAdditive) {
|
||||||
|
alertify.success('Added to selection!');
|
||||||
|
} else {
|
||||||
|
alertify.success('Selection complete! Hold Shift while dragging to add more.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateSelectionBounds() {
|
||||||
|
if (!this.maskCanvas) return;
|
||||||
|
|
||||||
|
var maskCtx = this.maskCanvas.getContext('2d');
|
||||||
|
var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
|
|
||||||
|
var minX = this.maskCanvas.width, minY = this.maskCanvas.height;
|
||||||
|
var maxX = 0, maxY = 0;
|
||||||
|
var hasSelection = false;
|
||||||
|
|
||||||
|
for (var y = 0; y < this.maskCanvas.height; y++) {
|
||||||
|
for (var x = 0; x < this.maskCanvas.width; x++) {
|
||||||
|
var i = (y * this.maskCanvas.width + x) * 4;
|
||||||
|
if (imageData.data[i] > 128) {
|
||||||
|
hasSelection = true;
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSelection && maxX > minX && maxY > minY) {
|
||||||
|
var scaleX = config.layer.width / config.layer.width_original;
|
||||||
|
var scaleY = config.layer.height / config.layer.height_original;
|
||||||
|
|
||||||
|
this.selectionBounds = {
|
||||||
|
x: config.layer.x + minX * scaleX,
|
||||||
|
y: config.layer.y + minY * scaleY,
|
||||||
|
width: (maxX - minX) * scaleX,
|
||||||
|
height: (maxY - minY) * scaleY,
|
||||||
|
origMinX: minX,
|
||||||
|
origMinY: minY,
|
||||||
|
origMaxX: maxX,
|
||||||
|
origMaxY: maxY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractContourPath() {
|
||||||
|
if (!this.maskCanvas) return;
|
||||||
|
|
||||||
|
var maskCtx = this.maskCanvas.getContext('2d');
|
||||||
|
var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
|
var width = this.maskCanvas.width;
|
||||||
|
var height = this.maskCanvas.height;
|
||||||
|
var data = imageData.data;
|
||||||
|
|
||||||
|
this.edgeCanvas = document.createElement('canvas');
|
||||||
|
this.edgeCanvas.width = width;
|
||||||
|
this.edgeCanvas.height = height;
|
||||||
|
var edgeCtx = this.edgeCanvas.getContext('2d');
|
||||||
|
var edgeImageData = edgeCtx.createImageData(width, height);
|
||||||
|
var edgeData = edgeImageData.data;
|
||||||
|
|
||||||
|
for (var y = 0; y < height; y++) {
|
||||||
|
for (var x = 0; x < width; x++) {
|
||||||
|
var i = (y * width + x) * 4;
|
||||||
|
var isMask = data[i] > 128;
|
||||||
|
|
||||||
|
if (isMask) {
|
||||||
|
var isEdge = false;
|
||||||
|
|
||||||
|
if (x > 0 && data[i - 4] <= 128) isEdge = true;
|
||||||
|
if (x < width - 1 && data[i + 4] <= 128) isEdge = true;
|
||||||
|
if (y > 0 && data[i - width * 4] <= 128) isEdge = true;
|
||||||
|
if (y < height - 1 && data[i + width * 4] <= 128) isEdge = true;
|
||||||
|
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) isEdge = true;
|
||||||
|
|
||||||
|
if (isEdge) {
|
||||||
|
edgeData[i] = 255;
|
||||||
|
edgeData[i + 1] = 255;
|
||||||
|
edgeData[i + 2] = 255;
|
||||||
|
edgeData[i + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeCtx.putImageData(edgeImageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_overlay(ctx) {
|
||||||
|
// Draw current ellipse being drawn
|
||||||
|
if (this.isDrawing && this.startPoint && this.currentPoint) {
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
var ellipse = this.calculateEllipse();
|
||||||
|
var scaleX = config.layer.width / config.layer.width_original;
|
||||||
|
var scaleY = config.layer.height / config.layer.height_original;
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#ffff00';
|
||||||
|
ctx.lineWidth = 2 / config.ZOOM;
|
||||||
|
ctx.setLineDash([5, 5]);
|
||||||
|
ctx.lineDashOffset = -this.marchingAntsOffset;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(
|
||||||
|
config.layer.x + ellipse.centerX * scaleX,
|
||||||
|
config.layer.y + ellipse.centerY * scaleY,
|
||||||
|
ellipse.radiusX * scaleX,
|
||||||
|
ellipse.radiusY * scaleY,
|
||||||
|
0, 0, Math.PI * 2
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw existing selection
|
||||||
|
if (!this.currentMask || !this.maskCanvas) return;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// Draw semi-transparent overlay
|
||||||
|
var inverseCanvas = document.createElement('canvas');
|
||||||
|
inverseCanvas.width = this.maskCanvas.width;
|
||||||
|
inverseCanvas.height = this.maskCanvas.height;
|
||||||
|
var inverseCtx = inverseCanvas.getContext('2d');
|
||||||
|
|
||||||
|
inverseCtx.fillStyle = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
inverseCtx.fillRect(0, 0, inverseCanvas.width, inverseCanvas.height);
|
||||||
|
|
||||||
|
inverseCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
inverseCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
inverseCanvas,
|
||||||
|
config.layer.x, config.layer.y,
|
||||||
|
config.layer.width, config.layer.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw marching ants
|
||||||
|
if (this.edgeCanvas) {
|
||||||
|
var antsCanvas = document.createElement('canvas');
|
||||||
|
antsCanvas.width = this.maskCanvas.width;
|
||||||
|
antsCanvas.height = this.maskCanvas.height;
|
||||||
|
var antsCtx = antsCanvas.getContext('2d');
|
||||||
|
|
||||||
|
antsCtx.drawImage(this.edgeCanvas, 0, 0);
|
||||||
|
antsCtx.globalCompositeOperation = 'source-in';
|
||||||
|
|
||||||
|
var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#ffff00' : '#ffffff';
|
||||||
|
antsCtx.fillStyle = color;
|
||||||
|
antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
antsCanvas,
|
||||||
|
config.layer.x, config.layer.y,
|
||||||
|
config.layer.width, config.layer.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToLayer() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to copy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = layer.width_original;
|
||||||
|
canvas.height = layer.height_original;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(layer.link, 0, 0);
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var croppedCanvas = document.createElement('canvas');
|
||||||
|
croppedCanvas.width = cropWidth;
|
||||||
|
croppedCanvas.height = cropHeight;
|
||||||
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
croppedCtx.drawImage(
|
||||||
|
canvas,
|
||||||
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
|
0, 0, cropWidth, cropHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
var scaleX = layer.width / layer.width_original;
|
||||||
|
var scaleY = layer.height / layer.height_original;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
|
width: cropWidth,
|
||||||
|
height: cropHeight,
|
||||||
|
width_original: cropWidth,
|
||||||
|
height_original: cropHeight,
|
||||||
|
type: 'image',
|
||||||
|
name: 'Ellipse Selection',
|
||||||
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
|
};
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
|
||||||
|
new app.Actions.Insert_layer_action(params)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
alertify.success('Selection copied to new layer!');
|
||||||
|
}
|
||||||
|
|
||||||
|
cutToLayer() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to cut');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = layer.width_original;
|
||||||
|
canvas.height = layer.height_original;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(layer.link, 0, 0);
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var croppedCanvas = document.createElement('canvas');
|
||||||
|
croppedCanvas.width = cropWidth;
|
||||||
|
croppedCanvas.height = cropHeight;
|
||||||
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
croppedCtx.drawImage(
|
||||||
|
canvas,
|
||||||
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
|
0, 0, cropWidth, cropHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
var scaleX = layer.width / layer.width_original;
|
||||||
|
var scaleY = layer.height / layer.height_original;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
|
width: cropWidth,
|
||||||
|
height: cropHeight,
|
||||||
|
width_original: cropWidth,
|
||||||
|
height_original: cropHeight,
|
||||||
|
type: 'image',
|
||||||
|
name: 'Ellipse Cut',
|
||||||
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
|
};
|
||||||
|
|
||||||
|
var holeCanvas = document.createElement('canvas');
|
||||||
|
holeCanvas.width = layer.width_original;
|
||||||
|
holeCanvas.height = layer.height_original;
|
||||||
|
var holeCtx = holeCanvas.getContext('2d');
|
||||||
|
|
||||||
|
holeCtx.drawImage(layer.link, 0, 0);
|
||||||
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
|
||||||
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
|
||||||
|
new app.Actions.Insert_layer_action(params)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearSelection();
|
||||||
|
alertify.success('Selection cut to new layer!');
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelection() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to delete');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var holeCanvas = document.createElement('canvas');
|
||||||
|
holeCanvas.width = layer.width_original;
|
||||||
|
holeCanvas.height = layer.height_original;
|
||||||
|
var holeCtx = holeCanvas.getContext('2d');
|
||||||
|
|
||||||
|
holeCtx.drawImage(layer.link, 0, 0);
|
||||||
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
|
||||||
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearSelection();
|
||||||
|
alertify.success('Selection deleted!');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.currentMask = null;
|
||||||
|
this.maskCanvas = null;
|
||||||
|
this.edgeCanvas = null;
|
||||||
|
this.selectionBounds = null;
|
||||||
|
this.startPoint = null;
|
||||||
|
this.currentPoint = null;
|
||||||
|
window.smartSelectMask = null;
|
||||||
|
config.need_render = true;
|
||||||
|
this.Base_layers.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
on_leave() {
|
||||||
|
this.isDrawing = false;
|
||||||
|
this.startPoint = null;
|
||||||
|
this.currentPoint = null;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Ellipse_select_class;
|
||||||
@@ -0,0 +1,597 @@
|
|||||||
|
/**
|
||||||
|
* Lasso Selection Tool - Freehand selection by drawing
|
||||||
|
* Draw around an area to select it, Shift+Draw to add to selection
|
||||||
|
*/
|
||||||
|
|
||||||
|
import app from './../app.js';
|
||||||
|
import config from './../config.js';
|
||||||
|
import Base_tools_class from './../core/base-tools.js';
|
||||||
|
import Base_layers_class from './../core/base-layers.js';
|
||||||
|
import Helper_class from './../libs/helpers.js';
|
||||||
|
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||||
|
|
||||||
|
class Lasso_class extends Base_tools_class {
|
||||||
|
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.Base_layers = new Base_layers_class();
|
||||||
|
this.Helper = new Helper_class();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.name = 'lasso';
|
||||||
|
|
||||||
|
// Drawing state
|
||||||
|
this.isDrawing = false;
|
||||||
|
this.points = [];
|
||||||
|
|
||||||
|
// Store the current mask data
|
||||||
|
this.currentMask = null;
|
||||||
|
this.maskCanvas = null;
|
||||||
|
this.selectionBounds = null;
|
||||||
|
|
||||||
|
// Marching ants animation
|
||||||
|
this.marchingAntsOffset = 0;
|
||||||
|
|
||||||
|
// Edge canvas for drawing the mask outline
|
||||||
|
this.edgeCanvas = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
document.addEventListener('mousedown', function (e) {
|
||||||
|
_this.mousedown(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('mousemove', function (e) {
|
||||||
|
_this.mousemove(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('mouseup', function (e) {
|
||||||
|
_this.mouseup(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
document.addEventListener('touchstart', function (e) {
|
||||||
|
_this.mousedown(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('touchmove', function (e) {
|
||||||
|
_this.mousemove(e);
|
||||||
|
});
|
||||||
|
document.addEventListener('touchend', function (e) {
|
||||||
|
_this.mouseup(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (config.TOOL.name != _this.name) return;
|
||||||
|
if (_this.Helper.is_input(e.target)) return;
|
||||||
|
|
||||||
|
var code = e.keyCode;
|
||||||
|
|
||||||
|
if (code == 46 && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.deleteSelection();
|
||||||
|
}
|
||||||
|
if (code == 27 && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.clearSelection();
|
||||||
|
}
|
||||||
|
if (code == 67 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.copyToLayer();
|
||||||
|
}
|
||||||
|
if (code == 88 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.cutToLayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startMarchingAnts();
|
||||||
|
}
|
||||||
|
|
||||||
|
startMarchingAnts() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
setInterval(function() {
|
||||||
|
if (_this.currentMask || _this.isDrawing) {
|
||||||
|
_this.marchingAntsOffset++;
|
||||||
|
if (_this.marchingAntsOffset > 16) {
|
||||||
|
_this.marchingAntsOffset = 0;
|
||||||
|
}
|
||||||
|
config.need_render = true;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown(e) {
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
|
||||||
|
if (config.TOOL.name != this.name) return;
|
||||||
|
if (mouse.click_valid == false) return;
|
||||||
|
|
||||||
|
if (config.layer.type != 'image') {
|
||||||
|
alertify.error('Please select an image layer first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDrawing = true;
|
||||||
|
this.points = [];
|
||||||
|
|
||||||
|
// Get point relative to image
|
||||||
|
var point = this.getImagePoint(mouse.x, mouse.y);
|
||||||
|
if (point) {
|
||||||
|
this.points.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.need_render = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove(e) {
|
||||||
|
if (!this.isDrawing) return;
|
||||||
|
if (config.TOOL.name != this.name) return;
|
||||||
|
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
|
||||||
|
var point = this.getImagePoint(mouse.x, mouse.y);
|
||||||
|
if (point) {
|
||||||
|
this.points.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.need_render = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup(e) {
|
||||||
|
if (!this.isDrawing) return;
|
||||||
|
if (config.TOOL.name != this.name) return;
|
||||||
|
|
||||||
|
this.isDrawing = false;
|
||||||
|
|
||||||
|
if (this.points.length < 3) {
|
||||||
|
alertify.warning('Draw a larger selection');
|
||||||
|
this.points = [];
|
||||||
|
config.need_render = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Shift key - additive selection
|
||||||
|
var isAdditive = e.shiftKey;
|
||||||
|
|
||||||
|
// Create mask from points
|
||||||
|
this.createMaskFromPoints(isAdditive);
|
||||||
|
|
||||||
|
this.points = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getImagePoint(mouseX, mouseY) {
|
||||||
|
var x = mouseX - config.layer.x;
|
||||||
|
var y = mouseY - config.layer.y;
|
||||||
|
|
||||||
|
// Adjust for layer scaling
|
||||||
|
if (config.layer.width != config.layer.width_original) {
|
||||||
|
x = x * (config.layer.width_original / config.layer.width);
|
||||||
|
}
|
||||||
|
if (config.layer.height != config.layer.height_original) {
|
||||||
|
y = y * (config.layer.height_original / config.layer.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp to image bounds
|
||||||
|
x = Math.max(0, Math.min(config.layer.width_original - 1, Math.round(x)));
|
||||||
|
y = Math.max(0, Math.min(config.layer.height_original - 1, Math.round(y)));
|
||||||
|
|
||||||
|
return { x: x, y: y };
|
||||||
|
}
|
||||||
|
|
||||||
|
createMaskFromPoints(isAdditive) {
|
||||||
|
var width = config.layer.width_original;
|
||||||
|
var height = config.layer.height_original;
|
||||||
|
|
||||||
|
// Create new mask canvas
|
||||||
|
var newMaskCanvas = document.createElement('canvas');
|
||||||
|
newMaskCanvas.width = width;
|
||||||
|
newMaskCanvas.height = height;
|
||||||
|
var maskCtx = newMaskCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Draw filled polygon
|
||||||
|
maskCtx.fillStyle = 'white';
|
||||||
|
maskCtx.beginPath();
|
||||||
|
maskCtx.moveTo(this.points[0].x, this.points[0].y);
|
||||||
|
for (var i = 1; i < this.points.length; i++) {
|
||||||
|
maskCtx.lineTo(this.points[i].x, this.points[i].y);
|
||||||
|
}
|
||||||
|
maskCtx.closePath();
|
||||||
|
maskCtx.fill();
|
||||||
|
|
||||||
|
// If additive and we have an existing mask, combine them
|
||||||
|
if (isAdditive && this.maskCanvas) {
|
||||||
|
var combinedCanvas = document.createElement('canvas');
|
||||||
|
combinedCanvas.width = width;
|
||||||
|
combinedCanvas.height = height;
|
||||||
|
var combinedCtx = combinedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
combinedCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
combinedCtx.globalCompositeOperation = 'lighter';
|
||||||
|
combinedCtx.drawImage(newMaskCanvas, 0, 0);
|
||||||
|
|
||||||
|
this.maskCanvas = combinedCanvas;
|
||||||
|
} else {
|
||||||
|
this.maskCanvas = newMaskCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentMask = {
|
||||||
|
canvas: this.maskCanvas
|
||||||
|
};
|
||||||
|
|
||||||
|
window.smartSelectMask = this.currentMask;
|
||||||
|
|
||||||
|
this.calculateSelectionBounds();
|
||||||
|
this.extractContourPath();
|
||||||
|
|
||||||
|
config.need_render = true;
|
||||||
|
this.Base_layers.render();
|
||||||
|
|
||||||
|
if (isAdditive) {
|
||||||
|
alertify.success('Added to selection!');
|
||||||
|
} else {
|
||||||
|
alertify.success('Selection complete! Shift+Draw to add more.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateSelectionBounds() {
|
||||||
|
if (!this.maskCanvas) return;
|
||||||
|
|
||||||
|
var maskCtx = this.maskCanvas.getContext('2d');
|
||||||
|
var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
|
|
||||||
|
var minX = this.maskCanvas.width, minY = this.maskCanvas.height;
|
||||||
|
var maxX = 0, maxY = 0;
|
||||||
|
var hasSelection = false;
|
||||||
|
|
||||||
|
for (var y = 0; y < this.maskCanvas.height; y++) {
|
||||||
|
for (var x = 0; x < this.maskCanvas.width; x++) {
|
||||||
|
var i = (y * this.maskCanvas.width + x) * 4;
|
||||||
|
if (imageData.data[i] > 128) {
|
||||||
|
hasSelection = true;
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSelection && maxX > minX && maxY > minY) {
|
||||||
|
var scaleX = config.layer.width / config.layer.width_original;
|
||||||
|
var scaleY = config.layer.height / config.layer.height_original;
|
||||||
|
|
||||||
|
this.selectionBounds = {
|
||||||
|
x: config.layer.x + minX * scaleX,
|
||||||
|
y: config.layer.y + minY * scaleY,
|
||||||
|
width: (maxX - minX) * scaleX,
|
||||||
|
height: (maxY - minY) * scaleY,
|
||||||
|
origMinX: minX,
|
||||||
|
origMinY: minY,
|
||||||
|
origMaxX: maxX,
|
||||||
|
origMaxY: maxY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractContourPath() {
|
||||||
|
if (!this.maskCanvas) return;
|
||||||
|
|
||||||
|
var maskCtx = this.maskCanvas.getContext('2d');
|
||||||
|
var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
|
var width = this.maskCanvas.width;
|
||||||
|
var height = this.maskCanvas.height;
|
||||||
|
var data = imageData.data;
|
||||||
|
|
||||||
|
this.edgeCanvas = document.createElement('canvas');
|
||||||
|
this.edgeCanvas.width = width;
|
||||||
|
this.edgeCanvas.height = height;
|
||||||
|
var edgeCtx = this.edgeCanvas.getContext('2d');
|
||||||
|
var edgeImageData = edgeCtx.createImageData(width, height);
|
||||||
|
var edgeData = edgeImageData.data;
|
||||||
|
|
||||||
|
for (var y = 0; y < height; y++) {
|
||||||
|
for (var x = 0; x < width; x++) {
|
||||||
|
var i = (y * width + x) * 4;
|
||||||
|
var isMask = data[i] > 128;
|
||||||
|
|
||||||
|
if (isMask) {
|
||||||
|
var isEdge = false;
|
||||||
|
|
||||||
|
if (x > 0 && data[i - 4] <= 128) isEdge = true;
|
||||||
|
if (x < width - 1 && data[i + 4] <= 128) isEdge = true;
|
||||||
|
if (y > 0 && data[i - width * 4] <= 128) isEdge = true;
|
||||||
|
if (y < height - 1 && data[i + width * 4] <= 128) isEdge = true;
|
||||||
|
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) isEdge = true;
|
||||||
|
|
||||||
|
if (isEdge) {
|
||||||
|
edgeData[i] = 255;
|
||||||
|
edgeData[i + 1] = 255;
|
||||||
|
edgeData[i + 2] = 255;
|
||||||
|
edgeData[i + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeCtx.putImageData(edgeImageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_overlay(ctx) {
|
||||||
|
// Draw current drawing path
|
||||||
|
if (this.isDrawing && this.points.length > 1) {
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
var scaleX = config.layer.width / config.layer.width_original;
|
||||||
|
var scaleY = config.layer.height / config.layer.height_original;
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#00ffff';
|
||||||
|
ctx.lineWidth = 2 / config.ZOOM;
|
||||||
|
ctx.setLineDash([5, 5]);
|
||||||
|
ctx.lineDashOffset = -this.marchingAntsOffset;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(
|
||||||
|
config.layer.x + this.points[0].x * scaleX,
|
||||||
|
config.layer.y + this.points[0].y * scaleY
|
||||||
|
);
|
||||||
|
for (var i = 1; i < this.points.length; i++) {
|
||||||
|
ctx.lineTo(
|
||||||
|
config.layer.x + this.points[i].x * scaleX,
|
||||||
|
config.layer.y + this.points[i].y * scaleY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw existing selection
|
||||||
|
if (!this.currentMask || !this.maskCanvas) return;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// Draw semi-transparent overlay
|
||||||
|
var inverseCanvas = document.createElement('canvas');
|
||||||
|
inverseCanvas.width = this.maskCanvas.width;
|
||||||
|
inverseCanvas.height = this.maskCanvas.height;
|
||||||
|
var inverseCtx = inverseCanvas.getContext('2d');
|
||||||
|
|
||||||
|
inverseCtx.fillStyle = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
inverseCtx.fillRect(0, 0, inverseCanvas.width, inverseCanvas.height);
|
||||||
|
|
||||||
|
inverseCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
inverseCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
inverseCanvas,
|
||||||
|
config.layer.x, config.layer.y,
|
||||||
|
config.layer.width, config.layer.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw marching ants
|
||||||
|
if (this.edgeCanvas) {
|
||||||
|
var antsCanvas = document.createElement('canvas');
|
||||||
|
antsCanvas.width = this.maskCanvas.width;
|
||||||
|
antsCanvas.height = this.maskCanvas.height;
|
||||||
|
var antsCtx = antsCanvas.getContext('2d');
|
||||||
|
|
||||||
|
antsCtx.drawImage(this.edgeCanvas, 0, 0);
|
||||||
|
antsCtx.globalCompositeOperation = 'source-in';
|
||||||
|
|
||||||
|
var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#00ffff' : '#ffffff';
|
||||||
|
antsCtx.fillStyle = color;
|
||||||
|
antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
antsCanvas,
|
||||||
|
config.layer.x, config.layer.y,
|
||||||
|
config.layer.width, config.layer.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToLayer() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to copy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = layer.width_original;
|
||||||
|
canvas.height = layer.height_original;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(layer.link, 0, 0);
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var croppedCanvas = document.createElement('canvas');
|
||||||
|
croppedCanvas.width = cropWidth;
|
||||||
|
croppedCanvas.height = cropHeight;
|
||||||
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
croppedCtx.drawImage(
|
||||||
|
canvas,
|
||||||
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
|
0, 0, cropWidth, cropHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
var scaleX = layer.width / layer.width_original;
|
||||||
|
var scaleY = layer.height / layer.height_original;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
|
width: cropWidth,
|
||||||
|
height: cropHeight,
|
||||||
|
width_original: cropWidth,
|
||||||
|
height_original: cropHeight,
|
||||||
|
type: 'image',
|
||||||
|
name: 'Lasso Selection',
|
||||||
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
|
};
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
|
||||||
|
new app.Actions.Insert_layer_action(params)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
alertify.success('Selection copied to new layer!');
|
||||||
|
}
|
||||||
|
|
||||||
|
cutToLayer() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to cut');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = layer.width_original;
|
||||||
|
canvas.height = layer.height_original;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(layer.link, 0, 0);
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var croppedCanvas = document.createElement('canvas');
|
||||||
|
croppedCanvas.width = cropWidth;
|
||||||
|
croppedCanvas.height = cropHeight;
|
||||||
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
croppedCtx.drawImage(
|
||||||
|
canvas,
|
||||||
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
|
0, 0, cropWidth, cropHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
var scaleX = layer.width / layer.width_original;
|
||||||
|
var scaleY = layer.height / layer.height_original;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
|
width: cropWidth,
|
||||||
|
height: cropHeight,
|
||||||
|
width_original: cropWidth,
|
||||||
|
height_original: cropHeight,
|
||||||
|
type: 'image',
|
||||||
|
name: 'Lasso Cut',
|
||||||
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
|
};
|
||||||
|
|
||||||
|
var holeCanvas = document.createElement('canvas');
|
||||||
|
holeCanvas.width = layer.width_original;
|
||||||
|
holeCanvas.height = layer.height_original;
|
||||||
|
var holeCtx = holeCanvas.getContext('2d');
|
||||||
|
|
||||||
|
holeCtx.drawImage(layer.link, 0, 0);
|
||||||
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
|
||||||
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
|
||||||
|
new app.Actions.Insert_layer_action(params)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearSelection();
|
||||||
|
alertify.success('Selection cut to new layer!');
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelection() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to delete');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var holeCanvas = document.createElement('canvas');
|
||||||
|
holeCanvas.width = layer.width_original;
|
||||||
|
holeCanvas.height = layer.height_original;
|
||||||
|
var holeCtx = holeCanvas.getContext('2d');
|
||||||
|
|
||||||
|
holeCtx.drawImage(layer.link, 0, 0);
|
||||||
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
|
||||||
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearSelection();
|
||||||
|
alertify.success('Selection deleted!');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.currentMask = null;
|
||||||
|
this.maskCanvas = null;
|
||||||
|
this.edgeCanvas = null;
|
||||||
|
this.selectionBounds = null;
|
||||||
|
this.points = [];
|
||||||
|
window.smartSelectMask = null;
|
||||||
|
config.need_render = true;
|
||||||
|
this.Base_layers.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
on_leave() {
|
||||||
|
this.points = [];
|
||||||
|
this.isDrawing = false;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Lasso_class;
|
||||||
@@ -0,0 +1,600 @@
|
|||||||
|
/**
|
||||||
|
* Magic Wand Selection Tool - Selects areas by color similarity
|
||||||
|
* Click to select similar colors, Shift+Click to add to selection
|
||||||
|
* Like GIMP's magic wand / fuzzy select tool
|
||||||
|
*/
|
||||||
|
|
||||||
|
import app from './../app.js';
|
||||||
|
import config from './../config.js';
|
||||||
|
import Base_tools_class from './../core/base-tools.js';
|
||||||
|
import Base_layers_class from './../core/base-layers.js';
|
||||||
|
import Helper_class from './../libs/helpers.js';
|
||||||
|
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||||
|
|
||||||
|
class Magic_wand_class extends Base_tools_class {
|
||||||
|
|
||||||
|
constructor(ctx) {
|
||||||
|
super();
|
||||||
|
this.Base_layers = new Base_layers_class();
|
||||||
|
this.Helper = new Helper_class();
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.name = 'magic_wand';
|
||||||
|
|
||||||
|
// Store the current mask data
|
||||||
|
this.currentMask = null;
|
||||||
|
this.maskCanvas = null;
|
||||||
|
this.selectionBounds = null;
|
||||||
|
|
||||||
|
// Marching ants animation
|
||||||
|
this.marchingAntsOffset = 0;
|
||||||
|
|
||||||
|
// Edge canvas for drawing the mask outline
|
||||||
|
this.edgeCanvas = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
// Mouse click event for selection
|
||||||
|
document.addEventListener('mousedown', function (e) {
|
||||||
|
_this.mousedown(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (config.TOOL.name != _this.name) return;
|
||||||
|
if (_this.Helper.is_input(e.target)) return;
|
||||||
|
|
||||||
|
var code = e.keyCode;
|
||||||
|
|
||||||
|
// Delete - delete selected area
|
||||||
|
if (code == 46 && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.deleteSelection();
|
||||||
|
}
|
||||||
|
// Escape - clear selection
|
||||||
|
if (code == 27 && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.clearSelection();
|
||||||
|
}
|
||||||
|
// Ctrl+C - copy to new layer
|
||||||
|
if (code == 67 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.copyToLayer();
|
||||||
|
}
|
||||||
|
// Ctrl+X - cut to new layer
|
||||||
|
if (code == 88 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
|
||||||
|
e.preventDefault();
|
||||||
|
_this.cutToLayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start marching ants animation
|
||||||
|
this.startMarchingAnts();
|
||||||
|
}
|
||||||
|
|
||||||
|
startMarchingAnts() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
setInterval(function() {
|
||||||
|
if (_this.currentMask) {
|
||||||
|
_this.marchingAntsOffset++;
|
||||||
|
if (_this.marchingAntsOffset > 16) {
|
||||||
|
_this.marchingAntsOffset = 0;
|
||||||
|
}
|
||||||
|
config.need_render = true;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown(e) {
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
|
||||||
|
if (config.TOOL.name != this.name) return;
|
||||||
|
if (mouse.click_valid == false) return;
|
||||||
|
|
||||||
|
// Check if we have an image layer
|
||||||
|
if (config.layer.type != 'image') {
|
||||||
|
alertify.error('Please select an image layer first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get click coordinates relative to the image
|
||||||
|
var x = mouse.x - config.layer.x;
|
||||||
|
var y = mouse.y - config.layer.y;
|
||||||
|
|
||||||
|
// Adjust for layer scaling
|
||||||
|
if (config.layer.width != config.layer.width_original) {
|
||||||
|
x = x * (config.layer.width_original / config.layer.width);
|
||||||
|
}
|
||||||
|
if (config.layer.height != config.layer.height_original) {
|
||||||
|
y = y * (config.layer.height_original / config.layer.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.round(x);
|
||||||
|
y = Math.round(y);
|
||||||
|
|
||||||
|
// Make sure click is within image bounds
|
||||||
|
if (x < 0 || y < 0 || x >= config.layer.width_original || y >= config.layer.height_original) {
|
||||||
|
alertify.error('Click inside the image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Shift key - additive selection
|
||||||
|
var isAdditive = e.shiftKey;
|
||||||
|
|
||||||
|
// Get tool parameters
|
||||||
|
var params = this.getParams();
|
||||||
|
var tolerance = params.tolerance || 30;
|
||||||
|
var contiguous = params.contiguous !== false;
|
||||||
|
|
||||||
|
// Perform the selection
|
||||||
|
this.selectByColor(x, y, tolerance, contiguous, isAdditive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select pixels by color similarity using flood fill algorithm
|
||||||
|
*/
|
||||||
|
selectByColor(startX, startY, tolerance, contiguous, isAdditive) {
|
||||||
|
var layer = config.layer;
|
||||||
|
|
||||||
|
// Get the layer's image data
|
||||||
|
var srcCanvas = document.createElement('canvas');
|
||||||
|
srcCanvas.width = layer.width_original;
|
||||||
|
srcCanvas.height = layer.height_original;
|
||||||
|
var srcCtx = srcCanvas.getContext('2d');
|
||||||
|
srcCtx.drawImage(layer.link, 0, 0);
|
||||||
|
|
||||||
|
var imageData = srcCtx.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
|
||||||
|
var data = imageData.data;
|
||||||
|
var width = srcCanvas.width;
|
||||||
|
var height = srcCanvas.height;
|
||||||
|
|
||||||
|
// Create mask canvas
|
||||||
|
var newMaskCanvas = document.createElement('canvas');
|
||||||
|
newMaskCanvas.width = width;
|
||||||
|
newMaskCanvas.height = height;
|
||||||
|
var maskCtx = newMaskCanvas.getContext('2d');
|
||||||
|
var maskImageData = maskCtx.createImageData(width, height);
|
||||||
|
var maskData = maskImageData.data;
|
||||||
|
|
||||||
|
// Get the color at the clicked point
|
||||||
|
var startIdx = (startY * width + startX) * 4;
|
||||||
|
var targetColor = {
|
||||||
|
r: data[startIdx],
|
||||||
|
g: data[startIdx + 1],
|
||||||
|
b: data[startIdx + 2],
|
||||||
|
a: data[startIdx + 3]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert tolerance to 0-255 range
|
||||||
|
var sens = tolerance * 255 / 100;
|
||||||
|
|
||||||
|
if (contiguous) {
|
||||||
|
// Flood fill - only connected pixels
|
||||||
|
var visited = new Uint8Array(width * height);
|
||||||
|
var stack = [[startX, startY]];
|
||||||
|
var dx = [0, -1, +1, 0];
|
||||||
|
var dy = [-1, 0, 0, +1];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
var point = stack.pop();
|
||||||
|
var px = point[0];
|
||||||
|
var py = point[1];
|
||||||
|
|
||||||
|
if (px < 0 || py < 0 || px >= width || py >= height) continue;
|
||||||
|
|
||||||
|
var idx = py * width + px;
|
||||||
|
if (visited[idx]) continue;
|
||||||
|
visited[idx] = 1;
|
||||||
|
|
||||||
|
var i = idx * 4;
|
||||||
|
|
||||||
|
// Check color similarity
|
||||||
|
if (Math.abs(data[i] - targetColor.r) <= sens &&
|
||||||
|
Math.abs(data[i + 1] - targetColor.g) <= sens &&
|
||||||
|
Math.abs(data[i + 2] - targetColor.b) <= sens &&
|
||||||
|
Math.abs(data[i + 3] - targetColor.a) <= sens) {
|
||||||
|
|
||||||
|
// Add to mask (white = selected)
|
||||||
|
maskData[i] = 255;
|
||||||
|
maskData[i + 1] = 255;
|
||||||
|
maskData[i + 2] = 255;
|
||||||
|
maskData[i + 3] = 255;
|
||||||
|
|
||||||
|
// Add neighbors to stack
|
||||||
|
for (var d = 0; d < 4; d++) {
|
||||||
|
stack.push([px + dx[d], py + dy[d]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Global - all matching pixels regardless of connection
|
||||||
|
for (var y = 0; y < height; y++) {
|
||||||
|
for (var x = 0; x < width; x++) {
|
||||||
|
var i = (y * width + x) * 4;
|
||||||
|
|
||||||
|
if (Math.abs(data[i] - targetColor.r) <= sens &&
|
||||||
|
Math.abs(data[i + 1] - targetColor.g) <= sens &&
|
||||||
|
Math.abs(data[i + 2] - targetColor.b) <= sens &&
|
||||||
|
Math.abs(data[i + 3] - targetColor.a) <= sens) {
|
||||||
|
|
||||||
|
maskData[i] = 255;
|
||||||
|
maskData[i + 1] = 255;
|
||||||
|
maskData[i + 2] = 255;
|
||||||
|
maskData[i + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maskCtx.putImageData(maskImageData, 0, 0);
|
||||||
|
|
||||||
|
// If additive and we have an existing mask, combine them
|
||||||
|
if (isAdditive && this.maskCanvas) {
|
||||||
|
var combinedCanvas = document.createElement('canvas');
|
||||||
|
combinedCanvas.width = width;
|
||||||
|
combinedCanvas.height = height;
|
||||||
|
var combinedCtx = combinedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Draw existing mask
|
||||||
|
combinedCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
// Add new mask
|
||||||
|
combinedCtx.globalCompositeOperation = 'lighter';
|
||||||
|
combinedCtx.drawImage(newMaskCanvas, 0, 0);
|
||||||
|
|
||||||
|
this.maskCanvas = combinedCanvas;
|
||||||
|
} else {
|
||||||
|
this.maskCanvas = newMaskCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentMask = {
|
||||||
|
canvas: this.maskCanvas
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store globally for other tools
|
||||||
|
window.smartSelectMask = this.currentMask;
|
||||||
|
|
||||||
|
// Calculate bounds and extract edge
|
||||||
|
this.calculateSelectionBounds();
|
||||||
|
this.extractContourPath();
|
||||||
|
|
||||||
|
config.need_render = true;
|
||||||
|
this.Base_layers.render();
|
||||||
|
|
||||||
|
if (isAdditive) {
|
||||||
|
alertify.success('Added to selection!');
|
||||||
|
} else {
|
||||||
|
alertify.success('Selection complete! Shift+Click to add more.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateSelectionBounds() {
|
||||||
|
if (!this.maskCanvas) return;
|
||||||
|
|
||||||
|
var maskCtx = this.maskCanvas.getContext('2d');
|
||||||
|
var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
|
|
||||||
|
var minX = this.maskCanvas.width, minY = this.maskCanvas.height;
|
||||||
|
var maxX = 0, maxY = 0;
|
||||||
|
var hasSelection = false;
|
||||||
|
|
||||||
|
for (var y = 0; y < this.maskCanvas.height; y++) {
|
||||||
|
for (var x = 0; x < this.maskCanvas.width; x++) {
|
||||||
|
var i = (y * this.maskCanvas.width + x) * 4;
|
||||||
|
if (imageData.data[i] > 128) {
|
||||||
|
hasSelection = true;
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSelection && maxX > minX && maxY > minY) {
|
||||||
|
var scaleX = config.layer.width / config.layer.width_original;
|
||||||
|
var scaleY = config.layer.height / config.layer.height_original;
|
||||||
|
|
||||||
|
this.selectionBounds = {
|
||||||
|
x: config.layer.x + minX * scaleX,
|
||||||
|
y: config.layer.y + minY * scaleY,
|
||||||
|
width: (maxX - minX) * scaleX,
|
||||||
|
height: (maxY - minY) * scaleY,
|
||||||
|
origMinX: minX,
|
||||||
|
origMinY: minY,
|
||||||
|
origMaxX: maxX,
|
||||||
|
origMaxY: maxY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractContourPath() {
|
||||||
|
if (!this.maskCanvas) return;
|
||||||
|
|
||||||
|
var maskCtx = this.maskCanvas.getContext('2d');
|
||||||
|
var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
|
var width = this.maskCanvas.width;
|
||||||
|
var height = this.maskCanvas.height;
|
||||||
|
var data = imageData.data;
|
||||||
|
|
||||||
|
this.edgeCanvas = document.createElement('canvas');
|
||||||
|
this.edgeCanvas.width = width;
|
||||||
|
this.edgeCanvas.height = height;
|
||||||
|
var edgeCtx = this.edgeCanvas.getContext('2d');
|
||||||
|
var edgeImageData = edgeCtx.createImageData(width, height);
|
||||||
|
var edgeData = edgeImageData.data;
|
||||||
|
|
||||||
|
for (var y = 0; y < height; y++) {
|
||||||
|
for (var x = 0; x < width; x++) {
|
||||||
|
var i = (y * width + x) * 4;
|
||||||
|
var isMask = data[i] > 128;
|
||||||
|
|
||||||
|
if (isMask) {
|
||||||
|
var isEdge = false;
|
||||||
|
|
||||||
|
if (x > 0 && data[i - 4] <= 128) isEdge = true;
|
||||||
|
if (x < width - 1 && data[i + 4] <= 128) isEdge = true;
|
||||||
|
if (y > 0 && data[i - width * 4] <= 128) isEdge = true;
|
||||||
|
if (y < height - 1 && data[i + width * 4] <= 128) isEdge = true;
|
||||||
|
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) isEdge = true;
|
||||||
|
|
||||||
|
if (isEdge) {
|
||||||
|
edgeData[i] = 255;
|
||||||
|
edgeData[i + 1] = 255;
|
||||||
|
edgeData[i + 2] = 255;
|
||||||
|
edgeData[i + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeCtx.putImageData(edgeImageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_overlay(ctx) {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) return;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// Draw semi-transparent overlay on non-selected areas
|
||||||
|
var inverseCanvas = document.createElement('canvas');
|
||||||
|
inverseCanvas.width = this.maskCanvas.width;
|
||||||
|
inverseCanvas.height = this.maskCanvas.height;
|
||||||
|
var inverseCtx = inverseCanvas.getContext('2d');
|
||||||
|
|
||||||
|
inverseCtx.fillStyle = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
inverseCtx.fillRect(0, 0, inverseCanvas.width, inverseCanvas.height);
|
||||||
|
|
||||||
|
inverseCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
inverseCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
inverseCanvas,
|
||||||
|
config.layer.x, config.layer.y,
|
||||||
|
config.layer.width, config.layer.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw marching ants
|
||||||
|
if (this.edgeCanvas) {
|
||||||
|
var antsCanvas = document.createElement('canvas');
|
||||||
|
antsCanvas.width = this.maskCanvas.width;
|
||||||
|
antsCanvas.height = this.maskCanvas.height;
|
||||||
|
var antsCtx = antsCanvas.getContext('2d');
|
||||||
|
|
||||||
|
antsCtx.drawImage(this.edgeCanvas, 0, 0);
|
||||||
|
antsCtx.globalCompositeOperation = 'source-in';
|
||||||
|
|
||||||
|
var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#ff00ff' : '#ffffff';
|
||||||
|
antsCtx.fillStyle = color;
|
||||||
|
antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
antsCanvas,
|
||||||
|
config.layer.x, config.layer.y,
|
||||||
|
config.layer.width, config.layer.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToLayer() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to copy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = layer.width_original;
|
||||||
|
canvas.height = layer.height_original;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(layer.link, 0, 0);
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var croppedCanvas = document.createElement('canvas');
|
||||||
|
croppedCanvas.width = cropWidth;
|
||||||
|
croppedCanvas.height = cropHeight;
|
||||||
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
croppedCtx.drawImage(
|
||||||
|
canvas,
|
||||||
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
|
0, 0, cropWidth, cropHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
var scaleX = layer.width / layer.width_original;
|
||||||
|
var scaleY = layer.height / layer.height_original;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
|
width: cropWidth,
|
||||||
|
height: cropHeight,
|
||||||
|
width_original: cropWidth,
|
||||||
|
height_original: cropHeight,
|
||||||
|
type: 'image',
|
||||||
|
name: 'Magic Wand Selection',
|
||||||
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
|
};
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
|
||||||
|
new app.Actions.Insert_layer_action(params)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
alertify.success('Selection copied to new layer!');
|
||||||
|
}
|
||||||
|
|
||||||
|
cutToLayer() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to cut');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = layer.width_original;
|
||||||
|
canvas.height = layer.height_original;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(layer.link, 0, 0);
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var croppedCanvas = document.createElement('canvas');
|
||||||
|
croppedCanvas.width = cropWidth;
|
||||||
|
croppedCanvas.height = cropHeight;
|
||||||
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
croppedCtx.drawImage(
|
||||||
|
canvas,
|
||||||
|
bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
|
||||||
|
0, 0, cropWidth, cropHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
var scaleX = layer.width / layer.width_original;
|
||||||
|
var scaleY = layer.height / layer.height_original;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
|
width: cropWidth,
|
||||||
|
height: cropHeight,
|
||||||
|
width_original: cropWidth,
|
||||||
|
height_original: cropHeight,
|
||||||
|
type: 'image',
|
||||||
|
name: 'Magic Wand Cut',
|
||||||
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
|
};
|
||||||
|
|
||||||
|
var holeCanvas = document.createElement('canvas');
|
||||||
|
holeCanvas.width = layer.width_original;
|
||||||
|
holeCanvas.height = layer.height_original;
|
||||||
|
var holeCtx = holeCanvas.getContext('2d');
|
||||||
|
|
||||||
|
holeCtx.drawImage(layer.link, 0, 0);
|
||||||
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
|
||||||
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
|
||||||
|
new app.Actions.Insert_layer_action(params)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearSelection();
|
||||||
|
alertify.success('Selection cut to new layer!');
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelection() {
|
||||||
|
if (!this.currentMask || !this.maskCanvas) {
|
||||||
|
alertify.error('No selection to delete');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = config.layer;
|
||||||
|
if (layer.type != 'image') {
|
||||||
|
alertify.error('Layer must be an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var holeCanvas = document.createElement('canvas');
|
||||||
|
holeCanvas.width = layer.width_original;
|
||||||
|
holeCanvas.height = layer.height_original;
|
||||||
|
var holeCtx = holeCanvas.getContext('2d');
|
||||||
|
|
||||||
|
holeCtx.drawImage(layer.link, 0, 0);
|
||||||
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
|
||||||
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearSelection();
|
||||||
|
alertify.success('Selection deleted!');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.currentMask = null;
|
||||||
|
this.maskCanvas = null;
|
||||||
|
this.edgeCanvas = null;
|
||||||
|
this.selectionBounds = null;
|
||||||
|
window.smartSelectMask = null;
|
||||||
|
config.need_render = true;
|
||||||
|
this.Base_layers.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
on_leave() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Magic_wand_class;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Smart Select Tool - Uses SAM (Segment Anything Model) for AI-powered selection
|
* Smart Select Tool - Uses SAM (Segment Anything Model) for AI-powered selection
|
||||||
* Click on any object to automatically select it
|
* Click on any object to automatically select it
|
||||||
|
* Shift+Click to add to existing selection (multi-select)
|
||||||
* Supports: Copy to layer, Cut to layer, Delete selection, AI Inpaint
|
* Supports: Copy to layer, Cut to layer, Delete selection, AI Inpaint
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -31,10 +32,9 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
|
|
||||||
// Marching ants animation
|
// Marching ants animation
|
||||||
this.marchingAntsOffset = 0;
|
this.marchingAntsOffset = 0;
|
||||||
this.animationFrame = null;
|
|
||||||
|
|
||||||
// Contour path for drawing the mask outline
|
// Edge canvas for drawing the mask outline
|
||||||
this.contourPath = null;
|
this.edgeCanvas = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
load() {
|
load() {
|
||||||
@@ -81,18 +81,7 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
startMarchingAnts() {
|
startMarchingAnts() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
|
||||||
var animate = function() {
|
// Animate every 100ms for smooth marching ants
|
||||||
_this.marchingAntsOffset++;
|
|
||||||
if (_this.marchingAntsOffset > 16) {
|
|
||||||
_this.marchingAntsOffset = 0;
|
|
||||||
}
|
|
||||||
if (_this.currentMask) {
|
|
||||||
config.need_render = true;
|
|
||||||
}
|
|
||||||
_this.animationFrame = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Slower animation - every 100ms
|
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
if (_this.currentMask) {
|
if (_this.currentMask) {
|
||||||
_this.marchingAntsOffset++;
|
_this.marchingAntsOffset++;
|
||||||
@@ -138,6 +127,9 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for Shift key - additive selection
|
||||||
|
var isAdditive = e.shiftKey;
|
||||||
|
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
alertify.message('AI is analyzing the image...');
|
alertify.message('AI is analyzing the image...');
|
||||||
|
|
||||||
@@ -148,10 +140,14 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
// Call SAM API
|
// Call SAM API
|
||||||
var result = await apiService.smartSelect(imageData, Math.round(x), Math.round(y));
|
var result = await apiService.smartSelect(imageData, Math.round(x), Math.round(y));
|
||||||
|
|
||||||
// Apply the mask as selection
|
// Apply the mask as selection (additive if Shift is held)
|
||||||
this.applyMask(result.mask, result.bbox);
|
this.applyMask(result.mask, result.bbox, isAdditive);
|
||||||
|
|
||||||
alertify.success('Selection complete! Use Ctrl+C to copy, Ctrl+X to cut, Delete to remove, or AI Inpaint to edit.');
|
if (isAdditive && this.currentMask) {
|
||||||
|
alertify.success('Added to selection! Shift+Click to add more.');
|
||||||
|
} else {
|
||||||
|
alertify.success('Selection complete! Shift+Click to add more, Ctrl+C to copy, Ctrl+X to cut.');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Smart select error:', error);
|
console.error('Smart select error:', error);
|
||||||
@@ -182,19 +178,39 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
* Apply the SAM mask as a selection
|
* Apply the SAM mask as a selection
|
||||||
* @param {string} maskBase64 - Base64 encoded mask image
|
* @param {string} maskBase64 - Base64 encoded mask image
|
||||||
* @param {Object} bbox - Bounding box {x, y, width, height}
|
* @param {Object} bbox - Bounding box {x, y, width, height}
|
||||||
|
* @param {boolean} isAdditive - If true, add to existing selection
|
||||||
*/
|
*/
|
||||||
applyMask(maskBase64, bbox) {
|
applyMask(maskBase64, bbox, isAdditive) {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
|
||||||
// Create mask image
|
// Create mask image
|
||||||
var maskImage = new Image();
|
var maskImage = new Image();
|
||||||
maskImage.onload = function() {
|
maskImage.onload = function() {
|
||||||
// Store mask for later use by inpaint tool
|
// Create new mask canvas
|
||||||
_this.maskCanvas = document.createElement('canvas');
|
var newMaskCanvas = document.createElement('canvas');
|
||||||
_this.maskCanvas.width = config.layer.width_original;
|
newMaskCanvas.width = config.layer.width_original;
|
||||||
_this.maskCanvas.height = config.layer.height_original;
|
newMaskCanvas.height = config.layer.height_original;
|
||||||
var maskCtx = _this.maskCanvas.getContext('2d');
|
var newMaskCtx = newMaskCanvas.getContext('2d');
|
||||||
maskCtx.drawImage(maskImage, 0, 0);
|
newMaskCtx.drawImage(maskImage, 0, 0);
|
||||||
|
|
||||||
|
// If additive and we have an existing mask, combine them
|
||||||
|
if (isAdditive && _this.maskCanvas) {
|
||||||
|
var combinedCanvas = document.createElement('canvas');
|
||||||
|
combinedCanvas.width = config.layer.width_original;
|
||||||
|
combinedCanvas.height = config.layer.height_original;
|
||||||
|
var combinedCtx = combinedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Draw existing mask
|
||||||
|
combinedCtx.drawImage(_this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
// Add new mask using 'lighter' composite to combine white areas
|
||||||
|
combinedCtx.globalCompositeOperation = 'lighter';
|
||||||
|
combinedCtx.drawImage(newMaskCanvas, 0, 0);
|
||||||
|
|
||||||
|
_this.maskCanvas = combinedCanvas;
|
||||||
|
} else {
|
||||||
|
_this.maskCanvas = newMaskCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
_this.currentMask = {
|
_this.currentMask = {
|
||||||
canvas: _this.maskCanvas,
|
canvas: _this.maskCanvas,
|
||||||
@@ -325,10 +341,6 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
// Scale to match layer
|
|
||||||
var scaleX = config.layer.width / config.layer.width_original;
|
|
||||||
var scaleY = config.layer.height / config.layer.height_original;
|
|
||||||
|
|
||||||
// Draw semi-transparent overlay on non-selected areas
|
// Draw semi-transparent overlay on non-selected areas
|
||||||
var inverseCanvas = document.createElement('canvas');
|
var inverseCanvas = document.createElement('canvas');
|
||||||
inverseCanvas.width = this.maskCanvas.width;
|
inverseCanvas.width = this.maskCanvas.width;
|
||||||
@@ -358,28 +370,15 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
antsCanvas.height = this.maskCanvas.height;
|
antsCanvas.height = this.maskCanvas.height;
|
||||||
var antsCtx = antsCanvas.getContext('2d');
|
var antsCtx = antsCanvas.getContext('2d');
|
||||||
|
|
||||||
// Draw the edge in white (visible part of marching ants)
|
// Draw the edge
|
||||||
antsCtx.drawImage(this.edgeCanvas, 0, 0);
|
antsCtx.drawImage(this.edgeCanvas, 0, 0);
|
||||||
|
|
||||||
// Apply marching ants pattern using composite
|
// Apply marching ants color using composite
|
||||||
antsCtx.globalCompositeOperation = 'source-in';
|
antsCtx.globalCompositeOperation = 'source-in';
|
||||||
|
|
||||||
// Create marching ants pattern (alternating black and white)
|
// Alternate color based on animation offset
|
||||||
var pattern = antsCtx.createLinearGradient(0, 0, 16, 16);
|
var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#00ff00' : '#ffffff';
|
||||||
var offset = this.marchingAntsOffset / 16;
|
antsCtx.fillStyle = color;
|
||||||
|
|
||||||
// Create dashed pattern
|
|
||||||
for (var i = 0; i < 2; i++) {
|
|
||||||
var pos1 = ((i * 0.5) + offset) % 1;
|
|
||||||
var pos2 = ((i * 0.5 + 0.25) + offset) % 1;
|
|
||||||
|
|
||||||
if (pos1 < pos2) {
|
|
||||||
pattern.addColorStop(pos1, '#00ff00');
|
|
||||||
pattern.addColorStop(pos2, '#00ff00');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
antsCtx.fillStyle = '#00ff00';
|
|
||||||
antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
|
antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
|
||||||
|
|
||||||
// Draw the marching ants outline
|
// Draw the marching ants outline
|
||||||
@@ -388,20 +387,6 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
config.layer.x, config.layer.y,
|
config.layer.x, config.layer.y,
|
||||||
config.layer.width, config.layer.height
|
config.layer.width, config.layer.height
|
||||||
);
|
);
|
||||||
|
|
||||||
// Also draw a second pass with offset for alternating colors
|
|
||||||
var antsCanvas2 = document.createElement('canvas');
|
|
||||||
antsCanvas2.width = this.maskCanvas.width;
|
|
||||||
antsCanvas2.height = this.maskCanvas.height;
|
|
||||||
var antsCtx2 = antsCanvas2.getContext('2d');
|
|
||||||
|
|
||||||
// Dilate the edge slightly for the black outline
|
|
||||||
antsCtx2.drawImage(this.edgeCanvas, 0, 0);
|
|
||||||
antsCtx2.globalCompositeOperation = 'source-in';
|
|
||||||
|
|
||||||
// Alternate pattern
|
|
||||||
antsCtx2.fillStyle = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#ffffff' : '#000000';
|
|
||||||
antsCtx2.fillRect(0, 0, antsCanvas2.width, antsCanvas2.height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -422,6 +407,13 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the bounds of the selection
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || !bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create canvas with just the selected pixels
|
// Create canvas with just the selected pixels
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = layer.width_original;
|
canvas.width = layer.width_original;
|
||||||
@@ -435,17 +427,16 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
ctx.globalCompositeOperation = 'destination-in';
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
ctx.drawImage(this.maskCanvas, 0, 0);
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
// Get the bounds of the selection to crop the canvas
|
// Crop to selection bounds
|
||||||
var bounds = this.selectionBounds;
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
if (!bounds) {
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
alertify.error('Invalid selection bounds');
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crop to selection bounds
|
|
||||||
var croppedCanvas = document.createElement('canvas');
|
var croppedCanvas = document.createElement('canvas');
|
||||||
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
|
||||||
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
|
||||||
croppedCanvas.width = cropWidth;
|
croppedCanvas.width = cropWidth;
|
||||||
croppedCanvas.height = cropHeight;
|
croppedCanvas.height = cropHeight;
|
||||||
var croppedCtx = croppedCanvas.getContext('2d');
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
@@ -460,12 +451,12 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
var scaleX = layer.width / layer.width_original;
|
var scaleX = layer.width / layer.width_original;
|
||||||
var scaleY = layer.height / layer.height_original;
|
var scaleY = layer.height / layer.height_original;
|
||||||
|
|
||||||
// Create new layer with the selection
|
// Create new layer with the selection - use data as dataURL string
|
||||||
var params = {
|
var params = {
|
||||||
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
width: Math.round(cropWidth * scaleX),
|
width: cropWidth,
|
||||||
height: Math.round(cropHeight * scaleY),
|
height: cropHeight,
|
||||||
width_original: cropWidth,
|
width_original: cropWidth,
|
||||||
height_original: cropHeight,
|
height_original: cropHeight,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
@@ -475,7 +466,7 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
|
|
||||||
app.State.do_action(
|
app.State.do_action(
|
||||||
new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
|
new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
|
||||||
new app.Actions.Insert_layer_action(params, false)
|
new app.Actions.Insert_layer_action(params)
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -497,7 +488,14 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First copy to new layer
|
// Get the bounds of the selection
|
||||||
|
var bounds = this.selectionBounds;
|
||||||
|
if (!bounds || bounds.origMinX === undefined) {
|
||||||
|
alertify.error('Invalid selection bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create canvas with just the selected pixels
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = layer.width_original;
|
canvas.width = layer.width_original;
|
||||||
canvas.height = layer.height_original;
|
canvas.height = layer.height_original;
|
||||||
@@ -510,17 +508,16 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
ctx.globalCompositeOperation = 'destination-in';
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
ctx.drawImage(this.maskCanvas, 0, 0);
|
ctx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
// Get the bounds of the selection
|
// Crop to selection bounds
|
||||||
var bounds = this.selectionBounds;
|
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
||||||
if (!bounds) {
|
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
||||||
alertify.error('Invalid selection bounds');
|
|
||||||
|
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||||
|
alertify.error('Selection is too small');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crop to selection bounds
|
|
||||||
var croppedCanvas = document.createElement('canvas');
|
var croppedCanvas = document.createElement('canvas');
|
||||||
var cropWidth = bounds.origMaxX - bounds.origMinX;
|
|
||||||
var cropHeight = bounds.origMaxY - bounds.origMinY;
|
|
||||||
croppedCanvas.width = cropWidth;
|
croppedCanvas.width = cropWidth;
|
||||||
croppedCanvas.height = cropHeight;
|
croppedCanvas.height = cropHeight;
|
||||||
var croppedCtx = croppedCanvas.getContext('2d');
|
var croppedCtx = croppedCanvas.getContext('2d');
|
||||||
@@ -539,8 +536,8 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
var params = {
|
var params = {
|
||||||
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
x: Math.round(layer.x + bounds.origMinX * scaleX),
|
||||||
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
y: Math.round(layer.y + bounds.origMinY * scaleY),
|
||||||
width: Math.round(cropWidth * scaleX),
|
width: cropWidth,
|
||||||
height: Math.round(cropHeight * scaleY),
|
height: cropHeight,
|
||||||
width_original: cropWidth,
|
width_original: cropWidth,
|
||||||
height_original: cropHeight,
|
height_original: cropHeight,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
@@ -548,7 +545,7 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
data: croppedCanvas.toDataURL('image/png')
|
data: croppedCanvas.toDataURL('image/png')
|
||||||
};
|
};
|
||||||
|
|
||||||
// Now delete from original - create canvas with hole
|
// Create canvas with hole where selection was
|
||||||
var holeCanvas = document.createElement('canvas');
|
var holeCanvas = document.createElement('canvas');
|
||||||
holeCanvas.width = layer.width_original;
|
holeCanvas.width = layer.width_original;
|
||||||
holeCanvas.height = layer.height_original;
|
holeCanvas.height = layer.height_original;
|
||||||
@@ -561,11 +558,11 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
holeCtx.globalCompositeOperation = 'destination-out';
|
holeCtx.globalCompositeOperation = 'destination-out';
|
||||||
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
holeCtx.drawImage(this.maskCanvas, 0, 0);
|
||||||
|
|
||||||
// Execute both actions
|
// Execute both actions - update original layer, then insert new layer
|
||||||
app.State.do_action(
|
app.State.do_action(
|
||||||
new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
|
new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
|
||||||
new app.Actions.Update_layer_image_action(holeCanvas),
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
|
||||||
new app.Actions.Insert_layer_action(params, false)
|
new app.Actions.Insert_layer_action(params)
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -605,7 +602,7 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
|
|
||||||
app.State.do_action(
|
app.State.do_action(
|
||||||
new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
|
new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
|
||||||
new app.Actions.Update_layer_image_action(holeCanvas)
|
new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -623,7 +620,6 @@ class Smart_select_class extends Base_tools_class {
|
|||||||
this.maskCanvas = null;
|
this.maskCanvas = null;
|
||||||
this.edgeCanvas = null;
|
this.edgeCanvas = null;
|
||||||
this.selectionBounds = null;
|
this.selectionBounds = null;
|
||||||
this.contourPath = null;
|
|
||||||
window.smartSelectMask = null;
|
window.smartSelectMask = null;
|
||||||
config.need_render = true;
|
config.need_render = true;
|
||||||
this.Base_layers.render();
|
this.Base_layers.render();
|
||||||
|
|||||||
Reference in New Issue
Block a user