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 onChange = useCallback(
(v: number) => {
dispatch(maskOpacityChanged(v / 100));
dispatch(maskOpacityChanged(Math.max(v / 100, 0.25)));
},
[dispatch]
);
@ -22,7 +22,7 @@ export const MaskOpacity = memo(() => {
<FormLabel m={0}>{t('controlLayers.globalMaskOpacity')}</FormLabel>
<Flex gap={4}>
<CompositeSlider
min={0}
min={25}
max={100}
step={1}
value={opacity}
@ -32,7 +32,7 @@ export const MaskOpacity = memo(() => {
minW={48}
/>
<CompositeNumberInput
min={0}
min={25}
max={100}
step={1}
value={opacity}

View File

@ -20,6 +20,7 @@ export class CanvasInpaintMask {
getLoggingContext: GetLoggingContext;
state: CanvasInpaintMaskState;
maskOpacity: number;
transformer: CanvasTransformer;
renderer: CanvasObjectRenderer;
@ -47,7 +48,7 @@ export class CanvasInpaintMask {
};
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');
this.konva.layer.add(this.konva.objectGroup);
@ -55,6 +56,7 @@ export class CanvasInpaintMask {
this.konva.layer.add(...this.transformer.getNodes());
this.state = state;
this.maskOpacity = this.manager.stateApi.getMaskOpacity();
}
destroy = (): void => {
@ -67,14 +69,18 @@ export class CanvasInpaintMask {
update = async (arg?: { state: CanvasInpaintMaskState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
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');
return;
}
// const maskOpacity = this.manager.stateApi.getMaskOpacity()
this.log.debug('Updating');
const { position, objects, isEnabled } = state;
@ -82,18 +88,23 @@ export class CanvasInpaintMask {
await this.updateObjects({ objects });
}
if (this.isFirstRender || position !== this.state.position) {
await this.transformer.updatePosition({ position });
this.transformer.updatePosition({ position });
}
// if (this.isFirstRender || opacity !== this.state.opacity) {
// await this.updateOpacity({ opacity });
// }
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();
if (this.isFirstRender) {
await this.transformer.updateBbox();
this.transformer.updateBbox();
}
this.state = state;
@ -110,8 +121,6 @@ export class CanvasInpaintMask {
if (didUpdate) {
this.transformer.requestRectCalculation();
}
this.isFirstRender = false;
};
// updateOpacity = (arg?: { opacity: number }) => {

View File

@ -366,6 +366,7 @@ export class CanvasManager {
let currentFill: RgbaColor = state.tool.fill;
const selectedEntity = this.getSelectedEntity();
if (selectedEntity) {
// These two entity types use a compositing rect for opacity. Their alpha is always 1.
if (selectedEntity.state.type === 'regional_guidance') {
currentFill = { ...selectedEntity.state.fill, a: 1 };
} else if (selectedEntity.state.type === 'inpaint_mask') {
@ -470,6 +471,7 @@ export class CanvasManager {
if (
this._isFirstRender ||
state.inpaintMask !== this._prevState.inpaintMask ||
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id

View File

@ -14,6 +14,7 @@ import type {
CanvasEraserLineState,
CanvasImageState,
CanvasRectState,
RgbColor,
} from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { Logger } from 'roarr';
@ -58,11 +59,26 @@ export class CanvasObjectRenderer {
*/
renderers: Map<string, AnyObjectRenderer> = new Map();
/**
* A object containing singleton Konva nodes.
*/
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;
};
constructor(parent: CanvasLayer | CanvasInpaintMask, withCompositingRect: boolean = false) {
constructor(parent: CanvasLayer | CanvasInpaintMask) {
this.id = getPrefixedId(CanvasObjectRenderer.TYPE);
this.parent = parent;
this.manager = parent.manager;
@ -74,10 +90,11 @@ export class CanvasObjectRenderer {
compositingRect: null,
};
if (withCompositingRect) {
if (this.parent.type === 'inpaint_mask') {
this.konva.compositingRect = new Konva.Rect({
name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME,
listening: false,
globalCompositeOperation: 'source-in',
});
this.parent.konva.objectGroup.add(this.konva.compositingRect);
}
@ -131,27 +148,20 @@ export class CanvasObjectRenderer {
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;
};
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
* parent entity's object group.
@ -230,12 +240,6 @@ export class CanvasObjectRenderer {
this.buffer = objectState;
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;
};
/**