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(() => {
|
||||
const tool = useAppSelector((s) => s.canvasV2.tool.selected);
|
||||
const canvasManager = useStore($canvasManager);
|
||||
|
||||
const onChangeDebugging = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!canvasManager) {
|
||||
|
@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt
|
||||
import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton';
|
||||
import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton';
|
||||
import { RectToolButton } from 'features/controlLayers/components/RectToolButton';
|
||||
import { ToolEyeDropperButton } from 'features/controlLayers/components/ToolEyeDropperButton';
|
||||
import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton';
|
||||
import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton';
|
||||
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
||||
@ -24,6 +25,7 @@ export const ToolChooser: React.FC = () => {
|
||||
<MoveToolButton />
|
||||
<ViewToolButton />
|
||||
<BboxToolButton />
|
||||
<ToolEyeDropperButton />
|
||||
</ButtonGroup>
|
||||
<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,
|
||||
entitySelected,
|
||||
eraserWidthChanged,
|
||||
fillChanged,
|
||||
rasterLayerCompositeRasterized,
|
||||
toolBufferChanged,
|
||||
toolChanged,
|
||||
@ -144,6 +145,9 @@ export class CanvasStateApi {
|
||||
log.trace({ toolBuffer }, 'Setting tool buffer');
|
||||
this._store.dispatch(toolBufferChanged(toolBuffer));
|
||||
};
|
||||
setFill = (fill: RgbaColor) => {
|
||||
return this._store.dispatch(fillChanged(fill));
|
||||
};
|
||||
|
||||
getBbox = () => {
|
||||
return this.getState().bbox;
|
||||
@ -257,6 +261,7 @@ export class CanvasStateApi {
|
||||
$currentFill: WritableAtom<RgbaColor> = atom();
|
||||
$selectedEntity: WritableAtom<EntityStateAndAdapter | null> = atom();
|
||||
$selectedEntityIdentifier: WritableAtom<CanvasEntityIdentifier | null> = atom();
|
||||
$colorUnderCursor: WritableAtom<RgbaColor | null> = atom();
|
||||
|
||||
// Read-write state, ephemeral interaction state
|
||||
$isDrawing = $isDrawing;
|
||||
|
@ -35,6 +35,11 @@ export class CanvasTool {
|
||||
innerBorderCircle: Konva.Circle;
|
||||
outerBorderCircle: Konva.Circle;
|
||||
};
|
||||
eyeDropper: {
|
||||
group: Konva.Group;
|
||||
fillCircle: Konva.Circle;
|
||||
transparentCenterCircle: Konva.Circle;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -96,6 +101,26 @@ export class CanvasTool {
|
||||
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.innerBorderCircle);
|
||||
@ -107,6 +132,10 @@ export class CanvasTool {
|
||||
this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle);
|
||||
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.manager.stateApi.$stageAttrs.listen(() => {
|
||||
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() {
|
||||
const stage = this.manager.stage;
|
||||
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 isDrawing = this.manager.stateApi.$isDrawing.get();
|
||||
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
||||
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
|
||||
|
||||
const tool = toolState.selected;
|
||||
|
||||
@ -180,6 +216,8 @@ export class CanvasTool {
|
||||
stage.container().style.cursor = 'none';
|
||||
} else if (tool === 'bbox') {
|
||||
stage.container().style.cursor = 'default';
|
||||
} else if (tool === 'eyeDropper') {
|
||||
stage.container().style.cursor = 'none';
|
||||
}
|
||||
|
||||
stage.draggable(tool === 'view');
|
||||
@ -216,9 +254,7 @@ export class CanvasTool {
|
||||
});
|
||||
|
||||
this.scaleTool();
|
||||
|
||||
this.konva.brush.group.visible(true);
|
||||
this.konva.eraser.group.visible(false);
|
||||
this.setToolVisibility('brush');
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
||||
|
||||
@ -243,12 +279,20 @@ export class CanvasTool {
|
||||
});
|
||||
|
||||
this.scaleTool();
|
||||
|
||||
this.konva.brush.group.visible(false);
|
||||
this.konva.eraser.group.visible(true);
|
||||
this.setToolVisibility('eraser');
|
||||
} else if (cursorPos && colorUnderCursor) {
|
||||
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 {
|
||||
this.konva.brush.group.visible(false);
|
||||
this.konva.eraser.group.visible(false);
|
||||
this.setToolVisibility('none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import type {
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
RgbaColor,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types';
|
||||
@ -115,6 +116,23 @@ const getLastPointOfLastLineOfEntity = (
|
||||
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) => {
|
||||
const { stage, stateApi } = manager;
|
||||
const {
|
||||
@ -174,6 +192,14 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||
const selectedEntity = getSelectedEntity();
|
||||
|
||||
if (toolState.selected === 'eyeDropper') {
|
||||
const color = getColorUnderCursor(stage);
|
||||
manager.stateApi.$colorUnderCursor.set(color);
|
||||
if (color) {
|
||||
manager.stateApi.setFill(color);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
pos &&
|
||||
selectedEntity &&
|
||||
@ -322,6 +348,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||
const selectedEntity = getSelectedEntity();
|
||||
|
||||
if (toolState.selected === 'eyeDropper') {
|
||||
const color = getColorUnderCursor(stage);
|
||||
manager.stateApi.$colorUnderCursor.set(color);
|
||||
}
|
||||
|
||||
if (
|
||||
pos &&
|
||||
selectedEntity &&
|
||||
|
@ -469,7 +469,7 @@ export const IMAGE_FILTERS: { [key in FilterConfig['type']]: ImageFilterData<key
|
||||
},
|
||||
} 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 function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' {
|
||||
return tool === 'brush' || tool === 'eraser' || tool === 'rect';
|
||||
|
Loading…
Reference in New Issue
Block a user