diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b403fde2c6..29a3c0907c 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": "Aspect Ratio" }, "settings": { "models": "Models", 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..f14a88af75 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamAspectRatio.tsx @@ -0,0 +1,49 @@ +import { 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'; +import { ReactNode } from 'react'; + +const aspectRatios = [ + { name: 'Free', value: null }, + { name: '4:3', value: 4 / 3 }, + { name: '16:9', value: 16 / 9 }, + { name: '3:2', value: 3 / 2 }, +]; + +export const roundToEight = (number: number) => { + return Math.round(number / 8) * 8; +}; + +export default function ParamAspectRatio() { + const aspectRatio = useAppSelector( + (state: RootState) => state.ui.aspectRatio + ); + + const dispatch = useAppDispatch(); + + const renderAspectRatios = () => { + const aspectRatiosToRender: ReactNode[] = []; + aspectRatios.forEach((ratio) => { + aspectRatiosToRender.push( + dispatch(setAspectRatio(ratio.value))} + > + {ratio.name} + + ); + }); + return aspectRatiosToRender; + }; + + return ( + + {renderAspectRatios()} + + ); +} 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..617fe07fc5 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 @@ -3,18 +3,21 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; 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'; +import { roundToEight } from './ParamAspectRatio'; 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,15 @@ const ParamHeight = (props: ParamHeightProps) => { const handleChange = useCallback( (v: number) => { dispatch(setHeight(v)); + if (aspectRatio) dispatch(setWidth(roundToEight(height * aspectRatio))); }, - [dispatch] + [dispatch, height, aspectRatio] ); const handleReset = useCallback(() => { dispatch(setHeight(initial)); - }, [dispatch, initial]); + if (aspectRatio) dispatch(setWidth(roundToEight(height * aspectRatio))); + }, [dispatch, initial, height, aspectRatio]); return ( state.generation.shouldFitToWidthHeight + ); + return ( + + + + {t('parameters.aspectRatio')} + + + + + + + + + ); +} 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..daf08e2ada 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 @@ -3,18 +3,21 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; 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'; +import { roundToEight } from './ParamAspectRatio'; 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,15 @@ const ParamWidth = (props: ParamWidthProps) => { const handleChange = useCallback( (v: number) => { dispatch(setWidth(v)); + if (aspectRatio) dispatch(setHeight(roundToEight(width / aspectRatio))); }, - [dispatch] + [dispatch, aspectRatio, width] ); const handleReset = useCallback(() => { dispatch(setWidth(initial)); - }, [dispatch, initial]); + if (aspectRatio) dispatch(setHeight(roundToEight(width / aspectRatio))); + }, [dispatch, initial, width, aspectRatio]); return ( { + const ratio = action.payload; + if (ratio) state.height = roundToEight(state.width / ratio); + }); }, }); 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[]; }