From abb0f8ef5338af91d89ebc39f95040a02f54284c Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 29 Jun 2025 21:26:53 +0200 Subject: [PATCH] Add export of canvas with mask as alpha channel Introduces a new method to export the flattened canvas with the mask applied as the alpha channel. Updates UI actions to allow previewing, copying, and saving the image with mask alpha, and ensures node previews use the new export method. This enhances workflows that require the mask to be embedded as transparency in the output image. --- js/Canvas.js | 11 ++++++- js/CanvasLayers.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++ js/CanvasView.js | 63 ++++++++++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 2 deletions(-) diff --git a/js/Canvas.js b/js/Canvas.js index 6a71db4..065b6da 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -197,6 +197,13 @@ export class Canvas { return this.canvasLayers.getFlattenedCanvasAsBlob(); } + /** + * Eksportuje spłaszczony canvas z maską jako kanałem alpha + */ + async getFlattenedCanvasWithMaskAsBlob() { + return this.canvasLayers.getFlattenedCanvasWithMaskAsBlob(); + } + /** * Importuje najnowszy obraz */ @@ -212,6 +219,7 @@ export class Canvas { * Uruchamia edytor masek */ async startMaskEditor() { + // Dla edytora masek używamy zwykłego spłaszczonego obrazu bez alpha const blob = await this.canvasLayers.getFlattenedCanvasAsBlob(); if (!blob) { log.warn("Canvas is empty, cannot open mask editor."); @@ -446,7 +454,8 @@ export class Canvas { this.saveState(); const new_preview = new Image(); - const blob = await this.canvasLayers.getFlattenedCanvasAsBlob(); + // Użyj nowej metody z maską jako kanałem alpha + const blob = await this.canvasLayers.getFlattenedCanvasWithMaskAsBlob(); if (blob) { new_preview.src = URL.createObjectURL(blob); await new Promise(r => new_preview.onload = r); diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 785bcc0..4466456 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -596,6 +596,84 @@ export class CanvasLayers { }); } + async getFlattenedCanvasWithMaskAsBlob() { + return new Promise((resolve, reject) => { + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.canvas.height; + const tempCtx = tempCanvas.getContext('2d'); + + // Najpierw renderuj wszystkie warstwy + const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex); + + sortedLayers.forEach(layer => { + if (!layer.image) return; + + tempCtx.save(); + tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; + tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + tempCtx.translate(centerX, centerY); + tempCtx.rotate(layer.rotation * Math.PI / 180); + tempCtx.drawImage( + layer.image, + -layer.width / 2, + -layer.height / 2, + layer.width, + layer.height + ); + + tempCtx.restore(); + }); + + // Pobierz dane obrazu + const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); + const data = imageData.data; + + // Pobierz maskę + const maskCanvas = this.canvas.maskTool.getMask(); + + if (maskCanvas && maskCanvas.width > 0 && maskCanvas.height > 0) { + // Stwórz tymczasowy canvas dla maski w rozmiarze output area + const maskTempCanvas = document.createElement('canvas'); + maskTempCanvas.width = this.canvas.width; + maskTempCanvas.height = this.canvas.height; + const maskTempCtx = maskTempCanvas.getContext('2d'); + + // Narysuj odpowiedni fragment maski (uwzględniając pozycję maskTool) + const maskX = -this.canvas.maskTool.x; + const maskY = -this.canvas.maskTool.y; + maskTempCtx.drawImage(maskCanvas, maskX, maskY); + + // Pobierz dane maski + const maskImageData = maskTempCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); + const maskData = maskImageData.data; + + // Zastosuj maskę jako kanał alpha + for (let i = 0; i < data.length; i += 4) { + // Pobierz wartość maski (używamy czerwonego kanału, bo maska jest biała) + const maskValue = maskData[i]; // R kanał maski + + // Maska biała (255) = pełna przezroczystość (alpha = 255) + // Maska czarna (0) = brak przezroczystości (alpha = 0) + data[i + 3] = maskValue; // Ustaw alpha na wartość maski + } + + // Zapisz zmodyfikowane dane obrazu + tempCtx.putImageData(imageData, 0, 0); + } + + tempCanvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error('Canvas toBlob failed.')); + } + }, 'image/png'); + }); + } + async getFlattenedSelectionAsBlob() { if (this.canvas.selectedLayers.length === 0) { return null; diff --git a/js/CanvasView.js b/js/CanvasView.js index 8d73959..8373880 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -917,8 +917,24 @@ async function createCanvasWidget(node, widget, app) { const triggerWidget = node.widgets.find(w => w.name === "trigger"); - const updateOutput = () => { + const updateOutput = async () => { triggerWidget.value = (triggerWidget.value + 1) % 99999999; + + // Aktualizuj podgląd node z maską jako alpha + try { + const new_preview = new Image(); + const blob = await canvas.getFlattenedCanvasWithMaskAsBlob(); + if (blob) { + new_preview.src = URL.createObjectURL(blob); + await new Promise(r => new_preview.onload = r); + node.imgs = [new_preview]; + } else { + node.imgs = []; + } + } catch (error) { + console.error("Error updating node preview:", error); + } + // app.graph.runStep(); // Potentially not needed if we just want to mark dirty }; @@ -1210,6 +1226,19 @@ app.registerExtension({ } }, }, + { + content: "Open Image with Mask Alpha", + callback: async () => { + try { + const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob(); + const url = URL.createObjectURL(blob); + window.open(url, '_blank'); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } catch (e) { + log.error("Error opening image with mask:", e); + } + }, + }, { content: "Copy Image", callback: async () => { @@ -1224,6 +1253,20 @@ app.registerExtension({ } }, }, + { + content: "Copy Image with Mask Alpha", + callback: async () => { + try { + const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob(); + const item = new ClipboardItem({'image/png': blob}); + await navigator.clipboard.write([item]); + log.info("Image with mask alpha copied to clipboard."); + } catch (e) { + log.error("Error copying image with mask:", e); + alert("Failed to copy image with mask to clipboard."); + } + }, + }, { content: "Save Image", callback: async () => { @@ -1242,6 +1285,24 @@ app.registerExtension({ } }, }, + { + content: "Save Image with Mask Alpha", + callback: async () => { + try { + const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'canvas_output_with_mask.png'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } catch (e) { + log.error("Error saving image with mask:", e); + } + }, + }, ]; if (options.length > 0) { options.unshift({content: "___", disabled: true});