mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 20:51:58 +00:00
fully refactor mask editor into vue-based (#6629)
## Summary
This PR refactors the mask editor from a vanilla JavaScript
implementation to Vue 3 + Composition API, aligning it with the ComfyUI
frontend's modern architecture. This is a structural refactor without UI
changes - all visual appearances and user interactions remain identical.
Net change: +1,700 lines (mostly tests)
## Changes
- Converted from class-based managers to Vue 3 Composition API
- Migrated state management to Pinia stores (maskEditorStore,
maskEditorDataStore)
- Split monolithic managers into focused composables:
- useBrushDrawing - Brush rendering and drawing logic
- useCanvasManager - Canvas lifecycle and operations
- useCanvasTools - Tool-specific canvas operations
- usePanAndZoom - Pan and zoom functionality
- useToolManager - Tool selection and coordination
- useKeyboard - Keyboard shortcuts
- useMaskEditorLoader/Saver - Data loading and saving
- useCoordinateTransform - Coordinate system transformations
- Replaced imperative DOM manipulation with Vue components
- Added comprehensive test coverage
## What This PR Does NOT Change
Preserved Original Styling:
- Original CSS retained in packages/design-system/src/css/style.css
- Some generic controls (DropdownControl, SliderControl, ToggleControl)
preserved as-is
- Future migration to Tailwind and PrimeVue components is planned but
out of scope for this PR
Preserved Core Functionality:
- Drawing algorithms and brush rendering logic remain unchanged
- Pan/zoom calculations preserved
- Canvas operations (composite modes, image processing) unchanged
- Tool behaviors (brush, color select, paint bucket) identical
- No changes to mask generation or export logic
DO NOT Review:
- CSS styling choices (preserved from original)
- Drawing algorithm implementations (unchanged)
- Canvas rendering logic (ported as-is)
- UI/UX changes (none exist)
- Component library choices (future work)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6629-fully-refactor-mask-editor-into-vue-based-2a46d73d36508114ab8bd2984b4b54e4)
by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1346,3 +1346,466 @@ audio.comfy-audio.empty-audio-widget {
|
||||
border-radius: 0;
|
||||
}
|
||||
/* END LOD specific styles */
|
||||
|
||||
/* ===================== Mask Editor Styles ===================== */
|
||||
/* To be migrated to Tailwind later */
|
||||
#maskEditor_brush {
|
||||
position: absolute;
|
||||
backgroundColor: transparent;
|
||||
z-index: 8889;
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
overflow: visible;
|
||||
outline: 1px dashed black;
|
||||
box-shadow: 0 0 0 1px white;
|
||||
}
|
||||
#maskEditor_brushPreviewGradient {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
.maskEditor_sidePanelTitle {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-family: sans-serif;
|
||||
color: var(--descrip-text);
|
||||
margin-top: 10px;
|
||||
}
|
||||
.maskEditor_sidePanelBrushShapeCircle {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-color);
|
||||
pointer-events: auto;
|
||||
transition: background 0.1s;
|
||||
margin-left: 7.5px;
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange {
|
||||
width: 180px;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-webkit-slider-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
margin-top: -8px;
|
||||
background: var(--p-surface-700);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-moz-range-thumb {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
background: var(--p-surface-800);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-webkit-slider-runnable-track {
|
||||
background: var(--p-surface-700);
|
||||
height: 3px;
|
||||
}
|
||||
.maskEditor_sidePanelBrushRange::-moz-range-track {
|
||||
background: var(--p-surface-700);
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelBrushShapeSquare {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
pointer-events: auto;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_dark {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_dark:hover {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_light {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.maskEditor_brushShape_light:hover {
|
||||
background: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
.maskEditor_sidePanelLayerVisibilityContainer {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.maskEditor_sidePanelVisibilityToggle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.maskEditor_sidePanelLayerIconContainer {
|
||||
width: 60px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
fill: var(--input-text);
|
||||
}
|
||||
.maskEditor_sidePanelLayerIconContainer svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.maskEditor_sidePanelBigButton {
|
||||
width: 85px;
|
||||
height: 30px;
|
||||
background: rgb(0 0 0 / 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--input-text);
|
||||
font-family: sans-serif;
|
||||
font-size: 15px;
|
||||
pointer-events: auto;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
.maskEditor_sidePanelBigButton:hover {
|
||||
background-color: var(--p-overlaybadge-outline-color);
|
||||
border: none;
|
||||
}
|
||||
.maskEditor_toolPanelContainer {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.maskEditor_toolPanelContainerSelected svg {
|
||||
fill: var(--p-button-text-primary-color) !important;
|
||||
}
|
||||
.maskEditor_toolPanelContainerSelected .maskEditor_toolPanelIndicator {
|
||||
display: block;
|
||||
}
|
||||
.maskEditor_toolPanelContainer svg {
|
||||
width: 75%;
|
||||
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%;
|
||||
width: 4px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: var(--p-button-text-primary-color);
|
||||
}
|
||||
.maskEditor_sidePanelSeparator {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--border-color);
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#maskEditorCanvasContainer {
|
||||
position: absolute;
|
||||
width: 1000px;
|
||||
height: 667px;
|
||||
left: 359px;
|
||||
top: 280px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
height: 30px;
|
||||
background: var(--p-surface-800);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
color: var(--input-text);
|
||||
font-family: sans-serif;
|
||||
pointer-events: auto;
|
||||
transition: 0.1s;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_dark:hover {
|
||||
background-color: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_light {
|
||||
height: 30px;
|
||||
background: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
border-radius: 10px;
|
||||
color: var(--input-text);
|
||||
font-family: sans-serif;
|
||||
pointer-events: auto;
|
||||
transition: 0.1s;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.maskEditor_topPanelButton_light:hover {
|
||||
background-color: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanel_paintBucket_Container {
|
||||
width: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanel_colorSelect_Container {
|
||||
display: flex;
|
||||
width: 180px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanel_colorSelect_tolerance_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelContainerColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelContainerRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_dark {
|
||||
background: var(--p-surface-800);
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_very_dark {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_light {
|
||||
background: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_accent_bg_very_light {
|
||||
background: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
|
||||
.maskEditor_sidePanelToggleContainer {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelToggleSwitch {
|
||||
display: inline-block;
|
||||
border-radius: 16px;
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
transition: background 0.25s;
|
||||
background: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelToggleSwitch {
|
||||
background: var(--p-surface-700);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelToggleSwitch::before, .maskEditor_sidePanelToggleSwitch::after {
|
||||
content: "";
|
||||
}
|
||||
.maskEditor_sidePanelToggleSwitch::before {
|
||||
display: block;
|
||||
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
transition: ease 0.2s;
|
||||
}
|
||||
.maskEditor_sidePanelToggleContainer:hover .maskEditor_sidePanelToggleSwitch::before {
|
||||
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
|
||||
}
|
||||
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch {
|
||||
background: var(--p-button-text-primary-color);
|
||||
}
|
||||
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
|
||||
background: var(--comfy-menu-bg);
|
||||
left: 20px;
|
||||
}
|
||||
.dark-theme .maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelToggleCheckbox {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown {
|
||||
border: 1px solid var(--p-form-field-border-color);
|
||||
background: var(--comfy-menu-bg);
|
||||
height: 24px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown option {
|
||||
background: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown:focus {
|
||||
outline: 1px solid var(--p-surface-300);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelDropdown option:hover {
|
||||
background: white;
|
||||
}
|
||||
.maskEditor_sidePanelDropdown option:active {
|
||||
background: var(--p-surface-300);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown option {
|
||||
background: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown:focus {
|
||||
outline: 1px solid var(--p-button-text-primary-color);
|
||||
}
|
||||
|
||||
.dark-theme .maskEditor_sidePanelDropdown option:active {
|
||||
background: var(--p-highlight-background);
|
||||
}
|
||||
|
||||
.maskEditor_layerRow {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayerPreviewContainer {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayerPreviewContainer > svg{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
fill: var(--p-surface-100);
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelImageLayerImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelSubTitle {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-family: sans-serif;
|
||||
color: var(--descrip-text);
|
||||
}
|
||||
|
||||
.maskEditor_containerDropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.maskEditor_sidePanelLayerCheckbox {
|
||||
margin-left: 15px;
|
||||
}
|
||||
/* ===================== End of Mask Editor Styles ===================== */
|
||||
Reference in New Issue
Block a user