mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 06:22:14 -03:00
Add ShapeTool for custom output area selection
Introduces ShapeTool to allow users to define custom polygonal output areas by holding Shift+S and clicking to add points. The selected shape is used to crop and mask images and layers, and is visualized on the canvas. Updates Canvas, CanvasIO, CanvasInteractions, CanvasLayers, CanvasRenderer, and types to support shape-based output areas, including shape-aware import, export, and rendering logic.
This commit is contained in:
30
js/Canvas.js
30
js/Canvas.js
@@ -1,6 +1,7 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { MaskTool } from "./MaskTool.js";
|
import { MaskTool } from "./MaskTool.js";
|
||||||
|
import { ShapeTool } from "./ShapeTool.js";
|
||||||
import { CanvasState } from "./CanvasState.js";
|
import { CanvasState } from "./CanvasState.js";
|
||||||
import { CanvasInteractions } from "./CanvasInteractions.js";
|
import { CanvasInteractions } from "./CanvasInteractions.js";
|
||||||
import { CanvasLayers } from "./CanvasLayers.js";
|
import { CanvasLayers } from "./CanvasLayers.js";
|
||||||
@@ -62,6 +63,8 @@ export class Canvas {
|
|||||||
this.imageCache = new Map();
|
this.imageCache = new Map();
|
||||||
this.requestSaveState = () => { };
|
this.requestSaveState = () => { };
|
||||||
this.maskTool = new MaskTool(this, { onStateChange: this.onStateChange });
|
this.maskTool = new MaskTool(this, { onStateChange: this.onStateChange });
|
||||||
|
this.shapeTool = new ShapeTool(this);
|
||||||
|
this.outputAreaShape = null;
|
||||||
this.canvasMask = new CanvasMask(this);
|
this.canvasMask = new CanvasMask(this);
|
||||||
this.canvasState = new CanvasState(this);
|
this.canvasState = new CanvasState(this);
|
||||||
this.canvasSelection = new CanvasSelection(this);
|
this.canvasSelection = new CanvasSelection(this);
|
||||||
@@ -295,6 +298,33 @@ export class Canvas {
|
|||||||
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
|
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
|
||||||
return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
|
return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
|
||||||
}
|
}
|
||||||
|
defineOutputAreaWithShape(shape) {
|
||||||
|
const boundingBox = this.shapeTool.getBoundingBox();
|
||||||
|
if (boundingBox && boundingBox.width > 1 && boundingBox.height > 1) {
|
||||||
|
this.saveState();
|
||||||
|
this.outputAreaShape = {
|
||||||
|
...shape,
|
||||||
|
points: shape.points.map(p => ({
|
||||||
|
x: p.x - boundingBox.x,
|
||||||
|
y: p.y - boundingBox.y
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
const newWidth = Math.round(boundingBox.width);
|
||||||
|
const newHeight = Math.round(boundingBox.height);
|
||||||
|
const finalX = boundingBox.x;
|
||||||
|
const finalY = boundingBox.y;
|
||||||
|
this.updateOutputAreaSize(newWidth, newHeight, false);
|
||||||
|
this.layers.forEach((layer) => {
|
||||||
|
layer.x -= finalX;
|
||||||
|
layer.y -= finalY;
|
||||||
|
});
|
||||||
|
this.maskTool.updatePosition(-finalX, -finalY);
|
||||||
|
this.viewport.x -= finalX;
|
||||||
|
this.viewport.y -= finalY;
|
||||||
|
this.saveState();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Zmienia rozmiar obszaru wyjściowego
|
* Zmienia rozmiar obszaru wyjściowego
|
||||||
* @param {number} width - Nowa szerokość
|
* @param {number} width - Nowa szerokość
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export class CanvasIO {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
|
const originalShape = this.canvas.outputAreaShape;
|
||||||
|
this.canvas.outputAreaShape = null;
|
||||||
const visibilityCanvas = document.createElement('canvas');
|
const visibilityCanvas = document.createElement('canvas');
|
||||||
visibilityCanvas.width = this.canvas.width;
|
visibilityCanvas.width = this.canvas.width;
|
||||||
visibilityCanvas.height = this.canvas.height;
|
visibilityCanvas.height = this.canvas.height;
|
||||||
@@ -74,6 +76,7 @@ export class CanvasIO {
|
|||||||
maskData.data[i + 3] = 255;
|
maskData.data[i + 3] = 255;
|
||||||
}
|
}
|
||||||
maskCtx.putImageData(maskData, 0, 0);
|
maskCtx.putImageData(maskData, 0, 0);
|
||||||
|
this.canvas.outputAreaShape = originalShape;
|
||||||
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
||||||
if (toolMaskCanvas) {
|
if (toolMaskCanvas) {
|
||||||
const tempMaskCanvas = document.createElement('canvas');
|
const tempMaskCanvas = document.createElement('canvas');
|
||||||
@@ -204,6 +207,8 @@ export class CanvasIO {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
|
const originalShape = this.canvas.outputAreaShape;
|
||||||
|
this.canvas.outputAreaShape = null;
|
||||||
const visibilityCanvas = document.createElement('canvas');
|
const visibilityCanvas = document.createElement('canvas');
|
||||||
visibilityCanvas.width = this.canvas.width;
|
visibilityCanvas.width = this.canvas.width;
|
||||||
visibilityCanvas.height = this.canvas.height;
|
visibilityCanvas.height = this.canvas.height;
|
||||||
@@ -260,6 +265,7 @@ export class CanvasIO {
|
|||||||
}
|
}
|
||||||
const imageDataUrl = tempCanvas.toDataURL('image/png');
|
const imageDataUrl = tempCanvas.toDataURL('image/png');
|
||||||
const maskDataUrl = maskCanvas.toDataURL('image/png');
|
const maskDataUrl = maskCanvas.toDataURL('image/png');
|
||||||
|
this.canvas.outputAreaShape = originalShape;
|
||||||
resolve({ image: imageDataUrl, mask: maskDataUrl });
|
resolve({ image: imageDataUrl, mask: maskDataUrl });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -656,7 +662,12 @@ export class CanvasIO {
|
|||||||
img.onerror = reject;
|
img.onerror = reject;
|
||||||
img.src = imageData;
|
img.src = imageData;
|
||||||
});
|
});
|
||||||
const newLayer = await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit', targetArea);
|
let processedImage = img;
|
||||||
|
// If there's a custom shape, clip the image to that shape
|
||||||
|
if (this.canvas.outputAreaShape && this.canvas.outputAreaShape.isClosed) {
|
||||||
|
processedImage = await this.clipImageToShape(img, this.canvas.outputAreaShape);
|
||||||
|
}
|
||||||
|
const newLayer = await this.canvas.canvasLayers.addLayerWithImage(processedImage, {}, 'fit', targetArea);
|
||||||
newLayers.push(newLayer);
|
newLayers.push(newLayer);
|
||||||
}
|
}
|
||||||
log.info("All new images imported and placed on canvas successfully.");
|
log.info("All new images imported and placed on canvas successfully.");
|
||||||
@@ -676,4 +687,51 @@ export class CanvasIO {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async clipImageToShape(image, shape) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { canvas, ctx } = createCanvas(image.width, image.height);
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error("Could not create canvas context for clipping"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Draw the image first
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
// Create a clipping mask using the shape
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||||
|
for (let i = 1; i < shape.points.length; i++) {
|
||||||
|
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
// Create a new image from the clipped canvas
|
||||||
|
const clippedImage = new Image();
|
||||||
|
clippedImage.onload = () => resolve(clippedImage);
|
||||||
|
clippedImage.onerror = () => reject(new Error("Failed to create clipped image"));
|
||||||
|
clippedImage.src = canvas.toDataURL();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
createMaskFromShape(shape, width, height) {
|
||||||
|
const { canvas, ctx } = createCanvas(width, height);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("Could not create canvas context for mask");
|
||||||
|
}
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||||
|
for (let i = 1; i < shape.points.length; i++) {
|
||||||
|
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
const imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
const maskData = new Float32Array(width * height);
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
maskData[i / 4] = imageData.data[i] / 255;
|
||||||
|
}
|
||||||
|
return maskData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export class CanvasInteractions {
|
|||||||
canvasResizeStart: { x: 0, y: 0 },
|
canvasResizeStart: { x: 0, y: 0 },
|
||||||
isCtrlPressed: false,
|
isCtrlPressed: false,
|
||||||
isAltPressed: false,
|
isAltPressed: false,
|
||||||
|
isShiftPressed: false,
|
||||||
|
isSPressed: false,
|
||||||
hasClonedInDrag: false,
|
hasClonedInDrag: false,
|
||||||
lastClickTime: 0,
|
lastClickTime: 0,
|
||||||
transformingLayer: null,
|
transformingLayer: null,
|
||||||
@@ -67,6 +69,10 @@ export class CanvasInteractions {
|
|||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.canvas.shapeTool.isActive) {
|
||||||
|
this.canvas.shapeTool.addPoint(worldCoords);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
||||||
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
||||||
if (e.shiftKey && e.ctrlKey) {
|
if (e.shiftKey && e.ctrlKey) {
|
||||||
@@ -74,6 +80,11 @@ export class CanvasInteractions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
|
// Clear custom shape when starting canvas resize
|
||||||
|
if (this.canvas.outputAreaShape) {
|
||||||
|
this.canvas.outputAreaShape = null;
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
this.startCanvasResize(worldCoords);
|
this.startCanvasResize(worldCoords);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -295,10 +306,22 @@ export class CanvasInteractions {
|
|||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
if (e.key === 'Control')
|
if (e.key === 'Control')
|
||||||
this.interaction.isCtrlPressed = true;
|
this.interaction.isCtrlPressed = true;
|
||||||
|
if (e.key === 'Shift')
|
||||||
|
this.interaction.isShiftPressed = true;
|
||||||
if (e.key === 'Alt') {
|
if (e.key === 'Alt') {
|
||||||
this.interaction.isAltPressed = true;
|
this.interaction.isAltPressed = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
if (e.key.toLowerCase() === 's') {
|
||||||
|
this.interaction.isSPressed = true;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
// Check if Shift+S is being held down
|
||||||
|
if (this.interaction.isShiftPressed && this.interaction.isSPressed && !this.interaction.isCtrlPressed && !this.canvas.shapeTool.isActive) {
|
||||||
|
this.canvas.shapeTool.activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Globalne skróty (Undo/Redo/Copy/Paste)
|
// Globalne skróty (Undo/Redo/Copy/Paste)
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
@@ -367,8 +390,16 @@ export class CanvasInteractions {
|
|||||||
handleKeyUp(e) {
|
handleKeyUp(e) {
|
||||||
if (e.key === 'Control')
|
if (e.key === 'Control')
|
||||||
this.interaction.isCtrlPressed = false;
|
this.interaction.isCtrlPressed = false;
|
||||||
|
if (e.key === 'Shift')
|
||||||
|
this.interaction.isShiftPressed = false;
|
||||||
if (e.key === 'Alt')
|
if (e.key === 'Alt')
|
||||||
this.interaction.isAltPressed = false;
|
this.interaction.isAltPressed = false;
|
||||||
|
if (e.key.toLowerCase() === 's')
|
||||||
|
this.interaction.isSPressed = false;
|
||||||
|
// Deactivate shape tool when Shift or S is released
|
||||||
|
if (this.canvas.shapeTool.isActive && (!this.interaction.isShiftPressed || !this.interaction.isSPressed)) {
|
||||||
|
this.canvas.shapeTool.deactivate();
|
||||||
|
}
|
||||||
const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight'];
|
const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight'];
|
||||||
if (movementKeys.includes(e.code) && this.interaction.keyMovementInProgress) {
|
if (movementKeys.includes(e.code) && this.interaction.keyMovementInProgress) {
|
||||||
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
|
this.canvas.requestSaveState(); // Użyj opóźnionego zapisu
|
||||||
@@ -379,7 +410,13 @@ export class CanvasInteractions {
|
|||||||
log.debug('Window lost focus, resetting key states.');
|
log.debug('Window lost focus, resetting key states.');
|
||||||
this.interaction.isCtrlPressed = false;
|
this.interaction.isCtrlPressed = false;
|
||||||
this.interaction.isAltPressed = false;
|
this.interaction.isAltPressed = false;
|
||||||
|
this.interaction.isShiftPressed = false;
|
||||||
|
this.interaction.isSPressed = false;
|
||||||
this.interaction.keyMovementInProgress = false;
|
this.interaction.keyMovementInProgress = false;
|
||||||
|
// Deactivate shape tool when window loses focus
|
||||||
|
if (this.canvas.shapeTool.isActive) {
|
||||||
|
this.canvas.shapeTool.deactivate();
|
||||||
|
}
|
||||||
// Also reset any interaction that relies on a key being held down
|
// Also reset any interaction that relies on a key being held down
|
||||||
if (this.interaction.mode === 'dragging' && this.interaction.hasClonedInDrag) {
|
if (this.interaction.mode === 'dragging' && this.interaction.hasClonedInDrag) {
|
||||||
// If we were in the middle of a cloning drag, finalize it
|
// If we were in the middle of a cloning drag, finalize it
|
||||||
|
|||||||
@@ -55,6 +55,34 @@ export class CanvasLayers {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
...layerProps
|
...layerProps
|
||||||
};
|
};
|
||||||
|
if (layer.mask) {
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
if (tempCtx) {
|
||||||
|
tempCanvas.width = layer.width;
|
||||||
|
tempCanvas.height = layer.height;
|
||||||
|
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
||||||
|
const maskCanvas = document.createElement('canvas');
|
||||||
|
const maskCtx = maskCanvas.getContext('2d');
|
||||||
|
if (maskCtx) {
|
||||||
|
maskCanvas.width = layer.width;
|
||||||
|
maskCanvas.height = layer.height;
|
||||||
|
const maskImageData = maskCtx.createImageData(layer.width, layer.height);
|
||||||
|
for (let i = 0; i < layer.mask.length; i++) {
|
||||||
|
maskImageData.data[i * 4] = 255;
|
||||||
|
maskImageData.data[i * 4 + 1] = 255;
|
||||||
|
maskImageData.data[i * 4 + 2] = 255;
|
||||||
|
maskImageData.data[i * 4 + 3] = layer.mask[i] * 255;
|
||||||
|
}
|
||||||
|
maskCtx.putImageData(maskImageData, 0, 0);
|
||||||
|
tempCtx.globalCompositeOperation = 'destination-in';
|
||||||
|
tempCtx.drawImage(maskCanvas, 0, 0);
|
||||||
|
const newImage = new Image();
|
||||||
|
newImage.src = tempCanvas.toDataURL();
|
||||||
|
layer.image = newImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
this.canvas.layers.push(layer);
|
this.canvas.layers.push(layer);
|
||||||
this.canvas.updateSelection([layer]);
|
this.canvas.updateSelection([layer]);
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export class CanvasRenderer {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
this.renderInteractionElements(ctx);
|
this.renderInteractionElements(ctx);
|
||||||
|
this.canvas.shapeTool.render(ctx);
|
||||||
this.renderLayerInfo(ctx);
|
this.renderLayerInfo(ctx);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width ||
|
if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width ||
|
||||||
@@ -257,6 +258,21 @@ export class CanvasRenderer {
|
|||||||
ctx.rect(0, 0, this.canvas.width, this.canvas.height);
|
ctx.rect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
if (this.canvas.outputAreaShape) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(0, 255, 255, 0.9)';
|
||||||
|
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
const shape = this.canvas.outputAreaShape;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||||
|
for (let i = 1; i < shape.points.length; i++) {
|
||||||
|
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
drawSelectionFrame(ctx, layer) {
|
drawSelectionFrame(ctx, layer) {
|
||||||
const lineWidth = 2 / this.canvas.viewport.zoom;
|
const lineWidth = 2 / this.canvas.viewport.zoom;
|
||||||
|
|||||||
125
js/ShapeTool.js
Normal file
125
js/ShapeTool.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||||
|
const log = createModuleLogger('ShapeTool');
|
||||||
|
export class ShapeTool {
|
||||||
|
constructor(canvas) {
|
||||||
|
this.isActive = false;
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.shape = {
|
||||||
|
points: [],
|
||||||
|
isClosed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
toggle() {
|
||||||
|
this.isActive = !this.isActive;
|
||||||
|
if (this.isActive) {
|
||||||
|
log.info('ShapeTool activated. Press "S" to exit.');
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.info('ShapeTool deactivated.');
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
activate() {
|
||||||
|
if (!this.isActive) {
|
||||||
|
this.isActive = true;
|
||||||
|
log.info('ShapeTool activated. Hold Shift+S to draw.');
|
||||||
|
this.reset();
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deactivate() {
|
||||||
|
if (this.isActive) {
|
||||||
|
this.isActive = false;
|
||||||
|
log.info('ShapeTool deactivated.');
|
||||||
|
this.reset();
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addPoint(point) {
|
||||||
|
if (this.shape.isClosed) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
// Check if the new point is close to the start point to close the shape
|
||||||
|
if (this.shape.points.length > 2) {
|
||||||
|
const firstPoint = this.shape.points[0];
|
||||||
|
const dx = point.x - firstPoint.x;
|
||||||
|
const dy = point.y - firstPoint.y;
|
||||||
|
if (Math.sqrt(dx * dx + dy * dy) < 10 / this.canvas.viewport.zoom) {
|
||||||
|
this.closeShape();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.shape.points.push(point);
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
closeShape() {
|
||||||
|
if (this.shape.points.length > 2) {
|
||||||
|
this.shape.isClosed = true;
|
||||||
|
log.info('Shape closed with', this.shape.points.length, 'points.');
|
||||||
|
this.canvas.defineOutputAreaWithShape(this.shape);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
getBoundingBox() {
|
||||||
|
if (this.shape.points.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
|
this.shape.points.forEach(p => {
|
||||||
|
minX = Math.min(minX, p.x);
|
||||||
|
minY = Math.min(minY, p.y);
|
||||||
|
maxX = Math.max(maxX, p.x);
|
||||||
|
maxY = Math.max(maxY, p.y);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
x: minX,
|
||||||
|
y: minY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
reset() {
|
||||||
|
this.shape = {
|
||||||
|
points: [],
|
||||||
|
isClosed: false,
|
||||||
|
};
|
||||||
|
log.info('ShapeTool reset.');
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
render(ctx) {
|
||||||
|
if (this.shape.points.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(0, 255, 255, 0.9)';
|
||||||
|
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||||
|
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
|
||||||
|
ctx.beginPath();
|
||||||
|
const startPoint = this.shape.points[0];
|
||||||
|
ctx.moveTo(startPoint.x, startPoint.y);
|
||||||
|
for (let i = 1; i < this.shape.points.length; i++) {
|
||||||
|
ctx.lineTo(this.shape.points[i].x, this.shape.points[i].y);
|
||||||
|
}
|
||||||
|
if (this.shape.isClosed) {
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = 'rgba(0, 255, 255, 0.2)';
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
else if (this.isActive) {
|
||||||
|
// Draw a line to the current mouse position
|
||||||
|
ctx.lineTo(this.canvas.lastMousePosition.x, this.canvas.lastMousePosition.y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
// Draw vertices
|
||||||
|
ctx.fillStyle = 'rgba(0, 255, 255, 1)';
|
||||||
|
this.shape.points.forEach((point, index) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(point.x, point.y, 4 / this.canvas.viewport.zoom, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {ComfyApp} from "../../scripts/app.js";
|
|||||||
|
|
||||||
import {removeImage} from "./db.js";
|
import {removeImage} from "./db.js";
|
||||||
import {MaskTool} from "./MaskTool.js";
|
import {MaskTool} from "./MaskTool.js";
|
||||||
|
import {ShapeTool} from "./ShapeTool.js";
|
||||||
import {CanvasState} from "./CanvasState.js";
|
import {CanvasState} from "./CanvasState.js";
|
||||||
import {CanvasInteractions} from "./CanvasInteractions.js";
|
import {CanvasInteractions} from "./CanvasInteractions.js";
|
||||||
import {CanvasLayers} from "./CanvasLayers.js";
|
import {CanvasLayers} from "./CanvasLayers.js";
|
||||||
@@ -19,7 +20,7 @@ import {createModuleLogger} from "./utils/LoggerUtils.js";
|
|||||||
import { debounce } from "./utils/CommonUtils.js";
|
import { debounce } from "./utils/CommonUtils.js";
|
||||||
import {CanvasMask} from "./CanvasMask.js";
|
import {CanvasMask} from "./CanvasMask.js";
|
||||||
import {CanvasSelection} from "./CanvasSelection.js";
|
import {CanvasSelection} from "./CanvasSelection.js";
|
||||||
import type { ComfyNode, Layer, Viewport, Point, AddMode } from './types';
|
import type { ComfyNode, Layer, Viewport, Point, AddMode, Shape } from './types';
|
||||||
|
|
||||||
const useChainCallback = (original: any, next: any) => {
|
const useChainCallback = (original: any, next: any) => {
|
||||||
if (original === undefined || original === null) {
|
if (original === undefined || original === null) {
|
||||||
@@ -63,6 +64,8 @@ export class Canvas {
|
|||||||
lastMousePosition: Point;
|
lastMousePosition: Point;
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
maskTool: MaskTool;
|
maskTool: MaskTool;
|
||||||
|
shapeTool: ShapeTool;
|
||||||
|
outputAreaShape: Shape | null;
|
||||||
node: ComfyNode;
|
node: ComfyNode;
|
||||||
offscreenCanvas: HTMLCanvasElement;
|
offscreenCanvas: HTMLCanvasElement;
|
||||||
offscreenCtx: CanvasRenderingContext2D | null;
|
offscreenCtx: CanvasRenderingContext2D | null;
|
||||||
@@ -108,6 +111,8 @@ export class Canvas {
|
|||||||
|
|
||||||
this.requestSaveState = () => {};
|
this.requestSaveState = () => {};
|
||||||
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange});
|
||||||
|
this.shapeTool = new ShapeTool(this);
|
||||||
|
this.outputAreaShape = null;
|
||||||
this.canvasMask = new CanvasMask(this);
|
this.canvasMask = new CanvasMask(this);
|
||||||
this.canvasState = new CanvasState(this);
|
this.canvasState = new CanvasState(this);
|
||||||
this.canvasSelection = new CanvasSelection(this);
|
this.canvasSelection = new CanvasSelection(this);
|
||||||
@@ -381,6 +386,41 @@ export class Canvas {
|
|||||||
return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
|
return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineOutputAreaWithShape(shape: Shape): void {
|
||||||
|
const boundingBox = this.shapeTool.getBoundingBox();
|
||||||
|
if (boundingBox && boundingBox.width > 1 && boundingBox.height > 1) {
|
||||||
|
this.saveState();
|
||||||
|
|
||||||
|
this.outputAreaShape = {
|
||||||
|
...shape,
|
||||||
|
points: shape.points.map(p => ({
|
||||||
|
x: p.x - boundingBox.x,
|
||||||
|
y: p.y - boundingBox.y
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const newWidth = Math.round(boundingBox.width);
|
||||||
|
const newHeight = Math.round(boundingBox.height);
|
||||||
|
const finalX = boundingBox.x;
|
||||||
|
const finalY = boundingBox.y;
|
||||||
|
|
||||||
|
this.updateOutputAreaSize(newWidth, newHeight, false);
|
||||||
|
|
||||||
|
this.layers.forEach((layer: Layer) => {
|
||||||
|
layer.x -= finalX;
|
||||||
|
layer.y -= finalY;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.maskTool.updatePosition(-finalX, -finalY);
|
||||||
|
|
||||||
|
this.viewport.x -= finalX;
|
||||||
|
this.viewport.y -= finalY;
|
||||||
|
|
||||||
|
this.saveState();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zmienia rozmiar obszaru wyjściowego
|
* Zmienia rozmiar obszaru wyjściowego
|
||||||
* @param {number} width - Nowa szerokość
|
* @param {number} width - Nowa szerokość
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createCanvas } from "./utils/CommonUtils.js";
|
|||||||
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||||
import { webSocketManager } from "./utils/WebSocketManager.js";
|
import { webSocketManager } from "./utils/WebSocketManager.js";
|
||||||
import type { Canvas } from './Canvas';
|
import type { Canvas } from './Canvas';
|
||||||
import type { Layer } from './types';
|
import type { Layer, Shape } from './types';
|
||||||
|
|
||||||
const log = createModuleLogger('CanvasIO');
|
const log = createModuleLogger('CanvasIO');
|
||||||
|
|
||||||
@@ -61,6 +61,9 @@ export class CanvasIO {
|
|||||||
const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(this.canvas.width, this.canvas.height);
|
const {canvas: tempCanvas, ctx: tempCtx} = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height);
|
const {canvas: maskCanvas, ctx: maskCtx} = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
const originalShape = this.canvas.outputAreaShape;
|
||||||
|
this.canvas.outputAreaShape = null;
|
||||||
|
|
||||||
const visibilityCanvas = document.createElement('canvas');
|
const visibilityCanvas = document.createElement('canvas');
|
||||||
visibilityCanvas.width = this.canvas.width;
|
visibilityCanvas.width = this.canvas.width;
|
||||||
visibilityCanvas.height = this.canvas.height;
|
visibilityCanvas.height = this.canvas.height;
|
||||||
@@ -75,7 +78,6 @@ export class CanvasIO {
|
|||||||
|
|
||||||
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||||
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||||
|
|
||||||
log.debug(`Finished rendering layers`);
|
log.debug(`Finished rendering layers`);
|
||||||
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
@@ -87,6 +89,9 @@ export class CanvasIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
maskCtx.putImageData(maskData, 0, 0);
|
maskCtx.putImageData(maskData, 0, 0);
|
||||||
|
|
||||||
|
this.canvas.outputAreaShape = originalShape;
|
||||||
|
|
||||||
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
||||||
if (toolMaskCanvas) {
|
if (toolMaskCanvas) {
|
||||||
|
|
||||||
@@ -233,6 +238,9 @@ export class CanvasIO {
|
|||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
const originalShape = this.canvas.outputAreaShape;
|
||||||
|
this.canvas.outputAreaShape = null;
|
||||||
|
|
||||||
const visibilityCanvas = document.createElement('canvas');
|
const visibilityCanvas = document.createElement('canvas');
|
||||||
visibilityCanvas.width = this.canvas.width;
|
visibilityCanvas.width = this.canvas.width;
|
||||||
visibilityCanvas.height = this.canvas.height;
|
visibilityCanvas.height = this.canvas.height;
|
||||||
@@ -298,13 +306,15 @@ export class CanvasIO {
|
|||||||
tempMaskCtx.putImageData(tempMaskData, 0, 0);
|
tempMaskCtx.putImageData(tempMaskData, 0, 0);
|
||||||
|
|
||||||
|
|
||||||
maskCtx.globalCompositeOperation = 'screen';
|
maskCtx.globalCompositeOperation = 'screen';
|
||||||
maskCtx.drawImage(tempMaskCanvas, 0, 0);
|
maskCtx.drawImage(tempMaskCanvas, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageDataUrl = tempCanvas.toDataURL('image/png');
|
const imageDataUrl = tempCanvas.toDataURL('image/png');
|
||||||
const maskDataUrl = maskCanvas.toDataURL('image/png');
|
const maskDataUrl = maskCanvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
this.canvas.outputAreaShape = originalShape;
|
||||||
|
|
||||||
resolve({image: imageDataUrl, mask: maskDataUrl});
|
resolve({image: imageDataUrl, mask: maskDataUrl});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -775,7 +785,15 @@ export class CanvasIO {
|
|||||||
img.onerror = reject;
|
img.onerror = reject;
|
||||||
img.src = imageData;
|
img.src = imageData;
|
||||||
});
|
});
|
||||||
const newLayer = await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit', targetArea);
|
|
||||||
|
let processedImage = img;
|
||||||
|
|
||||||
|
// If there's a custom shape, clip the image to that shape
|
||||||
|
if (this.canvas.outputAreaShape && this.canvas.outputAreaShape.isClosed) {
|
||||||
|
processedImage = await this.clipImageToShape(img, this.canvas.outputAreaShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLayer = await this.canvas.canvasLayers.addLayerWithImage(processedImage, {}, 'fit', targetArea);
|
||||||
newLayers.push(newLayer);
|
newLayers.push(newLayer);
|
||||||
}
|
}
|
||||||
log.info("All new images imported and placed on canvas successfully.");
|
log.info("All new images imported and placed on canvas successfully.");
|
||||||
@@ -793,4 +811,59 @@ export class CanvasIO {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clipImageToShape(image: HTMLImageElement, shape: Shape): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { canvas, ctx } = createCanvas(image.width, image.height);
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error("Could not create canvas context for clipping"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the image first
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
// Create a clipping mask using the shape
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||||
|
for (let i = 1; i < shape.points.length; i++) {
|
||||||
|
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Create a new image from the clipped canvas
|
||||||
|
const clippedImage = new Image();
|
||||||
|
clippedImage.onload = () => resolve(clippedImage);
|
||||||
|
clippedImage.onerror = () => reject(new Error("Failed to create clipped image"));
|
||||||
|
clippedImage.src = canvas.toDataURL();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createMaskFromShape(shape: Shape, width: number, height: number): Float32Array {
|
||||||
|
const { canvas, ctx } = createCanvas(width, height);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("Could not create canvas context for mask");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||||
|
for (let i = 1; i < shape.points.length; i++) {
|
||||||
|
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
const maskData = new Float32Array(width * height);
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
maskData[i / 4] = imageData.data[i] / 255;
|
||||||
|
}
|
||||||
|
return maskData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Layer, Point } from './types';
|
|||||||
const log = createModuleLogger('CanvasInteractions');
|
const log = createModuleLogger('CanvasInteractions');
|
||||||
|
|
||||||
interface InteractionState {
|
interface InteractionState {
|
||||||
mode: 'none' | 'panning' | 'dragging' | 'resizing' | 'rotating' | 'drawingMask' | 'resizingCanvas' | 'movingCanvas' | 'potential-drag';
|
mode: 'none' | 'panning' | 'dragging' | 'resizing' | 'rotating' | 'drawingMask' | 'resizingCanvas' | 'movingCanvas' | 'potential-drag' | 'drawingShape';
|
||||||
panStart: Point;
|
panStart: Point;
|
||||||
dragStart: Point;
|
dragStart: Point;
|
||||||
transformOrigin: Partial<Layer> & { centerX?: number, centerY?: number };
|
transformOrigin: Partial<Layer> & { centerX?: number, centerY?: number };
|
||||||
@@ -15,6 +15,8 @@ interface InteractionState {
|
|||||||
canvasResizeStart: Point;
|
canvasResizeStart: Point;
|
||||||
isCtrlPressed: boolean;
|
isCtrlPressed: boolean;
|
||||||
isAltPressed: boolean;
|
isAltPressed: boolean;
|
||||||
|
isShiftPressed: boolean;
|
||||||
|
isSPressed: boolean;
|
||||||
hasClonedInDrag: boolean;
|
hasClonedInDrag: boolean;
|
||||||
lastClickTime: number;
|
lastClickTime: number;
|
||||||
transformingLayer: Layer | null;
|
transformingLayer: Layer | null;
|
||||||
@@ -40,6 +42,8 @@ export class CanvasInteractions {
|
|||||||
canvasResizeStart: { x: 0, y: 0 },
|
canvasResizeStart: { x: 0, y: 0 },
|
||||||
isCtrlPressed: false,
|
isCtrlPressed: false,
|
||||||
isAltPressed: false,
|
isAltPressed: false,
|
||||||
|
isShiftPressed: false,
|
||||||
|
isSPressed: false,
|
||||||
hasClonedInDrag: false,
|
hasClonedInDrag: false,
|
||||||
lastClickTime: 0,
|
lastClickTime: 0,
|
||||||
transformingLayer: null,
|
transformingLayer: null,
|
||||||
@@ -103,6 +107,11 @@ export class CanvasInteractions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.canvas.shapeTool.isActive) {
|
||||||
|
this.canvas.shapeTool.addPoint(worldCoords);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
// --- Ostateczna, poprawna kolejność sprawdzania ---
|
||||||
|
|
||||||
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
// 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet)
|
||||||
@@ -111,6 +120,11 @@ export class CanvasInteractions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
|
// Clear custom shape when starting canvas resize
|
||||||
|
if (this.canvas.outputAreaShape) {
|
||||||
|
this.canvas.outputAreaShape = null;
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
this.startCanvasResize(worldCoords);
|
this.startCanvasResize(worldCoords);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -348,10 +362,22 @@ export class CanvasInteractions {
|
|||||||
|
|
||||||
handleKeyDown(e: KeyboardEvent): void {
|
handleKeyDown(e: KeyboardEvent): void {
|
||||||
if (e.key === 'Control') this.interaction.isCtrlPressed = true;
|
if (e.key === 'Control') this.interaction.isCtrlPressed = true;
|
||||||
|
if (e.key === 'Shift') this.interaction.isShiftPressed = true;
|
||||||
if (e.key === 'Alt') {
|
if (e.key === 'Alt') {
|
||||||
this.interaction.isAltPressed = true;
|
this.interaction.isAltPressed = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
if (e.key.toLowerCase() === 's') {
|
||||||
|
this.interaction.isSPressed = true;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Shift+S is being held down
|
||||||
|
if (this.interaction.isShiftPressed && this.interaction.isSPressed && !this.interaction.isCtrlPressed && !this.canvas.shapeTool.isActive) {
|
||||||
|
this.canvas.shapeTool.activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Globalne skróty (Undo/Redo/Copy/Paste)
|
// Globalne skróty (Undo/Redo/Copy/Paste)
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
@@ -420,7 +446,14 @@ export class CanvasInteractions {
|
|||||||
|
|
||||||
handleKeyUp(e: KeyboardEvent): void {
|
handleKeyUp(e: KeyboardEvent): void {
|
||||||
if (e.key === 'Control') this.interaction.isCtrlPressed = false;
|
if (e.key === 'Control') this.interaction.isCtrlPressed = false;
|
||||||
|
if (e.key === 'Shift') this.interaction.isShiftPressed = false;
|
||||||
if (e.key === 'Alt') this.interaction.isAltPressed = false;
|
if (e.key === 'Alt') this.interaction.isAltPressed = false;
|
||||||
|
if (e.key.toLowerCase() === 's') this.interaction.isSPressed = false;
|
||||||
|
|
||||||
|
// Deactivate shape tool when Shift or S is released
|
||||||
|
if (this.canvas.shapeTool.isActive && (!this.interaction.isShiftPressed || !this.interaction.isSPressed)) {
|
||||||
|
this.canvas.shapeTool.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight'];
|
const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight'];
|
||||||
if (movementKeys.includes(e.code) && this.interaction.keyMovementInProgress) {
|
if (movementKeys.includes(e.code) && this.interaction.keyMovementInProgress) {
|
||||||
@@ -433,8 +466,15 @@ export class CanvasInteractions {
|
|||||||
log.debug('Window lost focus, resetting key states.');
|
log.debug('Window lost focus, resetting key states.');
|
||||||
this.interaction.isCtrlPressed = false;
|
this.interaction.isCtrlPressed = false;
|
||||||
this.interaction.isAltPressed = false;
|
this.interaction.isAltPressed = false;
|
||||||
|
this.interaction.isShiftPressed = false;
|
||||||
|
this.interaction.isSPressed = false;
|
||||||
this.interaction.keyMovementInProgress = false;
|
this.interaction.keyMovementInProgress = false;
|
||||||
|
|
||||||
|
// Deactivate shape tool when window loses focus
|
||||||
|
if (this.canvas.shapeTool.isActive) {
|
||||||
|
this.canvas.shapeTool.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
// Also reset any interaction that relies on a key being held down
|
// Also reset any interaction that relies on a key being held down
|
||||||
if (this.interaction.mode === 'dragging' && this.interaction.hasClonedInDrag) {
|
if (this.interaction.mode === 'dragging' && this.interaction.hasClonedInDrag) {
|
||||||
// If we were in the middle of a cloning drag, finalize it
|
// If we were in the middle of a cloning drag, finalize it
|
||||||
|
|||||||
@@ -207,6 +207,38 @@ export class CanvasLayers {
|
|||||||
...layerProps
|
...layerProps
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (layer.mask) {
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
if(tempCtx) {
|
||||||
|
tempCanvas.width = layer.width;
|
||||||
|
tempCanvas.height = layer.height;
|
||||||
|
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
||||||
|
|
||||||
|
const maskCanvas = document.createElement('canvas');
|
||||||
|
const maskCtx = maskCanvas.getContext('2d');
|
||||||
|
if(maskCtx) {
|
||||||
|
maskCanvas.width = layer.width;
|
||||||
|
maskCanvas.height = layer.height;
|
||||||
|
const maskImageData = maskCtx.createImageData(layer.width, layer.height);
|
||||||
|
for (let i = 0; i < layer.mask.length; i++) {
|
||||||
|
maskImageData.data[i * 4] = 255;
|
||||||
|
maskImageData.data[i * 4 + 1] = 255;
|
||||||
|
maskImageData.data[i * 4 + 2] = 255;
|
||||||
|
maskImageData.data[i * 4 + 3] = layer.mask[i] * 255;
|
||||||
|
}
|
||||||
|
maskCtx.putImageData(maskImageData, 0, 0);
|
||||||
|
|
||||||
|
tempCtx.globalCompositeOperation = 'destination-in';
|
||||||
|
tempCtx.drawImage(maskCanvas, 0, 0);
|
||||||
|
|
||||||
|
const newImage = new Image();
|
||||||
|
newImage.src = tempCanvas.toDataURL();
|
||||||
|
layer.image = newImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.canvas.layers.push(layer);
|
this.canvas.layers.push(layer);
|
||||||
this.canvas.updateSelection([layer]);
|
this.canvas.updateSelection([layer]);
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.renderInteractionElements(ctx);
|
this.renderInteractionElements(ctx);
|
||||||
|
this.canvas.shapeTool.render(ctx);
|
||||||
this.renderLayerInfo(ctx);
|
this.renderLayerInfo(ctx);
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -307,6 +308,22 @@ export class CanvasRenderer {
|
|||||||
|
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
if (this.canvas.outputAreaShape) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(0, 255, 255, 0.9)';
|
||||||
|
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
const shape = this.canvas.outputAreaShape;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||||
|
for (let i = 1; i < shape.points.length; i++) {
|
||||||
|
ctx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawSelectionFrame(ctx: any, layer: any) {
|
drawSelectionFrame(ctx: any, layer: any) {
|
||||||
|
|||||||
155
src/ShapeTool.ts
Normal file
155
src/ShapeTool.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||||
|
import type { Canvas } from './Canvas.js';
|
||||||
|
import type { Point, Layer } from './types.js';
|
||||||
|
|
||||||
|
const log = createModuleLogger('ShapeTool');
|
||||||
|
|
||||||
|
interface Shape {
|
||||||
|
points: Point[];
|
||||||
|
isClosed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShapeTool {
|
||||||
|
private canvas: Canvas;
|
||||||
|
public shape: Shape;
|
||||||
|
public isActive: boolean = false;
|
||||||
|
|
||||||
|
constructor(canvas: Canvas) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.shape = {
|
||||||
|
points: [],
|
||||||
|
isClosed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isActive = !this.isActive;
|
||||||
|
if (this.isActive) {
|
||||||
|
log.info('ShapeTool activated. Press "S" to exit.');
|
||||||
|
this.reset();
|
||||||
|
} else {
|
||||||
|
log.info('ShapeTool deactivated.');
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
if (!this.isActive) {
|
||||||
|
this.isActive = true;
|
||||||
|
log.info('ShapeTool activated. Hold Shift+S to draw.');
|
||||||
|
this.reset();
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
if (this.isActive) {
|
||||||
|
this.isActive = false;
|
||||||
|
log.info('ShapeTool deactivated.');
|
||||||
|
this.reset();
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addPoint(point: Point) {
|
||||||
|
if (this.shape.isClosed) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the new point is close to the start point to close the shape
|
||||||
|
if (this.shape.points.length > 2) {
|
||||||
|
const firstPoint = this.shape.points[0];
|
||||||
|
const dx = point.x - firstPoint.x;
|
||||||
|
const dy = point.y - firstPoint.y;
|
||||||
|
if (Math.sqrt(dx * dx + dy * dy) < 10 / this.canvas.viewport.zoom) {
|
||||||
|
this.closeShape();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shape.points.push(point);
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeShape() {
|
||||||
|
if (this.shape.points.length > 2) {
|
||||||
|
this.shape.isClosed = true;
|
||||||
|
log.info('Shape closed with', this.shape.points.length, 'points.');
|
||||||
|
|
||||||
|
this.canvas.defineOutputAreaWithShape(this.shape);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBoundingBox(): { x: number, y: number, width: number, height: number } | null {
|
||||||
|
if (this.shape.points.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
|
this.shape.points.forEach(p => {
|
||||||
|
minX = Math.min(minX, p.x);
|
||||||
|
minY = Math.min(minY, p.y);
|
||||||
|
maxX = Math.max(maxX, p.x);
|
||||||
|
maxY = Math.max(maxY, p.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: minX,
|
||||||
|
y: minY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.shape = {
|
||||||
|
points: [],
|
||||||
|
isClosed: false,
|
||||||
|
};
|
||||||
|
log.info('ShapeTool reset.');
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx: CanvasRenderingContext2D) {
|
||||||
|
if (this.shape.points.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(0, 255, 255, 0.9)';
|
||||||
|
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||||
|
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
const startPoint = this.shape.points[0];
|
||||||
|
ctx.moveTo(startPoint.x, startPoint.y);
|
||||||
|
|
||||||
|
for (let i = 1; i < this.shape.points.length; i++) {
|
||||||
|
ctx.lineTo(this.shape.points[i].x, this.shape.points[i].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shape.isClosed) {
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = 'rgba(0, 255, 255, 0.2)';
|
||||||
|
ctx.fill();
|
||||||
|
} else if (this.isActive) {
|
||||||
|
// Draw a line to the current mouse position
|
||||||
|
ctx.lineTo(this.canvas.lastMousePosition.x, this.canvas.lastMousePosition.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw vertices
|
||||||
|
ctx.fillStyle = 'rgba(0, 255, 255, 1)';
|
||||||
|
this.shape.points.forEach((point, index) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(point.x, point.y, 4 / this.canvas.viewport.zoom, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,6 +129,11 @@ export interface Point {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Shape {
|
||||||
|
points: Point[];
|
||||||
|
isClosed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Viewport {
|
export interface Viewport {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user