fix(ui): inpaint mask rendering

This commit is contained in:
psychedelicious 2024-08-06 12:41:17 +10:00
parent a6a7fe8aba
commit f4e66bf14f
4 changed files with 53 additions and 38 deletions

View File

@ -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}

View File

@ -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 }) => {

View File

@ -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

View File

@ -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;
}; };
/** /**