diff --git a/js/CanvasIO.js b/js/CanvasIO.js index 7c3c704..eea43a3 100644 --- a/js/CanvasIO.js +++ b/js/CanvasIO.js @@ -77,43 +77,42 @@ export class CanvasIO { } maskCtx.putImageData(maskData, 0, 0); this.canvas.outputAreaShape = originalShape; - const toolMaskCanvas = this.canvas.maskTool.getMask(); + // Use optimized getMaskForOutputArea() instead of getMask() for better performance + // This only processes chunks that overlap with the output area + const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea(); if (toolMaskCanvas) { - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = this.canvas.width; - tempMaskCanvas.height = this.canvas.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); - if (!tempMaskCtx) - throw new Error("Could not create temp mask context"); - tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); - const maskX = this.canvas.maskTool.x; - const maskY = this.canvas.maskTool.y; - log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`); - const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading - const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); // Where in the output canvas to start writing - const destY = Math.max(0, maskY); - const copyWidth = Math.min(toolMaskCanvas.width - sourceX, // Available width in source - this.canvas.width - destX // Available width in destination - ); - const copyHeight = Math.min(toolMaskCanvas.height - sourceY, // Available height in source - this.canvas.height - destY // Available height in destination - ); - if (copyWidth > 0 && copyHeight > 0) { - log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`); - tempMaskCtx.drawImage(toolMaskCanvas, sourceX, sourceY, copyWidth, copyHeight, // Source rectangle - destX, destY, copyWidth, copyHeight // Destination rectangle - ); + log.debug(`Using optimized output area mask (${toolMaskCanvas.width}x${toolMaskCanvas.height}) instead of full mask`); + // The optimized mask is already sized and positioned for the output area + // So we can draw it directly without complex positioning calculations + const tempMaskData = toolMaskCanvas.getContext('2d', { willReadFrequently: true })?.getImageData(0, 0, toolMaskCanvas.width, toolMaskCanvas.height); + if (tempMaskData) { + // Ensure the mask data is in the correct format (white with alpha) + 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; + } + // Create a temporary canvas to hold the processed mask + const tempMaskCanvas = document.createElement('canvas'); + tempMaskCanvas.width = this.canvas.width; + tempMaskCanvas.height = this.canvas.height; + const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); + if (!tempMaskCtx) + throw new Error("Could not create temp mask context"); + // Put the processed mask data into a canvas that matches the output area size + const outputMaskCanvas = document.createElement('canvas'); + outputMaskCanvas.width = toolMaskCanvas.width; + outputMaskCanvas.height = toolMaskCanvas.height; + const outputMaskCtx = outputMaskCanvas.getContext('2d', { willReadFrequently: true }); + if (!outputMaskCtx) + throw new Error("Could not create output mask context"); + outputMaskCtx.putImageData(tempMaskData, 0, 0); + // Draw the optimized mask at the correct position (output area bounds) + const bounds = this.canvas.outputAreaBounds; + tempMaskCtx.drawImage(outputMaskCanvas, bounds.x, bounds.y); + maskCtx.globalCompositeOperation = 'source-over'; + maskCtx.drawImage(tempMaskCanvas, 0, 0); } - const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); - 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; - } - tempMaskCtx.putImageData(tempMaskData, 0, 0); - maskCtx.globalCompositeOperation = 'source-over'; - maskCtx.drawImage(tempMaskCanvas, 0, 0); } if (outputMode === 'ram') { const imageData = tempCanvas.toDataURL('image/png'); diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 4ab202e..ddc0b11 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -118,6 +118,11 @@ export class CanvasInteractions { if (e.shiftKey) { // Clear custom shape when starting canvas resize if (this.canvas.outputAreaShape) { + // If auto-apply shape mask is enabled, remove the mask before clearing the shape + if (this.canvas.autoApplyShapeMask) { + log.info("Removing shape mask before clearing custom shape for canvas resize"); + this.canvas.maskTool.removeShapeMask(); + } this.canvas.outputAreaShape = null; this.canvas.render(); } @@ -822,6 +827,11 @@ export class CanvasInteractions { const boundingBox = this.canvas.shapeTool.getBoundingBox(); if (boundingBox && boundingBox.width > 1 && boundingBox.height > 1) { this.canvas.saveState(); + // If there's an existing custom shape and auto-apply shape mask is enabled, remove the previous mask + if (this.canvas.outputAreaShape && this.canvas.autoApplyShapeMask) { + log.info("Removing previous shape mask before defining new custom shape"); + this.canvas.maskTool.removeShapeMask(); + } this.canvas.outputAreaShape = { ...shape, points: shape.points.map((p) => ({ @@ -866,6 +876,11 @@ export class CanvasInteractions { } // Update mask canvas to ensure it covers the new output area position this.canvas.maskTool.updateMaskCanvasForOutputArea(); + // If auto-apply shape mask is enabled, automatically apply the mask with current settings + if (this.canvas.autoApplyShapeMask) { + log.info("Auto-applying shape mask to new custom shape with current settings"); + this.canvas.maskTool.applyShapeMask(); + } this.canvas.saveState(); this.canvas.render(); } diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index ce7fed6..197c8c1 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -843,48 +843,24 @@ export class CanvasLayers { if (applyMask) { const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); const data = imageData.data; - const toolMaskCanvas = this.canvas.maskTool.getMask(); + // Use optimized getMaskForOutputArea() for better performance + // This only processes chunks that overlap with the output area + const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea(); if (toolMaskCanvas) { - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = bounds.width; - tempMaskCanvas.height = bounds.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); - if (!tempMaskCtx) { - reject(new Error("Could not create mask canvas context")); - return; + log.debug(`Using optimized output area mask (${toolMaskCanvas.width}x${toolMaskCanvas.height}) for _generateCanvasBlob`); + // The optimized mask is already sized and positioned for the output area + // So we can apply it directly without complex positioning calculations + const maskImageData = toolMaskCanvas.getContext('2d', { willReadFrequently: true })?.getImageData(0, 0, toolMaskCanvas.width, toolMaskCanvas.height); + if (maskImageData) { + const maskData = maskImageData.data; + for (let i = 0; i < data.length; i += 4) { + const originalAlpha = data[i + 3]; + const maskAlpha = maskData[i + 3] / 255; + const invertedMaskAlpha = 1 - maskAlpha; + data[i + 3] = originalAlpha * invertedMaskAlpha; + } + tempCtx.putImageData(imageData, 0, 0); } - tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); - // Pozycja maski w świecie (bez przesunięcia względem bounds) - const maskWorldX = this.canvas.maskTool.x; - const maskWorldY = this.canvas.maskTool.y; - // Pozycja maski względem output bounds (gdzie ma być narysowana w output canvas) - const maskX = maskWorldX - bounds.x; - const maskY = maskWorldY - bounds.y; - const sourceX = Math.max(0, -maskX); - const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); - const destY = Math.max(0, maskY); - const copyWidth = Math.min(toolMaskCanvas.width - sourceX, bounds.width - destX); - const copyHeight = Math.min(toolMaskCanvas.height - sourceY, bounds.height - destY); - if (copyWidth > 0 && copyHeight > 0) { - tempMaskCtx.drawImage(toolMaskCanvas, sourceX, sourceY, copyWidth, copyHeight, destX, destY, copyWidth, copyHeight); - } - const tempMaskData = tempMaskCtx.getImageData(0, 0, bounds.width, bounds.height); - 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; - } - tempMaskCtx.putImageData(tempMaskData, 0, 0); - const maskImageData = tempMaskCtx.getImageData(0, 0, bounds.width, bounds.height); - const maskData = maskImageData.data; - for (let i = 0; i < data.length; i += 4) { - const originalAlpha = data[i + 3]; - const maskAlpha = maskData[i + 3] / 255; - const invertedMaskAlpha = 1 - maskAlpha; - data[i + 3] = originalAlpha * invertedMaskAlpha; - } - tempCtx.putImageData(imageData, 0, 0); } } tempCanvas.toBlob((blob) => { @@ -963,43 +939,31 @@ export class CanvasLayers { maskData.data[i + 3] = 255; // Solidna maska } maskCtx.putImageData(maskData, 0, 0); - // Aplikuj maskę narzędzia jeśli istnieje - const toolMaskCanvas = this.canvas.maskTool.getMask(); + // Aplikuj maskę narzędzia jeśli istnieje - używaj zoptymalizowanej metody + const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea(); if (toolMaskCanvas) { - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = bounds.width; - tempMaskCanvas.height = bounds.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); - if (!tempMaskCtx) { - reject(new Error("Could not create temp mask context")); - return; + log.debug(`[getFlattenedMaskAsBlob] Using optimized output area mask (${toolMaskCanvas.width}x${toolMaskCanvas.height})`); + // Zoptymalizowana maska jest już odpowiednio pozycjonowana dla output area + // Możemy ją zastosować bezpośrednio + const tempMaskData = toolMaskCanvas.getContext('2d', { willReadFrequently: true })?.getImageData(0, 0, toolMaskCanvas.width, toolMaskCanvas.height); + if (tempMaskData) { + // Konwertuj dane maski do odpowiedniego formatu + 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] = alpha; + tempMaskData.data[i + 3] = 255; // Solidna alpha + } + // Stwórz tymczasowy canvas dla przetworzonej maski + const tempMaskCanvas = document.createElement('canvas'); + tempMaskCanvas.width = toolMaskCanvas.width; + tempMaskCanvas.height = toolMaskCanvas.height; + const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); + if (tempMaskCtx) { + tempMaskCtx.putImageData(tempMaskData, 0, 0); + maskCtx.globalCompositeOperation = 'screen'; + maskCtx.drawImage(tempMaskCanvas, 0, 0); + } } - tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); - // Pozycja maski w świecie (bez przesunięcia względem bounds) - const maskWorldX = this.canvas.maskTool.x; - const maskWorldY = this.canvas.maskTool.y; - // Pozycja maski względem output bounds (gdzie ma być narysowana w output canvas) - const maskX = maskWorldX - bounds.x; - const maskY = maskWorldY - bounds.y; - log.debug(`[getFlattenedMaskAsBlob] Mask world position (${maskWorldX}, ${maskWorldY}) relative to bounds (${maskX}, ${maskY})`); - const sourceX = Math.max(0, -maskX); - const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); - const destY = Math.max(0, maskY); - const copyWidth = Math.min(toolMaskCanvas.width - sourceX, bounds.width - destX); - const copyHeight = Math.min(toolMaskCanvas.height - sourceY, bounds.height - destY); - if (copyWidth > 0 && copyHeight > 0) { - tempMaskCtx.drawImage(toolMaskCanvas, sourceX, sourceY, copyWidth, copyHeight, destX, destY, copyWidth, copyHeight); - } - const tempMaskData = tempMaskCtx.getImageData(0, 0, bounds.width, bounds.height); - 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] = alpha; - tempMaskData.data[i + 3] = 255; // Solidna alpha - } - tempMaskCtx.putImageData(tempMaskData, 0, 0); - maskCtx.globalCompositeOperation = 'screen'; - maskCtx.drawImage(tempMaskCanvas, 0, 0); } log.info("=== MASK BLOB GENERATED ==="); maskCanvas.toBlob((blob) => { diff --git a/js/CustomShapeMenu.js b/js/CustomShapeMenu.js index 8d7304e..a1a0be0 100644 --- a/js/CustomShapeMenu.js +++ b/js/CustomShapeMenu.js @@ -148,13 +148,17 @@ export class CustomShapeMenu { margin-top: 2px; color: #aaa; `; + let expansionValueBeforeDrag = this.canvas.shapeMaskExpansionValue; const updateExpansionSliderDisplay = () => { const value = parseInt(expansionSlider.value); this.canvas.shapeMaskExpansionValue = value; expansionValueDisplay.textContent = value > 0 ? `+${value}px` : `${value}px`; }; let isExpansionDragging = false; - expansionSlider.onmousedown = () => { isExpansionDragging = true; }; + expansionSlider.onmousedown = () => { + isExpansionDragging = true; + expansionValueBeforeDrag = this.canvas.shapeMaskExpansionValue; // Store value before dragging + }; expansionSlider.oninput = () => { updateExpansionSliderDisplay(); if (this.canvas.autoApplyShapeMask) { @@ -172,6 +176,16 @@ export class CustomShapeMenu { expansionSlider.onmouseup = () => { isExpansionDragging = false; if (this.canvas.autoApplyShapeMask) { + const finalValue = parseInt(expansionSlider.value); + // If value changed during drag, remove old mask with previous expansion value + if (expansionValueBeforeDrag !== finalValue) { + // Temporarily set the previous value to remove the old mask properly + const tempValue = this.canvas.shapeMaskExpansionValue; + this.canvas.shapeMaskExpansionValue = expansionValueBeforeDrag; + this.canvas.maskTool.removeShapeMask(); + this.canvas.shapeMaskExpansionValue = tempValue; // Restore current value + log.info(`Removed old shape mask with expansion: ${expansionValueBeforeDrag}px before applying new value: ${finalValue}px`); + } this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(true); this.canvas.render(); diff --git a/js/MaskTool.js b/js/MaskTool.js index 16499a0..3f1053b 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -3,6 +3,7 @@ const log = createModuleLogger('Mask_tool'); export class MaskTool { constructor(canvasInstance, callbacks = {}) { this.ACTIVE_MASK_UPDATE_DELAY = 16; // ~60fps throttling + this.SHAPE_PREVIEW_THROTTLE_DELAY = 16; // ~60fps throttling for preview this.canvasInstance = canvasInstance; this.mainCanvas = canvasInstance.canvas; this.onStateChange = callbacks.onStateChange || null; @@ -50,6 +51,9 @@ export class MaskTool { // Initialize performance optimization flags this.activeMaskNeedsUpdate = false; this.activeMaskUpdateTimeout = null; + // Initialize shape preview throttling + this.shapePreviewThrottleTimeout = null; + this.pendingPreviewParams = null; this.initMaskCanvas(); } // Temporary compatibility getters - will be replaced with chunked system @@ -537,6 +541,7 @@ export class MaskTool { /** * Prepares shape mask configuration data - eliminates duplication between applyShapeMask and removeShapeMask * Returns all necessary data for shape mask operations including world coordinates and temporary canvas setup + * Now uses precise expansion calculation based on actual user values */ prepareShapeMaskConfiguration() { // Validate shape @@ -551,12 +556,14 @@ export class MaskTool { x: bounds.x + extensionOffset.x + p.x, y: bounds.y + extensionOffset.y + p.y })); - // Create a temporary canvas large enough to contain the shape and any expansion - const maxExpansion = Math.max(300, Math.abs(this.canvasInstance.shapeMaskExpansionValue || 0)); - const tempCanvasWidth = bounds.width + (maxExpansion * 2); - const tempCanvasHeight = bounds.height + (maxExpansion * 2); - const tempOffsetX = maxExpansion; - const tempOffsetY = maxExpansion; + // Use precise expansion calculation - only actual user value + small safety margin + const userExpansionValue = Math.abs(this.canvasInstance.shapeMaskExpansionValue || 0); + const safetyMargin = 10; // Small safety margin for precise operations + const preciseExpansion = userExpansionValue + safetyMargin; + const tempCanvasWidth = bounds.width + (preciseExpansion * 2); + const tempCanvasHeight = bounds.height + (preciseExpansion * 2); + const tempOffsetX = preciseExpansion; + const tempOffsetY = preciseExpansion; // Adjust shape points for the temporary canvas const tempShapePoints = worldShapePoints.map(p => ({ x: p.x - bounds.x + tempOffsetX, @@ -567,7 +574,7 @@ export class MaskTool { bounds, extensionOffset, worldShapePoints, - maxExpansion, + maxExpansion: preciseExpansion, tempCanvasWidth, tempCanvasHeight, tempOffsetX, @@ -943,6 +950,25 @@ export class MaskTool { * Show blue outline preview of expansion/contraction during slider adjustment */ showShapePreview(expansionValue, featherValue = 0) { + // Store the parameters for throttled execution + this.pendingPreviewParams = { expansionValue, featherValue }; + // If there's already a pending preview update, don't schedule another one + if (this.shapePreviewThrottleTimeout !== null) { + return; + } + // Schedule the preview update with throttling + this.shapePreviewThrottleTimeout = window.setTimeout(() => { + if (this.pendingPreviewParams) { + this.executeShapePreview(this.pendingPreviewParams.expansionValue, this.pendingPreviewParams.featherValue); + this.pendingPreviewParams = null; + } + this.shapePreviewThrottleTimeout = null; + }, this.SHAPE_PREVIEW_THROTTLE_DELAY); + } + /** + * Executes the actual shape preview rendering - separated from showShapePreview for throttling + */ + executeShapePreview(expansionValue, featherValue = 0) { if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) { return; } @@ -975,7 +1001,7 @@ export class MaskTool { const allFeatherContours = this._calculatePreviewPointsScreen(allContours, -featherValue, viewport.zoom); this.drawContoursForPreview(this.shapePreviewCtx, allFeatherContours, '#4A9EFF', 1, [3, 5], 0.6); } - log.debug(`Shape preview shown with expansion: ${expansionValue}px, feather: ${featherValue}px at bounds (${bounds.x}, ${bounds.y})`); + log.debug(`Shape preview executed with expansion: ${expansionValue}px, feather: ${featherValue}px at bounds (${bounds.x}, ${bounds.y})`); } /** * Hide shape preview and switch back to normal mode @@ -1230,6 +1256,47 @@ export class MaskTool { } return this.activeMaskCanvas; } + /** + * Gets mask only for the output area - optimized for performance + * Returns only the portion of the mask that overlaps with the output area + * This is much more efficient than returning the entire mask when there are many chunks + */ + getMaskForOutputArea() { + const bounds = this.canvasInstance.outputAreaBounds; + // Create canvas sized to output area + const outputMaskCanvas = document.createElement('canvas'); + outputMaskCanvas.width = bounds.width; + outputMaskCanvas.height = bounds.height; + const outputMaskCtx = outputMaskCanvas.getContext('2d', { willReadFrequently: true }); + if (!outputMaskCtx) { + throw new Error("Failed to get 2D context for output area mask canvas"); + } + // Calculate which chunks overlap with the output area + const outputLeft = bounds.x; + const outputTop = bounds.y; + const outputRight = bounds.x + bounds.width; + const outputBottom = bounds.y + bounds.height; + const chunkBounds = this.calculateChunkBounds(outputLeft, outputTop, outputRight, outputBottom); + // Only process chunks that overlap with output area + for (let chunkY = chunkBounds.minY; chunkY <= chunkBounds.maxY; chunkY++) { + for (let chunkX = chunkBounds.minX; chunkX <= chunkBounds.maxX; chunkX++) { + const chunkKey = `${chunkX},${chunkY}`; + const chunk = this.maskChunks.get(chunkKey); + if (chunk && !chunk.isEmpty) { + // Calculate intersection between chunk and output area + const intersection = this.calculateChunkIntersection(chunk, outputLeft, outputTop, outputRight, outputBottom); + if (intersection) { + // Draw only the intersecting portion + outputMaskCtx.drawImage(chunk.canvas, intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight, // Source from chunk + intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight // Destination on output canvas + ); + } + } + } + } + log.debug(`Generated output area mask (${bounds.width}x${bounds.height}) from ${chunkBounds.maxX - chunkBounds.minX + 1}x${chunkBounds.maxY - chunkBounds.minY + 1} chunks`); + return outputMaskCanvas; + } resize(width, height) { this.initPreviewCanvas(); const oldMask = this.maskCanvas; @@ -1512,20 +1579,22 @@ export class MaskTool { const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0; // Create a removal mask canvas - always hard-edged to ensure complete removal let removalMaskCanvas; + // Add safety margin to ensure complete removal of antialiasing artifacts + const safetyMargin = 2; // 2px margin to remove any antialiasing remnants if (needsExpansion) { - // If expansion was active, remove the expanded area with a hard edge - removalMaskCanvas = this._createExpandedMaskCanvas(config.tempShapePoints, this.canvasInstance.shapeMaskExpansionValue, config.tempCanvasWidth, config.tempCanvasHeight); + // If expansion was active, remove exactly the user's expansion value + safety margin + const userExpansionValue = this.canvasInstance.shapeMaskExpansionValue; + const expandedValue = Math.abs(userExpansionValue) + safetyMargin; + removalMaskCanvas = this._createExpandedMaskCanvas(config.tempShapePoints, expandedValue, config.tempCanvasWidth, config.tempCanvasHeight); } else { - // If no expansion, just remove the base shape with a hard edge - const { canvas, ctx } = this.createCanvas(config.tempCanvasWidth, config.tempCanvasHeight); - removalMaskCanvas = canvas; - this.drawShapeOnCanvas(ctx, config.tempShapePoints, 'evenodd'); + // If no expansion, remove the base shape with safety margin only + removalMaskCanvas = this._createExpandedMaskCanvas(config.tempShapePoints, safetyMargin, config.tempCanvasWidth, config.tempCanvasHeight); } // Now remove the shape mask from the chunked system this.removeMaskCanvasFromChunks(removalMaskCanvas, config.bounds.x - config.tempOffsetX, config.bounds.y - config.tempOffsetY); // Update the active mask canvas to show the changes - this.updateActiveMaskCanvas(); + this.updateActiveMaskCanvas(true); // Force full update to ensure all chunks are properly updated if (this.onStateChange) { this.onStateChange(); } diff --git a/js/SAMDetectorIntegration.js b/js/SAMDetectorIntegration.js index 97b7744..1a9cdb7 100644 --- a/js/SAMDetectorIntegration.js +++ b/js/SAMDetectorIntegration.js @@ -289,16 +289,26 @@ async function handleSAMDetectorResult(node, resultImage) { showNotification("Failed to load SAM Detector result. The mask file may not be available.", "#c54747", 5000); return; } - // Create temporary canvas for mask processing (same as CanvasMask) + // Create temporary canvas for mask processing with correct positioning log.debug("Creating temporary canvas for mask processing"); + // Get the output area bounds to position the mask correctly + const bounds = canvas.outputAreaBounds; + log.debug("Output area bounds for SAM mask positioning:", bounds); + // Create canvas sized to match the result image dimensions const tempCanvas = document.createElement('canvas'); - tempCanvas.width = canvas.width; - tempCanvas.height = canvas.height; + tempCanvas.width = resultImage.width; + tempCanvas.height = resultImage.height; const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); if (tempCtx) { - tempCtx.drawImage(resultImage, 0, 0, canvas.width, canvas.height); - log.debug("Processing image data to create mask"); - const imageData = tempCtx.getImageData(0, 0, canvas.width, canvas.height); + // Draw the result image at its natural size (no scaling) + tempCtx.drawImage(resultImage, 0, 0); + log.debug("Processing image data to create mask", { + imageWidth: resultImage.width, + imageHeight: resultImage.height, + boundsX: bounds.x, + boundsY: bounds.y + }); + const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); const data = imageData.data; // Convert to mask format (same as CanvasMask) for (let i = 0; i < data.length; i += 4) { diff --git a/src/CanvasIO.ts b/src/CanvasIO.ts index 126d502..4ff2e0b 100644 --- a/src/CanvasIO.ts +++ b/src/CanvasIO.ts @@ -92,57 +92,46 @@ export class CanvasIO { this.canvas.outputAreaShape = originalShape; - const toolMaskCanvas = this.canvas.maskTool.getMask(); + // Use optimized getMaskForOutputArea() instead of getMask() for better performance + // This only processes chunks that overlap with the output area + const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea(); if (toolMaskCanvas) { + log.debug(`Using optimized output area mask (${toolMaskCanvas.width}x${toolMaskCanvas.height}) instead of full mask`); - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = this.canvas.width; - tempMaskCanvas.height = this.canvas.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); - if (!tempMaskCtx) throw new Error("Could not create temp mask context"); - - tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); - - - const maskX = this.canvas.maskTool.x; - const maskY = this.canvas.maskTool.y; - - log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`); - - const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading - const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); // Where in the output canvas to start writing - const destY = Math.max(0, maskY); - - const copyWidth = Math.min( - toolMaskCanvas.width - sourceX, // Available width in source - this.canvas.width - destX // Available width in destination - ); - const copyHeight = Math.min( - toolMaskCanvas.height - sourceY, // Available height in source - this.canvas.height - destY // Available height in destination - ); - - if (copyWidth > 0 && copyHeight > 0) { - log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`); - - tempMaskCtx.drawImage( - toolMaskCanvas, - sourceX, sourceY, copyWidth, copyHeight, // Source rectangle - destX, destY, copyWidth, copyHeight // Destination rectangle - ); + // The optimized mask is already sized and positioned for the output area + // So we can draw it directly without complex positioning calculations + const tempMaskData = toolMaskCanvas.getContext('2d', { willReadFrequently: true })?.getImageData(0, 0, toolMaskCanvas.width, toolMaskCanvas.height); + if (tempMaskData) { + // Ensure the mask data is in the correct format (white with alpha) + 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; + } + + // Create a temporary canvas to hold the processed mask + const tempMaskCanvas = document.createElement('canvas'); + tempMaskCanvas.width = this.canvas.width; + tempMaskCanvas.height = this.canvas.height; + const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); + if (!tempMaskCtx) throw new Error("Could not create temp mask context"); + + // Put the processed mask data into a canvas that matches the output area size + const outputMaskCanvas = document.createElement('canvas'); + outputMaskCanvas.width = toolMaskCanvas.width; + outputMaskCanvas.height = toolMaskCanvas.height; + const outputMaskCtx = outputMaskCanvas.getContext('2d', { willReadFrequently: true }); + if (!outputMaskCtx) throw new Error("Could not create output mask context"); + + outputMaskCtx.putImageData(tempMaskData, 0, 0); + + // Draw the optimized mask at the correct position (output area bounds) + const bounds = this.canvas.outputAreaBounds; + tempMaskCtx.drawImage(outputMaskCanvas, bounds.x, bounds.y); + + maskCtx.globalCompositeOperation = 'source-over'; + maskCtx.drawImage(tempMaskCanvas, 0, 0); } - - const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); - 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; - } - tempMaskCtx.putImageData(tempMaskData, 0, 0); - - maskCtx.globalCompositeOperation = 'source-over'; - maskCtx.drawImage(tempMaskCanvas, 0, 0); } if (outputMode === 'ram') { const imageData = tempCanvas.toDataURL('image/png'); diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index d82415e..79f7e51 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -170,6 +170,11 @@ export class CanvasInteractions { if (e.shiftKey) { // Clear custom shape when starting canvas resize if (this.canvas.outputAreaShape) { + // If auto-apply shape mask is enabled, remove the mask before clearing the shape + if (this.canvas.autoApplyShapeMask) { + log.info("Removing shape mask before clearing custom shape for canvas resize"); + this.canvas.maskTool.removeShapeMask(); + } this.canvas.outputAreaShape = null; this.canvas.render(); } @@ -945,6 +950,12 @@ export class CanvasInteractions { if (boundingBox && boundingBox.width > 1 && boundingBox.height > 1) { this.canvas.saveState(); + // If there's an existing custom shape and auto-apply shape mask is enabled, remove the previous mask + if (this.canvas.outputAreaShape && this.canvas.autoApplyShapeMask) { + log.info("Removing previous shape mask before defining new custom shape"); + this.canvas.maskTool.removeShapeMask(); + } + this.canvas.outputAreaShape = { ...shape, points: shape.points.map((p: any) => ({ @@ -999,6 +1010,12 @@ export class CanvasInteractions { // Update mask canvas to ensure it covers the new output area position this.canvas.maskTool.updateMaskCanvasForOutputArea(); + // If auto-apply shape mask is enabled, automatically apply the mask with current settings + if (this.canvas.autoApplyShapeMask) { + log.info("Auto-applying shape mask to new custom shape with current settings"); + this.canvas.maskTool.applyShapeMask(); + } + this.canvas.saveState(); this.canvas.render(); } diff --git a/src/CanvasLayers.ts b/src/CanvasLayers.ts index b2993af..38d2408 100644 --- a/src/CanvasLayers.ts +++ b/src/CanvasLayers.ts @@ -982,60 +982,26 @@ export class CanvasLayers { const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); const data = imageData.data; - const toolMaskCanvas = this.canvas.maskTool.getMask(); + // Use optimized getMaskForOutputArea() for better performance + // This only processes chunks that overlap with the output area + const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea(); if (toolMaskCanvas) { - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = bounds.width; - tempMaskCanvas.height = bounds.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); - if (!tempMaskCtx) { - reject(new Error("Could not create mask canvas context")); - return; + log.debug(`Using optimized output area mask (${toolMaskCanvas.width}x${toolMaskCanvas.height}) for _generateCanvasBlob`); + + // The optimized mask is already sized and positioned for the output area + // So we can apply it directly without complex positioning calculations + const maskImageData = toolMaskCanvas.getContext('2d', { willReadFrequently: true })?.getImageData(0, 0, toolMaskCanvas.width, toolMaskCanvas.height); + if (maskImageData) { + const maskData = maskImageData.data; + + for (let i = 0; i < data.length; i += 4) { + const originalAlpha = data[i + 3]; + const maskAlpha = maskData[i + 3] / 255; + const invertedMaskAlpha = 1 - maskAlpha; + data[i + 3] = originalAlpha * invertedMaskAlpha; + } + tempCtx.putImageData(imageData, 0, 0); } - - tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); - - // Pozycja maski w świecie (bez przesunięcia względem bounds) - const maskWorldX = this.canvas.maskTool.x; - const maskWorldY = this.canvas.maskTool.y; - - // Pozycja maski względem output bounds (gdzie ma być narysowana w output canvas) - const maskX = maskWorldX - bounds.x; - const maskY = maskWorldY - bounds.y; - - const sourceX = Math.max(0, -maskX); - const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); - const destY = Math.max(0, maskY); - const copyWidth = Math.min(toolMaskCanvas.width - sourceX, bounds.width - destX); - const copyHeight = Math.min(toolMaskCanvas.height - sourceY, bounds.height - destY); - - if (copyWidth > 0 && copyHeight > 0) { - tempMaskCtx.drawImage( - toolMaskCanvas, - sourceX, sourceY, copyWidth, copyHeight, - destX, destY, copyWidth, copyHeight - ); - } - - const tempMaskData = tempMaskCtx.getImageData(0, 0, bounds.width, bounds.height); - 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; - } - tempMaskCtx.putImageData(tempMaskData, 0, 0); - - const maskImageData = tempMaskCtx.getImageData(0, 0, bounds.width, bounds.height); - const maskData = maskImageData.data; - - for (let i = 0; i < data.length; i += 4) { - const originalAlpha = data[i + 3]; - const maskAlpha = maskData[i + 3] / 255; - const invertedMaskAlpha = 1 - maskAlpha; - data[i + 3] = originalAlpha * invertedMaskAlpha; - } - tempCtx.putImageData(imageData, 0, 0); } } @@ -1126,56 +1092,34 @@ export class CanvasLayers { } maskCtx.putImageData(maskData, 0, 0); - // Aplikuj maskę narzędzia jeśli istnieje - const toolMaskCanvas = this.canvas.maskTool.getMask(); + // Aplikuj maskę narzędzia jeśli istnieje - używaj zoptymalizowanej metody + const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea(); if (toolMaskCanvas) { - const tempMaskCanvas = document.createElement('canvas'); - tempMaskCanvas.width = bounds.width; - tempMaskCanvas.height = bounds.height; - const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); - if (!tempMaskCtx) { - reject(new Error("Could not create temp mask context")); - return; + log.debug(`[getFlattenedMaskAsBlob] Using optimized output area mask (${toolMaskCanvas.width}x${toolMaskCanvas.height})`); + + // Zoptymalizowana maska jest już odpowiednio pozycjonowana dla output area + // Możemy ją zastosować bezpośrednio + const tempMaskData = toolMaskCanvas.getContext('2d', { willReadFrequently: true })?.getImageData(0, 0, toolMaskCanvas.width, toolMaskCanvas.height); + if (tempMaskData) { + // Konwertuj dane maski do odpowiedniego formatu + 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] = alpha; + tempMaskData.data[i + 3] = 255; // Solidna alpha + } + + // Stwórz tymczasowy canvas dla przetworzonej maski + const tempMaskCanvas = document.createElement('canvas'); + tempMaskCanvas.width = toolMaskCanvas.width; + tempMaskCanvas.height = toolMaskCanvas.height; + const tempMaskCtx = tempMaskCanvas.getContext('2d', { willReadFrequently: true }); + if (tempMaskCtx) { + tempMaskCtx.putImageData(tempMaskData, 0, 0); + + maskCtx.globalCompositeOperation = 'screen'; + maskCtx.drawImage(tempMaskCanvas, 0, 0); + } } - - tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); - - // Pozycja maski w świecie (bez przesunięcia względem bounds) - const maskWorldX = this.canvas.maskTool.x; - const maskWorldY = this.canvas.maskTool.y; - - // Pozycja maski względem output bounds (gdzie ma być narysowana w output canvas) - const maskX = maskWorldX - bounds.x; - const maskY = maskWorldY - bounds.y; - - log.debug(`[getFlattenedMaskAsBlob] Mask world position (${maskWorldX}, ${maskWorldY}) relative to bounds (${maskX}, ${maskY})`); - - const sourceX = Math.max(0, -maskX); - const sourceY = Math.max(0, -maskY); - const destX = Math.max(0, maskX); - const destY = Math.max(0, maskY); - - const copyWidth = Math.min(toolMaskCanvas.width - sourceX, bounds.width - destX); - const copyHeight = Math.min(toolMaskCanvas.height - sourceY, bounds.height - destY); - - if (copyWidth > 0 && copyHeight > 0) { - tempMaskCtx.drawImage( - toolMaskCanvas, - sourceX, sourceY, copyWidth, copyHeight, - destX, destY, copyWidth, copyHeight - ); - } - - const tempMaskData = tempMaskCtx.getImageData(0, 0, bounds.width, bounds.height); - 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] = alpha; - tempMaskData.data[i + 3] = 255; // Solidna alpha - } - tempMaskCtx.putImageData(tempMaskData, 0, 0); - - maskCtx.globalCompositeOperation = 'screen'; - maskCtx.drawImage(tempMaskCanvas, 0, 0); } log.info("=== MASK BLOB GENERATED ==="); diff --git a/src/CustomShapeMenu.ts b/src/CustomShapeMenu.ts index 07310b8..5b44fab 100644 --- a/src/CustomShapeMenu.ts +++ b/src/CustomShapeMenu.ts @@ -187,6 +187,8 @@ export class CustomShapeMenu { color: #aaa; `; + let expansionValueBeforeDrag = this.canvas.shapeMaskExpansionValue; + const updateExpansionSliderDisplay = () => { const value = parseInt(expansionSlider.value); this.canvas.shapeMaskExpansionValue = value; @@ -195,7 +197,10 @@ export class CustomShapeMenu { let isExpansionDragging = false; - expansionSlider.onmousedown = () => { isExpansionDragging = true; }; + expansionSlider.onmousedown = () => { + isExpansionDragging = true; + expansionValueBeforeDrag = this.canvas.shapeMaskExpansionValue; // Store value before dragging + }; expansionSlider.oninput = () => { updateExpansionSliderDisplay(); @@ -214,6 +219,18 @@ export class CustomShapeMenu { expansionSlider.onmouseup = () => { isExpansionDragging = false; if (this.canvas.autoApplyShapeMask) { + const finalValue = parseInt(expansionSlider.value); + + // If value changed during drag, remove old mask with previous expansion value + if (expansionValueBeforeDrag !== finalValue) { + // Temporarily set the previous value to remove the old mask properly + const tempValue = this.canvas.shapeMaskExpansionValue; + this.canvas.shapeMaskExpansionValue = expansionValueBeforeDrag; + this.canvas.maskTool.removeShapeMask(); + this.canvas.shapeMaskExpansionValue = tempValue; // Restore current value + log.info(`Removed old shape mask with expansion: ${expansionValueBeforeDrag}px before applying new value: ${finalValue}px`); + } + this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(true); this.canvas.render(); diff --git a/src/MaskTool.ts b/src/MaskTool.ts index 021e416..d1b4ef0 100644 --- a/src/MaskTool.ts +++ b/src/MaskTool.ts @@ -61,6 +61,11 @@ export class MaskTool { private activeMaskUpdateTimeout: number | null; private readonly ACTIVE_MASK_UPDATE_DELAY = 16; // ~60fps throttling + // Performance optimization for shape preview + private shapePreviewThrottleTimeout: number | null; + private pendingPreviewParams: { expansionValue: number, featherValue: number } | null; + private readonly SHAPE_PREVIEW_THROTTLE_DELAY = 16; // ~60fps throttling for preview + constructor(canvasInstance: Canvas & { canvasState: CanvasState, width: number, height: number }, callbacks: MaskToolCallbacks = {}) { this.canvasInstance = canvasInstance; this.mainCanvas = canvasInstance.canvas; @@ -118,6 +123,10 @@ export class MaskTool { this.activeMaskNeedsUpdate = false; this.activeMaskUpdateTimeout = null; + // Initialize shape preview throttling + this.shapePreviewThrottleTimeout = null; + this.pendingPreviewParams = null; + this.initMaskCanvas(); } @@ -691,6 +700,7 @@ export class MaskTool { /** * Prepares shape mask configuration data - eliminates duplication between applyShapeMask and removeShapeMask * Returns all necessary data for shape mask operations including world coordinates and temporary canvas setup + * Now uses precise expansion calculation based on actual user values */ private prepareShapeMaskConfiguration(): { shape: any, @@ -720,12 +730,15 @@ export class MaskTool { y: bounds.y + extensionOffset.y + p.y })); - // Create a temporary canvas large enough to contain the shape and any expansion - const maxExpansion = Math.max(300, Math.abs(this.canvasInstance.shapeMaskExpansionValue || 0)); - const tempCanvasWidth = bounds.width + (maxExpansion * 2); - const tempCanvasHeight = bounds.height + (maxExpansion * 2); - const tempOffsetX = maxExpansion; - const tempOffsetY = maxExpansion; + // Use precise expansion calculation - only actual user value + small safety margin + const userExpansionValue = Math.abs(this.canvasInstance.shapeMaskExpansionValue || 0); + const safetyMargin = 10; // Small safety margin for precise operations + const preciseExpansion = userExpansionValue + safetyMargin; + + const tempCanvasWidth = bounds.width + (preciseExpansion * 2); + const tempCanvasHeight = bounds.height + (preciseExpansion * 2); + const tempOffsetX = preciseExpansion; + const tempOffsetY = preciseExpansion; // Adjust shape points for the temporary canvas const tempShapePoints = worldShapePoints.map(p => ({ @@ -738,7 +751,7 @@ export class MaskTool { bounds, extensionOffset, worldShapePoints, - maxExpansion, + maxExpansion: preciseExpansion, tempCanvasWidth, tempCanvasHeight, tempOffsetX, @@ -1184,6 +1197,28 @@ export class MaskTool { * Show blue outline preview of expansion/contraction during slider adjustment */ showShapePreview(expansionValue: number, featherValue: number = 0): void { + // Store the parameters for throttled execution + this.pendingPreviewParams = { expansionValue, featherValue }; + + // If there's already a pending preview update, don't schedule another one + if (this.shapePreviewThrottleTimeout !== null) { + return; + } + + // Schedule the preview update with throttling + this.shapePreviewThrottleTimeout = window.setTimeout(() => { + if (this.pendingPreviewParams) { + this.executeShapePreview(this.pendingPreviewParams.expansionValue, this.pendingPreviewParams.featherValue); + this.pendingPreviewParams = null; + } + this.shapePreviewThrottleTimeout = null; + }, this.SHAPE_PREVIEW_THROTTLE_DELAY); + } + + /** + * Executes the actual shape preview rendering - separated from showShapePreview for throttling + */ + private executeShapePreview(expansionValue: number, featherValue: number = 0): void { if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) { return; } @@ -1225,7 +1260,7 @@ export class MaskTool { this.drawContoursForPreview(this.shapePreviewCtx, allFeatherContours, '#4A9EFF', 1, [3, 5], 0.6); } - log.debug(`Shape preview shown with expansion: ${expansionValue}px, feather: ${featherValue}px at bounds (${bounds.x}, ${bounds.y})`); + log.debug(`Shape preview executed with expansion: ${expansionValue}px, feather: ${featherValue}px at bounds (${bounds.x}, ${bounds.y})`); } /** @@ -1516,6 +1551,58 @@ export class MaskTool { return this.activeMaskCanvas; } + /** + * Gets mask only for the output area - optimized for performance + * Returns only the portion of the mask that overlaps with the output area + * This is much more efficient than returning the entire mask when there are many chunks + */ + getMaskForOutputArea(): HTMLCanvasElement { + const bounds = this.canvasInstance.outputAreaBounds; + + // Create canvas sized to output area + const outputMaskCanvas = document.createElement('canvas'); + outputMaskCanvas.width = bounds.width; + outputMaskCanvas.height = bounds.height; + const outputMaskCtx = outputMaskCanvas.getContext('2d', { willReadFrequently: true }); + + if (!outputMaskCtx) { + throw new Error("Failed to get 2D context for output area mask canvas"); + } + + // Calculate which chunks overlap with the output area + const outputLeft = bounds.x; + const outputTop = bounds.y; + const outputRight = bounds.x + bounds.width; + const outputBottom = bounds.y + bounds.height; + + const chunkBounds = this.calculateChunkBounds(outputLeft, outputTop, outputRight, outputBottom); + + // Only process chunks that overlap with output area + for (let chunkY = chunkBounds.minY; chunkY <= chunkBounds.maxY; chunkY++) { + for (let chunkX = chunkBounds.minX; chunkX <= chunkBounds.maxX; chunkX++) { + const chunkKey = `${chunkX},${chunkY}`; + const chunk = this.maskChunks.get(chunkKey); + + if (chunk && !chunk.isEmpty) { + // Calculate intersection between chunk and output area + const intersection = this.calculateChunkIntersection(chunk, outputLeft, outputTop, outputRight, outputBottom); + + if (intersection) { + // Draw only the intersecting portion + outputMaskCtx.drawImage( + chunk.canvas, + intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight, // Source from chunk + intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight // Destination on output canvas + ); + } + } + } + } + + log.debug(`Generated output area mask (${bounds.width}x${bounds.height}) from ${chunkBounds.maxX - chunkBounds.minX + 1}x${chunkBounds.maxY - chunkBounds.minY + 1} chunks`); + return outputMaskCanvas; + } + resize(width: number, height: number): void { this.initPreviewCanvas(); const oldMask = this.maskCanvas; @@ -1861,26 +1948,34 @@ export class MaskTool { // Create a removal mask canvas - always hard-edged to ensure complete removal let removalMaskCanvas: HTMLCanvasElement; + // Add safety margin to ensure complete removal of antialiasing artifacts + const safetyMargin = 2; // 2px margin to remove any antialiasing remnants + if (needsExpansion) { - // If expansion was active, remove the expanded area with a hard edge + // If expansion was active, remove exactly the user's expansion value + safety margin + const userExpansionValue = this.canvasInstance.shapeMaskExpansionValue; + const expandedValue = Math.abs(userExpansionValue) + safetyMargin; removalMaskCanvas = this._createExpandedMaskCanvas( config.tempShapePoints, - this.canvasInstance.shapeMaskExpansionValue, + expandedValue, config.tempCanvasWidth, config.tempCanvasHeight ); } else { - // If no expansion, just remove the base shape with a hard edge - const { canvas, ctx } = this.createCanvas(config.tempCanvasWidth, config.tempCanvasHeight); - removalMaskCanvas = canvas; - this.drawShapeOnCanvas(ctx, config.tempShapePoints, 'evenodd'); + // If no expansion, remove the base shape with safety margin only + removalMaskCanvas = this._createExpandedMaskCanvas( + config.tempShapePoints, + safetyMargin, + config.tempCanvasWidth, + config.tempCanvasHeight + ); } // Now remove the shape mask from the chunked system this.removeMaskCanvasFromChunks(removalMaskCanvas, config.bounds.x - config.tempOffsetX, config.bounds.y - config.tempOffsetY); // Update the active mask canvas to show the changes - this.updateActiveMaskCanvas(); + this.updateActiveMaskCanvas(true); // Force full update to ensure all chunks are properly updated if (this.onStateChange) { this.onStateChange(); diff --git a/src/SAMDetectorIntegration.ts b/src/SAMDetectorIntegration.ts index 46a7631..6f9d549 100644 --- a/src/SAMDetectorIntegration.ts +++ b/src/SAMDetectorIntegration.ts @@ -333,18 +333,31 @@ async function handleSAMDetectorResult(node: ComfyNode, resultImage: HTMLImageEl return; } - // Create temporary canvas for mask processing (same as CanvasMask) + // Create temporary canvas for mask processing with correct positioning log.debug("Creating temporary canvas for mask processing"); + + // Get the output area bounds to position the mask correctly + const bounds = canvas.outputAreaBounds; + log.debug("Output area bounds for SAM mask positioning:", bounds); + + // Create canvas sized to match the result image dimensions const tempCanvas = document.createElement('canvas'); - tempCanvas.width = canvas.width; - tempCanvas.height = canvas.height; + tempCanvas.width = resultImage.width; + tempCanvas.height = resultImage.height; const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); if (tempCtx) { - tempCtx.drawImage(resultImage, 0, 0, canvas.width, canvas.height); + // Draw the result image at its natural size (no scaling) + tempCtx.drawImage(resultImage, 0, 0); - log.debug("Processing image data to create mask"); - const imageData = tempCtx.getImageData(0, 0, canvas.width, canvas.height); + log.debug("Processing image data to create mask", { + imageWidth: resultImage.width, + imageHeight: resultImage.height, + boundsX: bounds.x, + boundsY: bounds.y + }); + + const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); const data = imageData.data; // Convert to mask format (same as CanvasMask)