From d9dea78c21a672665d9d75f7c505942a38a1ab91 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Wed, 29 Oct 2025 00:01:52 -0400 Subject: [PATCH 01/15] feat(canvas): add raster layer blend modes and boolean operations submenu; support per-layer globalCompositeOperation in compositor; UI to toggle and select color blend modes (multiply, screen, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, hue, saturation, color, luminosity). --- invokeai/frontend/web/public/locales/en.json | 12 +++ .../components/RasterLayer/RasterLayer.tsx | 2 + .../RasterLayerCompositeOperationSettings.tsx | 73 ++++++++++++++++++ .../RasterLayer/RasterLayerMenuItems.tsx | 4 + .../RasterLayerMenuItemsBooleanSubMenu.tsx | 74 +++++++++++++++++++ ...RasterLayerMenuItemsCompositeOperation.tsx | 37 ++++++++++ .../konva/CanvasBackgroundModule.ts | 7 ++ .../konva/CanvasCompositorModule.ts | 12 ++- .../CanvasEntityAdapterRasterLayer.ts | 35 +++++++++ .../controlLayers/store/canvasSlice.ts | 18 +++++ .../store/compositeOperations.ts | 35 +++++++++ .../src/features/controlLayers/store/types.ts | 3 + 12 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 8a6bd7b337e..7722752e574 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2077,6 +2077,18 @@ "pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage", "addAdjustments": "Add Adjustments", "removeAdjustments": "Remove Adjustments", + "compositeOperation": { + "label": "Blend Mode", + "add": "Add Blend Mode", + "remove": "Remove Blend Mode" + }, + "booleanOps": { + "label": "Boolean Operations", + "intersection": "Intersection", + "cutout": "Cutout", + "cutAway": "Cut Away", + "exclude": "Exclude" + }, "adjustments": { "simple": "Simple", "curves": "Curves", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 13dc30dea20..222397cd602 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -5,6 +5,7 @@ import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/componen import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { RasterLayerAdjustmentsPanel } from 'features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel'; +import { RasterLayerCompositeOperationSettings } from 'features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings'; import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate'; import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -41,6 +42,7 @@ export const RasterLayer = memo(({ id }: Props) => { + { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); + + const layer = useAppSelector((s) => + s.canvas.present.rasterLayers.entities.find((e: CanvasRasterLayerState) => e.id === entityIdentifier.id) + ); + + const showSettings = useMemo(() => { + return layer?.globalCompositeOperation !== undefined; + }, [layer]); + + const currentOperation = useMemo(() => { + return layer?.globalCompositeOperation ?? 'source-over'; + }, [layer]); + + const onChange = useCallback( + (e: ChangeEvent) => { + const value = e.target.value as CompositeOperation; + dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: value })); + }, + [dispatch, entityIdentifier] + ); + + if (!showSettings) { + return null; + } + + // Only expose the requested color blend modes in the UI + const COLOR_BLEND_MODES: CompositeOperation[] = [ + 'multiply', + 'screen', + 'darken', + 'lighten', + 'color-dodge', + 'color-burn', + 'hard-light', + 'soft-light', + 'difference', + 'hue', + 'saturation', + 'color', + 'luminosity', + ]; + + return ( + + + {t('controlLayers.compositeOperation.label')} + + + + ); +}); + +RasterLayerCompositeOperationSettings.displayName = 'RasterLayerCompositeOperationSettings'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx index 708f7f29cd6..72ed3f65aa9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx @@ -10,8 +10,10 @@ import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/com import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { RasterLayerMenuItemsAdjustments } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments'; +import { RasterLayerMenuItemsCompositeOperation } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation'; import { RasterLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertToSubMenu'; import { RasterLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCopyToSubMenu'; +import { RasterLayerMenuItemsBooleanSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu'; import { memo } from 'react'; export const RasterLayerMenuItems = memo(() => { @@ -26,6 +28,8 @@ export const RasterLayerMenuItems = memo(() => { + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx new file mode 100644 index 00000000000..df4ff921e89 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -0,0 +1,74 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier'; +import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useTranslation } from 'react-i18next'; + +export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { + const subMenu = useSubMenu(); + const canvasManager = useCanvasManager(); + const isBusy = useCanvasIsBusy(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); + const entityIdentifierBelowThisOne = useEntityIdentifierBelowThisOne(entityIdentifier as CanvasEntityIdentifier); + const { t } = useTranslation(); + + const perform = useCallback( + async (op: GlobalCompositeOperation) => { + if (!entityIdentifierBelowThisOne) return; + // Temporarily set composite op on the selected layer to drive the merge algorithm + dispatch( + rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: op }) + ); + try { + await canvasManager.compositor.mergeByEntityIdentifiers( + [entityIdentifierBelowThisOne, entityIdentifier], + true + ); + } finally { + // No need to reset - layers are deleted on success; but in case of failure, clear the op + dispatch( + rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined }) + ); + } + }, + [canvasManager.compositor, dispatch, entityIdentifier, entityIdentifierBelowThisOne] + ); + + const onIntersection = useCallback(() => perform('source-in'), [perform]); + const onCutout = useCallback(() => perform('destination-in'), [perform]); + const onCutAway = useCallback(() => perform('source-out'), [perform]); + const onExclude = useCallback(() => perform('xor'), [perform]); + + return ( + <> + + + + {subMenu.isOpen && ( + <> + + {t('controlLayers.booleanOps.intersection')} + + + {t('controlLayers.booleanOps.cutout')} + + + {t('controlLayers.booleanOps.cutAway')} + + + {t('controlLayers.booleanOps.exclude')} + + + )} + + ); +}); + +RasterLayerMenuItemsBooleanSubMenu.displayName = 'RasterLayerMenuItemsBooleanSubMenu'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx new file mode 100644 index 00000000000..23a626aaa46 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx @@ -0,0 +1,37 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiDropHalfBold } from 'react-icons/pi'; + +export const RasterLayerMenuItemsCompositeOperation = memo(() => { + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); + const { t } = useTranslation(); + const layer = useAppSelector((s) => + s.canvas.present.rasterLayers.entities.find((e: CanvasRasterLayerState) => e.id === entityIdentifier.id) + ); + const hasCompositeOperation = layer?.globalCompositeOperation !== undefined; + + const onToggleCompositeOperationPresence = useCallback(() => { + if (hasCompositeOperation) { + dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined })); + } else { + // default to multiply when enabling blend modes + dispatch( + rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: 'multiply' }) + ); + } + }, [dispatch, entityIdentifier, hasCompositeOperation]); + + return ( + }> + {hasCompositeOperation ? t('controlLayers.compositeOperation.remove') : t('controlLayers.compositeOperation.add')} + + ); +}); + +RasterLayerMenuItemsCompositeOperation.displayName = 'RasterLayerMenuItemsCompositeOperation'; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index b700392c05f..3130e477043 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -96,6 +96,13 @@ export class CanvasBackgroundModule extends CanvasModuleBase { }; this.checkboardPattern.src = 'anonymous'; this.checkboardPattern.src = this.config.CHECKERBOARD_PATTERN_DATAURL; + + // Isolate background to prevent blend modes affecting it + const backgroundCanvas = this.konva.layer.getCanvas()._canvas as HTMLCanvasElement | undefined; + if (backgroundCanvas) { + backgroundCanvas.style.isolation = 'isolate'; + } + this.render(); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index 3f419387439..44e0ad70184 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -226,12 +226,16 @@ export class CanvasCompositorModule extends CanvasModuleBase { ctx.imageSmoothingEnabled = false; - if (compositingOptions?.globalCompositeOperation) { - ctx.globalCompositeOperation = compositingOptions.globalCompositeOperation; - } - for (const adapter of adapters) { this.log.debug({ entityIdentifier: adapter.entityIdentifier }, 'Drawing entity to composite canvas'); + // Set composite operation for this specific layer + // Priority: 1) Per-layer setting, 2) Global compositing option, 3) Default 'source-over' + const layerCompositeOp = + adapter.state.type === 'raster_layer' || adapter.state.type === 'control_layer' + ? (adapter.state as any).globalCompositeOperation + : undefined; + ctx.globalCompositeOperation = layerCompositeOp || compositingOptions?.globalCompositeOperation || 'source-over'; + const adapterCanvas = adapter.getCanvas(rect); ctx.drawImage(adapterCanvas, 0, 0); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts index 8723664d258..dcc0a063e7f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts @@ -60,6 +60,9 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase< if (!prevState || this.state.opacity !== prevState.opacity) { this.syncOpacity(); } + if (!prevState || this.state.globalCompositeOperation !== prevState.globalCompositeOperation) { + this.syncGlobalCompositeOperation(); + } // Apply per-layer adjustments as a Konva filter if (!prevState || this.haveAdjustmentsChanged(prevState, this.state)) { @@ -67,6 +70,38 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase< } }; + private syncGlobalCompositeOperation = () => { + this.log.trace('Syncing globalCompositeOperation'); + const operation = this.state.globalCompositeOperation ?? 'source-over'; + + // Map globalCompositeOperation to CSS mix-blend-mode for live preview + const mixBlendModeMap: Record = { + 'source-over': 'normal', + multiply: 'multiply', + screen: 'screen', + overlay: 'overlay', + darken: 'darken', + lighten: 'lighten', + 'color-dodge': 'color-dodge', + 'color-burn': 'color-burn', + 'hard-light': 'hard-light', + 'soft-light': 'soft-light', + difference: 'difference', + exclusion: 'exclusion', + hue: 'hue', + saturation: 'saturation', + color: 'color', + luminosity: 'luminosity', + }; + + const mixBlendMode = mixBlendModeMap[operation] || 'normal'; + + const canvasElement = this.konva.layer.getCanvas()._canvas as HTMLCanvasElement | undefined; + if (canvasElement) { + canvasElement.style.mixBlendMode = mixBlendMode; + } + }; + getCanvas = (rect?: Rect): HTMLCanvasElement => { this.log.trace({ rect }, 'Getting canvas'); // The opacity may have been changed in response to user selecting a different entity category, so we must restore diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index f7eef4a6454..c39ba95b10b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -191,6 +191,23 @@ const slice = createSlice({ } layer.adjustments.collapsed = !layer.adjustments.collapsed; }, + rasterLayerGlobalCompositeOperationChanged: ( + state, + action: PayloadAction< + EntityIdentifierPayload<{ globalCompositeOperation?: GlobalCompositeOperation }, 'raster_layer'> + > + ) => { + const { entityIdentifier, globalCompositeOperation } = action.payload; + const layer = selectEntity(state, entityIdentifier); + if (!layer) { + return; + } + if (globalCompositeOperation === undefined) { + delete layer.globalCompositeOperation; + } else { + layer.globalCompositeOperation = globalCompositeOperation; + } + }, rasterLayerAdded: { reducer: ( state, @@ -1719,6 +1736,7 @@ export const { rasterLayerAdjustmentsCollapsedToggled, rasterLayerAdjustmentsSimpleUpdated, rasterLayerAdjustmentsCurvesUpdated, + rasterLayerGlobalCompositeOperationChanged, entityDeleted, entityArrangedForwardOne, entityArrangedToFront, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts new file mode 100644 index 00000000000..f9662efe3ec --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts @@ -0,0 +1,35 @@ +/** + * Available global composite operations (blend modes) for layers. + * These are the standard Canvas 2D composite operations. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation + */ +export const COMPOSITE_OPERATIONS = [ + 'source-over', + 'source-in', + 'source-out', + 'source-atop', + 'destination-over', + 'destination-in', + 'destination-out', + 'destination-atop', + 'lighter', + 'copy', + 'xor', + 'multiply', + 'screen', + 'overlay', + 'darken', + 'lighten', + 'color-dodge', + 'color-burn', + 'hard-light', + 'soft-light', + 'difference', + 'exclusion', + 'hue', + 'saturation', + 'color', + 'luminosity', +] as const; + +export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 87c173d7cca..d094be7daa7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,5 +1,6 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; +import { COMPOSITE_OPERATIONS } from 'features/controlLayers/store/compositeOperations'; import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; import { zParameterCanvasCoherenceMode, @@ -462,6 +463,8 @@ const zCanvasRasterLayerState = zCanvasEntityBase.extend({ objects: z.array(zCanvasObjectState), // Optional per-layer color adjustments (simple + curves). When undefined, no adjustments are applied. adjustments: zRasterLayerAdjustments.optional(), + // Optional per-layer composite operation. When undefined, defaults to 'source-over'. + globalCompositeOperation: z.enum(COMPOSITE_OPERATIONS).optional(), }); export type CanvasRasterLayerState = z.infer; From 7a4c87a5da15ca355dfe41e23e0a17a0eb830c0c Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Wed, 29 Oct 2025 00:08:23 -0400 Subject: [PATCH 02/15] feat(canvas): boolean ops submenu and UI polish --- .../RasterLayerMenuItemsBooleanSubMenu.tsx | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx index df4ff921e89..001a41d730c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -1,4 +1,4 @@ -import { MenuItem } from '@invoke-ai/ui-library'; +import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -11,31 +11,25 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { useTranslation } from 'react-i18next'; export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { + const { t } = useTranslation(); const subMenu = useSubMenu(); const canvasManager = useCanvasManager(); const isBusy = useCanvasIsBusy(); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); const entityIdentifierBelowThisOne = useEntityIdentifierBelowThisOne(entityIdentifier as CanvasEntityIdentifier); - const { t } = useTranslation(); const perform = useCallback( async (op: GlobalCompositeOperation) => { if (!entityIdentifierBelowThisOne) return; - // Temporarily set composite op on the selected layer to drive the merge algorithm - dispatch( - rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: op }) - ); + dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: op })); try { await canvasManager.compositor.mergeByEntityIdentifiers( [entityIdentifierBelowThisOne, entityIdentifier], true ); } finally { - // No need to reset - layers are deleted on success; but in case of failure, clear the op - dispatch( - rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined }) - ); + dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined })); } }, [canvasManager.compositor, dispatch, entityIdentifier, entityIdentifierBelowThisOne] @@ -46,28 +40,30 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { const onCutAway = useCallback(() => perform('source-out'), [perform]); const onExclude = useCallback(() => perform('xor'), [perform]); + const disabled = isBusy || !entityIdentifierBelowThisOne; + return ( - <> - - - - {subMenu.isOpen && ( - <> - + + + + + + + {t('controlLayers.booleanOps.intersection')} - + {t('controlLayers.booleanOps.cutout')} - + {t('controlLayers.booleanOps.cutAway')} - + {t('controlLayers.booleanOps.exclude')} - - )} - + + + ); }); From de37f87acb8abd554a8e6fbab9d698b5ab2f5258 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Wed, 29 Oct 2025 01:00:36 -0400 Subject: [PATCH 03/15] (chore): prettier lint --- invokeai/frontend/web/public/locales/en.json | 16 ++++++++-------- .../RasterLayerCompositeOperationSettings.tsx | 4 ++-- .../RasterLayer/RasterLayerMenuItems.tsx | 2 +- .../RasterLayerMenuItemsBooleanSubMenu.tsx | 13 ++++++------- .../RasterLayerMenuItemsCompositeOperation.tsx | 6 ++---- .../konva/CanvasCompositorModule.ts | 2 +- .../controlLayers/store/compositeOperations.ts | 2 +- 7 files changed, 21 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7722752e574..ec8b023964b 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2078,16 +2078,16 @@ "addAdjustments": "Add Adjustments", "removeAdjustments": "Remove Adjustments", "compositeOperation": { - "label": "Blend Mode", - "add": "Add Blend Mode", - "remove": "Remove Blend Mode" + "label": "Blend Mode", + "add": "Add Blend Mode", + "remove": "Remove Blend Mode" }, "booleanOps": { - "label": "Boolean Operations", - "intersection": "Intersection", - "cutout": "Cutout", - "cutAway": "Cut Away", - "exclude": "Exclude" + "label": "Boolean Operations", + "intersection": "Intersection", + "cutout": "Cutout", + "cutAway": "Cut Away", + "exclude": "Exclude" }, "adjustments": { "simple": "Simple", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx index 3b87c467fad..0661f8ca270 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx @@ -2,7 +2,7 @@ import { Flex, FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; -import { COMPOSITE_OPERATIONS, type CompositeOperation } from 'features/controlLayers/store/compositeOperations'; +import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; @@ -70,4 +70,4 @@ export const RasterLayerCompositeOperationSettings = memo(() => { ); }); -RasterLayerCompositeOperationSettings.displayName = 'RasterLayerCompositeOperationSettings'; \ No newline at end of file +RasterLayerCompositeOperationSettings.displayName = 'RasterLayerCompositeOperationSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx index 72ed3f65aa9..eefb5f9e083 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx @@ -10,10 +10,10 @@ import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/com import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { RasterLayerMenuItemsAdjustments } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments'; +import { RasterLayerMenuItemsBooleanSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu'; import { RasterLayerMenuItemsCompositeOperation } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation'; import { RasterLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertToSubMenu'; import { RasterLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCopyToSubMenu'; -import { RasterLayerMenuItemsBooleanSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu'; import { memo } from 'react'; export const RasterLayerMenuItems = memo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx index 001a41d730c..259466efb51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -1,4 +1,5 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -7,7 +8,6 @@ import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/us import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; -import { useAppDispatch } from 'app/store/storeHooks'; import { useTranslation } from 'react-i18next'; export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { @@ -21,13 +21,12 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { const perform = useCallback( async (op: GlobalCompositeOperation) => { - if (!entityIdentifierBelowThisOne) return; + if (!entityIdentifierBelowThisOne) { + return; + } dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: op })); try { - await canvasManager.compositor.mergeByEntityIdentifiers( - [entityIdentifierBelowThisOne, entityIdentifier], - true - ); + await canvasManager.compositor.mergeByEntityIdentifiers([entityIdentifierBelowThisOne, entityIdentifier], true); } finally { dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined })); } @@ -67,4 +66,4 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { ); }); -RasterLayerMenuItemsBooleanSubMenu.displayName = 'RasterLayerMenuItemsBooleanSubMenu'; \ No newline at end of file +RasterLayerMenuItemsBooleanSubMenu.displayName = 'RasterLayerMenuItemsBooleanSubMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx index 23a626aaa46..61bb84361fb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx @@ -21,9 +21,7 @@ export const RasterLayerMenuItemsCompositeOperation = memo(() => { dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined })); } else { // default to multiply when enabling blend modes - dispatch( - rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: 'multiply' }) - ); + dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: 'multiply' })); } }, [dispatch, entityIdentifier, hasCompositeOperation]); @@ -34,4 +32,4 @@ export const RasterLayerMenuItemsCompositeOperation = memo(() => { ); }); -RasterLayerMenuItemsCompositeOperation.displayName = 'RasterLayerMenuItemsCompositeOperation'; \ No newline at end of file +RasterLayerMenuItemsCompositeOperation.displayName = 'RasterLayerMenuItemsCompositeOperation'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index 44e0ad70184..6ff426db71b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -232,7 +232,7 @@ export class CanvasCompositorModule extends CanvasModuleBase { // Priority: 1) Per-layer setting, 2) Global compositing option, 3) Default 'source-over' const layerCompositeOp = adapter.state.type === 'raster_layer' || adapter.state.type === 'control_layer' - ? (adapter.state as any).globalCompositeOperation + ? adapter.state.globalCompositeOperation : undefined; ctx.globalCompositeOperation = layerCompositeOp || compositingOptions?.globalCompositeOperation || 'source-over'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts index f9662efe3ec..f94dec218d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts @@ -32,4 +32,4 @@ export const COMPOSITE_OPERATIONS = [ 'luminosity', ] as const; -export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; \ No newline at end of file +export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; From f3d93230892e962ec9598c52f34718342e9da071 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Wed, 29 Oct 2025 20:29:07 -0400 Subject: [PATCH 04/15] add icons to boolean submenu items --- invokeai/frontend/web/public/locales/en.json | 2 +- .../components/RasterLayer/RasterLayerMenuItems.tsx | 2 +- .../RasterLayerMenuItemsBooleanSubMenu.tsx | 11 ++++++----- .../controlLayers/konva/CanvasBackgroundModule.ts | 7 ------- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ec8b023964b..e00bf1ea4d1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2085,7 +2085,7 @@ "booleanOps": { "label": "Boolean Operations", "intersection": "Intersection", - "cutout": "Cutout", + "cutout": "Cut Out", "cutAway": "Cut Away", "exclude": "Exclude" }, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx index eefb5f9e083..cac242a84a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx @@ -29,9 +29,9 @@ export const RasterLayerMenuItems = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx index 259466efb51..d7ee42136f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -9,6 +9,7 @@ import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLaye import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { CgPathBack, CgPathCrop, CgPathExclude, CgPathFront, CgPathIntersect } from 'react-icons/cg'; export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { const { t } = useTranslation(); @@ -42,22 +43,22 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { const disabled = isBusy || !entityIdentifierBelowThisOne; return ( - + }> - + }> {t('controlLayers.booleanOps.intersection')} - + }> {t('controlLayers.booleanOps.cutout')} - + }> {t('controlLayers.booleanOps.cutAway')} - + }> {t('controlLayers.booleanOps.exclude')} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index 3130e477043..b700392c05f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -96,13 +96,6 @@ export class CanvasBackgroundModule extends CanvasModuleBase { }; this.checkboardPattern.src = 'anonymous'; this.checkboardPattern.src = this.config.CHECKERBOARD_PATTERN_DATAURL; - - // Isolate background to prevent blend modes affecting it - const backgroundCanvas = this.konva.layer.getCanvas()._canvas as HTMLCanvasElement | undefined; - if (backgroundCanvas) { - backgroundCanvas.style.isolation = 'isolate'; - } - this.render(); }; From 4adaa7039db1e4991cde78eb77cce629772af3a3 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Thu, 30 Oct 2025 18:40:55 -0400 Subject: [PATCH 05/15] add delete button for color blend operations --- .../RasterLayerCompositeOperationSettings.tsx | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx index 0661f8ca270..36e1e3505d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx @@ -1,5 +1,6 @@ import { Flex, FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InpaintMaskDeleteModifierButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskDeleteModifierButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; @@ -33,11 +34,19 @@ export const RasterLayerCompositeOperationSettings = memo(() => { [dispatch, entityIdentifier] ); + const onDelete = useCallback(() => { + dispatch( + rasterLayerGlobalCompositeOperationChanged({ + entityIdentifier, + globalCompositeOperation: undefined, + }) + ); + }, [dispatch, entityIdentifier]); + if (!showSettings) { return null; } - // Only expose the requested color blend modes in the UI const COLOR_BLEND_MODES: CompositeOperation[] = [ 'multiply', 'screen', @@ -57,14 +66,17 @@ export const RasterLayerCompositeOperationSettings = memo(() => { return ( - {t('controlLayers.compositeOperation.label')} - + {t('controlLayers.compositeOperation.label')} + + + + ); From 0e535ffe7fa8a76f8b487ffa5f7a1b62d88b3249 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sat, 1 Nov 2025 19:57:51 -0400 Subject: [PATCH 06/15] move composite operation type and imports --- .../RasterLayerCompositeOperationSettings.tsx | 18 +----------------- .../RasterLayerMenuItemsBooleanSubMenu.tsx | 3 ++- .../konva/CanvasCompositorModule.ts | 3 ++- .../controlLayers/store/canvasSlice.ts | 3 ++- .../controlLayers/store/compositeOperations.ts | 18 ++++++++++++++++++ .../src/features/controlLayers/store/types.ts | 1 + 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx index 36e1e3505d8..4a98282c120 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InpaintMaskDeleteModifierButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskDeleteModifierButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; -import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; +import { COLOR_BLEND_MODES, type CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; @@ -47,22 +47,6 @@ export const RasterLayerCompositeOperationSettings = memo(() => { return null; } - const COLOR_BLEND_MODES: CompositeOperation[] = [ - 'multiply', - 'screen', - 'darken', - 'lighten', - 'color-dodge', - 'color-burn', - 'hard-light', - 'soft-light', - 'difference', - 'hue', - 'saturation', - 'color', - 'luminosity', - ]; - return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx index d7ee42136f4..c22c682e72c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -6,6 +6,7 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier'; import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; +import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,7 +22,7 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { const entityIdentifierBelowThisOne = useEntityIdentifierBelowThisOne(entityIdentifier as CanvasEntityIdentifier); const perform = useCallback( - async (op: GlobalCompositeOperation) => { + async (op: CompositeOperation) => { if (!entityIdentifierBelowThisOne) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index 6ff426db71b..aef59c64d7f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -29,6 +29,7 @@ import type { } from 'features/controlLayers/store/types'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/util'; +import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { atom, computed } from 'nanostores'; @@ -46,7 +47,7 @@ type CompositingOptions = { * The global composite operation to use when compositing each entity. * See: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation */ - globalCompositeOperation?: GlobalCompositeOperation; + globalCompositeOperation?: CompositeOperation; }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index c39ba95b10b..b837e565263 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -70,6 +70,7 @@ import type { IPMethodV2, T2IAdapterConfig, } from './types'; +import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { ASPECT_RATIO_MAP, DEFAULT_ASPECT_RATIO_CONFIG, @@ -194,7 +195,7 @@ const slice = createSlice({ rasterLayerGlobalCompositeOperationChanged: ( state, action: PayloadAction< - EntityIdentifierPayload<{ globalCompositeOperation?: GlobalCompositeOperation }, 'raster_layer'> + EntityIdentifierPayload<{ globalCompositeOperation?: CompositeOperation }, 'raster_layer'> > ) => { const { entityIdentifier, globalCompositeOperation } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts index f94dec218d6..c75fe2af413 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts @@ -33,3 +33,21 @@ export const COMPOSITE_OPERATIONS = [ ] as const; export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; + +// Subset of color blend modes for UI selection +export const COLOR_BLEND_MODES: CompositeOperation[] = [ + 'multiply', + 'screen', + 'overlay', + 'darken', + 'lighten', + 'color-dodge', + 'color-burn', + 'hard-light', + 'soft-light', + 'difference', + 'hue', + 'saturation', + 'color', + 'luminosity', +]; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index d094be7daa7..2c9265258d5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,6 +1,7 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import { COMPOSITE_OPERATIONS } from 'features/controlLayers/store/compositeOperations'; +export type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; import { zParameterCanvasCoherenceMode, From 47a609294dd520b773b7e93305ca6abb83f8a070 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sat, 1 Nov 2025 20:07:31 -0400 Subject: [PATCH 07/15] chore: pnpm eslint --- .../features/controlLayers/konva/CanvasCompositorModule.ts | 2 +- .../web/src/features/controlLayers/store/canvasSlice.ts | 6 ++---- .../frontend/web/src/features/controlLayers/store/types.ts | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index aef59c64d7f..e1c15f6d525 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -14,6 +14,7 @@ import { mapId, previewBlob, } from 'features/controlLayers/konva/util'; +import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { selectActiveControlLayerEntities, selectActiveInpaintMaskEntities, @@ -29,7 +30,6 @@ import type { } from 'features/controlLayers/store/types'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/util'; -import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { atom, computed } from 'nanostores'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index b837e565263..957a6070296 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -7,6 +7,7 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul import { merge } from 'es-toolkit/compat'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; +import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { modelChanged } from 'features/controlLayers/store/paramsSlice'; import { selectAllEntities, @@ -70,7 +71,6 @@ import type { IPMethodV2, T2IAdapterConfig, } from './types'; -import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { ASPECT_RATIO_MAP, DEFAULT_ASPECT_RATIO_CONFIG, @@ -194,9 +194,7 @@ const slice = createSlice({ }, rasterLayerGlobalCompositeOperationChanged: ( state, - action: PayloadAction< - EntityIdentifierPayload<{ globalCompositeOperation?: CompositeOperation }, 'raster_layer'> - > + action: PayloadAction> ) => { const { entityIdentifier, globalCompositeOperation } = action.payload; const layer = selectEntity(state, entityIdentifier); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 2c9265258d5..d094be7daa7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,7 +1,6 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import { COMPOSITE_OPERATIONS } from 'features/controlLayers/store/compositeOperations'; -export type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; import { zParameterCanvasCoherenceMode, From 448434630ec375f235984f3b48ae8296a180db47 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sat, 1 Nov 2025 21:57:46 -0400 Subject: [PATCH 08/15] update blend modes order --- .../controlLayers/store/compositeOperations.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts index c75fe2af413..6460950b8a2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts @@ -36,18 +36,18 @@ export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; // Subset of color blend modes for UI selection export const COLOR_BLEND_MODES: CompositeOperation[] = [ - 'multiply', - 'screen', + 'color', + 'hue', 'overlay', + 'soft-light', + 'hard-light', + 'screen', + 'color-burn', + 'color-dodge', + 'multiply', 'darken', 'lighten', - 'color-dodge', - 'color-burn', - 'hard-light', - 'soft-light', 'difference', - 'hue', - 'saturation', - 'color', 'luminosity', + 'saturation', ]; From a00661940ef409711a090cbadb3acb1f98ff11d0 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 2 Nov 2025 01:32:56 -0500 Subject: [PATCH 09/15] update default blend mode to 'color' --- .../RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx index 61bb84361fb..65381c9f325 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCompositeOperation.tsx @@ -20,8 +20,8 @@ export const RasterLayerMenuItemsCompositeOperation = memo(() => { if (hasCompositeOperation) { dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined })); } else { - // default to multiply when enabling blend modes - dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: 'multiply' })); + // default to color when enabling blend modes + dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: 'color' })); } }, [dispatch, entityIdentifier, hasCompositeOperation]); From b6a89bd53649221eb46469334c984f260d989130 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Mon, 3 Nov 2025 12:40:18 -0500 Subject: [PATCH 10/15] add i18n for blend modes --- invokeai/frontend/web/public/locales/en.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e00bf1ea4d1..ef245fa3209 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2082,6 +2082,24 @@ "add": "Add Blend Mode", "remove": "Remove Blend Mode" }, + "compositeOperations": { + "blendModes": { + "color": "Color", + "hue": "Hue", + "overlay": "Overlay", + "soft-light": "Soft Light", + "hard-light": "Hard Light", + "screen": "Screen", + "color-burn": "Color Burn", + "color-dodge": "Color Dodge", + "multiply": "Multiply", + "darken": "Darken", + "lighten": "Lighten", + "difference": "Difference", + "luminosity": "Luminosity", + "saturation": "Saturation" + } + }, "booleanOps": { "label": "Boolean Operations", "intersection": "Intersection", From 5cd618328bb9cbf9a430ddcdd57446685dd5d291 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Mon, 3 Nov 2025 14:57:51 -0500 Subject: [PATCH 11/15] actually use translations for blend modes now --- invokeai/frontend/web/public/locales/en.json | 4 +--- .../RasterLayer/RasterLayerCompositeOperationSettings.tsx | 2 +- .../src/features/controlLayers/store/compositeOperations.ts | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ef245fa3209..0d82fe411b3 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2080,9 +2080,7 @@ "compositeOperation": { "label": "Blend Mode", "add": "Add Blend Mode", - "remove": "Remove Blend Mode" - }, - "compositeOperations": { + "remove": "Remove Blend Mode", "blendModes": { "color": "Color", "hue": "Hue", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx index 4a98282c120..628187c2443 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx @@ -55,7 +55,7 @@ export const RasterLayerCompositeOperationSettings = memo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts index 6460950b8a2..b9e23ef0307 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts @@ -2,6 +2,7 @@ * Available global composite operations (blend modes) for layers. * These are the standard Canvas 2D composite operations. * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation + * NOTE: All of these are supported by canvas layers, but not all are supported by CSS blend modes (live rendering). */ export const COMPOSITE_OPERATIONS = [ 'source-over', @@ -34,7 +35,7 @@ export const COMPOSITE_OPERATIONS = [ export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; -// Subset of color blend modes for UI selection +// Subset of color blend modes for UI selection. All are supported by both Konva and CSS. export const COLOR_BLEND_MODES: CompositeOperation[] = [ 'color', 'hue', From edcf922692d3da2e861eeb366420924a0d44755f Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Thu, 13 Nov 2025 13:56:48 -0500 Subject: [PATCH 12/15] move composite options into types.ts --- .../RasterLayerCompositeOperationSettings.tsx | 4 +- .../RasterLayerMenuItemsBooleanSubMenu.tsx | 3 +- .../konva/CanvasCompositorModule.ts | 2 +- .../controlLayers/store/canvasSlice.ts | 2 +- .../store/compositeOperations.ts | 54 ------------------ .../src/features/controlLayers/store/types.ts | 56 ++++++++++++++++++- 6 files changed, 60 insertions(+), 61 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx index 628187c2443..6cdd3e0b6ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCompositeOperationSettings.tsx @@ -3,8 +3,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InpaintMaskDeleteModifierButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskDeleteModifierButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; -import { COLOR_BLEND_MODES, type CompositeOperation } from 'features/controlLayers/store/compositeOperations'; -import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState, CompositeOperation } from 'features/controlLayers/store/types'; +import { COLOR_BLEND_MODES } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx index c22c682e72c..a09fe64ccae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -6,8 +6,7 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier'; import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; -import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import type { CanvasEntityIdentifier, CompositeOperation } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { CgPathBack, CgPathCrop, CgPathExclude, CgPathFront, CgPathIntersect } from 'react-icons/cg'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index e1c15f6d525..a6e5e7fa240 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -14,7 +14,6 @@ import { mapId, previewBlob, } from 'features/controlLayers/konva/util'; -import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { selectActiveControlLayerEntities, selectActiveInpaintMaskEntities, @@ -25,6 +24,7 @@ import type { CanvasEntityIdentifier, CanvasEntityState, CanvasEntityType, + CompositeOperation, GenerationMode, Rect, } from 'features/controlLayers/store/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 957a6070296..bad8ae8098b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -7,7 +7,6 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul import { merge } from 'es-toolkit/compat'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; -import type { CompositeOperation } from 'features/controlLayers/store/compositeOperations'; import { modelChanged } from 'features/controlLayers/store/paramsSlice'; import { selectAllEntities, @@ -22,6 +21,7 @@ import type { CanvasMetadata, ChannelName, ChannelPoints, + CompositeOperation, ControlLoRAConfig, EntityMovedByPayload, FillStyle, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts deleted file mode 100644 index b9e23ef0307..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/compositeOperations.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Available global composite operations (blend modes) for layers. - * These are the standard Canvas 2D composite operations. - * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation - * NOTE: All of these are supported by canvas layers, but not all are supported by CSS blend modes (live rendering). - */ -export const COMPOSITE_OPERATIONS = [ - 'source-over', - 'source-in', - 'source-out', - 'source-atop', - 'destination-over', - 'destination-in', - 'destination-out', - 'destination-atop', - 'lighter', - 'copy', - 'xor', - 'multiply', - 'screen', - 'overlay', - 'darken', - 'lighten', - 'color-dodge', - 'color-burn', - 'hard-light', - 'soft-light', - 'difference', - 'exclusion', - 'hue', - 'saturation', - 'color', - 'luminosity', -] as const; - -export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; - -// Subset of color blend modes for UI selection. All are supported by both Konva and CSS. -export const COLOR_BLEND_MODES: CompositeOperation[] = [ - 'color', - 'hue', - 'overlay', - 'soft-light', - 'hard-light', - 'screen', - 'color-burn', - 'color-dodge', - 'multiply', - 'darken', - 'lighten', - 'difference', - 'luminosity', - 'saturation', -]; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index d094be7daa7..ea7730424e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,6 +1,5 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; -import { COMPOSITE_OPERATIONS } from 'features/controlLayers/store/compositeOperations'; import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; import { zParameterCanvasCoherenceMode, @@ -456,6 +455,61 @@ const zRasterLayerAdjustments = z.object({ }); export type RasterLayerAdjustments = z.infer; +/** + * Available global composite operations (blend modes) for layers. + * These are the standard Canvas 2D composite operations. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation + * NOTE: All of these are supported by canvas layers, but not all are supported by CSS blend modes (live rendering). + */ +const COMPOSITE_OPERATIONS = [ + 'source-over', + 'source-in', + 'source-out', + 'source-atop', + 'destination-over', + 'destination-in', + 'destination-out', + 'destination-atop', + 'lighter', + 'copy', + 'xor', + 'multiply', + 'screen', + 'overlay', + 'darken', + 'lighten', + 'color-dodge', + 'color-burn', + 'hard-light', + 'soft-light', + 'difference', + 'exclusion', + 'hue', + 'saturation', + 'color', + 'luminosity', +] as const; + +export type CompositeOperation = (typeof COMPOSITE_OPERATIONS)[number]; + +// Subset of color blend modes for UI selection. All are supported by both Konva and CSS. +export const COLOR_BLEND_MODES: CompositeOperation[] = [ + 'color', + 'hue', + 'overlay', + 'soft-light', + 'hard-light', + 'screen', + 'color-burn', + 'color-dodge', + 'multiply', + 'darken', + 'lighten', + 'difference', + 'luminosity', + 'saturation', +]; + const zCanvasRasterLayerState = zCanvasEntityBase.extend({ type: z.literal('raster_layer'), position: zCoordinate, From 8a0069015a1513ee7da85005066626c99ff501d3 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Fri, 14 Nov 2025 18:02:57 -0500 Subject: [PATCH 13/15] cleanup and comments --- .../konva/CanvasCompositorModule.ts | 1 + .../CanvasEntityAdapterRasterLayer.ts | 66 +++++++++---------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index a6e5e7fa240..337151581c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -46,6 +46,7 @@ type CompositingOptions = { /** * The global composite operation to use when compositing each entity. * See: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation + * Invoke supports a subset of these modes for raster and control layer combinations. */ globalCompositeOperation?: CompositeOperation; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts index dcc0a063e7f..a900f71e27a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts @@ -70,38 +70,6 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase< } }; - private syncGlobalCompositeOperation = () => { - this.log.trace('Syncing globalCompositeOperation'); - const operation = this.state.globalCompositeOperation ?? 'source-over'; - - // Map globalCompositeOperation to CSS mix-blend-mode for live preview - const mixBlendModeMap: Record = { - 'source-over': 'normal', - multiply: 'multiply', - screen: 'screen', - overlay: 'overlay', - darken: 'darken', - lighten: 'lighten', - 'color-dodge': 'color-dodge', - 'color-burn': 'color-burn', - 'hard-light': 'hard-light', - 'soft-light': 'soft-light', - difference: 'difference', - exclusion: 'exclusion', - hue: 'hue', - saturation: 'saturation', - color: 'color', - luminosity: 'luminosity', - }; - - const mixBlendMode = mixBlendModeMap[operation] || 'normal'; - - const canvasElement = this.konva.layer.getCanvas()._canvas as HTMLCanvasElement | undefined; - if (canvasElement) { - canvasElement.style.mixBlendMode = mixBlendMode; - } - }; - getCanvas = (rect?: Rect): HTMLCanvasElement => { this.log.trace({ rect }, 'Getting canvas'); // The opacity may have been changed in response to user selecting a different entity category, so we must restore @@ -182,7 +150,7 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase< ) { return true; } - // curves reference (UI not implemented yet) - if arrays differ by ref, consider changed + // curves params const pc = pa.curves; const cc = ca.curves; if (pc !== cc) { @@ -190,4 +158,36 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase< } return false; }; + + private syncGlobalCompositeOperation = () => { + this.log.trace('Syncing globalCompositeOperation'); + const operation = this.state.globalCompositeOperation ?? 'source-over'; + + // Map globalCompositeOperation to CSS mix-blend-mode for live preview + const mixBlendModeMap: Record = { + 'source-over': 'normal', // this one is why we need the map + multiply: 'multiply', + screen: 'screen', + overlay: 'overlay', + darken: 'darken', + lighten: 'lighten', + 'color-dodge': 'color-dodge', + 'color-burn': 'color-burn', + 'hard-light': 'hard-light', + 'soft-light': 'soft-light', + difference: 'difference', + exclusion: 'exclusion', + hue: 'hue', + saturation: 'saturation', + color: 'color', + luminosity: 'luminosity', + }; + + const mixBlendMode = mixBlendModeMap[operation] || 'normal'; + + const canvasElement = this.konva.layer.getCanvas()._canvas as HTMLCanvasElement | undefined; + if (canvasElement) { + canvasElement.style.mixBlendMode = mixBlendMode; + } + }; } From 03f762761e7ef081c183a7b089f8150dbbcdf990 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Fri, 14 Nov 2025 18:36:04 -0500 Subject: [PATCH 14/15] update names --- invokeai/frontend/web/public/locales/en.json | 4 ++-- .../RasterLayerMenuItemsBooleanSubMenu.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0d82fe411b3..cbdd15f989a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2100,9 +2100,9 @@ }, "booleanOps": { "label": "Boolean Operations", - "intersection": "Intersection", + "intersect": "Intersect", "cutout": "Cut Out", - "cutAway": "Cut Away", + "cutaway": "Cut Away", "exclude": "Exclude" }, "adjustments": { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx index a09fe64ccae..8c824a1e302 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsBooleanSubMenu.tsx @@ -35,8 +35,8 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { [canvasManager.compositor, dispatch, entityIdentifier, entityIdentifierBelowThisOne] ); - const onIntersection = useCallback(() => perform('source-in'), [perform]); - const onCutout = useCallback(() => perform('destination-in'), [perform]); + const onIntersect = useCallback(() => perform('source-in'), [perform]); + const onCutOut = useCallback(() => perform('destination-in'), [perform]); const onCutAway = useCallback(() => perform('source-out'), [perform]); const onExclude = useCallback(() => perform('xor'), [perform]); @@ -49,14 +49,14 @@ export const RasterLayerMenuItemsBooleanSubMenu = memo(() => { - }> - {t('controlLayers.booleanOps.intersection')} + }> + {t('controlLayers.booleanOps.intersect')} - }> + }> {t('controlLayers.booleanOps.cutout')} }> - {t('controlLayers.booleanOps.cutAway')} + {t('controlLayers.booleanOps.cutaway')} }> {t('controlLayers.booleanOps.exclude')} From 7c4737f0631ca7609372f6740112e6aa1ed07e19 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Fri, 14 Nov 2025 19:06:29 -0500 Subject: [PATCH 15/15] move constant mapping out of function --- .../CanvasEntityAdapterRasterLayer.ts | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts index a900f71e27a..f1b50cabca7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts @@ -11,6 +11,26 @@ import type { CanvasEntityIdentifier, CanvasRasterLayerState, Rect } from 'featu import type { GroupConfig } from 'konva/lib/Group'; import type { JsonObject } from 'type-fest'; +// Map globalCompositeOperation to CSS mix-blend-mode for live preview +const mixBlendModeMap: Record = { + 'source-over': 'normal', // this one is why we need the map + multiply: 'multiply', + screen: 'screen', + overlay: 'overlay', + darken: 'darken', + lighten: 'lighten', + 'color-dodge': 'color-dodge', + 'color-burn': 'color-burn', + 'hard-light': 'hard-light', + 'soft-light': 'soft-light', + difference: 'difference', + exclusion: 'exclusion', + hue: 'hue', + saturation: 'saturation', + color: 'color', + luminosity: 'luminosity', +}; + export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase< CanvasRasterLayerState, 'raster_layer_adapter' @@ -162,29 +182,7 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase< private syncGlobalCompositeOperation = () => { this.log.trace('Syncing globalCompositeOperation'); const operation = this.state.globalCompositeOperation ?? 'source-over'; - - // Map globalCompositeOperation to CSS mix-blend-mode for live preview - const mixBlendModeMap: Record = { - 'source-over': 'normal', // this one is why we need the map - multiply: 'multiply', - screen: 'screen', - overlay: 'overlay', - darken: 'darken', - lighten: 'lighten', - 'color-dodge': 'color-dodge', - 'color-burn': 'color-burn', - 'hard-light': 'hard-light', - 'soft-light': 'soft-light', - difference: 'difference', - exclusion: 'exclusion', - hue: 'hue', - saturation: 'saturation', - color: 'color', - luminosity: 'luminosity', - }; - const mixBlendMode = mixBlendModeMap[operation] || 'normal'; - const canvasElement = this.konva.layer.getCanvas()._canvas as HTMLCanvasElement | undefined; if (canvasElement) { canvasElement.style.mixBlendMode = mixBlendMode;