diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask.ts index 03988f6efe2..6e873e5e297 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask.ts @@ -59,11 +59,11 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase< this.syncOpacity(); } if (!prevState || this.state.fill !== prevState.fill) { - // On first render, we must force the update - this.renderer.updateCompositingRectFill(!prevState); + // On first render, or when the fill changes, we must force the update + this.renderer.updateCompositingRectFill(true); } - if (!prevState) { - // On first render, we must force the updates + if (!prevState || this.state.objects !== prevState.objects) { + // On first render, or when the objects change, we must force the update this.renderer.updateCompositingRectSize(true); this.renderer.updateCompositingRectPosition(true); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance.ts index a56ea2ce0f7..a46209002a8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance.ts @@ -59,11 +59,11 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase this.syncOpacity(); } if (!prevState || this.state.fill !== prevState.fill) { - // On first render, we must force the update - this.renderer.updateCompositingRectFill(!prevState); + // On first render, or when the fill changes, we must force the update + this.renderer.updateCompositingRectFill(true); } - if (!prevState) { - // On first render, we must force the updates + if (!prevState || this.state.objects !== prevState.objects) { + // On first render, or when the objects change, we must force the update this.renderer.updateCompositingRectSize(true); this.renderer.updateCompositingRectPosition(true); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBrushToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBrushToolModule.ts index 249df35021d..d6c4dea4508 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBrushToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBrushToolModule.ts @@ -277,8 +277,11 @@ export class CanvasBrushToolModule extends CanvasModuleBase { let points: number[]; + let isShiftDraw = false; + if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point + isShiftDraw = true; points = [ lastLinePoint.x, lastLinePoint.y, @@ -298,15 +301,18 @@ export class CanvasBrushToolModule extends CanvasModuleBase { points, strokeWidth: settings.brushWidth, color: this.manager.stateApi.getCurrentColor(), - clip: this.parent.getClip(selectedEntity.state), + // When shift is held, the line may extend beyond the clip region. No clip for these lines. + clip: isShiftDraw ? null : this.parent.getClip(selectedEntity.state), }); } else { const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line'); let points: number[]; + let isShiftDraw = false; if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point + isShiftDraw = true; points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y]; } else { // Create a new line with the current point @@ -319,7 +325,8 @@ export class CanvasBrushToolModule extends CanvasModuleBase { points, strokeWidth: settings.brushWidth, color: this.manager.stateApi.getCurrentColor(), - clip: this.parent.getClip(selectedEntity.state), + // When shift is held, the line may extend beyond the clip region. No clip for these lines. + clip: isShiftDraw ? null : this.parent.getClip(selectedEntity.state), }); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasColorPickerToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasColorPickerToolModule.ts index d6f9393259a..f7901f0097c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasColorPickerToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasColorPickerToolModule.ts @@ -3,7 +3,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule'; import { getColorAtCoordinate, getPrefixedId } from 'features/controlLayers/konva/util'; -import type { RgbColor } from 'features/controlLayers/store/types'; +import type { RgbaColor } from 'features/controlLayers/store/types'; import { RGBA_BLACK } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -52,6 +52,39 @@ type CanvasColorPickerToolModuleConfig = { * The color of the crosshair line borders. */ CROSSHAIR_BORDER_COLOR: string; + /** + * The color of the RGBA value text. + */ + TEXT_COLOR: string; + /** + * The padding of the RGBA value text within the background rect. + */ + + TEXT_PADDING: number; + /** + * The font size of the RGBA value text. + */ + TEXT_FONT_SIZE: number; + /** + * The color of the RGBA value text background rect. + */ + TEXT_BG_COLOR: string; + /** + * The width of the RGBA value text background rect. + */ + TEXT_BG_WIDTH: number; + /** + * The height of the RGBA value text background rect. + */ + TEXT_BG_HEIGHT: number; + /** + * The corner radius of the RGBA value text background rect. + */ + TEXT_BG_CORNER_RADIUS: number; + /** + * The x offset of the RGBA value text background rect from the color picker ring. + */ + TEXT_BG_X_OFFSET: number; }; const DEFAULT_CONFIG: CanvasColorPickerToolModuleConfig = { @@ -65,6 +98,14 @@ const DEFAULT_CONFIG: CanvasColorPickerToolModuleConfig = { CROSSHAIR_LINE_LENGTH: 10, CROSSHAIR_LINE_COLOR: 'rgba(0,0,0,1)', CROSSHAIR_BORDER_COLOR: 'rgba(255,255,255,0.8)', + TEXT_COLOR: 'rgba(255,255,255,1)', + TEXT_BG_COLOR: 'rgba(0,0,0,0.8)', + TEXT_BG_HEIGHT: 62, + TEXT_BG_WIDTH: 62, + TEXT_BG_CORNER_RADIUS: 7, + TEXT_PADDING: 8, + TEXT_FONT_SIZE: 12, + TEXT_BG_X_OFFSET: 7, }; /** @@ -83,7 +124,7 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { /** * The color currently under the cursor. Only has a value when the color picker tool is active. */ - $colorUnderCursor = atom(RGBA_BLACK); + $colorUnderCursor = atom(RGBA_BLACK); /** * The Konva objects that make up the color picker tool preview: @@ -105,6 +146,9 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { crosshairSouthOuter: Konva.Line; crosshairWestInner: Konva.Line; crosshairWestOuter: Konva.Line; + rgbaTextGroup: Konva.Group; + rgbaText: Konva.Text; + rgbaTextBackground: Konva.Rect; }; constructor(parent: CanvasToolModule) { @@ -202,8 +246,28 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { stroke: this.config.CROSSHAIR_BORDER_COLOR, perfectDrawEnabled: false, }), + rgbaTextGroup: new Konva.Group({ + listening: false, + name: `${this.type}:color_picker_text_group`, + }), + rgbaText: new Konva.Text({ + listening: false, + name: `${this.type}:color_picker_text`, + fill: this.config.TEXT_COLOR, + fontFamily: 'monospace', + align: 'left', + fontStyle: 'bold', + verticalAlign: 'middle', + }), + rgbaTextBackground: new Konva.Rect({ + listening: false, + name: `${this.type}:color_picker_text_background`, + fill: this.config.TEXT_BG_COLOR, + }), }; + this.konva.rgbaTextGroup.add(this.konva.rgbaTextBackground, this.konva.rgbaText); + this.konva.group.add( this.konva.ringCandidateColor, this.konva.ringCurrentColor, @@ -216,7 +280,8 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { this.konva.crosshairSouthOuter, this.konva.crosshairSouthInner, this.konva.crosshairWestOuter, - this.konva.crosshairWestInner + this.konva.crosshairWestInner, + this.konva.rgbaTextGroup ); } @@ -233,11 +298,6 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { return; } - if (!this.parent.getCanDraw()) { - this.setVisibility(false); - return; - } - const cursorPos = this.parent.$cursorPos.get(); if (!cursorPos) { @@ -283,6 +343,24 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { outerRadius: colorPickerOuterRadius + twoPixels, }); + const textBgWidth = this.manager.stage.unscale(this.config.TEXT_BG_WIDTH); + const textBgHeight = this.manager.stage.unscale(this.config.TEXT_BG_HEIGHT); + + this.konva.rgbaTextBackground.setAttrs({ + width: textBgWidth, + height: textBgHeight, + cornerRadius: this.manager.stage.unscale(this.config.TEXT_BG_CORNER_RADIUS), + }); + this.konva.rgbaText.setAttrs({ + padding: this.manager.stage.unscale(this.config.TEXT_PADDING), + fontSize: this.manager.stage.unscale(this.config.TEXT_FONT_SIZE), + text: `R: ${colorUnderCursor.r}\nG: ${colorUnderCursor.g}\nB: ${colorUnderCursor.b}\nA: ${colorUnderCursor.a}`, + }); + this.konva.rgbaTextGroup.setAttrs({ + x: x + this.manager.stage.unscale(this.config.RING_OUTER_RADIUS + this.config.TEXT_BG_X_OFFSET), + y: y - textBgHeight / 2, + }); + const size = this.manager.stage.unscale(this.config.CROSSHAIR_LINE_LENGTH); const space = this.manager.stage.unscale(this.config.CROSSHAIR_INNER_RADIUS); const innerThickness = this.manager.stage.unscale(this.config.CROSSHAIR_LINE_THICKNESS); @@ -329,11 +407,8 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { onStagePointerUp = (_e: KonvaEventObject) => { const color = this.$colorUnderCursor.get(); - if (color) { - const settings = this.manager.stateApi.getSettings(); - // This will update the color but not the alpha value - this.manager.stateApi.setColor({ ...settings.color, ...color }); - } + const settings = this.manager.stateApi.getSettings(); + this.manager.stateApi.setColor({ ...settings.color, ...color }); }; onStagePointerMove = (_e: KonvaEventObject) => { @@ -346,7 +421,11 @@ export class CanvasColorPickerToolModule extends CanvasModuleBase { return; } + // Hide the background layer so we can get the color under the cursor without the grid interfering + this.manager.background.konva.layer.visible(false); const color = getColorAtCoordinate(this.manager.stage.konva.stage, cursorPos.absolute); + this.manager.background.konva.layer.visible(true); + if (color) { this.$colorUnderCursor.set(color); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts index c7ca36294aa..8ef2f44b3a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts @@ -22,6 +22,7 @@ import type { Coordinate, Tool, } from 'features/controlLayers/store/types'; +import { isRenderableEntityType } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import { atom } from 'nanostores'; @@ -177,24 +178,26 @@ export class CanvasToolModule extends CanvasModuleBase { stage.setCursor('not-allowed'); } else if (tool === 'bbox') { this.tools.bbox.syncCursorStyle(); - } else if (this.manager.stateApi.getRenderedEntityCount() === 0) { - stage.setCursor('not-allowed'); - } else if (selectedEntityAdapter?.$isDisabled.get()) { - stage.setCursor('not-allowed'); - } else if (selectedEntityAdapter?.$isEntityTypeHidden.get()) { - stage.setCursor('not-allowed'); - } else if (selectedEntityAdapter?.$isLocked.get()) { - stage.setCursor('not-allowed'); - } else if (tool === 'brush') { - this.tools.brush.syncCursorStyle(); - } else if (tool === 'eraser') { - this.tools.eraser.syncCursorStyle(); } else if (tool === 'colorPicker') { this.tools.colorPicker.syncCursorStyle(); - } else if (tool === 'move') { - this.tools.move.syncCursorStyle(); - } else if (tool === 'rect') { - this.tools.rect.syncCursorStyle(); + } else if (selectedEntityAdapter && isRenderableEntityType(selectedEntityAdapter.entityIdentifier.type)) { + if (selectedEntityAdapter.$isDisabled.get()) { + stage.setCursor('not-allowed'); + } else if (selectedEntityAdapter.$isEntityTypeHidden.get()) { + stage.setCursor('not-allowed'); + } else if (selectedEntityAdapter.$isLocked.get()) { + stage.setCursor('not-allowed'); + } else if (tool === 'brush') { + this.tools.brush.syncCursorStyle(); + } else if (tool === 'eraser') { + this.tools.eraser.syncCursorStyle(); + } else if (tool === 'move') { + this.tools.move.syncCursorStyle(); + } else if (tool === 'rect') { + this.tools.rect.syncCursorStyle(); + } + } else if (this.manager.stateApi.getRenderedEntityCount() === 0) { + stage.setCursor('not-allowed'); } else { stage.setCursor('not-allowed'); } @@ -387,15 +390,17 @@ export class CanvasToolModule extends CanvasModuleBase { try { this.$lastPointerType.set(e.evt.pointerType); - if (!this.getCanDraw()) { - return; - } - const tool = this.$tool.get(); if (tool === 'colorPicker') { this.tools.colorPicker.onStagePointerUp(e); - } else if (tool === 'brush') { + } + + if (!this.getCanDraw()) { + return; + } + + if (tool === 'brush') { this.tools.brush.onStagePointerUp(e); } else if (tool === 'eraser') { this.tools.eraser.onStagePointerUp(e); @@ -416,15 +421,17 @@ export class CanvasToolModule extends CanvasModuleBase { this.$lastPointerType.set(e.evt.pointerType); this.syncCursorPositions(); - if (!this.getCanDraw()) { - return; - } - const tool = this.$tool.get(); if (tool === 'colorPicker') { this.tools.colorPicker.onStagePointerMove(e); - } else if (tool === 'brush') { + } + + if (!this.getCanDraw()) { + return; + } + + if (tool === 'brush') { await this.tools.brush.onStagePointerMove(e); } else if (tool === 'eraser') { await this.tools.eraser.onStagePointerMove(e); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index b774b7ba8be..037f95f5315 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -7,6 +7,7 @@ import type { Coordinate, CoordinateWithPressure, Rect, + RgbaColor, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -15,7 +16,6 @@ import { clamp } from 'lodash-es'; import { customAlphabet } from 'nanoid'; import type { StrokeOptions } from 'perfect-freehand'; import getStroke from 'perfect-freehand'; -import type { RgbColor } from 'react-colorful'; import { assert } from 'tsafe'; /** @@ -724,7 +724,7 @@ export const getPointerType = (e: KonvaEventObject): 'mouse' | 'pe * @param coord The coordinate to get the color at. This must be the _absolute_ coordinate on the stage. * @returns The color under the coordinate, or null if there was a problem getting the color. */ -export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): RgbColor | null => { +export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): RgbaColor | null => { const ctx = stage .toCanvas({ x: coord.x, y: coord.y, width: 1, height: 1, imageSmoothingEnabled: false }) .getContext('2d'); @@ -733,13 +733,13 @@ export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): Rgb return null; } - const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data; + const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; - if (r === undefined || g === undefined || b === undefined) { + if (r === undefined || g === undefined || b === undefined || a === undefined) { return null; } - return { r, g, b }; + return { r, g, b, a }; }; export const roundRect = (rect: Rect): Rect => {