mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): revised image comparison slider
Should work for any components and image now.
This commit is contained in:
parent
72bbcb2d94
commit
7a4bbd092e
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user