feat(ui): make control image opacity filter toggleable

This commit is contained in:
psychedelicious 2024-04-30 18:50:29 +10:00 committed by Kent Keirsey
parent 1212698059
commit 8a791d4f16
6 changed files with 46 additions and 11 deletions

View File

@ -1541,6 +1541,7 @@
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
"globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapter": "Global $t(common.ipAdapter)",
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)" "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
"opacityFilter": "Opacity Filter"
} }
} }

View File

@ -10,10 +10,12 @@ import {
PopoverBody, PopoverBody,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
Switch,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
import { layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import { isFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi'; import { PiDropHalfFill } from 'react-icons/pi';
@ -28,13 +30,19 @@ const formatPct = (v: number | string) => `${v} %`;
const CALayerOpacity = ({ layerId }: Props) => { const CALayerOpacity = ({ layerId }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const opacity = useLayerOpacity(layerId); const { opacity, isFilterEnabled } = useLayerOpacity(layerId);
const onChange = useCallback( const onChangeOpacity = useCallback(
(v: number) => { (v: number) => {
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
}, },
[dispatch, layerId] [dispatch, layerId]
); );
const onChangeFilter = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(isFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked }));
},
[dispatch, layerId]
);
return ( return (
<Popover isLazy> <Popover isLazy>
<PopoverTrigger> <PopoverTrigger>
@ -49,7 +57,13 @@ const CALayerOpacity = ({ layerId }: Props) => {
<PopoverArrow /> <PopoverArrow />
<PopoverBody> <PopoverBody>
<Flex direction="column" gap={2}> <Flex direction="column" gap={2}>
<FormControl orientation="horizontal" minW={96}> <FormControl orientation="horizontal" w="full">
<FormLabel m={0} flexGrow={1} cursor="pointer">
{t('controlLayers.opacityFilter')}
</FormLabel>
<Switch isChecked={isFilterEnabled} onChange={onChangeFilter} />
</FormControl>
<FormControl orientation="horizontal">
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel> <FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<CompositeSlider <CompositeSlider
min={0} min={0}
@ -57,8 +71,9 @@ const CALayerOpacity = ({ layerId }: Props) => {
step={1} step={1}
value={opacity} value={opacity}
defaultValue={100} defaultValue={100}
onChange={onChange} onChange={onChangeOpacity}
marks={marks} marks={marks}
w={48}
/> />
<CompositeNumberInput <CompositeNumberInput
min={0} min={0}
@ -66,8 +81,8 @@ const CALayerOpacity = ({ layerId }: Props) => {
step={1} step={1}
value={opacity} value={opacity}
defaultValue={100} defaultValue={100}
onChange={onChange} onChange={onChangeOpacity}
minW={24} w={24}
format={formatPct} format={formatPct}
/> />
</FormControl> </FormControl>

View File

@ -72,7 +72,7 @@ export const useLayerOpacity = (layerId: string) => {
createSelector(selectControlLayersSlice, (controlLayers) => { createSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(layer, `Layer ${layerId} not found`);
return Math.round(layer.opacity * 100); return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled };
}), }),
[layerId] [layerId]
); );

View File

@ -141,6 +141,7 @@ export const controlLayersSlice = createSlice({
imageName: null, imageName: null,
opacity: 1, opacity: 1,
isSelected: true, isSelected: true,
isFilterEnabled: true,
}; };
state.layers.push(layer); state.layers.push(layer);
state.selectedLayerId = layer.id; state.selectedLayerId = layer.id;
@ -243,6 +244,19 @@ export const controlLayersSlice = createSlice({
}, },
//#endregion //#endregion
//#region CA Layers
isFilterEnabledChanged: (
state,
action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>
) => {
const { layerId, isFilterEnabled } = action.payload;
const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
if (layer) {
layer.isFilterEnabled = isFilterEnabled;
}
},
//#endregion
//#region Mask Layers //#region Mask Layers
maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
const { layerId, prompt } = action.payload; const { layerId, prompt } = action.payload;
@ -522,6 +536,8 @@ export const {
ipAdapterLayerAdded, ipAdapterLayerAdded,
controlAdapterLayerAdded, controlAdapterLayerAdded,
layerOpacityChanged, layerOpacityChanged,
// CA layer actions
isFilterEnabledChanged,
// Mask layer actions // Mask layer actions
maskLayerLineAdded, maskLayerLineAdded,
maskLayerPointsAdded, maskLayerPointsAdded,

View File

@ -50,6 +50,7 @@ export type ControlAdapterLayer = RenderableLayerBase & {
controlNetId: string; controlNetId: string;
imageName: string | null; imageName: string | null;
opacity: number; opacity: number;
isFilterEnabled: boolean;
}; };
export type IPAdapterLayer = LayerBase & { export type IPAdapterLayer = LayerBase & {

View File

@ -421,7 +421,6 @@ const createControlNetLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageEle
const konvaImage = new Konva.Image({ const konvaImage = new Konva.Image({
name: CONTROLNET_LAYER_IMAGE_NAME, name: CONTROLNET_LAYER_IMAGE_NAME,
image, image,
filters: [LightnessToAlphaFilter],
}); });
konvaLayer.add(konvaImage); konvaLayer.add(konvaImage);
return konvaImage; return konvaImage;
@ -469,10 +468,12 @@ const updateControlNetLayerImageAttrs = (
let needsCache = false; let needsCache = false;
const newWidth = stage.width() / stage.scaleX(); const newWidth = stage.width() / stage.scaleX();
const newHeight = stage.height() / stage.scaleY(); const newHeight = stage.height() / stage.scaleY();
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
if ( if (
konvaImage.width() !== newWidth || konvaImage.width() !== newWidth ||
konvaImage.height() !== newHeight || konvaImage.height() !== newHeight ||
konvaImage.visible() !== reduxLayer.isEnabled konvaImage.visible() !== reduxLayer.isEnabled ||
hasFilter !== reduxLayer.isFilterEnabled
) { ) {
konvaImage.setAttrs({ konvaImage.setAttrs({
opacity: reduxLayer.opacity, opacity: reduxLayer.opacity,
@ -481,6 +482,7 @@ const updateControlNetLayerImageAttrs = (
width: stage.width() / stage.scaleX(), width: stage.width() / stage.scaleX(),
height: stage.height() / stage.scaleY(), height: stage.height() / stage.scaleY(),
visible: reduxLayer.isEnabled, visible: reduxLayer.isEnabled,
filters: reduxLayer.isFilterEnabled ? [LightnessToAlphaFilter] : [],
}); });
needsCache = true; needsCache = true;
} }