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,
|
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];
|
||||||
|
@ -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}
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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;
|
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%;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user