pixel-editor/js/tools/SelectionTool.js
2022-01-09 23:42:37 +01:00

389 lines
14 KiB
JavaScript

/** TODO
* - Once the selected pixels have been obtained, save the selection outline in an image data
* - The move tool will then move those image datas and they'll be pasted on the right layer
* at the end of the selection
*
*/
class SelectionTool extends Tool {
switchFunc = undefined;
moveTool = undefined;
boundingBox = {minX: 9999999, maxX: -1, minY: 9999999, maxY: -1};
currSelection = {};
outlineData = undefined;
previewData = undefined;
selectedPixel = undefined;
moveOffset = [0, 0];
boundingBoxCenter = [0,0];
reconstruct = {left:false, right:false, top:false, bottom:false};
constructor(name, options, switchFunc, moveTool) {
super(name, options);
this.moveTool = moveTool;
this.switchFunc = switchFunc;
}
onStart(mousePos) {
super.onStart(mousePos);
// Putting the vfx layer on top of everything
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
currFile.VFXLayer.context.fillStyle = "rgba(0,0,0,1)";
this.startMousePos = [Math.floor(mousePos[0] / currFile.zoom),
Math.floor(mousePos[1] / currFile.zoom)];
this.endMousePos = [this.startMousePos[0], this.startMousePos[1]];
let mouseX = mousePos[0] / currFile.zoom;
let mouseY = mousePos[1] / currFile.zoom;
this.boundingBox = {minX: 9999999, maxX: -1, minY: 9999999, maxY: -1};
this.reconstruct = {left:false, right:false, top:false, bottom:false};
this.currSelection = {};
this.moveOffset = [0, 0];
this.updateBoundingBox(Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1),
Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1));
}
onDrag(mousePos) {
super.onDrag(mousePos);
let mouseX = mousePos[0] / currFile.zoom;
let mouseY = mousePos[1] / currFile.zoom;
if (mouseX > currFile.canvasSize[0])
this.reconstruct.right = true;
else if (mouseX < 0)
this.reconstruct.left = true;
if (mouseY > currFile.canvasSize[1])
this.reconstruct.bottom = true;
else if (mouseY < 0)
this.reconstruct.top = true;
this.updateBoundingBox(Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1),
Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1));
}
onEnd(mousePos) {
super.onEnd(mousePos);
let mouseX = mousePos[0] / currFile.zoom;
let mouseY = mousePos[1] / currFile.zoom;
this.updateBoundingBox(Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1),
Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1));
this.boundingBoxCenter = [this.boundingBox.minX + (this.boundingBox.maxX - this.boundingBox.minX) / 2,
this.boundingBox.minY + (this.boundingBox.maxY - this.boundingBox.minY) / 2];
}
cutSelection() {
let currLayerData = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]).data;
// Save the selected pixels so that they can be moved and pasted back in the right place
for (const key in this.currSelection) {
let x = parseInt(key.split(",")[0]);
let y = parseInt(key.split(",")[1]);
let index = (y * currFile.canvasSize[1] + x) * 4;
for (let i=0; i<4; i++) {
// Save the pixel
this.previewData.data[index + i] = currLayerData[index + i];
// Delete the data below
currLayerData[index + i] = 0;
}
}
currFile.currentLayer.context.putImageData(new ImageData(currLayerData, currFile.canvasSize[0]), 0, 0);
}
pasteSelection(){
if (this.currSelection == undefined)
return;
// I have to save the underlying data, so that the transparent pixels in the clipboard
// don't override the coloured pixels in the canvas
let underlyingImageData = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
let pasteData = currFile.TMPLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
// Clearing the tmp (move preview) and vfx (ants) layers
currFile.TMPLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.VFXLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
for (let i=0; i<underlyingImageData.data.length; i+=4) {
let currentMovePixel = [
pasteData.data[i], pasteData.data[i+1], pasteData.data[i+2], pasteData.data[i+3]
];
let currentUnderlyingPixel = [
underlyingImageData.data[i], underlyingImageData.data[i+1],
underlyingImageData.data[i+2], underlyingImageData.data[i+3]
];
// If the pixel of the clipboard is empty, but the one below it isn't, I use the pixel below
if (Util.isPixelEmpty(currentMovePixel)) {
if (!Util.isPixelEmpty(currentUnderlyingPixel)) {
pasteData.data[i] = currentUnderlyingPixel[0];
pasteData.data[i+1] = currentUnderlyingPixel[1];
pasteData.data[i+2] = currentUnderlyingPixel[2];
pasteData.data[i+3] = currentUnderlyingPixel[3];
}
}
}
console.log(currFile.canvasSize[0]);
currFile.currentLayer.context.putImageData(pasteData, 0, 0);
currFile.currentLayer.updateLayerPreview();
currFile.VFXLayer.canvas.style.zIndex = MIN_Z_INDEX;
}
copySelection() {
}
cursorInSelectedArea(mousePos) {
let floored = [Math.floor(mousePos[0] / currFile.zoom) - this.moveOffset[0],
Math.floor(mousePos[1] / currFile.zoom) - this.moveOffset[1]];
if (this.currSelection[floored] != undefined)
return true;
return false;
}
visit(pixel, visited, data) {
let toVisit = [pixel];
let selected = [];
let currVisited = {};
currFile.TMPLayer.context.clearRect(0, 0, 512, 512);
while (toVisit.length > 0) {
pixel = toVisit.pop();
selected.push(pixel);
visited[pixel] = true;
currVisited[pixel] = true;
let col = Util.getPixelColor(data, pixel[0], pixel[1], currFile.canvasSize[0]);
if (col[3] == 255)
continue;
if (this.isBorderOfBox(pixel))
return [];
let top, bottom, left, right;
if (pixel[1] > 0)
top = [pixel[0], pixel[1] - 1];
else
top = undefined;
if (pixel[0] > 0)
left = [pixel[0] - 1, pixel[1]];
else
left = undefined;
if (pixel[1] < currFile.canvasSize[1])
bottom = [pixel[0], pixel[1] + 1];
else
bottom = undefined;
if (pixel[0] < currFile.canvasSize[0])
right = [pixel[0] + 1, pixel[1]];
else
right = undefined;
if (right != undefined && currVisited[right] == undefined)
toVisit.push(right);
if (left != undefined && currVisited[left] == undefined)
toVisit.push(left);
if (top != undefined && currVisited[top] == undefined)
toVisit.push(top);
if (bottom != undefined && currVisited[bottom] == undefined)
toVisit.push(bottom);
}
return selected;
}
getSelection() {
let selected = [];
let visited = {};
let data = currFile.VFXLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]).data;
data = this.reconstructSelection(data);
// BFS: a pixel that causes the algorithm to visit a pixel of the bounding box is outside the
// selection. Otherwise, since the algorithm stops visiting when it reaches the outline,
// the pixel is inside the selection (and so are all the ones that have been visited)
for (let x=this.boundingBox.minX-1; x<=this.boundingBox.maxX+1; x++) {
for (let y=this.boundingBox.minY-1; y<=this.boundingBox.maxY+1; y++) {
if (visited[[x, y]] == undefined) {
let insidePixels = this.visit([x,y], visited, data);
for (let i=0; i<insidePixels.length; i++) {
selected.push(insidePixels[i]);
this.currSelection[insidePixels[i]] = true;
}
}
}
}
// Save the selection outline
this.outlineData = currFile.VFXLayer.context.getImageData(this.boundingBox.minX,
this.boundingBox.minY, this.boundingBox.maxX - this.boundingBox.minX,
this.boundingBox.maxY - this.boundingBox.minY);
// Create the image data containing the selected pixels
this.previewData = new ImageData(currFile.canvasSize[0], currFile.canvasSize[1]);
// Cut the selection
this.cutSelection();
// Put it on the TMP layer
currFile.TMPLayer.context.putImageData(this.previewData, 0, 0);
this.drawSelectedArea();
return this.previewData;
}
reconstructSelection(data) {
let x, y, index;
let fill = false;
if (this.reconstruct.top) {
y = Math.max(this.boundingBox.minY, 0);
this.boundingBox.minY = y;
for (x=this.boundingBox.minX; x<this.boundingBox.maxX; x++) {
index = (y * currFile.canvasSize[1] + x) * 4;
if (data[index + 3] == 255)
fill = !fill;
if (fill) {
data[index] = 0;
data[index+1] = 0;
data[index+2] = 0;
data[index+3] = 255;
currFile.VFXLayer.context.fillRect(x, y, 1, 1);
}
}
}
if (this.reconstruct.left) {
x = Math.max(this.boundingBox.minX, 0);
this.boundingBox.minX = x;
let startPixel = data[(this.boundingBox.minY * currFile.canvasSize[1] + x) * 4];
if ((this.reconstruct.top || this.reconstruct.bottom) && startPixel[3] == 255)
fill = !fill;
for (y=this.boundingBox.minY; y<this.boundingBox.maxY; y++) {
index = (y * currFile.canvasSize[1] + x) * 4;
if (data[index + 3] == 255)
fill = !fill;
if (fill) {
data[index] = 0;
data[index+1] = 0;
data[index+2] = 0;
data[index+3] = 255;
currFile.VFXLayer.context.fillRect(x, y, 1, 1);
}
}
}
if (this.reconstruct.bottom) {
y = Math.min(this.boundingBox.maxY, currFile.canvasSize[1]-1);
this.boundingBox.maxY = y+1;
let startPixel = data[(y * currFile.canvasSize[1] + this.boundingBox.minX) * 4];
if ((this.reconstruct.left || this.reconstruct.right) && startPixel[3] == 255)
fill = !fill;
for (x=this.boundingBox.minX; x<this.boundingBox.maxX; x++) {
index = (y * currFile.canvasSize[1] + x) * 4;
if (data[index + 3] == 255)
fill = !fill;
if (fill) {
data[index] = 0;
data[index+1] = 0;
data[index+2] = 0;
data[index+3] = 255;
currFile.VFXLayer.context.fillRect(x, y, 1, 1);
}
}
}
if (this.reconstruct.right) {
x = Math.min(this.boundingBox.maxX, currFile.canvasSize[0]-1);
this.boundingBox.maxX = x+1;
let startPixel = data[(this.boundingBox.minY * currFile.canvasSize[1] + x) * 4];
if ((this.reconstruct.top || this.reconstruct.bottom) && startPixel[3] == 255)
fill = !fill;
for (y=this.boundingBox.minY; y<this.boundingBox.maxY; y++) {
index = (y * currFile.canvasSize[1] + x) * 4;
if (data[index + 3] == 255)
fill = !fill;
if (fill) {
data[index] = 0;
data[index+1] = 0;
data[index+2] = 0;
data[index+3] = 255;
currFile.VFXLayer.context.fillRect(x, y, 1, 1);
}
}
}
return data;
}
drawSelectedArea() {
for (const key in this.currSelection) {
let x = parseInt(key.split(",")[0]);
let y = parseInt(key.split(",")[1]);
currFile.VFXLayer.context.fillStyle = "rgba(10, 0, 40, 0.3)";
currFile.VFXLayer.context.fillRect(x + this.moveOffset[0], y + this.moveOffset[1], 1, 1);
}
}
drawOutline() {
currFile.VFXLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.VFXLayer.context.putImageData(this.outlineData, this.boundingBox.minX + this.moveOffset[0],
this.boundingBox.minY + this.moveOffset[1]);
}
isBorderOfBox(pixel) {
return pixel[0] == this.boundingBox.minX || pixel[0] == this.boundingBox.maxX ||
pixel[1] == this.boundingBox.minY || pixel[1] == this.boundingBox.maxY;
}
updateBoundingBox(mouseX, mouseY) {
if (mouseX > this.boundingBox.maxX)
this.boundingBox.maxX = Math.floor(mouseX);
if (mouseX < this.boundingBox.minX)
this.boundingBox.minX = Math.floor(mouseX);
if (mouseY < this.boundingBox.minY)
this.boundingBox.minY = Math.floor(mouseY);
if (mouseY > this.boundingBox.maxY)
this.boundingBox.maxY = Math.floor(mouseY);
}
}