1
+ import React , { useRef , useState , useEffect } from 'react' ;
2
+ import { useStore } from '@/context/AnnotationStore' ;
3
+ import * as d3 from 'd3' ;
4
+ import { Annotation } from '@/types/Annotation' ;
5
+
6
+ export const Canvas = ( ) => {
7
+ const svgRef = useRef < SVGSVGElement > ( null ) ;
8
+ const imgRef = useRef < HTMLImageElement > ( null ) ;
9
+ const {
10
+ image,
11
+ annotations,
12
+ selectedTool,
13
+ selectedAnnotation,
14
+ addAnnotation,
15
+ updateAnnotation,
16
+ presets,
17
+ } = useStore ( ) ;
18
+ const [ drawing , setDrawing ] = useState ( false ) ;
19
+ const [ tempAnnotation , setTempAnnotation ] = useState < Partial < Annotation > | null > (
20
+ null
21
+ ) ;
22
+
23
+ useEffect ( ( ) => {
24
+ if ( ! svgRef . current || ! image ) return ;
25
+
26
+ const svg = d3 . select ( svgRef . current ) ;
27
+ svg . selectAll ( '*' ) . remove ( ) ;
28
+
29
+ // Create a group for the image
30
+ const imageGroup = svg . append ( 'g' ) . attr ( 'class' , 'image-layer' ) ;
31
+
32
+ // Add image
33
+ imageGroup
34
+ . append ( 'image' )
35
+ . attr ( 'href' , image )
36
+ . attr ( 'width' , '100%' )
37
+ . attr ( 'height' , '100%' )
38
+ . attr ( 'preserveAspectRatio' , 'xMidYMid meet' ) ;
39
+
40
+ // Create a group for annotations
41
+ const annotationGroup = svg . append ( 'g' ) . attr ( 'class' , 'annotation-layer' ) ;
42
+
43
+ // Render existing annotations
44
+ annotations . forEach ( ( ann ) => {
45
+ const group = annotationGroup . append ( 'g' ) ;
46
+
47
+ switch ( ann . type ) {
48
+ case 'rectangle' :
49
+ group
50
+ . append ( 'rect' )
51
+ . attr ( 'x' , ann . x )
52
+ . attr ( 'y' , ann . y )
53
+ . attr ( 'width' , ann . width )
54
+ . attr ( 'height' , ann . height )
55
+ . attr ( 'fill' , 'none' )
56
+ . attr ( 'stroke' , ann . color )
57
+ . attr ( 'stroke-width' , 2 )
58
+ . attr ( 'data-id' , ann . id ) ;
59
+ break ;
60
+ case 'circle' :
61
+ group
62
+ . append ( 'circle' )
63
+ . attr ( 'cx' , ann . x )
64
+ . attr ( 'cy' , ann . y )
65
+ . attr ( 'r' , ann . radius )
66
+ . attr ( 'fill' , 'none' )
67
+ . attr ( 'stroke' , ann . color )
68
+ . attr ( 'stroke-width' , 2 )
69
+ . attr ( 'data-id' , ann . id ) ;
70
+ break ;
71
+ case 'text' :
72
+ group
73
+ . append ( 'text' )
74
+ . attr ( 'x' , ann . x )
75
+ . attr ( 'y' , ann . y )
76
+ . text ( ann . text )
77
+ . attr ( 'fill' , ann . color )
78
+ . attr ( 'data-id' , ann . id ) ;
79
+ break ;
80
+ case 'freehand' :
81
+ group
82
+ . append ( 'path' )
83
+ . attr ( 'd' , ann . path )
84
+ . attr ( 'fill' , 'none' )
85
+ . attr ( 'stroke' , ann . color )
86
+ . attr ( 'stroke-width' , 2 )
87
+ . attr ( 'data-id' , ann . id ) ;
88
+ break ;
89
+ }
90
+
91
+ // Add labels
92
+ group
93
+ . append ( 'text' )
94
+ . attr ( 'x' , ann . x )
95
+ . attr ( 'y' , ann . y - 10 )
96
+ . text ( ann . label )
97
+ . attr ( 'fill' , ann . color )
98
+ . attr ( 'font-size' , '12px' ) ;
99
+ } ) ;
100
+
101
+ // Render temporary annotation while drawing
102
+ if ( tempAnnotation && drawing ) {
103
+ const tempGroup = annotationGroup . append ( 'g' ) . attr ( 'class' , 'temp' ) ;
104
+
105
+ switch ( tempAnnotation . type ) {
106
+ case 'rectangle' :
107
+ tempGroup
108
+ . append ( 'rect' )
109
+ . attr ( 'x' , tempAnnotation . x )
110
+ . attr ( 'y' , tempAnnotation . y )
111
+ . attr ( 'width' , tempAnnotation . width )
112
+ . attr ( 'height' , tempAnnotation . height )
113
+ . attr ( 'fill' , 'none' )
114
+ . attr ( 'stroke' , tempAnnotation . color )
115
+ . attr ( 'stroke-width' , 2 )
116
+ . attr ( 'stroke-dasharray' , '4' ) ;
117
+ break ;
118
+ case 'circle' :
119
+ tempGroup
120
+ . append ( 'circle' )
121
+ . attr ( 'cx' , tempAnnotation . x )
122
+ . attr ( 'cy' , tempAnnotation . y )
123
+ . attr ( 'r' , tempAnnotation . radius )
124
+ . attr ( 'fill' , 'none' )
125
+ . attr ( 'stroke' , tempAnnotation . color )
126
+ . attr ( 'stroke-width' , 2 )
127
+ . attr ( 'stroke-dasharray' , '4' ) ;
128
+ break ;
129
+ case 'freehand' :
130
+ tempGroup
131
+ . append ( 'path' )
132
+ . attr ( 'd' , tempAnnotation . path )
133
+ . attr ( 'fill' , 'none' )
134
+ . attr ( 'stroke' , tempAnnotation . color )
135
+ . attr ( 'stroke-width' , 2 ) ;
136
+ break ;
137
+ }
138
+ }
139
+ } , [ image , annotations , tempAnnotation , drawing ] ) ;
140
+
141
+ const handleMouseDown = ( e : React . MouseEvent ) => {
142
+ if ( ! selectedTool || ! image ) return ;
143
+
144
+ const svg = svgRef . current ;
145
+ if ( ! svg ) return ;
146
+
147
+ const point = d3 . pointer ( e . nativeEvent , svg ) ;
148
+ const newAnnotation : Partial < Annotation > = {
149
+ id : Date . now ( ) . toString ( ) ,
150
+ type : selectedTool ,
151
+ x : point [ 0 ] ,
152
+ y : point [ 1 ] ,
153
+ color : `#${ Math . floor ( Math . random ( ) * 16777215 ) . toString ( 16 ) } ` , // Fixed color assignment
154
+ } ;
155
+
156
+ setTempAnnotation ( newAnnotation ) ;
157
+ setDrawing ( true ) ;
158
+ } ;
159
+
160
+ const handleMouseMove = ( e : React . MouseEvent ) => {
161
+ if ( ! drawing || ! tempAnnotation || ! svgRef . current ) return ;
162
+
163
+ const point = d3 . pointer ( e . nativeEvent , svgRef . current ) ;
164
+ const updated = { ...tempAnnotation } ;
165
+
166
+ switch ( tempAnnotation . type ) {
167
+ case 'rectangle' :
168
+ updated . width = point [ 0 ] - tempAnnotation . x ! ;
169
+ updated . height = point [ 1 ] - tempAnnotation . y ! ;
170
+ break ;
171
+ case 'circle' :
172
+ const dx = point [ 0 ] - tempAnnotation . x ! ;
173
+ const dy = point [ 1 ] - tempAnnotation . y ! ;
174
+ updated . radius = Math . sqrt ( dx * dx + dy * dy ) ;
175
+ break ;
176
+ case 'freehand' :
177
+ const path = tempAnnotation . path || `M ${ tempAnnotation . x } ${ tempAnnotation . y } ` ;
178
+ updated . path = `${ path } L ${ point [ 0 ] } ${ point [ 1 ] } ` ;
179
+ break ;
180
+ }
181
+
182
+ setTempAnnotation ( updated ) ;
183
+ } ;
184
+
185
+ const handleMouseUp = ( ) => {
186
+ if ( ! drawing || ! tempAnnotation ) return ;
187
+
188
+ const label = prompt ( 'Enter label for this annotation:' ) ;
189
+ if ( label ) {
190
+ if ( tempAnnotation . type === 'text' ) {
191
+ const text = prompt ( 'Enter text:' ) ;
192
+ if ( text ) {
193
+ addAnnotation ( { ...tempAnnotation , text, label } as Annotation ) ;
194
+ }
195
+ } else {
196
+ addAnnotation ( { ...tempAnnotation , label } as Annotation ) ;
197
+ }
198
+ }
199
+
200
+ setDrawing ( false ) ;
201
+ setTempAnnotation ( null ) ;
202
+ } ;
203
+
204
+ return (
205
+ < svg
206
+ ref = { svgRef }
207
+ className = "w-full h-full border border-gray-300 rounded-lg"
208
+ onMouseDown = { handleMouseDown }
209
+ onMouseMove = { handleMouseMove }
210
+ onMouseUp = { handleMouseUp }
211
+ onMouseLeave = { handleMouseUp }
212
+ />
213
+ ) ;
214
+ } ;
0 commit comments