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;
align-items: center;
position: relative;
transition: background-color border 0.2s;
}
.maskEditor_toolPanelContainer:hover {
background-color: var(--p-overlaybadge-outline-color);
border: none;
transition: background-color 0.2s;
}
.maskEditor_toolPanelContainerSelected svg {
fill: var(--p-button-text-primary-color) !important;
@@ -292,6 +288,15 @@ var styles = `
aspect-ratio: 1/1;
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 {
display: none;
height: 100%;
@@ -379,16 +384,56 @@ var styles = `
}
#maskEditor_topBarShortcutsContainer {
display: flex;
gap: 10px;
margin-left: 5px;
}
.maskEditor_topPanelIconButton {
width: 53.3px;
.maskEditor_topPanelIconButton_dark {
width: 50px;
height: 30px;
pointer-events: auto;
display: flex;
justify-content: center;
align-items: center;
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 {
@@ -657,6 +702,24 @@ var styles = `
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')
@@ -1229,6 +1292,8 @@ class PaintBucketTool {
this.messageBroker.subscribe('paintBucketFill', (point: Point) =>
this.floodFill(point)
)
this.messageBroker.subscribe('invert', () => this.invertMask())
}
private addPullTopics() {
@@ -1374,6 +1439,48 @@ class PaintBucketTool {
getTolerance(): number {
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 {
@@ -1809,10 +1916,15 @@ class BrushTool {
smoothingLastDrawTime!: Date
maskCtx: CanvasRenderingContext2D | null = null
brushStrokeCanvas: HTMLCanvasElement | null = null
brushStrokeCtx: CanvasRenderingContext2D | null = null
//brush adjustment
isBrushAdjusting: boolean = false
brushPreviewGradient: HTMLElement | null = null
initialPoint: Point | null = null
useDominantAxis: boolean = false
brushAdjustmentSpeed: number = 1.0
maskEditor: MaskEditorDialog
messageBroker: MessageBroker
@@ -1823,6 +1935,13 @@ class BrushTool {
this.createListeners()
this.addPullTopics()
this.useDominantAxis = app.extensionManager.setting.get(
'Comfy.MaskEditor.UseDominantAxis'
)
this.brushAdjustmentSpeed = app.extensionManager.setting.get(
'Comfy.MaskEditor.BrushAdjustmentSpeed'
)
this.brushSettings = {
size: 10,
opacity: 100,
@@ -1860,7 +1979,7 @@ class BrushTool {
)
//drawing
this.messageBroker.subscribe('drawStart', (event: PointerEvent) =>
this.start_drawing(event)
this.startDrawing(event)
)
this.messageBroker.subscribe('draw', (event: PointerEvent) =>
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
let compositionOp: CompositionOperation
let currentTool = await this.messageBroker.pull('currentTool')
let coords = { x: event.offsetX, y: event.offsetY }
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
await this.createBrushStrokeCanvas()
//set drawing mode
if (currentTool === Tools.Eraser || event.buttons == 2) {
@@ -1931,15 +2065,6 @@ class BrushTool {
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
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)
requestAnimationFrame(() => {
this.init_shape(CompositionOperation.SourceOver)
@@ -2051,29 +2176,62 @@ class BrushTool {
private async handleBrushAdjustment(event: PointerEvent) {
const coords = { x: event.offsetX, y: event.offsetY }
const brushDeadZone = 5
let coords_canvas = await this.messageBroker.pull('screenToCanvas', coords)
const delta_x = coords_canvas.x - this.initialPoint!.x
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(
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(
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.hardness = newHardness
this.messageBroker.publish('updateBrushPreview')
return
}
//helper functions
@@ -2263,8 +2421,8 @@ class BrushTool {
}
private setBrushSmoothingPrecision(precision: number) {
console.log('precision', precision)
// this.brushSettings.smoothingPrecision = precision
//console.log('precision', precision)
this.smoothingPrecision = precision
}
}
@@ -2300,6 +2458,9 @@ class UIManager {
private mask_opacity: number = 0.7
private maskBlendMode: MaskBlendMode = MaskBlendMode.Black
private zoomTextHTML!: HTMLSpanElement
private dimensionsTextHTML!: HTMLSpanElement
constructor(rootElement: HTMLElement, maskEditor: MaskEditorDialog) {
this.rootElement = rootElement
this.maskEditor = maskEditor
@@ -2332,6 +2493,10 @@ class UIManager {
)
this.messageBroker.subscribe('updateCursor', () => this.updateCursor())
this.messageBroker.subscribe('setZoomText', (text: string) =>
this.setZoomText(text)
)
}
addPullTopics() {
@@ -2975,11 +3140,17 @@ class UIManager {
return separator
}
//----------------
private async createTopBar() {
const buttonAccentColor = this.darkMode
? 'maskEditor_topPanelButton_dark'
: 'maskEditor_topPanelButton_light'
const iconButtonAccentColor = this.darkMode
? 'maskEditor_topPanelIconButton_dark'
: 'maskEditor_topPanelIconButton_light'
var top_bar = document.createElement('div')
top_bar.id = 'maskEditor_topBar'
@@ -2997,9 +3168,9 @@ class UIManager {
var top_bar_undo_button = document.createElement('div')
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 =
'<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', () => {
this.messageBroker.publish('undo')
@@ -3007,19 +3178,21 @@ class UIManager {
var top_bar_redo_button = document.createElement('div')
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 =
'<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', () => {
this.messageBroker.publish('redo')
})
top_bar_shortcuts_container.appendChild(top_bar_undo_button)
top_bar_shortcuts_container.appendChild(top_bar_redo_button)
var top_bar_button_container = document.createElement('div')
top_bar_button_container.id = 'maskEditor_topBarButtonContainer'
var top_bar_invert_button = document.createElement('button')
top_bar_invert_button.id = 'maskEditor_topBarInvertButton'
top_bar_invert_button.classList.add(buttonAccentColor)
top_bar_invert_button.innerText = 'Invert'
top_bar_invert_button.addEventListener('click', () => {
this.messageBroker.publish('invert')
})
var top_bar_clear_button = document.createElement('button')
top_bar_clear_button.id = 'maskEditor_topBarClearButton'
@@ -3055,23 +3228,26 @@ class UIManager {
this.maskEditor.close()
})
top_bar_button_container.appendChild(top_bar_clear_button)
top_bar_button_container.appendChild(top_bar_save_button)
top_bar_button_container.appendChild(top_bar_cancel_button)
top_bar_shortcuts_container.appendChild(top_bar_undo_button)
top_bar_shortcuts_container.appendChild(top_bar_redo_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_shortcuts_container)
top_bar.appendChild(top_bar_button_container)
return top_bar
}
//----------------
private createToolPanel() {
var pen_tool_panel = document.createElement('div')
pen_tool_panel.id = 'maskEditor_toolPanel'
this.toolPanel = pen_tool_panel
var tool_panel = document.createElement('div')
tool_panel.id = 'maskEditor_toolPanel'
this.toolPanel = tool_panel
var toolPanelHoverAccent = this.darkMode
? 'maskEditor_toolPanelContainerDark'
: 'maskEditor_toolPanelContainerLight'
var toolElements: HTMLElement[] = []
@@ -3082,6 +3258,7 @@ class UIManager {
toolPanel_brushToolContainer.classList.add(
'maskEditor_toolPanelContainerSelected'
)
toolPanel_brushToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_brushToolContainer.innerHTML = `
<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"/>
@@ -3116,6 +3293,7 @@ class UIManager {
var toolPanel_eraserToolContainer = document.createElement('div')
toolPanel_eraserToolContainer.classList.add('maskEditor_toolPanelContainer')
toolPanel_eraserToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_eraserToolContainer.innerHTML = `
<svg viewBox="0 0 44 44">
<g>
@@ -3155,6 +3333,7 @@ class UIManager {
toolPanel_paintBucketToolContainer.classList.add(
'maskEditor_toolPanelContainer'
)
toolPanel_paintBucketToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_paintBucketToolContainer.innerHTML = `
<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"/>
@@ -3198,6 +3377,7 @@ class UIManager {
toolPanel_colorSelectToolContainer.classList.add(
'maskEditor_toolPanelContainer'
)
toolPanel_colorSelectToolContainer.classList.add(toolPanelHoverAccent)
toolPanel_colorSelectToolContainer.innerHTML = `
<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"/>
@@ -3230,17 +3410,35 @@ class UIManager {
toolPanel_colorSelectToolIndicator
)
pen_tool_panel.appendChild(toolPanel_brushToolContainer)
pen_tool_panel.appendChild(toolPanel_eraserToolContainer)
pen_tool_panel.appendChild(toolPanel_paintBucketToolContainer)
pen_tool_panel.appendChild(toolPanel_colorSelectToolContainer)
//zoom indicator
var toolPanel_zoomIndicator = document.createElement('div')
toolPanel_zoomIndicator.classList.add('maskEditor_toolPanelZoomIndicator')
toolPanel_zoomIndicator.classList.add(toolPanelHoverAccent)
var pen_tool_panel_change_tool_button = document.createElement('button')
pen_tool_panel_change_tool_button.id =
'maskEditor_toolPanelChangeToolButton'
pen_tool_panel_change_tool_button.innerText = 'change to Eraser'
var toolPanel_zoomText = document.createElement('span')
toolPanel_zoomText.id = 'maskEditor_toolPanelZoomText'
toolPanel_zoomText.innerText = '100%'
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() {
@@ -3384,6 +3582,8 @@ class UIManager {
maskCanvas.width = this.image.width
maskCanvas.height = this.image.height
this.dimensionsTextHTML.innerText = `${this.image.width}x${this.image.height}`
await this.invalidateCanvas(this.image, mask_image)
this.messageBroker.publish('initZoomPan', [this.image, this.rootElement])
}
@@ -3638,6 +3838,14 @@ class UIManager {
this.updateBrushPreview()
this.setBrushPreviewGradientVisibility(false)
}
setZoomText(zoomText: string) {
this.zoomTextHTML.innerText = zoomText
}
setDimensionsText(dimensionsText: string) {
this.dimensionsTextHTML.innerText = dimensionsText
}
}
class ToolManager {
@@ -3814,6 +4022,7 @@ class PanAndZoomManager {
lastTouchPoint: Point = { x: 0, y: 0 }
zoom_ratio: number = 1
interpolatedZoomRatio: number = 1
pan_offset: Offset = { x: 0, y: 0 }
mouseDownPoint: Point | null = null
@@ -3821,8 +4030,11 @@ class PanAndZoomManager {
canvasContainer: HTMLElement | null = null
maskCanvas: HTMLCanvasElement | null = null
rootElement: HTMLElement | null = null
image: HTMLImageElement | null = null
imageRootWidth: number = 0
imageRootHeight: number = 0
cursorPoint: Point = { x: 0, y: 0 }
@@ -3878,6 +4090,11 @@ class PanAndZoomManager {
this.handleTouchEnd(event)
}
)
this.messageBroker.subscribe('resetZoom', async () => {
if (this.interpolatedZoomRatio === 1) return
await this.smoothResetView()
})
}
private addPullTopics() {
@@ -4054,14 +4271,25 @@ class PanAndZoomManager {
const mouseX = cursorPoint.x - rect.left
const mouseY = cursorPoint.y - rect.top
console.log(oldZoom, newZoom)
// Calculate new pan position
const scaleFactor = newZoom / oldZoom
this.pan_offset.x += mouseX - mouseX * scaleFactor
this.pan_offset.y += mouseY - mouseY * scaleFactor
console.log(this.imageRootWidth, this.imageRootHeight)
// Update pan and zoom immediately
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
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(
image: HTMLImageElement,
rootElement: HTMLElement
) {
// Get side panel width
let sidePanelWidth = 220
const toolPanelWidth = 64
let topBarHeight = 44
this.rootElement = rootElement
// 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
// Initial dimensions
let drawWidth = image.width
let drawHeight = image.height
let zoomRatioWidth = availableWidth / image.width
let zoomRatioHeight = availableHeight / image.height
// First check if width needs scaling
if (drawWidth > availableWidth) {
drawWidth = availableWidth
drawHeight = (drawWidth / image.width) * image.height
}
let aspectRatio = image.width / image.height
// Then check if height needs scaling
if (drawHeight > availableHeight) {
drawHeight = availableHeight
drawWidth = (drawHeight / image.height) * image.width
let finalWidth = 0
let finalHeight = 0
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) {
this.image = image
}
this.zoom_ratio = drawWidth / image.width
this.imageRootWidth = finalWidth
this.imageRootHeight = finalHeight
// Center the canvas in the available space
const canvasX = sidePanelWidth + (availableWidth - drawWidth) / 2
const canvasY = (availableHeight - drawHeight) / 2
this.pan_offset.x = canvasX
this.pan_offset.y = canvasY
this.zoom_ratio = Math.min(zoomRatioWidth, zoomRatioHeight)
this.pan_offset = pan_offset
await this.invalidatePanZoom()
}
//probably move to PanZoomManager
async invalidatePanZoom() {
// Single validation check upfront
if (
@@ -4132,10 +4434,6 @@ class PanAndZoomManager {
const raw_width = this.image.width * 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
this.canvasContainer ??=
await this.messageBroker?.pull('getCanvasContainer')
@@ -4238,6 +4536,9 @@ class MessageBroker {
this.createPushTopic('setMaskBoundary')
this.createPushTopic('setMaskTolerance')
this.createPushTopic('setBrushSmoothingPrecision')
this.createPushTopic('setZoomText')
this.createPushTopic('resetZoom')
this.createPushTopic('invert')
}
/**
@@ -4425,12 +4726,38 @@ app.registerExtension({
settings: [
{
id: 'Comfy.MaskEditor.UseNewEditor',
category: ['Comfy', 'Masking'],
category: ['Mask Editor', 'NewEditor'],
name: 'Use new mask editor',
tooltip: 'Switch to the new mask editor interface',
type: 'boolean',
defaultValue: 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) {