1
- import React , { useRef , useState } from "react" ;
1
+ import React , { useEffect , useRef , useState } from "react" ;
2
2
import styled from "@emotion/styled" ;
3
- import { usePinch } from "@use-gesture/react" ;
3
+ import { useGesture } from "@use-gesture/react" ;
4
4
import { Resizable } from "re-resizable" ;
5
5
import { ZoomControl } from "./controller-zoom-control" ;
6
6
7
+ /**
8
+ * A React Hook that returns a delta state.
9
+ * When user completely stops interacting, after a short delay (600ms), set the value to false.
10
+ * When user starts interacting, immidiately set the value to true.
11
+ *
12
+ * the condition rather if the user is currently interacting or not is set on higher level, which this function accepts the condition as a parameter.
13
+ * @param interacting
14
+ */
15
+ function useIsInteractingDelta ( interacting : boolean ) {
16
+ throw new Error ( "Not implemented" ) ;
17
+ }
18
+
7
19
export function InteractiveCanvas ( {
8
20
children,
9
21
defaultSize,
10
22
} : {
11
23
defaultSize : { width : number ; height : number } ;
12
24
children ?: React . ReactNode ;
13
25
} ) {
14
- const [ scale , setScale ] = useState ( 1 ) ;
26
+ const __canvas_width = 800 ;
27
+ const __canvas_height = 900 ;
28
+ const __margin = 20 ;
29
+ const __y_start =
30
+ defaultSize . height < __canvas_height - __margin * 2
31
+ ? ( __canvas_height - defaultSize . height ) / 2
32
+ : __margin ;
33
+ const __initial_xy = [ 0 , __y_start ] as [ number , number ] ;
34
+ const __initial_scale =
35
+ defaultSize . width > __canvas_width
36
+ ? ( __canvas_width - __margin * 2 ) / defaultSize . width
37
+ : 1 ;
38
+
39
+ const [ scale , setScale ] = useState ( __initial_scale ) ;
40
+ const [ xy , setXY ] = useState < [ number , number ] > ( __initial_xy ) ;
41
+
42
+ const [ isPanning , setIsPanning ] = useState ( false ) ;
43
+ const [ isZooming , setIsZooming ] = useState ( false ) ;
44
+ const isDeltaInteracting = isPanning || isZooming ;
45
+
46
+ const ref = useRef ( ) ;
47
+
48
+ useGesture (
49
+ {
50
+ onPinch : ( state ) => {
51
+ setIsZooming ( state . pinching ) ;
52
+ setScale ( Math . max ( scale + state . delta [ 0 ] , 0.1 ) ) ;
53
+ } ,
54
+ onWheel : ( { delta : [ x , y ] , wheeling } ) => {
55
+ setIsPanning ( wheeling ) ;
56
+ setXY ( [ xy [ 0 ] - x / scale , xy [ 1 ] - y / scale ] ) ;
57
+ } ,
58
+ } ,
59
+ { target : ref }
60
+ ) ;
15
61
16
62
return (
17
63
< InteractiveCanvasWrapper id = "interactive-canvas" >
18
- < ScalableFrame onRescale = { setScale } scale = { scale } >
64
+ < div
65
+ id = "event-listener"
66
+ ref = { ref }
67
+ style = { {
68
+ flexGrow : 1 ,
69
+ display : "flex" ,
70
+ flexDirection : "column" ,
71
+ alignItems : "center" ,
72
+ } }
73
+ >
19
74
< Controls >
20
- < ZoomControl scale = { scale } onChange = { setScale } />
75
+ < ZoomControl
76
+ onReset = { ( ) => {
77
+ setScale ( __initial_scale ) ;
78
+ setXY ( __initial_xy ) ;
79
+ } }
80
+ scale = { scale }
81
+ onChange = { setScale }
82
+ />
21
83
</ Controls >
22
- < ScalingAreaStaticRoot >
23
- < ScalingArea scale = { scale } >
24
- < ResizableFrame defaultSize = { defaultSize } scale = { scale } >
25
- { children }
26
- </ ResizableFrame >
27
- </ ScalingArea >
28
- </ ScalingAreaStaticRoot >
29
- </ ScalableFrame >
84
+ { /* <ScalingAreaStaticRoot> */ }
85
+ < TransformContainer
86
+ scale = { scale }
87
+ xy = { xy }
88
+ isTransitioning = { isDeltaInteracting }
89
+ >
90
+ < ResizableFrame defaultSize = { defaultSize } scale = { scale } >
91
+ { children }
92
+ </ ResizableFrame >
93
+ </ TransformContainer >
94
+ { /* </ScalingAreaStaticRoot> */ }
95
+ </ div >
30
96
</ InteractiveCanvasWrapper >
31
97
) ;
32
98
}
33
99
34
100
const InteractiveCanvasWrapper = styled . div `
35
101
display: flex;
36
102
flex-direction: column;
37
- /* overflow-y: auto; */
38
- overflow-x: hidden;
39
- flex: 1;
103
+ overflow: hidden;
104
+ flex-grow: 1;
40
105
` ;
41
106
42
107
const Controls = styled . div `
@@ -46,67 +111,23 @@ const Controls = styled.div`
46
111
justify-content: flex-end;
47
112
` ;
48
113
49
- const ScalingAreaStaticRoot = styled . div `
50
- display: flex;
51
- align-items: flex-start; // when transform origin is top center.
52
- padding-top: 20px;
53
- justify-content: center;
54
- align-content: flex-start;
55
- align-self: stretch;
56
- flex: 1;
57
- max-height: 100vh; // TODO: make dynamic
58
- ` ;
59
-
60
- function ScalableFrame ( {
61
- children,
62
- scale,
63
- onRescale,
64
- } : {
65
- scale : number ;
66
- onRescale ?: ( scale : number ) => void ;
67
- children ?: React . ReactNode ;
68
- } ) {
69
- const ref = useRef ( ) ;
70
-
71
- usePinch (
72
- ( state ) => {
73
- const prevscale = scale ;
74
- const { offset } = state ;
75
- const thisscale = offset [ 0 ] ;
76
- // const newscale = thisscale - prevscale;
77
- onRescale ( thisscale ) ;
78
- } ,
79
- { target : ref }
80
- ) ;
81
-
82
- return (
83
- < div
84
- id = "scale-event-listener"
85
- ref = { ref }
86
- style = { {
87
- display : "flex" ,
88
- flexDirection : "column" ,
89
- flex : 1 ,
90
- alignItems : "center" ,
91
- alignContent : "center" ,
92
- } }
93
- >
94
- { children }
95
- </ div >
96
- ) ;
97
- }
98
-
99
- const ScalingArea = ( {
114
+ const TransformContainer = ( {
100
115
scale,
101
116
children,
117
+ xy,
118
+ isTransitioning,
102
119
} : {
103
120
scale : number ;
121
+ xy : [ number , number ] ;
122
+ isTransitioning : boolean ;
104
123
children : React . ReactNode ;
105
124
} ) => {
106
125
return (
107
126
< div
108
127
style = { {
109
- transform : `scale(${ scale } )` ,
128
+ pointerEvents : isTransitioning ? "none" : undefined ,
129
+ transform : `scale(${ scale } ) translateX(${ xy [ 0 ] } px) translateY(${ xy [ 1 ] } px)` ,
130
+ willChange : "transform" ,
110
131
transformOrigin : "top center" ,
111
132
} }
112
133
>
0 commit comments