Skip to content

Commit 4d9114a

Browse files
refactor(ui): editor (wip)
1 parent 67e2da1 commit 4d9114a

File tree

1 file changed

+47
-52
lines changed
  • invokeai/frontend/web/src/features/editImageModal/lib

1 file changed

+47
-52
lines changed

invokeai/frontend/web/src/features/editImageModal/lib/editor.ts

Lines changed: 47 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { $crossOrigin } from 'app/store/nanostores/authToken';
22
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
33
import Konva from 'konva';
44
import type { KonvaEventObject } from 'konva/lib/Node';
5+
import { objectEntries } from 'tsafe';
56

67
type CropConstraints = {
78
minWidth?: number;
@@ -29,18 +30,8 @@ type EditorCallbacks = {
2930
};
3031

3132
type HandleName = 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left' | 'top' | 'right' | 'bottom' | 'left';
32-
type GuideName = 'left' | 'right' | 'top' | 'bottom';
3333

34-
// const HANDLE_INIT_COORDS: Record<HandleName, { x: number; y: number }> = {
35-
// 'top-left': { x: 0, y: 0 },
36-
// 'top-right': { x: 1, y: 0 },
37-
// 'bottom-right': { x: 1, y: 1 },
38-
// 'bottom-left': { x: 0, y: 1 },
39-
// top: { x: 0.5, y: 0 },
40-
// right: { x: 1, y: 0.5 },
41-
// bottom: { x: 0.5, y: 1 },
42-
// left: { x: 0, y: 0.5 },
43-
// };
34+
type GuideName = 'left' | 'right' | 'top' | 'bottom';
4435

4536
type KonvaObjects = {
4637
stage: Konva.Stage;
@@ -96,8 +87,6 @@ export class Editor {
9687
private readonly CROP_HANDLE_STROKE = 'black';
9788
private readonly FIT_TO_CONTAINER_PADDING = 0.9;
9889
private readonly DEFAULT_CROP_BOX_SCALE = 0.8;
99-
private readonly CORNER_HANDLE_NAMES = ['top-left', 'top-right', 'bottom-right', 'bottom-left'];
100-
private readonly EDGE_HANDLE_NAMES = ['top', 'right', 'bottom', 'left'];
10190

10291
// Configuration
10392
private readonly ZOOM_MIN = 0.1;
@@ -391,7 +380,7 @@ export class Editor {
391380

392381
// Handle dragging
393382
rect.on('dragmove', () => {
394-
this.resizeCropBox(rect);
383+
this.resizeCropBox(name, rect);
395384
});
396385

397386
return rect;
@@ -687,14 +676,14 @@ export class Editor {
687676
this.callbacks.onCropStart?.();
688677
};
689678

690-
private resizeCropBox = (handle: Konva.Rect) => {
679+
private resizeCropBox = (handleName: HandleName, handleRect: Konva.Rect) => {
691680
if (!this.konva) {
692681
return;
693682
}
694683

695684
let { newX, newY, newWidth, newHeight } = this.cropConstraints.aspectRatio
696-
? this._resizeCropBoxWithAspectRatio(handle)
697-
: this._resizeCropBoxFree(handle);
685+
? this._resizeCropBoxWithAspectRatio(handleName, handleRect)
686+
: this._resizeCropBoxFree(handleName, handleRect);
698687

699688
// Apply general constraints
700689
if (this.cropConstraints.maxWidth) {
@@ -712,12 +701,11 @@ export class Editor {
712701
});
713702
};
714703

715-
private _resizeCropBoxFree = (handle: Konva.Rect) => {
704+
private _resizeCropBoxFree = (handleName: HandleName, handleRect: Konva.Rect) => {
716705
if (!this.konva?.image.image) {
717706
throw new Error('Crop box or image not found');
718707
}
719708
const rect = this.konva.crop.overlay.clear;
720-
const handleName = handle.name();
721709
const imgWidth = this.konva.image.image.width();
722710
const imgHeight = this.konva.image.image.height();
723711

@@ -726,8 +714,8 @@ export class Editor {
726714
let newWidth = rect.width();
727715
let newHeight = rect.height();
728716

729-
const handleX = handle.x() + handle.width() / 2;
730-
const handleY = handle.y() + handle.height() / 2;
717+
const handleX = handleRect.x() + handleRect.width() / 2;
718+
const handleY = handleRect.y() + handleRect.height() / 2;
731719

732720
const minWidth = this.cropConstraints.minWidth ?? this.MIN_CROP_DIMENSION;
733721
const minHeight = this.cropConstraints.minHeight ?? this.MIN_CROP_DIMENSION;
@@ -753,47 +741,56 @@ export class Editor {
753741
return { newX, newY, newWidth, newHeight };
754742
};
755743

756-
private _resizeCropBoxWithAspectRatio = (handle: Konva.Rect) => {
757-
if (!this.konva?.image.image || !this.cropConstraints.aspectRatio) {
744+
private _resizeCropBoxWithAspectRatio = (handleName: HandleName, handleRect: Konva.Rect) => {
745+
if (!this.konva?.image.image || !this.cropConstraints.aspectRatio || !this.cropBox) {
758746
throw new Error('Crop box, image, or aspect ratio not found');
759747
}
760-
const rect = this.konva.crop.overlay.clear;
761-
const handleName = handle.name();
762748
const imgWidth = this.konva.image.image.width();
763749
const imgHeight = this.konva.image.image.height();
764750
const ratio = this.cropConstraints.aspectRatio;
765751

766-
const handleX = handle.x() + handle.width() / 2;
767-
const handleY = handle.y() + handle.height() / 2;
752+
const handleX = handleRect.x() + handleRect.width() / 2;
753+
const handleY = handleRect.y() + handleRect.height() / 2;
768754

769755
const minWidth = this.cropConstraints.minWidth ?? this.MIN_CROP_DIMENSION;
770756
const minHeight = this.cropConstraints.minHeight ?? this.MIN_CROP_DIMENSION;
771757

772758
// Early boundary check for aspect ratio mode
773-
const atLeftEdge = rect.x() <= 0;
774-
const atRightEdge = rect.x() + rect.width() >= imgWidth;
775-
const atTopEdge = rect.y() <= 0;
776-
const atBottomEdge = rect.y() + rect.height() >= imgHeight;
759+
const atLeftEdge = this.cropBox.x <= 0;
760+
const atRightEdge = this.cropBox.x + this.cropBox.width >= imgWidth;
761+
const atTopEdge = this.cropBox.y <= 0;
762+
const atBottomEdge = this.cropBox.y + this.cropBox.height >= imgHeight;
777763

778764
if (
779-
(handleName === 'left' && atLeftEdge && handleX >= rect.x()) ||
780-
(handleName === 'right' && atRightEdge && handleX <= rect.x() + rect.width()) ||
781-
(handleName === 'top' && atTopEdge && handleY >= rect.y()) ||
782-
(handleName === 'bottom' && atBottomEdge && handleY <= rect.y() + rect.height())
765+
(handleName === 'left' && atLeftEdge && handleX < this.cropBox.x) ||
766+
(handleName === 'right' && atRightEdge && handleX > this.cropBox.x + this.cropBox.width) ||
767+
(handleName === 'top' && atTopEdge && handleY < this.cropBox.y) ||
768+
(handleName === 'bottom' && atBottomEdge && handleY > this.cropBox.y + this.cropBox.height)
783769
) {
784-
return { newX: rect.x(), newY: rect.y(), newWidth: rect.width(), newHeight: rect.height() };
770+
return {
771+
newX: this.cropBox.x,
772+
newY: this.cropBox.y,
773+
newWidth: this.cropBox.width,
774+
newHeight: this.cropBox.height,
775+
};
785776
}
786777

787-
const { newX: freeX, newY: freeY, newWidth: freeWidth, newHeight: freeHeight } = this._resizeCropBoxFree(handle);
778+
const {
779+
newX: freeX,
780+
newY: freeY,
781+
newWidth: freeWidth,
782+
newHeight: freeHeight,
783+
} = this._resizeCropBoxFree(handleName, handleRect);
784+
788785
let newX = freeX;
789786
let newY = freeY;
790787
let newWidth = freeWidth;
791788
let newHeight = freeHeight;
792789

793-
const oldX = rect.x();
794-
const oldY = rect.y();
795-
const oldWidth = rect.width();
796-
const oldHeight = rect.height();
790+
const oldX = this.cropBox.x;
791+
const oldY = this.cropBox.y;
792+
const oldWidth = this.cropBox.width;
793+
const oldHeight = this.cropBox.height;
797794

798795
// Define anchor points (opposite of the handle being dragged)
799796
let anchorX = oldX;
@@ -815,10 +812,8 @@ export class Editor {
815812
anchorY = oldY + oldHeight / 2; // Center Y is anchor for left/right
816813
}
817814

818-
const isCornerHandle = this.CORNER_HANDLE_NAMES.includes(handleName);
819-
820815
// Calculate new dimensions maintaining aspect ratio
821-
if (this.EDGE_HANDLE_NAMES.includes(handleName) && !isCornerHandle) {
816+
if (handleName === 'left' || handleName === 'right' || handleName === 'top' || handleName === 'bottom') {
822817
if (handleName === 'left' || handleName === 'right') {
823818
newHeight = newWidth / ratio;
824819
newY = anchorY - newHeight / 2;
@@ -827,7 +822,8 @@ export class Editor {
827822
newWidth = newHeight * ratio;
828823
newX = anchorX - newWidth / 2;
829824
}
830-
} else if (isCornerHandle) {
825+
} else {
826+
// Corner handles
831827
const mouseDistanceFromAnchorX = Math.abs(handleX - anchorX);
832828
const mouseDistanceFromAnchorY = Math.abs(handleY - anchorY);
833829

@@ -897,14 +893,13 @@ export class Editor {
897893
return { newX, newY, newWidth, newHeight };
898894
};
899895

900-
private positionHandle = (handle: Konva.Rect) => {
896+
private positionHandle = (handleName: HandleName, handleRect: Konva.Rect) => {
901897
if (!this.konva || !this.cropBox) {
902898
return;
903899
}
904900

905901
const { x, y, width, height } = this.cropBox;
906-
const handleName = handle.name();
907-
const handleSize = handle.width();
902+
const handleSize = handleRect.width();
908903

909904
let handleX = x;
910905
let handleY = y;
@@ -921,17 +916,17 @@ export class Editor {
921916
handleY += height / 2;
922917
}
923918

924-
handle.x(handleX - handleSize / 2);
925-
handle.y(handleY - handleSize / 2);
919+
handleRect.x(handleX - handleSize / 2);
920+
handleRect.y(handleY - handleSize / 2);
926921
};
927922

928923
private updateHandlePositions = () => {
929924
if (!this.konva) {
930925
return;
931926
}
932927

933-
for (const handle of Object.values(this.konva.crop.interaction.handles)) {
934-
this.positionHandle(handle);
928+
for (const [handleName, handleRect] of objectEntries(this.konva.crop.interaction.handles)) {
929+
this.positionHandle(handleName, handleRect);
935930
}
936931
};
937932

0 commit comments

Comments
 (0)