feat(ui): add opacity to initial image layer

This commit is contained in:
psychedelicious 2024-05-02 21:31:17 +10:00 committed by Kent Keirsey
parent 75be6814bb
commit 8b6a283eab
5 changed files with 116 additions and 10 deletions

View File

@ -1,5 +1,6 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IILayerOpacity from 'features/controlLayers/components/IILayer/IILayerOpacity';
import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
@ -60,6 +61,7 @@ export const IILayer = memo(({ layerId }: Props) => {
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="initial_image_layer" />
<Spacer />
<IILayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>

View File

@ -0,0 +1,98 @@
import {
CompositeNumberInput,
CompositeSlider,
Flex,
FormControl,
FormLabel,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import {
iiLayerOpacityChanged,
isInitialImageLayer,
selectControlLayersSlice,
} from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = {
layerId: string;
};
const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`;
const IILayerOpacity = ({ layerId }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectOpacity = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isInitialImageLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return Math.round(layer.opacity * 100);
}),
[layerId]
);
const opacity = useAppSelector(selectOpacity);
const onChangeOpacity = useCallback(
(v: number) => {
dispatch(iiLayerOpacityChanged({ layerId, opacity: v / 100 }));
},
[dispatch, layerId]
);
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton
aria-label={t('controlLayers.opacity')}
size="sm"
icon={<PiDropHalfFill size={16} />}
variant="ghost"
onDoubleClick={stopPropagation}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl orientation="horizontal">
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<CompositeSlider
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
marks={marks}
w={48}
/>
<CompositeNumberInput
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
w={24}
format={formatPct}
/>
</FormControl>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(IILayerOpacity);

View File

@ -631,6 +631,7 @@ export const controlLayersSlice = createSlice({
const layer: InitialImageLayer = {
id: layerId,
type: 'initial_image_layer',
opacity: 1,
x: 0,
y: 0,
bbox: null,
@ -652,12 +653,15 @@ export const controlLayersSlice = createSlice({
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
const { layerId, imageDTO } = action.payload;
const layer = selectIILayerOrThrow(state, layerId);
if (layer) {
layer.bbox = null;
layer.bboxNeedsUpdate = true;
layer.isEnabled = true;
layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
}
layer.bbox = null;
layer.bboxNeedsUpdate = true;
layer.isEnabled = true;
layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
iiLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => {
const { layerId, opacity } = action.payload;
const layer = selectIILayerOrThrow(state, layerId);
layer.opacity = opacity;
},
//#endregion
@ -833,6 +837,7 @@ export const {
// II Layer
iiLayerAdded,
iiLayerImageChanged,
iiLayerOpacityChanged,
// Globals
positivePromptChanged,
negativePromptChanged,

View File

@ -76,6 +76,7 @@ export type RegionalGuidanceLayer = RenderableLayerBase & {
export type InitialImageLayer = RenderableLayerBase & {
type: 'initial_image_layer';
opacity: number;
image: ImageWithDims | null;
};

View File

@ -443,7 +443,7 @@ const updateInitialImageLayerImageAttrs = (
konvaImage.visible() !== reduxLayer.isEnabled
) {
konvaImage.setAttrs({
// opacity: reduxLayer.opacity,
opacity: reduxLayer.opacity,
scaleX: 1,
scaleY: 1,
width: stage.width() / stage.scaleX(),
@ -451,9 +451,9 @@ const updateInitialImageLayerImageAttrs = (
visible: reduxLayer.isEnabled,
});
}
// if (konvaImage.opacity() !== reduxLayer.opacity) {
// konvaImage.opacity(reduxLayer.opacity);
// }
if (konvaImage.opacity() !== reduxLayer.opacity) {
konvaImage.opacity(reduxLayer.opacity);
}
};
const updateInitialImageLayerImageSource = async (