Adds bounding box handles

This commit is contained in:
psychedelicious 2022-10-27 16:50:27 +11:00
parent 44599a239f
commit 9e2ce00f7b
12 changed files with 426 additions and 210 deletions

View File

@ -1,3 +1,7 @@
export const roundDownToMultiple = (num: number, multiple: number): number => { export const roundDownToMultiple = (num: number, multiple: number): number => {
return Math.floor(num / multiple) * multiple; return Math.floor(num / multiple) * multiple;
}; };
export const roundToMultiple = (num: number, multiple: number): number => {
return Math.round(num / multiple) * multiple;
};

View File

@ -1,6 +1,5 @@
// lib // lib
import { import {
KeyboardEvent,
MutableRefObject, MutableRefObject,
useCallback, useCallback,
useEffect, useEffect,
@ -20,7 +19,6 @@ import {
setBoundingBoxCoordinate, setBoundingBoxCoordinate,
setCursorPosition, setCursorPosition,
setIsMovingBoundingBox, setIsMovingBoundingBox,
setTool,
} from './inpaintingSlice'; } from './inpaintingSlice';
import { inpaintingCanvasSelector } from './inpaintingSliceSelectors'; import { inpaintingCanvasSelector } from './inpaintingSliceSelectors';
@ -32,7 +30,9 @@ import Cacher from './components/Cacher';
import { Vector2d } from 'konva/lib/types'; import { Vector2d } from 'konva/lib/types';
import getScaledCursorPosition from './util/getScaledCursorPosition'; import getScaledCursorPosition from './util/getScaledCursorPosition';
import _ from 'lodash'; import _ from 'lodash';
import InpaintingBoundingBoxPreview from './components/InpaintingBoundingBoxPreview'; import InpaintingBoundingBoxPreview, {
InpaintingBoundingBoxPreviewOverlay,
} from './components/InpaintingBoundingBoxPreview';
import { KonvaEventObject } from 'konva/lib/Node'; import { KonvaEventObject } from 'konva/lib/Node';
import KeyboardEventManager from './components/KeyboardEventManager'; import KeyboardEventManager from './components/KeyboardEventManager';
@ -50,13 +50,14 @@ const InpaintingCanvas = () => {
shouldInvertMask, shouldInvertMask,
shouldShowMask, shouldShowMask,
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
maskOpacity, maskColor,
imageToInpaint, imageToInpaint,
isMovingBoundingBox, isMovingBoundingBox,
boundingBoxDimensions, boundingBoxDimensions,
canvasDimensions, canvasDimensions,
boundingBoxCoordinate, boundingBoxCoordinate,
stageScale, stageScale,
shouldShowBoundingBoxFill,
} = useAppSelector(inpaintingCanvasSelector); } = useAppSelector(inpaintingCanvasSelector);
// set the closure'd refs // set the closure'd refs
@ -248,7 +249,7 @@ const InpaintingCanvas = () => {
opacity={ opacity={
shouldShowCheckboardTransparency || shouldInvertMask shouldShowCheckboardTransparency || shouldInvertMask
? 1 ? 1
: maskOpacity : maskColor.a
} }
ref={maskLayerRef} ref={maskLayerRef}
> >
@ -270,8 +271,11 @@ const InpaintingCanvas = () => {
/> />
)} )}
</Layer> </Layer>
<Layer name={'preview-layer'} listening={false}> <Layer name={'preview-layer'}>
<InpaintingCanvasBrushPreviewOutline /> <InpaintingCanvasBrushPreviewOutline />
{shouldShowBoundingBoxFill && (
<InpaintingBoundingBoxPreviewOverlay />
)}
<InpaintingBoundingBoxPreview /> <InpaintingBoundingBoxPreview />
</Layer> </Layer>
</> </>
@ -284,6 +288,4 @@ const InpaintingCanvas = () => {
); );
}; };
// </div>
export default InpaintingCanvas; export default InpaintingCanvas;

View File

@ -6,32 +6,29 @@ import {
FaPalette, FaPalette,
FaPlus, FaPlus,
FaRedo, FaRedo,
FaTint,
FaTintSlash,
FaUndo, FaUndo,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { BiHide, BiShow } from 'react-icons/bi'; import { BiHide, BiShow } from 'react-icons/bi';
import { VscSplitHorizontal } from 'react-icons/vsc'; import { VscSplitHorizontal } from 'react-icons/vsc';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store'; import { useAppDispatch, useAppSelector } from '../../../app/store';
import IAIIconButton from '../../../common/components/IAIIconButton'; import IAIIconButton from '../../../common/components/IAIIconButton';
import { import {
clearMask, clearMask,
redo, redo,
setMaskColor, setMaskColor,
setBrushSize, setBrushSize,
setMaskOpacity,
setShouldShowBrushPreview, setShouldShowBrushPreview,
setTool, setTool,
undo, undo,
setShouldShowMask, setShouldShowMask,
setShouldInvertMask, setShouldInvertMask,
setNeedsRepaint, setNeedsRepaint,
setShouldShowBoundingBoxFill,
} from './inpaintingSlice'; } from './inpaintingSlice';
import { tabMap } from '../InvokeTabs'; import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
import {
MdInvertColors,
MdInvertColorsOff,
MdOutlineCloseFullscreen,
} from 'react-icons/md';
import IAISlider from '../../../common/components/IAISlider'; import IAISlider from '../../../common/components/IAISlider';
import IAINumberInput from '../../../common/components/IAINumberInput'; import IAINumberInput from '../../../common/components/IAINumberInput';
import { inpaintingControlsSelector } from './inpaintingSliceSelectors'; import { inpaintingControlsSelector } from './inpaintingSliceSelectors';
@ -39,14 +36,12 @@ import IAIPopover from '../../../common/components/IAIPopover';
import IAIColorPicker from '../../../common/components/IAIColorPicker'; import IAIColorPicker from '../../../common/components/IAIColorPicker';
import { RgbaColor } from 'react-colorful'; import { RgbaColor } from 'react-colorful';
import { setShowDualDisplay } from '../../options/optionsSlice'; import { setShowDualDisplay } from '../../options/optionsSlice';
import { useEffect } from 'react';
const InpaintingControls = () => { const InpaintingControls = () => {
const { const {
tool, tool,
brushSize, brushSize,
maskColor, maskColor,
maskOpacity,
shouldInvertMask, shouldInvertMask,
shouldShowMask, shouldShowMask,
canUndo, canUndo,
@ -54,12 +49,17 @@ const InpaintingControls = () => {
isMaskEmpty, isMaskEmpty,
activeTabName, activeTabName,
showDualDisplay, showDualDisplay,
shouldShowBoundingBoxFill,
} = useAppSelector(inpaintingControlsSelector); } = useAppSelector(inpaintingControlsSelector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const toast = useToast(); const toast = useToast();
// Hotkeys /**
* Hotkeys
*/
// Decrease brush size
useHotkeys( useHotkeys(
'[', '[',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -76,6 +76,7 @@ const InpaintingControls = () => {
[activeTabName, shouldShowMask, brushSize] [activeTabName, shouldShowMask, brushSize]
); );
// Increase brush size
useHotkeys( useHotkeys(
']', ']',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -88,30 +89,39 @@ const InpaintingControls = () => {
[activeTabName, shouldShowMask, brushSize] [activeTabName, shouldShowMask, brushSize]
); );
// Decrease mask opacity
useHotkeys( useHotkeys(
'shift+[', 'shift+[',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
handleChangeMaskOpacity(Math.max(maskOpacity - 0.05, 0)); handleChangeMaskColor({
...maskColor,
a: Math.max(maskColor.a - 0.05, 0),
});
}, },
{ {
enabled: activeTabName === 'inpainting' && shouldShowMask, enabled: activeTabName === 'inpainting' && shouldShowMask,
}, },
[activeTabName, shouldShowMask, maskOpacity] [activeTabName, shouldShowMask, maskColor.a]
); );
// Increase mask opacity
useHotkeys( useHotkeys(
'shift+]', 'shift+]',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
handleChangeMaskOpacity(Math.min(maskOpacity + 0.05, 100)); handleChangeMaskColor({
...maskColor,
a: Math.min(maskColor.a + 0.05, 100),
});
}, },
{ {
enabled: activeTabName === 'inpainting' && shouldShowMask, enabled: activeTabName === 'inpainting' && shouldShowMask,
}, },
[activeTabName, shouldShowMask, maskOpacity] [activeTabName, shouldShowMask, maskColor.a]
); );
// Set tool to eraser
useHotkeys( useHotkeys(
'e', 'e',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -125,6 +135,7 @@ const InpaintingControls = () => {
[activeTabName, shouldShowMask] [activeTabName, shouldShowMask]
); );
// Set tool to brush
useHotkeys( useHotkeys(
'b', 'b',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -137,6 +148,7 @@ const InpaintingControls = () => {
[activeTabName, shouldShowMask] [activeTabName, shouldShowMask]
); );
// Undo
useHotkeys( useHotkeys(
'cmd+z, control+z', 'cmd+z, control+z',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -149,6 +161,7 @@ const InpaintingControls = () => {
[activeTabName, shouldShowMask, canUndo] [activeTabName, shouldShowMask, canUndo]
); );
// Redo
useHotkeys( useHotkeys(
'cmd+shift+z, control+shift+z, control+y, cmd+y', 'cmd+shift+z, control+shift+z, control+y, cmd+y',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -161,6 +174,7 @@ const InpaintingControls = () => {
[activeTabName, shouldShowMask, canRedo] [activeTabName, shouldShowMask, canRedo]
); );
// Show/hide mask
useHotkeys( useHotkeys(
'h', 'h',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -173,6 +187,7 @@ const InpaintingControls = () => {
[activeTabName, shouldShowMask] [activeTabName, shouldShowMask]
); );
// Invert mask
useHotkeys( useHotkeys(
'shift+m', 'shift+m',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -185,6 +200,7 @@ const InpaintingControls = () => {
[activeTabName, shouldInvertMask, shouldShowMask] [activeTabName, shouldInvertMask, shouldShowMask]
); );
// Clear mask
useHotkeys( useHotkeys(
'shift+c', 'shift+c',
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -203,6 +219,7 @@ const InpaintingControls = () => {
[activeTabName, isMaskEmpty, shouldShowMask] [activeTabName, isMaskEmpty, shouldShowMask]
); );
// Toggle split view
useHotkeys( useHotkeys(
'shift+j', 'shift+j',
() => { () => {
@ -224,10 +241,6 @@ const InpaintingControls = () => {
dispatch(setBrushSize(v)); dispatch(setBrushSize(v));
}; };
const handleChangeMaskOpacity = (v: number) => {
dispatch(setMaskOpacity(v));
};
const handleToggleShouldShowMask = () => const handleToggleShouldShowMask = () =>
dispatch(setShouldShowMask(!shouldShowMask)); dispatch(setShouldShowMask(!shouldShowMask));
@ -242,10 +255,8 @@ const InpaintingControls = () => {
dispatch(setShouldShowBrushPreview(false)); dispatch(setShouldShowBrushPreview(false));
}; };
const handleChangeBrushColor = (newColor: RgbaColor) => { const handleChangeMaskColor = (newColor: RgbaColor) => {
const { r, g, b, a: maskOpacity } = newColor; dispatch(setMaskColor(newColor));
dispatch(setMaskColor({ r, g, b }));
dispatch(setMaskOpacity(maskOpacity));
}; };
const handleUndo = () => dispatch(undo()); const handleUndo = () => dispatch(undo());
@ -257,6 +268,10 @@ const InpaintingControls = () => {
dispatch(setNeedsRepaint(true)); dispatch(setNeedsRepaint(true));
}; };
const handleChangeShouldShowBoundingBoxFill = () => {
dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
};
return ( return (
<div className="inpainting-settings"> <div className="inpainting-settings">
<div className="inpainting-buttons"> <div className="inpainting-buttons">
@ -320,8 +335,8 @@ const InpaintingControls = () => {
} }
> >
<IAIColorPicker <IAIColorPicker
color={{ ...maskColor, a: maskOpacity }} color={maskColor}
onChange={handleChangeBrushColor} onChange={handleChangeMaskColor}
/> />
</IAIPopover> </IAIPopover>
<IAIIconButton <IAIIconButton
@ -375,6 +390,13 @@ const InpaintingControls = () => {
data-selected={showDualDisplay} data-selected={showDualDisplay}
onClick={handleDualDisplay} onClick={handleDualDisplay}
/> />
<IAIIconButton
aria-label="Darken Outside Bounding Box (xxx)"
tooltip="Darken Outside Bounding Box (xxx)"
icon={shouldShowBoundingBoxFill ? <FaTint /> : <FaTintSlash />}
data-selected={shouldShowBoundingBoxFill}
onClick={handleChangeShouldShowBoundingBoxFill}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,13 +1,26 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import Konva from 'konva'; import Konva from 'konva';
import { Vector2d } from 'konva/lib/types';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { Group, Rect } from 'react-konva'; import { Group, Rect, Transformer } from 'react-konva';
import { RootState, useAppSelector } from '../../../../app/store'; import {
import { InpaintingState } from '../inpaintingSlice'; RootState,
useAppDispatch,
useAppSelector,
} from '../../../../app/store';
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
import {
InpaintingState,
setBoundingBoxCoordinate,
setBoundingBoxDimensions,
} from '../inpaintingSlice';
import { rgbaColorToString } from '../util/colorToString'; import { rgbaColorToString } from '../util/colorToString';
import { DASH_WIDTH, MARCHING_ANTS_SPEED } from '../util/constants'; import {
DASH_WIDTH,
// MARCHING_ANTS_SPEED,
TRANSFORMER_ANCHOR_SIZE,
} from '../util/constants';
const boundingBoxPreviewSelector = createSelector( const boundingBoxPreviewSelector = createSelector(
(state: RootState) => state.inpainting, (state: RootState) => state.inpainting,
@ -18,14 +31,18 @@ const boundingBoxPreviewSelector = createSelector(
boundingBoxPreviewFill, boundingBoxPreviewFill,
canvasDimensions, canvasDimensions,
stageScale, stageScale,
imageToInpaint,
} = inpainting; } = inpainting;
return { return {
boundingBoxCoordinate, boundingBoxCoordinate,
boundingBoxDimensions, boundingBoxDimensions,
boundingBoxPreviewFillString: rgbaColorToString(boundingBoxPreviewFill), boundingBoxPreviewFillString: rgbaColorToString(boundingBoxPreviewFill),
canvasDimensions, canvasDimensions,
dash: DASH_WIDTH / stageScale, // scale dash lengths stageScale,
imageToInpaint,
dash: DASH_WIDTH / stageScale, // scale dash lengths
strokeWidth: 1 / stageScale, // scale stroke thickness strokeWidth: 1 / stageScale, // scale stroke thickness
anchorSize: TRANSFORMER_ANCHOR_SIZE,
}; };
}, },
{ {
@ -38,7 +55,7 @@ const boundingBoxPreviewSelector = createSelector(
/** /**
* Shades the area around the mask. * Shades the area around the mask.
*/ */
const InpaintingBoundingBoxPreviewOverlay = () => { export const InpaintingBoundingBoxPreviewOverlay = () => {
const { const {
boundingBoxCoordinate, boundingBoxCoordinate,
boundingBoxDimensions, boundingBoxDimensions,
@ -68,122 +85,228 @@ const InpaintingBoundingBoxPreviewOverlay = () => {
); );
}; };
/** // /**
* Draws marching ants around the mask. // * Draws marching ants around the mask. Unused.
*/ // */
const InpaintingBoundingBoxPreviewMarchingAnts = () => { // const _InpaintingBoundingBoxPreviewMarchingAnts = () => {
const { boundingBoxCoordinate, boundingBoxDimensions } = useAppSelector( // const { boundingBoxCoordinate, boundingBoxDimensions } = useAppSelector(
boundingBoxPreviewSelector // boundingBoxPreviewSelector
); // );
const blackStrokeRectRef = useRef<Konva.Rect>(null); // const blackStrokeRectRef = useRef<Konva.Rect>(null);
const whiteStrokeRectRef = useRef<Konva.Rect>(null); // const whiteStrokeRectRef = useRef<Konva.Rect>(null);
// useEffect(() => {
// const blackStrokeRect = blackStrokeRectRef.current;
// const whiteStrokeRect = whiteStrokeRectRef.current;
// const anim = new Konva.Animation((frame) => {
// if (!frame) return;
// blackStrokeRect?.dashOffset(
// -1 * (Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16)
// );
// whiteStrokeRect?.dashOffset(
// -1 * ((Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16) + 4)
// );
// });
// anim.start();
// return () => {
// anim.stop();
// };
// }, []);
// return (
// <Group>
// <Rect
// x={boundingBoxCoordinate.x}
// y={boundingBoxCoordinate.y}
// width={boundingBoxDimensions.width}
// height={boundingBoxDimensions.height}
// stroke={'black'}
// strokeWidth={1}
// dash={[4, 4]}
// ref={blackStrokeRectRef}
// listening={false}
// />
// <Rect
// x={boundingBoxCoordinate.x}
// y={boundingBoxCoordinate.y}
// width={boundingBoxDimensions.width}
// height={boundingBoxDimensions.height}
// stroke={'white'}
// dash={[4, 4]}
// strokeWidth={1}
// ref={whiteStrokeRectRef}
// listening={false}
// />
// </Group>
// );
// };
const InpaintingBoundingBoxPreview = () => {
const dispatch = useAppDispatch();
const {
boundingBoxCoordinate,
boundingBoxDimensions,
strokeWidth,
anchorSize,
stageScale,
imageToInpaint,
} = useAppSelector(boundingBoxPreviewSelector);
const transformerRef = useRef<Konva.Transformer>(null);
const shapeRef = useRef<Konva.Rect>(null);
useEffect(() => { useEffect(() => {
const blackStrokeRect = blackStrokeRectRef.current; if (!transformerRef.current || !shapeRef.current) return;
const whiteStrokeRect = whiteStrokeRectRef.current; transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
const anim = new Konva.Animation((frame) => {
if (!frame) return;
blackStrokeRect?.dashOffset(
-1 * (Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16)
);
whiteStrokeRect?.dashOffset(
-1 * ((Math.floor(frame.time / MARCHING_ANTS_SPEED) % 16) + 4)
);
});
anim.start();
return () => {
anim.stop();
};
}, []); }, []);
return ( return (
<Group> <>
<Rect
x={boundingBoxCoordinate.x}
y={boundingBoxCoordinate.y}
width={boundingBoxDimensions.width}
height={boundingBoxDimensions.height}
stroke={'black'}
strokeWidth={1}
dash={[4, 4]}
ref={blackStrokeRectRef}
listening={false}
/>
<Rect <Rect
x={boundingBoxCoordinate.x} x={boundingBoxCoordinate.x}
y={boundingBoxCoordinate.y} y={boundingBoxCoordinate.y}
width={boundingBoxDimensions.width} width={boundingBoxDimensions.width}
height={boundingBoxDimensions.height} height={boundingBoxDimensions.height}
ref={shapeRef}
stroke={'white'} stroke={'white'}
dash={[4, 4]}
strokeWidth={1}
ref={whiteStrokeRectRef}
listening={false}
/>
</Group>
);
};
/**
* Draws non-marching ants around the mask.
*/
const InpaintingBoundingBoxPreviewAnts = () => {
const { boundingBoxCoordinate, boundingBoxDimensions, dash, strokeWidth } =
useAppSelector(boundingBoxPreviewSelector);
return (
<Group>
<Rect
x={boundingBoxCoordinate.x}
y={boundingBoxCoordinate.y}
width={boundingBoxDimensions.width}
height={boundingBoxDimensions.height}
stroke={'black'}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
dash={[dash, dash]}
dashOffset={0}
listening={false} listening={false}
onTransform={() => {
/**
* The Konva Transformer changes the object's anchor point and scale factor,
* not its width and height. We need to un-scale the width and height before
* setting the values.
*/
if (!shapeRef.current) return;
const rect = shapeRef.current;
const scaleX = rect.scaleX();
const scaleY = rect.scaleY();
// undo the scaling
const width = Math.round(rect.width() * scaleX);
const height = Math.round(rect.height() * scaleY);
const x = Math.round(rect.x());
const y = Math.round(rect.y());
dispatch(
setBoundingBoxDimensions({
width,
height,
})
);
dispatch(
setBoundingBoxCoordinate({
x,
y,
})
);
// Reset the scale now that the coords/dimensions have been un-scaled
rect.scaleX(1);
rect.scaleY(1);
}}
/> />
<Rect <Transformer
x={boundingBoxCoordinate.x} ref={transformerRef}
y={boundingBoxCoordinate.y} rotateEnabled={false}
width={boundingBoxDimensions.width} anchorSize={anchorSize}
height={boundingBoxDimensions.height} anchorStroke={'rgb(42,42,42)'}
stroke={'white'} borderEnabled={true}
dash={[dash, dash]} borderStroke={'black'}
strokeWidth={strokeWidth} borderDash={[DASH_WIDTH, DASH_WIDTH]}
dashOffset={dash} anchorCornerRadius={3}
listening={false} ignoreStroke={true}
keepRatio={false}
flipEnabled={false}
anchorDragBoundFunc={(
oldPos: Vector2d, // old absolute position of anchor point
newPos: Vector2d, // new absolute position (potentially) of anchor point
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_event: MouseEvent
) => {
/**
* Konva does not transform with width or height. It transforms the anchor point
* and scale factor. This is then sent to the shape's onTransform listeners.
*
* We need to snap the new width to steps of 64 without also snapping the
* coordinates of the bounding box to steps of 64. But because the whole
* stage is scaled, our actual desired step is actually 64 * the stage scale.
*/
// Get the scaled step
const scaledStep = 64 * stageScale;
// Difference of the old coords from the nearest multiple the scaled step
const offsetX = oldPos.x % scaledStep;
const offsetY = oldPos.y % scaledStep;
// Round new position to the nearest multiple of the scaled step
const closestX = roundToMultiple(newPos.x, scaledStep) + offsetX;
const closestY = roundToMultiple(newPos.y, scaledStep) + offsetY;
// the difference between the old coord and new
const diffX = Math.abs(newPos.x - closestX);
const diffY = Math.abs(newPos.y - closestY);
// if the difference is less than the scaled step, we want to snap
const didSnapX = diffX < scaledStep;
const didSnapY = diffY < scaledStep;
// We may not change anything, stash the old position
let newCoordinate = { ...oldPos };
// Set the new coords based on what snapped
if (didSnapX && !didSnapY) {
newCoordinate = {
x: closestX,
y: oldPos.y,
};
} else if (!didSnapX && didSnapY) {
newCoordinate = {
x: oldPos.x,
y: closestY,
};
} else if (didSnapX && didSnapY) {
newCoordinate = {
x: closestX,
y: closestY,
};
}
return newCoordinate;
}}
boundBoxFunc={(oldBoundBox, newBoundBox) => {
/**
* The transformer uses this callback to limit valid transformations.
* Unlike anchorDragBoundFunc, it does get a width and height, so
* the logic to constrain the size of the bounding box is very simple.
*/
if (!imageToInpaint) return oldBoundBox;
if (
newBoundBox.width + newBoundBox.x > imageToInpaint.width ||
newBoundBox.height + newBoundBox.y > imageToInpaint.height ||
newBoundBox.x < 0 ||
newBoundBox.y < 0
) {
return oldBoundBox;
}
return newBoundBox;
}}
/> />
</Group> </>
); );
}; };
const boundingBoxPreviewTypeSelector = createSelector(
(state: RootState) => state.inpainting,
(inpainting: InpaintingState) => inpainting.boundingBoxPreviewType
);
const InpaintingBoundingBoxPreview = () => {
const boundingBoxPreviewType = useAppSelector(boundingBoxPreviewTypeSelector);
switch (boundingBoxPreviewType) {
case 'overlay': {
return <InpaintingBoundingBoxPreviewOverlay />;
}
case 'ants': {
return <InpaintingBoundingBoxPreviewAnts />;
}
case 'marchingAnts': {
return <InpaintingBoundingBoxPreviewMarchingAnts />;
}
default:
return null;
}
};
export default InpaintingBoundingBoxPreview; export default InpaintingBoundingBoxPreview;

View File

@ -3,7 +3,7 @@ import _ from 'lodash';
import { Circle } from 'react-konva'; import { Circle } from 'react-konva';
import { RootState, useAppSelector } from '../../../../app/store'; import { RootState, useAppSelector } from '../../../../app/store';
import { InpaintingState } from '../inpaintingSlice'; import { InpaintingState } from '../inpaintingSlice';
import { rgbColorToString } from '../util/colorToString'; import { rgbaColorToRgbString } from '../util/colorToString';
const inpaintingCanvasBrushPreviewSelector = createSelector( const inpaintingCanvasBrushPreviewSelector = createSelector(
(state: RootState) => state.inpainting, (state: RootState) => state.inpainting,
@ -23,7 +23,7 @@ const inpaintingCanvasBrushPreviewSelector = createSelector(
height, height,
shouldShowBrushPreview, shouldShowBrushPreview,
brushSize, brushSize,
maskColorString: rgbColorToString(maskColor), maskColorString: rgbaColorToRgbString(maskColor),
tool, tool,
}; };
}, },

View File

@ -1,5 +1,6 @@
import { Line } from 'react-konva'; import { Line } from 'react-konva';
import { RootState, useAppSelector } from '../../../../app/store'; import { useAppSelector } from '../../../../app/store';
import { inpaintingCanvasLinesSelector } from '../inpaintingSliceSelectors';
/** /**
* Draws the lines which comprise the mask. * Draws the lines which comprise the mask.
@ -7,11 +8,9 @@ import { RootState, useAppSelector } from '../../../../app/store';
* Uses globalCompositeOperation to handle the brush and eraser tools. * Uses globalCompositeOperation to handle the brush and eraser tools.
*/ */
const InpaintingCanvasLines = () => { const InpaintingCanvasLines = () => {
const { lines, maskColor } = useAppSelector( const { lines, maskColorString } = useAppSelector(
(state: RootState) => state.inpainting inpaintingCanvasLinesSelector
); );
const { r, g, b } = maskColor;
const maskColorString = `rgb(${r},${g},${b})`;
return ( return (
<> <>

View File

@ -39,24 +39,10 @@ const KeyboardEventManager = () => {
const isFirstEvent = useRef<boolean>(true); const isFirstEvent = useRef<boolean>(true);
const wasLastEventOverCanvas = useRef<boolean>(false); const wasLastEventOverCanvas = useRef<boolean>(false);
const lastEvent = useRef<KeyboardEvent | null>(null);
useEffect(() => { useEffect(() => {
const listener = (e: KeyboardEvent) => { const listener = (e: KeyboardEvent) => {
if (!isCursorOnCanvas) {
wasLastEventOverCanvas.current = false;
if (isFirstEvent.current) {
isFirstEvent.current = false;
}
return;
}
if (isFirstEvent.current) {
wasLastEventOverCanvas.current = true;
isFirstEvent.current = false;
}
if ( if (
!['Alt', ' '].includes(e.key) || !['Alt', ' '].includes(e.key) ||
activeTabName !== 'inpainting' || activeTabName !== 'inpainting' ||
@ -66,8 +52,27 @@ const KeyboardEventManager = () => {
return; return;
} }
if (!wasLastEventOverCanvas.current) { // cursor is NOT over canvas
if (!isCursorOnCanvas) {
if (!lastEvent.current) {
lastEvent.current = e;
}
wasLastEventOverCanvas.current = false;
return;
}
// cursor is over canvas
// if this is the first event
if (!lastEvent.current) {
wasLastEventOverCanvas.current = true; wasLastEventOverCanvas.current = true;
lastEvent.current = e;
}
if (!wasLastEventOverCanvas.current && e.type === 'keyup') {
wasLastEventOverCanvas.current = true;
lastEvent.current = e;
return; return;
} }
@ -83,9 +88,11 @@ const KeyboardEventManager = () => {
break; break;
} }
} }
lastEvent.current = e;
wasLastEventOverCanvas.current = true;
}; };
console.log('adding listeners');
document.addEventListener('keydown', listener); document.addEventListener('keydown', listener);
document.addEventListener('keyup', listener); document.addEventListener('keyup', listener);

View File

@ -1,7 +1,7 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { Vector2d } from 'konva/lib/types'; import { Vector2d } from 'konva/lib/types';
import { RgbaColor, RgbColor } from 'react-colorful'; import { RgbaColor } from 'react-colorful';
import * as InvokeAI from '../../../app/invokeai'; import * as InvokeAI from '../../../app/invokeai';
import _ from 'lodash'; import _ from 'lodash';
import { roundDownToMultiple } from '../../../common/util/roundDownToMultiple'; import { roundDownToMultiple } from '../../../common/util/roundDownToMultiple';
@ -31,15 +31,15 @@ export type BoundingBoxPreviewType = 'overlay' | 'ants' | 'marchingAnts';
export interface InpaintingState { export interface InpaintingState {
tool: 'brush' | 'eraser'; tool: 'brush' | 'eraser';
brushSize: number; brushSize: number;
maskColor: RgbColor; maskColor: RgbaColor;
maskOpacity: number;
cursorPosition: Vector2d | null; cursorPosition: Vector2d | null;
canvasDimensions: Dimensions; canvasDimensions: Dimensions;
boundingBoxDimensions: Dimensions; boundingBoxDimensions: Dimensions;
boundingBoxCoordinate: Vector2d; boundingBoxCoordinate: Vector2d;
isMovingBoundingBox: boolean; isMovingBoundingBox: boolean;
boundingBoxPreviewFill: RgbaColor; boundingBoxPreviewFill: RgbaColor;
boundingBoxPreviewType: BoundingBoxPreviewType; shouldShowBoundingBoxFill: boolean;
isBoundingBoxTransforming: boolean;
lines: MaskLine[]; lines: MaskLine[];
pastLines: MaskLine[][]; pastLines: MaskLine[][];
futureLines: MaskLine[][]; futureLines: MaskLine[][];
@ -50,18 +50,19 @@ export interface InpaintingState {
imageToInpaint?: InvokeAI.Image; imageToInpaint?: InvokeAI.Image;
needsRepaint: boolean; needsRepaint: boolean;
stageScale: number; stageScale: number;
isDrawing: boolean;
} }
const initialInpaintingState: InpaintingState = { const initialInpaintingState: InpaintingState = {
tool: 'brush', tool: 'brush',
brushSize: 50, brushSize: 50,
maskColor: { r: 255, g: 90, b: 90 }, maskColor: { r: 255, g: 90, b: 90, a: 0.5 },
maskOpacity: 0.5,
canvasDimensions: { width: 0, height: 0 }, canvasDimensions: { width: 0, height: 0 },
boundingBoxDimensions: { width: 64, height: 64 }, boundingBoxDimensions: { width: 64, height: 64 },
boundingBoxCoordinate: { x: 0, y: 0 }, boundingBoxCoordinate: { x: 0, y: 0 },
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 }, boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.7 },
boundingBoxPreviewType: 'ants', shouldShowBoundingBoxFill: false,
isBoundingBoxTransforming: false,
cursorPosition: null, cursorPosition: null,
lines: [], lines: [],
pastLines: [], pastLines: [],
@ -72,6 +73,7 @@ const initialInpaintingState: InpaintingState = {
shouldShowBrushPreview: false, shouldShowBrushPreview: false,
isMovingBoundingBox: false, isMovingBoundingBox: false,
needsRepaint: false, needsRepaint: false,
isDrawing: false,
stageScale: 1, stageScale: 1,
}; };
@ -142,12 +144,10 @@ export const inpaintingSlice = createSlice({
setShouldShowBrushPreview: (state, action: PayloadAction<boolean>) => { setShouldShowBrushPreview: (state, action: PayloadAction<boolean>) => {
state.shouldShowBrushPreview = action.payload; state.shouldShowBrushPreview = action.payload;
}, },
setMaskColor: (state, action: PayloadAction<RgbColor>) => { setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
state.maskColor = action.payload; state.maskColor = action.payload;
}, },
setMaskOpacity: (state, action: PayloadAction<number>) => { // },
state.maskOpacity = action.payload;
},
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => { setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
state.cursorPosition = action.payload; state.cursorPosition = action.payload;
}, },
@ -215,34 +215,67 @@ export const inpaintingSlice = createSlice({
}; };
}, },
setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => { setBoundingBoxDimensions: (state, action: PayloadAction<Dimensions>) => {
const { width: boundingBoxWidth, height: boundingBoxHeight } = state.boundingBoxDimensions = action.payload;
action.payload; // const { width: boundingBoxWidth, height: boundingBoxHeight } =
const { x: boundingBoxX, y: boundingBoxY } = state.boundingBoxCoordinate; // action.payload;
const { width: canvasWidth, height: canvasHeight } = // const { x: boundingBoxX, y: boundingBoxY } = state.boundingBoxCoordinate;
state.canvasDimensions; // const { width: canvasWidth, height: canvasHeight } =
// state.canvasDimensions;
const overflowX = boundingBoxX + boundingBoxWidth - canvasWidth; // const roundedCanvasWidth = roundDownToMultiple(canvasWidth, 64);
const overflowY = boundingBoxY + boundingBoxHeight - canvasHeight; // const roundedCanvasHeight = roundDownToMultiple(canvasHeight, 64);
// const roundedBoundingBoxWidth = roundDownToMultiple(boundingBoxWidth, 64);
// const roundedBoundingBoxHeight = roundDownToMultiple(
// boundingBoxHeight,
// 64
// );
const newBoundingBoxX = roundDownToMultiple( // const overflowX = boundingBoxX + boundingBoxWidth - canvasWidth;
overflowX > 0 ? boundingBoxX - overflowX : boundingBoxX, // const overflowY = boundingBoxY + boundingBoxHeight - canvasHeight;
64
);
const newBoundingBoxY = roundDownToMultiple( // const newBoundingBoxWidth = _.clamp(
overflowY > 0 ? boundingBoxY - overflowY : boundingBoxY, // roundedBoundingBoxWidth,
64 // 64,
); // roundedCanvasWidth
// );
state.boundingBoxDimensions = { // const newBoundingBoxHeight = _.clamp(
width: roundDownToMultiple(boundingBoxWidth, 64), // roundedBoundingBoxHeight,
height: roundDownToMultiple(boundingBoxHeight, 64), // 64,
}; // roundedCanvasHeight
// );
state.boundingBoxCoordinate = { // const overflowCorrectedX =
x: newBoundingBoxX, // overflowX > 0 ? boundingBoxX - overflowX : boundingBoxX;
y: newBoundingBoxY,
}; // const overflowCorrectedY =
// overflowY > 0 ? boundingBoxY - overflowY : boundingBoxY;
// const clampedX = _.clamp(
// overflowCorrectedX,
// 64,
// roundedCanvasWidth - newBoundingBoxWidth
// );
// const clampedY = _.clamp(
// overflowCorrectedY,
// 64,
// roundedCanvasHeight - newBoundingBoxHeight
// );
// const newBoundingBoxX = roundDownToMultiple(clampedX, 64);
// const newBoundingBoxY = roundDownToMultiple(clampedY, 64);
// state.boundingBoxDimensions = {
// width: newBoundingBoxWidth,
// height: newBoundingBoxHeight,
// };
// state.boundingBoxCoordinate = {
// x: newBoundingBoxX,
// y: newBoundingBoxY,
// };
}, },
setBoundingBoxCoordinate: (state, action: PayloadAction<Vector2d>) => { setBoundingBoxCoordinate: (state, action: PayloadAction<Vector2d>) => {
state.boundingBoxCoordinate = action.payload; state.boundingBoxCoordinate = action.payload;
@ -256,12 +289,6 @@ export const inpaintingSlice = createSlice({
setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => { setBoundingBoxPreviewFill: (state, action: PayloadAction<RgbaColor>) => {
state.boundingBoxPreviewFill = action.payload; state.boundingBoxPreviewFill = action.payload;
}, },
setBoundingBoxPreviewType: (
state,
action: PayloadAction<BoundingBoxPreviewType>
) => {
state.boundingBoxPreviewType = action.payload;
},
setNeedsRepaint: (state, action: PayloadAction<boolean>) => { setNeedsRepaint: (state, action: PayloadAction<boolean>) => {
state.needsRepaint = action.payload; state.needsRepaint = action.payload;
}, },
@ -269,6 +296,15 @@ export const inpaintingSlice = createSlice({
state.stageScale = action.payload; state.stageScale = action.payload;
state.needsRepaint = false; state.needsRepaint = false;
}, },
setShouldShowBoundingBoxFill: (state, action: PayloadAction<boolean>) => {
state.shouldShowBoundingBoxFill = action.payload;
},
setIsBoundingBoxTransforming: (state, action: PayloadAction<boolean>) => {
state.isBoundingBoxTransforming = action.payload;
},
setIsDrawing: (state, action: PayloadAction<boolean>) => {
state.isDrawing = action.payload;
},
}, },
}); });
@ -283,7 +319,6 @@ export const {
setShouldShowBrushPreview, setShouldShowBrushPreview,
setMaskColor, setMaskColor,
clearMask, clearMask,
setMaskOpacity,
undo, undo,
redo, redo,
setCursorPosition, setCursorPosition,
@ -293,11 +328,13 @@ export const {
setBoundingBoxCoordinate, setBoundingBoxCoordinate,
setIsMovingBoundingBox, setIsMovingBoundingBox,
setBoundingBoxPreviewFill, setBoundingBoxPreviewFill,
setBoundingBoxPreviewType,
setNeedsRepaint, setNeedsRepaint,
setStageScale, setStageScale,
toggleTool, toggleTool,
toggleIsMovingBoundingBox, toggleIsMovingBoundingBox,
setShouldShowBoundingBoxFill,
setIsBoundingBoxTransforming,
setIsDrawing,
} = inpaintingSlice.actions; } = inpaintingSlice.actions;
export default inpaintingSlice.reducer; export default inpaintingSlice.reducer;

View File

@ -4,6 +4,18 @@ import { RootState } from '../../../app/store';
import { OptionsState } from '../../options/optionsSlice'; import { OptionsState } from '../../options/optionsSlice';
import { tabMap } from '../InvokeTabs'; import { tabMap } from '../InvokeTabs';
import { InpaintingState } from './inpaintingSlice'; import { InpaintingState } from './inpaintingSlice';
import { rgbaColorToRgbString } from './util/colorToString';
export const inpaintingCanvasLinesSelector = createSelector(
(state: RootState) => state.inpainting,
(inpainting: InpaintingState) => {
const { lines, maskColor } = inpainting;
return {
lines,
maskColorString: rgbaColorToRgbString(maskColor),
};
}
);
export const inpaintingControlsSelector = createSelector( export const inpaintingControlsSelector = createSelector(
[(state: RootState) => state.inpainting, (state: RootState) => state.options], [(state: RootState) => state.inpainting, (state: RootState) => state.options],
@ -12,7 +24,6 @@ export const inpaintingControlsSelector = createSelector(
tool, tool,
brushSize, brushSize,
maskColor, maskColor,
maskOpacity,
shouldInvertMask, shouldInvertMask,
shouldShowMask, shouldShowMask,
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
@ -20,6 +31,7 @@ export const inpaintingControlsSelector = createSelector(
pastLines, pastLines,
futureLines, futureLines,
isMovingBoundingBox, isMovingBoundingBox,
shouldShowBoundingBoxFill,
} = inpainting; } = inpainting;
const { activeTab, showDualDisplay } = options; const { activeTab, showDualDisplay } = options;
@ -28,7 +40,6 @@ export const inpaintingControlsSelector = createSelector(
tool, tool,
brushSize, brushSize,
maskColor, maskColor,
maskOpacity,
shouldInvertMask, shouldInvertMask,
shouldShowMask, shouldShowMask,
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
@ -38,6 +49,7 @@ export const inpaintingControlsSelector = createSelector(
isMovingBoundingBox, isMovingBoundingBox,
activeTabName: tabMap[activeTab], activeTabName: tabMap[activeTab],
showDualDisplay, showDualDisplay,
shouldShowBoundingBoxFill,
}; };
}, },
{ {
@ -58,13 +70,13 @@ export const inpaintingCanvasSelector = createSelector(
shouldShowMask, shouldShowMask,
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
shouldShowBrushPreview, shouldShowBrushPreview,
maskOpacity,
imageToInpaint, imageToInpaint,
isMovingBoundingBox, isMovingBoundingBox,
boundingBoxDimensions, boundingBoxDimensions,
canvasDimensions, canvasDimensions,
boundingBoxCoordinate, boundingBoxCoordinate,
stageScale, stageScale,
shouldShowBoundingBoxFill,
} = inpainting; } = inpainting;
return { return {
tool, tool,
@ -74,13 +86,13 @@ export const inpaintingCanvasSelector = createSelector(
shouldShowMask, shouldShowMask,
shouldShowCheckboardTransparency, shouldShowCheckboardTransparency,
shouldShowBrushPreview, shouldShowBrushPreview,
maskOpacity,
imageToInpaint, imageToInpaint,
isMovingBoundingBox, isMovingBoundingBox,
boundingBoxDimensions, boundingBoxDimensions,
canvasDimensions, canvasDimensions,
boundingBoxCoordinate, boundingBoxCoordinate,
stageScale, stageScale,
shouldShowBoundingBoxFill,
}; };
}, },
{ {

View File

@ -5,6 +5,11 @@ export const rgbaColorToString = (color: RgbaColor): string => {
return `rgba(${r}, ${g}, ${b}, ${a})`; return `rgba(${r}, ${g}, ${b}, ${a})`;
}; };
export const rgbaColorToRgbString = (color: RgbaColor): string => {
const { r, g, b } = color;
return `rgba(${r}, ${g}, ${b})`;
};
export const rgbColorToString = (color: RgbColor): string => { export const rgbColorToString = (color: RgbColor): string => {
const { r, g, b } = color; const { r, g, b } = color;
return `rgba(${r}, ${g}, ${b})`; return `rgba(${r}, ${g}, ${b})`;

View File

@ -3,3 +3,8 @@ export const DASH_WIDTH = 4;
// speed of marching ants (lower is faster) // speed of marching ants (lower is faster)
export const MARCHING_ANTS_SPEED = 30; export const MARCHING_ANTS_SPEED = 30;
// bounding box anchor size
export const TRANSFORMER_ANCHOR_SIZE = 10;