Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion invokeai/frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
}
},
"dependencies": {
"@chakra-ui/react-use-size": "^2.1.0",
"@dagrejs/dagre": "^1.1.1",
"@dagrejs/graphlib": "^2.2.1",
"@dnd-kit/core": "^6.1.0",
Expand Down
3 changes: 0 additions & 3 deletions invokeai/frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@
"denoisingStrength": "Denoising Strength",
"downloadImage": "Download Image",
"general": "General",
"globalSettings": "Global Settings",
"height": "Height",
"imageFit": "Fit Initial Image To Output Size",
"images": "Images",
Expand Down Expand Up @@ -1518,7 +1519,7 @@
"moveForward": "Move Forward",
"moveBackward": "Move Backward",
"brushSize": "Brush Size",
"regionalPrompts": "Regional Prompts BETA",
"regionalControl": "Regional Control (ALPHA)",
"enableRegionalPrompts": "Enable $t(regionalPrompts.regionalPrompts)",
"globalMaskOpacity": "Global Mask Opacity",
"autoNegative": "Auto Negative",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import { clamp } from 'lodash-es';
import type { MutableRefObject } from 'react';
import { useCallback } from 'react';

export const calculateNewBrushSize = (brushSize: number, delta: number) => {
// This equation was derived by fitting a curve to the desired brush sizes and deltas
// see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565
const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize);
// This needs to be clamped to prevent the delta from getting too large
const finalDelta = clamp(targetDelta, -20, 20);
// The new brush size is also clamped to prevent it from getting too large or small
const newBrushSize = clamp(brushSize + finalDelta, 1, 500);

return newBrushSize;
};

const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const dispatch = useAppDispatch();
const stageScale = useAppSelector((s) => s.canvas.stageScale);
Expand All @@ -36,15 +48,7 @@ const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
}

if ($ctrl.get() || $meta.get()) {
// This equation was derived by fitting a curve to the desired brush sizes and deltas
// see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565
const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize);
// This needs to be clamped to prevent the delta from getting too large
const finalDelta = clamp(targetDelta, -20, 20);
// The new brush size is also clamped to prevent it from getting too large or small
const newBrushSize = clamp(brushSize + finalDelta, 1, 500);

dispatch(setBrushSize(newBrushSize));
dispatch(setBrushSize(calculateNewBrushSize(brushSize, delta)));
} else {
const cursorPos = stageRef.current.getPointerPosition();
let delta = e.evt.deltaY;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,13 @@
import { useSize } from '@chakra-ui/react-use-size';
import { Flex, Icon } from '@invoke-ai/ui-library';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { AnimatePresence, motion } from 'framer-motion';
import { useMemo, useRef } from 'react';
import { PiFrameCorners } from 'react-icons/pi';

import {
BOX_SIZE_CSS_CALC,
ICON_CONTAINER_STYLES,
ICON_HIGH_CUTOFF,
ICON_LOW_CUTOFF,
MOTION_ICON_ANIMATE,
MOTION_ICON_EXIT,
MOTION_ICON_INITIAL,
} from './constants';

export const AspectRatioPreview = () => {
const ctx = useImageSizeContext();
const containerRef = useRef<HTMLDivElement>(null);
const containerSize = useSize(containerRef);

const shouldShowIcon = useMemo(
() => ctx.aspectRatioState.value < ICON_HIGH_CUTOFF && ctx.aspectRatioState.value > ICON_LOW_CUTOFF,
[ctx.aspectRatioState.value]
);

const { width, height } = useMemo(() => {
if (!containerSize) {
return { width: 0, height: 0 };
}

let width = ctx.width;
let height = ctx.height;

if (ctx.width > ctx.height) {
width = containerSize.width;
height = width / ctx.aspectRatioState.value;
} else {
height = containerSize.height;
width = height * ctx.aspectRatioState.value;
}

return { width, height };
}, [containerSize, ctx.width, ctx.height, ctx.aspectRatioState.value]);
import { Flex } from '@invoke-ai/ui-library';
import { StageComponent } from 'features/regionalPrompts/components/StageComponent';
import { memo } from 'react';

export const AspectRatioPreview = memo(() => {
return (
<Flex w="full" h="full" alignItems="center" justifyContent="center" ref={containerRef}>
<Flex
bg="blackAlpha.400"
borderRadius="base"
width={`${width}px`}
height={`${height}px`}
alignItems="center"
justifyContent="center"
>
<AnimatePresence>
{shouldShowIcon && (
<Flex
as={motion.div}
initial={MOTION_ICON_INITIAL}
animate={MOTION_ICON_ANIMATE}
exit={MOTION_ICON_EXIT}
style={ICON_CONTAINER_STYLES}
>
<Icon as={PiFrameCorners} color="base.700" boxSize={BOX_SIZE_CSS_CALC} />
</Flex>
)}
</AnimatePresence>
</Flex>
<Flex w="full" h="full" alignItems="center" justifyContent="center" position="relative">
<StageComponent asPreview />
</Flex>
);
};
});

AspectRatioPreview.displayName = 'AspectRatioPreview';
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,6 @@ import type { ComboboxOption } from '@invoke-ai/ui-library';

import type { AspectRatioID, AspectRatioState } from './types';

// When the aspect ratio is between these two values, we show the icon (experimentally determined)
export const ICON_LOW_CUTOFF = 0.23;
export const ICON_HIGH_CUTOFF = 1 / ICON_LOW_CUTOFF;
const ICON_SIZE_PX = 64;
const ICON_PADDING_PX = 16;
export const BOX_SIZE_CSS_CALC = `min(${ICON_SIZE_PX}px, calc(100% - ${ICON_PADDING_PX}px))`;
export const MOTION_ICON_INITIAL = {
opacity: 0,
};
export const MOTION_ICON_ANIMATE = {
opacity: 1,
transition: { duration: 0.1 },
};
export const MOTION_ICON_EXIT = {
opacity: 0,
transition: { duration: 0.1 },
};
export const ICON_CONTAINER_STYLES = {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
};

export const ASPECT_RATIO_OPTIONS: ComboboxOption[] = [
{ label: 'Free' as const, value: 'Free' },
{ label: '16:9' as const, value: '16:9' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { layerAdded } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';

export const AddLayerButton = memo(() => {
const { t } = useTranslation();
Expand All @@ -11,7 +12,11 @@ export const AddLayerButton = memo(() => {
dispatch(layerAdded('vector_mask_layer'));
}, [dispatch]);

return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>;
return (
<Button onClick={onClick} leftIcon={<PiPlusBold />} variant="ghost">
{t('regionalPrompts.addLayer')}
</Button>
);
});

AddLayerButton.displayName = 'AddLayerButton';
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { allLayersDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';

export const DeleteAllLayersButton = memo(() => {
const { t } = useTranslation();
Expand All @@ -11,7 +12,11 @@ export const DeleteAllLayersButton = memo(() => {
dispatch(allLayersDeleted());
}, [dispatch]);

return <Button onClick={onClick}>{t('regionalPrompts.deleteAll')}</Button>;
return (
<Button onClick={onClick} leftIcon={<PiTrashSimpleBold />} variant="ghost" colorScheme="error">
{t('regionalPrompts.deleteAll')}
</Button>
);
});

DeleteAllLayersButton.displayName = 'DeleteAllLayersButton';
Original file line number Diff line number Diff line change
@@ -1,50 +1,21 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
import { GlobalMaskLayerOpacity } from 'features/regionalPrompts/components/GlobalMaskLayerOpacity';
import { RPLayerListItem } from 'features/regionalPrompts/components/RPLayerListItem';
import { Flex } from '@invoke-ai/ui-library';
import { RegionalPromptsToolbar } from 'features/regionalPrompts/components/RegionalPromptsToolbar';
import { StageComponent } from 'features/regionalPrompts/components/StageComponent';
import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/regionalPrompts/components/UndoRedoButtonGroup';
import { isVectorMaskLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo } from 'react';

const selectRPLayerIdsReversed = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
regionalPrompts.present.layers
.filter(isVectorMaskLayer)
.map((l) => l.id)
.reverse()
);

export const RegionalPromptsEditor = memo(() => {
const rpLayerIdsReversed = useAppSelector(selectRPLayerIdsReversed);
return (
<Flex gap={4} w="full" h="full">
<Flex flexDir="column" gap={4} minW={430}>
<Flex gap={3} w="full" justifyContent="space-between">
<AddLayerButton />
<DeleteAllLayersButton />
<Spacer />
<UndoRedoButtonGroup />
<ToolChooser />
</Flex>
<Flex justifyContent="space-between">
<BrushSize />
<GlobalMaskLayerOpacity />
</Flex>
<ScrollableContent>
<Flex flexDir="column" gap={2}>
{rpLayerIdsReversed.map((id) => (
<RPLayerListItem key={id} layerId={id} />
))}
</Flex>
</ScrollableContent>
</Flex>
<Flex
position="relative"
flexDirection="column"
height="100%"
width="100%"
rowGap={4}
alignItems="center"
justifyContent="center"
>
<RegionalPromptsToolbar />
<StageComponent />
</Flex>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable i18next/no-literal-string */
import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
import { RPLayerListItem } from 'features/regionalPrompts/components/RPLayerListItem';
import { isVectorMaskLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo } from 'react';

const selectRPLayerIdsReversed = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
regionalPrompts.present.layers
.filter(isVectorMaskLayer)
.map((l) => l.id)
.reverse()
);

export const RegionalPromptsPanelContent = memo(() => {
const rpLayerIdsReversed = useAppSelector(selectRPLayerIdsReversed);
return (
<Flex flexDir="column" gap={4} w="full" h="full">
<Flex justifyContent="space-around">
<AddLayerButton />
<DeleteAllLayersButton />
</Flex>
<ScrollableContent>
<Flex flexDir="column" gap={4}>
{rpLayerIdsReversed.map((id) => (
<RPLayerListItem key={id} layerId={id} />
))}
</Flex>
</ScrollableContent>
</Flex>
);
});

RegionalPromptsPanelContent.displayName = 'RegionalPromptsPanelContent';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable i18next/no-literal-string */
import { Flex } from '@invoke-ai/ui-library';
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
import { GlobalMaskLayerOpacity } from 'features/regionalPrompts/components/GlobalMaskLayerOpacity';
import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/regionalPrompts/components/UndoRedoButtonGroup';
import { memo } from 'react';

export const RegionalPromptsToolbar = memo(() => {
return (
<Flex gap={4}>
<BrushSize />
<GlobalMaskLayerOpacity />
<UndoRedoButtonGroup />
<ToolChooser />
</Flex>
);
});

RegionalPromptsToolbar.displayName = 'RegionalPromptsToolbar';
Loading