@@ -5,127 +5,158 @@ import { StrokeControl } from "./StrokeControl";
55import { toast } from "sonner" ;
66
77export const Canvas = ( ) => {
8- const canvasRef = useRef ( null ) ;
9- const [ activeTool , setActiveTool ] = useState ( "pen" ) ;
10- const [ activeColor , setActiveColor ] = useState ( "#000000" ) ;
11- const [ strokeWidth , setStrokeWidth ] = useState ( 3 ) ;
12- const [ isDrawing , setIsDrawing ] = useState ( false ) ;
13- const [ isCanvasFocused , setIsCanvasFocused ] = useState ( false ) ; // 👈 new state
14-
15- useEffect ( ( ) => {
16- const canvas = canvasRef . current ;
17- if ( ! canvas ) return ;
18-
19- canvas . width = window . innerWidth ;
20- canvas . height = window . innerHeight ;
21-
22- const ctx = canvas . getContext ( "2d" ) ;
23- ctx . lineCap = "round" ;
24- ctx . lineJoin = "round" ;
25-
26- const handleResize = ( ) => {
27- canvas . width = window . innerWidth ;
28- canvas . height = window . innerHeight ;
29- } ;
30-
31- window . addEventListener ( "resize" , handleResize ) ;
32- toast . success ( "Canvas ready! Start drawing!" ) ;
33-
34- return ( ) => window . removeEventListener ( "resize" , handleResize ) ;
35- } , [ ] ) ;
36-
37- // 🎹 Keyboard shortcut handling
38- useEffect ( ( ) => {
39- const handleKeyDown = ( e ) => {
40- if ( ! isCanvasFocused ) return ; // only when canvas focused
41-
42- if ( e . key === "p" || e . key === "P" || e . key === "1" ) {
43- handleToolChange ( "pen" ) ;
44- } else if ( e . key === "e" || e . key === "E" || e . key === "2" ) {
45- handleToolChange ( "eraser" ) ;
46- }
47- } ;
48-
49- window . addEventListener ( "keydown" , handleKeyDown ) ;
50- return ( ) => window . removeEventListener ( "keydown" , handleKeyDown ) ;
51- } , [ isCanvasFocused ] ) ;
52-
53- const startDrawing = ( e ) => {
54- if ( activeTool !== "pen" && activeTool !== "eraser" ) return ;
55-
56- setIsDrawing ( true ) ;
57- const ctx = canvasRef . current ?. getContext ( "2d" ) ;
58- if ( ! ctx ) return ;
59-
60- const rect = canvasRef . current . getBoundingClientRect ( ) ;
61- ctx . beginPath ( ) ;
62- ctx . moveTo ( e . clientX - rect . left , e . clientY - rect . top ) ;
8+ const canvasRef = useRef ( null ) ;
9+ const [ activeTool , setActiveTool ] = useState ( "pen" ) ;
10+ const [ activeColor , setActiveColor ] = useState ( "#000000" ) ;
11+ const [ strokeWidth , setStrokeWidth ] = useState ( 3 ) ;
12+ const [ isDrawing , setIsDrawing ] = useState ( false ) ;
13+ const [ isCanvasFocused , setIsCanvasFocused ] = useState ( false ) ;
14+
15+ const startPoint = useRef ( { x : 0 , y : 0 } ) ; //initial position of line
16+ const snapshot = useRef ( null ) ;
17+
18+ useEffect ( ( ) => {
19+ const canvas = canvasRef . current ;
20+ if ( ! canvas ) return ;
21+
22+ canvas . width = window . innerWidth ;
23+ canvas . height = window . innerHeight ;
24+
25+ const ctx = canvas . getContext ( "2d" ) ;
26+ ctx . lineCap = "round" ;
27+ ctx . lineJoin = "round" ;
28+
29+ const handleResize = ( ) => {
30+ canvas . width = window . innerWidth ;
31+ canvas . height = window . innerHeight ;
6332 } ;
6433
65- const draw = ( e ) => {
66- if ( ! isDrawing ) return ;
67- if ( activeTool !== "pen" && activeTool !== "eraser" ) return ;
34+ window . addEventListener ( "resize" , handleResize ) ;
35+ toast . success ( "Canvas ready! Start drawing!" ) ;
6836
69- const ctx = canvasRef . current ?. getContext ( "2d" ) ;
70- if ( ! ctx ) return ;
37+ return ( ) => window . removeEventListener ( "resize" , handleResize ) ;
38+ } , [ ] ) ;
7139
72- const rect = canvasRef . current . getBoundingClientRect ( ) ;
73- ctx . strokeStyle = activeTool === "eraser" ? "#ffffff" : activeColor ;
74- ctx . lineWidth = activeTool === "eraser" ? strokeWidth * 3 : strokeWidth ;
75- ctx . lineTo ( e . clientX - rect . left , e . clientY - rect . top ) ;
76- ctx . stroke ( ) ;
77- } ;
78-
79- const stopDrawing = ( ) => {
80- setIsDrawing ( false ) ;
81- const ctx = canvasRef . current ?. getContext ( "2d" ) ;
82- if ( ctx ) ctx . closePath ( ) ;
83- } ;
84-
85- const handleClear = ( ) => {
86- const ctx = canvasRef . current ?. getContext ( "2d" ) ;
87- if ( ! ctx || ! canvasRef . current ) return ;
88- ctx . clearRect ( 0 , 0 , canvasRef . current . width , canvasRef . current . height ) ;
89- toast . success ( "Canvas cleared!" ) ;
90- } ;
40+ // 🎹 Keyboard shortcuts
41+ useEffect ( ( ) => {
42+ const handleKeyDown = ( e ) => {
43+ if ( ! isCanvasFocused ) return ;
9144
92- const handleToolChange = ( tool ) => {
93- setActiveTool ( tool ) ;
94- toast . info ( `${ tool . charAt ( 0 ) . toUpperCase ( ) + tool . slice ( 1 ) } tool selected` ) ;
45+ if ( e . key === "p" || e . key === "P" || e . key === "1" ) {
46+ handleToolChange ( "pen" ) ;
47+ } else if ( e . key === "e" || e . key === "E" || e . key === "2" ) {
48+ handleToolChange ( "eraser" ) ;
49+ } else if ( e . key === "l" || e . key === "L" || e . key === "3" ) {
50+ handleToolChange ( "line" ) ;
51+ }
9552 } ;
9653
97- return (
98- < div className = "relative w-full h-screen overflow-hidden bg-canvas" >
99- < Toolbar
100- activeTool = { activeTool }
101- onToolChange = { handleToolChange }
102- onClear = { handleClear }
103- />
104- < ColorPicker activeColor = { activeColor } onColorChange = { setActiveColor } />
105- < StrokeControl
106- strokeWidth = { strokeWidth }
107- onStrokeWidthChange = { setStrokeWidth }
108- />
109-
110- < canvas
111- ref = { canvasRef }
112- tabIndex = { 0 } // 👈 allows focus
113- onFocus = { ( ) => setIsCanvasFocused ( true ) } // 👈 activate shortcuts
114- onBlur = { ( ) => setIsCanvasFocused ( false ) } // 👈 deactivate shortcuts
115- onMouseDown = { startDrawing }
116- onMouseMove = { draw }
117- onMouseUp = { stopDrawing }
118- onMouseLeave = { stopDrawing }
119- className = "cursor-crosshair focus:outline-2 focus:outline-primary"
120- />
121-
122- < div className = "fixed bottom-6 left-1/2 -translate-x-1/2 pointer-events-none" >
123- < div className = "bg-toolbar/95 border border-toolbar-border rounded-xl shadow-lg backdrop-blur-sm px-6 py-3" >
124- < p className = "text-sm text-foreground font-medium" >
125- Welcome to CollabCanvas - Select a tool and start drawing!
126- </ p >
127- </ div >
128- </ div >
54+ window . addEventListener ( "keydown" , handleKeyDown ) ;
55+ return ( ) => window . removeEventListener ( "keydown" , handleKeyDown ) ;
56+ } , [ isCanvasFocused ] ) ;
57+
58+ const startDrawing = ( e ) => {
59+ const canvas = canvasRef . current ;
60+ const ctx = canvas ?. getContext ( "2d" ) ;
61+ if ( ! ctx ) return ;
62+
63+ const rect = canvas . getBoundingClientRect ( ) ;
64+ const x = e . clientX - rect . left ;
65+ const y = e . clientY - rect . top ;
66+
67+ startPoint . current = { x, y } ;
68+ setIsDrawing ( true ) ;
69+
70+ if ( activeTool === "pen" || activeTool === "eraser" ) {
71+ ctx . beginPath ( ) ;
72+ ctx . moveTo ( x , y ) ;
73+ }
74+
75+ if ( activeTool === "line" ) {
76+ snapshot . current = ctx . getImageData ( 0 , 0 , canvas . width , canvas . height ) ;
77+ } //snapshot of current canvas state
78+ } ;
79+
80+ const draw = ( e ) => {
81+ if ( ! isDrawing ) return ;
82+ const canvas = canvasRef . current ;
83+ const ctx = canvas ?. getContext ( "2d" ) ;
84+ if ( ! ctx ) return ;
85+
86+ const rect = canvas . getBoundingClientRect ( ) ;
87+ const x = e . clientX - rect . left ;
88+ const y = e . clientY - rect . top ;
89+
90+ if ( activeTool === "pen" || activeTool === "eraser" ) {
91+ ctx . strokeStyle = activeTool === "eraser" ? "#ffffff" : activeColor ;
92+ ctx . lineWidth = activeTool === "eraser" ? strokeWidth * 3 : strokeWidth ;
93+ ctx . lineTo ( x , y ) ;
94+ ctx . stroke ( ) ;
95+ }
96+
97+ else if ( activeTool === "line" ) {
98+ // restore previous canvas snapshot
99+ ctx . putImageData ( snapshot . current , 0 , 0 ) ;
100+ ctx . beginPath ( ) ;
101+ ctx . moveTo ( startPoint . current . x , startPoint . current . y ) ;
102+ ctx . lineTo ( x , y ) ;
103+ ctx . strokeStyle = activeColor ;
104+ ctx . lineWidth = strokeWidth ;
105+ ctx . stroke ( ) ;
106+ ctx . closePath ( ) ;
107+ }
108+ } ;
109+
110+ const stopDrawing = ( ) => {
111+ setIsDrawing ( false ) ;
112+ const ctx = canvasRef . current ?. getContext ( "2d" ) ;
113+ if ( ctx ) ctx . closePath ( ) ;
114+ } ;
115+
116+ const handleClear = ( ) => {
117+ const ctx = canvasRef . current ?. getContext ( "2d" ) ;
118+ if ( ! ctx || ! canvasRef . current ) return ;
119+ ctx . clearRect ( 0 , 0 , canvasRef . current . width , canvasRef . current . height ) ;
120+ toast . success ( "Canvas cleared!" ) ;
121+ } ;
122+
123+ const handleToolChange = ( tool ) => {
124+ setActiveTool ( tool ) ;
125+ toast . info ( `${ tool . charAt ( 0 ) . toUpperCase ( ) + tool . slice ( 1 ) } tool selected` ) ;
126+ } ;
127+
128+ return (
129+ < div className = "relative w-full h-screen overflow-hidden bg-canvas" >
130+ < Toolbar
131+ activeTool = { activeTool }
132+ onToolChange = { handleToolChange }
133+ onClear = { handleClear }
134+ />
135+ < ColorPicker activeColor = { activeColor } onColorChange = { setActiveColor } />
136+ < StrokeControl
137+ strokeWidth = { strokeWidth }
138+ onStrokeWidthChange = { setStrokeWidth }
139+ />
140+
141+ < canvas
142+ ref = { canvasRef }
143+ tabIndex = { 0 }
144+ onFocus = { ( ) => setIsCanvasFocused ( true ) }
145+ onBlur = { ( ) => setIsCanvasFocused ( false ) }
146+ onMouseDown = { startDrawing }
147+ onMouseMove = { draw }
148+ onMouseUp = { stopDrawing }
149+ onMouseLeave = { stopDrawing }
150+ className = "cursor-crosshair focus:outline-2 focus:outline-primary"
151+ />
152+
153+ < div className = "fixed bottom-6 left-1/2 -translate-x-1/2 pointer-events-none" >
154+ < div className = "bg-toolbar/95 border border-toolbar-border rounded-xl shadow-lg backdrop-blur-sm px-6 py-3" >
155+ < p className = "text-sm text-foreground font-medium" >
156+ Welcome to CollabCanvas — Select a tool and start drawing!
157+ </ p >
129158 </ div >
130- ) ;
159+ </ div >
160+ </ div >
161+ ) ;
131162} ;
0 commit comments