mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): modularize imagesize components
Canvas and non-canvas have separate width and height and need their own separate aspect ratios. In order to not duplicate a lot of aspect ratio logic, the components relating to image size have been modularized.
This commit is contained in:
parent
011757c497
commit
4f43eda09b
@ -1,23 +0,0 @@
|
||||
// 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;
|
||||
export const ICON_SIZE_PX = 48;
|
||||
export 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',
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
import type { IconType } from 'react-icons';
|
||||
|
||||
export type AspectRatioPreviewProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
icon?: IconType;
|
||||
};
|
@ -10,8 +10,7 @@ import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants';
|
||||
import floorCoordinates from 'features/canvas/util/floorCoordinates';
|
||||
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
|
||||
import roundDimensionsTo64 from 'features/canvas/util/roundDimensionsTo64';
|
||||
import { ASPECT_RATIO_MAP } from 'features/parameters/components/ImageSize/constants';
|
||||
import { aspectRatioSelected } from 'features/parameters/store/generationSlice';
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import { clamp, cloneDeep } from 'lodash-es';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
@ -83,6 +82,11 @@ export const initialCanvasState: CanvasState = {
|
||||
stageScale: 1,
|
||||
tool: 'brush',
|
||||
batchIds: [],
|
||||
aspectRatio: {
|
||||
id: '1:1',
|
||||
value: 1,
|
||||
isLocked: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const canvasSlice = createSlice({
|
||||
@ -198,8 +202,14 @@ export const canvasSlice = createSlice({
|
||||
state.stageScale = newScale;
|
||||
state.stageCoordinates = newCoordinates;
|
||||
},
|
||||
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||
const newDimensions = roundDimensionsTo64(action.payload);
|
||||
setBoundingBoxDimensions: (
|
||||
state,
|
||||
action: PayloadAction<Partial<Dimensions>>
|
||||
) => {
|
||||
const newDimensions = roundDimensionsTo64({
|
||||
...state.boundingBoxDimensions,
|
||||
...action.payload,
|
||||
});
|
||||
state.boundingBoxDimensions = newDimensions;
|
||||
|
||||
if (state.boundingBoxScaleMethod === 'auto') {
|
||||
@ -722,6 +732,21 @@ export const canvasSlice = createSlice({
|
||||
|
||||
state.layerState.objects = [action.payload];
|
||||
},
|
||||
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
||||
state.aspectRatio = action.payload;
|
||||
// if (action.payload.id !== 'Free') {
|
||||
// state.boundingBoxDimensions.height = roundToMultiple(
|
||||
// state.boundingBoxDimensions.width /
|
||||
// ASPECT_RATIO_MAP[action.payload.id].ratio,
|
||||
// 64
|
||||
// );
|
||||
// state.scaledBoundingBoxDimensions.height = roundToMultiple(
|
||||
// state.scaledBoundingBoxDimensions.width /
|
||||
// ASPECT_RATIO_MAP[action.payload.id].ratio,
|
||||
// 64
|
||||
// );
|
||||
// }
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(appSocketQueueItemStatusChanged, (state, action) => {
|
||||
@ -736,21 +761,6 @@ export const canvasSlice = createSlice({
|
||||
);
|
||||
}
|
||||
});
|
||||
builder.addCase(aspectRatioSelected, (state, action) => {
|
||||
const aspectRatioID = action.payload;
|
||||
if (aspectRatioID !== 'Free') {
|
||||
state.boundingBoxDimensions.height = roundToMultiple(
|
||||
state.boundingBoxDimensions.width /
|
||||
ASPECT_RATIO_MAP[aspectRatioID].ratio,
|
||||
64
|
||||
);
|
||||
state.scaledBoundingBoxDimensions.height = roundToMultiple(
|
||||
state.scaledBoundingBoxDimensions.width /
|
||||
ASPECT_RATIO_MAP[aspectRatioID].ratio,
|
||||
64
|
||||
);
|
||||
}
|
||||
});
|
||||
builder.addMatcher(
|
||||
queueApi.endpoints.clearQueue.matchFulfilled,
|
||||
(state) => {
|
||||
@ -823,6 +833,7 @@ export const {
|
||||
canvasResized,
|
||||
canvasBatchIdAdded,
|
||||
canvasBatchIdsReset,
|
||||
aspectRatioChanged,
|
||||
} = canvasSlice.actions;
|
||||
|
||||
export default canvasSlice.reducer;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import { z } from 'zod';
|
||||
@ -150,6 +151,7 @@ export interface CanvasState {
|
||||
tool: CanvasTool;
|
||||
generationMode?: GenerationMode;
|
||||
batchIds: string[];
|
||||
aspectRatio: AspectRatioState;
|
||||
}
|
||||
|
||||
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
|
||||
|
@ -1,77 +1,45 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selector = createMemoizedSelector(
|
||||
[stateSelector, isStagingSelector],
|
||||
({ canvas, generation }, isStaging) => {
|
||||
const { boundingBoxDimensions } = canvas;
|
||||
const { model, aspectRatio } = generation;
|
||||
({ generation }, isStaging) => {
|
||||
const { model } = generation;
|
||||
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||
model?.base_model as string
|
||||
)
|
||||
? 1024
|
||||
: 512;
|
||||
return {
|
||||
initial,
|
||||
model,
|
||||
boundingBoxDimensions,
|
||||
isStaging,
|
||||
aspectRatio,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const ParamBoundingBoxWidth = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { model, boundingBoxDimensions, isStaging, aspectRatio } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const { isStaging, initial } = useAppSelector(selector);
|
||||
const ctx = useImageSizeContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string)
|
||||
? 1024
|
||||
: 512;
|
||||
|
||||
const handleChangeHeight = useCallback(
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(v),
|
||||
})
|
||||
);
|
||||
if (aspectRatio) {
|
||||
const newWidth = roundToMultiple(v * aspectRatio.value, 64);
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
width: newWidth,
|
||||
height: Math.floor(v),
|
||||
})
|
||||
);
|
||||
}
|
||||
ctx.heightChanged(v);
|
||||
},
|
||||
[aspectRatio, boundingBoxDimensions, dispatch]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const handleResetHeight = useCallback(() => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(initial),
|
||||
})
|
||||
);
|
||||
if (aspectRatio) {
|
||||
const newWidth = roundToMultiple(initial * aspectRatio.value, 64);
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
width: newWidth,
|
||||
height: Math.floor(initial),
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [aspectRatio, boundingBoxDimensions, dispatch, initial]);
|
||||
const onReset = useCallback(() => {
|
||||
ctx.heightChanged(initial);
|
||||
}, [ctx, initial]);
|
||||
|
||||
return (
|
||||
<InvControl label={t('parameters.height')} isDisabled={isStaging}>
|
||||
@ -79,9 +47,9 @@ const ParamBoundingBoxWidth = () => {
|
||||
min={64}
|
||||
max={1536}
|
||||
step={64}
|
||||
value={boundingBoxDimensions.height}
|
||||
onChange={handleChangeHeight}
|
||||
onReset={handleResetHeight}
|
||||
value={ctx.height}
|
||||
onChange={onChange}
|
||||
onReset={onReset}
|
||||
marks
|
||||
withNumberInput
|
||||
numberInputMax={4096}
|
||||
|
@ -1,117 +0,0 @@
|
||||
// import { Flex, FormControl, FormLabel, Spacer } from '@chakra-ui/react';
|
||||
// import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
// import { stateSelector } from 'app/store/store';
|
||||
// import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
// import { InvIconButton } from 'common/components';
|
||||
// import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
|
||||
// import { flipBoundingBoxAxes } from 'features/canvas/store/canvasSlice';
|
||||
// import ParamAspectRatio, {
|
||||
// mappedAspectRatios,
|
||||
// } from 'features/parameters/components/Parameters/Core/ParamAspectRatio';
|
||||
|
||||
// import { useCallback } from 'react';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
// import { FaLock } from 'react-icons/fa';
|
||||
// import { MdOutlineSwapVert } from 'react-icons/md';
|
||||
// import ParamBoundingBoxHeight from './ParamBoundingBoxHeight';
|
||||
// import ParamBoundingBoxWidth from './ParamBoundingBoxWidth';
|
||||
|
||||
// const sizeOptsSelector = createMemoizedSelector(
|
||||
// [stateSelector],
|
||||
// ({ generation, canvas }) => {
|
||||
// const { shouldFitToWidthHeight, shouldLockAspectRatio } = generation;
|
||||
// const { boundingBoxDimensions } = canvas;
|
||||
|
||||
// return {
|
||||
// shouldFitToWidthHeight,
|
||||
// shouldLockAspectRatio,
|
||||
// boundingBoxDimensions,
|
||||
// };
|
||||
// }
|
||||
// );
|
||||
|
||||
// export default function ParamBoundingBoxSize() {
|
||||
// const dispatch = useAppDispatch();
|
||||
// const { t } = useTranslation();
|
||||
|
||||
// const { shouldLockAspectRatio, boundingBoxDimensions } =
|
||||
// useAppSelector(sizeOptsSelector);
|
||||
|
||||
// const handleLockRatio = useCallback(() => {
|
||||
// if (shouldLockAspectRatio) {
|
||||
// dispatch(setShouldLockAspectRatio(false));
|
||||
// if (
|
||||
// !mappedAspectRatios.includes(
|
||||
// boundingBoxDimensions.width / boundingBoxDimensions.height
|
||||
// )
|
||||
// ) {
|
||||
// dispatch(setAspectRatio(null));
|
||||
// } else {
|
||||
// dispatch(
|
||||
// setAspectRatio(
|
||||
// boundingBoxDimensions.width / boundingBoxDimensions.height
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// } else {
|
||||
// dispatch(setShouldLockAspectRatio(true));
|
||||
// dispatch(
|
||||
// setAspectRatio(
|
||||
// boundingBoxDimensions.width / boundingBoxDimensions.height
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// }, [shouldLockAspectRatio, boundingBoxDimensions, dispatch]);
|
||||
|
||||
// const handleToggleSize = useCallback(() => {
|
||||
// dispatch(flipBoundingBoxAxes());
|
||||
// dispatch(setAspectRatio(null));
|
||||
// if (shouldLockAspectRatio) {
|
||||
// dispatch(
|
||||
// setAspectRatio(
|
||||
// boundingBoxDimensions.height / boundingBoxDimensions.width
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// }, [dispatch, shouldLockAspectRatio, boundingBoxDimensions]);
|
||||
|
||||
// return (
|
||||
// <Flex
|
||||
// sx={{
|
||||
// gap: 2,
|
||||
// p: 4,
|
||||
// borderRadius: 4,
|
||||
// flexDirection: 'column',
|
||||
// w: 'full',
|
||||
// bg: 'base.750',
|
||||
// }}
|
||||
// >
|
||||
// <IAIInformationalPopover feature="paramRatio">
|
||||
// <FormControl as={Flex} flexDir="row" alignItems="center" gap={2}>
|
||||
// <FormLabel>{t('parameters.aspectRatio')}</FormLabel>
|
||||
// <Spacer />
|
||||
// <ParamAspectRatio />
|
||||
// <InvIconButton
|
||||
// tooltip={t('ui.swapSizes')}
|
||||
// aria-label={t('ui.swapSizes')}
|
||||
// size="sm"
|
||||
// icon={<MdOutlineSwapVert />}
|
||||
// fontSize={20}
|
||||
// onClick={handleToggleSize}
|
||||
// />
|
||||
// <InvIconButton
|
||||
// tooltip={t('ui.lockRatio')}
|
||||
// aria-label={t('ui.lockRatio')}
|
||||
// size="sm"
|
||||
// icon={<FaLock />}
|
||||
// isChecked={shouldLockAspectRatio}
|
||||
// onClick={handleLockRatio}
|
||||
// />
|
||||
// </FormControl>
|
||||
// </IAIInformationalPopover>
|
||||
// <ParamBoundingBoxWidth />
|
||||
// <ParamBoundingBoxHeight />
|
||||
// </Flex>
|
||||
// );
|
||||
// }
|
||||
export default {};
|
@ -1,77 +1,45 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selector = createMemoizedSelector(
|
||||
[stateSelector, isStagingSelector],
|
||||
({ canvas, generation }, isStaging) => {
|
||||
const { boundingBoxDimensions } = canvas;
|
||||
const { model, aspectRatio } = generation;
|
||||
({ generation }, isStaging) => {
|
||||
const { model } = generation;
|
||||
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||
model?.base_model as string
|
||||
)
|
||||
? 1024
|
||||
: 512;
|
||||
return {
|
||||
initial,
|
||||
model,
|
||||
boundingBoxDimensions,
|
||||
isStaging,
|
||||
aspectRatio,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const ParamBoundingBoxWidth = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { model, boundingBoxDimensions, isStaging, aspectRatio } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string)
|
||||
? 1024
|
||||
: 512;
|
||||
|
||||
const { isStaging, initial } = useAppSelector(selector);
|
||||
const ctx = useImageSizeContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChangeWidth = useCallback(
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(v),
|
||||
})
|
||||
);
|
||||
if (aspectRatio) {
|
||||
const newHeight = roundToMultiple(v / aspectRatio.value, 64);
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
width: Math.floor(v),
|
||||
height: newHeight,
|
||||
})
|
||||
);
|
||||
}
|
||||
ctx.widthChanged(v);
|
||||
},
|
||||
[aspectRatio, boundingBoxDimensions, dispatch]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const handleResetWidth = useCallback(() => {
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(initial),
|
||||
})
|
||||
);
|
||||
if (aspectRatio) {
|
||||
const newHeight = roundToMultiple(initial / aspectRatio.value, 64);
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
width: Math.floor(initial),
|
||||
height: newHeight,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [aspectRatio, boundingBoxDimensions, dispatch, initial]);
|
||||
const onReset = useCallback(() => {
|
||||
ctx.widthChanged(initial);
|
||||
}, [ctx, initial]);
|
||||
|
||||
return (
|
||||
<InvControl label={t('parameters.width')} isDisabled={isStaging}>
|
||||
@ -79,9 +47,9 @@ const ParamBoundingBoxWidth = () => {
|
||||
min={64}
|
||||
max={1536}
|
||||
step={64}
|
||||
value={boundingBoxDimensions.width}
|
||||
onChange={handleChangeWidth}
|
||||
onReset={handleResetWidth}
|
||||
value={ctx.width}
|
||||
onChange={onChange}
|
||||
onReset={onReset}
|
||||
withNumberInput
|
||||
numberInputMax={4096}
|
||||
marks
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { heightChanged } from 'features/parameters/store/generationSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -12,7 +12,7 @@ const selector = createMemoizedSelector(
|
||||
[stateSelector],
|
||||
({ generation, config }) => {
|
||||
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height;
|
||||
const { model, height } = generation;
|
||||
const { model } = generation;
|
||||
|
||||
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||
model?.base_model as string
|
||||
@ -22,7 +22,6 @@ const selector = createMemoizedSelector(
|
||||
|
||||
return {
|
||||
initial,
|
||||
height,
|
||||
min,
|
||||
max: sliderMax,
|
||||
inputMax,
|
||||
@ -34,27 +33,27 @@ const selector = createMemoizedSelector(
|
||||
|
||||
export const ParamHeight = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { initial, height, min, max, inputMax, step, fineStep } =
|
||||
const ctx = useImageSizeContext();
|
||||
const { initial, min, max, inputMax, step, fineStep } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(heightChanged(v));
|
||||
ctx.heightChanged(v);
|
||||
},
|
||||
[dispatch]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(heightChanged(initial));
|
||||
}, [dispatch, initial]);
|
||||
ctx.heightChanged(initial);
|
||||
}, [ctx, initial]);
|
||||
|
||||
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
|
||||
|
||||
return (
|
||||
<InvControl label={t('parameters.height')}>
|
||||
<InvSlider
|
||||
value={height}
|
||||
value={ctx.height}
|
||||
onChange={onChange}
|
||||
onReset={onReset}
|
||||
min={min}
|
||||
@ -64,7 +63,7 @@ export const ParamHeight = memo(() => {
|
||||
marks={marks}
|
||||
/>
|
||||
<InvNumberInput
|
||||
value={height}
|
||||
value={ctx.height}
|
||||
onChange={onChange}
|
||||
min={min}
|
||||
max={inputMax}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { widthChanged } from 'features/parameters/store/generationSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -12,7 +12,7 @@ const selector = createMemoizedSelector(
|
||||
[stateSelector],
|
||||
({ generation, config }) => {
|
||||
const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width;
|
||||
const { model, width } = generation;
|
||||
const { model } = generation;
|
||||
|
||||
const initial = ['sdxl', 'sdxl-refiner'].includes(
|
||||
model?.base_model as string
|
||||
@ -22,7 +22,6 @@ const selector = createMemoizedSelector(
|
||||
|
||||
return {
|
||||
initial,
|
||||
width,
|
||||
min,
|
||||
max: sliderMax,
|
||||
step: coarseStep,
|
||||
@ -33,27 +32,27 @@ const selector = createMemoizedSelector(
|
||||
);
|
||||
export const ParamWidth = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { initial, width, min, max, inputMax, step, fineStep } =
|
||||
const ctx = useImageSizeContext();
|
||||
const { initial, min, max, inputMax, step, fineStep } =
|
||||
useAppSelector(selector);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(widthChanged(v));
|
||||
ctx.widthChanged(v);
|
||||
},
|
||||
[dispatch]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(widthChanged(initial));
|
||||
}, [dispatch, initial]);
|
||||
ctx.widthChanged(initial);
|
||||
}, [ctx, initial]);
|
||||
|
||||
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
|
||||
|
||||
return (
|
||||
<InvControl label={t('parameters.width')}>
|
||||
<InvSlider
|
||||
value={width}
|
||||
value={ctx.width}
|
||||
onChange={onChange}
|
||||
onReset={onReset}
|
||||
min={min}
|
||||
@ -63,7 +62,7 @@ export const ParamWidth = memo(() => {
|
||||
marks={marks}
|
||||
/>
|
||||
<InvNumberInput
|
||||
value={width}
|
||||
value={ctx.width}
|
||||
onChange={onChange}
|
||||
min={min}
|
||||
max={inputMax}
|
||||
|
@ -1,29 +1,55 @@
|
||||
import { Flex, Icon } from '@chakra-ui/react';
|
||||
import { useSize } from '@chakra-ui/react-use-size';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { FaImage } from 'react-icons/fa';
|
||||
|
||||
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';
|
||||
import { useAspectRatioPreviewState } from './hooks';
|
||||
import type { AspectRatioPreviewProps } from './types';
|
||||
|
||||
export const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
|
||||
const { width: _width, height: _height, icon = FaImage } = props;
|
||||
export type AspectRatioPreviewProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export const AspectRatioPreview = () => {
|
||||
const ctx = useImageSizeContext();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerSize = useSize(containerRef);
|
||||
|
||||
const { width, height, shouldShowIcon } = useAspectRatioPreviewState({
|
||||
width: _width,
|
||||
height: _height,
|
||||
containerSize,
|
||||
});
|
||||
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]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -50,7 +76,7 @@ export const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
|
||||
exit={MOTION_ICON_EXIT}
|
||||
style={ICON_CONTAINER_STYLES}
|
||||
>
|
||||
<Icon as={icon} color="base.700" boxSize={BOX_SIZE_CSS_CALC} />
|
||||
<Icon as={FaImage} color="base.700" boxSize={BOX_SIZE_CSS_CALC} />
|
||||
</Flex>
|
||||
)}
|
||||
</AnimatePresence>
|
@ -1,12 +0,0 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { AspectRatioPreview } from 'common/components/AspectRatioPreview/AspectRatioPreview';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const AspectRatioPreviewWrapper = memo(() => {
|
||||
const width = useAppSelector((state) => state.generation.width);
|
||||
const height = useAppSelector((state) => state.generation.height);
|
||||
|
||||
return <AspectRatioPreview width={width} height={height} />;
|
||||
});
|
||||
|
||||
AspectRatioPreviewWrapper.displayName = 'AspectRatioPreviewWrapper';
|
@ -1,39 +1,34 @@
|
||||
import type { SystemStyleObject } from '@chakra-ui/styled-system';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { SingleValue } from 'chakra-react-select';
|
||||
import { InvControl } from 'common/components/InvControl/InvControl';
|
||||
import { InvSelect } from 'common/components/InvSelect/InvSelect';
|
||||
import type { InvSelectOption } from 'common/components/InvSelect/types';
|
||||
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/ImageSize/constants';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { isAspectRatioID } from 'features/parameters/components/ImageSize/types';
|
||||
import { aspectRatioSelected } from 'features/parameters/store/generationSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { LockAspectRatioButton } from './LockAspectRatioButton';
|
||||
import { SetOptimalSizeButton } from './SetOptimalSizeButton';
|
||||
import { SwapDimensionsButton } from './SwapDimensionsButton';
|
||||
|
||||
export const AspectRatioSelect = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const aspectRatioID = useAppSelector(
|
||||
(state) => state.generation.aspectRatio.id
|
||||
);
|
||||
const ctx = useImageSizeContext();
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: SingleValue<InvSelectOption>) => {
|
||||
if (!v || !isAspectRatioID(v.value)) {
|
||||
return;
|
||||
}
|
||||
dispatch(aspectRatioSelected(v.value));
|
||||
ctx.aspectRatioSelected(v.value);
|
||||
},
|
||||
[dispatch]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ASPECT_RATIO_OPTIONS.filter((o) => o.value === aspectRatioID)[0],
|
||||
[aspectRatioID]
|
||||
() =>
|
||||
ASPECT_RATIO_OPTIONS.filter(
|
||||
(o) => o.value === ctx.aspectRatioState.id
|
||||
)[0],
|
||||
[ctx.aspectRatioState.id]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -44,9 +39,6 @@ export const AspectRatioSelect = memo(() => {
|
||||
options={ASPECT_RATIO_OPTIONS}
|
||||
sx={selectStyles}
|
||||
/>
|
||||
<SwapDimensionsButton />
|
||||
<LockAspectRatioButton />
|
||||
<SetOptimalSizeButton />
|
||||
</InvControl>
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import { InvControlGroup } from 'common/components/InvControl/InvControlGroup';
|
||||
import type { InvLabelProps } from 'common/components/InvControl/types';
|
||||
import { AspectRatioPreview } from 'features/parameters/components/ImageSize/AspectRatioPreview';
|
||||
import { AspectRatioSelect } from 'features/parameters/components/ImageSize/AspectRatioSelect';
|
||||
import type { ImageSizeContextInnerValue } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { ImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { LockAspectRatioButton } from 'features/parameters/components/ImageSize/LockAspectRatioButton';
|
||||
import { SetOptimalSizeButton } from 'features/parameters/components/ImageSize/SetOptimalSizeButton';
|
||||
import { SwapDimensionsButton } from 'features/parameters/components/ImageSize/SwapDimensionsButton';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type ImageSizeProps = ImageSizeContextInnerValue & {
|
||||
widthComponent: ReactNode;
|
||||
heightComponent: ReactNode;
|
||||
};
|
||||
|
||||
export const ImageSize = memo((props: ImageSizeProps) => {
|
||||
const { widthComponent, heightComponent, ...ctx } = props;
|
||||
return (
|
||||
<ImageSizeContext.Provider value={ctx}>
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
<InvControlGroup labelProps={labelProps}>
|
||||
<Flex gap={4}>
|
||||
<AspectRatioSelect />
|
||||
<SwapDimensionsButton />
|
||||
<LockAspectRatioButton />
|
||||
<SetOptimalSizeButton />
|
||||
</Flex>
|
||||
{widthComponent}
|
||||
{heightComponent}
|
||||
</InvControlGroup>
|
||||
</Flex>
|
||||
<Flex w="98px" h="98px" flexShrink={0} flexGrow={0}>
|
||||
<AspectRatioPreview />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ImageSizeContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
ImageSize.displayName = 'ImageSize';
|
||||
|
||||
const labelProps: InvLabelProps = {
|
||||
minW: 14,
|
||||
};
|
@ -0,0 +1,176 @@
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||
import {
|
||||
ASPECT_RATIO_MAP,
|
||||
initialAspectRatioState,
|
||||
} from 'features/parameters/components/ImageSize/constants';
|
||||
import type {
|
||||
AspectRatioID,
|
||||
AspectRatioState,
|
||||
} from 'features/parameters/components/ImageSize/types';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
export type ImageSizeContextInnerValue = {
|
||||
width: number;
|
||||
height: number;
|
||||
aspectRatioState: AspectRatioState;
|
||||
onChangeWidth: (width: number) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onChangeAspectRatioState: (aspectRatioState: AspectRatioState) => void;
|
||||
};
|
||||
|
||||
export type ImageSizeContext = {
|
||||
width: number;
|
||||
height: number;
|
||||
aspectRatioState: AspectRatioState;
|
||||
aspectRatioSelected: (aspectRatioID: AspectRatioID) => void;
|
||||
dimensionsSwapped: () => void;
|
||||
widthChanged: (width: number) => void;
|
||||
heightChanged: (height: number) => void;
|
||||
isLockedToggled: () => void;
|
||||
sizeReset: (width: number, height: number) => void;
|
||||
};
|
||||
|
||||
export const ImageSizeContext =
|
||||
createContext<ImageSizeContextInnerValue | null>(null);
|
||||
|
||||
export const useImageSizeContext = (): ImageSizeContext => {
|
||||
const _ctx = useContext(ImageSizeContext);
|
||||
|
||||
if (!_ctx) {
|
||||
throw new Error(
|
||||
'useImageSizeContext must be used within a ImageSizeContext.Provider'
|
||||
);
|
||||
}
|
||||
|
||||
const aspectRatioSelected = useCallback(
|
||||
(aspectRatioID: AspectRatioID) => {
|
||||
const state: AspectRatioState = {
|
||||
..._ctx.aspectRatioState,
|
||||
id: aspectRatioID,
|
||||
};
|
||||
if (state.id === 'Free') {
|
||||
// If the new aspect ratio is free, we only unlock
|
||||
state.isLocked = false;
|
||||
} else {
|
||||
// The new aspect ratio not free, so we need to coerce the size & lock
|
||||
state.isLocked = true;
|
||||
state.value = ASPECT_RATIO_MAP[state.id].ratio;
|
||||
const { width, height } = calculateNewSize(
|
||||
state.value,
|
||||
_ctx.width,
|
||||
_ctx.height
|
||||
);
|
||||
_ctx.onChangeWidth(width);
|
||||
_ctx.onChangeHeight(height);
|
||||
}
|
||||
_ctx.onChangeAspectRatioState(state);
|
||||
},
|
||||
[_ctx]
|
||||
);
|
||||
const dimensionsSwapped = useCallback(() => {
|
||||
const state = {
|
||||
..._ctx.aspectRatioState,
|
||||
};
|
||||
// We always invert the aspect ratio
|
||||
state.value = 1 / state.value;
|
||||
if (state.id === 'Free') {
|
||||
// If the aspect ratio is free, we just swap the dimensions
|
||||
const newWidth = _ctx.height;
|
||||
const newHeight = _ctx.width;
|
||||
_ctx.onChangeWidth(newWidth);
|
||||
_ctx.onChangeHeight(newHeight);
|
||||
} else {
|
||||
// Else we need to calculate the new size
|
||||
const { width, height } = calculateNewSize(
|
||||
state.value,
|
||||
_ctx.width,
|
||||
_ctx.height
|
||||
);
|
||||
_ctx.onChangeWidth(width);
|
||||
_ctx.onChangeHeight(height);
|
||||
// Update the aspect ratio ID to match the new aspect ratio
|
||||
state.id = ASPECT_RATIO_MAP[state.id].inverseID;
|
||||
}
|
||||
_ctx.onChangeAspectRatioState(state);
|
||||
}, [_ctx]);
|
||||
|
||||
const widthChanged = useCallback(
|
||||
(width: number) => {
|
||||
let height = _ctx.height;
|
||||
const state = { ..._ctx.aspectRatioState };
|
||||
if (state.isLocked) {
|
||||
// When locked, we calculate the new height based on the aspect ratio
|
||||
height = roundToMultiple(width / state.value, 8);
|
||||
} else {
|
||||
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
||||
state.isLocked = false;
|
||||
state.id = 'Free';
|
||||
state.value = width / height;
|
||||
}
|
||||
_ctx.onChangeWidth(width);
|
||||
_ctx.onChangeHeight(height);
|
||||
_ctx.onChangeAspectRatioState(state);
|
||||
},
|
||||
[_ctx]
|
||||
);
|
||||
|
||||
const heightChanged = useCallback(
|
||||
(height: number) => {
|
||||
let width = _ctx.width;
|
||||
const state = { ..._ctx.aspectRatioState };
|
||||
if (state.isLocked) {
|
||||
// When locked, we calculate the new width based on the aspect ratio
|
||||
width = roundToMultiple(height * state.value, 8);
|
||||
} else {
|
||||
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
||||
state.isLocked = false;
|
||||
state.id = 'Free';
|
||||
state.value = width / height;
|
||||
}
|
||||
_ctx.onChangeWidth(width);
|
||||
_ctx.onChangeHeight(height);
|
||||
_ctx.onChangeAspectRatioState(state);
|
||||
},
|
||||
[_ctx]
|
||||
);
|
||||
|
||||
const isLockedToggled = useCallback(() => {
|
||||
const state = { ..._ctx.aspectRatioState };
|
||||
state.isLocked = !state.isLocked;
|
||||
_ctx.onChangeAspectRatioState(state);
|
||||
}, [_ctx]);
|
||||
|
||||
const sizeReset = useCallback(
|
||||
(width: number, height: number) => {
|
||||
const state = { ...initialAspectRatioState };
|
||||
_ctx.onChangeAspectRatioState(state);
|
||||
_ctx.onChangeWidth(width);
|
||||
_ctx.onChangeHeight(height);
|
||||
},
|
||||
[_ctx]
|
||||
);
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({
|
||||
..._ctx,
|
||||
aspectRatioSelected,
|
||||
dimensionsSwapped,
|
||||
widthChanged,
|
||||
heightChanged,
|
||||
isLockedToggled,
|
||||
sizeReset,
|
||||
}),
|
||||
[
|
||||
_ctx,
|
||||
aspectRatioSelected,
|
||||
dimensionsSwapped,
|
||||
heightChanged,
|
||||
isLockedToggled,
|
||||
sizeReset,
|
||||
widthChanged,
|
||||
]
|
||||
);
|
||||
|
||||
return ctx;
|
||||
};
|
@ -1,27 +1,23 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { isLockedToggled } from 'features/parameters/store/generationSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaLock, FaLockOpen } from 'react-icons/fa6';
|
||||
|
||||
export const LockAspectRatioButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isLocked = useAppSelector(
|
||||
(state) => state.generation.aspectRatio.isLocked
|
||||
);
|
||||
const ctx = useImageSizeContext();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(isLockedToggled());
|
||||
}, [dispatch]);
|
||||
ctx.isLockedToggled();
|
||||
}, [ctx]);
|
||||
|
||||
return (
|
||||
<InvIconButton
|
||||
aria-label={t('parameters.lockAspectRatio')}
|
||||
onClick={onClick}
|
||||
variant={isLocked ? 'outline' : 'ghost'}
|
||||
variant={ctx.aspectRatioState.isLocked ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
icon={isLocked ? <FaLock /> : <FaLockOpen />}
|
||||
icon={ctx.aspectRatioState.isLocked ? <FaLock /> : <FaLockOpen />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { sizeReset } from 'features/parameters/store/generationSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IoSparkles } from 'react-icons/io5';
|
||||
|
||||
export const SetOptimalSizeButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const ctx = useImageSizeContext();
|
||||
const optimalDimension = useAppSelector((state) =>
|
||||
state.generation.model?.base_model === 'sdxl' ? 1024 : 512
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(sizeReset(optimalDimension));
|
||||
}, [dispatch, optimalDimension]);
|
||||
ctx.sizeReset(optimalDimension, optimalDimension);
|
||||
}, [ctx, optimalDimension]);
|
||||
|
||||
return (
|
||||
<InvIconButton
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
|
||||
import { dimensionsSwapped } from 'features/parameters/store/generationSlice';
|
||||
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IoSwapVertical } from 'react-icons/io5';
|
||||
|
||||
export const SwapDimensionsButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const ctx = useImageSizeContext();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(dimensionsSwapped());
|
||||
}, [dispatch]);
|
||||
ctx.dimensionsSwapped();
|
||||
}, [ctx]);
|
||||
return (
|
||||
<InvIconButton
|
||||
aria-label={t('parameters.swapDimensions')}
|
||||
|
@ -1,6 +1,30 @@
|
||||
import type { InvSelectOption } from 'common/components/InvSelect/types';
|
||||
|
||||
import type { AspectRatioID } from './types';
|
||||
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;
|
||||
export const ICON_SIZE_PX = 48;
|
||||
export 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: InvSelectOption[] = [
|
||||
{ label: 'Free' as const, value: 'Free' },
|
||||
@ -25,3 +49,9 @@ export const ASPECT_RATIO_MAP: Record<
|
||||
'2:3': { ratio: 2 / 3, inverseID: '3:2' },
|
||||
'9:16': { ratio: 9 / 16, inverseID: '16:9' },
|
||||
};
|
||||
|
||||
export const initialAspectRatioState: AspectRatioState = {
|
||||
id: '1:1',
|
||||
value: 1,
|
||||
isLocked: false,
|
||||
};
|
||||
|
@ -13,3 +13,9 @@ export const zAspectRatioID = z.enum([
|
||||
export type AspectRatioID = z.infer<typeof zAspectRatioID>;
|
||||
export const isAspectRatioID = (v: string): v is AspectRatioID =>
|
||||
zAspectRatioID.safeParse(v).success;
|
||||
|
||||
export type AspectRatioState = {
|
||||
id: AspectRatioID;
|
||||
value: number;
|
||||
isLocked: boolean;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { GenerationState } from './generationSlice';
|
||||
import type { GenerationState } from './types';
|
||||
|
||||
/**
|
||||
* Generation slice persist denylist
|
||||
|
@ -2,77 +2,25 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||
import { ASPECT_RATIO_MAP } from 'features/parameters/components/ImageSize/constants';
|
||||
import type { AspectRatioID } from 'features/parameters/components/ImageSize/types';
|
||||
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||
import type {
|
||||
ParameterCanvasCoherenceMode,
|
||||
ParameterCFGRescaleMultiplier,
|
||||
ParameterCFGScale,
|
||||
ParameterHeight,
|
||||
ParameterMaskBlurMethod,
|
||||
ParameterModel,
|
||||
ParameterNegativePrompt,
|
||||
ParameterPositivePrompt,
|
||||
ParameterPrecision,
|
||||
ParameterScheduler,
|
||||
ParameterSeed,
|
||||
ParameterSteps,
|
||||
ParameterStrength,
|
||||
ParameterVAEModel,
|
||||
ParameterWidth,
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { clamp, cloneDeep } from 'lodash-es';
|
||||
import { clamp } from 'lodash-es';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export interface GenerationState {
|
||||
cfgScale: ParameterCFGScale;
|
||||
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
|
||||
height: ParameterHeight;
|
||||
img2imgStrength: ParameterStrength;
|
||||
infillMethod: string;
|
||||
initialImage?: { imageName: string; width: number; height: number };
|
||||
iterations: number;
|
||||
perlin: number;
|
||||
positivePrompt: ParameterPositivePrompt;
|
||||
negativePrompt: ParameterNegativePrompt;
|
||||
scheduler: ParameterScheduler;
|
||||
maskBlur: number;
|
||||
maskBlurMethod: ParameterMaskBlurMethod;
|
||||
canvasCoherenceMode: ParameterCanvasCoherenceMode;
|
||||
canvasCoherenceSteps: number;
|
||||
canvasCoherenceStrength: ParameterStrength;
|
||||
seed: ParameterSeed;
|
||||
seedWeights: string;
|
||||
shouldFitToWidthHeight: boolean;
|
||||
shouldGenerateVariations: boolean;
|
||||
shouldRandomizeSeed: boolean;
|
||||
steps: ParameterSteps;
|
||||
threshold: number;
|
||||
infillTileSize: number;
|
||||
infillPatchmatchDownscaleSize: number;
|
||||
variationAmount: number;
|
||||
width: ParameterWidth;
|
||||
shouldUseSymmetry: boolean;
|
||||
horizontalSymmetrySteps: number;
|
||||
verticalSymmetrySteps: number;
|
||||
model: ParameterModel | null;
|
||||
vae: ParameterVAEModel | null;
|
||||
vaePrecision: ParameterPrecision;
|
||||
seamlessXAxis: boolean;
|
||||
seamlessYAxis: boolean;
|
||||
clipSkip: number;
|
||||
shouldUseCpuNoise: boolean;
|
||||
shouldShowAdvancedOptions: boolean;
|
||||
aspectRatio: {
|
||||
id: AspectRatioID;
|
||||
value: number;
|
||||
isLocked: boolean;
|
||||
};
|
||||
}
|
||||
import type { GenerationState } from './types';
|
||||
|
||||
export const initialGenerationState: GenerationState = {
|
||||
cfgScale: 7.5,
|
||||
@ -112,18 +60,12 @@ export const initialGenerationState: GenerationState = {
|
||||
clipSkip: 0,
|
||||
shouldUseCpuNoise: true,
|
||||
shouldShowAdvancedOptions: false,
|
||||
aspectRatio: {
|
||||
id: '1:1',
|
||||
value: 1,
|
||||
isLocked: false,
|
||||
},
|
||||
aspectRatio: { ...initialAspectRatioState },
|
||||
};
|
||||
|
||||
const initialState: GenerationState = initialGenerationState;
|
||||
|
||||
export const generationSlice = createSlice({
|
||||
name: 'generation',
|
||||
initialState,
|
||||
initialState: initialGenerationState,
|
||||
reducers: {
|
||||
setPositivePrompt: (state, action: PayloadAction<string>) => {
|
||||
state.positivePrompt = action.payload;
|
||||
@ -279,93 +221,15 @@ export const generationSlice = createSlice({
|
||||
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldUseCpuNoise = action.payload;
|
||||
},
|
||||
aspectRatioSelected: (
|
||||
state,
|
||||
action: PayloadAction<GenerationState['aspectRatio']['id']>
|
||||
) => {
|
||||
const aspectRatioID = action.payload;
|
||||
state.aspectRatio.id = aspectRatioID;
|
||||
if (aspectRatioID === 'Free') {
|
||||
// If the new aspect ratio is free, we only unlock
|
||||
state.aspectRatio.isLocked = false;
|
||||
} else {
|
||||
// The new aspect ratio not free, so we need to coerce the size & lock
|
||||
state.aspectRatio.isLocked = true;
|
||||
const aspectRatio = ASPECT_RATIO_MAP[aspectRatioID].ratio;
|
||||
state.aspectRatio.value = aspectRatio;
|
||||
const { width, height } = calculateNewSize(
|
||||
aspectRatio,
|
||||
state.width,
|
||||
state.height
|
||||
);
|
||||
state.width = width;
|
||||
state.height = height;
|
||||
}
|
||||
},
|
||||
dimensionsSwapped: (state) => {
|
||||
// We always invert the aspect ratio
|
||||
const aspectRatio = 1 / state.aspectRatio.value;
|
||||
state.aspectRatio.value = aspectRatio;
|
||||
if (state.aspectRatio.id === 'Free') {
|
||||
// If the aspect ratio is free, we just swap the dimensions
|
||||
const oldWidth = state.width;
|
||||
const oldHeight = state.height;
|
||||
state.width = oldHeight;
|
||||
state.height = oldWidth;
|
||||
} else {
|
||||
// Else we need to calculate the new size
|
||||
const { width, height } = calculateNewSize(
|
||||
aspectRatio,
|
||||
state.width,
|
||||
state.height
|
||||
);
|
||||
state.width = width;
|
||||
state.height = height;
|
||||
// Update the aspect ratio ID to match the new aspect ratio
|
||||
state.aspectRatio.id = ASPECT_RATIO_MAP[state.aspectRatio.id].inverseID;
|
||||
}
|
||||
},
|
||||
widthChanged: (state, action: PayloadAction<GenerationState['width']>) => {
|
||||
const width = action.payload;
|
||||
let height = state.height;
|
||||
if (state.aspectRatio.isLocked) {
|
||||
// When locked, we calculate the new height based on the aspect ratio
|
||||
height = roundToMultiple(width / state.aspectRatio.value, 8);
|
||||
} else {
|
||||
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
||||
state.aspectRatio.isLocked = false;
|
||||
state.aspectRatio.id = 'Free';
|
||||
state.aspectRatio.value = width / height;
|
||||
}
|
||||
state.width = width;
|
||||
state.height = height;
|
||||
},
|
||||
heightChanged: (
|
||||
state,
|
||||
action: PayloadAction<GenerationState['height']>
|
||||
) => {
|
||||
const height = action.payload;
|
||||
let width = state.width;
|
||||
if (state.aspectRatio.isLocked) {
|
||||
// When locked, we calculate the new width based on the aspect ratio
|
||||
width = roundToMultiple(height * state.aspectRatio.value, 8);
|
||||
} else {
|
||||
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
|
||||
state.aspectRatio.isLocked = false;
|
||||
state.aspectRatio.id = 'Free';
|
||||
state.aspectRatio.value = width / height;
|
||||
}
|
||||
state.height = height;
|
||||
state.width = width;
|
||||
},
|
||||
isLockedToggled: (state) => {
|
||||
state.aspectRatio.isLocked = !state.aspectRatio.isLocked;
|
||||
},
|
||||
sizeReset: (state, action: PayloadAction<number>) => {
|
||||
state.aspectRatio = cloneDeep(initialGenerationState.aspectRatio);
|
||||
widthChanged: (state, action: PayloadAction<number>) => {
|
||||
state.width = action.payload;
|
||||
},
|
||||
heightChanged: (state, action: PayloadAction<number>) => {
|
||||
state.height = action.payload;
|
||||
},
|
||||
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
||||
state.aspectRatio = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(configChanged, (state, action) => {
|
||||
@ -436,13 +300,10 @@ export const {
|
||||
setSeamlessYAxis,
|
||||
setClipSkip,
|
||||
shouldUseCpuNoiseChanged,
|
||||
aspectRatioSelected,
|
||||
dimensionsSwapped,
|
||||
vaePrecisionChanged,
|
||||
aspectRatioChanged,
|
||||
widthChanged,
|
||||
heightChanged,
|
||||
isLockedToggled,
|
||||
sizeReset,
|
||||
vaePrecisionChanged,
|
||||
} = generationSlice.actions;
|
||||
|
||||
export default generationSlice.reducer;
|
||||
|
60
invokeai/frontend/web/src/features/parameters/store/types.ts
Normal file
60
invokeai/frontend/web/src/features/parameters/store/types.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import type {
|
||||
ParameterCanvasCoherenceMode,
|
||||
ParameterCFGRescaleMultiplier,
|
||||
ParameterCFGScale,
|
||||
ParameterHeight,
|
||||
ParameterMaskBlurMethod,
|
||||
ParameterModel,
|
||||
ParameterNegativePrompt,
|
||||
ParameterPositivePrompt,
|
||||
ParameterPrecision,
|
||||
ParameterScheduler,
|
||||
ParameterSeed,
|
||||
ParameterSteps,
|
||||
ParameterStrength,
|
||||
ParameterVAEModel,
|
||||
ParameterWidth,
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
|
||||
export interface GenerationState {
|
||||
cfgScale: ParameterCFGScale;
|
||||
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
|
||||
height: ParameterHeight;
|
||||
img2imgStrength: ParameterStrength;
|
||||
infillMethod: string;
|
||||
initialImage?: { imageName: string; width: number; height: number };
|
||||
iterations: number;
|
||||
perlin: number;
|
||||
positivePrompt: ParameterPositivePrompt;
|
||||
negativePrompt: ParameterNegativePrompt;
|
||||
scheduler: ParameterScheduler;
|
||||
maskBlur: number;
|
||||
maskBlurMethod: ParameterMaskBlurMethod;
|
||||
canvasCoherenceMode: ParameterCanvasCoherenceMode;
|
||||
canvasCoherenceSteps: number;
|
||||
canvasCoherenceStrength: ParameterStrength;
|
||||
seed: ParameterSeed;
|
||||
seedWeights: string;
|
||||
shouldFitToWidthHeight: boolean;
|
||||
shouldGenerateVariations: boolean;
|
||||
shouldRandomizeSeed: boolean;
|
||||
steps: ParameterSteps;
|
||||
threshold: number;
|
||||
infillTileSize: number;
|
||||
infillPatchmatchDownscaleSize: number;
|
||||
variationAmount: number;
|
||||
width: ParameterWidth;
|
||||
shouldUseSymmetry: boolean;
|
||||
horizontalSymmetrySteps: number;
|
||||
verticalSymmetrySteps: number;
|
||||
model: ParameterModel | null;
|
||||
vae: ParameterVAEModel | null;
|
||||
vaePrecision: ParameterPrecision;
|
||||
seamlessXAxis: boolean;
|
||||
seamlessYAxis: boolean;
|
||||
clipSkip: number;
|
||||
shouldUseCpuNoise: boolean;
|
||||
shouldShowAdvancedOptions: boolean;
|
||||
aspectRatio: AspectRatioState;
|
||||
}
|
@ -7,25 +7,21 @@ import type { InvLabelProps } from 'common/components/InvControl/types';
|
||||
import { InvExpander } from 'common/components/InvExpander/InvExpander';
|
||||
import { InvSingleAccordion } from 'common/components/InvSingleAccordion/InvSingleAccordion';
|
||||
import { HrfSettings } from 'features/hrf/components/HrfSettings';
|
||||
import ParamBoundingBoxHeight from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight';
|
||||
import ParamBoundingBoxWidth from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth';
|
||||
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
|
||||
import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
|
||||
import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth';
|
||||
import { ParamHeight } from 'features/parameters/components/Core/ParamHeight';
|
||||
import { ParamWidth } from 'features/parameters/components/Core/ParamWidth';
|
||||
import { AspectRatioPreviewWrapper } from 'features/parameters/components/ImageSize/AspectRatioPreviewWrapper';
|
||||
import { AspectRatioSelect } from 'features/parameters/components/ImageSize/AspectRatioSelect';
|
||||
import ImageToImageFit from 'features/parameters/components/ImageToImage/ImageToImageFit';
|
||||
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
||||
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
|
||||
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
|
||||
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
|
||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ImageSizeCanvas } from './ImageSizeCanvas';
|
||||
import { ImageSizeLinear } from './ImageSizeLinear';
|
||||
|
||||
const selector = createMemoizedSelector(
|
||||
[stateSelector, activeTabNameSelector],
|
||||
({ generation, hrf }, activeTabName) => {
|
||||
@ -42,10 +38,6 @@ const selector = createMemoizedSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const labelProps: InvLabelProps = {
|
||||
minW: 12,
|
||||
};
|
||||
|
||||
const scalingLabelProps: InvLabelProps = {
|
||||
minW: '4.5rem',
|
||||
};
|
||||
@ -61,17 +53,11 @@ export const ImageSettingsAccordion = memo(() => {
|
||||
badges={badges}
|
||||
>
|
||||
<Flex px={4} pt={4} w="full" h="full" flexDir="column">
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
<InvControlGroup labelProps={labelProps}>
|
||||
<AspectRatioSelect />
|
||||
<WidthHeight activeTabName={activeTabName} />
|
||||
</InvControlGroup>
|
||||
</Flex>
|
||||
<Flex w="98px" h="98px" flexShrink={0} flexGrow={0}>
|
||||
<AspectRatioPreviewWrapper />
|
||||
</Flex>
|
||||
</Flex>
|
||||
{activeTabName === 'unifiedCanvas' ? (
|
||||
<ImageSizeCanvas />
|
||||
) : (
|
||||
<ImageSizeLinear />
|
||||
)}
|
||||
<InvExpander>
|
||||
<Flex gap={4} pb={4} flexDir="column">
|
||||
<Flex gap={4}>
|
||||
@ -79,10 +65,10 @@ export const ImageSettingsAccordion = memo(() => {
|
||||
<ParamSeedShuffle />
|
||||
<ParamSeedRandomize />
|
||||
</Flex>
|
||||
{activeTabName === 'txt2img' && <HrfSettings />}
|
||||
{activeTabName === 'img2img' && <ImageToImageFit />}
|
||||
{(activeTabName === 'img2img' ||
|
||||
activeTabName === 'unifiedCanvas') && <ImageToImageStrength />}
|
||||
{activeTabName === 'txt2img' && <HrfSettings />}
|
||||
{activeTabName === 'unifiedCanvas' && (
|
||||
<>
|
||||
<ParamScaleBeforeProcessing />
|
||||
@ -100,23 +86,3 @@ export const ImageSettingsAccordion = memo(() => {
|
||||
});
|
||||
|
||||
ImageSettingsAccordion.displayName = 'ImageSettingsAccordion';
|
||||
|
||||
const WidthHeight = memo((props: { activeTabName: InvokeTabName }) => {
|
||||
if (props.activeTabName === 'unifiedCanvas') {
|
||||
return (
|
||||
<>
|
||||
<ParamBoundingBoxWidth />
|
||||
<ParamBoundingBoxHeight />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ParamWidth />
|
||||
<ParamHeight />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
WidthHeight.displayName = 'WidthHeight';
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
aspectRatioChanged,
|
||||
setBoundingBoxDimensions,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
import ParamBoundingBoxHeight from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight';
|
||||
import ParamBoundingBoxWidth from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth';
|
||||
import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize';
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const ImageSizeCanvas = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { width, height } = useAppSelector(
|
||||
(state) => state.canvas.boundingBoxDimensions
|
||||
);
|
||||
const aspectRatioState = useAppSelector(
|
||||
(state) => state.canvas.aspectRatio
|
||||
);
|
||||
|
||||
const onChangeWidth = useCallback(
|
||||
(width: number) => {
|
||||
dispatch(setBoundingBoxDimensions({ width }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onChangeHeight = useCallback(
|
||||
(height: number) => {
|
||||
dispatch(setBoundingBoxDimensions({ height }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onChangeAspectRatioState = useCallback(
|
||||
(aspectRatioState: AspectRatioState) => {
|
||||
dispatch(aspectRatioChanged(aspectRatioState));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageSize
|
||||
width={width}
|
||||
height={height}
|
||||
aspectRatioState={aspectRatioState}
|
||||
heightComponent={<ParamBoundingBoxHeight />}
|
||||
widthComponent={<ParamBoundingBoxWidth />}
|
||||
onChangeAspectRatioState={onChangeAspectRatioState}
|
||||
onChangeWidth={onChangeWidth}
|
||||
onChangeHeight={onChangeHeight}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ImageSizeCanvas.displayName = 'ImageSizeCanvas';
|
@ -0,0 +1,56 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { ParamHeight } from 'features/parameters/components/Core/ParamHeight';
|
||||
import { ParamWidth } from 'features/parameters/components/Core/ParamWidth';
|
||||
import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize';
|
||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||
import {
|
||||
aspectRatioChanged,
|
||||
heightChanged,
|
||||
widthChanged,
|
||||
} from 'features/parameters/store/generationSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const ImageSizeLinear = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const width = useAppSelector((state) => state.generation.width);
|
||||
const height = useAppSelector((state) => state.generation.height);
|
||||
const aspectRatioState = useAppSelector(
|
||||
(state) => state.generation.aspectRatio
|
||||
);
|
||||
|
||||
const onChangeWidth = useCallback(
|
||||
(width: number) => {
|
||||
dispatch(widthChanged(width));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onChangeHeight = useCallback(
|
||||
(height: number) => {
|
||||
dispatch(heightChanged(height));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onChangeAspectRatioState = useCallback(
|
||||
(aspectRatioState: AspectRatioState) => {
|
||||
dispatch(aspectRatioChanged(aspectRatioState));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageSize
|
||||
width={width}
|
||||
height={height}
|
||||
aspectRatioState={aspectRatioState}
|
||||
heightComponent={<ParamHeight />}
|
||||
widthComponent={<ParamWidth />}
|
||||
onChangeAspectRatioState={onChangeAspectRatioState}
|
||||
onChangeWidth={onChangeWidth}
|
||||
onChangeHeight={onChangeHeight}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ImageSizeLinear.displayName = 'ImageSizeLinear';
|
Loading…
Reference in New Issue
Block a user