From 7c35490e6ecf5e21567d552f4285bcc19fe7ce80 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 26 Jun 2025 00:06:07 +0200 Subject: [PATCH] Refactor mask generation using visibility canvas Replaces per-layer mask processing with a single visibility canvas that composites all layers, then generates the mask based on the resulting alpha values. This simplifies the logic and ensures correct handling of partial transparency across all layers. --- js/Canvas.js | 87 +++++++++++++++++++--------------------------------- 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/js/Canvas.js b/js/Canvas.js index 01c0651..e80d60c 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -668,6 +668,13 @@ export class Canvas { tempCtx.fillStyle = '#ffffff'; tempCtx.fillRect(0, 0, this.width, this.height); + // Tworzymy tymczasowy canvas do renderowania warstw i maski + const visibilityCanvas = document.createElement('canvas'); + visibilityCanvas.width = this.width; + visibilityCanvas.height = this.height; + const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); + + // Czarne tło (całkowicie przezroczyste w masce) maskCtx.fillStyle = '#ffffff'; // Białe tło dla wolnych przestrzeni maskCtx.fillRect(0, 0, this.width, this.height); @@ -677,6 +684,7 @@ export class Canvas { const sortedLayers = this.layers.sort((a, b) => a.zIndex - b.zIndex); log.debug(`Processing ${sortedLayers.length} layers in order`); + // Najpierw renderujemy wszystkie warstwy do głównego obrazu sortedLayers.forEach((layer, index) => { log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`); log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`); @@ -691,62 +699,31 @@ export class Canvas { log.debug(`Layer ${index} rendered successfully`); - maskCtx.save(); - maskCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); - maskCtx.rotate(layer.rotation * Math.PI / 180); - maskCtx.globalCompositeOperation = 'source-over'; // Używamy source-over, aby uwzględnić stopniową przezroczystość - - if (layer.mask) { - // 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; - const layerCtx = layerCanvas.getContext('2d'); - layerCtx.drawImage(layer.image, 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); - } - maskCtx.restore(); + // Renderujemy również do canvas widoczności, aby śledzić, które piksele są widoczne + visibilityCtx.save(); + visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); + visibilityCtx.rotate(layer.rotation * Math.PI / 180); + visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); + visibilityCtx.restore(); }); + + // Teraz tworzymy maskę na podstawie widoczności pikseli, zachowując stopień przezroczystości + const visibilityData = visibilityCtx.getImageData(0, 0, this.width, this.height); + const maskData = maskCtx.getImageData(0, 0, this.width, this.height); + + // Używamy wartości alpha do określenia stopnia przezroczystości w masce + for (let i = 0; i < visibilityData.data.length; i += 4) { + const alpha = visibilityData.data[i + 3]; + // Odwracamy wartość alpha (255 - alpha), aby zachować logikę maski: + // - Przezroczyste piksele w obrazie (alpha = 0) -> białe w masce (255) + // - Nieprzezroczyste piksele w obrazie (alpha = 255) -> czarne w masce (0) + // - Częściowo przezroczyste piksele zachowują proporcjonalną wartość + const maskValue = 255 - alpha; + maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue; + maskData.data[i + 3] = 255; // Maska zawsze ma pełną nieprzezroczystość + } + + maskCtx.putImageData(maskData, 0, 0); // Nałóż maskę z narzędzia MaskTool, uwzględniając przezroczystość pędzla const toolMaskCanvas = this.maskTool.getMask();