feat(ui): mask layers choose own opacity

This commit is contained in:
psychedelicious 2024-08-16 19:56:01 +10:00
parent 43b3fab6be
commit cb293fd7ac
14 changed files with 43 additions and 124 deletions

View File

@ -12,7 +12,6 @@ import {
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import {
clipToBboxChanged,
@ -64,7 +63,6 @@ const ControlLayersSettingsPopover = () => {
<PopoverContent>
<PopoverBody>
<Flex direction="column" gap={2}>
<MaskOpacity />
<FormControl w="full">
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
<Checkbox isChecked={invertScroll} onChange={onChangeInvertScroll} />

View File

@ -1,13 +1,12 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { stopPropagation } from 'common/util/stopPropagation';
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { imFillColorChanged, imFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { FillStyle } from 'features/controlLayers/store/types';
import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
export const InpaintMaskMaskFillColorPicker = memo(() => {
@ -15,7 +14,7 @@ export const InpaintMaskMaskFillColorPicker = memo(() => {
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill);
const onChangeFillColor = useCallback(
(color: RgbColor) => {
(color: RgbaColor) => {
dispatch(imFillColorChanged({ color }));
},
[dispatch]
@ -44,7 +43,7 @@ export const InpaintMaskMaskFillColorPicker = memo(() => {
<PopoverContent>
<PopoverBody minH={64}>
<Flex flexDir="column" gap={4}>
<RgbColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<IAIColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<MaskFillStyle style={fill.style} onChange={onChangeFillStyle} />
</Flex>
</PopoverBody>

View File

@ -1,49 +0,0 @@
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { maskOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`;
export const MaskOpacity = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const opacity = useAppSelector((s) => Math.round(s.canvasV2.settings.maskOpacity * 100));
const onChange = useCallback(
(v: number) => {
dispatch(maskOpacityChanged(Math.max(v / 100, 0.25)));
},
[dispatch]
);
return (
<FormControl orientation="vertical">
<FormLabel m={0}>{t('controlLayers.globalMaskOpacity')}</FormLabel>
<Flex gap={4}>
<CompositeSlider
min={25}
max={100}
step={1}
value={opacity}
defaultValue={0.3}
onChange={onChange}
marks={marks}
minW={48}
/>
<CompositeNumberInput
min={25}
max={100}
step={1}
value={opacity}
defaultValue={0.3}
onChange={onChange}
w={28}
format={formatPct}
/>
</Flex>
</FormControl>
);
});
MaskOpacity.displayName = 'MaskOpacity';

View File

@ -1,15 +1,14 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { stopPropagation } from 'common/util/stopPropagation';
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import type { FillStyle } from 'features/controlLayers/store/types';
import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceMaskFillColorPicker = memo(() => {
@ -18,7 +17,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => {
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
const onChangeFillColor = useCallback(
(color: RgbColor) => {
(color: RgbaColor) => {
dispatch(rgFillColorChanged({ id: entityIdentifier.id, color }));
},
[dispatch, entityIdentifier.id]
@ -47,7 +46,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => {
<PopoverContent>
<PopoverBody minH={64}>
<Flex flexDir="column" gap={4}>
<RgbColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<IAIColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<MaskFillStyle style={fill.style} onChange={onChangeFillStyle} />
</Flex>
</PopoverBody>

View File

@ -323,7 +323,6 @@ export class CanvasManager {
if (
this._isFirstRender ||
state.regions.entities !== this._prevState.regions.entities ||
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) {
@ -355,7 +354,6 @@ export class CanvasManager {
if (
this._isFirstRender ||
state.inpaintMask !== this._prevState.inpaintMask ||
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) {

View File

@ -22,7 +22,6 @@ export class CanvasMaskAdapter {
log: Logger;
state: CanvasInpaintMaskState | CanvasRegionalGuidanceState;
maskOpacity: number;
transformer: CanvasTransformer;
renderer: CanvasObjectRenderer;
@ -54,8 +53,6 @@ export class CanvasMaskAdapter {
this.renderer = new CanvasObjectRenderer(this);
this.transformer = new CanvasTransformer(this);
this.maskOpacity = this.manager.stateApi.getMaskOpacity();
}
/**
@ -79,14 +76,8 @@ export class CanvasMaskAdapter {
isSelected: boolean;
}) => {
const state = get(arg, 'state', this.state);
const maskOpacity = this.manager.stateApi.getMaskOpacity();
if (
!this.isFirstRender &&
state === this.state &&
state.fill === this.state.fill &&
maskOpacity === this.maskOpacity
) {
if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) {
this.log.trace('State unchanged, skipping update');
return;
}
@ -107,10 +98,6 @@ export class CanvasMaskAdapter {
this.updateVisibility({ isEnabled });
}
if (this.isFirstRender || state.fill !== this.state.fill || maskOpacity !== this.maskOpacity) {
this.renderer.updateCompositingRect(state.fill, maskOpacity);
this.maskOpacity = maskOpacity;
}
// this.transformer.syncInteractionState();
if (this.isFirstRender) {

View File

@ -1,5 +1,5 @@
import type { JSONObject } from 'common/types';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { deepClone } from 'common/util/deepClone';
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
@ -149,7 +149,7 @@ export class CanvasObjectRenderer {
this.subscriptions.add(
this.manager.stateApi.$stageAttrs.listen(() => {
if (this.konva.compositing && this.parent.type === 'mask_adapter') {
this.updateCompositingRect(this.parent.state.fill, this.manager.stateApi.getMaskOpacity());
this.updateCompositingRect(this.parent.state.fill);
}
})
);
@ -183,14 +183,13 @@ export class CanvasObjectRenderer {
return didRender;
};
updateCompositingRect = (fill: Fill, opacity: number) => {
updateCompositingRect = (fill: Fill) => {
this.log.trace('Updating compositing rect');
assert(this.konva.compositing, 'Missing compositing rect');
const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get();
console.log('stageAttrs', this.manager.stateApi.$stageAttrs.get());
const attrs: RectConfig = {
opacity,
x: -x / scale,
y: -y / scale,
width: width / scale,
@ -198,7 +197,7 @@ export class CanvasObjectRenderer {
};
if (fill.style === 'solid') {
attrs.fill = rgbColorToString(fill.color);
attrs.fill = rgbaColorToString(fill.color);
attrs.fillPriority = 'color';
this.konva.compositing.rect.setAttrs(attrs);
} else {

View File

@ -171,9 +171,6 @@ export class CanvasStateApi {
getInpaintMaskState = () => {
return this.getState().inpaintMask;
};
getMaskOpacity = () => {
return this.getState().settings.maskOpacity;
};
getSession = () => {
return this.getState().session;
};
@ -232,26 +229,23 @@ export class CanvasStateApi {
let currentFill: RgbaColor = state.tool.fill;
const selectedEntity = this.getSelectedEntity();
if (selectedEntity) {
// These two entity types use a compositing rect for opacity. Their fill is always white.
// These two entity types use a compositing rect for opacity. Their fill is always a solid color.
if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') {
currentFill = RGBA_RED;
// currentFill = RGBA_WHITE;
}
}
return currentFill;
};
getBrushPreviewFill = () => {
const state = this.getState();
let currentFill: RgbaColor = state.tool.fill;
getBrushPreviewFill = (): RgbaColor => {
const selectedEntity = this.getSelectedEntity();
if (selectedEntity) {
if (selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'inpaint_mask') {
// The brush should use the mask opacity for these entity types
if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') {
currentFill = { ...selectedEntity.state.fill.color, a: this.getSettings().maskOpacity };
}
return selectedEntity.state.fill.color;
} else {
const state = this.getState();
return state.tool.fill;
}
return currentFill;
};
$transformingEntity = $transformingEntity;

View File

@ -1,5 +1,5 @@
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import type { FillStyle, RgbColor } from 'features/controlLayers/store/types';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types';
import crosshatch from './pattern-crosshatch.svg?raw';
import diagonal from './pattern-diagonal.svg?raw';
@ -7,7 +7,7 @@ import grid from './pattern-grid.svg?raw';
import horizontal from './pattern-horizontal.svg?raw';
import vertical from './pattern-vertical.svg?raw';
export function getPatternSVG(pattern: Exclude<FillStyle, 'solid'>, color: RgbColor) {
export function getPatternSVG(pattern: Exclude<FillStyle, 'solid'>, color: RgbaColor) {
let content: string = 'data:image/svg+xml;utf8,';
if (pattern === 'crosshatch') {
content += crosshatch;
@ -21,7 +21,7 @@ export function getPatternSVG(pattern: Exclude<FillStyle, 'solid'>, color: RgbCo
content += grid;
}
content = content.replaceAll('stroke:black', `stroke:${rgbColorToString(color)}`);
content = content.replaceAll('stroke:black', `stroke:${rgbaColorToString(color)}`);
return content;
}

View File

@ -40,7 +40,7 @@ import type {
FilterConfig,
StageAttrs,
} from './types';
import { IMAGE_FILTERS, isDrawableEntity, RGBA_RED } from './types';
import { IMAGE_FILTERS, isDrawableEntity } from './types';
const initialState: CanvasV2State = {
_version: 3,
@ -55,7 +55,7 @@ const initialState: CanvasV2State = {
type: 'inpaint_mask',
fill: {
style: 'diagonal',
color: RGBA_RED,
color: { r: 255, g: 122, b: 0, a: 1 }, // some orange color
},
rasterizationCache: [],
isEnabled: true,
@ -69,7 +69,7 @@ const initialState: CanvasV2State = {
selected: 'view',
selectedBuffer: null,
invertScroll: false,
fill: RGBA_RED,
fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
brush: {
width: 50,
},
@ -87,7 +87,6 @@ const initialState: CanvasV2State = {
},
},
settings: {
maskOpacity: 0.3,
// TODO(psyche): These are copied from old canvas state, need to be implemented
autoSave: false,
imageSmoothing: true,
@ -471,7 +470,6 @@ export const {
invertScrollChanged,
toolChanged,
toolBufferChanged,
maskOpacityChanged,
allEntitiesDeleted,
clipToBboxChanged,
canvasReset,

View File

@ -1,6 +1,5 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import type { CanvasInpaintMaskState, CanvasV2State, FillStyle } from 'features/controlLayers/store/types';
import type { RgbColor } from 'react-colorful';
import type { CanvasInpaintMaskState, CanvasV2State, FillStyle, RgbaColor } from 'features/controlLayers/store/types';
export const inpaintMaskReducers = {
imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
@ -8,7 +7,7 @@ export const inpaintMaskReducers = {
state.inpaintMask = data;
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
},
imFillColorChanged: (state, action: PayloadAction<{ color: RgbColor }>) => {
imFillColorChanged: (state, action: PayloadAction<{ color: RgbaColor }>) => {
const { color } = action.payload;
state.inpaintMask.fill.color = color;
},

View File

@ -6,6 +6,7 @@ import type {
FillStyle,
IPMethodV2,
RegionalGuidanceIPAdapterConfig,
RgbaColor,
} from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
@ -14,7 +15,7 @@ import { isEqual } from 'lodash-es';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import type { CanvasRegionalGuidanceState, RgbColor } from './types';
import type { CanvasRegionalGuidanceState } from './types';
export const selectRegionalGuidanceEntity = (state: CanvasV2State, id: string) => {
return state.regions.entities.find((rg) => rg.id === id);
@ -32,17 +33,17 @@ export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: st
return rg;
};
const DEFAULT_MASK_COLORS: RgbColor[] = [
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
{ r: 131, g: 214, b: 131 }, // rgb(131, 214, 131)
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
{ r: 220, g: 144, b: 101 }, // rgb(220, 144, 101)
{ r: 224, g: 117, b: 117 }, // rgb(224, 117, 117)
{ r: 213, g: 139, b: 202 }, // rgb(213, 139, 202)
{ r: 161, g: 120, b: 214 }, // rgb(161, 120, 214)
const DEFAULT_MASK_COLORS: RgbaColor[] = [
{ r: 121, g: 157, b: 219, a: 0.5 }, // rgb(121, 157, 219)
{ r: 131, g: 214, b: 131, a: 0.5 }, // rgb(131, 214, 131)
{ r: 250, g: 225, b: 80, a: 0.5 }, // rgb(250, 225, 80)
{ r: 220, g: 144, b: 101, a: 0.5 }, // rgb(220, 144, 101)
{ r: 224, g: 117, b: 117, a: 0.5 }, // rgb(224, 117, 117)
{ r: 213, g: 139, b: 202, a: 0.5 }, // rgb(213, 139, 202)
{ r: 161, g: 120, b: 214, a: 0.5 }, // rgb(161, 120, 214)
];
const getRGMaskFill = (state: CanvasV2State): RgbColor => {
const getRGMaskFill = (state: CanvasV2State): RgbaColor => {
const lastFill = state.regions.entities.slice(-1)[0]?.fill.color;
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
if (i === -1) {
@ -104,7 +105,7 @@ export const regionsReducers = {
}
entity.negativePrompt = prompt;
},
rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => {
rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbaColor }>) => {
const { id, color } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
if (!entity) {

View File

@ -2,9 +2,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import type { CanvasV2State } from 'features/controlLayers/store/types';
export const settingsReducers = {
maskOpacityChanged: (state, action: PayloadAction<number>) => {
state.settings.maskOpacity = action.payload;
},
clipToBboxChanged: (state, action: PayloadAction<boolean>) => {
state.settings.clipToBbox = action.payload;
},

View File

@ -644,7 +644,7 @@ const zMaskObject = z
const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
export type FillStyle = z.infer<typeof zFillStyle>;
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
const zFill = z.object({ style: zFillStyle, color: zRgbColor });
const zFill = z.object({ style: zFillStyle, color: zRgbaColor });
export type Fill = z.infer<typeof zFill>;
const zImageCache = z.object({
@ -858,7 +858,6 @@ export type CanvasV2State = {
};
settings: {
imageSmoothing: boolean;
maskOpacity: number;
showHUD: boolean;
autoSave: boolean;
preserveMaskedArea: boolean;