Adds color picker

This commit is contained in:
psychedelicious 2022-11-23 20:21:48 +11:00 committed by blessedcoolant
parent d44112c209
commit 9f1c1cf2e6
11 changed files with 309 additions and 132 deletions

View File

@ -7,7 +7,7 @@ import {
isStagingSelector,
} from 'features/canvas/store/canvasSelectors';
import IAICanvasMaskLines from './IAICanvasMaskLines';
import IAICanvasBrushPreview from './IAICanvasBrushPreview';
import IAICanvasToolPreview from './IAICanvasToolPreview';
import { Vector2d } from 'konva/lib/types';
import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox';
import useCanvasHotkeys from '../hooks/useCanvasHotkeys';
@ -183,7 +183,7 @@ const IAICanvas = () => {
</Layer>
<Layer id="preview" imageSmoothingEnabled={false}>
{!isStaging && (
<IAICanvasBrushPreview
<IAICanvasToolPreview
visible={tool !== 'move'}
listening={false}
/>

View File

@ -1,120 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { GroupConfig } from 'konva/lib/Group';
import _ from 'lodash';
import { Circle, Group } from 'react-konva';
import { useAppSelector } from 'app/store';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
const canvasBrushPreviewSelector = createSelector(
canvasSelector,
(canvas) => {
const {
cursorPosition,
stageDimensions: { width, height },
brushSize,
maskColor,
brushColor,
tool,
layer,
shouldShowBrush,
isMovingBoundingBox,
isTransformingBoundingBox,
stageScale,
} = canvas;
return {
cursorPosition,
width,
height,
radius: brushSize / 2,
brushColorString: rgbaColorToString(
layer === 'mask' ? { ...maskColor, a: 0.5 } : brushColor
),
tool,
shouldShowBrush,
shouldDrawBrushPreview:
!(
isMovingBoundingBox ||
isTransformingBoundingBox ||
!cursorPosition
) && shouldShowBrush,
strokeWidth: 1.5 / stageScale,
dotRadius: 1.5 / stageScale,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
/**
* Draws a black circle around the canvas brush preview.
*/
const IAICanvasBrushPreview = (props: GroupConfig) => {
const { ...rest } = props;
const {
cursorPosition,
width,
height,
radius,
brushColorString,
tool,
shouldDrawBrushPreview,
dotRadius,
strokeWidth,
} = useAppSelector(canvasBrushPreviewSelector);
if (!shouldDrawBrushPreview) return null;
return (
<Group listening={false} {...rest}>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={radius}
fill={brushColorString}
listening={false}
globalCompositeOperation={
tool === 'eraser' ? 'destination-out' : 'source-over'
}
/>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={radius}
stroke={'rgba(255,255,255,0.4)'}
strokeWidth={strokeWidth * 2}
strokeEnabled={true}
listening={false}
/>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={radius}
stroke={'rgba(0,0,0,1)'}
strokeWidth={strokeWidth}
strokeEnabled={true}
listening={false}
/>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={dotRadius * 2}
fill={'rgba(255,255,255,0.4)'}
listening={false}
/>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={dotRadius}
fill={'rgba(0,0,0,1)'}
listening={false}
/>
</Group>
);
};
export default IAICanvasBrushPreview;

View File

@ -0,0 +1,185 @@
import { createSelector } from '@reduxjs/toolkit';
import { GroupConfig } from 'konva/lib/Group';
import _ from 'lodash';
import { Circle, Group, Rect } from 'react-konva';
import { useAppDispatch, useAppSelector } from 'app/store';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { COLOR_PICKER_SIZE } from '../util/constants';
const canvasBrushPreviewSelector = createSelector(
canvasSelector,
(canvas) => {
const {
cursorPosition,
stageDimensions: { width, height },
brushSize,
colorPickerColor,
maskColor,
brushColor,
tool,
layer,
shouldShowBrush,
isMovingBoundingBox,
isTransformingBoundingBox,
stageScale,
} = canvas;
let fill = '';
if (layer === 'mask') {
fill = rgbaColorToString({ ...maskColor, a: 0.5 });
} else if (tool === 'colorPicker') {
fill = rgbaColorToString(colorPickerColor);
} else {
fill = rgbaColorToString(brushColor);
}
return {
cursorPosition,
width,
height,
radius: brushSize / 2,
colorPickerSize: COLOR_PICKER_SIZE / stageScale,
colorPickerOffset: COLOR_PICKER_SIZE / 2 / stageScale,
colorPickerCornerRadius: COLOR_PICKER_SIZE / 5 / stageScale,
brushColorString: fill,
tool,
shouldShowBrush,
shouldDrawBrushPreview:
!(
isMovingBoundingBox ||
isTransformingBoundingBox ||
!cursorPosition
) && shouldShowBrush,
strokeWidth: 1.5 / stageScale,
dotRadius: 1.5 / stageScale,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
/**
* Draws a black circle around the canvas brush preview.
*/
const IAICanvasToolPreview = (props: GroupConfig) => {
const { ...rest } = props;
const {
cursorPosition,
width,
height,
radius,
brushColorString,
tool,
shouldDrawBrushPreview,
dotRadius,
strokeWidth,
colorPickerSize,
colorPickerOffset,
colorPickerCornerRadius,
} = useAppSelector(canvasBrushPreviewSelector);
if (!shouldDrawBrushPreview) return null;
return (
<Group listening={false} {...rest}>
{tool === 'colorPicker' ? (
<>
<Rect
x={
cursorPosition ? cursorPosition.x - colorPickerOffset : width / 2
}
y={
cursorPosition ? cursorPosition.y - colorPickerOffset : height / 2
}
width={colorPickerSize}
height={colorPickerSize}
fill={brushColorString}
cornerRadius={colorPickerCornerRadius}
listening={false}
/>
<Rect
x={
cursorPosition ? cursorPosition.x - colorPickerOffset : width / 2
}
y={
cursorPosition ? cursorPosition.y - colorPickerOffset : height / 2
}
width={colorPickerSize}
height={colorPickerSize}
cornerRadius={colorPickerCornerRadius}
stroke={'rgba(255,255,255,0.4)'}
strokeWidth={strokeWidth * 2}
strokeEnabled={true}
listening={false}
/>
<Rect
x={
cursorPosition ? cursorPosition.x - colorPickerOffset : width / 2
}
y={
cursorPosition ? cursorPosition.y - colorPickerOffset : height / 2
}
width={colorPickerSize}
height={colorPickerSize}
cornerRadius={colorPickerCornerRadius}
stroke={'rgba(0,0,0,1)'}
strokeWidth={strokeWidth}
strokeEnabled={true}
listening={false}
/>
</>
) : (
<>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={radius}
fill={brushColorString}
globalCompositeOperation={
tool === 'eraser' ? 'destination-out' : 'source-over'
}
/>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={radius}
stroke={'rgba(255,255,255,0.4)'}
strokeWidth={strokeWidth * 2}
strokeEnabled={true}
listening={false}
/>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={radius}
stroke={'rgba(0,0,0,1)'}
strokeWidth={strokeWidth}
strokeEnabled={true}
listening={false}
/>
</>
)}
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={dotRadius * 2}
fill={'rgba(255,255,255,0.4)'}
listening={false}
/>
<Circle
x={cursorPosition ? cursorPosition.x : width / 2}
y={cursorPosition ? cursorPosition.y : height / 2}
radius={dotRadius}
fill={'rgba(0,0,0,1)'}
listening={false}
/>
</Group>
);
};
export default IAICanvasToolPreview;

View File

@ -8,7 +8,12 @@ import {
import { useAppDispatch, useAppSelector } from 'app/store';
import _ from 'lodash';
import IAIIconButton from 'common/components/IAIIconButton';
import { FaEraser, FaPaintBrush, FaSlidersH } from 'react-icons/fa';
import {
FaEraser,
FaEyeDropper,
FaPaintBrush,
FaSlidersH,
} from 'react-icons/fa';
import {
canvasSelector,
isStagingSelector,
@ -68,6 +73,18 @@ const IAICanvasToolChooserOptions = () => {
[tool]
);
useHotkeys(
['c'],
() => {
handleSelectColorPickerTool();
},
{
enabled: () => true,
preventDefault: true,
},
[tool]
);
useHotkeys(
['['],
() => {
@ -94,6 +111,7 @@ const IAICanvasToolChooserOptions = () => {
const handleSelectBrushTool = () => dispatch(setTool('brush'));
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
const handleSelectColorPickerTool = () => dispatch(setTool('colorPicker'));
return (
<ButtonGroup isAttached>
@ -111,7 +129,15 @@ const IAICanvasToolChooserOptions = () => {
icon={<FaEraser />}
data-selected={tool === 'eraser' && !isStaging}
isDisabled={isStaging}
onClick={() => dispatch(setTool('eraser'))}
onClick={handleSelectEraserTool}
/>
<IAIIconButton
aria-label="Color Picker (C)"
tooltip="Color Picker (C)"
icon={<FaEyeDropper />}
data-selected={tool === 'colorPicker' && !isStaging}
isDisabled={isStaging}
onClick={handleSelectColorPickerTool}
/>
<IAIPopover
trigger="hover"

View File

@ -5,13 +5,19 @@ import Konva from 'konva';
import { KonvaEventObject } from 'konva/lib/Node';
import _ from 'lodash';
import { MutableRefObject, useCallback } from 'react';
import { canvasSelector, isStagingSelector } from 'features/canvas/store/canvasSelectors';
import {
canvasSelector,
isStagingSelector,
} from 'features/canvas/store/canvasSelectors';
import {
addLine,
commitColorPickerColor,
setIsDrawing,
setIsMovingStage,
setTool,
} from 'features/canvas/store/canvasSlice';
import getScaledCursorPosition from '../util/getScaledCursorPosition';
import useColorPicker from './useColorUnderCursor';
const selector = createSelector(
[activeTabNameSelector, canvasSelector, isStagingSelector],
@ -29,6 +35,7 @@ const selector = createSelector(
const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const dispatch = useAppDispatch();
const { tool, isStaging } = useAppSelector(selector);
const { commitColorUnderCursor } = useColorPicker();
return useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
@ -41,6 +48,11 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
return;
}
if (tool === 'colorPicker') {
commitColorUnderCursor();
return;
}
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
if (!scaledCursorPosition) return;

View File

@ -5,12 +5,16 @@ import Konva from 'konva';
import { Vector2d } from 'konva/lib/types';
import _ from 'lodash';
import { MutableRefObject, useCallback } from 'react';
import { canvasSelector, isStagingSelector } from 'features/canvas/store/canvasSelectors';
import {
canvasSelector,
isStagingSelector,
} from 'features/canvas/store/canvasSelectors';
import {
addPointToCurrentLine,
setCursorPosition,
} from 'features/canvas/store/canvasSlice';
import getScaledCursorPosition from '../util/getScaledCursorPosition';
import useColorPicker from './useColorUnderCursor';
const selector = createSelector(
[activeTabNameSelector, canvasSelector, isStagingSelector],
@ -33,6 +37,7 @@ const useCanvasMouseMove = (
) => {
const dispatch = useAppDispatch();
const { isDrawing, tool, isStaging } = useAppSelector(selector);
const { updateColorUnderCursor } = useColorPicker();
return useCallback(() => {
if (!stageRef.current) return;
@ -45,6 +50,11 @@ const useCanvasMouseMove = (
lastCursorPositionRef.current = scaledCursorPosition;
if (tool === 'colorPicker') {
updateColorUnderCursor();
return;
}
if (!isDrawing || tool === 'move' || isStaging) return;
didMouseMoveRef.current = true;
@ -59,6 +69,7 @@ const useCanvasMouseMove = (
lastCursorPositionRef,
stageRef,
tool,
updateColorUnderCursor,
]);
};

View File

@ -0,0 +1,45 @@
import { useAppDispatch } from 'app/store';
import Konva from 'konva';
import _ from 'lodash';
import {
commitColorPickerColor,
setColorPickerColor,
} from '../store/canvasSlice';
import {
getCanvasBaseLayer,
getCanvasStage,
} from '../util/konvaInstanceProvider';
const useColorPicker = () => {
const dispatch = useAppDispatch();
const canvasBaseLayer = getCanvasBaseLayer();
const stage = getCanvasStage();
return {
updateColorUnderCursor: () => {
if (!stage || !canvasBaseLayer) return;
const position = stage.getPointerPosition();
if (!position) return;
const pixelRatio = Konva.pixelRatio;
const [r, g, b, a] = canvasBaseLayer
.getContext()
.getImageData(
position.x * pixelRatio,
position.y * pixelRatio,
1,
1
).data;
dispatch(setColorPickerColor({ r, g, b, a }));
},
commitColorUnderCursor: () => {
dispatch(commitColorPickerColor());
},
};
};
export default useColorPicker;

View File

@ -44,6 +44,7 @@ const initialCanvasState: CanvasState = {
brushColor: { r: 90, g: 90, b: 255, a: 1 },
brushSize: 50,
canvasContainerDimensions: { width: 0, height: 0 },
colorPickerColor: { r: 90, g: 90, b: 255, a: 1 },
cursorPosition: null,
doesCanvasNeedScaling: false,
futureLayerStates: [],
@ -345,7 +346,7 @@ export const canvasSlice = createSlice({
addLine: (state, action: PayloadAction<number[]>) => {
const { tool, layer, brushColor, brushSize } = state;
if (tool === 'move') return;
if (tool === 'move' || tool === 'colorPicker') return;
const newStrokeWidth = brushSize / 2;
@ -683,6 +684,13 @@ export const canvasSlice = createSlice({
) => {
state.shouldCropToBoundingBoxOnSave = action.payload;
},
setColorPickerColor: (state, action: PayloadAction<RgbaColor>) => {
state.colorPickerColor = action.payload;
},
commitColorPickerColor: (state) => {
state.brushColor = state.colorPickerColor;
state.tool = 'brush';
},
setMergedCanvas: (state, action: PayloadAction<CanvasImage>) => {
state.pastLayerStates.push({
...state.layerState,
@ -710,6 +718,8 @@ export const {
addLine,
addPointToCurrentLine,
clearMask,
commitColorPickerColor,
setColorPickerColor,
commitStagingAreaImage,
discardStagedImages,
fitBoundingBoxToStage,

View File

@ -13,7 +13,7 @@ export type CanvasLayer = typeof LAYER_NAMES[number];
export type CanvasDrawingTool = 'brush' | 'eraser';
export type CanvasTool = CanvasDrawingTool | 'move';
export type CanvasTool = CanvasDrawingTool | 'move' | 'colorPicker';
export type Dimensions = {
width: number;
@ -81,6 +81,7 @@ export interface CanvasState {
brushColor: RgbaColor;
brushSize: number;
canvasContainerDimensions: Dimensions;
colorPickerColor: RgbaColor,
cursorPosition: Vector2d | null;
doesCanvasNeedScaling: boolean;
futureLayerStates: CanvasLayerState[];

View File

@ -12,3 +12,5 @@ export const MAX_CANVAS_SCALE = 20;
// padding given to initial image/bounding box when stage view is reset
export const STAGE_PADDING_PERCENTAGE = 0.95;
export const COLOR_PICKER_SIZE = 60;

View File

@ -140,22 +140,22 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
const unifiedCanvasHotkeys = [
{
title: 'Select Brush',
desc: 'Selects the inpainting brush',
desc: 'Selects the canvas brush',
hotkey: 'B',
},
{
title: 'Select Eraser',
desc: 'Selects the inpainting eraser',
desc: 'Selects the canvas eraser',
hotkey: 'E',
},
{
title: 'Decrease Brush Size',
desc: 'Decreases the size of the inpainting brush/eraser',
desc: 'Decreases the size of the canvas brush/eraser',
hotkey: '[',
},
{
title: 'Increase Brush Size',
desc: 'Increases the size of the inpainting brush/eraser',
desc: 'Increases the size of the canvas brush/eraser',
hotkey: ']',
},
{
@ -163,6 +163,11 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
desc: 'Allows canvas navigation',
hotkey: 'V',
},
{
title: 'Select Color Picker',
desc: 'Selects the canvas color picker',
hotkey: 'C',
},
{
title: 'Quick Toggle Move',
desc: 'Temporarily toggles Move mode',