mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): re-implement with imperative konva api (wip)
This commit is contained in:
parent
ae7797f662
commit
bbbb5479e8
@ -1,5 +1,6 @@
|
|||||||
import { chakra } from '@invoke-ai/ui-library';
|
import { chakra } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
|
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
|
||||||
@ -14,7 +15,6 @@ import {
|
|||||||
import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox';
|
import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { Node, NodeConfig } from 'konva/lib/Node';
|
import type { Node, NodeConfig } from 'konva/lib/Node';
|
||||||
import type { StageConfig } from 'konva/lib/Stage';
|
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
@ -22,18 +22,6 @@ import { useMouseDown, useMouseEnter, useMouseLeave, useMouseMove, useMouseUp }
|
|||||||
|
|
||||||
export const $stage = atom<Konva.Stage | null>(null);
|
export const $stage = atom<Konva.Stage | null>(null);
|
||||||
|
|
||||||
const initStage = (container: StageConfig['container']) => {
|
|
||||||
const stage = new Konva.Stage({
|
|
||||||
container,
|
|
||||||
});
|
|
||||||
$stage.set(stage);
|
|
||||||
|
|
||||||
const layer = new Konva.Layer();
|
|
||||||
const circle = new Konva.Circle({ id: 'cursor', radius: 5, fill: 'red' });
|
|
||||||
layer.add(circle);
|
|
||||||
stage.add(layer);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
};
|
};
|
||||||
@ -41,6 +29,22 @@ type Props = {
|
|||||||
export const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
export const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
||||||
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
|
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
|
||||||
|
|
||||||
|
const BRUSH_PREVIEW_FILL = 'brushPreviewFill';
|
||||||
|
const BRUSH_PREVIEW_OUTLINE_INNER = 'brushPreviewOutlineInner';
|
||||||
|
const BRUSH_PREVIEW_OUTLINE_OUTER = 'brushPreviewOutlineOuter';
|
||||||
|
|
||||||
|
const isKonvaLayer = (node: Node<NodeConfig>): node is Konva.Layer => node.nodeType === 'Layer';
|
||||||
|
const isKonvaLine = (node: Node<NodeConfig>): node is Konva.Line => node.nodeType === 'Line';
|
||||||
|
const isKonvaGroup = (node: Node<NodeConfig>): node is Konva.Group => node.nodeType === 'Group';
|
||||||
|
const isKonvaRect = (node: Node<NodeConfig>): node is Konva.Rect => node.nodeType === 'Rect';
|
||||||
|
|
||||||
|
const $brushPreviewNodes = atom<{
|
||||||
|
layer: Konva.Layer;
|
||||||
|
fill: Konva.Circle;
|
||||||
|
outlineInner: Konva.Circle;
|
||||||
|
outlineOuter: Konva.Circle;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
export const LogicalStage = (props: Props) => {
|
export const LogicalStage = (props: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const width = useAppSelector((s) => s.generation.width);
|
const width = useAppSelector((s) => s.generation.width);
|
||||||
@ -59,7 +63,40 @@ export const LogicalStage = (props: Props) => {
|
|||||||
if (!props.container) {
|
if (!props.container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initStage(props.container);
|
|
||||||
|
const stage = new Konva.Stage({
|
||||||
|
container: props.container,
|
||||||
|
});
|
||||||
|
|
||||||
|
const brushPreviewLayer = new Konva.Layer({ id: 'brushPreviewLayer' });
|
||||||
|
stage.add(brushPreviewLayer);
|
||||||
|
const fill = new Konva.Circle({
|
||||||
|
id: BRUSH_PREVIEW_FILL,
|
||||||
|
listening: false,
|
||||||
|
strokeEnabled: false,
|
||||||
|
strokeHitEnabled: false,
|
||||||
|
});
|
||||||
|
const outlineInner = new Konva.Circle({
|
||||||
|
id: BRUSH_PREVIEW_OUTLINE_INNER,
|
||||||
|
listening: false,
|
||||||
|
stroke: 'rgba(0,0,0,1)',
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeEnabled: true,
|
||||||
|
});
|
||||||
|
const outlineOuter = new Konva.Circle({
|
||||||
|
id: BRUSH_PREVIEW_OUTLINE_OUTER,
|
||||||
|
listening: false,
|
||||||
|
stroke: 'rgba(255,255,255,0.8)',
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeEnabled: true,
|
||||||
|
});
|
||||||
|
brushPreviewLayer.add(fill);
|
||||||
|
brushPreviewLayer.add(outlineInner);
|
||||||
|
brushPreviewLayer.add(outlineOuter);
|
||||||
|
$brushPreviewNodes.set({ layer: brushPreviewLayer, fill, outlineInner, outlineOuter });
|
||||||
|
|
||||||
|
$stage.set(stage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const stage = $stage.get();
|
const stage = $stage.get();
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
@ -99,17 +136,33 @@ export const LogicalStage = (props: Props) => {
|
|||||||
}, [stage, width, height, props.container]);
|
}, [stage, width, height, props.container]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
console.log('cursor effect');
|
console.log('brush preview effect');
|
||||||
if (!stage || !cursorPosition) {
|
const brushPreviewNodes = $brushPreviewNodes.get();
|
||||||
|
brushPreviewNodes?.layer.visible(state.tool !== 'move');
|
||||||
|
if (!stage || !cursorPosition || !brushPreviewNodes) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cursor = stage.findOne('#cursor');
|
const color = getStore()
|
||||||
if (!cursor) {
|
.getState()
|
||||||
|
.regionalPrompts.layers.find((l) => l.id === state.selectedLayer)?.color;
|
||||||
|
if (!color) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cursor.x(cursorPosition?.x);
|
const fill = rgbColorToString(color);
|
||||||
cursor.y(cursorPosition?.y);
|
brushPreviewNodes.fill.setAttrs({
|
||||||
}, [cursorPosition, stage]);
|
x: cursorPosition.x,
|
||||||
|
y: cursorPosition.y,
|
||||||
|
radius: state.brushSize / 2,
|
||||||
|
fill,
|
||||||
|
globalCompositeOperation: state.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
|
});
|
||||||
|
brushPreviewNodes.outlineInner.setAttrs({ x: cursorPosition.x, y: cursorPosition.y, radius: state.brushSize / 2 });
|
||||||
|
brushPreviewNodes.outlineOuter.setAttrs({
|
||||||
|
x: cursorPosition.x,
|
||||||
|
y: cursorPosition.y,
|
||||||
|
radius: state.brushSize / 2 + 1,
|
||||||
|
});
|
||||||
|
}, [cursorPosition, stage, state.brushSize, state.selectedLayer, state.tool]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
console.log('obj effect');
|
console.log('obj effect');
|
||||||
@ -117,15 +170,36 @@ export const LogicalStage = (props: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle layer getting deleted and reset
|
const reduxLayerIds = state.layers.map((l) => l.id);
|
||||||
for (const l of state.layers) {
|
|
||||||
let layer = stage.findOne(`#${l.id}`) as Konva.Layer | null;
|
// Remove deleted layers - we know these are of type Layer
|
||||||
if (!layer) {
|
for (const konvaLayer of stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`) as Konva.Layer[]) {
|
||||||
layer = new Konva.Layer({ id: l.id, name: REGIONAL_PROMPT_LAYER_NAME, draggable: true });
|
if (!reduxLayerIds.includes(konvaLayer.id())) {
|
||||||
layer.on('dragmove', (e) => {
|
konvaLayer.destroy();
|
||||||
dispatch(layerTranslated({ layerId: l.id, x: e.target.x(), y: e.target.y() }));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const reduxLayer of state.layers) {
|
||||||
|
let konvaLayer = stage.findOne(`#${reduxLayer.id}`) as Konva.Layer | undefined;
|
||||||
|
|
||||||
|
// New layer - create a new Konva layer
|
||||||
|
if (!konvaLayer) {
|
||||||
|
konvaLayer = new Konva.Layer({
|
||||||
|
id: reduxLayer.id,
|
||||||
|
name: REGIONAL_PROMPT_LAYER_NAME,
|
||||||
|
draggable: true,
|
||||||
|
listening: reduxLayer.id === state.selectedLayer,
|
||||||
});
|
});
|
||||||
layer.dragBoundFunc(function (pos) {
|
konvaLayer.on('dragmove', function (e) {
|
||||||
|
dispatch(
|
||||||
|
layerTranslated({
|
||||||
|
layerId: reduxLayer.id,
|
||||||
|
x: e.target.x(),
|
||||||
|
y: e.target.y(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
konvaLayer.dragBoundFunc(function (pos) {
|
||||||
const cursorPos = getScaledCursorPosition(stage);
|
const cursorPos = getScaledCursorPosition(stage);
|
||||||
if (!cursorPos) {
|
if (!cursorPos) {
|
||||||
return this.getAbsolutePosition();
|
return this.getAbsolutePosition();
|
||||||
@ -137,44 +211,55 @@ export const LogicalStage = (props: Props) => {
|
|||||||
|
|
||||||
return pos;
|
return pos;
|
||||||
});
|
});
|
||||||
stage.add(layer);
|
stage.add(konvaLayer);
|
||||||
}
|
$brushPreviewNodes.get()?.layer.moveToTop();
|
||||||
|
|
||||||
if (state.tool === 'move') {
|
|
||||||
layer.listening(true);
|
|
||||||
} else {
|
} else {
|
||||||
layer.listening(l.id === state.selectedLayer);
|
konvaLayer.listening(reduxLayer.id === state.selectedLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const o of l.objects) {
|
const color = rgbColorToString(reduxLayer.color);
|
||||||
if (o.kind !== 'line') {
|
|
||||||
|
// Remove deleted objects
|
||||||
|
const objectIds = reduxLayer.objects.map((o) => o.id);
|
||||||
|
for (const objectNode of stage.find(`.${reduxLayer.id}-object`)) {
|
||||||
|
if (!objectIds.includes(objectNode.id())) {
|
||||||
|
objectNode.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const reduxObject of reduxLayer.objects) {
|
||||||
|
if (reduxObject.kind !== 'line') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let obj = stage.findOne(`#${o.id}`) as Konva.Line | null;
|
let konvaObject = stage.findOne(`#${reduxObject.id}`) as Konva.Line | undefined;
|
||||||
if (!obj) {
|
|
||||||
obj = new Konva.Line({
|
if (!konvaObject) {
|
||||||
id: o.id,
|
konvaObject = new Konva.Line({
|
||||||
key: o.id,
|
id: reduxObject.id,
|
||||||
strokeWidth: o.strokeWidth,
|
key: reduxObject.id,
|
||||||
stroke: rgbColorToString(l.color),
|
name: `${reduxLayer.id}-object`,
|
||||||
|
points: reduxObject.points,
|
||||||
|
strokeWidth: reduxObject.strokeWidth,
|
||||||
|
stroke: color,
|
||||||
tension: 0,
|
tension: 0,
|
||||||
lineCap: 'round',
|
lineCap: 'round',
|
||||||
lineJoin: 'round',
|
lineJoin: 'round',
|
||||||
shadowForStrokeEnabled: false,
|
shadowForStrokeEnabled: false,
|
||||||
globalCompositeOperation: o.tool === 'brush' ? 'source-over' : 'destination-out',
|
globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
listening: false,
|
listening: false,
|
||||||
visible: l.isVisible,
|
visible: reduxLayer.isVisible,
|
||||||
});
|
});
|
||||||
layer.add(obj);
|
konvaLayer.add(konvaObject);
|
||||||
|
} else {
|
||||||
|
if (konvaObject.points().length !== reduxObject.points.length) {
|
||||||
|
konvaObject.points(reduxObject.points);
|
||||||
}
|
}
|
||||||
if (obj.points().length < o.points.length) {
|
if (konvaObject.stroke() !== color) {
|
||||||
obj.points(o.points);
|
konvaObject.stroke(color);
|
||||||
}
|
}
|
||||||
if (obj.stroke() !== rgbColorToString(l.color)) {
|
if (konvaObject.visible() !== reduxLayer.isVisible) {
|
||||||
obj.stroke(rgbColorToString(l.color));
|
konvaObject.visible(reduxLayer.isVisible);
|
||||||
}
|
}
|
||||||
if (obj.visible() !== l.isVisible) {
|
|
||||||
obj.visible(l.isVisible);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,25 +271,26 @@ export const LogicalStage = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.tool !== 'move') {
|
if (state.tool !== 'move') {
|
||||||
|
// Tool was just changed to something other than move - hide all layer bounding boxes
|
||||||
for (const n of stage.find('.layer-bbox')) {
|
for (const n of stage.find('.layer-bbox')) {
|
||||||
n.visible(false);
|
n.visible(false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const layer of stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`) as Konva.Layer[]) {
|
for (const konvaLayer of stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`) as Konva.Layer[]) {
|
||||||
const bbox = getKonvaLayerBbox(layer);
|
const bbox = getKonvaLayerBbox(konvaLayer);
|
||||||
dispatch(layerBboxChanged({ layerId: layer.id(), bbox }));
|
dispatch(layerBboxChanged({ layerId: konvaLayer.id(), bbox }));
|
||||||
let rect = layer.findOne('.layer-bbox') as Konva.Rect | null;
|
let rect = konvaLayer.findOne('.layer-bbox') as Konva.Rect | undefined;
|
||||||
if (!rect) {
|
if (!rect) {
|
||||||
rect = new Konva.Rect({
|
rect = new Konva.Rect({
|
||||||
id: `${layer.id()}-bbox`,
|
id: `${konvaLayer.id()}-bbox`,
|
||||||
name: 'layer-bbox',
|
name: 'layer-bbox',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
});
|
});
|
||||||
layer.add(rect);
|
konvaLayer.add(rect);
|
||||||
layer.on('mousedown', () => {
|
konvaLayer.on('mousedown', () => {
|
||||||
dispatch(layerSelected(layer.id()));
|
dispatch(layerSelected(konvaLayer.id()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
rect.visible(true);
|
rect.visible(true);
|
||||||
@ -212,7 +298,7 @@ export const LogicalStage = (props: Props) => {
|
|||||||
rect.y(bbox.y);
|
rect.y(bbox.y);
|
||||||
rect.width(bbox.width);
|
rect.width(bbox.width);
|
||||||
rect.height(bbox.height);
|
rect.height(bbox.height);
|
||||||
rect.stroke(state.selectedLayer === layer.id() ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)');
|
rect.stroke(state.selectedLayer === konvaLayer.id() ? 'rgba(153, 187, 189, 1)' : 'rgba(255, 255, 255, 0.149)');
|
||||||
}
|
}
|
||||||
}, [dispatch, stage, state.tool, state.selectedLayer]);
|
}, [dispatch, stage, state.tool, state.selectedLayer]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user