Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d9dea78
feat(canvas): add raster layer blend modes and boolean operations sub…
dunkeroni Oct 29, 2025
7a4c87a
feat(canvas): boolean ops submenu and UI polish
dunkeroni Oct 29, 2025
de37f87
(chore): prettier lint
dunkeroni Oct 29, 2025
f3d9323
add icons to boolean submenu items
dunkeroni Oct 30, 2025
4adaa70
add delete button for color blend operations
dunkeroni Oct 30, 2025
0e535ff
move composite operation type and imports
dunkeroni Nov 1, 2025
47a6092
chore: pnpm eslint
dunkeroni Nov 2, 2025
4484346
update blend modes order
dunkeroni Nov 2, 2025
a006619
update default blend mode to 'color'
dunkeroni Nov 2, 2025
b6a89bd
add i18n for blend modes
dunkeroni Nov 3, 2025
5cd6183
actually use translations for blend modes now
dunkeroni Nov 3, 2025
edcf922
move composite options into types.ts
dunkeroni Nov 13, 2025
8a00690
cleanup and comments
dunkeroni Nov 14, 2025
03f7627
update names
dunkeroni Nov 14, 2025
7c4737f
move constant mapping out of function
dunkeroni Nov 15, 2025
31d7cfc
Merge branch 'main' into feature/raster-blend-boolean
dunkeroni Nov 15, 2025
14c0823
Merge branch 'main' into feature/raster-blend-boolean
dunkeroni Dec 22, 2025
63ff383
Merge branch 'main' into feature/raster-blend-boolean
lstein Dec 28, 2025
5e4a418
Merge branch 'main' into pr/8661
blessedcoolant Jan 31, 2026
15b589a
feat(ui): Refactor Blend Mode Implementation
blessedcoolant Jan 31, 2026
9b9a52d
fix: use source-over instead of normal
blessedcoolant Jan 31, 2026
0e5fe3a
fix: pixel fix for slightly offset action bar labels.
blessedcoolant Jan 31, 2026
58dc597
feat(canvas): boolean raster merge creates new layer and disables sou…
dunkeroni Feb 2, 2026
7db19b2
(fix) lint errors
dunkeroni Feb 2, 2026
5a3020c
remove extra typecast
dunkeroni Feb 2, 2026
6b8a219
Merge branch 'main' into pr/8661
blessedcoolant Feb 3, 2026
d5c59ee
ui: translations update from weblate (#8834)
weblate Feb 3, 2026
b7d7cd0
Feat(UI): Add linear and radial gradient tools to canvas (#8774)
DustyShoe Feb 3, 2026
3b9fe2a
Merge branch 'main' into pr/8661
blessedcoolant Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2205,6 +2205,35 @@
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
"addAdjustments": "Add Adjustments",
"removeAdjustments": "Remove Adjustments",
"compositeOperation": {
"label": "Blend Mode",
"add": "Add Blend Mode",
"remove": "Remove Blend Mode",
"blendModes": {
"source-over": "Normal",
"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",
"intersect": "Intersect",
"cutout": "Cut Out",
"cutaway": "Cut Away",
"exclude": "Exclude"
},
"adjustments": {
"simple": "Simple",
"curves": "Curves",
Expand Down Expand Up @@ -2433,10 +2462,16 @@
"horizontal": "Horizontal",
"diagonal": "Diagonal"
},
"gradient": {
"linear": "Linear",
"radial": "Radial",
"clip": "Clip Gradient"
},
"tool": {
"brush": "Brush",
"eraser": "Eraser",
"rectangle": "Rectangle",
"gradient": "Gradient",
"bbox": "Bbox",
"move": "Move",
"view": "View",
Expand Down
22 changes: 19 additions & 3 deletions invokeai/frontend/web/public/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@
"unableToLoad": "Impossibile caricare la Galleria",
"selectAnImageToCompare": "Seleziona un'immagine da confrontare",
"openViewer": "Apri Visualizzatore",
"closeViewer": "Chiudi Visualizzatore"
"closeViewer": "Chiudi Visualizzatore",
"usePagedGalleryView": "Utilizza la visualizzazione Galleria a pagine"
},
"hotkeys": {
"searchHotkeys": "Cerca tasti di scelta rapida",
Expand Down Expand Up @@ -804,7 +805,9 @@
"updatePathTooltip": "Aggiorna il percorso del file per questo modello se hai spostato i file del modello in una nuova posizione.",
"updatePath": "Aggiorna percorso",
"actions": "Azioni in blocco",
"zImageVae": "VAE (opzionale)"
"zImageVae": "VAE (opzionale)",
"missingFiles": "File mancanti",
"missingFilesTooltip": "I file del modello sono mancanti dal disco"
},
"parameters": {
"images": "Immagini",
Expand Down Expand Up @@ -1949,6 +1952,20 @@
"Off: generazione standard. Auto: abilita automaticamente per immagini > 1536px. 4K: impostazioni ottimizzate per output con risoluzione 4K."
],
"heading": "DyPE (alta risoluzione)"
},
"fluxDypeScale": {
"paragraphs": [
"Controlla l'entità della modulazione DyPE. Valori più alti = estrapolazione più forte.",
"Predefinito: 2.0. Intervallo: 0.0-8.0."
]
},
"fluxDypeExponent": {
"paragraphs": [
"Controlla l'intensità dell'effetto dinamico nel tempo.",
"2.0: Consigliato per risoluzioni 4K+. Programmazione aggressiva con transizioni rapide per la pulizia degli artefatti.",
"1.0: Buon punto di partenza per risoluzioni ~2K-3K.",
"0.5: Programma più delicato per risoluzioni appena superiori a quelle native (1024px)."
]
}
},
"sdxl": {
Expand Down Expand Up @@ -2001,7 +2018,6 @@
"vae": "VAE",
"parsingFailed": "Analisi non riuscita",
"recallParameter": "Richiama {{label}}",
"dypePreset": "Preimpostazione DyPE",
"seedVarianceRandomizePercent": "Casualità della varianza del seme %",
"seedVarianceEnabled": "Varianza seme abilitata",
"seedVarianceStrength": "Intensità della varianza del seme"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
import { Flex } from '@invoke-ai/ui-library';
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
import { EntityListSelectedEntityActionBarInvertMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton';
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton';
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
import { EntityListNonRasterLayerToggle } from 'features/controlLayers/components/common/CanvasNonRasterLayersIsHiddenToggle';
import { memo } from 'react';

import { EntityListSelectedEntityActionBarSaveToAssetsButton } from './EntityListSelectedEntityActionBarSaveToAssetsButton';
import { EntityListSelectedEntityActionBarCompositeOperation } from './EntityListSelectedEntityActionBarCompositeOperation';

export const EntityListSelectedEntityActionBar = memo(() => {
return (
<Flex w="full" gap={2} alignItems="center" ps={1}>
<EntityListSelectedEntityActionBarOpacity />
<Spacer />
<EntityListSelectedEntityActionBarFill />
<Flex h="full">
<EntityListSelectedEntityActionBarSelectObjectButton />
<EntityListSelectedEntityActionBarFilterButton />
<EntityListSelectedEntityActionBarTransformButton />
<EntityListSelectedEntityActionBarInvertMaskButton />
<EntityListSelectedEntityActionBarSaveToAssetsButton />
<EntityListSelectedEntityActionBarDuplicateButton />
<EntityListNonRasterLayerToggle />
<EntityListGlobalActionBarAddLayerMenu />
<Flex flexDirection="column" gap={2}>
<Flex w="full" gap={2} ps={1}>
<EntityListSelectedEntityActionBarCompositeOperation />
<EntityListSelectedEntityActionBarOpacity />
<EntityListSelectedEntityActionBarFill />
</Flex>
</Flex>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice';
import {
selectCanvasSlice,
selectEntity,
selectSelectedEntityIdentifier,
} from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
CanvasRasterLayerState,
CompositeOperation,
} from 'features/controlLayers/store/types';
import { COLOR_BLEND_MODES } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

const selectCompositeOperation = createSelector(selectCanvasSlice, (canvas) => {
const { selectedEntityIdentifier } = canvas;

if (selectedEntityIdentifier?.type !== 'raster_layer') {
return 'source-over';
}

const entity = selectEntity(canvas, selectedEntityIdentifier);

return (entity as CanvasRasterLayerState)?.globalCompositeOperation ?? 'source-over';
});

export const EntityListSelectedEntityActionBarCompositeOperation = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const currentOperation = useAppSelector(selectCompositeOperation);

const onChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
if (selectedEntityIdentifier?.type === 'raster_layer') {
const value = e.target.value as CompositeOperation;

dispatch(
rasterLayerGlobalCompositeOperationChanged({
entityIdentifier: selectedEntityIdentifier as CanvasEntityIdentifier<'raster_layer'>,
globalCompositeOperation: value,
})
);
}
},
[dispatch, selectedEntityIdentifier]
);

if (selectedEntityIdentifier?.type !== 'raster_layer') {
return null;
}

return (
<FormControl w="min-content" gap={2}>
<FormLabel m={0} mt={1} whiteSpace="nowrap">
{t('controlLayers.compositeOperation.label')}
</FormLabel>
<Select value={currentOperation} onChange={onChange} size="sm" variant="outline" minW="110px">
{COLOR_BLEND_MODES.map((op) => (
<option key={op} value={op}>
{t(`controlLayers.compositeOperation.blendModes.${op}`)}
</option>
))}
</Select>
</FormControl>
);
});

EntityListSelectedEntityActionBarCompositeOperation.displayName = 'EntityListSelectedEntityActionBarCompositeOperation';
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
return (
<Popover>
<FormControl w="min-content" gap={2} isDisabled={selectedEntityIdentifier === null}>
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<FormLabel m={0} mt={1}>
{t('controlLayers.opacity')}
</FormLabel>
<PopoverAnchor>
<NumberInput
display="flex"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Flex } from '@invoke-ai/ui-library';
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
import { EntityListSelectedEntityActionBarInvertMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton';
import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton';
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
import { EntityListNonRasterLayerToggle } from 'features/controlLayers/components/common/CanvasNonRasterLayersIsHiddenToggle';
import { memo } from 'react';

import { EntityListSelectedEntityActionBarSaveToAssetsButton } from './EntityListSelectedEntityActionBarSaveToAssetsButton';

export const EntityListSelectedEntityOperationsBar = memo(() => {
return (
<Flex w="full" minH="20px" gap={1} alignItems="center" ps={2} pr={2}>
<EntityListGlobalActionBarAddLayerMenu />
<EntityListSelectedEntityActionBarTransformButton />
<EntityListSelectedEntityActionBarDuplicateButton />
<EntityListSelectedEntityActionBarSelectObjectButton />
<EntityListSelectedEntityActionBarFilterButton />
<EntityListSelectedEntityActionBarInvertMaskButton />
<EntityListNonRasterLayerToggle />
<EntityListSelectedEntityActionBarSaveToAssetsButton />
</Flex>
);
});

EntityListSelectedEntityOperationsBar.displayName = 'EntityListSelectedEntityOperationsBar';
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/Canva
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo } from 'react';

import { EntityListSelectedEntityOperationsBar } from './CanvasEntityList/EntityListSelectedEntityOperationsBar';
import { ParamDenoisingStrength } from './ParamDenoisingStrength';

export const CanvasLayersPanel = memo(() => {
Expand All @@ -15,12 +16,14 @@ export const CanvasLayersPanel = memo(() => {
return (
<CanvasManagerProviderGate>
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListSelectedEntityActionBar />
<Divider py={0} />
<ParamDenoisingStrength />
<Divider py={0} />
<EntityListSelectedEntityActionBar />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
<Divider py={0} />
<EntityListSelectedEntityOperationsBar />
</Flex>
</CanvasManagerProviderGate>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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 { RasterLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertToSubMenu';
import { RasterLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCopyToSubMenu';
import { memo } from 'react';
Expand All @@ -28,6 +29,7 @@ export const RasterLayerMenuItems = memo(() => {
<RasterLayerMenuItemsAdjustments />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<RasterLayerMenuItemsBooleanSubMenu />
<RasterLayerMenuItemsCopyToSubMenu />
<RasterLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier';
import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice';
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';

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 perform = useCallback(
async (op: CompositeOperation) => {
if (!entityIdentifierBelowThisOne) {
return;
}
dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: op }));
try {
// Use boolean-specific merge which disables the source raster layers instead of deleting them
await canvasManager.compositor.mergeBooleanRasterLayers(
entityIdentifierBelowThisOne as CanvasEntityIdentifier<'raster_layer'>,
entityIdentifier as CanvasEntityIdentifier<'raster_layer'>,
true
);
} finally {
dispatch(rasterLayerGlobalCompositeOperationChanged({ entityIdentifier, globalCompositeOperation: undefined }));
}
},
[canvasManager.compositor, dispatch, entityIdentifier, entityIdentifierBelowThisOne]
);

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]);

const disabled = isBusy || !entityIdentifierBelowThisOne;

return (
<MenuItem {...subMenu.parentMenuItemProps} isDisabled={disabled} icon={<CgPathCrop size={18} />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.booleanOps.label')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={onIntersect} isDisabled={disabled} icon={<CgPathIntersect size={18} />}>
{t('controlLayers.booleanOps.intersect')}
</MenuItem>
<MenuItem onClick={onCutOut} isDisabled={disabled} icon={<CgPathBack size={18} />}>
{t('controlLayers.booleanOps.cutout')}
</MenuItem>
<MenuItem onClick={onCutAway} isDisabled={disabled} icon={<CgPathFront size={18} />}>
{t('controlLayers.booleanOps.cutaway')}
</MenuItem>
<MenuItem onClick={onExclude} isDisabled={disabled} icon={<CgPathExclude size={18} />}>
{t('controlLayers.booleanOps.exclude')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});

RasterLayerMenuItemsBooleanSubMenu.displayName = 'RasterLayerMenuItemsBooleanSubMenu';
Loading