mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): inpaint mask rendering
This commit is contained in:
parent
a6a7fe8aba
commit
f4e66bf14f
@ -13,7 +13,7 @@ export const MaskOpacity = memo(() => {
|
|||||||
const opacity = useAppSelector((s) => Math.round(s.canvasV2.settings.maskOpacity * 100));
|
const opacity = useAppSelector((s) => Math.round(s.canvasV2.settings.maskOpacity * 100));
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(maskOpacityChanged(v / 100));
|
dispatch(maskOpacityChanged(Math.max(v / 100, 0.25)));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -22,7 +22,7 @@ export const MaskOpacity = memo(() => {
|
|||||||
<FormLabel m={0}>{t('controlLayers.globalMaskOpacity')}</FormLabel>
|
<FormLabel m={0}>{t('controlLayers.globalMaskOpacity')}</FormLabel>
|
||||||
<Flex gap={4}>
|
<Flex gap={4}>
|
||||||
<CompositeSlider
|
<CompositeSlider
|
||||||
min={0}
|
min={25}
|
||||||
max={100}
|
max={100}
|
||||||
step={1}
|
step={1}
|
||||||
value={opacity}
|
value={opacity}
|
||||||
@ -32,7 +32,7 @@ export const MaskOpacity = memo(() => {
|
|||||||
minW={48}
|
minW={48}
|
||||||
/>
|
/>
|
||||||
<CompositeNumberInput
|
<CompositeNumberInput
|
||||||
min={0}
|
min={25}
|
||||||
max={100}
|
max={100}
|
||||||
step={1}
|
step={1}
|
||||||
value={opacity}
|
value={opacity}
|
||||||
|
@ -20,6 +20,7 @@ export class CanvasInpaintMask {
|
|||||||
getLoggingContext: GetLoggingContext;
|
getLoggingContext: GetLoggingContext;
|
||||||
|
|
||||||
state: CanvasInpaintMaskState;
|
state: CanvasInpaintMaskState;
|
||||||
|
maskOpacity: number;
|
||||||
|
|
||||||
transformer: CanvasTransformer;
|
transformer: CanvasTransformer;
|
||||||
renderer: CanvasObjectRenderer;
|
renderer: CanvasObjectRenderer;
|
||||||
@ -47,7 +48,7 @@ export class CanvasInpaintMask {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.transformer = new CanvasTransformer(this);
|
this.transformer = new CanvasTransformer(this);
|
||||||
this.renderer = new CanvasObjectRenderer(this, true);
|
this.renderer = new CanvasObjectRenderer(this);
|
||||||
assert(this.renderer.konva.compositingRect, 'Compositing rect must be set');
|
assert(this.renderer.konva.compositingRect, 'Compositing rect must be set');
|
||||||
|
|
||||||
this.konva.layer.add(this.konva.objectGroup);
|
this.konva.layer.add(this.konva.objectGroup);
|
||||||
@ -55,6 +56,7 @@ export class CanvasInpaintMask {
|
|||||||
this.konva.layer.add(...this.transformer.getNodes());
|
this.konva.layer.add(...this.transformer.getNodes());
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
this.maskOpacity = this.manager.stateApi.getMaskOpacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy = (): void => {
|
destroy = (): void => {
|
||||||
@ -67,14 +69,18 @@ export class CanvasInpaintMask {
|
|||||||
|
|
||||||
update = async (arg?: { state: CanvasInpaintMaskState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
|
update = async (arg?: { state: CanvasInpaintMaskState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
|
||||||
const state = get(arg, 'state', this.state);
|
const state = get(arg, 'state', this.state);
|
||||||
|
const maskOpacity = this.manager.stateApi.getMaskOpacity();
|
||||||
|
|
||||||
if (!this.isFirstRender && state === this.state) {
|
if (
|
||||||
|
!this.isFirstRender &&
|
||||||
|
state === this.state &&
|
||||||
|
state.fill === this.state.fill &&
|
||||||
|
maskOpacity === this.maskOpacity
|
||||||
|
) {
|
||||||
this.log.trace('State unchanged, skipping update');
|
this.log.trace('State unchanged, skipping update');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const maskOpacity = this.manager.stateApi.getMaskOpacity()
|
|
||||||
|
|
||||||
this.log.debug('Updating');
|
this.log.debug('Updating');
|
||||||
const { position, objects, isEnabled } = state;
|
const { position, objects, isEnabled } = state;
|
||||||
|
|
||||||
@ -82,18 +88,23 @@ export class CanvasInpaintMask {
|
|||||||
await this.updateObjects({ objects });
|
await this.updateObjects({ objects });
|
||||||
}
|
}
|
||||||
if (this.isFirstRender || position !== this.state.position) {
|
if (this.isFirstRender || position !== this.state.position) {
|
||||||
await this.transformer.updatePosition({ position });
|
this.transformer.updatePosition({ position });
|
||||||
}
|
}
|
||||||
// if (this.isFirstRender || opacity !== this.state.opacity) {
|
// if (this.isFirstRender || opacity !== this.state.opacity) {
|
||||||
// await this.updateOpacity({ opacity });
|
// await this.updateOpacity({ opacity });
|
||||||
// }
|
// }
|
||||||
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
||||||
await this.updateVisibility({ isEnabled });
|
this.updateVisibility({ isEnabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isFirstRender || state.fill !== this.state.fill || maskOpacity !== this.maskOpacity) {
|
||||||
|
this.renderer.updateCompositingRect(state.fill, maskOpacity);
|
||||||
|
this.maskOpacity = maskOpacity;
|
||||||
}
|
}
|
||||||
// this.transformer.syncInteractionState();
|
// this.transformer.syncInteractionState();
|
||||||
|
|
||||||
if (this.isFirstRender) {
|
if (this.isFirstRender) {
|
||||||
await this.transformer.updateBbox();
|
this.transformer.updateBbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
@ -110,8 +121,6 @@ export class CanvasInpaintMask {
|
|||||||
if (didUpdate) {
|
if (didUpdate) {
|
||||||
this.transformer.requestRectCalculation();
|
this.transformer.requestRectCalculation();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isFirstRender = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// updateOpacity = (arg?: { opacity: number }) => {
|
// updateOpacity = (arg?: { opacity: number }) => {
|
||||||
|
@ -366,6 +366,7 @@ export class CanvasManager {
|
|||||||
let currentFill: RgbaColor = state.tool.fill;
|
let currentFill: RgbaColor = state.tool.fill;
|
||||||
const selectedEntity = this.getSelectedEntity();
|
const selectedEntity = this.getSelectedEntity();
|
||||||
if (selectedEntity) {
|
if (selectedEntity) {
|
||||||
|
// These two entity types use a compositing rect for opacity. Their alpha is always 1.
|
||||||
if (selectedEntity.state.type === 'regional_guidance') {
|
if (selectedEntity.state.type === 'regional_guidance') {
|
||||||
currentFill = { ...selectedEntity.state.fill, a: 1 };
|
currentFill = { ...selectedEntity.state.fill, a: 1 };
|
||||||
} else if (selectedEntity.state.type === 'inpaint_mask') {
|
} else if (selectedEntity.state.type === 'inpaint_mask') {
|
||||||
@ -470,6 +471,7 @@ export class CanvasManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this._isFirstRender ||
|
this._isFirstRender ||
|
||||||
|
state.inpaintMask !== this._prevState.inpaintMask ||
|
||||||
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
|
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
|
||||||
state.tool.selected !== this._prevState.tool.selected ||
|
state.tool.selected !== this._prevState.tool.selected ||
|
||||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||||
|
@ -14,6 +14,7 @@ import type {
|
|||||||
CanvasEraserLineState,
|
CanvasEraserLineState,
|
||||||
CanvasImageState,
|
CanvasImageState,
|
||||||
CanvasRectState,
|
CanvasRectState,
|
||||||
|
RgbColor,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
@ -58,11 +59,26 @@ export class CanvasObjectRenderer {
|
|||||||
*/
|
*/
|
||||||
renderers: Map<string, AnyObjectRenderer> = new Map();
|
renderers: Map<string, AnyObjectRenderer> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A object containing singleton Konva nodes.
|
||||||
|
*/
|
||||||
konva: {
|
konva: {
|
||||||
|
/**
|
||||||
|
* The compositing rect is used to draw the inpaint mask as a single shape with a given opacity.
|
||||||
|
*
|
||||||
|
* When drawing multiple transparent shapes on a canvas, overlapping regions will be more opaque. This doesn't
|
||||||
|
* match the expectation for a mask, where all shapes should have the same opacity, even if they overlap.
|
||||||
|
*
|
||||||
|
* To prevent this, we use a trick. Instead of drawing all shapes at the desired opacity, we draw them at opacity of 1.
|
||||||
|
* Then we draw a single rect that covers the entire canvas at the desired opacity, with a globalCompositeOperation
|
||||||
|
* of 'source-in'. The shapes effectively become a mask for the "compositing rect".
|
||||||
|
*
|
||||||
|
* This node is only added when the parent of the renderer is an inpaint mask or region, which require this behavior.
|
||||||
|
*/
|
||||||
compositingRect: Konva.Rect | null;
|
compositingRect: Konva.Rect | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(parent: CanvasLayer | CanvasInpaintMask, withCompositingRect: boolean = false) {
|
constructor(parent: CanvasLayer | CanvasInpaintMask) {
|
||||||
this.id = getPrefixedId(CanvasObjectRenderer.TYPE);
|
this.id = getPrefixedId(CanvasObjectRenderer.TYPE);
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.manager = parent.manager;
|
this.manager = parent.manager;
|
||||||
@ -74,10 +90,11 @@ export class CanvasObjectRenderer {
|
|||||||
compositingRect: null,
|
compositingRect: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (withCompositingRect) {
|
if (this.parent.type === 'inpaint_mask') {
|
||||||
this.konva.compositingRect = new Konva.Rect({
|
this.konva.compositingRect = new Konva.Rect({
|
||||||
name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME,
|
name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME,
|
||||||
listening: false,
|
listening: false,
|
||||||
|
globalCompositeOperation: 'source-in',
|
||||||
});
|
});
|
||||||
this.parent.konva.objectGroup.add(this.konva.compositingRect);
|
this.parent.konva.objectGroup.add(this.konva.compositingRect);
|
||||||
}
|
}
|
||||||
@ -131,27 +148,20 @@ export class CanvasObjectRenderer {
|
|||||||
didRender = (await this.renderObject(this.buffer)) || didRender;
|
didRender = (await this.renderObject(this.buffer)) || didRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (didRender && this.parent.type === 'inpaint_mask') {
|
|
||||||
assert(this.konva.compositingRect, 'Compositing rect must exist for inpaint mask');
|
|
||||||
|
|
||||||
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
|
||||||
const rgbColor = rgbColorToString(this.parent.state.fill);
|
|
||||||
const maskOpacity = this.manager.stateApi.getMaskOpacity();
|
|
||||||
|
|
||||||
this.konva.compositingRect.setAttrs({
|
|
||||||
fill: rgbColor,
|
|
||||||
opacity: maskOpacity,
|
|
||||||
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
|
||||||
globalCompositeOperation: 'source-in',
|
|
||||||
visible: true,
|
|
||||||
// This rect must always be on top of all other shapes
|
|
||||||
// zIndex: this.renderers.size + 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return didRender;
|
return didRender;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateCompositingRect = (fill: RgbColor, opacity: number) => {
|
||||||
|
this.log.trace('Updating compositing rect');
|
||||||
|
assert(this.konva.compositingRect, 'Missing compositing rect');
|
||||||
|
|
||||||
|
const rgbColor = rgbColorToString(fill);
|
||||||
|
this.konva.compositingRect.setAttrs({
|
||||||
|
fill: rgbColor,
|
||||||
|
opacity,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the given object. If the object renderer does not exist, it will be created and its Konva group added to the
|
* Renders the given object. If the object renderer does not exist, it will be created and its Konva group added to the
|
||||||
* parent entity's object group.
|
* parent entity's object group.
|
||||||
@ -230,12 +240,6 @@ export class CanvasObjectRenderer {
|
|||||||
|
|
||||||
this.buffer = objectState;
|
this.buffer = objectState;
|
||||||
return await this.renderObject(this.buffer, true);
|
return await this.renderObject(this.buffer, true);
|
||||||
|
|
||||||
// const didDraw = await this.renderObject(this.buffer, true);
|
|
||||||
// if (didDraw && this.konva.compositingRect) {
|
|
||||||
// this.konva.compositingRect.zIndex(this.renderers.size + 1);
|
|
||||||
// }
|
|
||||||
// return didDraw;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user