@@ -15,6 +15,7 @@ const SHAPE_TYPE = {
1515 PEN : "pen" ,
1616 CIRCLE : "circle" ,
1717 ERASER : "eraser" ,
18+ IMAGE : 'image' ,
1819} ;
1920
2021export const Canvas = ( ) => {
@@ -52,35 +53,32 @@ export const Canvas = () => {
5253 const [ hoveredHandle , setHoveredHandle ] = useState ( null ) ; // { id, dir }
5354
5455 const handleLogout = async ( ) => {
55- try {
56- const token = localStorage . getItem ( "token" ) ;
57- if ( ! token ) {
58- toast . error ( "You are not logged in." ) ;
59- return ;
60- }
56+ // placeholder
57+ } ;
6158
62- const res = await fetch ( "http://localhost:3000/api/auth/logout" , {
63- method : "POST" ,
64- headers : { "Content-Type" : "application/json" } ,
65- body : JSON . stringify ( { token } ) ,
66- } ) ;
59+ const handleImageUpload = ( file ) => {
60+ if ( ! file ) return ;
61+ const reader = new FileReader ( ) ;
62+ reader . onload = ( event ) => {
63+ const img = new Image ( ) ;
64+ img . onload = ( ) => {
65+ const newShape = {
66+ id : Date . now ( ) . toString ( ) ,
67+ type : SHAPE_TYPE . IMAGE ,
68+ image : img ,
69+ start : { x : 100 , y : 100 } ,
70+ end : { x : 100 + img . width , y : 100 + img . height } ,
71+ width : img . width ,
72+ height : img . height ,
73+ } ;
74+ setShapes ( ( prev ) => [ ...prev , newShape ] ) ;
75+ } ;
76+ img . src = event . target . result ;
77+ } ;
78+ reader . readAsDataURL ( file ) ;
79+ } ;
6780
68- if ( ! res . ok ) {
69- const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
70- toast . error ( data . message || "Logout failed" ) ;
71- return ;
72- }
7381
74- // Clear auth state and redirect
75- localStorage . removeItem ( "token" ) ;
76- setIsLoggedIn ( false ) ;
77- toast . success ( "Logged out successfully" ) ;
78- window . location . href = "/" ;
79- } catch ( err ) {
80- console . error ( "Logout error:" , err ) ;
81- toast . error ( "Logout failed. Please try again." ) ;
82- }
83- } ;
8482
8583 // --- Helpers ---
8684 const getWorldPoint = ( e ) => {
@@ -136,6 +134,14 @@ export const Canvas = () => {
136134 const maxY = Math . max ( ...shape . path . map ( p => p . y ) ) ;
137135 return { minX, minY, maxX, maxY, width : maxX - minX , height : maxY - minY } ;
138136 }
137+ if ( shape . type === SHAPE_TYPE . IMAGE ) {
138+ const minX = Math . min ( shape . start . x , shape . end . x ) ;
139+ const maxX = Math . max ( shape . start . x , shape . end . x ) ;
140+ const minY = Math . min ( shape . start . y , shape . end . y ) ;
141+ const maxY = Math . max ( shape . start . y , shape . end . y ) ;
142+ return { minX, minY, maxX, maxY, width : maxX - minX , height : maxY - minY } ;
143+ }
144+
139145
140146 return null ;
141147} , [ ] ) ;
@@ -192,7 +198,7 @@ export const Canvas = () => {
192198 if ( isSelected ) {
193199 // draw purple glow under the stroke for visibility
194200 ctx . save ( ) ;
195- ctx . lineWidth = shape . width + 4 ;
201+ ctx . lineWidth = shape . type === SHAPE_TYPE . IMAGE ? 4 : shape . width + 4 ;
196202 ctx . strokeStyle = "rgba(76,29,149,1)" ;
197203 switch ( shape . type ) {
198204 case SHAPE_TYPE . LINE :
@@ -245,7 +251,7 @@ export const Canvas = () => {
245251
246252 // then draw the actual shape on top
247253 ctx . strokeStyle = shape . color ;
248- ctx . lineWidth = shape . width ;
254+ ctx . lineWidth = shape . type === SHAPE_TYPE . IMAGE ? 1 : shape . width ;
249255 }
250256
251257 switch ( shape . type ) {
@@ -517,6 +523,15 @@ export const Canvas = () => {
517523 ctx . globalAlpha = 1 ;
518524 }
519525 break ;
526+ case SHAPE_TYPE . IMAGE : {
527+ const { image, start, end } = shape ;
528+ if ( image ) {
529+ const width = end . x - start . x ;
530+ const height = end . y - start . y ;
531+ ctx . drawImage ( image , start . x , start . y , width , height ) ;
532+ }
533+ break ;
534+ }
520535 default :
521536 break ;
522537 }
@@ -547,12 +562,15 @@ export const Canvas = () => {
547562 const ctx = canvas ?. getContext ( "2d" ) ;
548563 if ( ! ctx ) return ;
549564
550- // clear and reset transform
551- ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
552- ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
565+ // clear and reset transform
566+ ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
567+ ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
568+ ctx . globalCompositeOperation = "source-over" ;
569+ ctx . globalAlpha = 1 ;
570+ ctx . shadowBlur = 0 ;
553571
554- // apply pan & zoom
555- ctx . setTransform ( 1 , 0 , 0 , 1 , offset . x , offset . y ) ;
572+ // apply pan & zoom
573+ ctx . setTransform ( 1 , 0 , 0 , 1 , offset . x , offset . y ) ;
556574 ctx . scale ( scale , scale ) ;
557575
558576 // draw non-selected shapes first
@@ -622,18 +640,29 @@ export const Canvas = () => {
622640 if ( handle && hitShape && hitShape . id === selectedShapeId ) {
623641 // begin resize
624642 const origShape = JSON . parse ( JSON . stringify ( hitShape ) ) ;
643+ if ( hitShape . type === SHAPE_TYPE . IMAGE ) origShape . image = hitShape . image ;
625644 const origBBox = getShapeBBox ( origShape ) ;
626645 manipulationMode . current = { mode : 'resize' , dir : handle . dir , origShape, origBBox } ;
627646 setIsDrawing ( true ) ;
628647 return ;
629648 }
630649
631650 if ( hitShape ) {
651+ try {
652+ const _ctx = canvasRef . current ?. getContext ( '2d' ) ;
653+ if ( _ctx ) {
654+ _ctx . globalCompositeOperation = 'source-over' ;
655+ _ctx . globalAlpha = 1 ;
656+ _ctx . shadowBlur = 0 ;
657+ }
658+ } catch ( err ) {
659+ console . debug ( '[canvas] composite reset failed' , err ) ;
660+ }
632661 setSelectedShapeId ( hitShape . id ) ;
633662 setIsDrawing ( true ) ;
634- manipulationMode . current = { mode : "move" } ;
663+ manipulationMode . current = { mode : "pending- move" } ;
635664 setActiveColor ( hitShape . color ) ;
636- setStrokeWidth ( hitShape . width ) ;
665+ if ( hitShape . type === SHAPE_TYPE . PEN ) setStrokeWidth ( hitShape . width ) ;
637666 } else {
638667 setSelectedShapeId ( null ) ;
639668 setIsDrawing ( false ) ;
@@ -665,7 +694,7 @@ export const Canvas = () => {
665694
666695
667696 // creation tools
668- if ( Object . values ( SHAPE_TYPE ) . includes ( activeTool ) || activeTool . startsWith ( 'brush-' ) ) {
697+ if ( ( Object . values ( SHAPE_TYPE ) . includes ( activeTool ) || activeTool . startsWith ( 'brush-' ) ) && activeTool !== SHAPE_TYPE . IMAGE ) {
669698 setSelectedShapeId ( null ) ;
670699 setIsDrawing ( true ) ;
671700 manipulationMode . current = { mode : "create" } ;
@@ -686,6 +715,15 @@ export const Canvas = () => {
686715 newShape . brush = brushType || "solid" ;
687716 newShape . _seed = Math . floor ( Math . random ( ) * 0xffffffff ) ;
688717 }
718+ if ( activeTool === SHAPE_TYPE . IMAGE ) {
719+ const hitShape = shapes . slice ( ) . reverse ( ) . find ( ( shape ) => isPointInShape ( worldPoint , shape ) ) ;
720+ if ( hitShape && hitShape . type === SHAPE_TYPE . IMAGE ) {
721+ // Just select, don't draw a new one
722+ setSelectedShapeId ( hitShape . id ) ;
723+ setIsDrawing ( false ) ;
724+ return ;
725+ }
726+ }
689727 if ( activeTool === SHAPE_TYPE . CIRCLE ) {
690728 newShape . radius = 0 ;
691729 }
@@ -702,8 +740,13 @@ export const Canvas = () => {
702740 if ( ! shape ) return false ;
703741 const bbox = getShapeBBox ( shape ) ;
704742 if ( ! bbox ) return false ;
705- const tol = shape . width + 6 ;
706- return ( point . x >= bbox . minX - tol && point . x <= bbox . maxX + tol && point . y >= bbox . minY - tol && point . y <= bbox . maxY + tol ) ;
743+ const tol = shape . type === SHAPE_TYPE . IMAGE ? 8 : ( shape . width || 0 ) + 6 ;
744+ return (
745+ point . x >= bbox . minX - tol &&
746+ point . x <= bbox . maxX + tol &&
747+ point . y >= bbox . minY - tol &&
748+ point . y <= bbox . maxY + tol
749+ ) ;
707750 } ;
708751
709752 const draw = ( e ) => {
@@ -722,6 +765,24 @@ export const Canvas = () => {
722765 }
723766
724767 if ( ! isDrawing ) return ;
768+ if (
769+ activeTool === 'select' &&
770+ selectedShapeId &&
771+ manipulationMode . current &&
772+ manipulationMode . current . mode === 'pending-move'
773+ ) {
774+ const dx0 = worldPoint . x - pointerStart . current . x ;
775+ const dy0 = worldPoint . y - pointerStart . current . y ;
776+ const distSq0 = dx0 * dx0 + dy0 * dy0 ;
777+ const threshold = 4 * 4 ; // squared threshold in world coords
778+ if ( distSq0 > threshold ) {
779+ // begin move: set pointerStart so subsequent deltas work from here
780+ manipulationMode . current . mode = 'move' ;
781+ pointerStart . current = worldPoint ;
782+ } else {
783+ return ;
784+ }
785+ }
725786
726787 // MOVE
727788 if ( activeTool === 'select' && selectedShapeId && manipulationMode . current && manipulationMode . current . mode === 'move' ) {
@@ -753,8 +814,9 @@ export const Canvas = () => {
753814 const shapeIndex = shapes . findIndex ( ( s ) => s . id === selectedShapeId ) ;
754815 if ( shapeIndex === - 1 ) return ;
755816
756- const newShapes = [ ...shapes ] ;
757- const sh = JSON . parse ( JSON . stringify ( origShape ) ) ;
817+ const newShapes = [ ...shapes ] ;
818+ const sh = JSON . parse ( JSON . stringify ( origShape ) ) ;
819+ if ( origShape && origShape . type === SHAPE_TYPE . IMAGE ) sh . image = origShape . image ;
758820
759821 // We'll compute a new bounding box keeping the opposite corner fixed depending on dir
760822 let { minX, minY, maxX, maxY } = origBBox ;
@@ -873,14 +935,15 @@ export const Canvas = () => {
873935 setIsPointerDown ( false ) ;
874936 if ( ! isDrawing ) return ;
875937 setIsDrawing ( false ) ;
938+ const prevMode = manipulationMode . current ?. mode ;
876939 newShapeId . current = null ;
877940 manipulationMode . current = null ;
878- if ( manipulationMode . current ?. mode === "erase" ) {
879- const ctx = canvasRef . current . getContext ( "2d" ) ;
880- ctx . globalCompositeOperation = "source-over" ;
881- }
941+ if ( prevMode === "erase" ) {
942+ const ctx = canvasRef . current ? .getContext ( "2d" ) ;
943+ if ( ctx ) ctx . globalCompositeOperation = "source-over" ;
944+ }
882945
883- } ;
946+ } ;
884947
885948 // delete
886949 const handleDeleteSelectedShape = useCallback ( ( ) => {
@@ -1053,6 +1116,7 @@ export const Canvas = () => {
10531116 onToolChange = { handleToolChange }
10541117 onClear = { handleClear }
10551118 onExport = { handleExport }
1119+ onImageUpload = { handleImageUpload }
10561120 />
10571121
10581122 { joined ? (
0 commit comments