switchable foreground/background colors

This commit is contained in:
Attila Cseh
2025-08-30 14:27:10 +02:00
committed by psychedelicious
parent 27f4af0eb4
commit 031d25ed63
7 changed files with 88 additions and 32 deletions

View File

@@ -589,9 +589,9 @@
"title": "Prev Layer",
"desc": "Select the previous layer in the list."
},
"setFillToWhite": {
"title": "Set Color to White",
"desc": "Set the current tool color to white."
"toggleFillColor": {
"title": "Toggle Fill Color",
"desc": "Toggle the current tool fill color."
},
"filterSelected": {
"title": "Filter",
@@ -2268,6 +2268,8 @@
},
"fill": {
"fillColor": "Fill Color",
"fillColor1": "Color1",
"fillColor2": "Color2",
"fillStyle": "Fill Style",
"solid": "Solid",
"grid": "Grid",

View File

@@ -12,29 +12,49 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { selectCanvasSettingsSlice, settingsColorChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import {
selectCanvasSettingsSlice,
settingsActiveColorToggled,
settingsColor1Changed,
settingsColor2Changed,
} from 'features/controlLayers/store/canvasSettingsSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selectColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.color);
const selectActiveColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.activeColor);
const selectColor1 = createSelector(selectCanvasSettingsSlice, (settings) => settings.color1);
const selectColor2 = createSelector(selectCanvasSettingsSlice, (settings) => settings.color2);
export const ToolColorPicker = memo(() => {
export const ToolFillColorPicker = memo(() => {
const { t } = useTranslation();
const fill = useAppSelector(selectColor);
const activeColorType = useAppSelector(selectActiveColor);
const color1 = useAppSelector(selectColor1);
const color2 = useAppSelector(selectColor2);
const { activeColor, tooltip, color1zIndex, color2zIndex } = useMemo(() => {
if (activeColorType === 'color1') {
return { activeColor: color1, tooltip: t('controlLayers.fill.fillColor1'), color1zIndex: 2, color2zIndex: 1 };
} else {
return { activeColor: color2, tooltip: t('controlLayers.fill.fillColor2'), color1zIndex: 1, color2zIndex: 2 };
}
}, [activeColorType, color1, color2, t]);
const dispatch = useAppDispatch();
const onChange = useCallback(
const onColorChange = useCallback(
(color: RgbaColor) => {
dispatch(settingsColorChanged(color));
if (activeColorType === 'color1') {
dispatch(settingsColor1Changed(color));
} else {
dispatch(settingsColor2Changed(color));
}
},
[dispatch]
[activeColorType, dispatch]
);
useRegisteredHotkeys({
id: 'setFillToWhite',
id: 'toggleFillColor',
category: 'canvas',
callback: () => dispatch(settingsColorChanged({ r: 255, g: 255, b: 255, a: 1 })),
callback: () => dispatch(settingsActiveColorToggled()),
options: { preventDefault: true },
dependencies: [dispatch],
});
@@ -43,15 +63,31 @@ export const ToolColorPicker = memo(() => {
<Popover isLazy>
<PopoverTrigger>
<Flex role="button" aria-label={t('controlLayers.fill.fillColor')} tabIndex={-1} w={8} h={8}>
<Tooltip label={t('controlLayers.fill.fillColor')}>
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<Tooltip label={tooltip}>
<Flex alignItems="center" justifyContent="center" position="relative" w="full" h="full">
<Box
borderRadius="full"
borderColor="base.600"
w={6}
h={6}
borderWidth={2}
bg={rgbaColorToString(fill)}
bg={rgbaColorToString(color1)}
position="absolute"
top="0"
left="0"
zIndex={color1zIndex}
/>
<Box
borderRadius="full"
borderColor="base.600"
w={6}
h={6}
borderWidth={2}
bg={rgbaColorToString(color2)}
position="absolute"
top="2"
left="2"
zIndex={color2zIndex}
/>
</Flex>
</Tooltip>
@@ -60,11 +96,11 @@ export const ToolColorPicker = memo(() => {
<PopoverContent>
<PopoverArrow />
<PopoverBody minH={64}>
<RgbaColorPicker color={fill} onChange={onChange} withNumberInput withSwatches />
<RgbaColorPicker color={activeColor} onChange={onColorChange} withNumberInput withSwatches />
</PopoverBody>
</PopoverContent>
</Popover>
);
});
ToolColorPicker.displayName = 'ToolFillColorPicker';
ToolFillColorPicker.displayName = 'ToolFillColorPicker';

View File

@@ -1,6 +1,6 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
import { CanvasToolbarFitBboxToMasksButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToMasksButton';
@@ -36,7 +36,7 @@ export const CanvasToolbar = memo(() => {
return (
<Flex w="full" gap={2} alignItems="center" px={2}>
<ToolColorPicker />
<ToolFillColorPicker />
<ToolSettings />
<Flex alignItems="center" h="full" flexGrow={1} justifyContent="flex-end">
<CanvasToolbarScale />

View File

@@ -11,7 +11,8 @@ import { createReduxSubscription, getPrefixedId } from 'features/controlLayers/k
import {
selectCanvasSettingsSlice,
settingsBrushWidthChanged,
settingsColorChanged,
settingsColor1Changed,
settingsColor2Changed,
settingsEraserWidthChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import {
@@ -232,7 +233,9 @@ export class CanvasStateApiModule extends CanvasModuleBase {
* Sets the drawing color, pushing state to redux.
*/
setColor = (color: Partial<RgbaColor>) => {
return this.store.dispatch(settingsColorChanged(color));
return this.getSettings().activeColor === 'color1'
? this.store.dispatch(settingsColor1Changed(color))
: this.store.dispatch(settingsColor2Changed(color));
};
/**
@@ -421,7 +424,8 @@ export class CanvasStateApiModule extends CanvasModuleBase {
* consistency with conventional black and white mask images, we use black as the color for these entities.
*/
getCurrentColor = (): RgbaColor => {
let color: RgbaColor = this.getSettings().color;
let color: RgbaColor =
this.getSettings().activeColor === 'color1' ? this.getSettings().color1 : this.getSettings().color2;
const selectedEntity = this.getSelectedEntityAdapter();
if (selectedEntity) {
// These two entity types use a compositing rect for opacity. Their fill is always a solid color.
@@ -449,7 +453,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
// selected entity's fill color with 50% opacity.
return { ...selectedEntity.state.fill.color, a: 0.5 };
} else {
return this.getSettings().color;
return this.getSettings().activeColor === 'color1' ? this.getSettings().color1 : this.getSettings().color2;
}
};

View File

@@ -328,6 +328,7 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
const colorPickerOuterRadius = this.manager.stage.unscale(this.config.RING_OUTER_RADIUS);
const onePixel = this.manager.stage.unscale(1);
const twoPixels = this.manager.stage.unscale(2);
const color = settings.activeColor === 'color1' ? settings.color1 : settings.color2;
this.konva.ringCandidateColor.setAttrs({
x,
@@ -339,7 +340,7 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase {
this.konva.ringCurrentColor.setAttrs({
x,
y,
fill: rgbColorToString(settings.color),
fill: rgbColorToString(color),
innerRadius: colorPickerInnerRadius,
outerRadius: colorPickerOuterRadius,
});

View File

@@ -2,6 +2,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { SliceConfig } from 'app/store/types';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { zRgbaColor } from 'features/controlLayers/store/types';
import { z } from 'zod';
@@ -35,9 +36,11 @@ const zCanvasSettingsState = z.object({
*/
eraserWidth: z.int().gt(0),
/**
* The color to use when drawing lines or filling shapes.
* The colors to use when drawing lines or filling shapes.
*/
color: zRgbaColor,
activeColor: z.enum(['color1', 'color2']),
color1: zRgbaColor,
color2: zRgbaColor,
/**
* Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations.
*
@@ -100,7 +103,9 @@ const getInitialState = (): CanvasSettingsState => ({
invertScrollForToolWidth: false,
brushWidth: 50,
eraserWidth: 50,
color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
activeColor: 'color1',
color1: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500
color2: { r: 0, g: 0, b: 0, a: 1 }, // black
outputOnlyMaskedRegions: true,
autoProcess: true,
snapToGrid: true,
@@ -134,8 +139,14 @@ const slice = createSlice({
settingsEraserWidthChanged: (state, action: PayloadAction<CanvasSettingsState['eraserWidth']>) => {
state.eraserWidth = Math.round(action.payload);
},
settingsColorChanged: (state, action: PayloadAction<Partial<CanvasSettingsState['color']>>) => {
state.color = { ...state.color, ...action.payload };
settingsActiveColorToggled: (state) => {
state.activeColor = state.activeColor === 'color1' ? 'color2' : 'color1';
},
settingsColor1Changed: (state, action: PayloadAction<Partial<RgbaColor>>) => {
state.color1 = { ...state.color1, ...action.payload };
},
settingsColor2Changed: (state, action: PayloadAction<Partial<RgbaColor>>) => {
state.color2 = { ...state.color2, ...action.payload };
},
settingsInvertScrollForToolWidthChanged: (
state,
@@ -191,7 +202,9 @@ export const {
settingsShowHUDToggled,
settingsBrushWidthChanged,
settingsEraserWidthChanged,
settingsColorChanged,
settingsActiveColorToggled,
settingsColor1Changed,
settingsColor2Changed,
settingsInvertScrollForToolWidthChanged,
settingsOutputOnlyMaskedRegionsToggled,
settingsAutoProcessToggled,

View File

@@ -118,7 +118,7 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('canvas', 'selectRectTool', ['u']);
addHotkey('canvas', 'selectViewTool', ['h']);
addHotkey('canvas', 'selectColorPickerTool', ['i']);
addHotkey('canvas', 'setFillToWhite', ['d']);
addHotkey('canvas', 'toggleFillColor', ['x']);
addHotkey('canvas', 'fitLayersToCanvas', ['mod+0']);
addHotkey('canvas', 'fitBboxToCanvas', ['mod+shift+0']);
addHotkey('canvas', 'fitBboxToLayers', ['shift+n']);