mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): rough out eyedropper tool
It's a bit slow bc we are converting the stage to canvas on every mouse move. Also need to improve the visual but it works.
This commit is contained in:
parent
11010236b3
commit
31ace5fb0c
@ -20,7 +20,6 @@ import { memo, useCallback } from 'react';
|
|||||||
export const ControlLayersToolbar = memo(() => {
|
export const ControlLayersToolbar = memo(() => {
|
||||||
const tool = useAppSelector((s) => s.canvasV2.tool.selected);
|
const tool = useAppSelector((s) => s.canvasV2.tool.selected);
|
||||||
const canvasManager = useStore($canvasManager);
|
const canvasManager = useStore($canvasManager);
|
||||||
|
|
||||||
const onChangeDebugging = useCallback(
|
const onChangeDebugging = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!canvasManager) {
|
if (!canvasManager) {
|
||||||
|
@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt
|
|||||||
import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton';
|
import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton';
|
||||||
import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton';
|
import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton';
|
||||||
import { RectToolButton } from 'features/controlLayers/components/RectToolButton';
|
import { RectToolButton } from 'features/controlLayers/components/RectToolButton';
|
||||||
|
import { ToolEyeDropperButton } from 'features/controlLayers/components/ToolEyeDropperButton';
|
||||||
import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton';
|
import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton';
|
||||||
import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton';
|
import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton';
|
||||||
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
||||||
@ -24,6 +25,7 @@ export const ToolChooser: React.FC = () => {
|
|||||||
<MoveToolButton />
|
<MoveToolButton />
|
||||||
<ViewToolButton />
|
<ViewToolButton />
|
||||||
<BboxToolButton />
|
<BboxToolButton />
|
||||||
|
<ToolEyeDropperButton />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<TransformToolButton />
|
<TransformToolButton />
|
||||||
</>
|
</>
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiEyedropperBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const ToolEyeDropperButton = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming);
|
||||||
|
const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eyeDropper');
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(toolChanged('eyeDropper'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useHotkeys('i', onClick, { enabled: !isDisabled }, [onClick, isDisabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('controlLayers.eyeDropper')} (I)`}
|
||||||
|
tooltip={`${t('controlLayers.eyeDropper')} (I)`}
|
||||||
|
icon={<PiEyedropperBold />}
|
||||||
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClick}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ToolEyeDropperButton.displayName = 'ToolEyeDropperButton';
|
@ -26,6 +26,7 @@ import {
|
|||||||
entityReset,
|
entityReset,
|
||||||
entitySelected,
|
entitySelected,
|
||||||
eraserWidthChanged,
|
eraserWidthChanged,
|
||||||
|
fillChanged,
|
||||||
rasterLayerCompositeRasterized,
|
rasterLayerCompositeRasterized,
|
||||||
toolBufferChanged,
|
toolBufferChanged,
|
||||||
toolChanged,
|
toolChanged,
|
||||||
@ -144,6 +145,9 @@ export class CanvasStateApi {
|
|||||||
log.trace({ toolBuffer }, 'Setting tool buffer');
|
log.trace({ toolBuffer }, 'Setting tool buffer');
|
||||||
this._store.dispatch(toolBufferChanged(toolBuffer));
|
this._store.dispatch(toolBufferChanged(toolBuffer));
|
||||||
};
|
};
|
||||||
|
setFill = (fill: RgbaColor) => {
|
||||||
|
return this._store.dispatch(fillChanged(fill));
|
||||||
|
};
|
||||||
|
|
||||||
getBbox = () => {
|
getBbox = () => {
|
||||||
return this.getState().bbox;
|
return this.getState().bbox;
|
||||||
@ -257,6 +261,7 @@ export class CanvasStateApi {
|
|||||||
$currentFill: WritableAtom<RgbaColor> = atom();
|
$currentFill: WritableAtom<RgbaColor> = atom();
|
||||||
$selectedEntity: WritableAtom<EntityStateAndAdapter | null> = atom();
|
$selectedEntity: WritableAtom<EntityStateAndAdapter | null> = atom();
|
||||||
$selectedEntityIdentifier: WritableAtom<CanvasEntityIdentifier | null> = atom();
|
$selectedEntityIdentifier: WritableAtom<CanvasEntityIdentifier | null> = atom();
|
||||||
|
$colorUnderCursor: WritableAtom<RgbaColor | null> = atom();
|
||||||
|
|
||||||
// Read-write state, ephemeral interaction state
|
// Read-write state, ephemeral interaction state
|
||||||
$isDrawing = $isDrawing;
|
$isDrawing = $isDrawing;
|
||||||
|
@ -35,6 +35,11 @@ export class CanvasTool {
|
|||||||
innerBorderCircle: Konva.Circle;
|
innerBorderCircle: Konva.Circle;
|
||||||
outerBorderCircle: Konva.Circle;
|
outerBorderCircle: Konva.Circle;
|
||||||
};
|
};
|
||||||
|
eyeDropper: {
|
||||||
|
group: Konva.Group;
|
||||||
|
fillCircle: Konva.Circle;
|
||||||
|
transparentCenterCircle: Konva.Circle;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,6 +101,26 @@ export class CanvasTool {
|
|||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
eyeDropper: {
|
||||||
|
group: new Konva.Group({ name: `${this.type}:eyeDropper_group` }),
|
||||||
|
fillCircle: new Konva.Circle({
|
||||||
|
name: `${this.type}:eyeDropper_fill_circle`,
|
||||||
|
listening: false,
|
||||||
|
fill: '',
|
||||||
|
radius: 20,
|
||||||
|
strokeWidth: 1,
|
||||||
|
stroke: 'black',
|
||||||
|
strokeScaleEnabled: false,
|
||||||
|
}),
|
||||||
|
transparentCenterCircle: new Konva.Circle({
|
||||||
|
name: `${this.type}:eyeDropper_fill_circle`,
|
||||||
|
listening: false,
|
||||||
|
strokeEnabled: false,
|
||||||
|
fill: 'white',
|
||||||
|
radius: 5,
|
||||||
|
globalCompositeOperation: 'destination-out',
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
this.konva.brush.group.add(this.konva.brush.fillCircle);
|
this.konva.brush.group.add(this.konva.brush.fillCircle);
|
||||||
this.konva.brush.group.add(this.konva.brush.innerBorderCircle);
|
this.konva.brush.group.add(this.konva.brush.innerBorderCircle);
|
||||||
@ -107,6 +132,10 @@ export class CanvasTool {
|
|||||||
this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle);
|
this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle);
|
||||||
this.konva.group.add(this.konva.eraser.group);
|
this.konva.group.add(this.konva.eraser.group);
|
||||||
|
|
||||||
|
this.konva.eyeDropper.group.add(this.konva.eyeDropper.fillCircle);
|
||||||
|
this.konva.eyeDropper.group.add(this.konva.eyeDropper.transparentCenterCircle);
|
||||||
|
this.konva.group.add(this.konva.eyeDropper.group);
|
||||||
|
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.manager.stateApi.$stageAttrs.listen(() => {
|
this.manager.stateApi.$stageAttrs.listen(() => {
|
||||||
this.render();
|
this.render();
|
||||||
@ -146,6 +175,12 @@ export class CanvasTool {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setToolVisibility = (tool: 'brush' | 'eraser' | 'eyeDropper' | 'none') => {
|
||||||
|
this.konva.brush.group.visible(tool === 'brush');
|
||||||
|
this.konva.eraser.group.visible(tool === 'eraser');
|
||||||
|
this.konva.eyeDropper.group.visible(tool === 'eyeDropper');
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const stage = this.manager.stage;
|
const stage = this.manager.stage;
|
||||||
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
||||||
@ -154,6 +189,7 @@ export class CanvasTool {
|
|||||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||||
const isDrawing = this.manager.stateApi.$isDrawing.get();
|
const isDrawing = this.manager.stateApi.$isDrawing.get();
|
||||||
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
||||||
|
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
|
||||||
|
|
||||||
const tool = toolState.selected;
|
const tool = toolState.selected;
|
||||||
|
|
||||||
@ -180,6 +216,8 @@ export class CanvasTool {
|
|||||||
stage.container().style.cursor = 'none';
|
stage.container().style.cursor = 'none';
|
||||||
} else if (tool === 'bbox') {
|
} else if (tool === 'bbox') {
|
||||||
stage.container().style.cursor = 'default';
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (tool === 'eyeDropper') {
|
||||||
|
stage.container().style.cursor = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
stage.draggable(tool === 'view');
|
stage.draggable(tool === 'view');
|
||||||
@ -216,9 +254,7 @@ export class CanvasTool {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.scaleTool();
|
this.scaleTool();
|
||||||
|
this.setToolVisibility('brush');
|
||||||
this.konva.brush.group.visible(true);
|
|
||||||
this.konva.eraser.group.visible(false);
|
|
||||||
} else if (cursorPos && tool === 'eraser') {
|
} else if (cursorPos && tool === 'eraser') {
|
||||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
||||||
|
|
||||||
@ -243,12 +279,20 @@ export class CanvasTool {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.scaleTool();
|
this.scaleTool();
|
||||||
|
this.setToolVisibility('eraser');
|
||||||
this.konva.brush.group.visible(false);
|
} else if (cursorPos && colorUnderCursor) {
|
||||||
this.konva.eraser.group.visible(true);
|
this.konva.eyeDropper.fillCircle.setAttrs({
|
||||||
|
x: cursorPos.x,
|
||||||
|
y: cursorPos.y,
|
||||||
|
fill: rgbaColorToString(colorUnderCursor),
|
||||||
|
});
|
||||||
|
this.konva.eyeDropper.transparentCenterCircle.setAttrs({
|
||||||
|
x: cursorPos.x,
|
||||||
|
y: cursorPos.y,
|
||||||
|
});
|
||||||
|
this.setToolVisibility('eyeDropper');
|
||||||
} else {
|
} else {
|
||||||
this.konva.brush.group.visible(false);
|
this.setToolVisibility('none');
|
||||||
this.konva.eraser.group.visible(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import type {
|
|||||||
CanvasRegionalGuidanceState,
|
CanvasRegionalGuidanceState,
|
||||||
CanvasV2State,
|
CanvasV2State,
|
||||||
Coordinate,
|
Coordinate,
|
||||||
|
RgbaColor,
|
||||||
Tool,
|
Tool,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types';
|
import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types';
|
||||||
@ -115,6 +116,23 @@ const getLastPointOfLastLineOfEntity = (
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getColorUnderCursor = (stage: Konva.Stage): RgbaColor | null => {
|
||||||
|
const pos = stage.getPointerPosition();
|
||||||
|
if (!pos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ctx = stage.toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1 }).getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
||||||
|
if (r === undefined || g === undefined || b === undefined || a === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { r, g, b, a };
|
||||||
|
};
|
||||||
|
|
||||||
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||||
const { stage, stateApi } = manager;
|
const { stage, stateApi } = manager;
|
||||||
const {
|
const {
|
||||||
@ -174,6 +192,14 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||||
const selectedEntity = getSelectedEntity();
|
const selectedEntity = getSelectedEntity();
|
||||||
|
|
||||||
|
if (toolState.selected === 'eyeDropper') {
|
||||||
|
const color = getColorUnderCursor(stage);
|
||||||
|
manager.stateApi.$colorUnderCursor.set(color);
|
||||||
|
if (color) {
|
||||||
|
manager.stateApi.setFill(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pos &&
|
pos &&
|
||||||
selectedEntity &&
|
selectedEntity &&
|
||||||
@ -322,6 +348,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||||
const selectedEntity = getSelectedEntity();
|
const selectedEntity = getSelectedEntity();
|
||||||
|
|
||||||
|
if (toolState.selected === 'eyeDropper') {
|
||||||
|
const color = getColorUnderCursor(stage);
|
||||||
|
manager.stateApi.$colorUnderCursor.set(color);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pos &&
|
pos &&
|
||||||
selectedEntity &&
|
selectedEntity &&
|
||||||
|
@ -469,7 +469,7 @@ export const IMAGE_FILTERS: { [key in FilterConfig['type']]: ImageFilterData<key
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']);
|
const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox', 'eyeDropper']);
|
||||||
export type Tool = z.infer<typeof zTool>;
|
export type Tool = z.infer<typeof zTool>;
|
||||||
export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' {
|
export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' {
|
||||||
return tool === 'brush' || tool === 'eraser' || tool === 'rect';
|
return tool === 'brush' || tool === 'eraser' || tool === 'rect';
|
||||||
|
Loading…
Reference in New Issue
Block a user