improved mouse brush adjustment, added zoom level indicator with reset, added invert button, bug fixes (#1664)

This commit is contained in:
Tristan Sommer
2024-11-24 17:32:13 +01:00
committed by GitHub
parent f8bd910e63
commit 4bedd873a1

View File

@@ -275,11 +275,7 @@ var styles = `
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative; position: relative;
transition: background-color border 0.2s; transition: background-color 0.2s;
}
.maskEditor_toolPanelContainer:hover {
background-color: var(--p-overlaybadge-outline-color);
border: none;
} }
.maskEditor_toolPanelContainerSelected svg { .maskEditor_toolPanelContainerSelected svg {
fill: var(--p-button-text-primary-color) !important; fill: var(--p-button-text-primary-color) !important;
@@ -292,6 +288,15 @@ var styles = `
aspect-ratio: 1/1; aspect-ratio: 1/1;
fill: var(--p-button-text-secondary-color); fill: var(--p-button-text-secondary-color);
} }
.maskEditor_toolPanelContainerDark:hover {
background-color: var(--p-surface-800);
}
.maskEditor_toolPanelContainerLight:hover {
background-color: var(--p-surface-300);
}
.maskEditor_toolPanelIndicator { .maskEditor_toolPanelIndicator {
display: none; display: none;
height: 100%; height: 100%;
@@ -379,16 +384,56 @@ var styles = `
} }
#maskEditor_topBarShortcutsContainer { #maskEditor_topBarShortcutsContainer {
display: flex; display: flex;
gap: 10px;
margin-left: 5px;
} }
.maskEditor_topPanelIconButton { .maskEditor_topPanelIconButton_dark {
width: 53.3px; width: 50px;
height: 30px; height: 30px;
pointer-events: auto; pointer-events: auto;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transition: background-color 0.1s; transition: background-color 0.1s;
background: var(--p-surface-800);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
}
.maskEditor_topPanelIconButton_dark:hover {
background-color: var(--p-surface-900);
}
.maskEditor_topPanelIconButton_dark svg {
width: 25px;
height: 25px;
pointer-events: none;
fill: var(--input-text);
}
.maskEditor_topPanelIconButton_light {
width: 50px;
height: 30px;
pointer-events: auto;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.1s;
background: var(--comfy-menu-bg);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
}
.maskEditor_topPanelIconButton_light:hover {
background-color: var(--p-surface-300);
}
.maskEditor_topPanelIconButton_light svg {
width: 25px;
height: 25px;
pointer-events: none;
fill: var(--input-text);
} }
.maskEditor_topPanelButton_dark { .maskEditor_topPanelButton_dark {
@@ -657,6 +702,24 @@ var styles = `
margin-left: 15px; margin-left: 15px;
} }
.maskEditor_toolPanelZoomIndicator {
width: var(--sidebar-width);
height: var(--sidebar-width);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
color: var(--p-button-text-secondary-color);
position: absolute;
bottom: 0;
transition: background-color 0.2s;
}
#maskEditor_toolPanelDimensionsText {
font-size: 12px;
}
` `
var styleSheet = document.createElement('style') var styleSheet = document.createElement('style')
@@ -1229,6 +1292,8 @@ class PaintBucketTool {
this.messageBroker.subscribe('paintBucketFill', (point: Point) => this.messageBroker.subscribe('paintBucketFill', (point: Point) =>
this.floodFill(point) this.floodFill(point)
) )
this.messageBroker.subscribe('invert', () => this.invertMask())
} }
private addPullTopics() { private addPullTopics() {
@@ -1374,6 +1439,48 @@ class PaintBucketTool {
getTolerance(): number { getTolerance(): number {
return this.tolerance return this.tolerance
} }
//invert mask
private invertMask() {
const imageData = this.ctx.getImageData(
0,
0,
this.canvas.width,
this.canvas.height
)
const data = imageData.data
// Find first non-transparent pixel to get mask color
let maskR = 0,
maskG = 0,
maskB = 0
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] > 0) {
maskR = data[i]
maskG = data[i + 1]
maskB = data[i + 2]
break
}
}
// Process each pixel
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3]
// Invert alpha channel (0 becomes 255, 255 becomes 0)
data[i + 3] = 255 - alpha
// If this was originally transparent (now opaque), fill with mask color
if (alpha === 0) {
data[i] = maskR
data[i + 1] = maskG
data[i + 2] = maskB
}
}
this.ctx.putImageData(imageData, 0, 0)
this.messageBroker.publish('saveState')
}
} }
class ColorSelectTool { class ColorSelectTool {
@@ -1809,10 +1916,15 @@ class BrushTool {
smoothingLastDrawTime!: Date smoothingLastDrawTime!: Date
maskCtx: CanvasRenderingContext2D | null = null maskCtx: CanvasRenderingContext2D | null = null
brushStrokeCanvas: HTMLCanvasElement | null = null
brushStrokeCtx: CanvasRenderingContext2D | null = null
//brush adjustment //brush adjustment
isBrushAdjusting: boolean = false isBrushAdjusting: boolean = false
brushPreviewGradient: HTMLElement | null = null brushPreviewGradient: HTMLElement | null = null
initialPoint: Point | null = null initialPoint: Point | null = null
useDominantAxis: boolean = false
brushAdjustmentSpeed: number = 1.0
maskEditor: MaskEditorDialog maskEditor: MaskEditorDialog
messageBroker: MessageBroker messageBroker: MessageBroker
@@ -1823,6 +1935,13 @@ class BrushTool {
this.createListeners() this.createListeners()
this.addPullTopics() this.addPullTopics()
this.useDominantAxis = app.extensionManager.setting.get(
'Comfy.MaskEditor.UseDominantAxis'
)
this.brushAdjustmentSpeed = app.extensionManager.setting.get(
'Comfy.MaskEditor.BrushAdjustmentSpeed'
)
this.brushSettings = { this.brushSettings = {
size: 10, size: 10,
opacity: 100, opacity: 100,
@@ -1860,7 +1979,7 @@ class BrushTool {
) )
//drawing //drawing
this.messageBroker.subscribe('drawStart', (event: PointerEvent) => this.messageBroker.subscribe('drawStart', (event: PointerEvent) =>
this.start_drawing(event) this.startDrawing(event)
) )
this.messageBroker.subscribe('draw', (event: PointerEvent) => this.messageBroker.subscribe('draw', (event: PointerEvent) =>
this.handleDrawing(event) this.handleDrawing(event)
@@ -1897,12 +2016,27 @@ class BrushTool {
) )
} }
private async start_drawing(event: PointerEvent) { private async createBrushStrokeCanvas() {
if (this.brushStrokeCanvas !== null) {
return
}
const maskCanvas = await this.messageBroker.pull('maskCanvas')
const canvas = document.createElement('canvas')
canvas.width = maskCanvas.width
canvas.height = maskCanvas.height
this.brushStrokeCanvas = canvas
this.brushStrokeCtx = canvas.getContext('2d')!
}
private async startDrawing(event: PointerEvent) {
this.isDrawing = true this.isDrawing = true
let compositionOp: CompositionOperation let compositionOp: CompositionOperation
let currentTool = await this.messageBroker.pull('currentTool') let currentTool = await this.messageBroker.pull('currentTool')
let coords = { x: event.offsetX, y: event.offsetY } let coords = { x: event.offsetX, y: event.offsetY }
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords) let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
await this.createBrushStrokeCanvas()
//set drawing mode //set drawing mode
if (currentTool === Tools.Eraser || event.buttons == 2) { if (currentTool === Tools.Eraser || event.buttons == 2) {
@@ -1931,15 +2065,6 @@ class BrushTool {
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords) let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
let currentTool = await this.messageBroker.pull('currentTool') let currentTool = await this.messageBroker.pull('currentTool')
/* move to draw
if (event instanceof PointerEvent && event.pointerType == 'pen') {
brush_size *= event.pressure
this.last_pressure = event.pressure
} else {
brush_size = this.brush_size //this is the problem with pen pressure
}
*/
if (diff > 20 && !this.isDrawing) if (diff > 20 && !this.isDrawing)
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.init_shape(CompositionOperation.SourceOver) this.init_shape(CompositionOperation.SourceOver)
@@ -2051,29 +2176,62 @@ class BrushTool {
private async handleBrushAdjustment(event: PointerEvent) { private async handleBrushAdjustment(event: PointerEvent) {
const coords = { x: event.offsetX, y: event.offsetY } const coords = { x: event.offsetX, y: event.offsetY }
const brushDeadZone = 5
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords) let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
const delta_x = coords_canvas.x - this.initialPoint!.x const delta_x = coords_canvas.x - this.initialPoint!.x
const delta_y = coords_canvas.y - this.initialPoint!.y const delta_y = coords_canvas.y - this.initialPoint!.y
// Adjust brush size (horizontal movement) const effectiveDeltaX = Math.abs(delta_x) < brushDeadZone ? 0 : delta_x
const effectiveDeltaY = Math.abs(delta_y) < brushDeadZone ? 0 : delta_y
// New dominant axis logic
let finalDeltaX = effectiveDeltaX
let finalDeltaY = effectiveDeltaY
console.log(this.useDominantAxis)
if (this.useDominantAxis) {
// New setting flag
const ratio = Math.abs(effectiveDeltaX) / Math.abs(effectiveDeltaY)
const threshold = 2.0 // Configurable threshold
if (ratio > threshold) {
finalDeltaY = 0 // X is dominant
} else if (ratio < 1 / threshold) {
finalDeltaX = 0 // Y is dominant
}
}
const cappedDeltaX = Math.max(-100, Math.min(100, finalDeltaX))
const cappedDeltaY = Math.max(-100, Math.min(100, finalDeltaY))
// Rest of the function remains the same
const sizeDelta = cappedDeltaX / 40
const hardnessDelta = cappedDeltaY / 800
const newSize = Math.max( const newSize = Math.max(
1, 1,
Math.min(100, this.brushSettings.size! + delta_x / 10) Math.min(
100,
this.brushSettings.size! +
(cappedDeltaX / 35) * this.brushAdjustmentSpeed
)
) )
// Adjust brush hardness (vertical movement)
const newHardness = Math.max( const newHardness = Math.max(
0, 0,
Math.min(1, this.brushSettings!.hardness - delta_y / 200) Math.min(
1,
this.brushSettings!.hardness -
(cappedDeltaY / 4000) * this.brushAdjustmentSpeed
)
) )
this.brushSettings.size = newSize this.brushSettings.size = newSize
this.brushSettings.hardness = newHardness this.brushSettings.hardness = newHardness
this.messageBroker.publish('updateBrushPreview') this.messageBroker.publish('updateBrushPreview')
return
} }
//helper functions //helper functions
@@ -2263,8 +2421,8 @@ class BrushTool {
} }
private setBrushSmoothingPrecision(precision: number) { private setBrushSmoothingPrecision(precision: number) {
console.log('precision', precision) //console.log('precision', precision)
// this.brushSettings.smoothingPrecision = precision this.smoothingPrecision = precision
} }
} }
@@ -2300,6 +2458,9 @@ class UIManager {
private mask_opacity: number = 0.7 private mask_opacity: number = 0.7
private maskBlendMode: MaskBlendMode = MaskBlendMode.Black private maskBlendMode: MaskBlendMode = MaskBlendMode.Black
private zoomTextHTML!: HTMLSpanElement
private dimensionsTextHTML!: HTMLSpanElement
constructor(rootElement: HTMLElement, maskEditor: MaskEditorDialog) { constructor(rootElement: HTMLElement, maskEditor: MaskEditorDialog) {
this.rootElement = rootElement this.rootElement = rootElement
this.maskEditor = maskEditor this.maskEditor = maskEditor
@@ -2332,6 +2493,10 @@ class UIManager {
) )
this.messageBroker.subscribe('updateCursor', () => this.updateCursor()) this.messageBroker.subscribe('updateCursor', () => this.updateCursor())
this.messageBroker.subscribe('setZoomText', (text: string) =>
this.setZoomText(text)
)
} }
addPullTopics() { addPullTopics() {
@@ -2975,11 +3140,17 @@ class UIManager {
return separator return separator
} }
//----------------
private async createTopBar() { private async createTopBar() {
const buttonAccentColor = this.darkMode const buttonAccentColor = this.darkMode
? 'maskEditor_topPanelButton_dark' ? 'maskEditor_topPanelButton_dark'
: 'maskEditor_topPanelButton_light' : 'maskEditor_topPanelButton_light'
const iconButtonAccentColor = this.darkMode
? 'maskEditor_topPanelIconButton_dark'
: 'maskEditor_topPanelIconButton_light'
var top_bar = document.createElement('div') var top_bar = document.createElement('div')
top_bar.id = 'maskEditor_topBar' top_bar.id = 'maskEditor_topBar'
@@ -2997,9 +3168,9 @@ class UIManager {
var top_bar_undo_button = document.createElement('div') var top_bar_undo_button = document.createElement('div')
top_bar_undo_button.id = 'maskEditor_topBarUndoButton' top_bar_undo_button.id = 'maskEditor_topBarUndoButton'
top_bar_undo_button.classList.add('maskEditor_topPanelIconButton') top_bar_undo_button.classList.add(iconButtonAccentColor)
top_bar_undo_button.innerHTML = top_bar_undo_button.innerHTML =
'<svg viewBox="0 0 15 15" style="width: 36px;height: 36px;pointer-events: none;fill: var(--input-text);"><path d="M8.77,12.18c-.25,0-.46-.2-.46-.46s.2-.46.46-.46c1.47,0,2.67-1.2,2.67-2.67,0-1.57-1.34-2.67-3.26-2.67h-3.98l1.43,1.43c.18.18.18.47,0,.64-.18.18-.47.18-.64,0l-2.21-2.21c-.18-.18-.18-.47,0-.64l2.21-2.21c.18-.18.47-.18.64,0,.18.18.18.47,0,.64l-1.43,1.43h3.98c2.45,0,4.17,1.47,4.17,3.58,0,1.97-1.61,3.58-3.58,3.58Z"></path> </svg>' '<svg viewBox="0 0 15 15"><path d="M8.77,12.18c-.25,0-.46-.2-.46-.46s.2-.46.46-.46c1.47,0,2.67-1.2,2.67-2.67,0-1.57-1.34-2.67-3.26-2.67h-3.98l1.43,1.43c.18.18.18.47,0,.64-.18.18-.47.18-.64,0l-2.21-2.21c-.18-.18-.18-.47,0-.64l2.21-2.21c.18-.18.47-.18.64,0,.18.18.18.47,0,.64l-1.43,1.43h3.98c2.45,0,4.17,1.47,4.17,3.58,0,1.97-1.61,3.58-3.58,3.58Z"></path> </svg>'
top_bar_undo_button.addEventListener('click', () => { top_bar_undo_button.addEventListener('click', () => {
this.messageBroker.publish('undo') this.messageBroker.publish('undo')
@@ -3007,19 +3178,21 @@ class UIManager {
var top_bar_redo_button = document.createElement('div') var top_bar_redo_button = document.createElement('div')
top_bar_redo_button.id = 'maskEditor_topBarRedoButton' top_bar_redo_button.id = 'maskEditor_topBarRedoButton'
top_bar_redo_button.classList.add('maskEditor_topPanelIconButton') top_bar_redo_button.classList.add(iconButtonAccentColor)
top_bar_redo_button.innerHTML = top_bar_redo_button.innerHTML =
'<svg viewBox="0 0 15 15" style="width: 36px;height: 36px;pointer-events: none;fill: var(--input-text);"> <path class="cls-1" d="M6.23,12.18c-1.97,0-3.58-1.61-3.58-3.58,0-2.11,1.71-3.58,4.17-3.58h3.98l-1.43-1.43c-.18-.18-.18-.47,0-.64.18-.18.46-.18.64,0l2.21,2.21c.09.09.13.2.13.32s-.05.24-.13.32l-2.21,2.21c-.18.18-.47.18-.64,0-.18-.18-.18-.47,0-.64l1.43-1.43h-3.98c-1.92,0-3.26,1.1-3.26,2.67,0,1.47,1.2,2.67,2.67,2.67.25,0,.46.2.46.46s-.2.46-.46.46Z"/></svg>' '<svg viewBox="0 0 15 15"> <path class="cls-1" d="M6.23,12.18c-1.97,0-3.58-1.61-3.58-3.58,0-2.11,1.71-3.58,4.17-3.58h3.98l-1.43-1.43c-.18-.18-.18-.47,0-.64.18-.18.46-.18.64,0l2.21,2.21c.09.09.13.2.13.32s-.05.24-.13.32l-2.21,2.21c-.18.18-.47.18-.64,0-.18-.18-.18-.47,0-.64l1.43-1.43h-3.98c-1.92,0-3.26,1.1-3.26,2.67,0,1.47,1.2,2.67,2.67,2.67.25,0,.46.2.46.46s-.2.46-.46.46Z"/></svg>'
top_bar_redo_button.addEventListener('click', () => { top_bar_redo_button.addEventListener('click', () => {
this.messageBroker.publish('redo') this.messageBroker.publish('redo')
}) })
top_bar_shortcuts_container.appendChild(top_bar_undo_button) var top_bar_invert_button = document.createElement('button')
top_bar_shortcuts_container.appendChild(top_bar_redo_button) top_bar_invert_button.id = 'maskEditor_topBarInvertButton'
top_bar_invert_button.classList.add(buttonAccentColor)
var top_bar_button_container = document.createElement('div') top_bar_invert_button.innerText = 'Invert'
top_bar_button_container.id = 'maskEditor_topBarButtonContainer' top_bar_invert_button.addEventListener('click', () => {
this.messageBroker.publish('invert')
})
var top_bar_clear_button = document.createElement('button') var top_bar_clear_button = document.createElement('button')
top_bar_clear_button.id = 'maskEditor_topBarClearButton' top_bar_clear_button.id = 'maskEditor_topBarClearButton'
@@ -3055,23 +3228,26 @@ class UIManager {
this.maskEditor.close() this.maskEditor.close()
}) })
top_bar_button_container.appendChild(top_bar_clear_button) top_bar_shortcuts_container.appendChild(top_bar_undo_button)
top_bar_button_container.appendChild(top_bar_save_button) top_bar_shortcuts_container.appendChild(top_bar_redo_button)
top_bar_button_container.appendChild(top_bar_cancel_button) top_bar_shortcuts_container.appendChild(top_bar_invert_button)
top_bar_shortcuts_container.appendChild(top_bar_clear_button)
top_bar_shortcuts_container.appendChild(top_bar_save_button)
top_bar_shortcuts_container.appendChild(top_bar_cancel_button)
top_bar.appendChild(top_bar_title_container) top_bar.appendChild(top_bar_title_container)
top_bar.appendChild(top_bar_shortcuts_container) top_bar.appendChild(top_bar_shortcuts_container)
top_bar.appendChild(top_bar_button_container)
return top_bar return top_bar
} }
//----------------
private createToolPanel() { private createToolPanel() {
var pen_tool_panel = document.createElement('div') var tool_panel = document.createElement('div')
pen_tool_panel.id = 'maskEditor_toolPanel' tool_panel.id = 'maskEditor_toolPanel'
this.toolPanel = pen_tool_panel this.toolPanel = tool_panel
var toolPanelHoverAccent = this.darkMode
? 'maskEditor_toolPanelContainerDark'
: 'maskEditor_toolPanelContainerLight'
var toolElements: HTMLElement[] = [] var toolElements: HTMLElement[] = []
@@ -3082,6 +3258,7 @@ class UIManager {
toolPanel_brushToolContainer.classList.add( toolPanel_brushToolContainer.classList.add(
'maskEditor_toolPanelContainerSelected' 'maskEditor_toolPanelContainerSelected'
) )
toolPanel_brushToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_brushToolContainer.innerHTML = ` toolPanel_brushToolContainer.innerHTML = `
<svg viewBox="0 0 44 44"> <svg viewBox="0 0 44 44">
<path class="cls-1" d="M34,13.93c0,.47-.19.94-.55,1.31l-13.02,13.04c-.09.07-.18.15-.27.22-.07-1.39-1.21-2.48-2.61-2.49.07-.12.16-.24.27-.34l13.04-13.04c.72-.72,1.89-.72,2.6,0,.35.35.55.83.55,1.3Z"/> <path class="cls-1" d="M34,13.93c0,.47-.19.94-.55,1.31l-13.02,13.04c-.09.07-.18.15-.27.22-.07-1.39-1.21-2.48-2.61-2.49.07-.12.16-.24.27-.34l13.04-13.04c.72-.72,1.89-.72,2.6,0,.35.35.55.83.55,1.3Z"/>
@@ -3116,6 +3293,7 @@ class UIManager {
var toolPanel_eraserToolContainer = document.createElement('div') var toolPanel_eraserToolContainer = document.createElement('div')
toolPanel_eraserToolContainer.classList.add('maskEditor_toolPanelContainer') toolPanel_eraserToolContainer.classList.add('maskEditor_toolPanelContainer')
toolPanel_eraserToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_eraserToolContainer.innerHTML = ` toolPanel_eraserToolContainer.innerHTML = `
<svg viewBox="0 0 44 44"> <svg viewBox="0 0 44 44">
<g> <g>
@@ -3155,6 +3333,7 @@ class UIManager {
toolPanel_paintBucketToolContainer.classList.add( toolPanel_paintBucketToolContainer.classList.add(
'maskEditor_toolPanelContainer' 'maskEditor_toolPanelContainer'
) )
toolPanel_paintBucketToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_paintBucketToolContainer.innerHTML = ` toolPanel_paintBucketToolContainer.innerHTML = `
<svg viewBox="0 0 44 44"> <svg viewBox="0 0 44 44">
<path class="cls-1" d="M33.4,21.76l-11.42,11.41-.04.05c-.61.61-1.6.61-2.21,0l-8.91-8.91c-.61-.61-.61-1.6,0-2.21l.04-.05.3-.29h22.24Z"/> <path class="cls-1" d="M33.4,21.76l-11.42,11.41-.04.05c-.61.61-1.6.61-2.21,0l-8.91-8.91c-.61-.61-.61-1.6,0-2.21l.04-.05.3-.29h22.24Z"/>
@@ -3198,6 +3377,7 @@ class UIManager {
toolPanel_colorSelectToolContainer.classList.add( toolPanel_colorSelectToolContainer.classList.add(
'maskEditor_toolPanelContainer' 'maskEditor_toolPanelContainer'
) )
toolPanel_colorSelectToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_colorSelectToolContainer.innerHTML = ` toolPanel_colorSelectToolContainer.innerHTML = `
<svg viewBox="0 0 44 44"> <svg viewBox="0 0 44 44">
<path class="cls-1" d="M30.29,13.72c-1.09-1.1-2.85-1.09-3.94,0l-2.88,2.88-.75-.75c-.2-.19-.51-.19-.71,0-.19.2-.19.51,0,.71l1.4,1.4-9.59,9.59c-.35.36-.54.82-.54,1.32,0,.14,0,.28.05.41-.05.04-.1.08-.15.13-.39.39-.39,1.01,0,1.4.38.39,1.01.39,1.4,0,.04-.04.08-.09.11-.13.14.04.3.06.45.06.5,0,.97-.19,1.32-.55l9.59-9.59,1.38,1.38c.1.09.22.14.35.14s.26-.05.35-.14c.2-.2.2-.52,0-.71l-.71-.72,2.88-2.89c1.08-1.08,1.08-2.85-.01-3.94ZM19.43,25.82h-2.46l7.15-7.15,1.23,1.23-5.92,5.92Z"/> <path class="cls-1" d="M30.29,13.72c-1.09-1.1-2.85-1.09-3.94,0l-2.88,2.88-.75-.75c-.2-.19-.51-.19-.71,0-.19.2-.19.51,0,.71l1.4,1.4-9.59,9.59c-.35.36-.54.82-.54,1.32,0,.14,0,.28.05.41-.05.04-.1.08-.15.13-.39.39-.39,1.01,0,1.4.38.39,1.01.39,1.4,0,.04-.04.08-.09.11-.13.14.04.3.06.45.06.5,0,.97-.19,1.32-.55l9.59-9.59,1.38,1.38c.1.09.22.14.35.14s.26-.05.35-.14c.2-.2.2-.52,0-.71l-.71-.72,2.88-2.89c1.08-1.08,1.08-2.85-.01-3.94ZM19.43,25.82h-2.46l7.15-7.15,1.23,1.23-5.92,5.92Z"/>
@@ -3230,17 +3410,35 @@ class UIManager {
toolPanel_colorSelectToolIndicator toolPanel_colorSelectToolIndicator
) )
pen_tool_panel.appendChild(toolPanel_brushToolContainer) //zoom indicator
pen_tool_panel.appendChild(toolPanel_eraserToolContainer) var toolPanel_zoomIndicator = document.createElement('div')
pen_tool_panel.appendChild(toolPanel_paintBucketToolContainer) toolPanel_zoomIndicator.classList.add('maskEditor_toolPanelZoomIndicator')
pen_tool_panel.appendChild(toolPanel_colorSelectToolContainer) toolPanel_zoomIndicator.classList.add(toolPanelHoverAccent)
var pen_tool_panel_change_tool_button = document.createElement('button') var toolPanel_zoomText = document.createElement('span')
pen_tool_panel_change_tool_button.id = toolPanel_zoomText.id = 'maskEditor_toolPanelZoomText'
'maskEditor_toolPanelChangeToolButton' toolPanel_zoomText.innerText = '100%'
pen_tool_panel_change_tool_button.innerText = 'change to Eraser' this.zoomTextHTML = toolPanel_zoomText
return pen_tool_panel var toolPanel_DimensionsText = document.createElement('span')
toolPanel_DimensionsText.id = 'maskEditor_toolPanelDimensionsText'
toolPanel_DimensionsText.innerText = ' '
this.dimensionsTextHTML = toolPanel_DimensionsText
toolPanel_zoomIndicator.appendChild(toolPanel_zoomText)
toolPanel_zoomIndicator.appendChild(toolPanel_DimensionsText)
toolPanel_zoomIndicator.addEventListener('click', () => {
this.messageBroker.publish('resetZoom')
})
tool_panel.appendChild(toolPanel_brushToolContainer)
tool_panel.appendChild(toolPanel_eraserToolContainer)
tool_panel.appendChild(toolPanel_paintBucketToolContainer)
tool_panel.appendChild(toolPanel_colorSelectToolContainer)
tool_panel.appendChild(toolPanel_zoomIndicator)
return tool_panel
} }
private createPointerZone() { private createPointerZone() {
@@ -3384,6 +3582,8 @@ class UIManager {
maskCanvas.width = this.image.width maskCanvas.width = this.image.width
maskCanvas.height = this.image.height maskCanvas.height = this.image.height
this.dimensionsTextHTML.innerText = `${this.image.width}x${this.image.height}`
await this.invalidateCanvas(this.image, mask_image) await this.invalidateCanvas(this.image, mask_image)
this.messageBroker.publish('initZoomPan', [this.image, this.rootElement]) this.messageBroker.publish('initZoomPan', [this.image, this.rootElement])
} }
@@ -3638,6 +3838,14 @@ class UIManager {
this.updateBrushPreview() this.updateBrushPreview()
this.setBrushPreviewGradientVisibility(false) this.setBrushPreviewGradientVisibility(false)
} }
setZoomText(zoomText: string) {
this.zoomTextHTML.innerText = zoomText
}
setDimensionsText(dimensionsText: string) {
this.dimensionsTextHTML.innerText = dimensionsText
}
} }
class ToolManager { class ToolManager {
@@ -3814,6 +4022,7 @@ class PanAndZoomManager {
lastTouchPoint: Point = { x: 0, y: 0 } lastTouchPoint: Point = { x: 0, y: 0 }
zoom_ratio: number = 1 zoom_ratio: number = 1
interpolatedZoomRatio: number = 1
pan_offset: Offset = { x: 0, y: 0 } pan_offset: Offset = { x: 0, y: 0 }
mouseDownPoint: Point | null = null mouseDownPoint: Point | null = null
@@ -3821,8 +4030,11 @@ class PanAndZoomManager {
canvasContainer: HTMLElement | null = null canvasContainer: HTMLElement | null = null
maskCanvas: HTMLCanvasElement | null = null maskCanvas: HTMLCanvasElement | null = null
rootElement: HTMLElement | null = null
image: HTMLImageElement | null = null image: HTMLImageElement | null = null
imageRootWidth: number = 0
imageRootHeight: number = 0
cursorPoint: Point = { x: 0, y: 0 } cursorPoint: Point = { x: 0, y: 0 }
@@ -3878,6 +4090,11 @@ class PanAndZoomManager {
this.handleTouchEnd(event) this.handleTouchEnd(event)
} }
) )
this.messageBroker.subscribe('resetZoom', async () => {
if (this.interpolatedZoomRatio === 1) return
await this.smoothResetView()
})
} }
private addPullTopics() { private addPullTopics() {
@@ -4054,14 +4271,25 @@ class PanAndZoomManager {
const mouseX = cursorPoint.x - rect.left const mouseX = cursorPoint.x - rect.left
const mouseY = cursorPoint.y - rect.top const mouseY = cursorPoint.y - rect.top
console.log(oldZoom, newZoom)
// Calculate new pan position // Calculate new pan position
const scaleFactor = newZoom / oldZoom const scaleFactor = newZoom / oldZoom
this.pan_offset.x += mouseX - mouseX * scaleFactor this.pan_offset.x += mouseX - mouseX * scaleFactor
this.pan_offset.y += mouseY - mouseY * scaleFactor this.pan_offset.y += mouseY - mouseY * scaleFactor
console.log(this.imageRootWidth, this.imageRootHeight)
// Update pan and zoom immediately // Update pan and zoom immediately
await this.invalidatePanZoom() await this.invalidatePanZoom()
const newImageWidth = maskCanvas.clientWidth
const zoomRatio = newImageWidth / this.imageRootWidth
this.interpolatedZoomRatio = zoomRatio
this.messageBroker.publish('setZoomText', `${Math.round(zoomRatio * 100)}%`)
// Update cursor position with new pan values // Update cursor position with new pan values
this.updateCursorPosition(cursorPoint) this.updateCursorPosition(cursorPoint)
@@ -4071,51 +4299,125 @@ class PanAndZoomManager {
}) })
} }
private async smoothResetView(duration: number = 500) {
// Store initial state
const startZoom = this.zoom_ratio
const startPan = { ...this.pan_offset }
// Panel dimensions
const sidePanelWidth = 220
const toolPanelWidth = 64
const topBarHeight = 44
// Calculate available space
const availableWidth =
this.rootElement!.clientWidth - sidePanelWidth - toolPanelWidth
const availableHeight = this.rootElement!.clientHeight - topBarHeight
// Calculate target zoom
const zoomRatioWidth = availableWidth / this.image!.width
const zoomRatioHeight = availableHeight / this.image!.height
const targetZoom = Math.min(zoomRatioWidth, zoomRatioHeight)
// Calculate final dimensions
const aspectRatio = this.image!.width / this.image!.height
let finalWidth = 0
let finalHeight = 0
// Calculate target pan position
const targetPan = { x: toolPanelWidth, y: topBarHeight }
if (zoomRatioHeight > zoomRatioWidth) {
finalWidth = availableWidth
finalHeight = finalWidth / aspectRatio
targetPan.y = (availableHeight - finalHeight) / 2 + topBarHeight
} else {
finalHeight = availableHeight
finalWidth = finalHeight * aspectRatio
targetPan.x = (availableWidth - finalWidth) / 2 + toolPanelWidth
}
const startTime = performance.now()
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// Cubic easing out for smooth deceleration
const eased = 1 - Math.pow(1 - progress, 3)
// Calculate intermediate zoom and pan values
const currentZoom = startZoom + (targetZoom - startZoom) * eased
this.zoom_ratio = currentZoom
this.pan_offset.x = startPan.x + (targetPan.x - startPan.x) * eased
this.pan_offset.y = startPan.y + (targetPan.y - startPan.y) * eased
this.invalidatePanZoom()
const interpolatedZoomRatio = startZoom + (1.0 - startZoom) * eased
this.messageBroker.publish(
'setZoomText',
`${Math.round(interpolatedZoomRatio * 100)}%`
)
if (progress < 1) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
this.interpolatedZoomRatio = 1.0
}
async initializeCanvasPanZoom( async initializeCanvasPanZoom(
image: HTMLImageElement, image: HTMLImageElement,
rootElement: HTMLElement rootElement: HTMLElement
) { ) {
// Get side panel width // Get side panel width
let sidePanelWidth = 220 let sidePanelWidth = 220
const toolPanelWidth = 64
let topBarHeight = 44 let topBarHeight = 44
this.rootElement = rootElement
// Calculate available width accounting for both side panels // Calculate available width accounting for both side panels
let availableWidth = rootElement.clientWidth - 2 * sidePanelWidth let availableWidth =
rootElement.clientWidth - sidePanelWidth - toolPanelWidth
let availableHeight = rootElement.clientHeight - topBarHeight let availableHeight = rootElement.clientHeight - topBarHeight
// Initial dimensions let zoomRatioWidth = availableWidth / image.width
let drawWidth = image.width let zoomRatioHeight = availableHeight / image.height
let drawHeight = image.height
// First check if width needs scaling let aspectRatio = image.width / image.height
if (drawWidth > availableWidth) {
drawWidth = availableWidth
drawHeight = (drawWidth / image.width) * image.height
}
// Then check if height needs scaling let finalWidth = 0
if (drawHeight > availableHeight) { let finalHeight = 0
drawHeight = availableHeight
drawWidth = (drawHeight / image.height) * image.width let pan_offset: Offset = { x: toolPanelWidth, y: topBarHeight }
if (zoomRatioHeight > zoomRatioWidth) {
finalWidth = availableWidth
finalHeight = finalWidth / aspectRatio
pan_offset.y = (availableHeight - finalHeight) / 2 + topBarHeight
} else {
finalHeight = availableHeight
finalWidth = finalHeight * aspectRatio
pan_offset.x = (availableWidth - finalWidth) / 2 + toolPanelWidth
} }
if (this.image === null) { if (this.image === null) {
this.image = image this.image = image
} }
this.zoom_ratio = drawWidth / image.width this.imageRootWidth = finalWidth
this.imageRootHeight = finalHeight
// Center the canvas in the available space this.zoom_ratio = Math.min(zoomRatioWidth, zoomRatioHeight)
const canvasX = sidePanelWidth + (availableWidth - drawWidth) / 2 this.pan_offset = pan_offset
const canvasY = (availableHeight - drawHeight) / 2
this.pan_offset.x = canvasX
this.pan_offset.y = canvasY
await this.invalidatePanZoom() await this.invalidatePanZoom()
} }
//probably move to PanZoomManager
async invalidatePanZoom() { async invalidatePanZoom() {
// Single validation check upfront // Single validation check upfront
if ( if (
@@ -4132,10 +4434,6 @@ class PanAndZoomManager {
const raw_width = this.image.width * this.zoom_ratio const raw_width = this.image.width * this.zoom_ratio
const raw_height = this.image.height * this.zoom_ratio const raw_height = this.image.height * this.zoom_ratio
// Adjust pan offset
this.pan_offset.x = Math.max(10 - raw_width, this.pan_offset.x)
this.pan_offset.y = Math.max(10 - raw_height, this.pan_offset.y)
// Get canvas container // Get canvas container
this.canvasContainer ??= this.canvasContainer ??=
await this.messageBroker?.pull('getCanvasContainer') await this.messageBroker?.pull('getCanvasContainer')
@@ -4238,6 +4536,9 @@ class MessageBroker {
this.createPushTopic('setMaskBoundary') this.createPushTopic('setMaskBoundary')
this.createPushTopic('setMaskTolerance') this.createPushTopic('setMaskTolerance')
this.createPushTopic('setBrushSmoothingPrecision') this.createPushTopic('setBrushSmoothingPrecision')
this.createPushTopic('setZoomText')
this.createPushTopic('resetZoom')
this.createPushTopic('invert')
} }
/** /**
@@ -4425,12 +4726,38 @@ app.registerExtension({
settings: [ settings: [
{ {
id: 'Comfy.MaskEditor.UseNewEditor', id: 'Comfy.MaskEditor.UseNewEditor',
category: ['Comfy', 'Masking'], category: ['Mask Editor', 'NewEditor'],
name: 'Use new mask editor', name: 'Use new mask editor',
tooltip: 'Switch to the new mask editor interface', tooltip: 'Switch to the new mask editor interface',
type: 'boolean', type: 'boolean',
defaultValue: true, defaultValue: true,
experimental: true experimental: true
},
{
id: 'Comfy.MaskEditor.BrushAdjustmentSpeed',
category: ['Mask Editor', 'BrushAdjustment', 'Sensitivity'],
name: 'Brush adjustment speed multiplier',
tooltip:
'Controls how quickly the brush size and hardness change when adjusting. Higher values mean faster changes.',
experimental: true,
type: 'slider',
attrs: {
min: 0.1,
max: 2.0,
step: 0.1
},
defaultValue: 1.0,
versionAdded: '1.0.0'
},
{
id: 'Comfy.MaskEditor.UseDominantAxis',
category: ['Mask Editor', 'BrushAdjustment', 'UseDominantAxis'],
name: 'Lock brush adjustment to dominant axis',
tooltip:
'When enabled, brush adjustments will only affect size OR hardness based on which direction you move more',
type: 'boolean',
defaultValue: true,
experimental: true
} }
], ],
init(app) { init(app) {