@@ -13,6 +13,7 @@ const SHAPE_TYPE = {
1313 RECTANGLE : "rectangle" ,
1414 LINE : "line" ,
1515 PEN : "pen" ,
16+ CIRCLE : "circle" ,
1617} ;
1718
1819export const Canvas = ( ) => {
@@ -80,24 +81,37 @@ export const Canvas = () => {
8081 } ;
8182
8283 // Compute bounding box for a shape (world coords)
83- const getShapeBBox = useCallback ( ( shape ) => {
84- if ( ! shape ) return null ;
85- if ( shape . type === SHAPE_TYPE . RECTANGLE || shape . type === SHAPE_TYPE . LINE ) {
86- const minX = Math . min ( shape . start . x , shape . end . x ) ;
87- const maxX = Math . max ( shape . start . x , shape . end . x ) ;
88- const minY = Math . min ( shape . start . y , shape . end . y ) ;
89- const maxY = Math . max ( shape . start . y , shape . end . y ) ;
90- return { minX, minY, maxX, maxY, width : maxX - minX , height : maxY - minY } ;
91- }
92- if ( shape . type === SHAPE_TYPE . PEN && shape . path && shape . path . length ) {
93- const minX = Math . min ( ...shape . path . map ( p => p . x ) ) ;
94- const maxX = Math . max ( ...shape . path . map ( p => p . x ) ) ;
95- const minY = Math . min ( ...shape . path . map ( p => p . y ) ) ;
96- const maxY = Math . max ( ...shape . path . map ( p => p . y ) ) ;
97- return { minX, minY, maxX, maxY, width : maxX - minX , height : maxY - minY } ;
98- }
99- return null ;
100- } , [ ] ) ;
84+ const getShapeBBox = useCallback ( ( shape ) => {
85+ if ( ! shape ) return null ;
86+
87+ if ( shape . type === SHAPE_TYPE . RECTANGLE || shape . type === SHAPE_TYPE . LINE ) {
88+ const minX = Math . min ( shape . start . x , shape . end . x ) ;
89+ const maxX = Math . max ( shape . start . x , shape . end . x ) ;
90+ const minY = Math . min ( shape . start . y , shape . end . y ) ;
91+ const maxY = Math . max ( shape . start . y , shape . end . y ) ;
92+ return { minX, minY, maxX, maxY, width : maxX - minX , height : maxY - minY } ;
93+ }
94+
95+ if ( shape . type === SHAPE_TYPE . CIRCLE ) {
96+ const r = Math . max ( shape . radius || 0 , 0 ) ;
97+ const minX = shape . start . x - r ;
98+ const maxX = shape . start . x + r ;
99+ const minY = shape . start . y - r ;
100+ const maxY = shape . start . y + r ;
101+ return { minX, minY, maxX, maxY, width : maxX - minX , height : maxY - minY } ;
102+ }
103+
104+ if ( shape . type === SHAPE_TYPE . PEN && shape . path && shape . path . length ) {
105+ const minX = Math . min ( ...shape . path . map ( p => p . x ) ) ;
106+ const maxX = Math . max ( ...shape . path . map ( p => p . x ) ) ;
107+ const minY = Math . min ( ...shape . path . map ( p => p . y ) ) ;
108+ const maxY = Math . max ( ...shape . path . map ( p => p . y ) ) ;
109+ return { minX, minY, maxX, maxY, width : maxX - minX , height : maxY - minY } ;
110+ }
111+
112+ return null ;
113+ } , [ ] ) ;
114+
101115
102116 // Returns array of handle objects: { x, y, dir }
103117 const getHandlesForShape = useCallback (
@@ -137,132 +151,151 @@ export const Canvas = () => {
137151 } ;
138152
139153 // --- Drawing Utilities ---
140- const drawShape = useCallback ( ( ctx , shape , isSelected = false ) => {
141- ctx . save ( ) ;
142- ctx . beginPath ( ) ;
143- ctx . strokeStyle = shape . color ;
144- ctx . lineWidth = shape . width ;
145- ctx . lineCap = "round" ;
146- ctx . lineJoin = "round" ;
147- ctx . setLineDash ( [ ] ) ;
148- ctx . globalAlpha = 1 ;
149-
150- if ( isSelected ) {
154+ const drawShape = useCallback ( ( ctx , shape , isSelected = false ) => {
155+ ctx . save ( ) ;
156+ ctx . beginPath ( ) ;
157+ ctx . strokeStyle = shape . color ;
158+ ctx . lineWidth = shape . width ;
159+ ctx . lineCap = "round" ;
160+ ctx . lineJoin = "round" ;
161+ ctx . setLineDash ( [ ] ) ;
162+ ctx . globalAlpha = 1 ;
163+
164+ if ( isSelected ) {
151165 // draw purple glow under the stroke for visibility
152- ctx . save ( ) ;
153- ctx . lineWidth = shape . width + 4 ;
154- ctx . strokeStyle = "rgba(76,29,149,1)" ;
155- switch ( shape . type ) {
156- case SHAPE_TYPE . LINE :
157- ctx . beginPath ( ) ;
158- ctx . moveTo ( shape . start . x , shape . start . y ) ;
159- ctx . lineTo ( shape . end . x , shape . end . y ) ;
160- ctx . stroke ( ) ;
161- break ;
162- case SHAPE_TYPE . RECTANGLE :
163- ctx . strokeRect ( shape . start . x , shape . start . y , shape . end . x - shape . start . x , shape . end . y - shape . start . y ) ;
164- break ;
165- case SHAPE_TYPE . PEN :
166- if ( shape . path && shape . path . length > 1 ) {
167- ctx . beginPath ( ) ;
168- ctx . moveTo ( shape . path [ 0 ] . x , shape . path [ 0 ] . y ) ;
169- shape . path . forEach ( ( p ) => ctx . lineTo ( p . x , p . y ) ) ;
170- ctx . stroke ( ) ;
171- }
172- break ;
173- default :
174- break ;
175- }
176- ctx . restore ( ) ;
177-
178- // then draw the actual shape on top
179- ctx . strokeStyle = shape . color ;
180- ctx . lineWidth = shape . width ;
181- }
182-
166+ ctx . save ( ) ;
167+ ctx . lineWidth = shape . width + 4 ;
168+ ctx . strokeStyle = "rgba(76,29,149,1)" ;
183169 switch ( shape . type ) {
184170 case SHAPE_TYPE . LINE :
185171 ctx . beginPath ( ) ;
186172 ctx . moveTo ( shape . start . x , shape . start . y ) ;
187173 ctx . lineTo ( shape . end . x , shape . end . y ) ;
188174 ctx . stroke ( ) ;
189175 break ;
190- case SHAPE_TYPE . RECTANGLE : {
191- const x = shape . start . x ;
192- const y = shape . start . y ;
193- const width = shape . end . x - shape . start . x ;
194- const height = shape . end . y - shape . start . y ;
176+ case SHAPE_TYPE . RECTANGLE :
177+ ctx . strokeRect (
178+ shape . start . x ,
179+ shape . start . y ,
180+ shape . end . x - shape . start . x ,
181+ shape . end . y - shape . start . y
182+ ) ;
183+ break ;
184+ case SHAPE_TYPE . CIRCLE :
195185 ctx . beginPath ( ) ;
196- ctx . strokeRect ( x , y , width , height ) ;
186+ ctx . arc ( shape . start . x , shape . start . y , shape . radius || 0 , 0 , Math . PI * 2 ) ;
187+ ctx . stroke ( ) ;
197188 break ;
198- }
199189 case SHAPE_TYPE . PEN :
200190 if ( shape . path && shape . path . length > 1 ) {
201- const brush = shape . brush || "solid" ;
202-
203- const drawPath = ( offsetJitter = 0 ) => {
204- ctx . beginPath ( ) ;
205- ctx . moveTo (
206- shape . path [ 0 ] . x + ( Math . random ( ) - 0.5 ) * offsetJitter ,
207- shape . path [ 0 ] . y + ( Math . random ( ) - 0.5 ) * offsetJitter
208- ) ;
209- shape . path . forEach ( ( p ) =>
210- ctx . lineTo (
211- p . x + ( Math . random ( ) - 0.5 ) * offsetJitter ,
212- p . y + ( Math . random ( ) - 0.5 ) * offsetJitter
213- )
214- ) ;
215- ctx . stroke ( ) ;
216- } ;
217-
218- // Reset any brush-specific state first
219- ctx . setLineDash ( [ ] ) ;
220- ctx . shadowBlur = 0 ;
221- ctx . globalAlpha = 1 ;
191+ ctx . beginPath ( ) ;
192+ ctx . moveTo ( shape . path [ 0 ] . x , shape . path [ 0 ] . y ) ;
193+ shape . path . forEach ( ( p ) => ctx . lineTo ( p . x , p . y ) ) ;
194+ ctx . stroke ( ) ;
195+ }
196+ break ;
197+ default :
198+ break ;
199+ }
200+ ctx . restore ( ) ;
222201
223- if ( brush === "dashed" ) {
224- const base = Math . max ( 4 , shape . width * 3 ) ;
225- const dash = Math . round ( base ) ;
226- const gap = Math . round ( base * 0.6 ) ;
227- ctx . setLineDash ( [ dash , gap ] ) ;
228- ctx . lineWidth = shape . width ;
229- ctx . strokeStyle = shape . color ;
230- drawPath ( 0 ) ;
231- ctx . setLineDash ( [ ] ) ;
232- } else if ( brush === 'paint' ) {
233- ctx . lineCap = "round" ;
234- ctx . lineJoin = "round" ;
202+ // then draw the actual shape on top
203+ ctx . strokeStyle = shape . color ;
204+ ctx . lineWidth = shape . width ;
205+ }
235206
236- const baseWidth = Math . max ( shape . width , 1.5 ) ; // Ensure a minimum body
237- const layers = 8 ;
207+ switch ( shape . type ) {
208+ case SHAPE_TYPE . LINE :
209+ ctx . beginPath ( ) ;
210+ ctx . moveTo ( shape . start . x , shape . start . y ) ;
211+ ctx . lineTo ( shape . end . x , shape . end . y ) ;
212+ ctx . stroke ( ) ;
213+ break ;
238214
239- for ( let i = 0 ; i < layers ; i ++ ) {
240- const opacity = 0.18 + Math . random ( ) * 0.12 ;
241- const color = tinycolor ( shape . color )
242- . brighten ( ( Math . random ( ) - 0.5 ) * 2.5 )
243- . setAlpha ( opacity )
244- . toRgbString ( ) ;
215+ case SHAPE_TYPE . RECTANGLE : {
216+ const x = shape . start . x ;
217+ const y = shape . start . y ;
218+ const width = shape . end . x - shape . start . x ;
219+ const height = shape . end . y - shape . start . y ;
220+ ctx . beginPath ( ) ;
221+ ctx . strokeRect ( x , y , width , height ) ;
222+ break ;
223+ }
245224
246- ctx . strokeStyle = color ;
247- ctx . globalAlpha = 0.9 ;
248- const widthFactor = baseWidth < 4 ? 3.8 : 2.2 ;
249- ctx . lineWidth = baseWidth * ( widthFactor + i * 0.2 ) ;
225+ case SHAPE_TYPE . CIRCLE : {
226+ const r = Math . max ( shape . radius || 0 , 0 ) ;
227+ ctx . beginPath ( ) ;
228+ ctx . arc ( shape . start . x , shape . start . y , r , 0 , Math . PI * 2 ) ;
229+ ctx . stroke ( ) ;
230+ break ;
231+ }
250232
251- drawPath ( 0 ) ;
252- }
233+ case SHAPE_TYPE . PEN :
234+ if ( shape . path && shape . path . length > 1 ) {
235+ const brush = shape . brush || "solid" ;
253236
254- ctx . globalAlpha = 0.25 ;
255- ctx . lineWidth = baseWidth * ( baseWidth < 4 ? 4.8 : 3.2 ) ;
256- ctx . strokeStyle = tinycolor ( shape . color )
257- . lighten ( 3 )
258- . setAlpha ( 0.25 )
237+ const drawPath = ( offsetJitter = 0 ) => {
238+ ctx . beginPath ( ) ;
239+ ctx . moveTo (
240+ shape . path [ 0 ] . x + ( Math . random ( ) - 0.5 ) * offsetJitter ,
241+ shape . path [ 0 ] . y + ( Math . random ( ) - 0.5 ) * offsetJitter
242+ ) ;
243+ shape . path . forEach ( ( p ) =>
244+ ctx . lineTo (
245+ p . x + ( Math . random ( ) - 0.5 ) * offsetJitter ,
246+ p . y + ( Math . random ( ) - 0.5 ) * offsetJitter
247+ )
248+ ) ;
249+ ctx . stroke ( ) ;
250+ } ;
251+
252+ // Reset brush states
253+ ctx . setLineDash ( [ ] ) ;
254+ ctx . shadowBlur = 0 ;
255+ ctx . globalAlpha = 1 ;
256+
257+ if ( brush === "dashed" ) {
258+ const base = Math . max ( 4 , shape . width * 3 ) ;
259+ const dash = Math . round ( base ) ;
260+ const gap = Math . round ( base * 0.6 ) ;
261+ ctx . setLineDash ( [ dash , gap ] ) ;
262+ ctx . lineWidth = shape . width ;
263+ ctx . strokeStyle = shape . color ;
264+ drawPath ( 0 ) ;
265+ ctx . setLineDash ( [ ] ) ;
266+ } else if ( brush === "paint" ) {
267+ ctx . lineCap = "round" ;
268+ ctx . lineJoin = "round" ;
269+ const baseWidth = Math . max ( shape . width , 1.5 ) ;
270+ const layers = 8 ;
271+
272+ for ( let i = 0 ; i < layers ; i ++ ) {
273+ const opacity = 0.18 + Math . random ( ) * 0.12 ;
274+ const color = tinycolor ( shape . color )
275+ . brighten ( ( Math . random ( ) - 0.5 ) * 2.5 )
276+ . setAlpha ( opacity )
259277 . toRgbString ( ) ;
260- drawPath ( 0 ) ;
261278
262- ctx . globalAlpha = 0.95 ;
263- ctx . lineWidth = baseWidth * ( baseWidth < 4 ? 3.4 : 2.4 ) ;
264- ctx . strokeStyle = shape . color ;
279+ ctx . strokeStyle = color ;
280+ ctx . globalAlpha = 0.9 ;
281+ const widthFactor = baseWidth < 4 ? 3.8 : 2.2 ;
282+ ctx . lineWidth = baseWidth * ( widthFactor + i * 0.2 ) ;
283+
265284 drawPath ( 0 ) ;
285+ }
286+
287+ ctx . globalAlpha = 0.25 ;
288+ ctx . lineWidth = baseWidth * ( baseWidth < 4 ? 4.8 : 3.2 ) ;
289+ ctx . strokeStyle = tinycolor ( shape . color )
290+ . lighten ( 3 )
291+ . setAlpha ( 0.25 )
292+ . toRgbString ( ) ;
293+ drawPath ( 0 ) ;
294+
295+ ctx . globalAlpha = 0.95 ;
296+ ctx . lineWidth = baseWidth * ( baseWidth < 4 ? 3.4 : 2.4 ) ;
297+ ctx . strokeStyle = shape . color ;
298+ drawPath ( 0 ) ;
266299
267300 ctx . globalAlpha = 1 ;
268301 ctx . lineWidth = shape . width ;
@@ -593,6 +626,9 @@ export const Canvas = () => {
593626 newShape . brush = brushType || "solid" ;
594627 newShape . _seed = Math . floor ( Math . random ( ) * 0xffffffff ) ;
595628 }
629+ if ( activeTool === SHAPE_TYPE . CIRCLE ) {
630+ newShape . radius = 0 ;
631+ }
596632 newShapeId . current = newShape . id ;
597633 setShapes ( ( prev ) => [ ...prev , newShape ] ) ;
598634 } else {
@@ -625,7 +661,7 @@ export const Canvas = () => {
625661 return ;
626662 }
627663
628- if ( ! isDrawing ) return ;
664+ if ( ! isDrawing ) return ;
629665
630666 // MOVE
631667 if ( activeTool === 'select' && selectedShapeId && manipulationMode . current && manipulationMode . current . mode === 'move' ) {
@@ -709,7 +745,19 @@ export const Canvas = () => {
709745 sh . start = { x : nx , y : ny } ;
710746 sh . end = { x : nx + nw , y : ny + nh } ;
711747 }
712-
748+ if ( sh . type === SHAPE_TYPE . CIRCLE ) {
749+ // Resize based on bounding box change
750+ const cx = ( minX + maxX ) / 2 ;
751+ const cy = ( minY + maxY ) / 2 ;
752+ const newRadius = Math . max (
753+ Math . abs ( maxX - minX ) ,
754+ Math . abs ( maxY - minY )
755+ ) / 2 ;
756+
757+ sh . start = { x : cx , y : cy } ;
758+ sh . radius = newRadius ;
759+ sh . end = { x : cx + newRadius , y : cy } ; // optional
760+ }
713761 newShapes [ shapeIndex ] = sh ;
714762 setShapes ( newShapes ) ;
715763 return ;
@@ -732,6 +780,15 @@ export const Canvas = () => {
732780 } else if ( cur . type === SHAPE_TYPE . LINE || cur . type === SHAPE_TYPE . RECTANGLE ) {
733781 cur . end = worldPoint ;
734782 }
783+ else if ( cur . type === SHAPE_TYPE . CIRCLE ) {
784+ // Circle creation: start = center, drag defines radius
785+ const dx = worldPoint . x - cur . start . x ;
786+ const dy = worldPoint . y - cur . start . y ;
787+ const r = Math . sqrt ( dx * dx + dy * dy ) ;
788+ cur . radius = r ;
789+ cur . end = worldPoint ; // optional for reference
790+ }
791+
735792 return newShapes ;
736793 } ) ;
737794 }
0 commit comments