From 5924dc6ff627e0982bd9fd20759a047a29a36d1e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:31:08 +1000 Subject: [PATCH] feat(ui): transparency on regional prompts canvas --- .../components/imperative/konvaApiDraft.tsx | 4 +- .../components/imperative/renderers.ts | 42 ++++++++++++++++--- .../store/regionalPromptsSlice.ts | 1 + .../src/features/regionalPrompts/util/bbox.ts | 2 +- .../regionalPrompts/util/getLayerBlobs.ts | 6 ++- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx index 614c3bc348..dfaf8c7823 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx @@ -129,8 +129,8 @@ export const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTML if (!stage) { return; } - renderLayers(stage, state.layers, state.selectedLayer, onLayerPosChanged); - }, [onLayerPosChanged, stage, state.layers, state.selectedLayer]); + renderLayers(stage, state.layers, state.selectedLayer, state.promptLayerOpacity, state.tool, onLayerPosChanged); + }, [onLayerPosChanged, stage, state.layers, state.promptLayerOpacity, state.tool, state.selectedLayer]); useLayoutEffect(() => { console.log('Rendering bbox'); diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/renderers.ts b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/renderers.ts index f4c3cd415b..a50f9666a4 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/renderers.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/renderers.ts @@ -8,6 +8,7 @@ import { BRUSH_PREVIEW_LAYER_ID, getPromptRegionLayerBboxId, getPromptRegionLayerObjectGroupId, + getPromptRegionLayerTransparencyRectId, REGIONAL_PROMPT_LAYER_BBOX_NAME, REGIONAL_PROMPT_LAYER_LINE_NAME, REGIONAL_PROMPT_LAYER_NAME, @@ -15,12 +16,14 @@ import { } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox'; import Konva from 'konva'; +import type { Node, NodeConfig } from 'konva/lib/Node'; import type { IRect, Vector2d } from 'konva/lib/types'; import type { RgbColor } from 'react-colorful'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import { selectPromptLayerObjectGroup } from './konvaApiDraft'; +const BRUSH_PREVIEW_BORDER_INNER_COLOR = 'rgba(0,0,0,1)'; +const BRUSH_PREVIEW_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; /** * Renders the brush preview for the selected tool. @@ -44,7 +47,7 @@ export const renderBrushPreview = ( let layer = stage.findOne(`#${BRUSH_PREVIEW_LAYER_ID}`); if (!layer) { // Initialize the brush preview layer & add to the stage - layer = new Konva.Layer({ id: BRUSH_PREVIEW_LAYER_ID, visible: tool !== 'move' }); + layer = new Konva.Layer({ id: BRUSH_PREVIEW_LAYER_ID, visible: tool !== 'move', listening: false }); stage.add(layer); // The brush preview is hidden and shown as the mouse leaves and enters the stage stage.on('mouseleave', (e) => { @@ -82,7 +85,7 @@ export const renderBrushPreview = ( borderInner = new Konva.Circle({ id: BRUSH_PREVIEW_BORDER_INNER_ID, listening: false, - stroke: 'rgba(0,0,0,1)', + stroke: BRUSH_PREVIEW_BORDER_INNER_COLOR, strokeWidth: 1, strokeEnabled: true, }); @@ -96,7 +99,7 @@ export const renderBrushPreview = ( borderOuter = new Konva.Circle({ id: BRUSH_PREVIEW_BORDER_OUTER_ID, listening: false, - stroke: 'rgba(255,255,255,0.8)', + stroke: BRUSH_PREVIEW_BORDER_OUTER_COLOR, strokeWidth: 1, strokeEnabled: true, }); @@ -114,6 +117,7 @@ export const renderBrushPreview = ( * @param stage The konva stage to render on. * @param reduxLayers Array of the layers from the redux store. * @param selectedLayerId The selected layer id. + * @param layerOpacity The opacity of the layer. * @param onLayerPosChanged Callback for when the layer's position changes. This is optional to allow for offscreen rendering. * @returns */ @@ -121,6 +125,8 @@ export const renderLayers = ( stage: Konva.Stage, reduxLayers: Layer[], selectedLayerId: string | null, + layerOpacity: number, + tool: Tool, onLayerPosChanged?: (layerId: string, x: number, y: number) => void ) => { const reduxLayerIds = reduxLayers.map((l) => l.id); @@ -177,6 +183,16 @@ export const renderLayers = ( }); konvaLayer.add(konvaObjectGroup); + // To achieve performant transparency, we use the `source-in` blending mode on a rect that covers the entire layer. + // The brush strokes group functions as a mask for this rect, which has the layer's fill and opacity. The brush + // strokes' color doesn't matter - the only requirement is that they are not transparent. + const transparencyRect = new Konva.Rect({ + id: getPromptRegionLayerTransparencyRectId(reduxLayer.id), + globalCompositeOperation: 'source-in', + listening: false, + }); + konvaLayer.add(transparencyRect); + stage.add(konvaLayer); // When a layer is added, it ends up on top of the brush preview - we need to move the preview back to the top. @@ -185,7 +201,7 @@ export const renderLayers = ( // Update the layer's position and listening state (only the selected layer is listening) konvaLayer.setAttrs({ - listening: reduxLayer.id === selectedLayerId, + listening: reduxLayer.id === selectedLayerId && tool === 'move', x: reduxLayer.x, y: reduxLayer.y, }); @@ -193,6 +209,10 @@ export const renderLayers = ( const color = rgbColorToString(reduxLayer.color); const konvaObjectGroup = konvaLayer.findOne(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`); assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`); + const transparencyRect = konvaLayer.findOne( + `#${getPromptRegionLayerTransparencyRectId(reduxLayer.id)}` + ); + assert(transparencyRect, `Transparency rect not found for layer ${reduxLayer.id}`); // Remove deleted objects const objectIds = reduxLayer.objects.map((o) => o.id); @@ -240,9 +260,19 @@ export const renderLayers = ( konvaObject.visible(reduxLayer.isVisible); } } + + // Set the layer opacity - must happen after all objects are added to the layer so the rect is the right size + transparencyRect.setAttrs({ + ...konvaLayer.getClientRect({ skipTransform: true }), + fill: color, + opacity: layerOpacity, + }); } }; +const selectPromptLayerObjectGroup = (item: Node) => + item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME; + /** * * @param stage The konva stage to render on. @@ -260,6 +290,7 @@ export const renderBbox = ( // Hide all bounding boxes for (const bboxRect of stage.find(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`)) { bboxRect.visible(false); + bboxRect.listening(false); } // No selected layer or not using the move tool - nothing more to do here @@ -288,6 +319,7 @@ export const renderBbox = ( y: bbox.y, width: bbox.width, height: bbox.height, + listening: true, stroke: selectedLayerId === selectedLayerId ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)', }); }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts index 28f941e642..db85d88c20 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -316,3 +316,4 @@ export const getPromptRegionLayerLineId = (layerId: string, lineId: string) => ` export const getPromptRegionLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getPromptRegionLayerBboxId = (layerId: string) => `${layerId}.bbox`; +export const getPromptRegionLayerTransparencyRectId = (layerId: string) => `${layerId}.transparency_rect`; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/bbox.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/bbox.ts index 5f0dc42088..622be8e5f4 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/bbox.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/bbox.ts @@ -68,7 +68,7 @@ export const getKonvaLayerBbox = ( child.destroy(); } else { // We need to re-cache to handle children with transparency and multiple objects - like prompt region layers. - child.cache(); + // child.cache(); } } diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts index 52bca435cf..cbdf07cb3b 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts @@ -19,7 +19,7 @@ export const getRegionalPromptLayerBlobs = async ( const state = getStore().getState(); const container = document.createElement('div'); const stage = new Konva.Stage({ container, width: state.generation.width, height: state.generation.height }); - renderLayers(stage, state.regionalPrompts.layers, state.regionalPrompts.selectedLayer); + renderLayers(stage, state.regionalPrompts.layers, state.regionalPrompts.selectedLayer, 1, 'brush'); const layers = stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`); const blobs: Record = {}; @@ -48,7 +48,9 @@ export const getRegionalPromptLayerBlobs = async ( if (preview) { const base64 = await blobToDataURL(blob); - openBase64ImageInTab([{ base64, caption: `${reduxLayer.id}: ${reduxLayer.positivePrompt} / ${reduxLayer.negativePrompt}` }]); + openBase64ImageInTab([ + { base64, caption: `${reduxLayer.id}: ${reduxLayer.positivePrompt} / ${reduxLayer.negativePrompt}` }, + ]); } layer.remove(); blobs[layer.id()] = blob;