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,
} 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) {

View File

@ -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}

View File

@ -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 (

View File

@ -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>
);
};

View File

@ -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">

View File

@ -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;
};

View File

@ -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,
}
: {};
}
);

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;
flex-direction: column;
align-items: center;
justify-content: center;
row-gap: 1rem;
width: 100%;
height: 100%;