diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index d7ac0a809e..97e2544362 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1907,6 +1907,7 @@
"addPositivePrompt": "Add $t(controlLayers.prompt)",
"addNegativePrompt": "Add $t(controlLayers.negativePrompt)",
"addReferenceImage": "Add $t(controlLayers.referenceImage)",
+ "addImageNoise": "Add $t(controlLayers.imageNoise)",
"addRasterLayer": "Add $t(controlLayers.rasterLayer)",
"addControlLayer": "Add $t(controlLayers.controlLayer)",
"addInpaintMask": "Add $t(controlLayers.inpaintMask)",
@@ -2011,6 +2012,7 @@
"replaceCurrent": "Replace Current",
"controlLayerEmptyState": "Upload an image, drag an image from the gallery onto this layer, pull the bounding box into this layer, or draw on the canvas to get started.",
"referenceImageEmptyState": "Upload an image, drag an image from the gallery onto this layer, or pull the bounding box into this layer to get started.",
+ "imageNoise": "Image Noise",
"warnings": {
"problemsFound": "Problems found",
"unsupportedModel": "layer not supported for selected base model",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx
index ecfe91c33d..fc600018cc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx
@@ -4,6 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
+import { InpaintMaskSettings } from 'features/controlLayers/components/InpaintMask/InpaintMaskSettings';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { InpaintMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
@@ -28,6 +29,7 @@ export const InpaintMask = memo(({ id }: Props) => {
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton.tsx
new file mode 100644
index 0000000000..2ad235b85a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton.tsx
@@ -0,0 +1,19 @@
+import { Button, Flex } from '@invoke-ai/ui-library';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { useAddInpaintMaskNoise } from 'features/controlLayers/hooks/addLayerHooks';
+import { useTranslation } from 'react-i18next';
+import { PiPlusBold } from 'react-icons/pi';
+
+export const InpaintMaskAddNoiseButton = () => {
+ const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
+ const { t } = useTranslation();
+ const addInpaintMaskNoise = useAddInpaintMaskNoise(entityIdentifier);
+
+ return (
+
+ } onClick={addInpaintMaskNoise}>
+ {t('controlLayers.imageNoise')}
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDeleteNoiseButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDeleteNoiseButton.tsx
new file mode 100644
index 0000000000..17b6120e0e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDeleteNoiseButton.tsx
@@ -0,0 +1,29 @@
+import type { IconButtonProps } from '@invoke-ai/ui-library';
+import { IconButton } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXBold } from 'react-icons/pi';
+
+type Props = Omit & {
+ onDelete: () => void;
+};
+
+export const InpaintMaskDeleteNoiseButton = memo(({ onDelete, ...rest }: Props) => {
+ const { t } = useTranslation();
+ return (
+ }
+ onClick={onDelete}
+ flexGrow={0}
+ size="sm"
+ p={0}
+ colorScheme="error"
+ {...rest}
+ />
+ );
+});
+
+InpaintMaskDeleteNoiseButton.displayName = 'InpaintMaskDeleteNoiseButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx
index ae99388bd7..179c9acc26 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx
@@ -7,6 +7,7 @@ import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/component
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
+import { InpaintMaskMenuItemsAddNoise } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise';
import { InpaintMaskMenuItemsConvertToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsConvertToSubMenu';
import { InpaintMaskMenuItemsCopyToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsCopyToSubMenu';
import { memo } from 'react';
@@ -20,6 +21,8 @@ export const InpaintMaskMenuItems = memo(() => {
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise.tsx
new file mode 100644
index 0000000000..ad8bea2c29
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise.tsx
@@ -0,0 +1,21 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { useAddInpaintMaskNoise } from 'features/controlLayers/hooks/addLayerHooks';
+import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const InpaintMaskMenuItemsAddNoise = memo(() => {
+ const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
+ const { t } = useTranslation();
+ const isBusy = useCanvasIsBusy();
+ const addInpaintMaskNoise = useAddInpaintMaskNoise(entityIdentifier);
+
+ return (
+
+ );
+});
+
+InpaintMaskMenuItemsAddNoise.displayName = 'InpaintMaskMenuItemsAddNoise';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx
new file mode 100644
index 0000000000..8b206ceb28
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx
@@ -0,0 +1,68 @@
+import { Flex, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text } from '@invoke-ai/ui-library';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { inpaintMaskNoiseChanged, inpaintMaskNoiseDeleted } from 'features/controlLayers/store/canvasSlice';
+import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { InpaintMaskDeleteNoiseButton } from './InpaintMaskDeleteNoiseButton';
+
+export const InpaintMaskNoiseSlider = memo(() => {
+ const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+
+ const selectNoiseLevel = useMemo(
+ () =>
+ createSelector(
+ selectCanvasSlice,
+ (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskNoiseSlider').noiseLevel
+ ),
+ [entityIdentifier]
+ );
+ const noiseLevel = useAppSelector(selectNoiseLevel);
+
+ const handleNoiseChange = useCallback(
+ (value: number) => {
+ dispatch(inpaintMaskNoiseChanged({ entityIdentifier, noiseLevel: value }));
+ },
+ [dispatch, entityIdentifier]
+ );
+
+ const onDeleteNoise = useCallback(() => {
+ dispatch(inpaintMaskNoiseDeleted({ entityIdentifier }));
+ }, [dispatch, entityIdentifier]);
+
+ if (noiseLevel === null) {
+ return null;
+ }
+
+ return (
+
+
+ {t('controlLayers.imageNoise')}
+
+ {Math.round(noiseLevel * 100)}%
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+InpaintMaskNoiseSlider.displayName = 'InpaintMaskNoiseSlider';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx
new file mode 100644
index 0000000000..a3d378ae23
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx
@@ -0,0 +1,32 @@
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import { useAppSelector } from 'app/store/storeHooks';
+import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
+import { InpaintMaskAddNoiseButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton';
+import { InpaintMaskNoiseSlider } from 'features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
+import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
+import { memo, useMemo } from 'react';
+
+const buildSelectFlags = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) =>
+ createMemoizedSelector(selectCanvasSlice, (canvas) => {
+ const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings');
+ return {
+ hasNoiseLevel: entity.noiseLevel !== null,
+ };
+ });
+
+export const InpaintMaskSettings = memo(() => {
+ const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
+ const selectFlags = useMemo(() => buildSelectFlags(entityIdentifier), [entityIdentifier]);
+ const flags = useAppSelector(selectFlags);
+
+ return (
+
+ {!flags.hasNoiseLevel && }
+ {flags.hasNoiseLevel && }
+
+ );
+});
+
+InpaintMaskSettings.displayName = 'InpaintMaskSettings';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index 27b01fe494..dc038bc664 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -6,6 +6,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
controlLayerAdded,
inpaintMaskAdded,
+ inpaintMaskNoiseAdded,
rasterLayerAdded,
referenceImageAdded,
rgAdded,
@@ -222,6 +223,15 @@ export const useAddRegionalGuidanceNegativePrompt = (entityIdentifier: CanvasEnt
return runc;
};
+export const useAddInpaintMaskNoise = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => {
+ const dispatch = useAppDispatch();
+ const func = useCallback(() => {
+ dispatch(inpaintMaskNoiseAdded({ entityIdentifier }));
+ }, [dispatch, entityIdentifier]);
+
+ return func;
+};
+
export const buildSelectValidRegionalGuidanceActions = (
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>
) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
index 8a2b866e56..b1348395fa 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts
@@ -1096,6 +1096,30 @@ export const canvasSlice = createSlice({
state.inpaintMasks.entities = [data];
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
},
+ inpaintMaskNoiseAdded: (state, action: PayloadAction>) => {
+ const { entityIdentifier } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
+ if (entity && entity.type === 'inpaint_mask') {
+ entity.noiseLevel = 0.1; // Default noise level
+ }
+ },
+ inpaintMaskNoiseChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, noiseLevel } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
+ if (entity && entity.type === 'inpaint_mask') {
+ entity.noiseLevel = noiseLevel;
+ }
+ },
+ inpaintMaskNoiseDeleted: (state, action: PayloadAction>) => {
+ const { entityIdentifier } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
+ if (entity && entity.type === 'inpaint_mask') {
+ entity.noiseLevel = null;
+ }
+ },
inpaintMaskConvertedToRegionalGuidance: {
reducer: (
state,
@@ -1869,6 +1893,9 @@ export const {
// Inpaint mask
inpaintMaskAdded,
inpaintMaskConvertedToRegionalGuidance,
+ inpaintMaskNoiseAdded,
+ inpaintMaskNoiseChanged,
+ inpaintMaskNoiseDeleted,
// inpaintMaskRecalled,
} = canvasSlice.actions;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 268e6004dd..4b211fd6cb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -310,6 +310,7 @@ const zCanvasInpaintMaskState = zCanvasEntityBase.extend({
fill: zFill,
opacity: zOpacity,
objects: z.array(zCanvasObjectState),
+ noiseLevel: z.number().gte(0).lte(1).nullable().default(null),
});
export type CanvasInpaintMaskState = z.infer;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/util.ts b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
index c5d23b8c08..4acf92228c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
@@ -199,6 +199,7 @@ export const getInpaintMaskState = (
style: 'diagonal',
color: getInpaintMaskFillColor(),
},
+ noiseLevel: null,
};
merge(entityState, overrides);
return entityState;