Skip to content

Commit 449bc4d

Browse files
feat(ui): abstract out and share logic between comparisons
1 parent 34d68a3 commit 449bc4d

File tree

13 files changed

+260
-242
lines changed

13 files changed

+260
-242
lines changed

invokeai/frontend/web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"@fontsource-variable/inter": "^5.0.18",
6262
"@invoke-ai/ui-library": "^0.0.25",
6363
"@nanostores/react": "^0.7.2",
64-
"@reactuses/core": "^5.0.14",
6564
"@reduxjs/toolkit": "2.2.3",
6665
"@roarr/browser-log-writer": "^1.3.0",
6766
"chakra-react-select": "^4.7.6",

invokeai/frontend/web/pnpm-lock.yaml

Lines changed: 0 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
6565
});
6666
}
6767

68-
$needsFit.set(true)
68+
$needsFit.set(true);
6969
} catch (e) {
7070
if (e instanceof WorkflowVersionError) {
7171
// The workflow version was not recognized in the valid list of versions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useCallback, useMemo, useState } from 'react';
2+
3+
export const useBoolean = (initialValue: boolean) => {
4+
const [isTrue, set] = useState(initialValue);
5+
const setTrue = useCallback(() => set(true), []);
6+
const setFalse = useCallback(() => set(false), []);
7+
const toggle = useCallback(() => set((v) => !v), []);
8+
9+
const api = useMemo(
10+
() => ({
11+
isTrue,
12+
set,
13+
setTrue,
14+
setFalse,
15+
toggle,
16+
}),
17+
[isTrue, set, setTrue, setFalse, toggle]
18+
);
19+
20+
return api;
21+
};

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
22
import { useAppSelector } from 'app/store/storeHooks';
33
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
4+
import type { Dimensions } from 'features/canvas/store/canvasTypes';
45
import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover';
56
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
67
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
@@ -15,7 +16,11 @@ const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
1516
return { firstImage, secondImage };
1617
});
1718

18-
export const ImageComparison = memo(() => {
19+
type Props = {
20+
containerDims: Dimensions;
21+
};
22+
23+
export const ImageComparison = memo(({ containerDims }: Props) => {
1924
const { t } = useTranslation();
2025
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
2126
const { firstImage, secondImage } = useAppSelector(selector);
@@ -26,15 +31,17 @@ export const ImageComparison = memo(() => {
2631
}
2732

2833
if (comparisonMode === 'slider') {
29-
return <ImageComparisonSlider firstImage={firstImage} secondImage={secondImage} />;
34+
return <ImageComparisonSlider containerDims={containerDims} firstImage={firstImage} secondImage={secondImage} />;
3035
}
3136

3237
if (comparisonMode === 'side-by-side') {
33-
return <ImageComparisonSideBySide firstImage={firstImage} secondImage={secondImage} />;
38+
return (
39+
<ImageComparisonSideBySide containerDims={containerDims} firstImage={firstImage} secondImage={secondImage} />
40+
);
3441
}
3542

3643
if (comparisonMode === 'hover') {
37-
return <ImageComparisonHover firstImage={firstImage} secondImage={secondImage} />;
44+
return <ImageComparisonHover containerDims={containerDims} firstImage={firstImage} secondImage={secondImage} />;
3845
}
3946
});
4047

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx

Lines changed: 96 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,114 @@
1-
import { Flex, Image, Text } from '@invoke-ai/ui-library';
1+
import { Box, Flex, Image } from '@invoke-ai/ui-library';
22
import { useAppSelector } from 'app/store/storeHooks';
3+
import { useBoolean } from 'common/hooks/useBoolean';
34
import { preventDefault } from 'common/util/stopPropagation';
4-
import { DROP_SHADOW } from 'features/gallery/components/ImageViewer/useImageViewer';
5-
import { memo, useCallback, useState } from 'react';
6-
import { useTranslation } from 'react-i18next';
7-
import type { ImageDTO } from 'services/api/types';
5+
import type { Dimensions } from 'features/canvas/store/canvasTypes';
6+
import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
7+
import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
8+
import { memo, useMemo, useRef } from 'react';
89

9-
type Props = {
10-
/**
11-
* The first image to compare
12-
*/
13-
firstImage: ImageDTO;
14-
/**
15-
* The second image to compare
16-
*/
17-
secondImage: ImageDTO;
18-
};
10+
import type { ComparisonProps } from './common';
11+
import { fitDimsToContainer, getSecondImageDims } from './common';
1912

20-
export const ImageComparisonHover = memo(({ firstImage, secondImage }: Props) => {
21-
const { t } = useTranslation();
13+
export const ImageComparisonHover = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => {
2214
const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit);
23-
const [isMouseOver, setIsMouseOver] = useState(false);
24-
const onMouseOver = useCallback(() => {
25-
setIsMouseOver(true);
26-
}, []);
27-
const onMouseOut = useCallback(() => {
28-
setIsMouseOver(false);
29-
}, []);
15+
const imageContainerRef = useRef<HTMLDivElement>(null);
16+
const mouseOver = useBoolean(false);
17+
const fittedDims = useMemo<Dimensions>(
18+
() => fitDimsToContainer(containerDims, firstImage),
19+
[containerDims, firstImage]
20+
);
21+
const compareImageDims = useMemo<Dimensions>(
22+
() => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage),
23+
[comparisonFit, fittedDims, firstImage, secondImage]
24+
);
3025
return (
3126
<Flex w="full" h="full" maxW="full" maxH="full" position="relative" alignItems="center" justifyContent="center">
32-
<Flex position="absolute" maxW="full" maxH="full" aspectRatio={firstImage.width / firstImage.height}>
33-
<Image
34-
id="image-comparison-first-image"
35-
w={firstImage.width}
36-
h={firstImage.height}
27+
<Flex
28+
id="image-comparison-wrapper"
29+
w="full"
30+
h="full"
31+
maxW="full"
32+
maxH="full"
33+
position="absolute"
34+
alignItems="center"
35+
justifyContent="center"
36+
>
37+
<Box
38+
ref={imageContainerRef}
39+
position="relative"
40+
id="image-comparison-hover-image-container"
41+
w={fittedDims.width}
42+
h={fittedDims.height}
3743
maxW="full"
3844
maxH="full"
39-
src={firstImage.image_url}
40-
fallbackSrc={firstImage.thumbnail_url}
41-
objectFit="contain"
42-
/>
43-
<Text
44-
position="absolute"
45-
bottom={4}
46-
insetInlineStart={4}
47-
textOverflow="clip"
48-
whiteSpace="nowrap"
49-
filter={DROP_SHADOW}
50-
color="base.50"
51-
>
52-
{t('gallery.viewerImage')}
53-
</Text>
54-
<Flex
55-
position="absolute"
56-
top={0}
57-
right={0}
58-
bottom={0}
59-
left={0}
60-
opacity={isMouseOver ? 1 : 0}
61-
transitionDuration="0.2s"
62-
transitionProperty="common"
45+
userSelect="none"
46+
overflow="hidden"
47+
borderRadius="base"
6348
>
6449
<Image
65-
id="image-comparison-second-image"
66-
w={comparisonFit === 'fill' ? 'full' : secondImage.width}
67-
h={comparisonFit === 'fill' ? 'full' : secondImage.height}
68-
maxW={comparisonFit === 'contain' ? 'full' : undefined}
69-
maxH={comparisonFit === 'contain' ? 'full' : undefined}
70-
src={secondImage.image_url}
71-
fallbackSrc={secondImage.thumbnail_url}
72-
objectFit={comparisonFit}
50+
id="image-comparison-hover-first-image"
51+
src={firstImage.image_url}
52+
fallbackSrc={firstImage.thumbnail_url}
53+
w={fittedDims.width}
54+
h={fittedDims.height}
55+
maxW="full"
56+
maxH="full"
57+
objectFit="cover"
7358
objectPosition="top left"
7459
/>
75-
<Text
60+
<ImageComparisonLabel type="first" opacity={mouseOver.isTrue ? 0 : 1} />
61+
62+
<Box
63+
id="image-comparison-hover-second-image-container"
7664
position="absolute"
77-
bottom={4}
78-
insetInlineStart={4}
79-
textOverflow="clip"
80-
whiteSpace="nowrap"
81-
filter={DROP_SHADOW}
82-
color="base.50"
65+
top={0}
66+
left={0}
67+
right={0}
68+
bottom={0}
69+
overflow="hidden"
70+
opacity={mouseOver.isTrue ? 1 : 0}
71+
transitionDuration="0.2s"
72+
transitionProperty="common"
8373
>
84-
{t('gallery.compareImage')}
85-
</Text>
86-
</Flex>
87-
<Flex
88-
id="image-comparison-interaction-overlay"
89-
position="absolute"
90-
top={0}
91-
right={0}
92-
bottom={0}
93-
left={0}
94-
onMouseOver={onMouseOver}
95-
onMouseOut={onMouseOut}
96-
onContextMenu={preventDefault}
97-
userSelect="none"
98-
/>
74+
<Box
75+
id="image-comparison-hover-bg"
76+
position="absolute"
77+
top={0}
78+
left={0}
79+
right={0}
80+
bottom={0}
81+
backgroundImage={STAGE_BG_DATAURL}
82+
backgroundRepeat="repeat"
83+
opacity={0.2}
84+
/>
85+
<Image
86+
position="relative"
87+
id="image-comparison-hover-second-image"
88+
src={secondImage.image_url}
89+
fallbackSrc={secondImage.thumbnail_url}
90+
w={compareImageDims.width}
91+
h={compareImageDims.height}
92+
maxW={fittedDims.width}
93+
maxH={fittedDims.height}
94+
objectFit={comparisonFit}
95+
objectPosition="top left"
96+
/>
97+
<ImageComparisonLabel type="second" opacity={mouseOver.isTrue ? 1 : 0} />
98+
</Box>
99+
<Box
100+
id="image-comparison-hover-interaction-overlay"
101+
position="absolute"
102+
top={0}
103+
right={0}
104+
bottom={0}
105+
left={0}
106+
onMouseOver={mouseOver.setTrue}
107+
onMouseOut={mouseOver.setFalse}
108+
onContextMenu={preventDefault}
109+
userSelect="none"
110+
/>
111+
</Box>
99112
</Flex>
100113
</Flex>
101114
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { TextProps } from '@invoke-ai/ui-library';
2+
import { Text } from '@invoke-ai/ui-library';
3+
import { memo } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
6+
import { DROP_SHADOW } from './common';
7+
8+
type Props = TextProps & {
9+
type: 'first' | 'second';
10+
};
11+
12+
export const ImageComparisonLabel = memo(({ type, ...rest }: Props) => {
13+
const { t } = useTranslation();
14+
return (
15+
<Text
16+
position="absolute"
17+
bottom={4}
18+
insetInlineEnd={type === 'first' ? undefined : 4}
19+
insetInlineStart={type === 'first' ? 4 : undefined}
20+
textOverflow="clip"
21+
whiteSpace="nowrap"
22+
filter={DROP_SHADOW}
23+
color="base.50"
24+
transitionDuration="0.2s"
25+
transitionProperty="common"
26+
{...rest}
27+
>
28+
{type === 'first' ? t('gallery.viewerImage') : t('gallery.compareImage')}
29+
</Text>
30+
);
31+
});
32+
33+
ImageComparisonLabel.displayName = 'ImageComparisonLabel';

0 commit comments

Comments
 (0)