mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
WIP refactor to unified canvas
This commit is contained in:
parent
c0ad1b3469
commit
cfb87bc116
@ -38,7 +38,7 @@ import {
|
||||
requestSystemConfig,
|
||||
} from './actions';
|
||||
import {
|
||||
addImageToOutpainting,
|
||||
addImageToStagingArea,
|
||||
setImageToInpaint,
|
||||
} from 'features/canvas/canvasSlice';
|
||||
import { tabMap } from 'features/tabs/InvokeTabs';
|
||||
@ -126,12 +126,17 @@ const makeSocketIOListeners = (
|
||||
) {
|
||||
newImage.category = 'temp';
|
||||
const { boundingBox } = data;
|
||||
dispatch(
|
||||
addImageToOutpainting({
|
||||
image: newImage,
|
||||
boundingBox,
|
||||
})
|
||||
);
|
||||
|
||||
if (generationMode === 'inpainting') {
|
||||
dispatch(setImageToInpaint(newImage));
|
||||
} else {
|
||||
dispatch(
|
||||
addImageToStagingArea({
|
||||
image: newImage,
|
||||
boundingBox,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldLoopback) {
|
||||
|
@ -2,13 +2,17 @@ import { GroupConfig } from 'konva/lib/Group';
|
||||
import { Group, Line } from 'react-konva';
|
||||
import { useAppSelector } from 'app/store';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { currentCanvasSelector, isCanvasMaskLine } from './canvasSlice';
|
||||
import {
|
||||
canvasClipSelector,
|
||||
currentCanvasSelector,
|
||||
isCanvasMaskLine,
|
||||
} from './canvasSlice';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const canvasLinesSelector = createSelector(
|
||||
currentCanvasSelector,
|
||||
(currentCanvas) => {
|
||||
return currentCanvas.layerState.objects;
|
||||
[currentCanvasSelector, canvasClipSelector],
|
||||
(currentCanvas, canvasClip) => {
|
||||
return { objects: currentCanvas.layerState.objects, canvasClip };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
@ -26,10 +30,10 @@ type InpaintingCanvasLinesProps = GroupConfig;
|
||||
*/
|
||||
const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
|
||||
const { ...rest } = props;
|
||||
const objects = useAppSelector(canvasLinesSelector);
|
||||
const { objects, canvasClip } = useAppSelector(canvasLinesSelector);
|
||||
|
||||
return (
|
||||
<Group listening={false} {...rest}>
|
||||
<Group listening={false} {...rest} {...canvasClip}>
|
||||
{objects.filter(isCanvasMaskLine).map((line, i) => (
|
||||
<Line
|
||||
key={i}
|
||||
|
@ -3,6 +3,9 @@ import { useAppSelector } from 'app/store';
|
||||
import _ from 'lodash';
|
||||
import { Group, Line } from 'react-konva';
|
||||
import {
|
||||
baseCanvasImageSelector,
|
||||
canvasClipSelector,
|
||||
canvasModeSelector,
|
||||
currentCanvasSelector,
|
||||
isCanvasBaseImage,
|
||||
isCanvasBaseLine,
|
||||
@ -11,11 +14,13 @@ import IAICanvasImage from './IAICanvasImage';
|
||||
import { rgbaColorToString } from './util/colorToString';
|
||||
|
||||
const selector = createSelector(
|
||||
[currentCanvasSelector],
|
||||
(currentCanvas) => {
|
||||
[currentCanvasSelector, canvasClipSelector],
|
||||
(currentCanvas, canvasClip) => {
|
||||
const { objects } = currentCanvas.layerState;
|
||||
|
||||
return {
|
||||
objects,
|
||||
canvasClip,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -26,12 +31,12 @@ const selector = createSelector(
|
||||
);
|
||||
|
||||
const IAICanvasObjectRenderer = () => {
|
||||
const { objects } = useAppSelector(selector);
|
||||
const { objects, canvasClip } = useAppSelector(selector);
|
||||
|
||||
if (!objects) return null;
|
||||
|
||||
return (
|
||||
<Group name="outpainting-objects" listening={false}>
|
||||
<Group name="outpainting-objects" listening={false} {...canvasClip}>
|
||||
{objects.map((obj, i) => {
|
||||
if (isCanvasBaseImage(obj)) {
|
||||
return (
|
||||
|
@ -4,15 +4,18 @@ import {
|
||||
currentCanvasSelector,
|
||||
isStagingSelector,
|
||||
resetCanvas,
|
||||
resetCanvasView,
|
||||
setCanvasMode,
|
||||
setTool,
|
||||
} from './canvasSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||
import { RootState, useAppDispatch, useAppSelector } from 'app/store';
|
||||
import _ from 'lodash';
|
||||
import { canvasImageLayerRef } from './IAICanvas';
|
||||
import { canvasImageLayerRef, stageRef } from './IAICanvas';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import {
|
||||
FaArrowsAlt,
|
||||
FaCopy,
|
||||
FaCrosshairs,
|
||||
FaDownload,
|
||||
FaLayerGroup,
|
||||
FaSave,
|
||||
@ -26,15 +29,21 @@ import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover';
|
||||
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
|
||||
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
|
||||
import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas';
|
||||
import IAICheckbox from 'common/components/IAICheckbox';
|
||||
|
||||
export const canvasControlsSelector = createSelector(
|
||||
[currentCanvasSelector, isStagingSelector],
|
||||
(currentCanvas, isStaging) => {
|
||||
[
|
||||
(state: RootState) => state.canvas,
|
||||
currentCanvasSelector,
|
||||
isStagingSelector,
|
||||
],
|
||||
(canvas, currentCanvas, isStaging) => {
|
||||
const { tool } = currentCanvas;
|
||||
|
||||
const { mode } = canvas;
|
||||
return {
|
||||
tool,
|
||||
isStaging,
|
||||
mode,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -46,7 +55,7 @@ export const canvasControlsSelector = createSelector(
|
||||
|
||||
const IAICanvasOutpaintingControls = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { tool, isStaging } = useAppSelector(canvasControlsSelector);
|
||||
const { tool, isStaging, mode } = useAppSelector(canvasControlsSelector);
|
||||
|
||||
return (
|
||||
<div className="inpainting-settings">
|
||||
@ -110,6 +119,20 @@ const IAICanvasOutpaintingControls = () => {
|
||||
tooltip="Upload"
|
||||
icon={<FaUpload />}
|
||||
/>
|
||||
<IAIIconButton
|
||||
aria-label="Reset Canvas View"
|
||||
tooltip="Reset Canvas View"
|
||||
icon={<FaCrosshairs />}
|
||||
onClick={() => {
|
||||
if (!stageRef.current || !canvasImageLayerRef.current) return;
|
||||
const clientRect = canvasImageLayerRef.current.getClientRect({skipTransform: true});
|
||||
dispatch(
|
||||
resetCanvasView({
|
||||
clientRect,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IAIIconButton
|
||||
aria-label="Reset Canvas"
|
||||
tooltip="Reset Canvas"
|
||||
@ -117,6 +140,15 @@ const IAICanvasOutpaintingControls = () => {
|
||||
onClick={() => dispatch(resetCanvas())}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<IAICheckbox
|
||||
label={'inpainting'}
|
||||
isChecked={mode === 'inpainting'}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setCanvasMode(e.target.checked ? 'inpainting' : 'outpainting')
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,66 +4,107 @@ import { RootState, useAppDispatch, useAppSelector } from 'app/store';
|
||||
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||
import {
|
||||
baseCanvasImageSelector,
|
||||
CanvasState,
|
||||
setStageDimensions,
|
||||
setStageScale,
|
||||
currentCanvasSelector,
|
||||
initializeCanvas,
|
||||
resizeCanvas,
|
||||
setDoesCanvasNeedScaling,
|
||||
} from 'features/canvas/canvasSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
const canvasResizerSelector = createSelector(
|
||||
(state: RootState) => state.canvas,
|
||||
currentCanvasSelector,
|
||||
baseCanvasImageSelector,
|
||||
activeTabNameSelector,
|
||||
(canvas: CanvasState, baseCanvasImage, activeTabName) => {
|
||||
const { doesCanvasNeedScaling } = canvas;
|
||||
|
||||
(canvas, currentCanvas, baseCanvasImage, activeTabName) => {
|
||||
const { doesCanvasNeedScaling, mode, isCanvasInitialized } = canvas;
|
||||
return {
|
||||
doesCanvasNeedScaling,
|
||||
mode,
|
||||
activeTabName,
|
||||
baseCanvasImage,
|
||||
isCanvasInitialized,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const IAICanvasResizer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { doesCanvasNeedScaling, activeTabName, baseCanvasImage } =
|
||||
useAppSelector(canvasResizerSelector);
|
||||
const {
|
||||
doesCanvasNeedScaling,
|
||||
mode,
|
||||
activeTabName,
|
||||
baseCanvasImage,
|
||||
isCanvasInitialized,
|
||||
} = useAppSelector(canvasResizerSelector);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.setTimeout(() => {
|
||||
if (!ref.current) return;
|
||||
const { width: imageWidth, height: imageHeight } = baseCanvasImage?.image
|
||||
? baseCanvasImage.image
|
||||
: { width: 512, height: 512 };
|
||||
|
||||
const { clientWidth, clientHeight } = ref.current;
|
||||
|
||||
const scale = Math.min(
|
||||
1,
|
||||
Math.min(clientWidth / imageWidth, clientHeight / imageHeight)
|
||||
);
|
||||
if (!baseCanvasImage?.image) return;
|
||||
|
||||
dispatch(setStageScale(scale));
|
||||
const { width: imageWidth, height: imageHeight } = baseCanvasImage.image;
|
||||
|
||||
if (activeTabName === 'inpainting') {
|
||||
if (!isCanvasInitialized) {
|
||||
dispatch(
|
||||
setStageDimensions({
|
||||
width: Math.floor(imageWidth * scale),
|
||||
height: Math.floor(imageHeight * scale),
|
||||
initializeCanvas({
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
})
|
||||
);
|
||||
} else if (activeTabName === 'outpainting') {
|
||||
} else {
|
||||
dispatch(
|
||||
setStageDimensions({
|
||||
width: Math.floor(clientWidth),
|
||||
height: Math.floor(clientHeight),
|
||||
resizeCanvas({
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(setDoesCanvasNeedScaling(false));
|
||||
// }
|
||||
// if ((activeTabName === 'inpainting') && baseCanvasImage?.image) {
|
||||
// const { width: imageWidth, height: imageHeight } =
|
||||
// baseCanvasImage.image;
|
||||
|
||||
// const scale = Math.min(
|
||||
// 1,
|
||||
// Math.min(clientWidth / imageWidth, clientHeight / imageHeight)
|
||||
// );
|
||||
|
||||
// dispatch(setStageScale(scale));
|
||||
|
||||
// dispatch(
|
||||
// setStageDimensions({
|
||||
// width: Math.floor(imageWidth * scale),
|
||||
// height: Math.floor(imageHeight * scale),
|
||||
// })
|
||||
// );
|
||||
// dispatch(setDoesCanvasNeedScaling(false));
|
||||
// } else if (activeTabName === 'outpainting') {
|
||||
// dispatch(
|
||||
// setStageDimensions({
|
||||
// width: Math.floor(clientWidth),
|
||||
// height: Math.floor(clientHeight),
|
||||
// })
|
||||
// );
|
||||
// dispatch(setDoesCanvasNeedScaling(false));
|
||||
// }
|
||||
}, 0);
|
||||
}, [dispatch, baseCanvasImage, doesCanvasNeedScaling, activeTabName]);
|
||||
}, [
|
||||
dispatch,
|
||||
baseCanvasImage,
|
||||
doesCanvasNeedScaling,
|
||||
activeTabName,
|
||||
isCanvasInitialized,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="inpainting-canvas-area">
|
||||
|
@ -2,101 +2,93 @@ import * as InvokeAI from 'app/invokeai';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { CanvasState, Dimensions, initialLayerState } from './canvasSlice';
|
||||
import { Vector2d } from 'konva/lib/types';
|
||||
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import {
|
||||
roundDownToMultiple,
|
||||
roundToMultiple,
|
||||
} from 'common/util/roundDownToMultiple';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const setImageToInpaint_reducer = (
|
||||
// export const setInitialInpaintingImage = (
|
||||
// state: CanvasState,
|
||||
// image: InvokeAI.Image
|
||||
// // action: PayloadAction<InvokeAI.Image>
|
||||
// ) => {
|
||||
// const { width: canvasWidth, height: canvasHeight } =
|
||||
// state.inpainting.stageDimensions;
|
||||
// const { width, height } = state.inpainting.boundingBoxDimensions;
|
||||
// const { x, y } = state.inpainting.boundingBoxCoordinates;
|
||||
|
||||
// const maxWidth = Math.min(image.width, canvasWidth);
|
||||
// const maxHeight = Math.min(image.height, canvasHeight);
|
||||
|
||||
// const newCoordinates: Vector2d = { x, y };
|
||||
// const newDimensions: Dimensions = { width, height };
|
||||
|
||||
// if (width + x > maxWidth) {
|
||||
// // Bounding box at least needs to be translated
|
||||
// if (width > maxWidth) {
|
||||
// // Bounding box also needs to be resized
|
||||
// newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
||||
// }
|
||||
// newCoordinates.x = maxWidth - newDimensions.width;
|
||||
// }
|
||||
|
||||
// if (height + y > maxHeight) {
|
||||
// // Bounding box at least needs to be translated
|
||||
// if (height > maxHeight) {
|
||||
// // Bounding box also needs to be resized
|
||||
// newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
||||
// }
|
||||
// newCoordinates.y = maxHeight - newDimensions.height;
|
||||
// }
|
||||
|
||||
// state.inpainting.boundingBoxDimensions = newDimensions;
|
||||
// state.inpainting.boundingBoxCoordinates = newCoordinates;
|
||||
|
||||
// state.inpainting.pastLayerStates.push(state.inpainting.layerState);
|
||||
|
||||
// state.inpainting.layerState = {
|
||||
// ...initialLayerState,
|
||||
// objects: [
|
||||
// {
|
||||
// kind: 'image',
|
||||
// layer: 'base',
|
||||
// x: 0,
|
||||
// y: 0,
|
||||
// width: image.width,
|
||||
// height: image.height,
|
||||
// image: image,
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
|
||||
// state.outpainting.futureLayerStates = [];
|
||||
// state.doesCanvasNeedScaling = true;
|
||||
// };
|
||||
|
||||
export const setInitialCanvasImage = (
|
||||
state: CanvasState,
|
||||
image: InvokeAI.Image
|
||||
// action: PayloadAction<InvokeAI.Image>
|
||||
) => {
|
||||
const { width: canvasWidth, height: canvasHeight } =
|
||||
state.inpainting.stageDimensions;
|
||||
const { width, height } = state.inpainting.boundingBoxDimensions;
|
||||
const { x, y } = state.inpainting.boundingBoxCoordinates;
|
||||
|
||||
const maxWidth = Math.min(image.width, canvasWidth);
|
||||
const maxHeight = Math.min(image.height, canvasHeight);
|
||||
|
||||
const newCoordinates: Vector2d = { x, y };
|
||||
const newDimensions: Dimensions = { width, height };
|
||||
|
||||
if (width + x > maxWidth) {
|
||||
// Bounding box at least needs to be translated
|
||||
if (width > maxWidth) {
|
||||
// Bounding box also needs to be resized
|
||||
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
||||
}
|
||||
newCoordinates.x = maxWidth - newDimensions.width;
|
||||
}
|
||||
|
||||
if (height + y > maxHeight) {
|
||||
// Bounding box at least needs to be translated
|
||||
if (height > maxHeight) {
|
||||
// Bounding box also needs to be resized
|
||||
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
||||
}
|
||||
newCoordinates.y = maxHeight - newDimensions.height;
|
||||
}
|
||||
|
||||
state.inpainting.boundingBoxDimensions = newDimensions;
|
||||
state.inpainting.boundingBoxCoordinates = newCoordinates;
|
||||
|
||||
state.inpainting.pastLayerStates.push(state.inpainting.layerState);
|
||||
|
||||
state.inpainting.layerState = {
|
||||
...initialLayerState,
|
||||
objects: [
|
||||
{
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
image: image,
|
||||
},
|
||||
],
|
||||
const newBoundingBoxDimensions = {
|
||||
width: roundDownToMultiple(_.clamp(image.width, 64, 512), 64),
|
||||
height: roundDownToMultiple(_.clamp(image.height, 64, 512), 64),
|
||||
};
|
||||
|
||||
state.outpainting.futureLayerStates = [];
|
||||
state.doesCanvasNeedScaling = true;
|
||||
};
|
||||
const newBoundingBoxCoordinates = {
|
||||
x: roundToMultiple(
|
||||
image.width / 2 - newBoundingBoxDimensions.width / 2,
|
||||
64
|
||||
),
|
||||
y: roundToMultiple(
|
||||
image.height / 2 - newBoundingBoxDimensions.height / 2,
|
||||
64
|
||||
),
|
||||
};
|
||||
|
||||
export const setImageToOutpaint_reducer = (
|
||||
state: CanvasState,
|
||||
image: InvokeAI.Image
|
||||
) => {
|
||||
const { width: canvasWidth, height: canvasHeight } =
|
||||
state.outpainting.stageDimensions;
|
||||
const { width, height } = state.outpainting.boundingBoxDimensions;
|
||||
const { x, y } = state.outpainting.boundingBoxCoordinates;
|
||||
state.outpainting.boundingBoxDimensions = newBoundingBoxDimensions;
|
||||
|
||||
const maxWidth = Math.min(image.width, canvasWidth);
|
||||
const maxHeight = Math.min(image.height, canvasHeight);
|
||||
|
||||
const newCoordinates: Vector2d = { x, y };
|
||||
const newDimensions: Dimensions = { width, height };
|
||||
|
||||
if (width + x > maxWidth) {
|
||||
// Bounding box at least needs to be translated
|
||||
if (width > maxWidth) {
|
||||
// Bounding box also needs to be resized
|
||||
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
||||
}
|
||||
newCoordinates.x = maxWidth - newDimensions.width;
|
||||
}
|
||||
|
||||
if (height + y > maxHeight) {
|
||||
// Bounding box at least needs to be translated
|
||||
if (height > maxHeight) {
|
||||
// Bounding box also needs to be resized
|
||||
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
||||
}
|
||||
newCoordinates.y = maxHeight - newDimensions.height;
|
||||
}
|
||||
|
||||
state.outpainting.boundingBoxDimensions = newDimensions;
|
||||
state.outpainting.boundingBoxCoordinates = newCoordinates;
|
||||
state.outpainting.boundingBoxCoordinates = newBoundingBoxCoordinates;
|
||||
|
||||
state.outpainting.pastLayerStates.push(state.outpainting.layerState);
|
||||
state.outpainting.layerState = {
|
||||
@ -114,5 +106,7 @@ export const setImageToOutpaint_reducer = (
|
||||
],
|
||||
};
|
||||
state.outpainting.futureLayerStates = [];
|
||||
|
||||
state.isCanvasInitialized = false;
|
||||
state.doesCanvasNeedScaling = true;
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
createAsyncThunk,
|
||||
createSelector,
|
||||
createSlice,
|
||||
current,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
@ -17,49 +18,50 @@ import { tabMap } from 'features/tabs/InvokeTabs';
|
||||
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||
import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas';
|
||||
import { uploadImage } from 'features/gallery/util/uploadImage';
|
||||
import {
|
||||
setImageToInpaint_reducer,
|
||||
setImageToOutpaint_reducer,
|
||||
} from './canvasReducers';
|
||||
import { setInitialCanvasImage } from './canvasReducers';
|
||||
import calculateScale from './util/calculateScale';
|
||||
import calculateCoordinates from './util/calculateCoordinates';
|
||||
|
||||
export interface GenericCanvasState {
|
||||
tool: CanvasTool;
|
||||
brushSize: number;
|
||||
brushColor: RgbaColor;
|
||||
eraserSize: number;
|
||||
maskColor: RgbaColor;
|
||||
cursorPosition: Vector2d | null;
|
||||
stageDimensions: Dimensions;
|
||||
stageCoordinates: Vector2d;
|
||||
boundingBoxDimensions: Dimensions;
|
||||
boundingBoxCoordinates: Vector2d;
|
||||
boundingBoxDimensions: Dimensions;
|
||||
boundingBoxPreviewFill: RgbaColor;
|
||||
shouldShowBoundingBox: boolean;
|
||||
shouldDarkenOutsideBoundingBox: boolean;
|
||||
isMaskEnabled: boolean;
|
||||
shouldPreserveMaskedArea: boolean;
|
||||
shouldShowCheckboardTransparency: boolean;
|
||||
shouldShowBrush: boolean;
|
||||
shouldShowBrushPreview: boolean;
|
||||
stageScale: number;
|
||||
isDrawing: boolean;
|
||||
isTransformingBoundingBox: boolean;
|
||||
isMouseOverBoundingBox: boolean;
|
||||
isMovingBoundingBox: boolean;
|
||||
isMovingStage: boolean;
|
||||
shouldUseInpaintReplace: boolean;
|
||||
brushColor: RgbaColor;
|
||||
brushSize: number;
|
||||
cursorPosition: Vector2d | null;
|
||||
eraserSize: number;
|
||||
futureLayerStates: CanvasLayerState[];
|
||||
inpaintReplace: number;
|
||||
shouldLockBoundingBox: boolean;
|
||||
intermediateImage?: InvokeAI.Image;
|
||||
isDrawing: boolean;
|
||||
isMaskEnabled: boolean;
|
||||
isMouseOverBoundingBox: boolean;
|
||||
isMoveBoundingBoxKeyHeld: boolean;
|
||||
isMoveStageKeyHeld: boolean;
|
||||
intermediateImage?: InvokeAI.Image;
|
||||
shouldShowIntermediates: boolean;
|
||||
maxHistory: number;
|
||||
isMovingBoundingBox: boolean;
|
||||
isMovingStage: boolean;
|
||||
isTransformingBoundingBox: boolean;
|
||||
layerState: CanvasLayerState;
|
||||
maskColor: RgbaColor;
|
||||
maxHistory: number;
|
||||
pastLayerStates: CanvasLayerState[];
|
||||
futureLayerStates: CanvasLayerState[];
|
||||
shouldDarkenOutsideBoundingBox: boolean;
|
||||
shouldLockBoundingBox: boolean;
|
||||
shouldPreserveMaskedArea: boolean;
|
||||
shouldShowBoundingBox: boolean;
|
||||
shouldShowBrush: boolean;
|
||||
shouldShowBrushPreview: boolean;
|
||||
shouldShowCheckboardTransparency: boolean;
|
||||
shouldShowIntermediates: boolean;
|
||||
shouldUseInpaintReplace: boolean;
|
||||
stageCoordinates: Vector2d;
|
||||
stageDimensions: Dimensions;
|
||||
stageScale: number;
|
||||
tool: CanvasTool;
|
||||
}
|
||||
|
||||
export type CanvasMode = 'inpainting' | 'outpainting';
|
||||
|
||||
export type CanvasLayer = 'base' | 'mask';
|
||||
|
||||
export type CanvasDrawingTool = 'brush' | 'eraser';
|
||||
@ -145,6 +147,8 @@ export interface CanvasState {
|
||||
currentCanvas: ValidCanvasName;
|
||||
inpainting: InpaintingCanvasState;
|
||||
outpainting: OutpaintingCanvasState;
|
||||
mode: CanvasMode;
|
||||
isCanvasInitialized: boolean;
|
||||
}
|
||||
|
||||
export const initialLayerState: CanvasLayerState = {
|
||||
@ -160,45 +164,47 @@ export const initialLayerState: CanvasLayerState = {
|
||||
};
|
||||
|
||||
const initialGenericCanvasState: GenericCanvasState = {
|
||||
tool: 'brush',
|
||||
boundingBoxCoordinates: { x: 0, y: 0 },
|
||||
boundingBoxDimensions: { width: 512, height: 512 },
|
||||
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 },
|
||||
brushColor: { r: 90, g: 90, b: 255, a: 1 },
|
||||
brushSize: 50,
|
||||
maskColor: { r: 255, g: 90, b: 90, a: 1 },
|
||||
eraserSize: 50,
|
||||
stageDimensions: { width: 0, height: 0 },
|
||||
stageCoordinates: { x: 0, y: 0 },
|
||||
boundingBoxDimensions: { width: 512, height: 512 },
|
||||
boundingBoxCoordinates: { x: 0, y: 0 },
|
||||
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 },
|
||||
shouldShowBoundingBox: true,
|
||||
shouldDarkenOutsideBoundingBox: false,
|
||||
cursorPosition: null,
|
||||
isMaskEnabled: true,
|
||||
shouldPreserveMaskedArea: false,
|
||||
shouldShowCheckboardTransparency: false,
|
||||
shouldShowBrush: true,
|
||||
shouldShowBrushPreview: false,
|
||||
isDrawing: false,
|
||||
isTransformingBoundingBox: false,
|
||||
isMouseOverBoundingBox: false,
|
||||
isMovingBoundingBox: false,
|
||||
stageScale: 1,
|
||||
shouldUseInpaintReplace: false,
|
||||
eraserSize: 50,
|
||||
futureLayerStates: [],
|
||||
inpaintReplace: 0.1,
|
||||
shouldLockBoundingBox: false,
|
||||
isDrawing: false,
|
||||
isMaskEnabled: true,
|
||||
isMouseOverBoundingBox: false,
|
||||
isMoveBoundingBoxKeyHeld: false,
|
||||
isMoveStageKeyHeld: false,
|
||||
shouldShowIntermediates: true,
|
||||
isMovingBoundingBox: false,
|
||||
isMovingStage: false,
|
||||
maxHistory: 128,
|
||||
isTransformingBoundingBox: false,
|
||||
layerState: initialLayerState,
|
||||
futureLayerStates: [],
|
||||
maskColor: { r: 255, g: 90, b: 90, a: 1 },
|
||||
maxHistory: 128,
|
||||
pastLayerStates: [],
|
||||
shouldDarkenOutsideBoundingBox: false,
|
||||
shouldLockBoundingBox: false,
|
||||
shouldPreserveMaskedArea: false,
|
||||
shouldShowBoundingBox: true,
|
||||
shouldShowBrush: true,
|
||||
shouldShowBrushPreview: false,
|
||||
shouldShowCheckboardTransparency: false,
|
||||
shouldShowIntermediates: true,
|
||||
shouldUseInpaintReplace: false,
|
||||
stageCoordinates: { x: 0, y: 0 },
|
||||
stageDimensions: { width: 0, height: 0 },
|
||||
stageScale: 1,
|
||||
tool: 'brush',
|
||||
};
|
||||
|
||||
const initialCanvasState: CanvasState = {
|
||||
currentCanvas: 'inpainting',
|
||||
doesCanvasNeedScaling: false,
|
||||
mode: 'outpainting',
|
||||
isCanvasInitialized: false,
|
||||
inpainting: {
|
||||
layer: 'mask',
|
||||
...initialGenericCanvasState,
|
||||
@ -293,10 +299,10 @@ export const canvasSlice = createSlice({
|
||||
// state.inpainting.imageToInpaint = undefined;
|
||||
},
|
||||
setImageToOutpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
setImageToOutpaint_reducer(state, action.payload);
|
||||
setInitialCanvasImage(state, action.payload);
|
||||
},
|
||||
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
setImageToInpaint_reducer(state, action.payload);
|
||||
setInitialCanvasImage(state, action.payload);
|
||||
},
|
||||
setStageDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||
state[state.currentCanvas].stageDimensions = action.payload;
|
||||
@ -412,7 +418,6 @@ export const canvasSlice = createSlice({
|
||||
},
|
||||
setStageScale: (state, action: PayloadAction<number>) => {
|
||||
state[state.currentCanvas].stageScale = action.payload;
|
||||
state.doesCanvasNeedScaling = false;
|
||||
},
|
||||
setShouldDarkenOutsideBoundingBox: (
|
||||
state,
|
||||
@ -462,7 +467,7 @@ export const canvasSlice = createSlice({
|
||||
setCurrentCanvas: (state, action: PayloadAction<ValidCanvasName>) => {
|
||||
state.currentCanvas = action.payload;
|
||||
},
|
||||
addImageToOutpainting: (
|
||||
addImageToStagingArea: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
boundingBox: IRect;
|
||||
@ -590,6 +595,99 @@ export const canvasSlice = createSlice({
|
||||
state[state.currentCanvas].layerState = initialLayerState;
|
||||
state[state.currentCanvas].futureLayerStates = [];
|
||||
},
|
||||
initializeCanvas: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
clientWidth: number;
|
||||
clientHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
}>
|
||||
) => {
|
||||
const { clientWidth, clientHeight, imageWidth, imageHeight } =
|
||||
action.payload;
|
||||
|
||||
const currentCanvas = state[state.currentCanvas];
|
||||
|
||||
const newScale = calculateScale(
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
imageWidth,
|
||||
imageHeight
|
||||
);
|
||||
|
||||
const newCoordinates = calculateCoordinates(
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
0,
|
||||
0,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
newScale
|
||||
);
|
||||
|
||||
currentCanvas.stageScale = newScale;
|
||||
currentCanvas.stageCoordinates = newCoordinates;
|
||||
|
||||
currentCanvas.stageDimensions = {
|
||||
width: Math.floor(clientWidth),
|
||||
height: Math.floor(clientHeight),
|
||||
};
|
||||
state.isCanvasInitialized = true;
|
||||
},
|
||||
resizeCanvas: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
clientWidth: number;
|
||||
clientHeight: number;
|
||||
}>
|
||||
) => {
|
||||
const { clientWidth, clientHeight } = action.payload;
|
||||
|
||||
const currentCanvas = state[state.currentCanvas];
|
||||
|
||||
currentCanvas.stageDimensions = {
|
||||
width: Math.floor(clientWidth),
|
||||
height: Math.floor(clientHeight),
|
||||
};
|
||||
},
|
||||
resetCanvasView: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
clientRect: IRect;
|
||||
}>
|
||||
) => {
|
||||
const { clientRect } = action.payload;
|
||||
const currentCanvas = state[state.currentCanvas];
|
||||
const baseCanvasImage =
|
||||
currentCanvas.layerState.objects.find(isCanvasBaseImage);
|
||||
|
||||
if (!baseCanvasImage) return;
|
||||
|
||||
const {
|
||||
stageDimensions: { width: stageWidth, height: stageHeight },
|
||||
} = currentCanvas;
|
||||
|
||||
const { x, y, width, height } = clientRect;
|
||||
|
||||
const newScale = calculateScale(stageWidth, stageHeight, width, height);
|
||||
const newCoordinates = calculateCoordinates(
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
newScale
|
||||
);
|
||||
|
||||
currentCanvas.stageScale = newScale;
|
||||
|
||||
currentCanvas.stageCoordinates = {
|
||||
x: stageWidth / 2 - (x + width / 2) * newScale,
|
||||
y: stageHeight / 2 - (y + height / 2) * newScale,
|
||||
};
|
||||
},
|
||||
nextStagingAreaImage: (state) => {
|
||||
const currentIndex =
|
||||
state.outpainting.layerState.stagingArea.selectedImageIndex;
|
||||
@ -630,6 +728,9 @@ export const canvasSlice = createSlice({
|
||||
|
||||
currentCanvas.futureLayerStates = [];
|
||||
},
|
||||
setCanvasMode: (state, action: PayloadAction<CanvasMode>) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
|
||||
@ -661,9 +762,9 @@ export const canvasSlice = createSlice({
|
||||
if (kind !== 'init') return;
|
||||
|
||||
if (activeTabName === 'inpainting') {
|
||||
setImageToInpaint_reducer(state, image);
|
||||
setInitialCanvasImage(state, image);
|
||||
} else if (activeTabName === 'outpainting') {
|
||||
setImageToOutpaint_reducer(state, image);
|
||||
setInitialCanvasImage(state, image);
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -712,7 +813,7 @@ export const {
|
||||
setIsMoveStageKeyHeld,
|
||||
setStageCoordinates,
|
||||
setCurrentCanvas,
|
||||
addImageToOutpainting,
|
||||
addImageToStagingArea,
|
||||
resetCanvas,
|
||||
setShouldShowGrid,
|
||||
setShouldSnapToGrid,
|
||||
@ -723,6 +824,10 @@ export const {
|
||||
prevStagingAreaImage,
|
||||
commitStagingAreaImage,
|
||||
discardStagedImages,
|
||||
setCanvasMode,
|
||||
initializeCanvas,
|
||||
resizeCanvas,
|
||||
resetCanvasView,
|
||||
} = canvasSlice.actions;
|
||||
|
||||
export default canvasSlice.reducer;
|
||||
@ -742,9 +847,26 @@ export const inpaintingCanvasSelector = (
|
||||
state: RootState
|
||||
): InpaintingCanvasState => state.canvas.inpainting;
|
||||
|
||||
export const canvasModeSelector = (state: RootState): CanvasMode =>
|
||||
state.canvas.mode;
|
||||
|
||||
export const baseCanvasImageSelector = createSelector(
|
||||
[currentCanvasSelector],
|
||||
(currentCanvas) => {
|
||||
return currentCanvas.layerState.objects.find(isCanvasBaseImage);
|
||||
}
|
||||
);
|
||||
|
||||
export const canvasClipSelector = createSelector(
|
||||
[canvasModeSelector, baseCanvasImageSelector],
|
||||
(canvasMode, baseCanvasImage) => {
|
||||
return canvasMode === 'inpainting'
|
||||
? {
|
||||
clipX: 0,
|
||||
clipY: 0,
|
||||
clipWidth: baseCanvasImage?.width,
|
||||
clipHeight: baseCanvasImage?.height,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
);
|
||||
|
17
frontend/src/features/canvas/util/calculateCoordinates.ts
Normal file
17
frontend/src/features/canvas/util/calculateCoordinates.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Vector2d } from 'konva/lib/types';
|
||||
|
||||
const calculateCoordinates = (
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
containerX: number,
|
||||
containerY: number,
|
||||
contentWidth: number,
|
||||
contentHeight: number,
|
||||
scale: number
|
||||
): Vector2d => {
|
||||
const x = containerWidth / 2 - (containerX + contentWidth / 2) * scale;
|
||||
const y = containerHeight / 2 - (containerY + contentHeight / 2) * scale;
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
export default calculateCoordinates;
|
14
frontend/src/features/canvas/util/calculateScale.ts
Normal file
14
frontend/src/features/canvas/util/calculateScale.ts
Normal file
@ -0,0 +1,14 @@
|
||||
const calculateScale = (
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
contentWidth: number,
|
||||
contentHeight: number,
|
||||
padding = 0.95
|
||||
): number => {
|
||||
const scaleX = (containerWidth * padding) / contentWidth;
|
||||
const scaleY = (containerHeight * padding) / contentHeight;
|
||||
const scaleFit = Math.min(1, Math.min(scaleX, scaleY));
|
||||
return scaleFit;
|
||||
};
|
||||
|
||||
export default calculateScale;
|
@ -44,6 +44,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
row-gap: 1rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
Loading…
Reference in New Issue
Block a user