Skip to content

Commit 12acc2c

Browse files
feat: added line tool functionality
1 parent 537781a commit 12acc2c

File tree

1 file changed

+146
-115
lines changed

1 file changed

+146
-115
lines changed

client/src/components/Canvas.jsx

Lines changed: 146 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -5,127 +5,158 @@ import { StrokeControl } from "./StrokeControl";
55
import { toast } from "sonner";
66

77
export 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

Comments
 (0)