Skip to content

Commit 15b589a

Browse files
feat(ui): Refactor Blend Mode Implementation
- Blend Modes are not right click menu options anymore. Instead they rest above the layer panel as they do in other art programs readily available for each layer. - Blend Modes have been resorted to match the listings of other art programs so users can avail their muscle memory. - Blend Mode now defaults to `Normal` for each layer as it should. - The extra layer operations have now been moved down to the `Operations Bar` at the bottom of the layer stack. This is to increase familiarity again with other art programs and also to make space for us in the top action bar. - The Operations Bars operations have been resorted in order of usage that makes sense.
1 parent 5e4a418 commit 15b589a

File tree

9 files changed

+142
-88
lines changed

9 files changed

+142
-88
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2207,6 +2207,7 @@
22072207
"add": "Add Blend Mode",
22082208
"remove": "Remove Blend Mode",
22092209
"blendModes": {
2210+
"normal": "Normal",
22102211
"color": "Color",
22112212
"hue": "Hue",
22122213
"overlay": "Overlay",

invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,17 @@
1-
import { Flex, Spacer } from '@invoke-ai/ui-library';
2-
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
3-
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
1+
import { Flex } from '@invoke-ai/ui-library';
42
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
5-
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
6-
import { EntityListSelectedEntityActionBarInvertMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton';
73
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
8-
import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton';
9-
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
10-
import { EntityListNonRasterLayerToggle } from 'features/controlLayers/components/common/CanvasNonRasterLayersIsHiddenToggle';
114
import { memo } from 'react';
125

13-
import { EntityListSelectedEntityActionBarSaveToAssetsButton } from './EntityListSelectedEntityActionBarSaveToAssetsButton';
6+
import { EntityListSelectedEntityActionBarCompositeOperation } from './EntityListSelectedEntityActionBarCompositeOperation';
147

158
export const EntityListSelectedEntityActionBar = memo(() => {
169
return (
17-
<Flex w="full" gap={2} alignItems="center" ps={1}>
18-
<EntityListSelectedEntityActionBarOpacity />
19-
<Spacer />
20-
<EntityListSelectedEntityActionBarFill />
21-
<Flex h="full">
22-
<EntityListSelectedEntityActionBarSelectObjectButton />
23-
<EntityListSelectedEntityActionBarFilterButton />
24-
<EntityListSelectedEntityActionBarTransformButton />
25-
<EntityListSelectedEntityActionBarInvertMaskButton />
26-
<EntityListSelectedEntityActionBarSaveToAssetsButton />
27-
<EntityListSelectedEntityActionBarDuplicateButton />
28-
<EntityListNonRasterLayerToggle />
29-
<EntityListGlobalActionBarAddLayerMenu />
10+
<Flex flexDirection="column" gap={2}>
11+
<Flex w="full" gap={2} ps={1}>
12+
<EntityListSelectedEntityActionBarCompositeOperation />
13+
<EntityListSelectedEntityActionBarOpacity />
14+
<EntityListSelectedEntityActionBarFill />
3015
</Flex>
3116
</Flex>
3217
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
2+
import { createSelector } from '@reduxjs/toolkit';
3+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
4+
import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice';
5+
import {
6+
selectCanvasSlice,
7+
selectEntity,
8+
selectSelectedEntityIdentifier,
9+
} from 'features/controlLayers/store/selectors';
10+
import type {
11+
CanvasEntityIdentifier,
12+
CanvasRasterLayerState,
13+
CompositeOperation,
14+
} from 'features/controlLayers/store/types';
15+
import { COLOR_BLEND_MODES } from 'features/controlLayers/store/types';
16+
import type { ChangeEvent } from 'react';
17+
import { memo, useCallback } from 'react';
18+
import { useTranslation } from 'react-i18next';
19+
20+
const selectCompositeOperation = createSelector(selectCanvasSlice, (canvas) => {
21+
const { selectedEntityIdentifier } = canvas;
22+
23+
if (selectedEntityIdentifier?.type !== 'raster_layer') {
24+
return 'source-over';
25+
}
26+
27+
const entity = selectEntity(canvas, selectedEntityIdentifier);
28+
29+
return (entity as CanvasRasterLayerState)?.globalCompositeOperation ?? 'source-over';
30+
});
31+
32+
export const EntityListSelectedEntityActionBarCompositeOperation = memo(() => {
33+
const { t } = useTranslation();
34+
const dispatch = useAppDispatch();
35+
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
36+
const currentOperation = useAppSelector(selectCompositeOperation);
37+
38+
const onChange = useCallback(
39+
(e: ChangeEvent<HTMLSelectElement>) => {
40+
if (selectedEntityIdentifier?.type === 'raster_layer') {
41+
const value = e.target.value as CompositeOperation;
42+
43+
dispatch(
44+
rasterLayerGlobalCompositeOperationChanged({
45+
entityIdentifier: selectedEntityIdentifier as CanvasEntityIdentifier<'raster_layer'>,
46+
globalCompositeOperation: value,
47+
})
48+
);
49+
}
50+
},
51+
[dispatch, selectedEntityIdentifier]
52+
);
53+
54+
if (selectedEntityIdentifier?.type !== 'raster_layer') {
55+
return null;
56+
}
57+
58+
return (
59+
<FormControl w="min-content" gap={2}>
60+
<FormLabel m={0} whiteSpace="nowrap">
61+
{t('controlLayers.compositeOperation.label')}
62+
</FormLabel>
63+
<Select value={currentOperation} onChange={onChange} size="sm" variant="outline" minW="110px">
64+
{COLOR_BLEND_MODES.map((op) => (
65+
<option key={op} value={op}>
66+
{t(`controlLayers.compositeOperation.blendModes.${op}`)}
67+
</option>
68+
))}
69+
</Select>
70+
</FormControl>
71+
);
72+
});
73+
74+
EntityListSelectedEntityActionBarCompositeOperation.displayName = 'EntityListSelectedEntityActionBarCompositeOperation';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Flex } from '@invoke-ai/ui-library';
2+
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
3+
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
4+
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
5+
import { EntityListSelectedEntityActionBarInvertMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton';
6+
import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton';
7+
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
8+
import { EntityListNonRasterLayerToggle } from 'features/controlLayers/components/common/CanvasNonRasterLayersIsHiddenToggle';
9+
import { memo } from 'react';
10+
11+
import { EntityListSelectedEntityActionBarSaveToAssetsButton } from './EntityListSelectedEntityActionBarSaveToAssetsButton';
12+
13+
export const EntityListSelectedEntityOperationsBar = memo(() => {
14+
return (
15+
<Flex w="full" minH="20px" gap={1} alignItems="center" ps={2} pr={2}>
16+
<EntityListGlobalActionBarAddLayerMenu />
17+
<EntityListSelectedEntityActionBarTransformButton />
18+
<EntityListSelectedEntityActionBarDuplicateButton />
19+
<EntityListSelectedEntityActionBarSelectObjectButton />
20+
<EntityListSelectedEntityActionBarFilterButton />
21+
<EntityListSelectedEntityActionBarInvertMaskButton />
22+
<EntityListNonRasterLayerToggle />
23+
<EntityListSelectedEntityActionBarSaveToAssetsButton />
24+
</Flex>
25+
);
26+
});
27+
28+
EntityListSelectedEntityOperationsBar.displayName = 'EntityListSelectedEntityOperationsBar';

invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/Canva
77
import { selectHasEntities } from 'features/controlLayers/store/selectors';
88
import { memo } from 'react';
99

10+
import { EntityListSelectedEntityOperationsBar } from './CanvasEntityList/EntityListSelectedEntityOperationsBar';
1011
import { ParamDenoisingStrength } from './ParamDenoisingStrength';
1112

1213
export const CanvasLayersPanel = memo(() => {
@@ -15,12 +16,14 @@ export const CanvasLayersPanel = memo(() => {
1516
return (
1617
<CanvasManagerProviderGate>
1718
<Flex flexDir="column" gap={2} w="full" h="full">
18-
<EntityListSelectedEntityActionBar />
19-
<Divider py={0} />
2019
<ParamDenoisingStrength />
2120
<Divider py={0} />
21+
<EntityListSelectedEntityActionBar />
22+
<Divider py={0} />
2223
{!hasEntities && <CanvasAddEntityButtons />}
2324
{hasEntities && <CanvasEntityList />}
25+
<Divider py={0} />
26+
<EntityListSelectedEntityOperationsBar />
2427
</Flex>
2528
</CanvasManagerProviderGate>
2629
);

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/componen
55
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
66
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
77
import { RasterLayerAdjustmentsPanel } from 'features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel';
8-
import { RasterLayerCompositeOperationSettings } from 'features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings';
98
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
109
import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
1110
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
@@ -42,7 +41,6 @@ export const RasterLayer = memo(({ id }: Props) => {
4241
<CanvasEntityHeaderCommonActions />
4342
</CanvasEntityHeader>
4443
<RasterLayerAdjustmentsPanel />
45-
<RasterLayerCompositeOperationSettings />
4644
<DndDropTarget
4745
dndTarget={replaceCanvasEntityObjectsWithImageDndTarget}
4846
dndTargetData={dndTargetData}

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/compon
1111
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
1212
import { RasterLayerMenuItemsAdjustments } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments';
1313
import { RasterLayerMenuItemsBooleanSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu';
14-
import { RasterLayerMenuItemsCompositeOperation } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation';
1514
import { RasterLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertToSubMenu';
1615
import { RasterLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCopyToSubMenu';
1716
import { memo } from 'react';
@@ -28,7 +27,6 @@ export const RasterLayerMenuItems = memo(() => {
2827
<CanvasEntityMenuItemsFilter />
2928
<CanvasEntityMenuItemsSelectObject />
3029
<RasterLayerMenuItemsAdjustments />
31-
<RasterLayerMenuItemsCompositeOperation />
3230
<MenuDivider />
3331
<CanvasEntityMenuItemsMergeDown />
3432
<RasterLayerMenuItemsBooleanSubMenu />

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx

Lines changed: 0 additions & 35 deletions
This file was deleted.

invokeai/frontend/web/src/features/controlLayers/store/types.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -491,52 +491,54 @@ export type RasterLayerAdjustments = z.infer<typeof zRasterLayerAdjustments>;
491491
* NOTE: All of these are supported by canvas layers, but not all are supported by CSS blend modes (live rendering).
492492
*/
493493
const COMPOSITE_OPERATIONS = [
494-
'source-over',
495-
'source-in',
496-
'source-out',
497-
'source-atop',
498-
'destination-over',
499-
'destination-in',
500-
'destination-out',
501-
'destination-atop',
502-
'lighter',
503-
'copy',
504-
'xor',
505-
'multiply',
506-
'screen',
507-
'overlay',
494+
'normal',
508495
'darken',
496+
'multiply',
497+
'color-burn',
509498
'lighten',
499+
'screen',
510500
'color-dodge',
511-
'color-burn',
512-
'hard-light',
501+
'lighter',
502+
'overlay',
513503
'soft-light',
504+
'hard-light',
514505
'difference',
515506
'exclusion',
507+
'xor',
516508
'hue',
517509
'saturation',
518510
'color',
519511
'luminosity',
512+
'source-over',
513+
'source-in',
514+
'source-out',
515+
'source-atop',
516+
'destination-over',
517+
'destination-in',
518+
'destination-out',
519+
'destination-atop',
520+
'copy',
520521
] as const;
521522

522523
export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number];
523524

524525
// Subset of color blend modes for UI selection. All are supported by both Konva and CSS.
525526
export const COLOR_BLEND_MODES: CompositeOperation[] = [
526-
'color',
527-
'hue',
527+
'normal',
528+
'darken',
529+
'multiply',
530+
'color-burn',
531+
'lighten',
532+
'screen',
533+
'color-dodge',
528534
'overlay',
529535
'soft-light',
530536
'hard-light',
531-
'screen',
532-
'color-burn',
533-
'color-dodge',
534-
'multiply',
535-
'darken',
536-
'lighten',
537537
'difference',
538-
'luminosity',
538+
'hue',
539539
'saturation',
540+
'color',
541+
'luminosity',
540542
];
541543

542544
const zCanvasRasterLayerState = zCanvasEntityBase.extend({

0 commit comments

Comments
 (0)