Add shape mask preview system for expansion and feather sliders

Implements a real-time blue outline preview when adjusting the expansion and feather sliders in the custom shape mask UI. The preview updates dynamically while dragging, and applies the mask only on mouse release. Adds a viewport change listener to keep the preview in sync during zooming or panning. Refactors mask expansion/contraction to use fast morphological operations for sharp edges and updates mask drawing to handle holes correctly.
This commit is contained in:
Dariusz L
2025-07-26 00:16:11 +02:00
parent 4c4856f9e7
commit ccfa2b6cfb
2 changed files with 454 additions and 192 deletions

View File

@@ -141,8 +141,12 @@ export class CustomShapeMenu {
this.canvas.shapeMaskExpansionValue = value;
expansionValueDisplay.textContent = value > 0 ? `+${value}px` : `${value}px`;
};
// Add debouncing for expansion slider
// Add preview system for expansion slider
let expansionTimeout = null;
let isExpansionDragging = false;
expansionSlider.onmousedown = () => {
isExpansionDragging = true;
};
expansionSlider.oninput = () => {
updateExpansionSliderDisplay();
if (this.canvas.autoApplyShapeMask) {
@@ -150,13 +154,31 @@ export class CustomShapeMenu {
if (expansionTimeout) {
clearTimeout(expansionTimeout);
}
// Apply mask immediately for visual feedback (without saving state)
this.canvas.maskTool.applyShapeMask(false); // false = don't save state
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();
// Save state after 500ms of no changes
expansionTimeout = window.setTimeout(() => {
this.canvas.canvasState.saveMaskState();
}, 500);
}
};
updateExpansionSliderDisplay();
@@ -214,22 +236,35 @@ export class CustomShapeMenu {
this.canvas.shapeMaskFeatherValue = value;
featherValueDisplay.textContent = `${value}px`;
};
// Add debouncing for feather slider
// Add preview system for feather slider (mirrors expansion slider)
let featherTimeout = null;
let isFeatherDragging = false;
featherSlider.onmousedown = () => {
isFeatherDragging = true;
};
featherSlider.oninput = () => {
updateFeatherSliderDisplay();
if (this.canvas.autoApplyShapeMask) {
// Clear previous timeout
if (featherTimeout) {
clearTimeout(featherTimeout);
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);
}
// Apply mask immediately for visual feedback (without saving state)
this.canvas.maskTool.applyShapeMask(false); // false = don't save state
else {
// Apply immediately for programmatic changes
this.canvas.maskTool.hideShapePreview();
this.canvas.maskTool.applyShapeMask(false);
this.canvas.render();
}
}
};
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();
// Save state after 500ms of no changes
featherTimeout = window.setTimeout(() => {
this.canvas.canvasState.saveMaskState();
}, 500);
}
};
updateFeatherSliderDisplay();
@@ -246,6 +281,8 @@ export class CustomShapeMenu {
}
this.uiInitialized = true;
this._updateUI();
// Add viewport change listener to update shape preview when zooming/panning
this._addViewportChangeListener();
}
_createCheckbox(textFn, clickHandler) {
const container = document.createElement('div');
@@ -314,4 +351,42 @@ export class CustomShapeMenu {
}
});
}
/**
* Add viewport change listener to update shape preview when zooming/panning
*/
_addViewportChangeListener() {
// Store previous viewport state to detect changes
let previousViewport = {
x: this.canvas.viewport.x,
y: this.canvas.viewport.y,
zoom: this.canvas.viewport.zoom
};
// Check for viewport changes in render loop
const checkViewportChange = () => {
if (this.canvas.maskTool.shapePreviewVisible) {
const current = this.canvas.viewport;
// Check if viewport has changed
if (current.x !== previousViewport.x ||
current.y !== previousViewport.y ||
current.zoom !== previousViewport.zoom) {
// Update shape preview with current expansion/feather values
const expansionValue = this.canvas.shapeMaskExpansionValue || 0;
const featherValue = this.canvas.shapeMaskFeather ? (this.canvas.shapeMaskFeatherValue || 0) : 0;
this.canvas.maskTool.showShapePreview(expansionValue, featherValue);
// Update previous viewport state
previousViewport = {
x: current.x,
y: current.y,
zoom: current.zoom
};
}
}
// Continue checking if UI is still active
if (this.uiInitialized) {
requestAnimationFrame(checkViewportChange);
}
};
// Start the viewport change detection
requestAnimationFrame(checkViewportChange);
}
}