mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip slider implementations
This commit is contained in:
parent
cfb12615e1
commit
c2eef93476
@ -59,8 +59,10 @@
|
|||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource-variable/inter": "^5.0.18",
|
"@fontsource-variable/inter": "^5.0.18",
|
||||||
|
"@img-comparison-slider/react": "^8.0.2",
|
||||||
"@invoke-ai/ui-library": "^0.0.25",
|
"@invoke-ai/ui-library": "^0.0.25",
|
||||||
"@nanostores/react": "^0.7.2",
|
"@nanostores/react": "^0.7.2",
|
||||||
|
"@reactuses/core": "^5.0.14",
|
||||||
"@reduxjs/toolkit": "2.2.3",
|
"@reduxjs/toolkit": "2.2.3",
|
||||||
"@roarr/browser-log-writer": "^1.3.0",
|
"@roarr/browser-log-writer": "^1.3.0",
|
||||||
"chakra-react-select": "^4.7.6",
|
"chakra-react-select": "^4.7.6",
|
||||||
|
@ -29,12 +29,18 @@ dependencies:
|
|||||||
'@fontsource-variable/inter':
|
'@fontsource-variable/inter':
|
||||||
specifier: ^5.0.18
|
specifier: ^5.0.18
|
||||||
version: 5.0.18
|
version: 5.0.18
|
||||||
|
'@img-comparison-slider/react':
|
||||||
|
specifier: ^8.0.2
|
||||||
|
version: 8.0.2
|
||||||
'@invoke-ai/ui-library':
|
'@invoke-ai/ui-library':
|
||||||
specifier: ^0.0.25
|
specifier: ^0.0.25
|
||||||
version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1)
|
version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1)
|
||||||
'@nanostores/react':
|
'@nanostores/react':
|
||||||
specifier: ^0.7.2
|
specifier: ^0.7.2
|
||||||
version: 0.7.2(nanostores@0.10.3)(react@18.3.1)
|
version: 0.7.2(nanostores@0.10.3)(react@18.3.1)
|
||||||
|
'@reactuses/core':
|
||||||
|
specifier: ^5.0.14
|
||||||
|
version: 5.0.14(react@18.3.1)
|
||||||
'@reduxjs/toolkit':
|
'@reduxjs/toolkit':
|
||||||
specifier: 2.2.3
|
specifier: 2.2.3
|
||||||
version: 2.2.3(react-redux@9.1.2)(react@18.3.1)
|
version: 2.2.3(react-redux@9.1.2)(react@18.3.1)
|
||||||
@ -3544,6 +3550,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@img-comparison-slider/react@8.0.2:
|
||||||
|
resolution: {integrity: sha512-Him0yhbXpMXdnV6R3XE3LiXcMRhSXFMsbk6I7ct5HxO2YpK/BAGz3ub+7+akJRnK2XI7c3vQqvoIE507N1K4SA==}
|
||||||
|
dependencies:
|
||||||
|
img-comparison-slider: 8.0.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@internationalized/date@3.5.3:
|
/@internationalized/date@3.5.3:
|
||||||
resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==}
|
resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3982,6 +3994,18 @@ packages:
|
|||||||
- immer
|
- immer
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@reactuses/core@5.0.14(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-lg640pRPOPT0HZ8XQAA1VRZ47fLIvSd2JrUTtKpzm4t3MtZvza+w2RHBGgPsdmtiLV3GsJJC9x5ge7XOQmiJ/Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
js-cookie: 3.0.5
|
||||||
|
lodash-es: 4.17.21
|
||||||
|
react: 18.3.1
|
||||||
|
screenfull: 5.2.0
|
||||||
|
use-sync-external-store: 1.2.2(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@reduxjs/toolkit@2.2.3(react-redux@9.1.2)(react@18.3.1):
|
/@reduxjs/toolkit@2.2.3(react-redux@9.1.2)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==}
|
resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -9223,6 +9247,10 @@ packages:
|
|||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/img-comparison-slider@8.0.6:
|
||||||
|
resolution: {integrity: sha512-ej4de7mWyjcXZvDgHq8K2a/dG8Vv+qYTdUjZa3cVILf316rLtDrHyGbh9fPvixmAFgbs30zTLfmaRDa7abjtzw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/immer@10.1.1:
|
/immer@10.1.1:
|
||||||
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -9668,6 +9696,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
|
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/js-cookie@3.0.5:
|
||||||
|
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/js-tokens@4.0.0:
|
/js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import { ImgComparisonSlider } from '@img-comparison-slider/react';
|
||||||
|
import { Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import { atom } from 'nanostores';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
|
||||||
|
import { useMeasure } from 'react-use';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
const $compareWith = atom<ImageDTO | null>(null);
|
||||||
|
|
||||||
|
export const ImageSliderComparison = memo(() => {
|
||||||
|
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
|
||||||
|
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||||
|
const imageToCompare = useAppSelector((s) => s.gallery.selection[0]);
|
||||||
|
// const imageToCompare = useStore($imageToCompare);
|
||||||
|
const { imageA, imageB } = useAppSelector((s) => {
|
||||||
|
const images = s.gallery.selection.slice(-2);
|
||||||
|
return { imageA: images[0] ?? null, imageB: images[1] ?? null };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!imageA || !imageB) {
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" maxW="full" maxH="full" alignItems="center" justifyContent="center" position="relative">
|
||||||
|
<Text>Select images to compare</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
ref={containerRef}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Flex top={0} right={0} bottom={0} left={0} position="absolute" alignItems="center" justifyContent="center">
|
||||||
|
<ImgComparisonSlider>
|
||||||
|
<Image
|
||||||
|
slot="first"
|
||||||
|
src={imageA.image_url}
|
||||||
|
alt={imageA.image_name}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
maxW={containerDims.width}
|
||||||
|
maxH={containerDims.height}
|
||||||
|
backdropFilter="blur(20%)"
|
||||||
|
objectPosition="top left"
|
||||||
|
objectFit="contain"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
slot="second"
|
||||||
|
src={imageB.image_url}
|
||||||
|
alt={imageB.image_name}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
maxW={containerDims.width}
|
||||||
|
maxH={containerDims.height}
|
||||||
|
objectFit="contain"
|
||||||
|
objectPosition="top left"
|
||||||
|
/>
|
||||||
|
<Flex slot="handle" gap={4}>
|
||||||
|
<Icon as={PiCaretLeftBold} />
|
||||||
|
<Icon as={PiCaretRightBold} />
|
||||||
|
</Flex>
|
||||||
|
</ImgComparisonSlider>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageSliderComparison.displayName = 'ImageSliderComparison';
|
@ -0,0 +1,148 @@
|
|||||||
|
import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||||
|
import { memo, useCallback, useRef } from 'react';
|
||||||
|
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const INITIAL_POS = '50%';
|
||||||
|
const HANDLE_WIDTH = 2;
|
||||||
|
const HANDLE_WIDTH_PX = `${HANDLE_WIDTH}px`;
|
||||||
|
const HANDLE_HITBOX = 20;
|
||||||
|
const HANDLE_HITBOX_PX = `${HANDLE_HITBOX}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`;
|
||||||
|
|
||||||
|
export const ImageSliderComparison = memo(() => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const imageAContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handleRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const updateHandlePos = useCallback((clientX: number) => {
|
||||||
|
if (!containerRef.current || !imageAContainerRef.current || !handleRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { x, width } = containerRef.current.getBoundingClientRect();
|
||||||
|
const rawHandlePos = ((clientX - x) * 100) / width;
|
||||||
|
const handleWidthPct = (HANDLE_WIDTH * 100) / width;
|
||||||
|
const newHandlePos = Math.min(100 - handleWidthPct, Math.max(0, rawHandlePos));
|
||||||
|
imageAContainerRef.current.style.width = `${newHandlePos}%`;
|
||||||
|
handleRef.current.style.left = `calc(${newHandlePos}% - ${HANDLE_HITBOX / 2}px)`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseMove = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
updateHandlePos(e.clientX);
|
||||||
|
},
|
||||||
|
[updateHandlePos]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMouseUp = useCallback(() => {
|
||||||
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
}, [onMouseMove]);
|
||||||
|
|
||||||
|
const onMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
updateHandlePos(e.clientX);
|
||||||
|
window.addEventListener('mouseup', onMouseUp, { once: true });
|
||||||
|
window.addEventListener('mousemove', onMouseMove);
|
||||||
|
},
|
||||||
|
[onMouseMove, onMouseUp, updateHandlePos]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { imageA, imageB } = useAppSelector((s) => {
|
||||||
|
const images = s.gallery.selection.slice(-2);
|
||||||
|
return { imageA: images[0] ?? null, imageB: images[1] ?? null };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (imageA && !imageB) {
|
||||||
|
return <CurrentImagePreview />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageA || !imageB) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
position="relative"
|
||||||
|
border="1px solid cyan"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
ref={containerRef}
|
||||||
|
w={imageA.width}
|
||||||
|
h={imageA.height}
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
position="relative"
|
||||||
|
userSelect="none"
|
||||||
|
border="1px solid magenta"
|
||||||
|
objectFit="contain"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<Image src={imageB.image_url} objectFit="contain" objectPosition="top left" />
|
||||||
|
<Flex
|
||||||
|
ref={imageAContainerRef}
|
||||||
|
w={INITIAL_POS}
|
||||||
|
h={imageA.height}
|
||||||
|
position="absolute"
|
||||||
|
overflow="hidden"
|
||||||
|
objectFit="contain"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={imageA.image_url}
|
||||||
|
width={`${imageA.width}px`}
|
||||||
|
height={`${imageA.height}px`}
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
objectFit="none"
|
||||||
|
objectPosition="left"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<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
|
||||||
|
w={HANDLE_WIDTH_PX}
|
||||||
|
h="full"
|
||||||
|
bg="base.50"
|
||||||
|
shadow="dark-lg"
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={HANDLE_INNER_LEFT_INITIAL_PX}
|
||||||
|
/>
|
||||||
|
<Flex gap={4} position="absolute" left="50%" top="50%" transform="translate(-50%, 0)">
|
||||||
|
<Icon as={PiCaretLeftBold} />
|
||||||
|
<Icon as={PiCaretRightBold} />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
id="image-comparison-overlay"
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
userSelect="none"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageSliderComparison.displayName = 'ImageSliderComparison';
|
@ -0,0 +1,162 @@
|
|||||||
|
import { Box, Flex, Icon } from '@invoke-ai/ui-library';
|
||||||
|
import { useMeasure } from '@reactuses/core';
|
||||||
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
|
||||||
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
const INITIAL_POS = '50%';
|
||||||
|
const HANDLE_WIDTH = 2;
|
||||||
|
const HANDLE_WIDTH_PX = `${HANDLE_WIDTH}px`;
|
||||||
|
const HANDLE_HITBOX = 20;
|
||||||
|
const HANDLE_HITBOX_PX = `${HANDLE_HITBOX}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 = {
|
||||||
|
firstImage: ImageDTO;
|
||||||
|
secondImage: ImageDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageSliderComparison = memo(({ firstImage, secondImage }: Props) => {
|
||||||
|
const secondImageContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handleRef = useRef<HTMLDivElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [containerSize] = useMeasure(containerRef);
|
||||||
|
|
||||||
|
const updateHandlePos = useCallback((clientX: number) => {
|
||||||
|
if (!secondImageContainerRef.current || !handleRef.current || !containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { x, width } = containerRef.current.getBoundingClientRect();
|
||||||
|
const rawHandlePos = ((clientX - x) * 100) / width;
|
||||||
|
const handleWidthPct = (HANDLE_WIDTH * 100) / width;
|
||||||
|
const newHandlePos = Math.min(100 - handleWidthPct, Math.max(0, rawHandlePos));
|
||||||
|
secondImageContainerRef.current.style.width = `${newHandlePos}%`;
|
||||||
|
handleRef.current.style.left = `calc(${newHandlePos}% - ${HANDLE_HITBOX / 2}px)`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseMove = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
updateHandlePos(e.clientX);
|
||||||
|
},
|
||||||
|
[updateHandlePos]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMouseUp = useCallback(() => {
|
||||||
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
}, [onMouseMove]);
|
||||||
|
|
||||||
|
const onMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
updateHandlePos(e.clientX);
|
||||||
|
window.addEventListener('mouseup', onMouseUp, { once: true });
|
||||||
|
window.addEventListener('mousemove', onMouseMove);
|
||||||
|
},
|
||||||
|
[onMouseMove, onMouseUp, updateHandlePos]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fittedSize = useMemo(() => {
|
||||||
|
let width = containerSize.width;
|
||||||
|
let height = containerSize.height;
|
||||||
|
const aspectRatio = firstImage.width / firstImage.height;
|
||||||
|
if (firstImage.width > firstImage.height) {
|
||||||
|
width = firstImage.width;
|
||||||
|
height = width / aspectRatio;
|
||||||
|
} else {
|
||||||
|
height = firstImage.height;
|
||||||
|
width = height * aspectRatio;
|
||||||
|
}
|
||||||
|
return { width, height };
|
||||||
|
}, [containerSize.height, containerSize.width, firstImage.height, firstImage.width]);
|
||||||
|
|
||||||
|
console.log({ containerSize, fittedSize });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" maxW="full" maxH="full" position="relative" alignItems="center" justifyContent="center">
|
||||||
|
<Flex
|
||||||
|
id="image-comparison-container"
|
||||||
|
ref={containerRef}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
position="relative"
|
||||||
|
border="1px solid cyan"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
id="image-comparison-first-image-container"
|
||||||
|
w={fittedSize.width}
|
||||||
|
h={fittedSize.height}
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
backgroundImage={`url(${secondImage.image_url})`}
|
||||||
|
backgroundSize="contain"
|
||||||
|
backgroundRepeat="no-repeat"
|
||||||
|
userSelect="none"
|
||||||
|
border="1px solid green"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
id="image-comparison-second-image-container"
|
||||||
|
ref={secondImageContainerRef}
|
||||||
|
backgroundImage={`url(${firstImage.image_url})`}
|
||||||
|
backgroundSize="auto"
|
||||||
|
backgroundPosition="top left"
|
||||||
|
backgroundRepeat="no-repeat"
|
||||||
|
w={INITIAL_POS}
|
||||||
|
h="full"
|
||||||
|
maxW="full"
|
||||||
|
maxH="full"
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
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
|
||||||
|
w={HANDLE_WIDTH_PX}
|
||||||
|
h="full"
|
||||||
|
bg="base.50"
|
||||||
|
shadow="dark-lg"
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={HANDLE_INNER_LEFT_INITIAL_PX}
|
||||||
|
/>
|
||||||
|
<Flex gap={4} position="absolute" left="50%" top="50%" transform="translate(-50%, 0)">
|
||||||
|
<Icon as={PiCaretLeftBold} />
|
||||||
|
<Icon as={PiCaretRightBold} />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Box
|
||||||
|
id="image-comparison-interaction-overlay"
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
userSelect="none"
|
||||||
|
bg="rgba(255,0,0,0.3)"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageSliderComparison.displayName = 'ImageSliderComparison';
|
@ -1,5 +1,7 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||||
|
import { ImageSliderComparison } from 'features/gallery/components/ImageViewer/ImageSliderComparison3';
|
||||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
@ -9,7 +11,6 @@ import { memo, useMemo } from 'react';
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
|
||||||
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||||
|
|
||||||
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
||||||
@ -28,6 +29,11 @@ export const ImageViewer = memo(() => {
|
|||||||
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
|
||||||
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
|
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
|
||||||
|
|
||||||
|
const { firstImage, secondImage } = useAppSelector((s) => {
|
||||||
|
const images = s.gallery.selection.slice(-2);
|
||||||
|
return { firstImage: images[0] ?? null, secondImage: images[0] ? images[1] ?? null : null };
|
||||||
|
});
|
||||||
|
|
||||||
if (!shouldShowViewer) {
|
if (!shouldShowViewer) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -64,7 +70,8 @@ export const ImageViewer = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<CurrentImagePreview />
|
{firstImage && !secondImage && <CurrentImagePreview />}
|
||||||
|
{firstImage && secondImage && <ImageSliderComparison firstImage={firstImage} secondImage={secondImage} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user