mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): control adapter image rendering
This commit is contained in:
parent
2f21a2220d
commit
4a556f84e0
@ -1,20 +1,24 @@
|
|||||||
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
|
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
|
||||||
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
import type { ControlAdapterEntity } from 'features/controlLayers/store/types';
|
import { type ControlAdapterEntity, isDrawingTool } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export class CanvasControlAdapter {
|
export class CanvasControlAdapter {
|
||||||
id: string;
|
id: string;
|
||||||
|
manager: CanvasManager;
|
||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
|
objectsGroup: Konva.Group;
|
||||||
image: CanvasImage | null;
|
image: CanvasImage | null;
|
||||||
|
transformer: Konva.Transformer;
|
||||||
|
private controlAdapterState: ControlAdapterEntity;
|
||||||
|
|
||||||
constructor(entity: ControlAdapterEntity) {
|
constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) {
|
||||||
const { id } = entity;
|
const { id } = controlAdapterState;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.manager = manager;
|
||||||
this.layer = new Konva.Layer({
|
this.layer = new Konva.Layer({
|
||||||
id,
|
id,
|
||||||
imageSmoothingEnabled: false,
|
imageSmoothingEnabled: false,
|
||||||
@ -24,47 +28,123 @@ export class CanvasControlAdapter {
|
|||||||
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
||||||
listening: false,
|
listening: false,
|
||||||
});
|
});
|
||||||
|
this.objectsGroup = new Konva.Group({ listening: false });
|
||||||
|
this.group.add(this.objectsGroup);
|
||||||
this.layer.add(this.group);
|
this.layer.add(this.group);
|
||||||
|
|
||||||
|
this.transformer = new Konva.Transformer({
|
||||||
|
shouldOverdrawWholeArea: true,
|
||||||
|
draggable: true,
|
||||||
|
dragDistance: 0,
|
||||||
|
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||||
|
rotateEnabled: false,
|
||||||
|
flipEnabled: false,
|
||||||
|
});
|
||||||
|
this.transformer.on('transformend', () => {
|
||||||
|
this.manager.stateApi.onScaleChanged(
|
||||||
|
{ id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() },
|
||||||
|
'layer'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.transformer.on('dragend', () => {
|
||||||
|
this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'layer');
|
||||||
|
});
|
||||||
|
this.layer.add(this.transformer);
|
||||||
|
|
||||||
this.image = null;
|
this.image = null;
|
||||||
|
this.controlAdapterState = controlAdapterState;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(entity: ControlAdapterEntity) {
|
async render(controlAdapterState: ControlAdapterEntity) {
|
||||||
const imageObject = entity.processedImageObject ?? entity.imageObject;
|
this.controlAdapterState = controlAdapterState;
|
||||||
|
const imageObject = controlAdapterState.processedImageObject ?? controlAdapterState.imageObject;
|
||||||
|
|
||||||
|
let didDraw = false;
|
||||||
|
|
||||||
if (!imageObject) {
|
if (!imageObject) {
|
||||||
if (this.image) {
|
if (this.image) {
|
||||||
this.image.konvaImageGroup.visible(false);
|
this.image.konvaImageGroup.visible(false);
|
||||||
|
didDraw = true;
|
||||||
}
|
}
|
||||||
return;
|
} else if (!this.image) {
|
||||||
}
|
|
||||||
|
|
||||||
const opacity = entity.opacity;
|
|
||||||
const visible = entity.isEnabled;
|
|
||||||
const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : [];
|
|
||||||
|
|
||||||
if (!this.image) {
|
|
||||||
this.image = await new CanvasImage(imageObject, {
|
this.image = await new CanvasImage(imageObject, {
|
||||||
onLoad: (konvaImage) => {
|
onLoad: () => {
|
||||||
konvaImage.filters(filters);
|
this.updateGroup(true);
|
||||||
konvaImage.cache();
|
|
||||||
konvaImage.opacity(opacity);
|
|
||||||
konvaImage.visible(visible);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.group.add(this.image.konvaImageGroup);
|
this.objectsGroup.add(this.image.konvaImageGroup);
|
||||||
|
await this.image.updateImageSource(imageObject.image.name);
|
||||||
|
} else if (!this.image.isLoading && !this.image.isError) {
|
||||||
|
if (await this.image.update(imageObject)) {
|
||||||
|
didDraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateGroup(didDraw);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGroup(didDraw: boolean) {
|
||||||
|
this.layer.visible(this.controlAdapterState.isEnabled);
|
||||||
|
|
||||||
|
this.group.opacity(this.controlAdapterState.opacity);
|
||||||
|
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||||
|
const selectedTool = this.manager.stateApi.getToolState().selected;
|
||||||
|
|
||||||
|
if (!this.image?.konvaImage) {
|
||||||
|
// If the layer is totally empty, reset the cache and bail out.
|
||||||
|
this.layer.listening(false);
|
||||||
|
this.transformer.nodes([]);
|
||||||
|
if (this.group.isCached()) {
|
||||||
|
this.group.clearCache();
|
||||||
}
|
}
|
||||||
if (this.image.isLoading || this.image.isError) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.image.imageName !== imageObject.image.name) {
|
|
||||||
this.image.updateImageSource(imageObject.image.name);
|
if (isSelected && selectedTool === 'move') {
|
||||||
|
// When the layer is selected and being moved, we should always cache it.
|
||||||
|
// We should update the cache if we drew to the layer.
|
||||||
|
if (!this.group.isCached() || didDraw) {
|
||||||
|
this.group.cache();
|
||||||
}
|
}
|
||||||
if (this.image.konvaImage) {
|
// Activate the transformer
|
||||||
if (!isEqual(this.image.konvaImage.filters(), filters)) {
|
this.layer.listening(true);
|
||||||
this.image.konvaImage.filters(filters);
|
this.transformer.nodes([this.group]);
|
||||||
this.image.konvaImage.cache();
|
this.transformer.forceUpdate();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.image.konvaImage.opacity(opacity);
|
|
||||||
this.image.konvaImage.visible(visible);
|
if (isSelected && selectedTool !== 'move') {
|
||||||
|
// If the layer is selected but not using the move tool, we don't want the layer to be listening.
|
||||||
|
this.layer.listening(false);
|
||||||
|
// The transformer also does not need to be active.
|
||||||
|
this.transformer.nodes([]);
|
||||||
|
if (isDrawingTool(selectedTool)) {
|
||||||
|
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
|
||||||
|
// should never be cached.
|
||||||
|
if (this.group.isCached()) {
|
||||||
|
this.group.clearCache();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
|
||||||
|
// We should update the cache if we drew to the layer.
|
||||||
|
if (!this.group.isCached() || didDraw) {
|
||||||
|
this.group.cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSelected) {
|
||||||
|
// Unselected layers should not be listening
|
||||||
|
this.layer.listening(false);
|
||||||
|
// The transformer also does not need to be active.
|
||||||
|
this.transformer.nodes([]);
|
||||||
|
// Update the layer's cache if it's not already cached or we drew to it.
|
||||||
|
if (!this.group.isCached() || didDraw) {
|
||||||
|
this.group.cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { FILTER_MAP } from 'features/controlLayers/konva/filters';
|
||||||
import type { ImageObject } from 'features/controlLayers/store/types';
|
import type { ImageObject } from 'features/controlLayers/store/types';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
@ -30,7 +31,7 @@ export class CanvasImage {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { getImageDTO, onLoading, onLoad, onError } = options;
|
const { getImageDTO, onLoading, onLoad, onError } = options;
|
||||||
const { id, width, height, x, y } = imageObject;
|
const { id, width, height, x, y, filters } = imageObject;
|
||||||
this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y });
|
this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y });
|
||||||
this.konvaPlaceholderGroup = new Konva.Group({ listening: false });
|
this.konvaPlaceholderGroup = new Konva.Group({ listening: false });
|
||||||
this.konvaPlaceholderRect = new Konva.Rect({
|
this.konvaPlaceholderRect = new Konva.Rect({
|
||||||
@ -85,6 +86,7 @@ export class CanvasImage {
|
|||||||
image: imageEl,
|
image: imageEl,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
filters: filters.map((f) => FILTER_MAP[f]),
|
||||||
});
|
});
|
||||||
this.konvaImageGroup.add(this.konvaImage);
|
this.konvaImageGroup.add(this.konvaImage);
|
||||||
}
|
}
|
||||||
@ -138,11 +140,11 @@ export class CanvasImage {
|
|||||||
|
|
||||||
async update(imageObject: ImageObject, force?: boolean): Promise<boolean> {
|
async update(imageObject: ImageObject, force?: boolean): Promise<boolean> {
|
||||||
if (this.lastImageObject !== imageObject || force) {
|
if (this.lastImageObject !== imageObject || force) {
|
||||||
const { width, height, x, y, image } = imageObject;
|
const { width, height, x, y, image, filters } = imageObject;
|
||||||
if (this.lastImageObject.image.name !== image.name || force) {
|
if (this.lastImageObject.image.name !== image.name || force) {
|
||||||
await this.updateImageSource(image.name);
|
await this.updateImageSource(image.name);
|
||||||
}
|
}
|
||||||
this.konvaImage?.setAttrs({ x, y, width, height });
|
this.konvaImage?.setAttrs({ x, y, width, height, filters: filters.map((f) => FILTER_MAP[f]) });
|
||||||
this.konvaPlaceholderRect.setAttrs({ width, height });
|
this.konvaPlaceholderRect.setAttrs({ width, height });
|
||||||
this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 });
|
this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 });
|
||||||
this.lastImageObject = imageObject;
|
this.lastImageObject = imageObject;
|
||||||
|
@ -164,7 +164,7 @@ export class CanvasManager {
|
|||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
let adapter = this.controlAdapters.get(entity.id);
|
let adapter = this.controlAdapters.get(entity.id);
|
||||||
if (!adapter) {
|
if (!adapter) {
|
||||||
adapter = new CanvasControlAdapter(entity);
|
adapter = new CanvasControlAdapter(entity, this);
|
||||||
this.controlAdapters.set(adapter.id, adapter);
|
this.controlAdapters.set(adapter.id, adapter);
|
||||||
this.stage.add(adapter.layer);
|
this.stage.add(adapter.layer);
|
||||||
}
|
}
|
||||||
|
@ -19,3 +19,7 @@ export const LightnessToAlphaFilter = (imageData: ImageData): void => {
|
|||||||
imageData.data[i * 4 + 3] = (cMin + cMax) / 2;
|
imageData.data[i * 4 + 3] = (cMin + cMax) / 2;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FILTER_MAP = {
|
||||||
|
LightnessToAlphaFilter: LightnessToAlphaFilter,
|
||||||
|
} as const;
|
||||||
|
@ -139,7 +139,7 @@ export const controlAdaptersReducers = {
|
|||||||
ca.bboxNeedsUpdate = true;
|
ca.bboxNeedsUpdate = true;
|
||||||
ca.isEnabled = true;
|
ca.isEnabled = true;
|
||||||
if (imageDTO) {
|
if (imageDTO) {
|
||||||
const newImageObject = imageDTOToImageObject(id, objectId, imageDTO);
|
const newImageObject = imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filter ? [ca.filter] : [] });
|
||||||
if (isEqual(newImageObject, ca.imageObject)) {
|
if (isEqual(newImageObject, ca.imageObject)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -162,7 +162,9 @@ export const controlAdaptersReducers = {
|
|||||||
ca.bbox = null;
|
ca.bbox = null;
|
||||||
ca.bboxNeedsUpdate = true;
|
ca.bboxNeedsUpdate = true;
|
||||||
ca.isEnabled = true;
|
ca.isEnabled = true;
|
||||||
ca.processedImageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null;
|
ca.processedImageObject = imageDTO
|
||||||
|
? imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filter ? [ca.filter] : [] })
|
||||||
|
: null;
|
||||||
},
|
},
|
||||||
prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
|
prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
|
||||||
},
|
},
|
||||||
|
@ -761,7 +761,12 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO)
|
|||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const imageDTOToImageObject = (entityId: string, objectId: string, imageDTO: ImageDTO): ImageObject => {
|
export const imageDTOToImageObject = (
|
||||||
|
entityId: string,
|
||||||
|
objectId: string,
|
||||||
|
imageDTO: ImageDTO,
|
||||||
|
overrides?: Partial<ImageObject>
|
||||||
|
): ImageObject => {
|
||||||
const { width, height, image_name } = imageDTO;
|
const { width, height, image_name } = imageDTO;
|
||||||
return {
|
return {
|
||||||
id: getImageObjectId(entityId, objectId),
|
id: getImageObjectId(entityId, objectId),
|
||||||
@ -776,6 +781,7 @@ export const imageDTOToImageObject = (entityId: string, objectId: string, imageD
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
},
|
},
|
||||||
|
...overrides,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user