mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat: Add Aspect Ratio (#3709)
- Adds Aspect Ratio functionality to the UI - The ratios are placeholder. Feel free to add any ratios you want. https://github.com/invoke-ai/InvokeAI/assets/54517381/43921f57-fe0a-457f-baf2-b003310d4f85 - I did not add the same to Bounding Box width and height on the canvas. But its very easy to extend it to that too. So feel free to add if you want to.
This commit is contained in:
commit
b6fabe5146
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<Flex gap={2} flexGrow={1}>
|
||||
<ButtonGroup isAttached>
|
||||
{aspectRatios.map((ratio) => (
|
||||
<IAIButton
|
||||
key={ratio.name}
|
||||
size="sm"
|
||||
isChecked={aspectRatio === ratio.value}
|
||||
onClick={() => dispatch(setAspectRatio(ratio.value))}
|
||||
>
|
||||
{ratio.name}
|
||||
</IAIButton>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<IAISlider
|
||||
|
@ -0,0 +1,64 @@
|
||||
import { Flex, Spacer, Text } from '@chakra-ui/react';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { toggleSize } from 'features/parameters/store/generationSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdOutlineSwapVert } from 'react-icons/md';
|
||||
import ParamAspectRatio from './ParamAspectRatio';
|
||||
import ParamHeight from './ParamHeight';
|
||||
import ParamWidth from './ParamWidth';
|
||||
|
||||
export default function ParamSize() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldFitToWidthHeight = useAppSelector(
|
||||
(state: RootState) => state.generation.shouldFitToWidthHeight
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
gap: 2,
|
||||
p: 4,
|
||||
borderRadius: 4,
|
||||
flexDirection: 'column',
|
||||
w: 'full',
|
||||
bg: 'base.150',
|
||||
_dark: {
|
||||
bg: 'base.750',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 'sm',
|
||||
width: 'full',
|
||||
color: 'base.700',
|
||||
_dark: {
|
||||
color: 'base.300',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t('parameters.aspectRatio')}
|
||||
</Text>
|
||||
<Spacer />
|
||||
<ParamAspectRatio />
|
||||
<IAIIconButton
|
||||
tooltip={t('ui.swapSizes')}
|
||||
aria-label={t('ui.swapSizes')}
|
||||
size="sm"
|
||||
icon={<MdOutlineSwapVert />}
|
||||
fontSize={20}
|
||||
onClick={() => dispatch(toggleSize())}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={2} alignItems="center">
|
||||
<Flex gap={2} flexDirection="column" width="full">
|
||||
<ParamWidth isDisabled={!shouldFitToWidthHeight} />
|
||||
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -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<IAIFullSliderProps, 'label' | 'value' | 'onChange'>;
|
||||
|
||||
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 (
|
||||
<IAISlider
|
||||
|
@ -1,8 +1,12 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { DEFAULT_SCHEDULER_NAME } from 'app/constants';
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { setShouldShowAdvancedOptions } from 'features/ui/store/uiSlice';
|
||||
import {
|
||||
setAspectRatio,
|
||||
setShouldShowAdvancedOptions,
|
||||
} from 'features/ui/store/uiSlice';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { clipSkipMap } from '../components/Parameters/Advanced/ParamClipSkip';
|
||||
@ -139,6 +143,11 @@ export const generationSlice = createSlice({
|
||||
setWidth: (state, action: PayloadAction<number>) => {
|
||||
state.width = action.payload;
|
||||
},
|
||||
toggleSize: (state) => {
|
||||
const [width, height] = [state.width, state.height];
|
||||
state.width = height;
|
||||
state.height = width;
|
||||
},
|
||||
setScheduler: (state, action: PayloadAction<SchedulerParam>) => {
|
||||
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,
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
<ParamIterations />
|
||||
<ParamSteps />
|
||||
<ParamCFGScale />
|
||||
<ParamModelandVAEandScheduler />
|
||||
<Box pt={2}>
|
||||
<ParamSeedFull />
|
||||
</Box>
|
||||
<ParamIterations />
|
||||
<ParamSteps />
|
||||
<ParamCFGScale />
|
||||
<ParamWidth isDisabled={!shouldFitToWidthHeight} />
|
||||
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
|
||||
<ParamSize />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -68,8 +66,7 @@ const ImageToImageTabCoreParameters = () => {
|
||||
<Box pt={2}>
|
||||
<ParamSeedFull />
|
||||
</Box>
|
||||
<ParamWidth isDisabled={!shouldFitToWidthHeight} />
|
||||
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
|
||||
<ParamSize />
|
||||
</>
|
||||
)}
|
||||
<ImageToImageStrength />
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
<ParamIterations />
|
||||
<ParamSteps />
|
||||
<ParamCFGScale />
|
||||
<ParamModelandVAEandScheduler />
|
||||
<Box pt={2}>
|
||||
<ParamSeedFull />
|
||||
</Box>
|
||||
<ParamIterations />
|
||||
<ParamSteps />
|
||||
<ParamCFGScale />
|
||||
<ParamWidth />
|
||||
<ParamHeight />
|
||||
<ParamSize />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -64,8 +62,7 @@ const TextToImageTabCoreParameters = () => {
|
||||
<Box pt={2}>
|
||||
<ParamSeedFull />
|
||||
</Box>
|
||||
<ParamWidth />
|
||||
<ParamHeight />
|
||||
<ParamSize />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
@ -44,13 +44,13 @@ const UnifiedCanvasCoreParameters = () => {
|
||||
>
|
||||
{shouldUseSliders ? (
|
||||
<>
|
||||
<ParamIterations />
|
||||
<ParamSteps />
|
||||
<ParamCFGScale />
|
||||
<ParamModelandVAEandScheduler />
|
||||
<Box pt={2}>
|
||||
<ParamSeedFull />
|
||||
</Box>
|
||||
<ParamIterations />
|
||||
<ParamSteps />
|
||||
<ParamCFGScale />
|
||||
<ParamBoundingBoxWidth />
|
||||
<ParamBoundingBoxHeight />
|
||||
</>
|
||||
|
@ -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<boolean>) => {
|
||||
state.shouldShowAdvancedOptions = action.payload;
|
||||
},
|
||||
setAspectRatio: (state, action: PayloadAction<number | null>) => {
|
||||
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;
|
||||
|
@ -29,5 +29,6 @@ export interface UIState {
|
||||
shouldShowProgressInViewer: boolean;
|
||||
shouldShowEmbeddingPicker: boolean;
|
||||
shouldShowAdvancedOptions: boolean;
|
||||
aspectRatio: number | null;
|
||||
favoriteSchedulers: SchedulerParam[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user