diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b403fde2c6..6734f1dcd1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -528,7 +528,8 @@ "hidePreview": "Hide Preview", "showPreview": "Show Preview", "controlNetControlMode": "Control Mode", - "clipSkip": "Clip Skip" + "clipSkip": "Clip Skip", + "aspectRatio": "Ratio" }, "settings": { "models": "Models", @@ -671,6 +672,7 @@ }, "ui": { "showProgressImages": "Show Progress Images", - "hideProgressImages": "Hide Progress Images" + "hideProgressImages": "Hide Progress Images", + "swapSizes": "Swap Sizes" } } diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx new file mode 100644 index 0000000000..0a568fb84d --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx @@ -0,0 +1,37 @@ +import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIButton from 'common/components/IAIButton'; +import { setAspectRatio } from 'features/ui/store/uiSlice'; + +const aspectRatios = [ + { name: 'Free', value: null }, + { name: 'Portrait', value: 0.67 / 1 }, + { name: 'Wide', value: 16 / 9 }, + { name: 'Square', value: 1 / 1 }, +]; + +export default function ParamAspectRatio() { + const aspectRatio = useAppSelector( + (state: RootState) => state.ui.aspectRatio + ); + + const dispatch = useAppDispatch(); + + return ( + + + {aspectRatios.map((ratio) => ( + dispatch(setAspectRatio(ratio.value))} + > + {ratio.name} + + ))} + + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx index 6939ede424..63abe0ddf9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx @@ -2,19 +2,22 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { setHeight } from 'features/parameters/store/generationSlice'; +import { setHeight, setWidth } from 'features/parameters/store/generationSlice'; import { configSelector } from 'features/system/store/configSelectors'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice'; +import { uiSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - [generationSelector, hotkeysSelector, configSelector], - (generation, hotkeys, config) => { + [generationSelector, hotkeysSelector, configSelector, uiSelector], + (generation, hotkeys, config, ui) => { const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height; const { height } = generation; + const { aspectRatio } = ui; const step = hotkeys.shift ? fineStep : coarseStep; @@ -25,6 +28,7 @@ const selector = createSelector( sliderMax, inputMax, step, + aspectRatio, }; }, defaultSelectorOptions @@ -36,7 +40,7 @@ type ParamHeightProps = Omit< >; const ParamHeight = (props: ParamHeightProps) => { - const { height, initial, min, sliderMax, inputMax, step } = + const { height, initial, min, sliderMax, inputMax, step, aspectRatio } = useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -44,13 +48,21 @@ const ParamHeight = (props: ParamHeightProps) => { const handleChange = useCallback( (v: number) => { dispatch(setHeight(v)); + if (aspectRatio) { + const newWidth = roundToMultiple(v * aspectRatio, 8); + dispatch(setWidth(newWidth)); + } }, - [dispatch] + [dispatch, aspectRatio] ); const handleReset = useCallback(() => { dispatch(setHeight(initial)); - }, [dispatch, initial]); + if (aspectRatio) { + const newWidth = roundToMultiple(initial * aspectRatio, 8); + dispatch(setWidth(newWidth)); + } + }, [dispatch, initial, aspectRatio]); return ( state.generation.shouldFitToWidthHeight + ); + return ( + + + + {t('parameters.aspectRatio')} + + + + } + fontSize={20} + onClick={() => dispatch(toggleSize())} + /> + + + + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx index b4121184b5..991db19097 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx @@ -2,19 +2,22 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { setWidth } from 'features/parameters/store/generationSlice'; +import { setHeight, setWidth } from 'features/parameters/store/generationSlice'; import { configSelector } from 'features/system/store/configSelectors'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice'; +import { uiSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - [generationSelector, hotkeysSelector, configSelector], - (generation, hotkeys, config) => { + [generationSelector, hotkeysSelector, configSelector, uiSelector], + (generation, hotkeys, config, ui) => { const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width; const { width } = generation; + const { aspectRatio } = ui; const step = hotkeys.shift ? fineStep : coarseStep; @@ -25,6 +28,7 @@ const selector = createSelector( sliderMax, inputMax, step, + aspectRatio, }; }, defaultSelectorOptions @@ -33,7 +37,7 @@ const selector = createSelector( type ParamWidthProps = Omit; const ParamWidth = (props: ParamWidthProps) => { - const { width, initial, min, sliderMax, inputMax, step } = + const { width, initial, min, sliderMax, inputMax, step, aspectRatio } = useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -41,13 +45,21 @@ const ParamWidth = (props: ParamWidthProps) => { const handleChange = useCallback( (v: number) => { dispatch(setWidth(v)); + if (aspectRatio) { + const newHeight = roundToMultiple(v / aspectRatio, 8); + dispatch(setHeight(newHeight)); + } }, - [dispatch] + [dispatch, aspectRatio] ); const handleReset = useCallback(() => { dispatch(setWidth(initial)); - }, [dispatch, initial]); + if (aspectRatio) { + const newHeight = roundToMultiple(initial / aspectRatio, 8); + dispatch(setHeight(newHeight)); + } + }, [dispatch, initial, aspectRatio]); return ( ) => { state.width = action.payload; }, + toggleSize: (state) => { + const [width, height] = [state.width, state.height]; + state.width = height; + state.height = width; + }, setScheduler: (state, action: PayloadAction) => { state.scheduler = action.payload; }, @@ -262,6 +271,12 @@ export const generationSlice = createSlice({ const advancedOptionsStatus = action.payload; if (!advancedOptionsStatus) state.clipSkip = 0; }); + builder.addCase(setAspectRatio, (state, action) => { + const ratio = action.payload; + if (ratio) { + state.height = roundToMultiple(state.width / ratio, 8); + } + }); }, }); @@ -271,7 +286,9 @@ export const { resetParametersState, resetSeed, setCfgScale, + setWidth, setHeight, + toggleSize, setImg2imgStrength, setInfillMethod, setIterations, @@ -292,7 +309,6 @@ export const { setThreshold, setTileSize, setVariationAmount, - setWidth, setShouldUseSymmetry, setHorizontalSymmetrySteps, setVerticalSymmetrySteps, diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx index b333a0caf2..cda908da50 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx @@ -4,11 +4,10 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICollapse from 'common/components/IAICollapse'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; -import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler'; +import ParamSize from 'features/parameters/components/Parameters/Core/ParamSize'; import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; -import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit'; import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength'; import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; @@ -47,15 +46,14 @@ const ImageToImageTabCoreParameters = () => { > {shouldUseSliders ? ( <> + + + - - - - - + ) : ( <> @@ -68,8 +66,7 @@ const ImageToImageTabCoreParameters = () => { - - + )} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx index b007497db2..de8f64e661 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx @@ -5,11 +5,10 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICollapse from 'common/components/IAICollapse'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; -import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler'; +import ParamSize from 'features/parameters/components/Parameters/Core/ParamSize'; import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; -import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; import { memo } from 'react'; @@ -43,15 +42,14 @@ const TextToImageTabCoreParameters = () => { > {shouldUseSliders ? ( <> + + + - - - - - + ) : ( <> @@ -64,8 +62,7 @@ const TextToImageTabCoreParameters = () => { - - + )} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx index ecce61c218..6ea9d4bc8d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx @@ -44,13 +44,13 @@ const UnifiedCanvasCoreParameters = () => { > {shouldUseSliders ? ( <> + + + - - - diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 04fee42126..4f38f84fe2 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -21,6 +21,7 @@ export const initialUIState: UIState = { shouldShowProgressInViewer: true, shouldShowEmbeddingPicker: false, shouldShowAdvancedOptions: false, + aspectRatio: null, favoriteSchedulers: [], }; @@ -104,6 +105,9 @@ export const uiSlice = createSlice({ setShouldShowAdvancedOptions: (state, action: PayloadAction) => { state.shouldShowAdvancedOptions = action.payload; }, + setAspectRatio: (state, action: PayloadAction) => { + state.aspectRatio = action.payload; + }, }, extraReducers(builder) { builder.addCase(initialImageChanged, (state) => { @@ -132,6 +136,7 @@ export const { favoriteSchedulersChanged, toggleEmbeddingPicker, setShouldShowAdvancedOptions, + setAspectRatio, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 2356446030..e574f0ab79 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -29,5 +29,6 @@ export interface UIState { shouldShowProgressInViewer: boolean; shouldShowEmbeddingPicker: boolean; shouldShowAdvancedOptions: boolean; + aspectRatio: number | null; favoriteSchedulers: SchedulerParam[]; }