diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison.tsx index 2cb5034b30..3965cd5fd0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison.tsx @@ -1,74 +1,260 @@ -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 { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library'; +import type { UseMeasureRect } from '@reactuses/core'; +import type { Dimensions } from 'features/canvas/store/canvasTypes'; +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 { useMeasure } from 'react-use'; import type { ImageDTO } from 'services/api/types'; -const $compareWith = atom(null); +const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0))'; +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_INNER_LEFT_PX = `${HANDLE_HITBOX / 2 - HANDLE_WIDTH / 2}px`; +const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`; -export const ImageSliderComparison = memo(() => { - const [containerRef, containerDims] = useMeasure(); - 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 }; - }); +type Props = { + /** + * The first image to compare + */ + firstImage: ImageDTO; + /** + * The second image to compare + */ + secondImage: ImageDTO; + /** + * The size of the container, required to fit the component correctly and manage aspect ratios. + */ + containerSize: UseMeasureRect; +}; - if (!imageA || !imageB) { - return ( - - Select images to compare - - ); - } +export const ImageSliderComparison = memo(({ firstImage, secondImage, containerSize }: Props) => { + const { t } = useTranslation(); + // How far the handle is from the left - this will be a CSS calculation that takes into account the handle width + const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX); + // How wide the first image is + const [width, setWidth] = useState(INITIAL_POS); + const handleRef = useRef(null); + // If the container size is not provided, use an internal ref and measure - can cause flicker on mount tho + const containerRef = useRef(null); + // To keep things smooth, we use RAF to update the handle position & gate it to 60fps + const rafRef = useRef(null); + const lastMoveTimeRef = useRef(0); + + const updateHandlePos = useCallback( + (clientX: number) => { + if (!handleRef.current || !containerRef.current) { + return; + } + lastMoveTimeRef.current = performance.now(); + 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)); + setWidth(`${newHandlePos}%`); + setLeft(`calc(${newHandlePos}% - ${HANDLE_HITBOX / 2}px)`); + }, + [containerRef] + ); + + const onMouseMove = useCallback( + (e: MouseEvent) => { + if (rafRef.current === null && performance.now() > lastMoveTimeRef.current + 1000 / 60) { + rafRef.current = window.requestAnimationFrame(() => { + updateHandlePos(e.clientX); + rafRef.current = null; + }); + } + }, + [updateHandlePos] + ); + + const onMouseUp = useCallback(() => { + window.removeEventListener('mousemove', onMouseMove); + }, [onMouseMove]); + + const onMouseDown = useCallback( + (e: React.MouseEvent) => { + // Update the handle position immediately on click + updateHandlePos(e.clientX); + window.addEventListener('mouseup', onMouseUp, { once: true }); + window.addEventListener('mousemove', onMouseMove); + }, + [onMouseMove, onMouseUp, updateHandlePos] + ); + + const fittedSize = useMemo(() => { + // Fit the first image to the container + if (containerSize.width === 0 || containerSize.height === 0) { + 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) { + return { width: firstImage.width, height: firstImage.height }; + } + + let width: number; + let height: number; + + if (imageAspectRatio > targetAspectRatio) { + // Image is wider than container's aspect ratio + width = containerSize.width; + height = width / imageAspectRatio; + } else { + // Image is taller than container's aspect ratio + height = containerSize.height; + width = height * imageAspectRatio; + } + return { width, height }; + }, [containerSize, firstImage.height, firstImage.width]); + + useEffect( + () => () => { + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + } + }, + [] + ); return ( - - - - {imageA.image_name} + + + {imageB.image_name} - - - + + {t('gallery.secondImage')} + + + + + {t('gallery.firstImage')} + + + + + + + + - + + ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison2.tsx deleted file mode 100644 index a6f441c7a4..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison2.tsx +++ /dev/null @@ -1,148 +0,0 @@ -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(null); - const imageAContainerRef = useRef(null); - const handleRef = useRef(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) => { - 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 ; - } - - if (!imageA || !imageB) { - return null; - } - - return ( - - - - - - - - - - - - - - - - - ); -}); - -ImageSliderComparison.displayName = 'ImageSliderComparison'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison3.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison3.tsx deleted file mode 100644 index fbb3cef3a7..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageSliderComparison3.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import { Box, Flex, Icon, Image, Text } from '@invoke-ai/ui-library'; -import { useMeasure, type UseMeasureRect } from '@reactuses/core'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; -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 type { ImageDTO } from 'services/api/types'; - -const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0))'; -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_INNER_LEFT_PX = `${HANDLE_HITBOX / 2 - HANDLE_WIDTH / 2}px`; -const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`; - -type Props = { - /** - * The first image to compare - */ - firstImage: ImageDTO; - /** - * The second image to compare - */ - 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; -}; - -export const ImageSliderComparison = memo( - ({ firstImage, secondImage, containerSize: containerSizeProp, containerRef: containerRefProp }: Props) => { - const { t } = useTranslation(); - // How far the handle is from the left - this will be a CSS calculation that takes into account the handle width - const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX); - // How wide the first image is - const [width, setWidth] = useState(INITIAL_POS); - const handleRef = useRef(null); - // If the container size is not provided, use an internal ref and measure - can cause flicker on mount tho - const _containerRef = useRef(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(null); - const lastMoveTimeRef = useRef(0); - - const updateHandlePos = useCallback( - (clientX: number) => { - if (!handleRef.current || !containerRef.current) { - return; - } - lastMoveTimeRef.current = performance.now(); - 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)); - setWidth(`${newHandlePos}%`); - setLeft(`calc(${newHandlePos}% - ${HANDLE_HITBOX / 2}px)`); - }, - [containerRef] - ); - - const onMouseMove = useCallback( - (e: MouseEvent) => { - if (rafRef.current === null && performance.now() > lastMoveTimeRef.current + 1000 / 60) { - rafRef.current = window.requestAnimationFrame(() => { - updateHandlePos(e.clientX); - rafRef.current = null; - }); - } - }, - [updateHandlePos] - ); - - const onMouseUp = useCallback(() => { - window.removeEventListener('mousemove', onMouseMove); - }, [onMouseMove]); - - const onMouseDown = useCallback( - (e: React.MouseEvent) => { - // Update the handle position immediately on click - updateHandlePos(e.clientX); - window.addEventListener('mouseup', onMouseUp, { once: true }); - window.addEventListener('mousemove', onMouseMove); - }, - [onMouseMove, onMouseUp, updateHandlePos] - ); - - const fittedSize = useMemo(() => { - // Fit the first image to the container - if (containerSize.width === 0 || containerSize.height === 0) { - 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) { - return { width: firstImage.width, height: firstImage.height }; - } - - let width: number; - let height: number; - - if (imageAspectRatio > targetAspectRatio) { - // Image is wider than container's aspect ratio - width = containerSize.width; - height = width / imageAspectRatio; - } else { - // Image is taller than container's aspect ratio - height = containerSize.height; - width = height * imageAspectRatio; - } - return { width, height }; - }, [containerSize, firstImage.height, firstImage.width]); - - useEffect( - () => () => { - if (rafRef.current !== null) { - cancelAnimationFrame(rafRef.current); - } - }, - [] - ); - - return ( - - - - - - - {t('gallery.secondImage')} - - - - - {t('gallery.firstImage')} - - - - - - - - - - - - - - ); - } -); - -ImageSliderComparison.displayName = 'ImageSliderComparison'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 5669ec5550..6697f9fd1d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -1,8 +1,8 @@ -import { Flex } from '@invoke-ai/ui-library'; +import { Box, Flex } from '@invoke-ai/ui-library'; import { useMeasure } from '@reactuses/core'; import { useAppSelector } from 'app/store/storeHooks'; import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; -import { ImageSliderComparison } from 'features/gallery/components/ImageViewer/ImageSliderComparison3'; +import { ImageSliderComparison } from 'features/gallery/components/ImageViewer/ImageSliderComparison'; import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; @@ -43,7 +43,6 @@ export const ImageViewer = memo(() => { return ( { - {firstImage && !secondImage && } - {firstImage && secondImage && ( - - )} + + {firstImage && !secondImage && } + {firstImage && secondImage && ( + + )} + ); });