mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 08:20:53 +00:00
## Summary - Remove hardcoded `bg-white` from mask editor canvas background div to prevent white flash on dialog open - Add a loading spinner while the mask editor initializes (image loading, canvas setup, GPU resources) - Background color is set dynamically by `setCanvasBackground()` after initialization Fixes #9852 ### AS IS https://github.com/user-attachments/assets/7da61e32-671b-4056-b5ec-8cb246fc7689 ### TO BE https://github.com/user-attachments/assets/bfdedc69-f690-42c5-8591-619623c04f55 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9860-fix-prevent-white-flash-when-opening-mask-editor-3226d73d365081de9b7ad4622438e6ed) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
236 lines
6.0 KiB
Vue
236 lines
6.0 KiB
Vue
<template>
|
|
<div
|
|
ref="containerRef"
|
|
class="maskEditor-dialog-root flex size-full flex-col"
|
|
@contextmenu.prevent
|
|
@dragstart="handleDragStart"
|
|
@keydown.stop
|
|
>
|
|
<div
|
|
id="maskEditorCanvasContainer"
|
|
ref="canvasContainerRef"
|
|
@contextmenu.prevent
|
|
>
|
|
<canvas
|
|
ref="imgCanvasRef"
|
|
class="absolute top-0 left-0 z-0 size-full"
|
|
@contextmenu.prevent
|
|
/>
|
|
<canvas
|
|
ref="rgbCanvasRef"
|
|
class="absolute top-0 left-0 z-10 size-full"
|
|
@contextmenu.prevent
|
|
/>
|
|
<canvas
|
|
ref="maskCanvasRef"
|
|
class="absolute top-0 left-0 z-30 size-full"
|
|
@contextmenu.prevent
|
|
/>
|
|
<!-- GPU Preview Canvas -->
|
|
<canvas
|
|
ref="gpuCanvasRef"
|
|
class="pointer-events-none absolute top-0 left-0 size-full"
|
|
:class="{
|
|
'z-20': store.activeLayer === 'rgb',
|
|
'z-40': store.activeLayer === 'mask'
|
|
}"
|
|
/>
|
|
<div ref="canvasBackgroundRef" class="size-full" />
|
|
</div>
|
|
|
|
<LoadingOverlay :loading="!initialized" size="sm" />
|
|
|
|
<div class="maskEditor-ui-container flex min-h-0 flex-1 flex-col">
|
|
<div class="flex min-h-0 flex-1 overflow-hidden">
|
|
<ToolPanel
|
|
v-if="initialized"
|
|
ref="toolPanelRef"
|
|
:tool-manager="toolManager!"
|
|
/>
|
|
|
|
<PointerZone
|
|
v-if="initialized"
|
|
:tool-manager="toolManager!"
|
|
:pan-zoom="panZoom!"
|
|
/>
|
|
|
|
<SidePanel
|
|
v-if="initialized"
|
|
ref="sidePanelRef"
|
|
:tool-manager="toolManager!"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<BrushCursor v-if="initialized" :container-ref="containerRef" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
|
import { useImageLoader } from '@/composables/maskeditor/useImageLoader'
|
|
import { useKeyboard } from '@/composables/maskeditor/useKeyboard'
|
|
import { useMaskEditorLoader } from '@/composables/maskeditor/useMaskEditorLoader'
|
|
import { usePanAndZoom } from '@/composables/maskeditor/usePanAndZoom'
|
|
import { useToolManager } from '@/composables/maskeditor/useToolManager'
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import { useDialogStore } from '@/stores/dialogStore'
|
|
import { useMaskEditorDataStore } from '@/stores/maskEditorDataStore'
|
|
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
|
|
|
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
|
|
|
import BrushCursor from './BrushCursor.vue'
|
|
import PointerZone from './PointerZone.vue'
|
|
import SidePanel from './SidePanel.vue'
|
|
import ToolPanel from './ToolPanel.vue'
|
|
|
|
const { node } = defineProps<{
|
|
node: LGraphNode
|
|
}>()
|
|
|
|
const store = useMaskEditorStore()
|
|
const dataStore = useMaskEditorDataStore()
|
|
const dialogStore = useDialogStore()
|
|
|
|
const loader = useMaskEditorLoader()
|
|
|
|
const containerRef = ref<HTMLElement>()
|
|
const canvasContainerRef = ref<HTMLDivElement>()
|
|
const imgCanvasRef = ref<HTMLCanvasElement>()
|
|
const maskCanvasRef = ref<HTMLCanvasElement>()
|
|
const rgbCanvasRef = ref<HTMLCanvasElement>()
|
|
const gpuCanvasRef = ref<HTMLCanvasElement>()
|
|
const canvasBackgroundRef = ref<HTMLDivElement>()
|
|
|
|
const toolPanelRef = ref<InstanceType<typeof ToolPanel>>()
|
|
const sidePanelRef = ref<InstanceType<typeof SidePanel>>()
|
|
|
|
const initialized = ref(false)
|
|
|
|
const keyboard = useKeyboard()
|
|
const panZoom = usePanAndZoom()
|
|
|
|
const toolManager = useToolManager(keyboard, panZoom)
|
|
|
|
let resizeObserver: ResizeObserver | null = null
|
|
|
|
const handleDragStart = (event: DragEvent) => {
|
|
if (event.ctrlKey) {
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
const initUI = async () => {
|
|
if (!containerRef.value) {
|
|
console.error(
|
|
'[MaskEditorContent] Cannot initialize - missing required refs'
|
|
)
|
|
return
|
|
}
|
|
|
|
if (
|
|
!imgCanvasRef.value ||
|
|
!maskCanvasRef.value ||
|
|
!rgbCanvasRef.value ||
|
|
!canvasContainerRef.value ||
|
|
!canvasBackgroundRef.value
|
|
) {
|
|
console.error('[MaskEditorContent] Cannot initialize - missing canvas refs')
|
|
return
|
|
}
|
|
|
|
store.maskCanvas = maskCanvasRef.value
|
|
store.rgbCanvas = rgbCanvasRef.value
|
|
store.imgCanvas = imgCanvasRef.value
|
|
store.canvasContainer = canvasContainerRef.value
|
|
store.canvasBackground = canvasBackgroundRef.value
|
|
|
|
try {
|
|
await loader.loadFromNode(node)
|
|
|
|
const imageLoader = useImageLoader()
|
|
const image = await imageLoader.loadImages()
|
|
|
|
await panZoom.initializeCanvasPanZoom(
|
|
image,
|
|
containerRef.value,
|
|
toolPanelRef.value?.$el as HTMLElement | undefined,
|
|
sidePanelRef.value?.$el as HTMLElement | undefined
|
|
)
|
|
|
|
store.canvasHistory.saveInitialState()
|
|
|
|
// Initialize GPU resources
|
|
if (toolManager.brushDrawing) {
|
|
await toolManager.brushDrawing.initGPUResources()
|
|
if (gpuCanvasRef.value && toolManager.brushDrawing.initPreviewCanvas) {
|
|
// Match preview canvas resolution to mask canvas
|
|
gpuCanvasRef.value.width = maskCanvasRef.value.width
|
|
gpuCanvasRef.value.height = maskCanvasRef.value.height
|
|
|
|
toolManager.brushDrawing.initPreviewCanvas(gpuCanvasRef.value)
|
|
}
|
|
}
|
|
|
|
initialized.value = true
|
|
} catch (error) {
|
|
console.error('[MaskEditorContent] Initialization failed:', error)
|
|
dialogStore.closeDialog()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
keyboard.addListeners()
|
|
|
|
if (containerRef.value) {
|
|
resizeObserver = new ResizeObserver(async () => {
|
|
if (panZoom) {
|
|
await panZoom.invalidatePanZoom()
|
|
}
|
|
})
|
|
resizeObserver.observe(containerRef.value)
|
|
}
|
|
|
|
void initUI()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
toolManager.brushDrawing.saveBrushSettings()
|
|
|
|
keyboard?.removeListeners()
|
|
|
|
if (resizeObserver) {
|
|
resizeObserver.disconnect()
|
|
resizeObserver = null
|
|
}
|
|
|
|
store.canvasHistory.clearStates()
|
|
|
|
store.resetState()
|
|
dataStore.reset()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.maskEditor-dialog-root {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.maskEditor-ui-container {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
:deep(#maskEditorCanvasContainer) {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
}
|
|
</style>
|