feat(ui): revised image comparison slider

Should work for any components and image now.
This commit is contained in:
psychedelicious 2024-05-31 15:18:31 +10:00
parent 72bbcb2d94
commit 7a4bbd092e
4 changed files with 248 additions and 136 deletions

View File

@ -375,7 +375,9 @@
"bulkDownloadRequestFailed": "Problem Preparing Download", "bulkDownloadRequestFailed": "Problem Preparing Download",
"bulkDownloadFailed": "Download Failed", "bulkDownloadFailed": "Download Failed",
"problemDeletingImages": "Problem Deleting Images", "problemDeletingImages": "Problem Deleting Images",
"problemDeletingImagesDesc": "One or more images could not be deleted" "problemDeletingImagesDesc": "One or more images could not be deleted",
"firstImage": "First Image",
"secondImage": "Second Image"
}, },
"hotkeys": { "hotkeys": {
"searchHotkeys": "Search Hotkeys", "searchHotkeys": "Search Hotkeys",

View File

@ -54,7 +54,7 @@ const BBOX_SELECTED_STROKE = 'rgba(78, 190, 255, 1)';
const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)'; const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
// This is invokeai/frontend/web/public/assets/images/transparent_bg.png as a dataURL // This is invokeai/frontend/web/public/assets/images/transparent_bg.png as a dataURL
const STAGE_BG_DATAURL = export const STAGE_BG_DATAURL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII='; 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII=';
const mapId = (object: { id: string }) => object.id; const mapId = (object: { id: string }) => object.id;

View File

@ -1,175 +1,274 @@
import { Box, Flex, Icon } from '@invoke-ai/ui-library'; import { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
import { useMeasure } from '@reactuses/core'; import { useMeasure, type UseMeasureRect } from '@reactuses/core';
import type { Dimensions } from 'features/canvas/store/canvasTypes'; import type { Dimensions } from 'features/canvas/store/canvasTypes';
import { memo, useCallback, useMemo, useRef } from 'react'; import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0))';
const INITIAL_POS = '50%'; const INITIAL_POS = '50%';
const HANDLE_WIDTH = 2; const HANDLE_WIDTH = 2;
const HANDLE_WIDTH_PX = `${HANDLE_WIDTH}px`; const HANDLE_WIDTH_PX = `${HANDLE_WIDTH}px`;
const HANDLE_HITBOX = 20; const HANDLE_HITBOX = 20;
const HANDLE_HITBOX_PX = `${HANDLE_HITBOX}px`; const HANDLE_HITBOX_PX = `${HANDLE_HITBOX}px`;
const HANDLE_INNER_LEFT_PX = `${HANDLE_HITBOX / 2 - HANDLE_WIDTH / 2}px`;
const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`; const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`;
const HANDLE_INNER_LEFT_INITIAL_PX = `${HANDLE_HITBOX / 2 - HANDLE_WIDTH / 2}px`;
type Props = { type Props = {
/**
* The first image to compare
*/
firstImage: ImageDTO; firstImage: ImageDTO;
/**
* The second image to compare
*/
secondImage: ImageDTO; secondImage: ImageDTO;
/**
* The size of the container, used for sizing.
* If not provided, an internal container will be used, but this can cause a flicker effect as the component is first rendered.
*/
containerSize?: UseMeasureRect;
/**
* The ref of the container, used for sizing.
* If not provided, an internal container will be used, but this can cause a flicker effect as the component is first rendered.
*/
containerRef?: React.RefObject<HTMLDivElement>;
}; };
export const ImageSliderComparison = memo(({ firstImage, secondImage }: Props) => { export const ImageSliderComparison = memo(
const secondImageContainerRef = useRef<HTMLDivElement>(null); ({ firstImage, secondImage, containerSize: containerSizeProp, containerRef: containerRefProp }: Props) => {
const handleRef = useRef<HTMLDivElement>(null); const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null); // How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
const [containerSize] = useMeasure(containerRef); const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
// How wide the first image is
const [width, setWidth] = useState(INITIAL_POS);
const handleRef = useRef<HTMLDivElement>(null);
// If the container size is not provided, use an internal ref and measure - can cause flicker on mount tho
const _containerRef = useRef<HTMLDivElement>(null);
const [_containerSize] = useMeasure(_containerRef);
const containerRef = useMemo(() => containerRefProp ?? _containerRef, [containerRefProp, _containerRef]);
const containerSize = useMemo(() => containerSizeProp ?? _containerSize, [containerSizeProp, _containerSize]);
// To keep things smooth, we use RAF to update the handle position & gate it to 60fps
const rafRef = useRef<number | null>(null);
const lastMoveTimeRef = useRef<number>(0);
const updateHandlePos = useCallback((clientX: number) => { const updateHandlePos = useCallback(
if (!secondImageContainerRef.current || !handleRef.current || !containerRef.current) { (clientX: number) => {
return; if (!handleRef.current || !containerRef.current) {
} return;
const { x, width } = containerRef.current.getBoundingClientRect(); }
const rawHandlePos = ((clientX - x) * 100) / width; lastMoveTimeRef.current = performance.now();
const handleWidthPct = (HANDLE_WIDTH * 100) / width; const { x, width } = containerRef.current.getBoundingClientRect();
const newHandlePos = Math.min(100 - handleWidthPct, Math.max(0, rawHandlePos)); const rawHandlePos = ((clientX - x) * 100) / width;
secondImageContainerRef.current.style.width = `${newHandlePos}%`; const handleWidthPct = (HANDLE_WIDTH * 100) / width;
handleRef.current.style.left = `calc(${newHandlePos}% - ${HANDLE_HITBOX / 2}px)`; const newHandlePos = Math.min(100 - handleWidthPct, Math.max(0, rawHandlePos));
}, []); setWidth(`${newHandlePos}%`);
setLeft(`calc(${newHandlePos}% - ${HANDLE_HITBOX / 2}px)`);
},
[containerRef]
);
const onMouseMove = useCallback( const onMouseMove = useCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
updateHandlePos(e.clientX); if (rafRef.current === null && performance.now() > lastMoveTimeRef.current + 1000 / 60) {
}, rafRef.current = window.requestAnimationFrame(() => {
[updateHandlePos] updateHandlePos(e.clientX);
); rafRef.current = null;
});
}
},
[updateHandlePos]
);
const onMouseUp = useCallback(() => { const onMouseUp = useCallback(() => {
window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mousemove', onMouseMove);
}, [onMouseMove]); }, [onMouseMove]);
const onMouseDown = useCallback( const onMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => { (e: React.MouseEvent<HTMLDivElement>) => {
updateHandlePos(e.clientX); // Update the handle position immediately on click
window.addEventListener('mouseup', onMouseUp, { once: true }); updateHandlePos(e.clientX);
window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp, { once: true });
}, window.addEventListener('mousemove', onMouseMove);
[onMouseMove, onMouseUp, updateHandlePos] },
); [onMouseMove, onMouseUp, updateHandlePos]
);
const fittedSize = useMemo<Dimensions>(() => { const fittedSize = useMemo<Dimensions>(() => {
// Fit the first image to the container // Fit the first image to the container
const targetAspectRatio = containerSize.width / containerSize.height; if (containerSize.width === 0 || containerSize.height === 0) {
const imageAspectRatio = firstImage.width / firstImage.height; return { width: firstImage.width, height: firstImage.height };
}
const targetAspectRatio = containerSize.width / containerSize.height;
const imageAspectRatio = firstImage.width / firstImage.height;
if (firstImage.width <= containerSize.width && firstImage.height <= containerSize.height) { if (firstImage.width <= containerSize.width && firstImage.height <= containerSize.height) {
return { width: firstImage.width, height: firstImage.height }; return { width: firstImage.width, height: firstImage.height };
} }
let width: number; let width: number;
let height: number; let height: number;
if (imageAspectRatio > targetAspectRatio) { if (imageAspectRatio > targetAspectRatio) {
// Image is wider than container's aspect ratio // Image is wider than container's aspect ratio
width = containerSize.width; width = containerSize.width;
height = width / imageAspectRatio; height = width / imageAspectRatio;
} else { } else {
// Image is taller than container's aspect ratio // Image is taller than container's aspect ratio
height = containerSize.height; height = containerSize.height;
width = height * imageAspectRatio; width = height * imageAspectRatio;
} }
return { width, height }; return { width, height };
}, [containerSize.height, containerSize.width, firstImage]); }, [containerSize, firstImage.height, firstImage.width]);
return ( useEffect(
<Flex w="full" h="full" maxW="full" maxH="full" position="relative" alignItems="center" justifyContent="center" bg='green'> () => () => {
<Flex if (rafRef.current !== null) {
id="image-comparison-container" cancelAnimationFrame(rafRef.current);
ref={containerRef} }
w="full" },
h="full" []
maxW="full" );
maxH="full"
position="relative" return (
alignItems="center" <Flex w="full" h="full" maxW="full" maxH="full" position="relative" alignItems="center" justifyContent="center">
justifyContent="center" <Flex
> id="image-comparison-container"
<Box ref={_containerRef}
position="relative" w="full"
id="image-comparison-second-image-container" h="full"
w={fittedSize.width}
h={fittedSize.height}
maxW="full" maxW="full"
maxH="full" maxH="full"
backgroundImage={`url(${secondImage.image_url})`} position="relative"
backgroundSize="contain" alignItems="center"
backgroundRepeat="no-repeat" justifyContent="center"
userSelect="none"
overflow="hidden"
> >
<Box <Box
id="image-comparison-first-image-container" position="relative"
ref={secondImageContainerRef} id="image-comparison-second-image-container"
backgroundImage={`url(${firstImage.image_url})`} w={fittedSize.width}
backgroundSize={`${fittedSize.width}px ${fittedSize.height}px`}
backgroundPosition="top left"
backgroundRepeat="no-repeat"
w={INITIAL_POS}
h={fittedSize.height} h={fittedSize.height}
maxW="full" maxW="full"
maxH="full" maxH="full"
position="absolute" userSelect="none"
top={0} overflow="hidden"
left={0} borderRadius="base"
right={0}
bottom={0}
/>
<Flex
id="image-comparison-handle"
ref={handleRef}
position="absolute"
top={0}
bottom={0}
left={HANDLE_LEFT_INITIAL_PX}
w={HANDLE_HITBOX_PX}
tabIndex={-1}
cursor="ew-resize"
filter="drop-shadow(0px 0px 4px rgb(0, 0, 0))"
> >
<Box <Box
w={HANDLE_WIDTH_PX} id="image-comparison-bg"
h="full"
bg="base.50"
shadow="dark-lg"
position="absolute" position="absolute"
top={0} top={0}
left={HANDLE_INNER_LEFT_INITIAL_PX} left={0}
right={0}
bottom={0}
backgroundImage={STAGE_BG_DATAURL}
backgroundRepeat="repeat"
opacity={0.2}
zIndex={-1}
/> />
<Flex <Image
gap={4} src={secondImage.image_url}
fallbackSrc={secondImage.thumbnail_url}
w={secondImage.width}
h={secondImage.height}
maxW="full"
maxH="full"
objectFit="contain"
objectPosition="top left"
/>
<Text
position="absolute" position="absolute"
left="50%" bottom={4}
top="50%" insetInlineEnd={4}
transform="translate(-50%, 0)" textOverflow="clip"
filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgb(0, 0, 0))" whiteSpace="nowrap"
filter={DROP_SHADOW}
color="base.50"
> >
<Icon as={PiCaretLeftBold} /> {t('gallery.secondImage')}
<Icon as={PiCaretRightBold} /> </Text>
<Box
id="image-comparison-first-image-container"
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
w={width}
overflow="hidden"
>
<Image
src={firstImage.image_url}
fallbackSrc={firstImage.thumbnail_url}
w={fittedSize.width}
h={fittedSize.height}
objectFit="cover"
objectPosition="top left"
/>
<Text
position="absolute"
bottom={4}
insetInlineStart={4}
textOverflow="clip"
whiteSpace="nowrap"
filter={DROP_SHADOW}
color="base.50"
>
{t('gallery.firstImage')}
</Text>
</Box>
<Flex
id="image-comparison-handle"
ref={handleRef}
position="absolute"
top={0}
bottom={0}
left={left}
w={HANDLE_HITBOX_PX}
tabIndex={-1}
cursor="ew-resize"
filter={DROP_SHADOW}
>
<Box
id="image-comparison-handle-divider"
w={HANDLE_WIDTH_PX}
h="full"
bg="base.50"
shadow="dark-lg"
position="absolute"
top={0}
left={HANDLE_INNER_LEFT_PX}
/>
<Flex
id="image-comparison-handle-icons"
gap={4}
position="absolute"
left="50%"
top="50%"
transform="translate(-50%, 0)"
filter={DROP_SHADOW}
>
<Icon as={PiCaretLeftBold} />
<Icon as={PiCaretRightBold} />
</Flex>
</Flex> </Flex>
</Flex> <Box
<Box id="image-comparison-interaction-overlay"
id="image-comparison-interaction-overlay" position="absolute"
position="absolute" top={0}
top={0} right={0}
right={0} bottom={0}
bottom={0} left={0}
left={0} onMouseDown={onMouseDown}
onMouseDown={onMouseDown} userSelect="none"
userSelect="none" />
/> </Box>
</Box> </Flex>
</Flex> </Flex>
</Flex> );
); }
}); );
ImageSliderComparison.displayName = 'ImageSliderComparison'; ImageSliderComparison.displayName = 'ImageSliderComparison';

View File

@ -1,4 +1,5 @@
import { Flex } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library';
import { useMeasure } from '@reactuses/core';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
import { ImageSliderComparison } from 'features/gallery/components/ImageViewer/ImageSliderComparison3'; import { ImageSliderComparison } from 'features/gallery/components/ImageViewer/ImageSliderComparison3';
@ -7,7 +8,7 @@ import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/To
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import type { InvokeTabName } from 'features/ui/store/tabMap'; import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react'; import { memo, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import CurrentImageButtons from './CurrentImageButtons'; import CurrentImageButtons from './CurrentImageButtons';
@ -19,6 +20,8 @@ export const ImageViewer = memo(() => {
const { isOpen, onToggle, onClose } = useImageViewer(); const { isOpen, onToggle, onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]); const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
const containerRef = useRef<HTMLDivElement>(null);
const [containerSize] = useMeasure(containerRef);
const shouldShowViewer = useMemo(() => { const shouldShowViewer = useMemo(() => {
if (!isViewerEnabled) { if (!isViewerEnabled) {
return false; return false;
@ -40,6 +43,7 @@ export const ImageViewer = memo(() => {
return ( return (
<Flex <Flex
ref={containerRef}
layerStyle="first" layerStyle="first"
borderRadius="base" borderRadius="base"
position="absolute" position="absolute"
@ -71,7 +75,14 @@ export const ImageViewer = memo(() => {
</Flex> </Flex>
</Flex> </Flex>
{firstImage && !secondImage && <CurrentImagePreview />} {firstImage && !secondImage && <CurrentImagePreview />}
{firstImage && secondImage && <ImageSliderComparison firstImage={firstImage} secondImage={secondImage} />} {firstImage && secondImage && (
<ImageSliderComparison
containerSize={containerSize}
containerRef={containerRef}
firstImage={firstImage}
secondImage={secondImage}
/>
)}
</Flex> </Flex>
); );
}); });