@@ -2,6 +2,7 @@ import { $crossOrigin } from 'app/store/nanostores/authToken';
2
2
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern' ;
3
3
import Konva from 'konva' ;
4
4
import type { KonvaEventObject } from 'konva/lib/Node' ;
5
+ import { objectEntries } from 'tsafe' ;
5
6
6
7
type CropConstraints = {
7
8
minWidth ?: number ;
@@ -29,18 +30,8 @@ type EditorCallbacks = {
29
30
} ;
30
31
31
32
type HandleName = 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left' | 'top' | 'right' | 'bottom' | 'left' ;
32
- type GuideName = 'left' | 'right' | 'top' | 'bottom' ;
33
33
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' ;
44
35
45
36
type KonvaObjects = {
46
37
stage : Konva . Stage ;
@@ -96,8 +87,6 @@ export class Editor {
96
87
private readonly CROP_HANDLE_STROKE = 'black' ;
97
88
private readonly FIT_TO_CONTAINER_PADDING = 0.9 ;
98
89
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' ] ;
101
90
102
91
// Configuration
103
92
private readonly ZOOM_MIN = 0.1 ;
@@ -391,7 +380,7 @@ export class Editor {
391
380
392
381
// Handle dragging
393
382
rect . on ( 'dragmove' , ( ) => {
394
- this . resizeCropBox ( rect ) ;
383
+ this . resizeCropBox ( name , rect ) ;
395
384
} ) ;
396
385
397
386
return rect ;
@@ -687,14 +676,14 @@ export class Editor {
687
676
this . callbacks . onCropStart ?.( ) ;
688
677
} ;
689
678
690
- private resizeCropBox = ( handle : Konva . Rect ) => {
679
+ private resizeCropBox = ( handleName : HandleName , handleRect : Konva . Rect ) => {
691
680
if ( ! this . konva ) {
692
681
return ;
693
682
}
694
683
695
684
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 ) ;
698
687
699
688
// Apply general constraints
700
689
if ( this . cropConstraints . maxWidth ) {
@@ -712,12 +701,11 @@ export class Editor {
712
701
} ) ;
713
702
} ;
714
703
715
- private _resizeCropBoxFree = ( handle : Konva . Rect ) => {
704
+ private _resizeCropBoxFree = ( handleName : HandleName , handleRect : Konva . Rect ) => {
716
705
if ( ! this . konva ?. image . image ) {
717
706
throw new Error ( 'Crop box or image not found' ) ;
718
707
}
719
708
const rect = this . konva . crop . overlay . clear ;
720
- const handleName = handle . name ( ) ;
721
709
const imgWidth = this . konva . image . image . width ( ) ;
722
710
const imgHeight = this . konva . image . image . height ( ) ;
723
711
@@ -726,8 +714,8 @@ export class Editor {
726
714
let newWidth = rect . width ( ) ;
727
715
let newHeight = rect . height ( ) ;
728
716
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 ;
731
719
732
720
const minWidth = this . cropConstraints . minWidth ?? this . MIN_CROP_DIMENSION ;
733
721
const minHeight = this . cropConstraints . minHeight ?? this . MIN_CROP_DIMENSION ;
@@ -753,47 +741,56 @@ export class Editor {
753
741
return { newX, newY, newWidth, newHeight } ;
754
742
} ;
755
743
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 ) {
758
746
throw new Error ( 'Crop box, image, or aspect ratio not found' ) ;
759
747
}
760
- const rect = this . konva . crop . overlay . clear ;
761
- const handleName = handle . name ( ) ;
762
748
const imgWidth = this . konva . image . image . width ( ) ;
763
749
const imgHeight = this . konva . image . image . height ( ) ;
764
750
const ratio = this . cropConstraints . aspectRatio ;
765
751
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 ;
768
754
769
755
const minWidth = this . cropConstraints . minWidth ?? this . MIN_CROP_DIMENSION ;
770
756
const minHeight = this . cropConstraints . minHeight ?? this . MIN_CROP_DIMENSION ;
771
757
772
758
// 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 ;
777
763
778
764
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 )
783
769
) {
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
+ } ;
785
776
}
786
777
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
+
788
785
let newX = freeX ;
789
786
let newY = freeY ;
790
787
let newWidth = freeWidth ;
791
788
let newHeight = freeHeight ;
792
789
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 ;
797
794
798
795
// Define anchor points (opposite of the handle being dragged)
799
796
let anchorX = oldX ;
@@ -815,10 +812,8 @@ export class Editor {
815
812
anchorY = oldY + oldHeight / 2 ; // Center Y is anchor for left/right
816
813
}
817
814
818
- const isCornerHandle = this . CORNER_HANDLE_NAMES . includes ( handleName ) ;
819
-
820
815
// 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' ) {
822
817
if ( handleName === 'left' || handleName === 'right' ) {
823
818
newHeight = newWidth / ratio ;
824
819
newY = anchorY - newHeight / 2 ;
@@ -827,7 +822,8 @@ export class Editor {
827
822
newWidth = newHeight * ratio ;
828
823
newX = anchorX - newWidth / 2 ;
829
824
}
830
- } else if ( isCornerHandle ) {
825
+ } else {
826
+ // Corner handles
831
827
const mouseDistanceFromAnchorX = Math . abs ( handleX - anchorX ) ;
832
828
const mouseDistanceFromAnchorY = Math . abs ( handleY - anchorY ) ;
833
829
@@ -897,14 +893,13 @@ export class Editor {
897
893
return { newX, newY, newWidth, newHeight } ;
898
894
} ;
899
895
900
- private positionHandle = ( handle : Konva . Rect ) => {
896
+ private positionHandle = ( handleName : HandleName , handleRect : Konva . Rect ) => {
901
897
if ( ! this . konva || ! this . cropBox ) {
902
898
return ;
903
899
}
904
900
905
901
const { x, y, width, height } = this . cropBox ;
906
- const handleName = handle . name ( ) ;
907
- const handleSize = handle . width ( ) ;
902
+ const handleSize = handleRect . width ( ) ;
908
903
909
904
let handleX = x ;
910
905
let handleY = y ;
@@ -921,17 +916,17 @@ export class Editor {
921
916
handleY += height / 2 ;
922
917
}
923
918
924
- handle . x ( handleX - handleSize / 2 ) ;
925
- handle . y ( handleY - handleSize / 2 ) ;
919
+ handleRect . x ( handleX - handleSize / 2 ) ;
920
+ handleRect . y ( handleY - handleSize / 2 ) ;
926
921
} ;
927
922
928
923
private updateHandlePositions = ( ) => {
929
924
if ( ! this . konva ) {
930
925
return ;
931
926
}
932
927
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 ) ;
935
930
}
936
931
} ;
937
932
0 commit comments