Clips lines drawn while canvas locked

When drawing with the locked canvas, if a brush stroke gets too close to the edge of the canvas and its stroke would extend past the edge of the canvas, the edge of that stroke will be seen after unlocking the canvas.

This could cause a problem if you unlock the canvas and now have a bunch of strokes just outside the init image area, which are far back in undo history and you cannot easily erase.

With this change, lines drawn while the canvas is locked get clipped to the initial image bbox, fixing this issue.

Additionally, the merge and save to gallery functions have been updated to respect the initial image bbox so they function how you'd expect.
This commit is contained in:
psychedelicious 2022-11-17 19:16:53 +11:00 committed by blessedcoolant
parent 3994c28b77
commit 425d3bc95d
6 changed files with 87 additions and 32 deletions

View File

@ -10,8 +10,9 @@ import { canvasSelector } from 'features/canvas/store/canvasSelectors';
const selector = createSelector(
[canvasSelector],
(canvas) => {
const { objects } = canvas.layerState;
const {
layerState: { objects },
} = canvas;
return {
objects,
};
@ -37,20 +38,22 @@ const IAICanvasObjectRenderer = () => {
);
} else if (isCanvasBaseLine(obj)) {
return (
<Line
key={i}
points={obj.points}
stroke={obj.color ? rgbaColorToString(obj.color) : 'rgb(0,0,0)'} // The lines can be any color, just need alpha > 0
strokeWidth={obj.strokeWidth * 2}
tension={0}
lineCap="round"
lineJoin="round"
shadowForStrokeEnabled={false}
listening={false}
globalCompositeOperation={
obj.tool === 'brush' ? 'source-over' : 'destination-out'
}
/>
<Group {...obj.clipRect}>
<Line
key={i}
points={obj.points}
stroke={obj.color ? rgbaColorToString(obj.color) : 'rgb(0,0,0)'} // The lines can be any color, just need alpha > 0
strokeWidth={obj.strokeWidth * 2}
tension={0}
lineCap="round"
lineJoin="round"
shadowForStrokeEnabled={false}
listening={false}
globalCompositeOperation={
obj.tool === 'brush' ? 'source-over' : 'destination-out'
}
/>
</Group>
);
}
})}

View File

@ -48,6 +48,7 @@ const initialCanvasState: CanvasState = {
eraserSize: 50,
futureLayerStates: [],
inpaintReplace: 0.1,
initialCanvasImageClipRect: undefined,
isCanvasInitialized: false,
isDrawing: false,
isMaskEnabled: true,
@ -296,6 +297,10 @@ export const canvasSlice = createSlice({
tool,
strokeWidth: newStrokeWidth,
points: action.payload,
clipRect:
state.shouldLockToInitialImage && state.initialCanvasImageClipRect
? state.initialCanvasImageClipRect
: undefined,
...newColor,
});
@ -369,10 +374,14 @@ export const canvasSlice = createSlice({
state.layerState.objects.find(isCanvasBaseImage);
if (!initialCanvasImage) return;
const { shouldLockToInitialImage, initialCanvasImageClipRect } = state;
const { width: imageWidth, height: imageHeight } = initialCanvasImage;
let { width: imageWidth, height: imageHeight } = initialCanvasImage;
const { shouldLockToInitialImage } = state;
if (shouldLockToInitialImage && initialCanvasImageClipRect) {
imageWidth = initialCanvasImageClipRect.clipWidth;
imageHeight = initialCanvasImageClipRect.clipHeight;
}
const padding = shouldLockToInitialImage ? 1 : 0.95;
@ -403,11 +412,13 @@ export const canvasSlice = createSlice({
newScale
);
state.stageScale = newScale;
state.minimumStageScale = newScale;
state.stageCoordinates = newCoordinates;
if (!_.isEqual(state.stageDimensions, newDimensions)) {
state.minimumStageScale = newScale;
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
state.stageDimensions = newDimensions;
}
state.stageDimensions = newDimensions;
state.isCanvasInitialized = true;
},
resizeCanvas: (state) => {

View File

@ -1,5 +1,5 @@
import * as InvokeAI from 'app/invokeai';
import { Vector2d } from 'konva/lib/types';
import { IRect, Vector2d } from 'konva/lib/types';
import { RgbaColor } from 'react-colorful';
export type CanvasLayer = 'base' | 'mask';
@ -8,6 +8,13 @@ export type CanvasDrawingTool = 'brush' | 'eraser';
export type CanvasTool = CanvasDrawingTool | 'move';
export type ClipRect = {
clipX: number;
clipY: number;
clipWidth: number;
clipHeight: number;
};
export type Dimensions = {
width: number;
height: number;
@ -18,6 +25,7 @@ export type CanvasAnyLine = {
tool: CanvasDrawingTool;
strokeWidth: number;
points: number[];
clipRect: ClipRect | undefined;
};
export type CanvasImage = {
@ -78,6 +86,7 @@ export interface CanvasState {
doesCanvasNeedScaling: boolean;
eraserSize: number;
futureLayerStates: CanvasLayerState[];
initialCanvasImageClipRect?: ClipRect;
inpaintReplace: number;
intermediateImage?: InvokeAI.Image;
isCanvasInitialized: boolean;

View File

@ -28,7 +28,6 @@ export const setInitialCanvasImage = (
};
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
state.pastLayerStates.push(state.layerState);
@ -48,6 +47,13 @@ export const setInitialCanvasImage = (
};
state.futureLayerStates = [];
state.initialCanvasImageClipRect = {
clipX: 0,
clipY: 0,
clipWidth: image.width,
clipHeight: image.height,
};
state.isCanvasInitialized = false;
state.doesCanvasNeedScaling = true;
};

View File

@ -1,6 +1,11 @@
import Konva from 'konva';
import { IRect } from 'konva/lib/types';
const layerToDataURL = (layer: Konva.Layer, stageScale: number) => {
const layerToDataURL = (
layer: Konva.Layer,
stageScale: number,
boundingBox?: IRect
) => {
const tempScale = layer.scale();
const relativeClientRect = layer.getClientRect({
@ -15,12 +20,21 @@ const layerToDataURL = (layer: Konva.Layer, stageScale: number) => {
const { x, y, width, height } = layer.getClientRect();
const dataURL = layer.toDataURL({
x: Math.round(x),
y: Math.round(y),
width: Math.round(width),
height: Math.round(height),
});
const scaledBoundingBox = boundingBox
? {
x: Math.round(boundingBox.x / stageScale),
y: Math.round(boundingBox.y / stageScale),
width: Math.round(boundingBox.width / stageScale),
height: Math.round(boundingBox.height / stageScale),
}
: {
x: Math.round(x),
y: Math.round(y),
width: Math.round(width),
height: Math.round(height),
};
const dataURL = layer.toDataURL(scaledBoundingBox);
// Unscale the canvas
layer.scale(tempScale);

View File

@ -22,12 +22,24 @@ export const mergeAndUploadCanvas = createAsyncThunk(
const state = getState() as RootState;
const stageScale = state.canvas.stageScale;
const clipRect = state.canvas.initialCanvasImageClipRect;
const boundingBox =
state.canvas.shouldLockToInitialImage && clipRect && saveToGallery
? {
x: clipRect.clipX,
y: clipRect.clipY,
width: clipRect.clipWidth,
height: clipRect.clipHeight,
}
: undefined;
if (!canvasImageLayerRef.current) return;
const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
canvasImageLayerRef.current,
stageScale
stageScale,
boundingBox
);
if (!dataURL) return;