Refine shape mask UI and ensure hard-edged mask removal

Consolidates shape mask controls into a styled container, improves UI logic for showing/hiding sub-options, and updates slider initialization to reflect current values. Mask removal now always uses a hard-edged shape, even if feathering was previously applied, ensuring complete erasure of feathered areas. The mask application logic also clears the maximum possible mask area before applying the new mask to prevent artifacts from previous slider values. Checkbox and slider labels are updated for clarity.
This commit is contained in:
Dariusz L
2025-07-26 01:50:44 +02:00
parent ccfa2b6cfb
commit 1fc06f65a2
4 changed files with 165 additions and 150 deletions

View File

@@ -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.