Skip to content

Commit e4a640f

Browse files
psychedelicioushipsterusername
authored andcommitted
feat(ui): optimized rendering of selected layer
Instead of caching on every stroke, we can use a compositing rect when the layer is being drawn to improve performance.
1 parent b5b6a96 commit e4a640f

File tree

3 files changed

+52
-6
lines changed

3 files changed

+52
-6
lines changed

invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,7 @@ export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect';
889889
export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer';
890890
export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
891891
export const LAYER_BBOX_NAME = 'layer.bbox';
892+
export const COMPOSITING_RECT_NAME = 'compositing-rect';
892893

893894
// Getters for non-singleton layer and object IDs
894895
const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;

invokeai/frontend/web/src/features/controlLayers/util/bbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
123123
return correctedLayerBbox;
124124
};
125125

126-
export const getLayerBboxFast = (layer: KonvaLayerType): IRect | null => {
126+
export const getLayerBboxFast = (layer: KonvaLayerType): IRect => {
127127
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
128128
return {
129129
x: Math.floor(bbox.x),

invokeai/frontend/web/src/features/controlLayers/util/renderers.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
BACKGROUND_RECT_ID,
88
CA_LAYER_IMAGE_NAME,
99
CA_LAYER_NAME,
10+
COMPOSITING_RECT_NAME,
1011
getCALayerImageId,
1112
getIILayerImageId,
1213
getLayerBboxId,
@@ -324,6 +325,12 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro
324325
return vectorMaskRect;
325326
};
326327

328+
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
329+
const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false });
330+
konvaLayer.add(compositingRect);
331+
return compositingRect;
332+
};
333+
327334
/**
328335
* Renders a vector mask layer.
329336
* @param stage The konva stage to render on.
@@ -401,15 +408,53 @@ const renderRegionalGuidanceLayer = (
401408
groupNeedsCache = true;
402409
}
403410

404-
if (konvaObjectGroup.children.length === 0) {
411+
if (konvaObjectGroup.getChildren().length === 0) {
405412
// No objects - clear the cache to reset the previous pixel data
406413
konvaObjectGroup.clearCache();
407-
} else if (groupNeedsCache) {
408-
konvaObjectGroup.cache();
414+
return;
409415
}
410416

411-
// Updating group opacity does not require re-caching
412-
if (konvaObjectGroup.opacity() !== globalMaskLayerOpacity) {
417+
const compositingRect =
418+
konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer);
419+
420+
/**
421+
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
422+
* shapes to render as a "raster" layer with all pixels drawn at the same color and opacity.
423+
*
424+
* Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The
425+
* effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity.
426+
* Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes.
427+
*
428+
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
429+
* a single raster image, and _then_ applied the 50% opacity.
430+
*/
431+
if (reduxLayer.isSelected && tool !== 'move') {
432+
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
433+
if (konvaObjectGroup.isCached()) {
434+
konvaObjectGroup.clearCache();
435+
}
436+
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
437+
konvaObjectGroup.opacity(1);
438+
439+
compositingRect.setAttrs({
440+
// The rect should be the size of the layer - use the fast method bc it's OK if the rect is larger
441+
...getLayerBboxFast(konvaLayer),
442+
fill: rgbColor,
443+
opacity: globalMaskLayerOpacity,
444+
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
445+
globalCompositeOperation: 'source-in',
446+
visible: true,
447+
// This rect must always be on top of all other shapes
448+
zIndex: konvaObjectGroup.getChildren().length,
449+
});
450+
} else {
451+
// The compositing rect should only be shown when the layer is selected.
452+
compositingRect.visible(false);
453+
// Cache only if needed - or if we are on this code path and _don't_ have a cache
454+
if (groupNeedsCache || !konvaObjectGroup.isCached()) {
455+
konvaObjectGroup.cache();
456+
}
457+
// Updating group opacity does not require re-caching
413458
konvaObjectGroup.opacity(globalMaskLayerOpacity);
414459
}
415460
};

0 commit comments

Comments
 (0)