mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Add mask drawing tool and improve mask handling
Introduces a new MaskTool for interactive mask drawing with adjustable brush size, strength, and softness. Updates Canvas.js to integrate mask editing, rendering, and saving, including support for saving images with and without masks. Enhances the UI in Canvas_view.js with mask controls and a dialog for canvas resizing. Updates canvas_node.py to load images without masks for processing. These changes improve user control over mask creation and management in the canvas workflow.
This commit is contained in:
@@ -250,9 +250,9 @@ class CanvasNode:
|
||||
self.__class__._canvas_cache['cache_enabled'] = cache_enabled
|
||||
|
||||
try:
|
||||
|
||||
path_image = folder_paths.get_annotated_filepath(canvas_image)
|
||||
i = Image.open(path_image)
|
||||
# Wczytaj obraz bez maski
|
||||
path_image_without_mask = folder_paths.get_annotated_filepath(canvas_image.replace('.png', '_without_mask.png'))
|
||||
i = Image.open(path_image_without_mask)
|
||||
i = ImageOps.exif_transpose(i)
|
||||
if i.mode not in ['RGB', 'RGBA']:
|
||||
i = i.convert('RGB')
|
||||
@@ -263,23 +263,22 @@ class CanvasNode:
|
||||
image = rgb * alpha + (1 - alpha) * 0.5
|
||||
processed_image = torch.from_numpy(image)[None,]
|
||||
except Exception as e:
|
||||
|
||||
print(f"Error loading image without mask: {str(e)}")
|
||||
processed_image = torch.ones((1, 512, 512, 3), dtype=torch.float32)
|
||||
|
||||
try:
|
||||
|
||||
# Wczytaj maskę
|
||||
path_image = folder_paths.get_annotated_filepath(canvas_image)
|
||||
path_mask = path_image.replace('.png', '_mask.png')
|
||||
if os.path.exists(path_mask):
|
||||
mask = Image.open(path_mask).convert('L')
|
||||
mask = np.array(mask).astype(np.float32) / 255.0
|
||||
processed_mask = torch.from_numpy(mask)[None,]
|
||||
else:
|
||||
|
||||
processed_mask = torch.ones((1, processed_image.shape[1], processed_image.shape[2]),
|
||||
dtype=torch.float32)
|
||||
except Exception as e:
|
||||
print(f"Error loading mask: {str(e)}")
|
||||
|
||||
processed_mask = torch.ones((1, processed_image.shape[1], processed_image.shape[2]),
|
||||
dtype=torch.float32)
|
||||
|
||||
|
||||
200
js/Canvas.js
200
js/Canvas.js
@@ -1,4 +1,5 @@
|
||||
import { getCanvasState, setCanvasState, removeCanvasState } from "./db.js";
|
||||
import { MaskTool } from "./Mask_tool.js";
|
||||
|
||||
export class Canvas {
|
||||
constructor(node, widget) {
|
||||
@@ -50,6 +51,7 @@ export class Canvas {
|
||||
|
||||
this.dataInitialized = false;
|
||||
this.pendingDataCheck = null;
|
||||
this.maskTool = new MaskTool(this);
|
||||
this.initCanvas();
|
||||
this.setupEventListeners();
|
||||
this.initNodeData();
|
||||
@@ -325,9 +327,20 @@ export class Canvas {
|
||||
|
||||
handleMouseDown(e) {
|
||||
this.canvas.focus();
|
||||
const worldCoords = this.getMouseWorldCoordinates(e);
|
||||
|
||||
if (this.maskTool.isActive) {
|
||||
if (e.button === 1) { // Środkowy przycisk myszy (kółko)
|
||||
this.startPanning(e);
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
this.maskTool.handleMouseDown(worldCoords);
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
const worldCoords = this.getMouseWorldCoordinates(e);
|
||||
if (e.shiftKey && e.ctrlKey) {
|
||||
this.startCanvasMove(worldCoords);
|
||||
this.render();
|
||||
@@ -466,6 +479,16 @@ export class Canvas {
|
||||
const worldCoords = this.getMouseWorldCoordinates(e);
|
||||
this.lastMousePosition = worldCoords;
|
||||
|
||||
if (this.maskTool.isActive) {
|
||||
if (this.interaction.mode === 'panning') {
|
||||
this.panViewport(e);
|
||||
return;
|
||||
}
|
||||
this.maskTool.handleMouseMove(worldCoords);
|
||||
if (this.maskTool.isDrawing) this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.interaction.mode) {
|
||||
case 'panning':
|
||||
this.panViewport(e);
|
||||
@@ -493,6 +516,18 @@ export class Canvas {
|
||||
|
||||
|
||||
handleMouseUp(e) {
|
||||
if (this.maskTool.isActive) {
|
||||
if (this.interaction.mode === 'panning') {
|
||||
this.resetInteractionState();
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
this.maskTool.handleMouseUp();
|
||||
this.saveState();
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const interactionEnded = this.interaction.mode !== 'none' && this.interaction.mode !== 'panning';
|
||||
|
||||
if (this.interaction.mode === 'resizingCanvas') {
|
||||
@@ -510,6 +545,11 @@ export class Canvas {
|
||||
|
||||
|
||||
handleMouseLeave(e) {
|
||||
if (this.maskTool.isActive) {
|
||||
this.maskTool.handleMouseUp();
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
if (this.interaction.mode !== 'none') {
|
||||
this.resetInteractionState();
|
||||
this.render();
|
||||
@@ -519,7 +559,20 @@ export class Canvas {
|
||||
|
||||
handleWheel(e) {
|
||||
e.preventDefault();
|
||||
if (this.selectedLayer) {
|
||||
if (this.maskTool.isActive) {
|
||||
// W trybie maski zezwalaj tylko na zoom i przesuwanie canvasu
|
||||
const worldCoords = this.getMouseWorldCoordinates(e);
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const mouseBufferX = (e.clientX - rect.left) * (this.offscreenCanvas.width / rect.width);
|
||||
const mouseBufferY = (e.clientY - rect.top) * (this.offscreenCanvas.height / rect.height);
|
||||
|
||||
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||
const newZoom = this.viewport.zoom * zoomFactor;
|
||||
|
||||
this.viewport.zoom = Math.max(0.1, Math.min(10, newZoom));
|
||||
this.viewport.x = worldCoords.x - (mouseBufferX / this.viewport.zoom);
|
||||
this.viewport.y = worldCoords.y - (mouseBufferY / this.viewport.zoom);
|
||||
} else if (this.selectedLayer) {
|
||||
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
|
||||
|
||||
this.selectedLayers.forEach(layer => {
|
||||
@@ -593,6 +646,35 @@ export class Canvas {
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (this.maskTool.isActive) {
|
||||
// W trybie maski zezwalaj tylko na podstawowe skróty (np. cofnij/powtórz)
|
||||
if (e.key === 'Control') this.interaction.isCtrlPressed = true;
|
||||
if (e.key === 'Alt') {
|
||||
this.interaction.isAltPressed = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.ctrlKey) {
|
||||
if (e.key.toLowerCase() === 'z') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.shiftKey) {
|
||||
this.redo();
|
||||
} else {
|
||||
this.undo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === 'y') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.redo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return; // Blokuj inne interakcje klawiaturowe w trybie maski
|
||||
}
|
||||
|
||||
if (e.key === 'Control') this.interaction.isCtrlPressed = true;
|
||||
if (e.key === 'Alt') {
|
||||
this.interaction.isAltPressed = true;
|
||||
@@ -1128,6 +1210,7 @@ export class Canvas {
|
||||
}
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.maskTool.resize(width, height);
|
||||
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
@@ -1210,6 +1293,23 @@ export class Canvas {
|
||||
});
|
||||
|
||||
this.drawCanvasOutline(ctx);
|
||||
|
||||
// Renderowanie maski w zależności od trybu
|
||||
const maskImage = this.maskTool.getMask();
|
||||
if (this.maskTool.isActive) {
|
||||
// W trybie maski pokazuj maskę z przezroczystością 0.5
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.drawImage(maskImage, 0, 0);
|
||||
ctx.globalAlpha = 1.0;
|
||||
} else if (maskImage) {
|
||||
// W trybie warstw pokazuj maskę jako widoczną, ale nieedytowalną
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 1.0;
|
||||
ctx.drawImage(maskImage, 0, 0);
|
||||
ctx.globalAlpha = 1.0;
|
||||
}
|
||||
|
||||
if (this.interaction.mode === 'resizingCanvas' && this.interaction.canvasResizeRect) {
|
||||
const rect = this.interaction.canvasResizeRect;
|
||||
ctx.save();
|
||||
@@ -1496,7 +1596,6 @@ export class Canvas {
|
||||
|
||||
async saveToServer(fileName) {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const maskCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.width;
|
||||
@@ -1510,36 +1609,50 @@ export class Canvas {
|
||||
tempCtx.fillStyle = '#ffffff';
|
||||
tempCtx.fillRect(0, 0, this.width, this.height);
|
||||
|
||||
maskCtx.fillStyle = '#000000';
|
||||
maskCtx.fillStyle = '#ffffff'; // Białe tło dla wolnych przestrzeni
|
||||
maskCtx.fillRect(0, 0, this.width, this.height);
|
||||
|
||||
// Rysowanie warstw
|
||||
this.layers.sort((a, b) => a.zIndex - b.zIndex).forEach(layer => {
|
||||
|
||||
tempCtx.save();
|
||||
|
||||
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||
|
||||
tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
|
||||
tempCtx.rotate(layer.rotation * Math.PI / 180);
|
||||
tempCtx.drawImage(
|
||||
layer.image,
|
||||
-layer.width / 2,
|
||||
-layer.height / 2,
|
||||
layer.width,
|
||||
layer.height
|
||||
);
|
||||
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
||||
tempCtx.restore();
|
||||
|
||||
maskCtx.save();
|
||||
maskCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
|
||||
maskCtx.rotate(layer.rotation * Math.PI / 180);
|
||||
maskCtx.globalCompositeOperation = 'lighter';
|
||||
maskCtx.globalCompositeOperation = 'source-over'; // Używamy source-over, aby uwzględnić stopniową przezroczystość
|
||||
|
||||
if (layer.mask) {
|
||||
maskCtx.drawImage(layer.mask, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
||||
} else {
|
||||
// Jeśli warstwa ma maskę, używamy jej jako alpha kanału
|
||||
const layerCanvas = document.createElement('canvas');
|
||||
layerCanvas.width = layer.width;
|
||||
layerCanvas.height = layer.height;
|
||||
const layerCtx = layerCanvas.getContext('2d');
|
||||
layerCtx.drawImage(layer.mask, 0, 0, layer.width, layer.height);
|
||||
const imageData = layerCtx.getImageData(0, 0, layer.width, layer.height);
|
||||
|
||||
const alphaCanvas = document.createElement('canvas');
|
||||
alphaCanvas.width = layer.width;
|
||||
alphaCanvas.height = layer.height;
|
||||
const alphaCtx = alphaCanvas.getContext('2d');
|
||||
const alphaData = alphaCtx.createImageData(layer.width, layer.height);
|
||||
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
const alpha = imageData.data[i + 3] * (layer.opacity !== undefined ? layer.opacity : 1);
|
||||
// Odwracamy alpha, aby przezroczyste obszary warstwy były nieprzezroczyste na masce
|
||||
alphaData.data[i] = alphaData.data[i + 1] = alphaData.data[i + 2] = 255 - alpha;
|
||||
alphaData.data[i + 3] = 255;
|
||||
}
|
||||
|
||||
alphaCtx.putImageData(alphaData, 0, 0);
|
||||
maskCtx.drawImage(alphaCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
||||
} else {
|
||||
// Jeśli warstwa nie ma maski, używamy jej alpha kanału
|
||||
const layerCanvas = document.createElement('canvas');
|
||||
layerCanvas.width = layer.width;
|
||||
layerCanvas.height = layer.height;
|
||||
@@ -1555,7 +1668,8 @@ export class Canvas {
|
||||
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
const alpha = imageData.data[i + 3] * (layer.opacity !== undefined ? layer.opacity : 1);
|
||||
alphaData.data[i] = alphaData.data[i + 1] = alphaData.data[i + 2] = alpha;
|
||||
// Odwracamy alpha, aby przezroczyste obszary warstwy były nieprzezroczyste na masce
|
||||
alphaData.data[i] = alphaData.data[i + 1] = alphaData.data[i + 2] = 255 - alpha;
|
||||
alphaData.data[i + 3] = 255;
|
||||
}
|
||||
|
||||
@@ -1565,15 +1679,48 @@ export class Canvas {
|
||||
maskCtx.restore();
|
||||
});
|
||||
|
||||
const finalMaskData = maskCtx.getImageData(0, 0, this.width, this.height);
|
||||
for (let i = 0; i < finalMaskData.data.length; i += 4) {
|
||||
finalMaskData.data[i] =
|
||||
finalMaskData.data[i + 1] =
|
||||
finalMaskData.data[i + 2] = 255 - finalMaskData.data[i];
|
||||
finalMaskData.data[i + 3] = 255;
|
||||
}
|
||||
maskCtx.putImageData(finalMaskData, 0, 0);
|
||||
// Nałóż maskę z narzędzia MaskTool, uwzględniając przezroczystość pędzla
|
||||
const toolMaskCanvas = this.maskTool.getMask();
|
||||
if (toolMaskCanvas) {
|
||||
// Utwórz tymczasowy canvas, aby zachować wartości alpha maski z MaskTool
|
||||
const tempMaskCanvas = document.createElement('canvas');
|
||||
tempMaskCanvas.width = this.width;
|
||||
tempMaskCanvas.height = this.height;
|
||||
const tempMaskCtx = tempMaskCanvas.getContext('2d');
|
||||
tempMaskCtx.drawImage(toolMaskCanvas, 0, 0);
|
||||
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.width, this.height);
|
||||
|
||||
// Zachowaj wartości alpha, aby obszary narysowane pędzlem były nieprzezroczyste na masce
|
||||
for (let i = 0; i < tempMaskData.data.length; i += 4) {
|
||||
const alpha = tempMaskData.data[i + 3];
|
||||
tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = 255;
|
||||
tempMaskData.data[i + 3] = alpha; // Zachowaj oryginalną przezroczystość pędzla
|
||||
}
|
||||
tempMaskCtx.putImageData(tempMaskData, 0, 0);
|
||||
|
||||
// Nałóż maskę z MaskTool na maskę główną
|
||||
maskCtx.globalCompositeOperation = 'source-over'; // Dodaje nieprzezroczystość tam, gdzie pędzel był użyty
|
||||
maskCtx.drawImage(tempMaskCanvas, 0, 0);
|
||||
}
|
||||
|
||||
// Zapisz obraz bez maski
|
||||
const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png');
|
||||
tempCanvas.toBlob(async (blobWithoutMask) => {
|
||||
const formDataWithoutMask = new FormData();
|
||||
formDataWithoutMask.append("image", blobWithoutMask, fileNameWithoutMask);
|
||||
formDataWithoutMask.append("overwrite", "true");
|
||||
|
||||
try {
|
||||
await fetch("/upload/image", {
|
||||
method: "POST",
|
||||
body: formDataWithoutMask,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error uploading image without mask:", error);
|
||||
}
|
||||
}, "image/png");
|
||||
|
||||
// Zapisz obraz z maską
|
||||
tempCanvas.toBlob(async (blob) => {
|
||||
const formData = new FormData();
|
||||
formData.append("image", blob, fileName);
|
||||
@@ -1586,7 +1733,6 @@ export class Canvas {
|
||||
});
|
||||
|
||||
if (resp.status === 200) {
|
||||
|
||||
maskCanvas.toBlob(async (maskBlob) => {
|
||||
const maskFormData = new FormData();
|
||||
const maskFileName = fileName.replace('.png', '_mask.png');
|
||||
|
||||
@@ -65,6 +65,19 @@ async function createCanvasWidget(node, widget, app) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.painter-slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.painter-slider-container input[type="range"] {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
|
||||
.painter-button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -388,12 +401,89 @@ async function createCanvasWidget(node, widget, app) {
|
||||
|
||||
// --- Group: Canvas & Layers ---
|
||||
$el("div.painter-button-group", {}, [
|
||||
$el("button.painter-button", {
|
||||
textContent: "Canvas Size",
|
||||
onclick: () => {
|
||||
// Dialog logic remains the same
|
||||
}
|
||||
}),
|
||||
$el("button.painter-button", {
|
||||
textContent: "Canvas Size",
|
||||
onclick: () => {
|
||||
const dialog = $el("div.painter-dialog", {
|
||||
style: {
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: '1000'
|
||||
}
|
||||
}, [
|
||||
$el("div", {
|
||||
style: {
|
||||
color: "white",
|
||||
marginBottom: "10px"
|
||||
}
|
||||
}, [
|
||||
$el("label", {
|
||||
style: {
|
||||
marginRight: "5px"
|
||||
}
|
||||
}, [
|
||||
$el("span", {}, ["Width: "])
|
||||
]),
|
||||
$el("input", {
|
||||
type: "number",
|
||||
id: "canvas-width",
|
||||
value: canvas.width,
|
||||
min: "1",
|
||||
max: "4096"
|
||||
})
|
||||
]),
|
||||
$el("div", {
|
||||
style: {
|
||||
color: "white",
|
||||
marginBottom: "10px"
|
||||
}
|
||||
}, [
|
||||
$el("label", {
|
||||
style: {
|
||||
marginRight: "5px"
|
||||
}
|
||||
}, [
|
||||
$el("span", {}, ["Height: "])
|
||||
]),
|
||||
$el("input", {
|
||||
type: "number",
|
||||
id: "canvas-height",
|
||||
value: canvas.height,
|
||||
min: "1",
|
||||
max: "4096"
|
||||
})
|
||||
]),
|
||||
$el("div", {
|
||||
style: {
|
||||
textAlign: "right"
|
||||
}
|
||||
}, [
|
||||
$el("button", {
|
||||
id: "cancel-size",
|
||||
textContent: "Cancel"
|
||||
}),
|
||||
$el("button", {
|
||||
id: "confirm-size",
|
||||
textContent: "OK"
|
||||
})
|
||||
])
|
||||
]);
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
document.getElementById('confirm-size').onclick = () => {
|
||||
const width = parseInt(document.getElementById('canvas-width').value) || canvas.width;
|
||||
const height = parseInt(document.getElementById('canvas-height').value) || canvas.height;
|
||||
canvas.updateCanvasSize(width, height);
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
|
||||
document.getElementById('cancel-size').onclick = () => {
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
}
|
||||
}),
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Remove Layer",
|
||||
onclick: () => {
|
||||
@@ -485,10 +575,128 @@ async function createCanvasWidget(node, widget, app) {
|
||||
|
||||
$el("div.painter-separator"),
|
||||
|
||||
// --- Group: Cache ---
|
||||
$el("div.painter-button-group", {}, [
|
||||
$el("button.painter-button", {
|
||||
textContent: "Clear Cache",
|
||||
// --- Group: Tools & History ---
|
||||
$el("div.painter-button-group", {}, [
|
||||
$el("button.painter-button.requires-selection.matting-button", {
|
||||
textContent: "Matting",
|
||||
onclick: async (e) => {
|
||||
const button = e.target.closest('.matting-button');
|
||||
if (button.classList.contains('loading')) return;
|
||||
const spinner = $el("div.matting-spinner");
|
||||
button.appendChild(spinner);
|
||||
button.classList.add('loading');
|
||||
|
||||
try {
|
||||
if (canvas.selectedLayers.length !== 1) throw new Error("Please select exactly one image layer for matting.");
|
||||
|
||||
const selectedLayer = canvas.selectedLayers[0];
|
||||
const imageData = await canvas.getLayerImageData(selectedLayer);
|
||||
const response = await fetch("/matting", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({image: imageData})
|
||||
});
|
||||
if (!response.ok) throw new Error(`Server error: ${response.status} - ${response.statusText}`);
|
||||
|
||||
const result = await response.json();
|
||||
const mattedImage = new Image();
|
||||
mattedImage.src = result.matted_image;
|
||||
await mattedImage.decode();
|
||||
const newLayer = { ...selectedLayer, image: mattedImage, zIndex: canvas.layers.length };
|
||||
canvas.layers.push(newLayer);
|
||||
canvas.updateSelection([newLayer]);
|
||||
canvas.render();
|
||||
canvas.saveState();
|
||||
await canvas.saveToServer(widget.value);
|
||||
app.graph.runStep();
|
||||
} catch (error) {
|
||||
console.error("Matting error:", error);
|
||||
alert(`Error during matting process: ${error.message}`);
|
||||
} finally {
|
||||
button.classList.remove('loading');
|
||||
button.removeChild(spinner);
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el("button.painter-button", { id: `undo-button-${node.id}`, textContent: "Undo", disabled: true, onclick: () => canvas.undo() }),
|
||||
$el("button.painter-button", { id: `redo-button-${node.id}`, textContent: "Redo", disabled: true, onclick: () => canvas.redo() }),
|
||||
]),
|
||||
|
||||
$el("div.painter-separator"),
|
||||
|
||||
// --- Group: Masking ---
|
||||
$el("div.painter-button-group", { id: "mask-controls" }, [
|
||||
$el("button.painter-button", {
|
||||
id: "mask-mode-btn",
|
||||
textContent: "Draw Mask",
|
||||
onclick: () => {
|
||||
const maskBtn = controlPanel.querySelector('#mask-mode-btn');
|
||||
const maskControls = controlPanel.querySelector('#mask-controls');
|
||||
|
||||
if (canvas.maskTool.isActive) {
|
||||
canvas.maskTool.deactivate();
|
||||
maskBtn.classList.remove('primary');
|
||||
maskControls.querySelectorAll('.mask-control').forEach(c => c.style.display = 'none');
|
||||
} else {
|
||||
canvas.maskTool.activate();
|
||||
maskBtn.classList.add('primary');
|
||||
maskControls.querySelectorAll('.mask-control').forEach(c => c.style.display = 'flex');
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
|
||||
$el("label", { for: "brush-size-slider", textContent: "Size:" }),
|
||||
$el("input", {
|
||||
id: "brush-size-slider",
|
||||
type: "range",
|
||||
min: "1",
|
||||
max: "200",
|
||||
value: "20",
|
||||
oninput: (e) => canvas.maskTool.setBrushSize(parseInt(e.target.value))
|
||||
})
|
||||
]),
|
||||
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
|
||||
$el("label", { for: "brush-strength-slider", textContent: "Strength:" }),
|
||||
$el("input", {
|
||||
id: "brush-strength-slider",
|
||||
type: "range",
|
||||
min: "0",
|
||||
max: "1",
|
||||
step: "0.05",
|
||||
value: "0.5",
|
||||
oninput: (e) => canvas.maskTool.setBrushStrength(parseFloat(e.target.value))
|
||||
})
|
||||
]),
|
||||
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
|
||||
$el("label", { for: "brush-softness-slider", textContent: "Softness:" }),
|
||||
$el("input", {
|
||||
id: "brush-softness-slider",
|
||||
type: "range",
|
||||
min: "0",
|
||||
max: "1",
|
||||
step: "0.05",
|
||||
value: "0.5",
|
||||
oninput: (e) => canvas.maskTool.setBrushSoftness(parseFloat(e.target.value))
|
||||
})
|
||||
]),
|
||||
$el("button.painter-button.mask-control", {
|
||||
textContent: "Clear Mask",
|
||||
style: { display: 'none' },
|
||||
onclick: () => {
|
||||
if (confirm("Are you sure you want to clear the mask?")) {
|
||||
canvas.maskTool.clear();
|
||||
canvas.render();
|
||||
}
|
||||
}
|
||||
})
|
||||
]),
|
||||
|
||||
$el("div.painter-separator"),
|
||||
|
||||
// --- Group: Cache ---
|
||||
$el("div.painter-button-group", {}, [
|
||||
$el("button.painter-button", {
|
||||
textContent: "Clear Cache",
|
||||
style: { backgroundColor: "#c54747", borderColor: "#a53737" },
|
||||
onclick: async () => {
|
||||
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
|
||||
@@ -503,7 +711,8 @@ async function createCanvasWidget(node, widget, app) {
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
$el("div.painter-separator")
|
||||
]);
|
||||
|
||||
|
||||
|
||||
142
js/Mask_tool.js
Normal file
142
js/Mask_tool.js
Normal file
@@ -0,0 +1,142 @@
|
||||
export class MaskTool {
|
||||
constructor(canvasInstance) {
|
||||
this.canvasInstance = canvasInstance;
|
||||
this.mainCanvas = canvasInstance.canvas;
|
||||
this.maskCanvas = document.createElement('canvas');
|
||||
this.maskCtx = this.maskCanvas.getContext('2d');
|
||||
|
||||
this.isActive = false;
|
||||
this.brushSize = 20;
|
||||
this.brushStrength = 0.5;
|
||||
this.brushSoftness = 0.5; // Domyślna miękkość pędzla (0 - twardy, 1 - bardzo miękki)
|
||||
this.isDrawing = false;
|
||||
this.lastPosition = null;
|
||||
|
||||
this.initMaskCanvas();
|
||||
}
|
||||
|
||||
setBrushSoftness(softness) {
|
||||
this.brushSoftness = Math.max(0, Math.min(1, softness));
|
||||
}
|
||||
|
||||
initMaskCanvas() {
|
||||
this.maskCanvas.width = this.mainCanvas.width;
|
||||
this.maskCanvas.height = this.mainCanvas.height;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
activate() {
|
||||
this.isActive = true;
|
||||
this.canvasInstance.interaction.mode = 'drawingMask';
|
||||
console.log("Mask tool activated");
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this.isActive = false;
|
||||
this.canvasInstance.interaction.mode = 'none';
|
||||
console.log("Mask tool deactivated");
|
||||
}
|
||||
|
||||
setBrushSize(size) {
|
||||
this.brushSize = Math.max(1, size);
|
||||
}
|
||||
|
||||
setBrushStrength(strength) {
|
||||
this.brushStrength = Math.max(0, Math.min(1, strength));
|
||||
}
|
||||
|
||||
handleMouseDown(worldCoords) {
|
||||
if (!this.isActive) return;
|
||||
this.isDrawing = true;
|
||||
this.lastPosition = worldCoords;
|
||||
this.draw(worldCoords);
|
||||
}
|
||||
|
||||
handleMouseMove(worldCoords) {
|
||||
if (!this.isActive || !this.isDrawing) return;
|
||||
this.draw(worldCoords);
|
||||
this.lastPosition = worldCoords;
|
||||
}
|
||||
|
||||
handleMouseUp() {
|
||||
if (!this.isActive) return;
|
||||
this.isDrawing = false;
|
||||
this.lastPosition = null;
|
||||
}
|
||||
|
||||
draw(worldCoords) {
|
||||
if (!this.lastPosition) {
|
||||
this.lastPosition = worldCoords;
|
||||
}
|
||||
|
||||
this.maskCtx.beginPath();
|
||||
this.maskCtx.moveTo(this.lastPosition.x, this.lastPosition.y);
|
||||
this.maskCtx.lineTo(worldCoords.x, worldCoords.y);
|
||||
|
||||
// Utwórz gradient radialny dla miękkości pędzla
|
||||
const gradientRadius = this.brushSize / 2;
|
||||
const softnessFactor = this.brushSoftness * gradientRadius;
|
||||
const gradient = this.maskCtx.createRadialGradient(
|
||||
worldCoords.x, worldCoords.y, gradientRadius - softnessFactor,
|
||||
worldCoords.x, worldCoords.y, gradientRadius
|
||||
);
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${this.brushStrength})`);
|
||||
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
||||
|
||||
this.maskCtx.strokeStyle = gradient;
|
||||
this.maskCtx.lineWidth = this.brushSize;
|
||||
this.maskCtx.lineCap = 'round';
|
||||
this.maskCtx.lineJoin = 'round';
|
||||
|
||||
this.maskCtx.globalCompositeOperation = 'source-over';
|
||||
this.maskCtx.stroke();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||
}
|
||||
|
||||
getMask() {
|
||||
return this.maskCanvas;
|
||||
}
|
||||
|
||||
getMaskImageWithAlpha() {
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = this.maskCanvas.width;
|
||||
tempCanvas.height = this.maskCanvas.height;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
|
||||
// Kopiuj maskę na tymczasowy canvas
|
||||
tempCtx.drawImage(this.maskCanvas, 0, 0);
|
||||
|
||||
// Pobierz dane pikseli
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
// Modyfikuj kanał alfa, aby zachować zróżnicowaną przezroczystość
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const alpha = data[i]; // Wartość alfa (0-255)
|
||||
data[i] = 255; // Czerwony
|
||||
data[i + 1] = 255; // Zielony
|
||||
data[i + 2] = 255; // Niebieski
|
||||
data[i + 3] = alpha; // Alfa (zachowaj oryginalną wartość)
|
||||
}
|
||||
|
||||
// Zapisz zmodyfikowane dane pikseli
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Utwórz obraz z tymczasowego canvasu
|
||||
const maskImage = new Image();
|
||||
maskImage.src = tempCanvas.toDataURL();
|
||||
return maskImage;
|
||||
}
|
||||
|
||||
resize(width, height){
|
||||
const oldMask = this.maskCanvas;
|
||||
this.maskCanvas = document.createElement('canvas');
|
||||
this.maskCanvas.width = width;
|
||||
this.maskCanvas.height = height;
|
||||
this.maskCtx = this.maskCanvas.getContext('2d');
|
||||
this.maskCtx.drawImage(oldMask, 0, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user