diff --git a/js/CustomShapeMenu.js b/js/CustomShapeMenu.js index 99e606a..b282803 100644 --- a/js/CustomShapeMenu.js +++ b/js/CustomShapeMenu.js @@ -63,9 +63,7 @@ export class CustomShapeMenu { `; // Create menu content const lines = [ - "🎯 Custom Output Area Active", - "Press Shift+S to modify shape", - "Shape defines generation area" + "🎯 Custom Output Area Active" ]; lines.forEach(line => { const lineElement = document.createElement('div'); @@ -76,7 +74,17 @@ export class CustomShapeMenu { `; this.element.appendChild(lineElement); }); - // Add main auto-apply checkbox + // Create a container for the entire shape mask feature set + const featureContainer = document.createElement('div'); + featureContainer.id = 'shape-mask-feature-container'; + featureContainer.style.cssText = ` + background-color: #282828; + border-radius: 6px; + margin-top: 6px; + padding: 4px 0; + border: 1px solid #444; + `; + // Add main auto-apply checkbox to the new container const checkboxContainer = this._createCheckbox(() => `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`, () => { this.canvas.autoApplyShapeMask = !this.canvas.autoApplyShapeMask; if (this.canvas.autoApplyShapeMask) { @@ -85,13 +93,15 @@ export class CustomShapeMenu { } else { this.canvas.maskTool.removeShapeMask(); - log.info("Auto-apply shape mask disabled - mask removed automatically"); + this.canvas.shapeMaskExpansion = false; + this.canvas.shapeMaskFeather = false; + log.info("Auto-apply shape mask disabled - mask area removed and sub-options reset."); } this._updateUI(); this.canvas.render(); }); - this.element.appendChild(checkboxContainer); - // Add expansion checkbox (only visible when auto-apply is enabled) + featureContainer.appendChild(checkboxContainer); + // Add expansion checkbox const expansionContainer = this._createCheckbox(() => `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`, () => { this.canvas.shapeMaskExpansion = !this.canvas.shapeMaskExpansion; this._updateUI(); @@ -101,12 +111,12 @@ export class CustomShapeMenu { } }); expansionContainer.id = 'expansion-checkbox'; - this.element.appendChild(expansionContainer); - // Add expansion slider container (only visible when expansion is enabled) + featureContainer.appendChild(expansionContainer); + // Add expansion slider container const expansionSliderContainer = document.createElement('div'); expansionSliderContainer.id = 'expansion-slider-container'; expansionSliderContainer.style.cssText = ` - margin: 6px 0; + margin: 0 8px 6px 8px; padding: 4px 8px; display: none; `; @@ -121,7 +131,7 @@ export class CustomShapeMenu { expansionSlider.type = 'range'; expansionSlider.min = '-300'; expansionSlider.max = '300'; - expansionSlider.value = '0'; + expansionSlider.value = String(this.canvas.shapeMaskExpansionValue); expansionSlider.style.cssText = ` width: 100%; height: 4px; @@ -141,41 +151,25 @@ export class CustomShapeMenu { this.canvas.shapeMaskExpansionValue = value; expansionValueDisplay.textContent = value > 0 ? `+${value}px` : `${value}px`; }; - // Add preview system for expansion slider - let expansionTimeout = null; let isExpansionDragging = false; - expansionSlider.onmousedown = () => { - isExpansionDragging = true; - }; + expansionSlider.onmousedown = () => { isExpansionDragging = true; }; expansionSlider.oninput = () => { updateExpansionSliderDisplay(); if (this.canvas.autoApplyShapeMask) { - // Clear previous timeout - if (expansionTimeout) { - clearTimeout(expansionTimeout); - } if (isExpansionDragging) { - // Show blue preview line while dragging - NO mask application const featherValue = this.canvas.shapeMaskFeather ? this.canvas.shapeMaskFeatherValue : 0; this.canvas.maskTool.showShapePreview(this.canvas.shapeMaskExpansionValue, featherValue); } else { - // Apply mask immediately for programmatic changes (not user dragging) this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(false); this.canvas.render(); } - // Clear any pending timeout - we only apply mask on mouseup now - if (expansionTimeout) { - clearTimeout(expansionTimeout); - expansionTimeout = null; - } } }; expansionSlider.onmouseup = () => { isExpansionDragging = false; if (this.canvas.autoApplyShapeMask) { - // Apply final mask immediately when user releases slider this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(true); this.canvas.render(); @@ -185,8 +179,8 @@ export class CustomShapeMenu { expansionSliderContainer.appendChild(expansionSliderLabel); expansionSliderContainer.appendChild(expansionSlider); expansionSliderContainer.appendChild(expansionValueDisplay); - this.element.appendChild(expansionSliderContainer); - // Add feather checkbox (only visible when auto-apply is enabled) + featureContainer.appendChild(expansionSliderContainer); + // Add feather checkbox const featherContainer = this._createCheckbox(() => `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`, () => { this.canvas.shapeMaskFeather = !this.canvas.shapeMaskFeather; this._updateUI(); @@ -196,12 +190,12 @@ export class CustomShapeMenu { } }); featherContainer.id = 'feather-checkbox'; - this.element.appendChild(featherContainer); - // Add feather slider container (only visible when feather is enabled) + featureContainer.appendChild(featherContainer); + // Add feather slider container const featherSliderContainer = document.createElement('div'); featherSliderContainer.id = 'feather-slider-container'; featherSliderContainer.style.cssText = ` - margin: 6px 0; + margin: 0 8px 6px 8px; padding: 4px 8px; display: none; `; @@ -216,7 +210,7 @@ export class CustomShapeMenu { featherSlider.type = 'range'; featherSlider.min = '0'; featherSlider.max = '300'; - featherSlider.value = '0'; + featherSlider.value = String(this.canvas.shapeMaskFeatherValue); featherSlider.style.cssText = ` width: 100%; height: 4px; @@ -236,22 +230,16 @@ export class CustomShapeMenu { this.canvas.shapeMaskFeatherValue = value; featherValueDisplay.textContent = `${value}px`; }; - // Add preview system for feather slider (mirrors expansion slider) - let featherTimeout = null; let isFeatherDragging = false; - featherSlider.onmousedown = () => { - isFeatherDragging = true; - }; + featherSlider.onmousedown = () => { isFeatherDragging = true; }; featherSlider.oninput = () => { updateFeatherSliderDisplay(); if (this.canvas.autoApplyShapeMask) { if (isFeatherDragging) { - // Show blue preview line while dragging const expansionValue = this.canvas.shapeMaskExpansion ? this.canvas.shapeMaskExpansionValue : 0; this.canvas.maskTool.showShapePreview(expansionValue, this.canvas.shapeMaskFeatherValue); } else { - // Apply immediately for programmatic changes this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(false); this.canvas.render(); @@ -261,7 +249,6 @@ export class CustomShapeMenu { featherSlider.onmouseup = () => { isFeatherDragging = false; if (this.canvas.autoApplyShapeMask) { - // Apply final mask when user releases slider this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(true); // true = save state this.canvas.render(); @@ -271,7 +258,8 @@ export class CustomShapeMenu { featherSliderContainer.appendChild(featherSliderLabel); featherSliderContainer.appendChild(featherSlider); featherSliderContainer.appendChild(featherValueDisplay); - this.element.appendChild(featherSliderContainer); + featureContainer.appendChild(featherSliderContainer); + this.element.appendChild(featureContainer); // Add to DOM if (this.canvas.canvas.parentElement) { this.canvas.canvas.parentElement.appendChild(this.element); @@ -315,27 +303,23 @@ export class CustomShapeMenu { _updateUI() { if (!this.element) return; - // Update expansion checkbox visibility + // Toggle visibility of sub-options based on the main checkbox state const expansionCheckbox = this.element.querySelector('#expansion-checkbox'); if (expansionCheckbox) { expansionCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none'; } - // Update expansion slider container visibility - const expansionSliderContainer = this.element.querySelector('#expansion-slider-container'); - if (expansionSliderContainer) { - expansionSliderContainer.style.display = - (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none'; - } - // Update feather checkbox visibility const featherCheckbox = this.element.querySelector('#feather-checkbox'); if (featherCheckbox) { featherCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none'; } - // Update feather slider container visibility + // Update sliders visibility based on their respective checkboxes + const expansionSliderContainer = this.element.querySelector('#expansion-slider-container'); + if (expansionSliderContainer) { + expansionSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none'; + } const featherSliderContainer = this.element.querySelector('#feather-slider-container'); if (featherSliderContainer) { - featherSliderContainer.style.display = - (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none'; + featherSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none'; } // Update checkbox texts const checkboxes = this.element.querySelectorAll('div[style*="cursor: pointer"]'); @@ -344,7 +328,7 @@ export class CustomShapeMenu { checkbox.textContent = `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`; } else if (index === 1) { // Expansion checkbox - checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`; + checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Dilate/Erode mask`; } else if (index === 2) { // Feather checkbox checkbox.textContent = `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`; diff --git a/js/MaskTool.js b/js/MaskTool.js index 2779467..dd67208 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -620,10 +620,17 @@ export class MaskTool { const shape = this.canvasInstance.outputAreaShape; const destX = -this.x; const destY = -this.y; - // Clear the entire mask canvas first - this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); - // Create points relative to the mask canvas's coordinate system (by applying the offset) const maskPoints = shape.points.map(p => ({ x: p.x + destX, y: p.y + destY })); + // --- Clear Previous State --- + // To prevent artifacts from previous slider values, we first clear the maximum + // possible area the shape could have occupied. + const maxExpansion = 300; // The maximum value of the expansion slider + const clearingMaskCanvas = this._createExpandedMaskCanvas(maskPoints, maxExpansion, this.maskCanvas.width, this.maskCanvas.height); + this.maskCtx.globalCompositeOperation = 'destination-out'; + this.maskCtx.drawImage(clearingMaskCanvas, 0, 0); + // --- Apply Current State --- + // Now, apply the new, correct mask additively. + this.maskCtx.globalCompositeOperation = 'source-over'; // Check if we need expansion or feathering const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0; const needsFeather = this.canvasInstance.shapeMaskFeather && this.canvasInstance.shapeMaskFeatherValue > 0; @@ -668,7 +675,8 @@ export class MaskTool { log.info(`Applied shape mask with expansion: ${needsExpansion}, feather: ${needsFeather}.`); } /** - * Removes mask in the area of the custom output area shape + * Removes mask in the area of the custom output area shape. This must use a hard-edged + * shape to correctly erase any feathered "glow" that might have been applied. */ removeShapeMask() { if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) { @@ -679,22 +687,34 @@ export class MaskTool { const shape = this.canvasInstance.outputAreaShape; const destX = -this.x; const destY = -this.y; - this.maskCtx.save(); + // Use 'destination-out' to erase the shape area this.maskCtx.globalCompositeOperation = 'destination-out'; - this.maskCtx.translate(destX, destY); - this.maskCtx.beginPath(); - this.maskCtx.moveTo(shape.points[0].x, shape.points[0].y); - for (let i = 1; i < shape.points.length; i++) { - this.maskCtx.lineTo(shape.points[i].x, shape.points[i].y); + const maskPoints = shape.points.map(p => ({ x: p.x + destX, y: p.y + destY })); + const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0; + // IMPORTANT: Removal should always be hard-edged, even if feather was on. + // This ensures the feathered "glow" is completely removed. We only care about expansion. + if (needsExpansion) { + // If expansion was active, remove the expanded area with a hard edge. + const expandedMaskCanvas = this._createExpandedMaskCanvas(maskPoints, this.canvasInstance.shapeMaskExpansionValue, this.maskCanvas.width, this.maskCanvas.height); + this.maskCtx.drawImage(expandedMaskCanvas, 0, 0); } - this.maskCtx.closePath(); - this.maskCtx.fill(); - this.maskCtx.restore(); + else { + // If no expansion, just remove the base shape with a hard edge. + this.maskCtx.beginPath(); + this.maskCtx.moveTo(maskPoints[0].x, maskPoints[0].y); + for (let i = 1; i < maskPoints.length; i++) { + this.maskCtx.lineTo(maskPoints[i].x, maskPoints[i].y); + } + this.maskCtx.closePath(); + this.maskCtx.fill('evenodd'); + } + // Restore default composite operation + this.maskCtx.globalCompositeOperation = 'source-over'; if (this.onStateChange) { this.onStateChange(); } this.canvasInstance.render(); - log.info(`Removed shape mask with ${shape.points.length} points`); + log.info(`Removed shape mask area (hard-edged) with expansion: ${needsExpansion}.`); } _createFeatheredMaskCanvas(points, featherRadius, width, height) { // 1. Create a binary mask on a temporary canvas. diff --git a/src/CustomShapeMenu.ts b/src/CustomShapeMenu.ts index 25ecd2d..1ca4246 100644 --- a/src/CustomShapeMenu.ts +++ b/src/CustomShapeMenu.ts @@ -82,9 +82,7 @@ export class CustomShapeMenu { // Create menu content const lines = [ - "🎯 Custom Output Area Active", - "Press Shift+S to modify shape", - "Shape defines generation area" + "🎯 Custom Output Area Active" ]; lines.forEach(line => { @@ -97,7 +95,18 @@ export class CustomShapeMenu { this.element!.appendChild(lineElement); }); - // Add main auto-apply checkbox + // Create a container for the entire shape mask feature set + const featureContainer = document.createElement('div'); + featureContainer.id = 'shape-mask-feature-container'; + featureContainer.style.cssText = ` + background-color: #282828; + border-radius: 6px; + margin-top: 6px; + padding: 4px 0; + border: 1px solid #444; + `; + + // Add main auto-apply checkbox to the new container const checkboxContainer = this._createCheckbox( () => `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`, () => { @@ -108,16 +117,18 @@ export class CustomShapeMenu { log.info("Auto-apply shape mask enabled - mask applied automatically"); } else { this.canvas.maskTool.removeShapeMask(); - log.info("Auto-apply shape mask disabled - mask removed automatically"); + this.canvas.shapeMaskExpansion = false; + this.canvas.shapeMaskFeather = false; + log.info("Auto-apply shape mask disabled - mask area removed and sub-options reset."); } this._updateUI(); this.canvas.render(); } ); - this.element.appendChild(checkboxContainer); - - // Add expansion checkbox (only visible when auto-apply is enabled) + featureContainer.appendChild(checkboxContainer); + + // Add expansion checkbox const expansionContainer = this._createCheckbox( () => `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`, () => { @@ -131,13 +142,13 @@ export class CustomShapeMenu { } ); expansionContainer.id = 'expansion-checkbox'; - this.element.appendChild(expansionContainer); + featureContainer.appendChild(expansionContainer); - // Add expansion slider container (only visible when expansion is enabled) + // Add expansion slider container const expansionSliderContainer = document.createElement('div'); expansionSliderContainer.id = 'expansion-slider-container'; expansionSliderContainer.style.cssText = ` - margin: 6px 0; + margin: 0 8px 6px 8px; padding: 4px 8px; display: none; `; @@ -154,7 +165,7 @@ export class CustomShapeMenu { expansionSlider.type = 'range'; expansionSlider.min = '-300'; expansionSlider.max = '300'; - expansionSlider.value = '0'; + expansionSlider.value = String(this.canvas.shapeMaskExpansionValue); expansionSlider.style.cssText = ` width: 100%; height: 4px; @@ -177,46 +188,27 @@ export class CustomShapeMenu { expansionValueDisplay.textContent = value > 0 ? `+${value}px` : `${value}px`; }; - // Add preview system for expansion slider - let expansionTimeout: number | null = null; let isExpansionDragging = false; - expansionSlider.onmousedown = () => { - isExpansionDragging = true; - }; + expansionSlider.onmousedown = () => { isExpansionDragging = true; }; expansionSlider.oninput = () => { updateExpansionSliderDisplay(); - if (this.canvas.autoApplyShapeMask) { - // Clear previous timeout - if (expansionTimeout) { - clearTimeout(expansionTimeout); - } - if (isExpansionDragging) { - // Show blue preview line while dragging - NO mask application const featherValue = this.canvas.shapeMaskFeather ? this.canvas.shapeMaskFeatherValue : 0; this.canvas.maskTool.showShapePreview(this.canvas.shapeMaskExpansionValue, featherValue); } else { - // Apply mask immediately for programmatic changes (not user dragging) this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(false); this.canvas.render(); } - - // Clear any pending timeout - we only apply mask on mouseup now - if (expansionTimeout) { - clearTimeout(expansionTimeout); - expansionTimeout = null; - } } }; expansionSlider.onmouseup = () => { isExpansionDragging = false; if (this.canvas.autoApplyShapeMask) { - // Apply final mask immediately when user releases slider this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(true); this.canvas.render(); @@ -228,9 +220,9 @@ export class CustomShapeMenu { expansionSliderContainer.appendChild(expansionSliderLabel); expansionSliderContainer.appendChild(expansionSlider); expansionSliderContainer.appendChild(expansionValueDisplay); - this.element.appendChild(expansionSliderContainer); + featureContainer.appendChild(expansionSliderContainer); - // Add feather checkbox (only visible when auto-apply is enabled) + // Add feather checkbox const featherContainer = this._createCheckbox( () => `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`, () => { @@ -244,13 +236,13 @@ export class CustomShapeMenu { } ); featherContainer.id = 'feather-checkbox'; - this.element.appendChild(featherContainer); + featureContainer.appendChild(featherContainer); - // Add feather slider container (only visible when feather is enabled) + // Add feather slider container const featherSliderContainer = document.createElement('div'); featherSliderContainer.id = 'feather-slider-container'; featherSliderContainer.style.cssText = ` - margin: 6px 0; + margin: 0 8px 6px 8px; padding: 4px 8px; display: none; `; @@ -267,7 +259,7 @@ export class CustomShapeMenu { featherSlider.type = 'range'; featherSlider.min = '0'; featherSlider.max = '300'; - featherSlider.value = '0'; + featherSlider.value = String(this.canvas.shapeMaskFeatherValue); featherSlider.style.cssText = ` width: 100%; height: 4px; @@ -289,25 +281,18 @@ export class CustomShapeMenu { this.canvas.shapeMaskFeatherValue = value; featherValueDisplay.textContent = `${value}px`; }; - - // Add preview system for feather slider (mirrors expansion slider) - let featherTimeout: number | null = null; + let isFeatherDragging = false; - featherSlider.onmousedown = () => { - isFeatherDragging = true; - }; + featherSlider.onmousedown = () => { isFeatherDragging = true; }; featherSlider.oninput = () => { updateFeatherSliderDisplay(); - if (this.canvas.autoApplyShapeMask) { if (isFeatherDragging) { - // Show blue preview line while dragging const expansionValue = this.canvas.shapeMaskExpansion ? this.canvas.shapeMaskExpansionValue : 0; this.canvas.maskTool.showShapePreview(expansionValue, this.canvas.shapeMaskFeatherValue); } else { - // Apply immediately for programmatic changes this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(false); this.canvas.render(); @@ -318,7 +303,6 @@ export class CustomShapeMenu { featherSlider.onmouseup = () => { isFeatherDragging = false; if (this.canvas.autoApplyShapeMask) { - // Apply final mask when user releases slider this.canvas.maskTool.hideShapePreview(); this.canvas.maskTool.applyShapeMask(true); // true = save state this.canvas.render(); @@ -330,8 +314,10 @@ export class CustomShapeMenu { featherSliderContainer.appendChild(featherSliderLabel); featherSliderContainer.appendChild(featherSlider); featherSliderContainer.appendChild(featherValueDisplay); - this.element.appendChild(featherSliderContainer); + featureContainer.appendChild(featherSliderContainer); + this.element.appendChild(featureContainer); + // Add to DOM if (this.canvas.canvas.parentElement) { this.canvas.canvas.parentElement.appendChild(this.element); @@ -383,30 +369,26 @@ export class CustomShapeMenu { private _updateUI(): void { if (!this.element) return; - // Update expansion checkbox visibility + // Toggle visibility of sub-options based on the main checkbox state const expansionCheckbox = this.element.querySelector('#expansion-checkbox') as HTMLElement; if (expansionCheckbox) { expansionCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none'; } - - // Update expansion slider container visibility - const expansionSliderContainer = this.element.querySelector('#expansion-slider-container') as HTMLElement; - if (expansionSliderContainer) { - expansionSliderContainer.style.display = - (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none'; - } - - // Update feather checkbox visibility + const featherCheckbox = this.element.querySelector('#feather-checkbox') as HTMLElement; if (featherCheckbox) { featherCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none'; } - // Update feather slider container visibility + // Update sliders visibility based on their respective checkboxes + const expansionSliderContainer = this.element.querySelector('#expansion-slider-container') as HTMLElement; + if (expansionSliderContainer) { + expansionSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none'; + } + const featherSliderContainer = this.element.querySelector('#feather-slider-container') as HTMLElement; if (featherSliderContainer) { - featherSliderContainer.style.display = - (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none'; + featherSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none'; } // Update checkbox texts @@ -415,7 +397,7 @@ export class CustomShapeMenu { if (index === 0) { // Main checkbox checkbox.textContent = `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`; } else if (index === 1) { // Expansion checkbox - checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`; + checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Dilate/Erode mask`; } else if (index === 2) { // Feather checkbox checkbox.textContent = `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`; } diff --git a/src/MaskTool.ts b/src/MaskTool.ts index 9fdd9f0..57eb3b3 100644 --- a/src/MaskTool.ts +++ b/src/MaskTool.ts @@ -750,12 +750,21 @@ export class MaskTool { const destX = -this.x; const destY = -this.y; - // Clear the entire mask canvas first - this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); - - // Create points relative to the mask canvas's coordinate system (by applying the offset) const maskPoints = shape.points.map(p => ({ x: p.x + destX, y: p.y + destY })); + // --- Clear Previous State --- + // To prevent artifacts from previous slider values, we first clear the maximum + // possible area the shape could have occupied. + const maxExpansion = 300; // The maximum value of the expansion slider + const clearingMaskCanvas = this._createExpandedMaskCanvas(maskPoints, maxExpansion, this.maskCanvas.width, this.maskCanvas.height); + + this.maskCtx.globalCompositeOperation = 'destination-out'; + this.maskCtx.drawImage(clearingMaskCanvas, 0, 0); + + // --- Apply Current State --- + // Now, apply the new, correct mask additively. + this.maskCtx.globalCompositeOperation = 'source-over'; + // Check if we need expansion or feathering const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0; const needsFeather = this.canvasInstance.shapeMaskFeather && this.canvasInstance.shapeMaskFeatherValue > 0; @@ -802,37 +811,57 @@ export class MaskTool { } /** - * Removes mask in the area of the custom output area shape + * Removes mask in the area of the custom output area shape. This must use a hard-edged + * shape to correctly erase any feathered "glow" that might have been applied. */ removeShapeMask(): void { if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) { log.warn("Shape has insufficient points for mask removal"); return; } - + this.canvasInstance.canvasState.saveMaskState(); + const shape = this.canvasInstance.outputAreaShape; const destX = -this.x; const destY = -this.y; - - this.maskCtx.save(); + + // Use 'destination-out' to erase the shape area this.maskCtx.globalCompositeOperation = 'destination-out'; - this.maskCtx.translate(destX, destY); - - this.maskCtx.beginPath(); - this.maskCtx.moveTo(shape.points[0].x, shape.points[0].y); - for (let i = 1; i < shape.points.length; i++) { - this.maskCtx.lineTo(shape.points[i].x, shape.points[i].y); + + const maskPoints = shape.points.map(p => ({ x: p.x + destX, y: p.y + destY })); + const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0; + + // IMPORTANT: Removal should always be hard-edged, even if feather was on. + // This ensures the feathered "glow" is completely removed. We only care about expansion. + if (needsExpansion) { + // If expansion was active, remove the expanded area with a hard edge. + const expandedMaskCanvas = this._createExpandedMaskCanvas( + maskPoints, + this.canvasInstance.shapeMaskExpansionValue, + this.maskCanvas.width, + this.maskCanvas.height + ); + this.maskCtx.drawImage(expandedMaskCanvas, 0, 0); + } else { + // If no expansion, just remove the base shape with a hard edge. + this.maskCtx.beginPath(); + this.maskCtx.moveTo(maskPoints[0].x, maskPoints[0].y); + for (let i = 1; i < maskPoints.length; i++) { + this.maskCtx.lineTo(maskPoints[i].x, maskPoints[i].y); + } + this.maskCtx.closePath(); + this.maskCtx.fill('evenodd'); } - this.maskCtx.closePath(); - this.maskCtx.fill(); - this.maskCtx.restore(); + + // Restore default composite operation + this.maskCtx.globalCompositeOperation = 'source-over'; if (this.onStateChange) { this.onStateChange(); } this.canvasInstance.render(); - log.info(`Removed shape mask with ${shape.points.length} points`); + log.info(`Removed shape mask area (hard-edged) with expansion: ${needsExpansion}.`); } private _createFeatheredMaskCanvas(points: Point[], featherRadius: number, width: number, height: number): HTMLCanvasElement {