tidy(ui): update canvas classes, organise location of konva nodes

This commit is contained in:
psychedelicious 2024-07-17 14:18:38 +10:00
parent 2ef8a8cf5a
commit 9483c8cc29
18 changed files with 675 additions and 598 deletions

View File

@ -33,16 +33,19 @@ export class CanvasBackground {
static BASE_NAME = 'background';
static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`;
layer: Konva.Layer;
konva: {
layer: Konva.Layer;
};
manager: CanvasManager;
constructor(manager: CanvasManager) {
this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false });
this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) };
}
render() {
this.layer.zIndex(0);
this.konva.layer.zIndex(0);
const scale = this.manager.stage.scaleX();
const gridSpacing = getGridSpacing(scale);
const x = this.manager.stage.x();
@ -86,11 +89,11 @@ export class CanvasBackground {
let _x = 0;
let _y = 0;
this.layer.destroyChildren();
this.konva.layer.destroyChildren();
for (let i = 0; i < xSteps; i++) {
_x = gridFullRect.x1 + i * gridSpacing;
this.layer.add(
this.konva.layer.add(
new Konva.Line({
x: _x,
y: gridFullRect.y1,
@ -103,7 +106,7 @@ export class CanvasBackground {
}
for (let i = 0; i < ySteps; i++) {
_y = gridFullRect.y1 + i * gridSpacing;
this.layer.add(
this.konva.layer.add(
new Konva.Line({
x: gridFullRect.x1,
y: _y,

View File

@ -11,11 +11,10 @@ export class CanvasBbox {
static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`;
static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`;
group: Konva.Group;
rect: Konva.Rect;
transformer: Konva.Transformer;
manager: CanvasManager;
konva: { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer };
ALL_ANCHORS: string[] = [
'top-left',
'top-center',
@ -36,88 +35,89 @@ export class CanvasBbox {
const bbox = this.manager.stateApi.getBbox();
const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height);
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
// transparent rect for this purpose.
this.group = new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false });
this.rect = new Konva.Rect({
name: CanvasBbox.RECT_NAME,
listening: false,
strokeEnabled: false,
draggable: true,
...this.manager.stateApi.getBbox(),
});
this.rect.on('dragmove', () => {
this.konva = {
group: new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }),
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
// transparent rect for this purpose.
rect: new Konva.Rect({
name: CanvasBbox.RECT_NAME,
listening: false,
strokeEnabled: false,
draggable: true,
...this.manager.stateApi.getBbox(),
}),
transformer: new Konva.Transformer({
name: CanvasBbox.TRANSFORMER_NAME,
borderDash: [5, 5],
borderStroke: 'rgba(212,216,234,1)',
borderEnabled: true,
rotateEnabled: false,
keepRatio: false,
ignoreStroke: true,
listening: false,
flipEnabled: false,
anchorFill: 'rgba(212,216,234,1)',
anchorStroke: 'rgb(42,42,42)',
anchorSize: 12,
anchorCornerRadius: 3,
shiftBehavior: 'none', // we will implement our own shift behavior
centeredScaling: false,
anchorStyleFunc: (anchor) => {
// Make the x/y resize anchors little bars
if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) {
anchor.height(8);
anchor.offsetY(4);
anchor.width(30);
anchor.offsetX(15);
}
if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) {
anchor.height(30);
anchor.offsetY(15);
anchor.width(8);
anchor.offsetX(4);
}
},
anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => {
// This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed
// to konva's internal coordinate system.
const stage = this.konva.transformer.getStage();
assert(stage, 'Stage must exist');
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
const scaledGridSize = gridSize * stage.scaleX();
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
const stageAbsPos = stage.getAbsolutePosition();
// The offset is the remainder of the stage's absolute position divided by the scaled grid size.
const offsetX = stageAbsPos.x % scaledGridSize;
const offsetY = stageAbsPos.y % scaledGridSize;
// Finally, calculate the position by rounding to the grid and adding the offset.
return {
x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX,
y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY,
};
},
}),
};
this.konva.rect.on('dragmove', () => {
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
const bbox = this.manager.stateApi.getBbox();
const bboxRect: Rect = {
...bbox.rect,
x: roundToMultiple(this.rect.x(), gridSize),
y: roundToMultiple(this.rect.y(), gridSize),
x: roundToMultiple(this.konva.rect.x(), gridSize),
y: roundToMultiple(this.konva.rect.y(), gridSize),
};
this.rect.setAttrs(bboxRect);
this.konva.rect.setAttrs(bboxRect);
if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) {
this.manager.stateApi.onBboxTransformed(bboxRect);
}
});
this.transformer = new Konva.Transformer({
name: CanvasBbox.TRANSFORMER_NAME,
borderDash: [5, 5],
borderStroke: 'rgba(212,216,234,1)',
borderEnabled: true,
rotateEnabled: false,
keepRatio: false,
ignoreStroke: true,
listening: false,
flipEnabled: false,
anchorFill: 'rgba(212,216,234,1)',
anchorStroke: 'rgb(42,42,42)',
anchorSize: 12,
anchorCornerRadius: 3,
shiftBehavior: 'none', // we will implement our own shift behavior
centeredScaling: false,
anchorStyleFunc: (anchor) => {
// Make the x/y resize anchors little bars
if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) {
anchor.height(8);
anchor.offsetY(4);
anchor.width(30);
anchor.offsetX(15);
}
if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) {
anchor.height(30);
anchor.offsetY(15);
anchor.width(8);
anchor.offsetX(4);
}
},
anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => {
// This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed
// to konva's internal coordinate system.
const stage = this.transformer.getStage();
assert(stage, 'Stage must exist');
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
const scaledGridSize = gridSize * stage.scaleX();
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
const stageAbsPos = stage.getAbsolutePosition();
// The offset is the remainder of the stage's absolute position divided by the scaled grid size.
const offsetX = stageAbsPos.x % scaledGridSize;
const offsetY = stageAbsPos.y % scaledGridSize;
// Finally, calculate the position by rounding to the grid and adding the offset.
return {
x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX,
y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY,
};
},
});
this.transformer.on('transform', () => {
this.konva.transformer.on('transform', () => {
// In the transform callback, we calculate the bbox's new dims and pos and update the konva object.
// Some special handling is needed depending on the anchor being dragged.
const anchor = this.transformer.getActiveAnchor();
const anchor = this.konva.transformer.getActiveAnchor();
if (!anchor) {
// Pretty sure we should always have an anchor here?
return;
@ -140,14 +140,14 @@ export class CanvasBbox {
}
// The coords should be correct per the anchorDragBoundFunc.
let x = this.rect.x();
let y = this.rect.y();
let x = this.konva.rect.x();
let y = this.konva.rect.y();
// Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height
// *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap
// them to the grid.
let width = roundToMultipleMin(this.rect.width() * this.rect.scaleX(), gridSize);
let height = roundToMultipleMin(this.rect.height() * this.rect.scaleY(), gridSize);
let width = roundToMultipleMin(this.konva.rect.width() * this.konva.rect.scaleX(), gridSize);
let height = roundToMultipleMin(this.konva.rect.height() * this.konva.rect.scaleY(), gridSize);
// If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this
// if alt/opt is held - this requires math too big for my brain.
@ -187,7 +187,7 @@ export class CanvasBbox {
// Update the bboxRect's attrs directly with the new transform, and reset its scale to 1.
// TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly.
// Gotta be a way to avoid setting it twice...
this.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 });
this.konva.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 });
// Update the bbox in internal state.
this.manager.stateApi.onBboxTransformed(bboxRect);
@ -199,16 +199,16 @@ export class CanvasBbox {
}
});
this.transformer.on('transformend', () => {
this.konva.transformer.on('transformend', () => {
// Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held,
// we have the correct aspect ratio to start from.
$aspectRatioBuffer.set(this.rect.width() / this.rect.height());
$aspectRatioBuffer.set(this.konva.rect.width() / this.konva.rect.height());
});
// The transformer will always be transforming the dummy rect
this.transformer.nodes([this.rect]);
this.group.add(this.rect);
this.group.add(this.transformer);
this.konva.transformer.nodes([this.konva.rect]);
this.konva.group.add(this.konva.rect);
this.konva.group.add(this.konva.transformer);
}
render() {
@ -217,14 +217,14 @@ export class CanvasBbox {
const toolState = this.manager.stateApi.getToolState();
if (!session.isActive) {
this.group.listening(false);
this.group.visible(false);
this.konva.group.listening(false);
this.konva.group.visible(false);
return;
}
this.group.visible(true);
this.group.listening(toolState.selected === 'bbox');
this.rect.setAttrs({
this.konva.group.visible(true);
this.konva.group.listening(toolState.selected === 'bbox');
this.konva.rect.setAttrs({
x: bbox.rect.x,
y: bbox.rect.y,
width: bbox.rect.width,
@ -233,7 +233,7 @@ export class CanvasBbox {
scaleY: 1,
listening: toolState.selected === 'bbox',
});
this.transformer.setAttrs({
this.konva.transformer.setAttrs({
listening: toolState.selected === 'bbox',
enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS,
});

View File

@ -8,40 +8,44 @@ export class CanvasBrushLine {
static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`;
id: string;
konvaLineGroup: Konva.Group;
konvaLine: Konva.Line;
konva: {
group: Konva.Group;
line: Konva.Line;
};
lastBrushLine: BrushLine;
constructor(brushLine: BrushLine) {
const { id, strokeWidth, clip, color, points } = brushLine;
this.id = id;
this.konvaLineGroup = new Konva.Group({
name: CanvasBrushLine.GROUP_NAME,
clip,
listening: false,
});
this.konvaLine = new Konva.Line({
name: CanvasBrushLine.LINE_NAME,
id,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
tension: 0,
lineCap: 'round',
lineJoin: 'round',
globalCompositeOperation: 'source-over',
stroke: rgbaColorToString(color),
// A line with only one point will not be rendered, so we duplicate the points to make it visible
points: points.length === 2 ? [...points, ...points] : points,
});
this.konvaLineGroup.add(this.konvaLine);
this.konva = {
group: new Konva.Group({
name: CanvasBrushLine.GROUP_NAME,
clip,
listening: false,
}),
line: new Konva.Line({
name: CanvasBrushLine.LINE_NAME,
id,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
tension: 0,
lineCap: 'round',
lineJoin: 'round',
globalCompositeOperation: 'source-over',
stroke: rgbaColorToString(color),
// A line with only one point will not be rendered, so we duplicate the points to make it visible
points: points.length === 2 ? [...points, ...points] : points,
}),
};
this.konva.group.add(this.konva.line);
this.lastBrushLine = brushLine;
}
update(brushLine: BrushLine, force?: boolean): boolean {
if (this.lastBrushLine !== brushLine || force) {
const { points, color, clip, strokeWidth } = brushLine;
this.konvaLine.setAttrs({
this.konva.line.setAttrs({
// A line with only one point will not be rendered, so we duplicate the points to make it visible
points: points.length === 2 ? [...points, ...points] : points,
stroke: rgbaColorToString(color),
@ -56,6 +60,6 @@ export class CanvasBrushLine {
}
destroy() {
this.konvaLineGroup.destroy();
this.konva.group.destroy();
}
}

View File

@ -14,51 +14,60 @@ export class CanvasControlAdapter {
id: string;
manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group;
objectsGroup: Konva.Group;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
transformer: Konva.Transformer;
};
image: CanvasImage | null;
transformer: Konva.Transformer;
constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) {
const { id } = controlAdapterState;
this.id = id;
this.manager = manager;
this.layer = new Konva.Layer({
name: CanvasControlAdapter.LAYER_NAME,
imageSmoothingEnabled: false,
listening: false,
});
this.group = new Konva.Group({
name: CanvasControlAdapter.GROUP_NAME,
listening: false,
});
this.objectsGroup = new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false });
this.group.add(this.objectsGroup);
this.layer.add(this.group);
this.transformer = new Konva.Transformer({
name: CanvasControlAdapter.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
});
this.transformer.on('transformend', () => {
this.konva = {
layer: new Konva.Layer({
name: CanvasControlAdapter.LAYER_NAME,
imageSmoothingEnabled: false,
listening: false,
}),
group: new Konva.Group({
name: CanvasControlAdapter.GROUP_NAME,
listening: false,
}),
objectGroup: new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }),
transformer: new Konva.Transformer({
name: CanvasControlAdapter.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
}),
};
this.konva.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } },
{
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'control_adapter'
);
});
this.transformer.on('dragend', () => {
this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.group.x(), y: this.group.y() } },
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'control_adapter'
);
});
this.layer.add(this.transformer);
this.konva.group.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.group);
this.konva.layer.add(this.konva.transformer);
this.image = null;
this.controlAdapterState = controlAdapterState;
@ -68,7 +77,7 @@ export class CanvasControlAdapter {
this.controlAdapterState = controlAdapterState;
// Update the layer's position and listening state
this.group.setAttrs({
this.konva.group.setAttrs({
x: controlAdapterState.position.x,
y: controlAdapterState.position.y,
scaleX: 1,
@ -81,13 +90,13 @@ export class CanvasControlAdapter {
if (!imageObject) {
if (this.image) {
this.image.konvaImageGroup.visible(false);
this.image.konva.group.visible(false);
didDraw = true;
}
} else if (!this.image) {
this.image = new CanvasImage(imageObject);
this.updateGroup(true);
this.objectsGroup.add(this.image.konvaImageGroup);
this.konva.objectGroup.add(this.image.konva.group);
await this.image.updateImageSource(imageObject.image.name);
} else if (!this.image.isLoading && !this.image.isError) {
if (await this.image.update(imageObject)) {
@ -99,18 +108,18 @@ export class CanvasControlAdapter {
}
updateGroup(didDraw: boolean) {
this.layer.visible(this.controlAdapterState.isEnabled);
this.konva.layer.visible(this.controlAdapterState.isEnabled);
this.group.opacity(this.controlAdapterState.opacity);
this.konva.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 (!this.image?.image) {
// 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();
this.konva.layer.listening(false);
this.konva.transformer.nodes([]);
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
}
return;
}
@ -118,32 +127,32 @@ export class CanvasControlAdapter {
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.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
// Activate the transformer
this.layer.listening(true);
this.transformer.nodes([this.group]);
this.transformer.forceUpdate();
this.konva.layer.listening(true);
this.konva.transformer.nodes([this.konva.group]);
this.konva.transformer.forceUpdate();
return;
}
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);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (this.konva.group.isCached()) {
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
}
return;
@ -151,12 +160,12 @@ export class CanvasControlAdapter {
if (!isSelected) {
// Unselected layers should not be listening
this.layer.listening(false);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
return;
@ -164,6 +173,6 @@ export class CanvasControlAdapter {
}
destroy(): void {
this.layer.destroy();
this.konva.layer.destroy();
}
}

View File

@ -9,40 +9,44 @@ export class CanvasEraserLine {
static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`;
id: string;
konvaLineGroup: Konva.Group;
konvaLine: Konva.Line;
konva: {
group: Konva.Group;
line: Konva.Line;
};
lastEraserLine: EraserLine;
constructor(eraserLine: EraserLine) {
const { id, strokeWidth, clip, points } = eraserLine;
this.id = id;
this.konvaLineGroup = new Konva.Group({
name: CanvasEraserLine.GROUP_NAME,
clip,
listening: false,
});
this.konvaLine = new Konva.Line({
name: CanvasEraserLine.LINE_NAME,
id,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
tension: 0,
lineCap: 'round',
lineJoin: 'round',
globalCompositeOperation: 'destination-out',
stroke: rgbaColorToString(RGBA_RED),
// A line with only one point will not be rendered, so we duplicate the points to make it visible
points: points.length === 2 ? [...points, ...points] : points,
});
this.konvaLineGroup.add(this.konvaLine);
this.konva = {
group: new Konva.Group({
name: CanvasEraserLine.GROUP_NAME,
clip,
listening: false,
}),
line: new Konva.Line({
name: CanvasEraserLine.LINE_NAME,
id,
listening: false,
shadowForStrokeEnabled: false,
strokeWidth,
tension: 0,
lineCap: 'round',
lineJoin: 'round',
globalCompositeOperation: 'destination-out',
stroke: rgbaColorToString(RGBA_RED),
// A line with only one point will not be rendered, so we duplicate the points to make it visible
points: points.length === 2 ? [...points, ...points] : points,
}),
};
this.konva.group.add(this.konva.line);
this.lastEraserLine = eraserLine;
}
update(eraserLine: EraserLine, force?: boolean): boolean {
if (this.lastEraserLine !== eraserLine || force) {
const { points, clip, strokeWidth } = eraserLine;
this.konvaLine.setAttrs({
this.konva.line.setAttrs({
// A line with only one point will not be rendered, so we duplicate the points to make it visible
points: points.length === 2 ? [...points, ...points] : points,
clip,
@ -56,6 +60,6 @@ export class CanvasEraserLine {
}
destroy() {
this.konvaLineGroup.destroy();
this.konva.group.destroy();
}
}

View File

@ -15,48 +15,51 @@ export class CanvasImage {
static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`;
id: string;
konvaImageGroup: Konva.Group;
konvaPlaceholderGroup: Konva.Group;
konvaPlaceholderRect: Konva.Rect;
konvaPlaceholderText: Konva.Text;
konva: {
group: Konva.Group;
placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text };
};
imageName: string | null;
konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
isLoading: boolean;
isError: boolean;
lastImageObject: ImageObject;
constructor(imageObject: ImageObject) {
const { id, width, height, x, y } = imageObject;
this.konvaImageGroup = new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y });
this.konvaPlaceholderGroup = new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false });
this.konvaPlaceholderRect = new Konva.Rect({
name: CanvasImage.PLACEHOLDER_RECT_NAME,
fill: 'hsl(220 12% 45% / 1)', // 'base.500'
width,
height,
listening: false,
});
this.konvaPlaceholderText = new Konva.Text({
name: CanvasImage.PLACEHOLDER_TEXT_NAME,
fill: 'hsl(220 12% 10% / 1)', // 'base.900'
width,
height,
align: 'center',
verticalAlign: 'middle',
fontFamily: '"Inter Variable", sans-serif',
fontSize: width / 16,
fontStyle: '600',
text: t('common.loadingImage', 'Loading Image'),
listening: false,
});
this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect);
this.konvaPlaceholderGroup.add(this.konvaPlaceholderText);
this.konvaImageGroup.add(this.konvaPlaceholderGroup);
this.konva = {
group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
placeholder: {
group: new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }),
rect: new Konva.Rect({
name: CanvasImage.PLACEHOLDER_RECT_NAME,
fill: 'hsl(220 12% 45% / 1)', // 'base.500'
width,
height,
listening: false,
}),
text: new Konva.Text({
name: CanvasImage.PLACEHOLDER_TEXT_NAME,
fill: 'hsl(220 12% 10% / 1)', // 'base.900'
width,
height,
align: 'center',
verticalAlign: 'middle',
fontFamily: '"Inter Variable", sans-serif',
fontSize: width / 16,
fontStyle: '600',
text: t('common.loadingImage', 'Loading Image'),
listening: false,
}),
},
};
this.konva.placeholder.group.add(this.konva.placeholder.rect);
this.konva.placeholder.group.add(this.konva.placeholder.text);
this.konva.group.add(this.konva.placeholder.group);
this.id = id;
this.imageName = null;
this.konvaImage = null;
this.image = null;
this.isLoading = false;
this.isError = false;
this.lastImageObject = imageObject;
@ -65,51 +68,51 @@ export class CanvasImage {
async updateImageSource(imageName: string) {
try {
this.isLoading = true;
this.konvaImageGroup.visible(true);
this.konva.group.visible(true);
if (!this.konvaImage) {
this.konvaPlaceholderGroup.visible(true);
this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image'));
if (!this.image) {
this.konva.placeholder.group.visible(true);
this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image'));
}
const imageDTO = await getImageDTO(imageName);
assert(imageDTO !== null, 'imageDTO is null');
const imageEl = await loadImage(imageDTO.image_url);
if (this.konvaImage) {
this.konvaImage.setAttrs({
if (this.image) {
this.image.setAttrs({
image: imageEl,
});
} else {
this.konvaImage = new Konva.Image({
this.image = new Konva.Image({
name: CanvasImage.IMAGE_NAME,
listening: false,
image: imageEl,
width: this.lastImageObject.width,
height: this.lastImageObject.height,
});
this.konvaImageGroup.add(this.konvaImage);
this.konva.group.add(this.image);
}
if (this.lastImageObject.filters.length > 0) {
this.konvaImage.cache();
this.konvaImage.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f]));
this.image.cache();
this.image.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f]));
} else {
this.konvaImage.clearCache();
this.konvaImage.filters([]);
this.image.clearCache();
this.image.filters([]);
}
this.imageName = imageName;
this.isLoading = false;
this.isError = false;
this.konvaPlaceholderGroup.visible(false);
this.konva.placeholder.group.visible(false);
} catch {
this.konvaImage?.visible(false);
this.image?.visible(false);
this.imageName = null;
this.isLoading = false;
this.isError = true;
this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
this.konvaPlaceholderGroup.visible(true);
this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load'));
this.konva.placeholder.group.visible(true);
}
}
@ -119,16 +122,16 @@ export class CanvasImage {
if (this.lastImageObject.image.name !== image.name || force) {
await this.updateImageSource(image.name);
}
this.konvaImage?.setAttrs({ x, y, width, height });
this.image?.setAttrs({ x, y, width, height });
if (filters.length > 0) {
this.konvaImage?.cache();
this.konvaImage?.filters(filters.map((f) => FILTER_MAP[f]));
this.image?.cache();
this.image?.filters(filters.map((f) => FILTER_MAP[f]));
} else {
this.konvaImage?.clearCache();
this.konvaImage?.filters([]);
this.image?.clearCache();
this.image?.filters([]);
}
this.konvaPlaceholderRect.setAttrs({ width, height });
this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 });
this.konva.placeholder.rect.setAttrs({ width, height });
this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 });
this.lastImageObject = imageObject;
return true;
} else {
@ -137,6 +140,6 @@ export class CanvasImage {
}
destroy() {
this.konvaImageGroup.destroy();
this.konva.group.destroy();
}
}

View File

@ -1,33 +1,37 @@
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
import type { InitialImageEntity } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { v4 as uuidv4 } from 'uuid';
export class CanvasInitialImage {
static NAME_PREFIX = 'initial-image';
static LAYER_NAME = `${CanvasInitialImage.NAME_PREFIX}_layer`;
static GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_group`;
static OBJECT_GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_object-group`;
id = 'initial_image';
manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group;
objectsGroup: Konva.Group;
image: CanvasImage | null;
private initialImageState: InitialImageEntity;
manager: CanvasManager;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
};
image: CanvasImage | null;
constructor(initialImageState: InitialImageEntity, manager: CanvasManager) {
this.manager = manager;
this.layer = new Konva.Layer({
id: this.id,
imageSmoothingEnabled: true,
listening: false,
});
this.group = new Konva.Group({
id: getObjectGroupId(this.layer.id(), uuidv4()),
listening: false,
});
this.objectsGroup = new Konva.Group({ listening: false });
this.group.add(this.objectsGroup);
this.layer.add(this.group);
this.konva = {
layer: new Konva.Layer({ name: CanvasInitialImage.LAYER_NAME, imageSmoothingEnabled: true, listening: false }),
group: new Konva.Group({ name: CanvasInitialImage.GROUP_NAME, listening: false }),
objectGroup: new Konva.Group({ name: CanvasInitialImage.OBJECT_GROUP_NAME, listening: false }),
};
this.konva.group.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.group);
this.image = null;
this.initialImageState = initialImageState;
@ -37,26 +41,26 @@ export class CanvasInitialImage {
this.initialImageState = initialImageState;
if (!this.initialImageState.imageObject) {
this.layer.visible(false);
this.konva.layer.visible(false);
return;
}
if (!this.image) {
this.image = new CanvasImage(this.initialImageState.imageObject);
this.objectsGroup.add(this.image.konvaImageGroup);
this.konva.objectGroup.add(this.image.konva.group);
await this.image.update(this.initialImageState.imageObject, true);
} else if (!this.image.isLoading && !this.image.isError) {
await this.image.update(this.initialImageState.imageObject);
}
if (this.initialImageState && this.initialImageState.isEnabled && !this.image?.isLoading && !this.image?.isError) {
this.layer.visible(true);
this.konva.layer.visible(true);
} else {
this.layer.visible(false);
this.konva.layer.visible(false);
}
}
destroy(): void {
this.layer.destroy();
this.konva.layer.destroy();
}
}

View File

@ -23,57 +23,64 @@ export class CanvasInpaintMask {
id = 'inpaint_mask';
manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group;
objectsGroup: Konva.Group;
compositingRect: Konva.Rect;
transformer: Konva.Transformer;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
transformer: Konva.Transformer;
compositingRect: Konva.Rect;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
constructor(entity: InpaintMaskEntity, manager: CanvasManager) {
this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME });
this.group = new Konva.Group({
name: CanvasInpaintMask.GROUP_NAME,
listening: false,
});
this.objectsGroup = new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false });
this.group.add(this.objectsGroup);
this.layer.add(this.group);
this.konva = {
layer: new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }),
group: new Konva.Group({ name: CanvasInpaintMask.GROUP_NAME, listening: false }),
objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }),
transformer: new Konva.Transformer({
name: CanvasInpaintMask.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
}),
compositingRect: new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }),
};
this.transformer = new Konva.Transformer({
name: CanvasInpaintMask.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
});
this.transformer.on('transformend', () => {
this.konva.group.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.group);
this.konva.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } },
{
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'inpaint_mask'
);
});
this.transformer.on('dragend', () => {
this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.group.x(), y: this.group.y() } },
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'inpaint_mask'
);
});
this.layer.add(this.transformer);
this.konva.layer.add(this.konva.transformer);
this.compositingRect = new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false });
this.group.add(this.compositingRect);
this.konva.group.add(this.konva.compositingRect);
this.objects = new Map();
this.drawingBuffer = null;
this.inpaintMaskState = entity;
}
destroy(): void {
this.layer.destroy();
this.konva.layer.destroy();
}
getDrawingBuffer() {
@ -112,7 +119,7 @@ export class CanvasInpaintMask {
this.inpaintMaskState = inpaintMaskState;
// Update the layer's position and listening state
this.group.setAttrs({
this.konva.group.setAttrs({
x: inpaintMaskState.position.x,
y: inpaintMaskState.position.y,
scaleX: 1,
@ -154,7 +161,7 @@ export class CanvasInpaintMask {
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup);
this.konva.objectGroup.add(brushLine.konva.group);
return true;
} else {
if (brushLine.update(obj, force)) {
@ -168,7 +175,7 @@ export class CanvasInpaintMask {
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup);
this.konva.objectGroup.add(eraserLine.konva.group);
return true;
} else {
if (eraserLine.update(obj, force)) {
@ -182,7 +189,7 @@ export class CanvasInpaintMask {
if (!rect) {
rect = new CanvasRect(obj);
this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect);
this.konva.objectGroup.add(rect.konva.group);
return true;
} else {
if (rect.update(obj, force)) {
@ -195,19 +202,19 @@ export class CanvasInpaintMask {
}
updateGroup(didDraw: boolean) {
this.layer.visible(this.inpaintMaskState.isEnabled);
this.konva.layer.visible(this.inpaintMaskState.isEnabled);
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
this.group.opacity(1);
this.konva.group.opacity(1);
if (didDraw) {
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(this.inpaintMaskState.fill);
const maskOpacity = this.manager.stateApi.getMaskOpacity();
this.compositingRect.setAttrs({
this.konva.compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...getNodeBboxFast(this.objectsGroup),
...getNodeBboxFast(this.konva.objectGroup),
fill: rgbColor,
opacity: maskOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
@ -223,10 +230,10 @@ export class CanvasInpaintMask {
if (this.objects.size === 0) {
// 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();
this.konva.layer.listening(false);
this.konva.transformer.nodes([]);
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
}
return;
}
@ -234,32 +241,32 @@ export class CanvasInpaintMask {
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.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
// Activate the transformer
this.layer.listening(true);
this.transformer.nodes([this.group]);
this.transformer.forceUpdate();
this.konva.layer.listening(true);
this.konva.transformer.nodes([this.konva.group]);
this.konva.transformer.forceUpdate();
return;
}
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);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (this.konva.group.isCached()) {
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
}
return;
@ -267,12 +274,12 @@ export class CanvasInpaintMask {
if (!isSelected) {
// Unselected layers should not be listening
this.layer.listening(false);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
return;

View File

@ -21,40 +21,53 @@ export class CanvasLayer {
id: string;
manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group;
objectsGroup: Konva.Group;
transformer: Konva.Transformer;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
transformer: Konva.Transformer;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
constructor(entity: LayerEntity, manager: CanvasManager) {
this.id = entity.id;
this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false });
this.group = new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false });
this.objectsGroup = new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false });
this.group.add(this.objectsGroup);
this.layer.add(this.group);
this.konva = {
layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }),
group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }),
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
transformer: new Konva.Transformer({
name: CanvasLayer.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
}),
};
this.transformer = new Konva.Transformer({
name: CanvasLayer.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
});
this.transformer.on('transformend', () => {
this.konva.group.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.group);
this.konva.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } },
{
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'layer'
);
});
this.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged({ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, 'layer');
this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'layer'
);
});
this.layer.add(this.transformer);
this.konva.layer.add(this.konva.transformer);
this.objects = new Map();
this.drawingBuffer = null;
@ -62,7 +75,7 @@ export class CanvasLayer {
}
destroy(): void {
this.layer.destroy();
this.konva.layer.destroy();
}
getDrawingBuffer() {
@ -97,7 +110,7 @@ export class CanvasLayer {
this.layerState = layerState;
// Update the layer's position and listening state
this.group.setAttrs({
this.konva.group.setAttrs({
x: layerState.position.x,
y: layerState.position.y,
scaleX: 1,
@ -139,7 +152,7 @@ export class CanvasLayer {
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup);
this.konva.objectGroup.add(brushLine.konva.group);
return true;
} else {
if (brushLine.update(obj, force)) {
@ -153,7 +166,7 @@ export class CanvasLayer {
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup);
this.konva.objectGroup.add(eraserLine.konva.group);
return true;
} else {
if (eraserLine.update(obj, force)) {
@ -167,7 +180,7 @@ export class CanvasLayer {
if (!rect) {
rect = new CanvasRect(obj);
this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect);
this.konva.objectGroup.add(rect.konva.group);
return true;
} else {
if (rect.update(obj, force)) {
@ -181,7 +194,7 @@ export class CanvasLayer {
if (!image) {
image = new CanvasImage(obj);
this.objects.set(image.id, image);
this.objectsGroup.add(image.konvaImageGroup);
this.konva.objectGroup.add(image.konva.group);
await image.updateImageSource(obj.image.name);
return true;
} else {
@ -196,58 +209,58 @@ export class CanvasLayer {
updateGroup(didDraw: boolean) {
if (!this.layerState.isEnabled) {
this.layer.visible(false);
this.konva.layer.visible(false);
return;
}
this.layer.visible(true);
this.group.opacity(this.layerState.opacity);
this.konva.layer.visible(true);
this.konva.group.opacity(this.layerState.opacity);
const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected;
if (this.objects.size === 0) {
// 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();
this.konva.layer.listening(false);
this.konva.transformer.nodes([]);
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
}
} else 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.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
// Activate the transformer
this.layer.listening(true);
this.transformer.nodes([this.group]);
this.transformer.forceUpdate();
this.konva.layer.listening(true);
this.konva.transformer.nodes([this.konva.group]);
this.konva.transformer.forceUpdate();
} else 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);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (this.konva.group.isCached()) {
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
}
} else if (!isSelected) {
// Unselected layers should not be listening
this.layer.listening(false);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
}
}

View File

@ -97,17 +97,17 @@ export class CanvasManager {
this.stage.add(this.preview.layer);
this.background = new CanvasBackground(this);
this.stage.add(this.background.layer);
this.stage.add(this.background.konva.layer);
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
this.stage.add(this.inpaintMask.layer);
this.stage.add(this.inpaintMask.konva.layer);
this.layers = new Map();
this.regions = new Map();
this.controlAdapters = new Map();
this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this);
this.stage.add(this.initialImage.layer);
this.stage.add(this.initialImage.konva.layer);
}
async renderInitialImage() {
@ -129,7 +129,7 @@ export class CanvasManager {
if (!adapter) {
adapter = new CanvasLayer(entity, this);
this.layers.set(adapter.id, adapter);
this.stage.add(adapter.layer);
this.stage.add(adapter.konva.layer);
}
await adapter.render(entity);
}
@ -151,7 +151,7 @@ export class CanvasManager {
if (!adapter) {
adapter = new CanvasRegion(entity, this);
this.regions.set(adapter.id, adapter);
this.stage.add(adapter.layer);
this.stage.add(adapter.konva.layer);
}
await adapter.render(entity);
}
@ -181,7 +181,7 @@ export class CanvasManager {
if (!adapter) {
adapter = new CanvasControlAdapter(entity, this);
this.controlAdapters.set(adapter.id, adapter);
this.stage.add(adapter.layer);
this.stage.add(adapter.konva.layer);
}
await adapter.render(entity);
}
@ -193,18 +193,18 @@ export class CanvasManager {
const controlAdapters = getControlAdaptersState().entities;
const regions = getRegionsState().entities;
let zIndex = 0;
this.background.layer.zIndex(++zIndex);
this.initialImage.layer.zIndex(++zIndex);
this.background.konva.layer.zIndex(++zIndex);
this.initialImage.konva.layer.zIndex(++zIndex);
for (const layer of layers) {
this.layers.get(layer.id)?.layer.zIndex(++zIndex);
this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex);
}
for (const ca of controlAdapters) {
this.controlAdapters.get(ca.id)?.layer.zIndex(++zIndex);
this.controlAdapters.get(ca.id)?.konva.layer.zIndex(++zIndex);
}
for (const rg of regions) {
this.regions.get(rg.id)?.layer.zIndex(++zIndex);
this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex);
}
this.inpaintMask.layer.zIndex(++zIndex);
this.inpaintMask.konva.layer.zIndex(++zIndex);
this.preview.layer.zIndex(++zIndex);
}

View File

@ -21,15 +21,15 @@ export class CanvasPreview {
this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false });
this.stagingArea = stagingArea;
this.layer.add(this.stagingArea.group);
this.layer.add(this.stagingArea.konva.group);
this.bbox = bbox;
this.layer.add(this.bbox.group);
this.layer.add(this.bbox.konva.group);
this.tool = tool;
this.layer.add(this.tool.group);
this.layer.add(this.tool.konva.group);
this.progressPreview = progressPreview;
this.layer.add(this.progressPreview.group);
this.layer.add(this.progressPreview.konva.group);
}
}

View File

@ -8,17 +8,21 @@ export class CanvasProgressImage {
id: string;
progressImageId: string | null;
konvaImageGroup: Konva.Group;
konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
konva: {
group: Konva.Group;
image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately
};
isLoading: boolean;
isError: boolean;
constructor(arg: { id: string }) {
const { id } = arg;
this.konvaImageGroup = new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false });
this.konva = {
group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }),
image: null,
};
this.id = id;
this.progressImageId = null;
this.konvaImage = null;
this.isLoading = false;
this.isError = false;
}
@ -37,8 +41,8 @@ export class CanvasProgressImage {
this.isLoading = true;
try {
const imageEl = await loadImage(dataURL);
if (this.konvaImage) {
this.konvaImage.setAttrs({
if (this.konva.image) {
this.konva.image.setAttrs({
image: imageEl,
x,
y,
@ -46,7 +50,7 @@ export class CanvasProgressImage {
height,
});
} else {
this.konvaImage = new Konva.Image({
this.konva.image = new Konva.Image({
name: CanvasProgressImage.IMAGE_NAME,
listening: false,
image: imageEl,
@ -55,7 +59,7 @@ export class CanvasProgressImage {
width,
height,
});
this.konvaImageGroup.add(this.konvaImage);
this.konva.group.add(this.konva.image);
}
this.isLoading = false;
this.id = progressImageId;
@ -65,6 +69,6 @@ export class CanvasProgressImage {
}
destroy() {
this.konvaImageGroup.destroy();
this.konva.group.destroy();
}
}

View File

@ -7,15 +7,19 @@ export class CanvasProgressPreview {
static NAME_PREFIX = 'progress-preview';
static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`;
group: Konva.Group;
progressImage: CanvasProgressImage;
konva: {
group: Konva.Group;
progressImage: CanvasProgressImage;
};
manager: CanvasManager;
constructor(manager: CanvasManager) {
this.manager = manager;
this.group = new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false });
this.progressImage = new CanvasProgressImage({ id: 'progress-image' });
this.group.add(this.progressImage.konvaImageGroup);
this.konva = {
group: new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }),
progressImage: new CanvasProgressImage({ id: 'progress-image' }),
};
this.konva.group.add(this.konva.progressImage.konva.group);
}
async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) {
@ -28,15 +32,15 @@ export class CanvasProgressPreview {
const { x, y, width, height } = bboxRect;
const progressImageId = `${invocation.id}_${step}`;
if (
!this.progressImage.isLoading &&
!this.progressImage.isError &&
this.progressImage.progressImageId !== progressImageId
!this.konva.progressImage.isLoading &&
!this.konva.progressImage.isError &&
this.konva.progressImage.progressImageId !== progressImageId
) {
await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height);
this.progressImage.konvaImageGroup.visible(true);
await this.konva.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height);
this.konva.progressImage.konva.group.visible(true);
}
} else {
this.progressImage.konvaImageGroup.visible(false);
this.konva.progressImage.konva.group.visible(false);
}
}
}

View File

@ -4,33 +4,40 @@ import Konva from 'konva';
export class CanvasRect {
static NAME_PREFIX = 'canvas-rect';
static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`;
static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`;
id: string;
konvaRect: Konva.Rect;
konva: {
group: Konva.Group;
rect: Konva.Rect;
};
lastRectShape: RectShape;
constructor(rectShape: RectShape) {
const { id, x, y, width, height } = rectShape;
this.id = id;
const konvaRect = new Konva.Rect({
name: CanvasRect.RECT_NAME,
id,
x,
y,
width,
height,
listening: false,
fill: rgbaColorToString(rectShape.color),
});
this.konvaRect = konvaRect;
this.konva = {
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
rect: new Konva.Rect({
name: CanvasRect.RECT_NAME,
id,
x,
y,
width,
height,
listening: false,
fill: rgbaColorToString(rectShape.color),
}),
};
this.konva.group.add(this.konva.rect);
this.lastRectShape = rectShape;
}
update(rectShape: RectShape, force?: boolean): boolean {
if (this.lastRectShape !== rectShape || force) {
const { x, y, width, height, color } = rectShape;
this.konvaRect.setAttrs({
this.konva.rect.setAttrs({
x,
y,
width,
@ -45,6 +52,6 @@ export class CanvasRect {
}
destroy() {
this.konvaRect.destroy();
this.konva.group.destroy();
}
}

View File

@ -23,54 +23,63 @@ export class CanvasRegion {
id: string;
manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group;
objectsGroup: Konva.Group;
compositingRect: Konva.Rect;
transformer: Konva.Transformer;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
compositingRect: Konva.Rect;
transformer: Konva.Transformer;
};
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
constructor(entity: RegionEntity, manager: CanvasManager) {
this.id = entity.id;
this.manager = manager;
this.layer = new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false });
this.group = new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false });
this.objectsGroup = new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false });
this.group.add(this.objectsGroup);
this.layer.add(this.group);
this.transformer = new Konva.Transformer({
name: CanvasRegion.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
});
this.transformer.on('transformend', () => {
this.konva = {
layer: new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }),
group: new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }),
objectGroup: new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }),
transformer: new Konva.Transformer({
name: CanvasRegion.TRANSFORMER_NAME,
shouldOverdrawWholeArea: true,
draggable: true,
dragDistance: 0,
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
rotateEnabled: false,
flipEnabled: false,
}),
compositingRect: new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }),
};
this.konva.group.add(this.konva.objectGroup);
this.konva.layer.add(this.konva.group);
this.konva.transformer.on('transformend', () => {
this.manager.stateApi.onScaleChanged(
{ id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } },
{
id: this.id,
scale: this.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'regional_guidance'
);
});
this.transformer.on('dragend', () => {
this.konva.transformer.on('dragend', () => {
this.manager.stateApi.onPosChanged(
{ id: this.id, position: { x: this.group.x(), y: this.group.y() } },
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'regional_guidance'
);
});
this.layer.add(this.transformer);
this.compositingRect = new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false });
this.group.add(this.compositingRect);
this.konva.layer.add(this.konva.transformer);
this.konva.group.add(this.konva.compositingRect);
this.objects = new Map();
this.drawingBuffer = null;
this.regionState = entity;
}
destroy(): void {
this.layer.destroy();
this.konva.layer.destroy();
}
getDrawingBuffer() {
@ -109,7 +118,7 @@ export class CanvasRegion {
this.regionState = regionState;
// Update the layer's position and listening state
this.group.setAttrs({
this.konva.group.setAttrs({
x: regionState.position.x,
y: regionState.position.y,
scaleX: 1,
@ -151,7 +160,7 @@ export class CanvasRegion {
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup);
this.konva.objectGroup.add(brushLine.konva.group);
return true;
} else {
if (brushLine.update(obj, force)) {
@ -165,7 +174,7 @@ export class CanvasRegion {
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup);
this.konva.objectGroup.add(eraserLine.konva.group);
return true;
} else {
if (eraserLine.update(obj, force)) {
@ -179,7 +188,7 @@ export class CanvasRegion {
if (!rect) {
rect = new CanvasRect(obj);
this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect);
this.konva.objectGroup.add(rect.konva.group);
return true;
} else {
if (rect.update(obj, force)) {
@ -192,18 +201,18 @@ export class CanvasRegion {
}
updateGroup(didDraw: boolean) {
this.layer.visible(this.regionState.isEnabled);
this.konva.layer.visible(this.regionState.isEnabled);
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
this.group.opacity(1);
this.konva.group.opacity(1);
if (didDraw) {
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(this.regionState.fill);
const maskOpacity = this.manager.stateApi.getMaskOpacity();
this.compositingRect.setAttrs({
this.konva.compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...getNodeBboxFast(this.objectsGroup),
...getNodeBboxFast(this.konva.objectGroup),
fill: rgbColor,
opacity: maskOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
@ -218,10 +227,10 @@ export class CanvasRegion {
if (this.objects.size === 0) {
// 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();
this.konva.layer.listening(false);
this.konva.transformer.nodes([]);
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
}
return;
}
@ -229,32 +238,32 @@ export class CanvasRegion {
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.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
// Activate the transformer
this.layer.listening(true);
this.transformer.nodes([this.group]);
this.transformer.forceUpdate();
this.konva.layer.listening(true);
this.konva.transformer.nodes([this.konva.group]);
this.konva.transformer.forceUpdate();
return;
}
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);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (this.konva.group.isCached()) {
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
}
return;
@ -262,12 +271,12 @@ export class CanvasRegion {
if (!isSelected) {
// Unselected layers should not be listening
this.layer.listening(false);
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.transformer.nodes([]);
this.konva.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();
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
return;

View File

@ -4,14 +4,18 @@ import type { StagingAreaImage } from 'features/controlLayers/store/types';
import Konva from 'konva';
export class CanvasStagingArea {
group: Konva.Group;
static NAME_PREFIX = 'staging-area';
static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`;
konva: { group: Konva.Group };
image: CanvasImage | null;
selectedImage: StagingAreaImage | null;
manager: CanvasManager;
constructor(manager: CanvasManager) {
this.manager = manager;
this.group = new Konva.Group({ listening: false });
this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) };
this.image = null;
this.selectedImage = null;
}
@ -42,20 +46,20 @@ export class CanvasStagingArea {
height,
},
});
this.group.add(this.image.konvaImageGroup);
this.konva.group.add(this.image.konva.group);
}
if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) {
this.image.konvaImage?.width(imageDTO.width);
this.image.konvaImage?.height(imageDTO.height);
this.image.konvaImageGroup.x(bboxRect.x + offsetX);
this.image.konvaImageGroup.y(bboxRect.y + offsetY);
this.image.image?.width(imageDTO.width);
this.image.image?.height(imageDTO.height);
this.image.konva.group.x(bboxRect.x + offsetX);
this.image.konva.group.y(bboxRect.y + offsetY);
await this.image.updateImageSource(imageDTO.image_name);
this.manager.stateApi.resetLastProgressEvent();
}
this.image.konvaImageGroup.visible(shouldShowStagedImage);
this.image.konva.group.visible(shouldShowStagedImage);
} else {
this.image?.konvaImageGroup.visible(false);
this.image?.konva.group.visible(false);
}
}
}

View File

@ -25,80 +25,82 @@ export class CanvasTool {
static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`;
manager: CanvasManager;
group: Konva.Group;
brush: {
konva: {
group: Konva.Group;
fillCircle: Konva.Circle;
innerBorderCircle: Konva.Circle;
outerBorderCircle: Konva.Circle;
};
eraser: {
group: Konva.Group;
fillCircle: Konva.Circle;
innerBorderCircle: Konva.Circle;
outerBorderCircle: Konva.Circle;
brush: {
group: Konva.Group;
fillCircle: Konva.Circle;
innerBorderCircle: Konva.Circle;
outerBorderCircle: Konva.Circle;
};
eraser: {
group: Konva.Group;
fillCircle: Konva.Circle;
innerBorderCircle: Konva.Circle;
outerBorderCircle: Konva.Circle;
};
};
constructor(manager: CanvasManager) {
this.manager = manager;
this.group = new Konva.Group({ name: CanvasTool.GROUP_NAME });
// Create the brush preview group & circles
this.brush = {
group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }),
fillCircle: new Konva.Circle({
name: CanvasTool.BRUSH_FILL_CIRCLE_NAME,
listening: false,
strokeEnabled: false,
}),
innerBorderCircle: new Konva.Circle({
name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_INNER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
outerBorderCircle: new Konva.Circle({
name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_OUTER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
this.konva = {
group: new Konva.Group({ name: CanvasTool.GROUP_NAME }),
brush: {
group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }),
fillCircle: new Konva.Circle({
name: CanvasTool.BRUSH_FILL_CIRCLE_NAME,
listening: false,
strokeEnabled: false,
}),
innerBorderCircle: new Konva.Circle({
name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_INNER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
outerBorderCircle: new Konva.Circle({
name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_OUTER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
},
eraser: {
group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }),
fillCircle: new Konva.Circle({
name: CanvasTool.ERASER_FILL_CIRCLE_NAME,
listening: false,
strokeEnabled: false,
fill: 'white',
globalCompositeOperation: 'destination-out',
}),
innerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_INNER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
outerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_OUTER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
},
};
this.brush.group.add(this.brush.fillCircle);
this.brush.group.add(this.brush.innerBorderCircle);
this.brush.group.add(this.brush.outerBorderCircle);
this.group.add(this.brush.group);
this.konva.brush.group.add(this.konva.brush.fillCircle);
this.konva.brush.group.add(this.konva.brush.innerBorderCircle);
this.konva.brush.group.add(this.konva.brush.outerBorderCircle);
this.konva.group.add(this.konva.brush.group);
this.eraser = {
group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }),
fillCircle: new Konva.Circle({
name: CanvasTool.ERASER_FILL_CIRCLE_NAME,
listening: false,
strokeEnabled: false,
fill: 'white',
globalCompositeOperation: 'destination-out',
}),
innerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_INNER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
outerBorderCircle: new Konva.Circle({
name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME,
listening: false,
stroke: BRUSH_BORDER_OUTER_COLOR,
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
strokeEnabled: true,
}),
};
this.eraser.group.add(this.eraser.fillCircle);
this.eraser.group.add(this.eraser.innerBorderCircle);
this.eraser.group.add(this.eraser.outerBorderCircle);
this.group.add(this.eraser.group);
this.konva.eraser.group.add(this.konva.eraser.fillCircle);
this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle);
this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle);
this.konva.group.add(this.konva.eraser.group);
// // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
// this.rect = {
@ -110,7 +112,7 @@ export class CanvasTool {
// }),
// };
// this.rect.group.add(this.rect.fillRect);
// this.group.add(this.rect.group);
// this.konva.group.add(this.rect.group);
}
scaleTool = () => {
@ -118,15 +120,15 @@ export class CanvasTool {
const scale = this.manager.stage.scaleX();
const brushRadius = toolState.brush.width / 2;
this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
this.brush.outerBorderCircle.setAttrs({
this.konva.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
this.konva.brush.outerBorderCircle.setAttrs({
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale,
});
const eraserRadius = toolState.eraser.width / 2;
this.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
this.eraser.outerBorderCircle.setAttrs({
this.konva.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
this.konva.eraser.outerBorderCircle.setAttrs({
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale,
});
@ -175,16 +177,16 @@ export class CanvasTool {
if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) {
// We can bail early if the mouse isn't over the stage or there are no layers
this.group.visible(false);
this.konva.group.visible(false);
} else {
this.group.visible(true);
this.konva.group.visible(true);
// No need to render the brush preview if the cursor position or color is missing
if (cursorPos && tool === 'brush') {
const scale = stage.scaleX();
// Update the fill circle
const radius = toolState.brush.width / 2;
this.brush.fillCircle.setAttrs({
this.konva.brush.fillCircle.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius,
@ -192,10 +194,10 @@ export class CanvasTool {
});
// Update the inner border of the brush preview
this.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
this.konva.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
// Update the outer border of the brush preview
this.brush.outerBorderCircle.setAttrs({
this.konva.brush.outerBorderCircle.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
@ -203,14 +205,14 @@ export class CanvasTool {
this.scaleTool();
this.brush.group.visible(true);
this.eraser.group.visible(false);
this.konva.brush.group.visible(true);
this.konva.eraser.group.visible(false);
// this.rect.group.visible(false);
} else if (cursorPos && tool === 'eraser') {
const scale = stage.scaleX();
// Update the fill circle
const radius = toolState.eraser.width / 2;
this.eraser.fillCircle.setAttrs({
this.konva.eraser.fillCircle.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius,
@ -218,10 +220,10 @@ export class CanvasTool {
});
// Update the inner border of the eraser preview
this.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
this.konva.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
// Update the outer border of the eraser preview
this.eraser.outerBorderCircle.setAttrs({
this.konva.eraser.outerBorderCircle.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
@ -229,8 +231,8 @@ export class CanvasTool {
this.scaleTool();
this.brush.group.visible(false);
this.eraser.group.visible(true);
this.konva.brush.group.visible(false);
this.konva.eraser.group.visible(true);
// this.rect.group.visible(false);
// } else if (cursorPos && lastMouseDownPos && tool === 'rect') {
// this.rect.fillRect.setAttrs({
@ -241,12 +243,12 @@ export class CanvasTool {
// fill: rgbaColorToString(currentFill),
// visible: true,
// });
// this.brush.group.visible(false);
// this.eraser.group.visible(false);
// this.konva.brush.group.visible(false);
// this.konva.eraser.group.visible(false);
// this.rect.group.visible(true);
} else {
this.brush.group.visible(false);
this.eraser.group.visible(false);
this.konva.brush.group.visible(false);
this.konva.eraser.group.visible(false);
// this.rect.group.visible(false);
}
}

View File

@ -269,8 +269,8 @@ export const previewBlob = async (blob: Blob, label?: string) => {
export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer {
const { manager } = arg;
const layerClone = manager.inpaintMask.layer.clone();
const objectGroupClone = manager.inpaintMask.group.clone();
const layerClone = manager.inpaintMask.konva.layer.clone();
const objectGroupClone = manager.inpaintMask.konva.group.clone();
layerClone.destroyChildren();
layerClone.add(objectGroupClone);
@ -287,8 +287,8 @@ export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: strin
const canvasRegion = manager.regions.get(id);
assert(canvasRegion, `Canvas region with id ${id} not found`);
const layerClone = canvasRegion.layer.clone();
const objectGroupClone = canvasRegion.group.clone();
const layerClone = canvasRegion.konva.layer.clone();
const objectGroupClone = canvasRegion.konva.group.clone();
layerClone.destroyChildren();
layerClone.add(objectGroupClone);
@ -305,8 +305,8 @@ export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: s
const controlAdapter = manager.controlAdapters.get(id);
assert(controlAdapter, `Canvas region with id ${id} not found`);
const controlAdapterClone = controlAdapter.layer.clone();
const objectGroupClone = controlAdapter.group.clone();
const controlAdapterClone = controlAdapter.konva.layer.clone();
const objectGroupClone = controlAdapter.konva.group.clone();
controlAdapterClone.destroyChildren();
controlAdapterClone.add(objectGroupClone);
@ -322,8 +322,8 @@ export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konv
const initialImage = manager.initialImage;
const initialImageClone = initialImage.layer.clone();
const objectGroupClone = initialImage.group.clone();
const initialImageClone = initialImage.konva.layer.clone();
const objectGroupClone = initialImage.konva.group.clone();
initialImageClone.destroyChildren();
initialImageClone.add(objectGroupClone);