diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index dbce62c8a4..885a937de3 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1541,6 +1541,7 @@ "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", - "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)" + "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", + "opacityFilter": "Opacity Filter" } } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayerOpacity.tsx index 02e63dc8d4..eb414f5369 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayerOpacity.tsx @@ -10,10 +10,12 @@ import { PopoverBody, PopoverContent, PopoverTrigger, + Switch, } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; 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 { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; @@ -28,13 +30,19 @@ const formatPct = (v: number | string) => `${v} %`; const CALayerOpacity = ({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const opacity = useLayerOpacity(layerId); - const onChange = useCallback( + const { opacity, isFilterEnabled } = useLayerOpacity(layerId); + const onChangeOpacity = useCallback( (v: number) => { dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); }, [dispatch, layerId] ); + const onChangeFilter = useCallback( + (e: ChangeEvent) => { + dispatch(isFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); + }, + [dispatch, layerId] + ); return ( @@ -49,7 +57,13 @@ const CALayerOpacity = ({ layerId }: Props) => { - + + + {t('controlLayers.opacityFilter')} + + + + {t('controlLayers.opacity')} { step={1} value={opacity} defaultValue={100} - onChange={onChange} + onChange={onChangeOpacity} marks={marks} + w={48} /> { step={1} value={opacity} defaultValue={100} - onChange={onChange} - minW={24} + onChange={onChangeOpacity} + w={24} format={formatPct} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index abeb7b801d..b4880d1dc6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -72,7 +72,7 @@ export const useLayerOpacity = (layerId: string) => { createSelector(selectControlLayersSlice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); - return Math.round(layer.opacity * 100); + return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; }), [layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 6e3fba453a..3c7371e596 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -141,6 +141,7 @@ export const controlLayersSlice = createSlice({ imageName: null, opacity: 1, isSelected: true, + isFilterEnabled: true, }; state.layers.push(layer); state.selectedLayerId = layer.id; @@ -243,6 +244,19 @@ export const controlLayersSlice = createSlice({ }, //#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 maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; @@ -522,6 +536,8 @@ export const { ipAdapterLayerAdded, controlAdapterLayerAdded, layerOpacityChanged, + // CA layer actions + isFilterEnabledChanged, // Mask layer actions maskLayerLineAdded, maskLayerPointsAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 5f3d11c765..58b25f967b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -50,6 +50,7 @@ export type ControlAdapterLayer = RenderableLayerBase & { controlNetId: string; imageName: string | null; opacity: number; + isFilterEnabled: boolean; }; export type IPAdapterLayer = LayerBase & { diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index f2245b1faf..da835655cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -421,7 +421,6 @@ const createControlNetLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageEle const konvaImage = new Konva.Image({ name: CONTROLNET_LAYER_IMAGE_NAME, image, - filters: [LightnessToAlphaFilter], }); konvaLayer.add(konvaImage); return konvaImage; @@ -469,10 +468,12 @@ const updateControlNetLayerImageAttrs = ( let needsCache = false; const newWidth = stage.width() / stage.scaleX(); const newHeight = stage.height() / stage.scaleY(); + const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; if ( konvaImage.width() !== newWidth || konvaImage.height() !== newHeight || - konvaImage.visible() !== reduxLayer.isEnabled + konvaImage.visible() !== reduxLayer.isEnabled || + hasFilter !== reduxLayer.isFilterEnabled ) { konvaImage.setAttrs({ opacity: reduxLayer.opacity, @@ -481,6 +482,7 @@ const updateControlNetLayerImageAttrs = ( width: stage.width() / stage.scaleX(), height: stage.height() / stage.scaleY(), visible: reduxLayer.isEnabled, + filters: reduxLayer.isFilterEnabled ? [LightnessToAlphaFilter] : [], }); needsCache = true; }