WIP refactor to unified canvas

This commit is contained in:
psychedelicious 2022-11-16 10:02:08 +11:00 committed by blessedcoolant
parent c0ad1b3469
commit cfb87bc116
10 changed files with 434 additions and 199 deletions

View File

@ -38,7 +38,7 @@ import {
requestSystemConfig, requestSystemConfig,
} from './actions'; } from './actions';
import { import {
addImageToOutpainting, addImageToStagingArea,
setImageToInpaint, setImageToInpaint,
} from 'features/canvas/canvasSlice'; } from 'features/canvas/canvasSlice';
import { tabMap } from 'features/tabs/InvokeTabs'; import { tabMap } from 'features/tabs/InvokeTabs';
@ -126,13 +126,18 @@ const makeSocketIOListeners = (
) { ) {
newImage.category = 'temp'; newImage.category = 'temp';
const { boundingBox } = data; const { boundingBox } = data;
if (generationMode === 'inpainting') {
dispatch(setImageToInpaint(newImage));
} else {
dispatch( dispatch(
addImageToOutpainting({ addImageToStagingArea({
image: newImage, image: newImage,
boundingBox, boundingBox,
}) })
); );
} }
}
if (shouldLoopback) { if (shouldLoopback) {
const activeTabName = tabMap[activeTab]; const activeTabName = tabMap[activeTab];

View File

@ -2,13 +2,17 @@ import { GroupConfig } from 'konva/lib/Group';
import { Group, Line } from 'react-konva'; import { Group, Line } from 'react-konva';
import { useAppSelector } from 'app/store'; import { useAppSelector } from 'app/store';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { currentCanvasSelector, isCanvasMaskLine } from './canvasSlice'; import {
canvasClipSelector,
currentCanvasSelector,
isCanvasMaskLine,
} from './canvasSlice';
import _ from 'lodash'; import _ from 'lodash';
export const canvasLinesSelector = createSelector( export const canvasLinesSelector = createSelector(
currentCanvasSelector, [currentCanvasSelector, canvasClipSelector],
(currentCanvas) => { (currentCanvas, canvasClip) => {
return currentCanvas.layerState.objects; return { objects: currentCanvas.layerState.objects, canvasClip };
}, },
{ {
memoizeOptions: { memoizeOptions: {
@ -26,10 +30,10 @@ type InpaintingCanvasLinesProps = GroupConfig;
*/ */
const IAICanvasLines = (props: InpaintingCanvasLinesProps) => { const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
const { ...rest } = props; const { ...rest } = props;
const objects = useAppSelector(canvasLinesSelector); const { objects, canvasClip } = useAppSelector(canvasLinesSelector);
return ( return (
<Group listening={false} {...rest}> <Group listening={false} {...rest} {...canvasClip}>
{objects.filter(isCanvasMaskLine).map((line, i) => ( {objects.filter(isCanvasMaskLine).map((line, i) => (
<Line <Line
key={i} key={i}

View File

@ -3,6 +3,9 @@ import { useAppSelector } from 'app/store';
import _ from 'lodash'; import _ from 'lodash';
import { Group, Line } from 'react-konva'; import { Group, Line } from 'react-konva';
import { import {
baseCanvasImageSelector,
canvasClipSelector,
canvasModeSelector,
currentCanvasSelector, currentCanvasSelector,
isCanvasBaseImage, isCanvasBaseImage,
isCanvasBaseLine, isCanvasBaseLine,
@ -11,11 +14,13 @@ import IAICanvasImage from './IAICanvasImage';
import { rgbaColorToString } from './util/colorToString'; import { rgbaColorToString } from './util/colorToString';
const selector = createSelector( const selector = createSelector(
[currentCanvasSelector], [currentCanvasSelector, canvasClipSelector],
(currentCanvas) => { (currentCanvas, canvasClip) => {
const { objects } = currentCanvas.layerState; const { objects } = currentCanvas.layerState;
return { return {
objects, objects,
canvasClip,
}; };
}, },
{ {
@ -26,12 +31,12 @@ const selector = createSelector(
); );
const IAICanvasObjectRenderer = () => { const IAICanvasObjectRenderer = () => {
const { objects } = useAppSelector(selector); const { objects, canvasClip } = useAppSelector(selector);
if (!objects) return null; if (!objects) return null;
return ( return (
<Group name="outpainting-objects" listening={false}> <Group name="outpainting-objects" listening={false} {...canvasClip}>
{objects.map((obj, i) => { {objects.map((obj, i) => {
if (isCanvasBaseImage(obj)) { if (isCanvasBaseImage(obj)) {
return ( return (

View File

@ -4,15 +4,18 @@ import {
currentCanvasSelector, currentCanvasSelector,
isStagingSelector, isStagingSelector,
resetCanvas, resetCanvas,
resetCanvasView,
setCanvasMode,
setTool, setTool,
} from './canvasSlice'; } from './canvasSlice';
import { useAppDispatch, useAppSelector } from 'app/store'; import { RootState, useAppDispatch, useAppSelector } from 'app/store';
import _ from 'lodash'; import _ from 'lodash';
import { canvasImageLayerRef } from './IAICanvas'; import { canvasImageLayerRef, stageRef } from './IAICanvas';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { import {
FaArrowsAlt, FaArrowsAlt,
FaCopy, FaCopy,
FaCrosshairs,
FaDownload, FaDownload,
FaLayerGroup, FaLayerGroup,
FaSave, FaSave,
@ -26,15 +29,21 @@ import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover';
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover'; import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover'; import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas'; import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas';
import IAICheckbox from 'common/components/IAICheckbox';
export const canvasControlsSelector = createSelector( export const canvasControlsSelector = createSelector(
[currentCanvasSelector, isStagingSelector], [
(currentCanvas, isStaging) => { (state: RootState) => state.canvas,
currentCanvasSelector,
isStagingSelector,
],
(canvas, currentCanvas, isStaging) => {
const { tool } = currentCanvas; const { tool } = currentCanvas;
const { mode } = canvas;
return { return {
tool, tool,
isStaging, isStaging,
mode,
}; };
}, },
{ {
@ -46,7 +55,7 @@ export const canvasControlsSelector = createSelector(
const IAICanvasOutpaintingControls = () => { const IAICanvasOutpaintingControls = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { tool, isStaging } = useAppSelector(canvasControlsSelector); const { tool, isStaging, mode } = useAppSelector(canvasControlsSelector);
return ( return (
<div className="inpainting-settings"> <div className="inpainting-settings">
@ -110,6 +119,20 @@ const IAICanvasOutpaintingControls = () => {
tooltip="Upload" tooltip="Upload"
icon={<FaUpload />} 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 <IAIIconButton
aria-label="Reset Canvas" aria-label="Reset Canvas"
tooltip="Reset Canvas" tooltip="Reset Canvas"
@ -117,6 +140,15 @@ const IAICanvasOutpaintingControls = () => {
onClick={() => dispatch(resetCanvas())} onClick={() => dispatch(resetCanvas())}
/> />
</ButtonGroup> </ButtonGroup>
<IAICheckbox
label={'inpainting'}
isChecked={mode === 'inpainting'}
onChange={(e) =>
dispatch(
setCanvasMode(e.target.checked ? 'inpainting' : 'outpainting')
)
}
/>
</div> </div>
); );
}; };

View File

@ -4,66 +4,107 @@ import { RootState, useAppDispatch, useAppSelector } from 'app/store';
import { activeTabNameSelector } from 'features/options/optionsSelectors'; import { activeTabNameSelector } from 'features/options/optionsSelectors';
import { import {
baseCanvasImageSelector, baseCanvasImageSelector,
CanvasState, currentCanvasSelector,
setStageDimensions, initializeCanvas,
setStageScale, resizeCanvas,
setDoesCanvasNeedScaling,
} from 'features/canvas/canvasSlice'; } from 'features/canvas/canvasSlice';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
const canvasResizerSelector = createSelector( const canvasResizerSelector = createSelector(
(state: RootState) => state.canvas, (state: RootState) => state.canvas,
currentCanvasSelector,
baseCanvasImageSelector, baseCanvasImageSelector,
activeTabNameSelector, activeTabNameSelector,
(canvas: CanvasState, baseCanvasImage, activeTabName) => { (canvas, currentCanvas, baseCanvasImage, activeTabName) => {
const { doesCanvasNeedScaling } = canvas; const { doesCanvasNeedScaling, mode, isCanvasInitialized } = canvas;
return { return {
doesCanvasNeedScaling, doesCanvasNeedScaling,
mode,
activeTabName, activeTabName,
baseCanvasImage, baseCanvasImage,
isCanvasInitialized,
}; };
} }
); );
const IAICanvasResizer = () => { const IAICanvasResizer = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { doesCanvasNeedScaling, activeTabName, baseCanvasImage } = const {
useAppSelector(canvasResizerSelector); doesCanvasNeedScaling,
mode,
activeTabName,
baseCanvasImage,
isCanvasInitialized,
} = useAppSelector(canvasResizerSelector);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
window.setTimeout(() => { window.setTimeout(() => {
if (!ref.current) return; if (!ref.current) return;
const { width: imageWidth, height: imageHeight } = baseCanvasImage?.image
? baseCanvasImage.image
: { width: 512, height: 512 };
const { clientWidth, clientHeight } = ref.current; const { clientWidth, clientHeight } = ref.current;
const scale = Math.min( if (!baseCanvasImage?.image) return;
1,
Math.min(clientWidth / imageWidth, clientHeight / imageHeight)
);
dispatch(setStageScale(scale)); const { width: imageWidth, height: imageHeight } = baseCanvasImage.image;
if (activeTabName === 'inpainting') { if (!isCanvasInitialized) {
dispatch( dispatch(
setStageDimensions({ initializeCanvas({
width: Math.floor(imageWidth * scale), clientWidth,
height: Math.floor(imageHeight * scale), clientHeight,
imageWidth,
imageHeight,
}) })
); );
} else if (activeTabName === 'outpainting') { } else {
dispatch( dispatch(
setStageDimensions({ resizeCanvas({
width: Math.floor(clientWidth), clientWidth,
height: Math.floor(clientHeight), 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); }, 0);
}, [dispatch, baseCanvasImage, doesCanvasNeedScaling, activeTabName]); }, [
dispatch,
baseCanvasImage,
doesCanvasNeedScaling,
activeTabName,
isCanvasInitialized,
]);
return ( return (
<div ref={ref} className="inpainting-canvas-area"> <div ref={ref} className="inpainting-canvas-area">

View File

@ -2,101 +2,93 @@ import * as InvokeAI from 'app/invokeai';
import { PayloadAction } from '@reduxjs/toolkit'; import { PayloadAction } from '@reduxjs/toolkit';
import { CanvasState, Dimensions, initialLayerState } from './canvasSlice'; import { CanvasState, Dimensions, initialLayerState } from './canvasSlice';
import { Vector2d } from 'konva/lib/types'; 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, // state: CanvasState,
image: InvokeAI.Image // image: InvokeAI.Image
// action: PayloadAction<InvokeAI.Image> // // action: PayloadAction<InvokeAI.Image>
) => { // ) => {
const { width: canvasWidth, height: canvasHeight } = // const { width: canvasWidth, height: canvasHeight } =
state.inpainting.stageDimensions; // state.inpainting.stageDimensions;
const { width, height } = state.inpainting.boundingBoxDimensions; // const { width, height } = state.inpainting.boundingBoxDimensions;
const { x, y } = state.inpainting.boundingBoxCoordinates; // const { x, y } = state.inpainting.boundingBoxCoordinates;
const maxWidth = Math.min(image.width, canvasWidth); // const maxWidth = Math.min(image.width, canvasWidth);
const maxHeight = Math.min(image.height, canvasHeight); // const maxHeight = Math.min(image.height, canvasHeight);
const newCoordinates: Vector2d = { x, y }; // const newCoordinates: Vector2d = { x, y };
const newDimensions: Dimensions = { width, height }; // const newDimensions: Dimensions = { width, height };
if (width + x > maxWidth) { // if (width + x > maxWidth) {
// Bounding box at least needs to be translated // // Bounding box at least needs to be translated
if (width > maxWidth) { // if (width > maxWidth) {
// Bounding box also needs to be resized // // Bounding box also needs to be resized
newDimensions.width = roundDownToMultiple(maxWidth, 64); // newDimensions.width = roundDownToMultiple(maxWidth, 64);
} // }
newCoordinates.x = maxWidth - newDimensions.width; // newCoordinates.x = maxWidth - newDimensions.width;
} // }
if (height + y > maxHeight) { // if (height + y > maxHeight) {
// Bounding box at least needs to be translated // // Bounding box at least needs to be translated
if (height > maxHeight) { // if (height > maxHeight) {
// Bounding box also needs to be resized // // Bounding box also needs to be resized
newDimensions.height = roundDownToMultiple(maxHeight, 64); // newDimensions.height = roundDownToMultiple(maxHeight, 64);
} // }
newCoordinates.y = maxHeight - newDimensions.height; // newCoordinates.y = maxHeight - newDimensions.height;
} // }
state.inpainting.boundingBoxDimensions = newDimensions; // state.inpainting.boundingBoxDimensions = newDimensions;
state.inpainting.boundingBoxCoordinates = newCoordinates; // state.inpainting.boundingBoxCoordinates = newCoordinates;
state.inpainting.pastLayerStates.push(state.inpainting.layerState); // state.inpainting.pastLayerStates.push(state.inpainting.layerState);
state.inpainting.layerState = { // state.inpainting.layerState = {
...initialLayerState, // ...initialLayerState,
objects: [ // objects: [
{ // {
kind: 'image', // kind: 'image',
layer: 'base', // layer: 'base',
x: 0, // x: 0,
y: 0, // y: 0,
width: image.width, // width: image.width,
height: image.height, // height: image.height,
image: image, // image: image,
}, // },
], // ],
}; // };
state.outpainting.futureLayerStates = []; // state.outpainting.futureLayerStates = [];
state.doesCanvasNeedScaling = true; // state.doesCanvasNeedScaling = true;
}; // };
export const setImageToOutpaint_reducer = ( export const setInitialCanvasImage = (
state: CanvasState, state: CanvasState,
image: InvokeAI.Image image: InvokeAI.Image
) => { ) => {
const { width: canvasWidth, height: canvasHeight } = const newBoundingBoxDimensions = {
state.outpainting.stageDimensions; width: roundDownToMultiple(_.clamp(image.width, 64, 512), 64),
const { width, height } = state.outpainting.boundingBoxDimensions; height: roundDownToMultiple(_.clamp(image.height, 64, 512), 64),
const { x, y } = state.outpainting.boundingBoxCoordinates; };
const maxWidth = Math.min(image.width, canvasWidth); const newBoundingBoxCoordinates = {
const maxHeight = Math.min(image.height, canvasHeight); x: roundToMultiple(
image.width / 2 - newBoundingBoxDimensions.width / 2,
64
),
y: roundToMultiple(
image.height / 2 - newBoundingBoxDimensions.height / 2,
64
),
};
const newCoordinates: Vector2d = { x, y }; state.outpainting.boundingBoxDimensions = newBoundingBoxDimensions;
const newDimensions: Dimensions = { width, height };
if (width + x > maxWidth) { state.outpainting.boundingBoxCoordinates = newBoundingBoxCoordinates;
// 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.pastLayerStates.push(state.outpainting.layerState); state.outpainting.pastLayerStates.push(state.outpainting.layerState);
state.outpainting.layerState = { state.outpainting.layerState = {
@ -114,5 +106,7 @@ export const setImageToOutpaint_reducer = (
], ],
}; };
state.outpainting.futureLayerStates = []; state.outpainting.futureLayerStates = [];
state.isCanvasInitialized = false;
state.doesCanvasNeedScaling = true; state.doesCanvasNeedScaling = true;
}; };

View File

@ -2,6 +2,7 @@ import {
createAsyncThunk, createAsyncThunk,
createSelector, createSelector,
createSlice, createSlice,
current,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
@ -17,49 +18,50 @@ import { tabMap } from 'features/tabs/InvokeTabs';
import { activeTabNameSelector } from 'features/options/optionsSelectors'; import { activeTabNameSelector } from 'features/options/optionsSelectors';
import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas'; import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas';
import { uploadImage } from 'features/gallery/util/uploadImage'; import { uploadImage } from 'features/gallery/util/uploadImage';
import { import { setInitialCanvasImage } from './canvasReducers';
setImageToInpaint_reducer, import calculateScale from './util/calculateScale';
setImageToOutpaint_reducer, import calculateCoordinates from './util/calculateCoordinates';
} from './canvasReducers';
export interface GenericCanvasState { export interface GenericCanvasState {
tool: CanvasTool;
brushSize: number;
brushColor: RgbaColor;
eraserSize: number;
maskColor: RgbaColor;
cursorPosition: Vector2d | null;
stageDimensions: Dimensions;
stageCoordinates: Vector2d;
boundingBoxDimensions: Dimensions;
boundingBoxCoordinates: Vector2d; boundingBoxCoordinates: Vector2d;
boundingBoxDimensions: Dimensions;
boundingBoxPreviewFill: RgbaColor; boundingBoxPreviewFill: RgbaColor;
shouldShowBoundingBox: boolean; brushColor: RgbaColor;
shouldDarkenOutsideBoundingBox: boolean; brushSize: number;
isMaskEnabled: boolean; cursorPosition: Vector2d | null;
shouldPreserveMaskedArea: boolean; eraserSize: number;
shouldShowCheckboardTransparency: boolean; futureLayerStates: CanvasLayerState[];
shouldShowBrush: boolean;
shouldShowBrushPreview: boolean;
stageScale: number;
isDrawing: boolean;
isTransformingBoundingBox: boolean;
isMouseOverBoundingBox: boolean;
isMovingBoundingBox: boolean;
isMovingStage: boolean;
shouldUseInpaintReplace: boolean;
inpaintReplace: number; inpaintReplace: number;
shouldLockBoundingBox: boolean; intermediateImage?: InvokeAI.Image;
isDrawing: boolean;
isMaskEnabled: boolean;
isMouseOverBoundingBox: boolean;
isMoveBoundingBoxKeyHeld: boolean; isMoveBoundingBoxKeyHeld: boolean;
isMoveStageKeyHeld: boolean; isMoveStageKeyHeld: boolean;
intermediateImage?: InvokeAI.Image; isMovingBoundingBox: boolean;
shouldShowIntermediates: boolean; isMovingStage: boolean;
maxHistory: number; isTransformingBoundingBox: boolean;
layerState: CanvasLayerState; layerState: CanvasLayerState;
maskColor: RgbaColor;
maxHistory: number;
pastLayerStates: CanvasLayerState[]; 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 CanvasLayer = 'base' | 'mask';
export type CanvasDrawingTool = 'brush' | 'eraser'; export type CanvasDrawingTool = 'brush' | 'eraser';
@ -145,6 +147,8 @@ export interface CanvasState {
currentCanvas: ValidCanvasName; currentCanvas: ValidCanvasName;
inpainting: InpaintingCanvasState; inpainting: InpaintingCanvasState;
outpainting: OutpaintingCanvasState; outpainting: OutpaintingCanvasState;
mode: CanvasMode;
isCanvasInitialized: boolean;
} }
export const initialLayerState: CanvasLayerState = { export const initialLayerState: CanvasLayerState = {
@ -160,45 +164,47 @@ export const initialLayerState: CanvasLayerState = {
}; };
const initialGenericCanvasState: GenericCanvasState = { 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 }, brushColor: { r: 90, g: 90, b: 255, a: 1 },
brushSize: 50, 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, cursorPosition: null,
isMaskEnabled: true, eraserSize: 50,
shouldPreserveMaskedArea: false, futureLayerStates: [],
shouldShowCheckboardTransparency: false,
shouldShowBrush: true,
shouldShowBrushPreview: false,
isDrawing: false,
isTransformingBoundingBox: false,
isMouseOverBoundingBox: false,
isMovingBoundingBox: false,
stageScale: 1,
shouldUseInpaintReplace: false,
inpaintReplace: 0.1, inpaintReplace: 0.1,
shouldLockBoundingBox: false, isDrawing: false,
isMaskEnabled: true,
isMouseOverBoundingBox: false,
isMoveBoundingBoxKeyHeld: false, isMoveBoundingBoxKeyHeld: false,
isMoveStageKeyHeld: false, isMoveStageKeyHeld: false,
shouldShowIntermediates: true, isMovingBoundingBox: false,
isMovingStage: false, isMovingStage: false,
maxHistory: 128, isTransformingBoundingBox: false,
layerState: initialLayerState, layerState: initialLayerState,
futureLayerStates: [], maskColor: { r: 255, g: 90, b: 90, a: 1 },
maxHistory: 128,
pastLayerStates: [], 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 = { const initialCanvasState: CanvasState = {
currentCanvas: 'inpainting', currentCanvas: 'inpainting',
doesCanvasNeedScaling: false, doesCanvasNeedScaling: false,
mode: 'outpainting',
isCanvasInitialized: false,
inpainting: { inpainting: {
layer: 'mask', layer: 'mask',
...initialGenericCanvasState, ...initialGenericCanvasState,
@ -293,10 +299,10 @@ export const canvasSlice = createSlice({
// state.inpainting.imageToInpaint = undefined; // state.inpainting.imageToInpaint = undefined;
}, },
setImageToOutpaint: (state, action: PayloadAction<InvokeAI.Image>) => { setImageToOutpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
setImageToOutpaint_reducer(state, action.payload); setInitialCanvasImage(state, action.payload);
}, },
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => { setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
setImageToInpaint_reducer(state, action.payload); setInitialCanvasImage(state, action.payload);
}, },
setStageDimensions: (state, action: PayloadAction<Dimensions>) => { setStageDimensions: (state, action: PayloadAction<Dimensions>) => {
state[state.currentCanvas].stageDimensions = action.payload; state[state.currentCanvas].stageDimensions = action.payload;
@ -412,7 +418,6 @@ export const canvasSlice = createSlice({
}, },
setStageScale: (state, action: PayloadAction<number>) => { setStageScale: (state, action: PayloadAction<number>) => {
state[state.currentCanvas].stageScale = action.payload; state[state.currentCanvas].stageScale = action.payload;
state.doesCanvasNeedScaling = false;
}, },
setShouldDarkenOutsideBoundingBox: ( setShouldDarkenOutsideBoundingBox: (
state, state,
@ -462,7 +467,7 @@ export const canvasSlice = createSlice({
setCurrentCanvas: (state, action: PayloadAction<ValidCanvasName>) => { setCurrentCanvas: (state, action: PayloadAction<ValidCanvasName>) => {
state.currentCanvas = action.payload; state.currentCanvas = action.payload;
}, },
addImageToOutpainting: ( addImageToStagingArea: (
state, state,
action: PayloadAction<{ action: PayloadAction<{
boundingBox: IRect; boundingBox: IRect;
@ -590,6 +595,99 @@ export const canvasSlice = createSlice({
state[state.currentCanvas].layerState = initialLayerState; state[state.currentCanvas].layerState = initialLayerState;
state[state.currentCanvas].futureLayerStates = []; 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) => { nextStagingAreaImage: (state) => {
const currentIndex = const currentIndex =
state.outpainting.layerState.stagingArea.selectedImageIndex; state.outpainting.layerState.stagingArea.selectedImageIndex;
@ -630,6 +728,9 @@ export const canvasSlice = createSlice({
currentCanvas.futureLayerStates = []; currentCanvas.futureLayerStates = [];
}, },
setCanvasMode: (state, action: PayloadAction<CanvasMode>) => {
state.mode = action.payload;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => { builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
@ -661,9 +762,9 @@ export const canvasSlice = createSlice({
if (kind !== 'init') return; if (kind !== 'init') return;
if (activeTabName === 'inpainting') { if (activeTabName === 'inpainting') {
setImageToInpaint_reducer(state, image); setInitialCanvasImage(state, image);
} else if (activeTabName === 'outpainting') { } else if (activeTabName === 'outpainting') {
setImageToOutpaint_reducer(state, image); setInitialCanvasImage(state, image);
} }
}); });
}, },
@ -712,7 +813,7 @@ export const {
setIsMoveStageKeyHeld, setIsMoveStageKeyHeld,
setStageCoordinates, setStageCoordinates,
setCurrentCanvas, setCurrentCanvas,
addImageToOutpainting, addImageToStagingArea,
resetCanvas, resetCanvas,
setShouldShowGrid, setShouldShowGrid,
setShouldSnapToGrid, setShouldSnapToGrid,
@ -723,6 +824,10 @@ export const {
prevStagingAreaImage, prevStagingAreaImage,
commitStagingAreaImage, commitStagingAreaImage,
discardStagedImages, discardStagedImages,
setCanvasMode,
initializeCanvas,
resizeCanvas,
resetCanvasView,
} = canvasSlice.actions; } = canvasSlice.actions;
export default canvasSlice.reducer; export default canvasSlice.reducer;
@ -742,9 +847,26 @@ export const inpaintingCanvasSelector = (
state: RootState state: RootState
): InpaintingCanvasState => state.canvas.inpainting; ): InpaintingCanvasState => state.canvas.inpainting;
export const canvasModeSelector = (state: RootState): CanvasMode =>
state.canvas.mode;
export const baseCanvasImageSelector = createSelector( export const baseCanvasImageSelector = createSelector(
[currentCanvasSelector], [currentCanvasSelector],
(currentCanvas) => { (currentCanvas) => {
return currentCanvas.layerState.objects.find(isCanvasBaseImage); 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,
}
: {};
}
);

View 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;

View 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;

View File

@ -44,6 +44,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
row-gap: 1rem; row-gap: 1rem;
width: 100%; width: 100%;
height: 100%; height: 100%;